// ── Procedure block definitions ──────────────────────────────────────────── // // Blocks that define and call procedures: // WCProcedure — procedure definition (lives in Procedures tab) // WCProcCall — procedure call block (lives in Algorithm tab) // // Each extends WCBlock (from wcblock.js). // // Load order dependencies: // wcblock.js → WCBlock base class // helpers.js → findDataSource, resolveSourceName, escapeString, toCName, commentPrefix // dom.js → $id // draw.js → deleteLink /** @type {Object} Maps library IDs to icon image paths for procedure call blocks. */ const LIB_ICONS = { stdio: "lib-icons/c.svg", stdlib: "lib-icons/c.svg", string: "lib-icons/c.svg", math: "lib-icons/c.svg", }; // ── WCProcedure — procedure definition ───────────────────────────────────── /** Procedure definition block with inputs/outputs/description/return-style. Lives in the Procedures tab. */ class WCProcedure extends WCBlock { delete() { if (this.dataset.readonly) { showError("Cannot delete a library procedure."); return; } super.delete(); } connectedCallback() { super.connectedCallback(); this._el.naturalName = this._require(".proc-natural-name"); this._el.techName = this._require(".proc-tech-name"); this._el.inputs = this._require(".proc-inputs"); this._el.outputs = this._require(".proc-outputs"); this._el.description = this.querySelector(".proc-description"); this._el.descBody = this.querySelector(".proc-desc-body"); this._require(".proc-add-input").addEventListener("click", () => { if (this.dataset.readonly) return; this._addParamRow(this._el.inputs, "in"); }); this._require(".proc-add-output").addEventListener("click", () => { if (this.dataset.readonly) return; this._addParamRow(this._el.outputs, "out"); }); this._el.naturalName.addEventListener("input", () => { if (!this.dataset.readonly) { this._el.techName.textContent = toCName(this._el.naturalName.value) || "proc"; } }); this._el.returnStyle = this.querySelector(".proc-return-style"); if (this._el.returnStyle) { this.dataset.returnStyle = this._el.returnStyle.value; this._el.returnStyle.addEventListener("change", () => { if (this.dataset.readonly) return; this.dataset.returnStyle = this._el.returnStyle.value; }); } } _addParamRow(container, dir) { const row = document.createElement("div"); row.className = "proc-param-row"; const readonly = this.dataset.readonly; row.innerHTML = `` + `` + (readonly ? "" : ``); if (!readonly) { row.querySelector(".proc-param-del").addEventListener("click", () => { row.remove(); }); } container.appendChild(row); } getLibrary() { const pane = this.closest(".lib-pane"); return pane ? pane.dataset.libName : "unknown"; } getQualifiedName() { return this.getLibrary() + "/" + (this._el.techName.textContent || "unnamed"); } getInputParams() { return this._getParams(this._el.inputs); } getOutputParams() { return this._getParams(this._el.outputs); } _getParams(container) { return Array.from(container.querySelectorAll(".proc-param-row")).map(row => ({ name: row.querySelector(".proc-param-name").value || "arg", type: row.querySelector(".proc-param-type").value || "scalar" })); } saveData() { return { library: this.getLibrary(), naturalName: this._el.naturalName.value, name: this._el.techName.textContent, inputs: this.getInputParams(), outputs: this.getOutputParams(), returnStyle: this._el.returnStyle ? this._el.returnStyle.value : (this.dataset.returnStyle || "pointer"), readonly: !!this.dataset.readonly, formatString: this.dataset.formatString !== undefined, description: this._el.descBody ? this._el.descBody.textContent : "", descOpen: this._el.description ? this._el.description.open : false }; } restoreData(d) { this._el.naturalName.value = d.naturalName || ""; this._el.techName.textContent = d.name || "proc"; if (d.readonly) this.dataset.readonly = "true"; if (d.formatString) this.dataset.formatString = "true"; if (d.returnStyle) { this.dataset.returnStyle = d.returnStyle; if (this._el.returnStyle) this._el.returnStyle.value = d.returnStyle; } if (this._el.descBody && d.description) this._el.descBody.textContent = d.description; if (this._el.description && d.descOpen) this._el.description.open = true; for (const p of (d.inputs || [])) { this._addParamRow(this._el.inputs, "in"); const rows = this._el.inputs.querySelectorAll(".proc-param-row"); const last = rows[rows.length - 1]; last.querySelector(".proc-param-name").value = p.name || "arg"; last.querySelector(".proc-param-type").value = p.type || "scalar"; } for (const p of (d.outputs || [])) { this._addParamRow(this._el.outputs, "out"); const rows = this._el.outputs.querySelectorAll(".proc-param-row"); const last = rows[rows.length - 1]; last.querySelector(".proc-param-name").value = p.name || "arg"; last.querySelector(".proc-param-type").value = p.type || "scalar"; } } generateOwnCode() { return ""; } } // ── WCProcCall — procedure call block ────────────────────────────────────── /** @type {RegExp} Matches C printf-format specifiers, capturing the type character. */ const _FMT_SPEC_RE = /%(?:[+\- #0]*\d*(?:\.\d+)?)([diufFeEgGxXoscpaAn%])/g; /** * Maps a printf format specifier character to a C data type. * @param {string} spec Single format character (e.g. "d", "f", "s") * @returns {string} "int", "double", or "string" */ function _specDataType(spec) { if ("di".includes(spec)) return "int"; if ("u".includes(spec)) return "int"; if ("fFeEgGaA".includes(spec)) return "double"; if ("s".includes(spec)) return "string"; return "int"; } /** Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. */ class WCProcCall extends WCBlock { connectedCallback() { super.connectedCallback(); this._selectedProc = ""; this._fmtProc = null; this._fmtInput = null; this._fmtObserver = null; this._el.icon = this._require(".proccall-icon"); this._el.natName = this._require(".proccall-nat-name"); this._el.techName = this._require(".proccall-tech-name"); this._el.changeBtn = this._require(".proccall-change-btn"); this._el.nameInput = this._require(".proccall-name"); this._el.nameRow = this._require(".proccall-name-row"); this._el.inputs = this._require(".proccall-inputs"); this._el.outputs = this._require(".proccall-outputs"); this._el.errHook = this.querySelector(".proccall-error-hook"); this._el.fmtargs = this._require(".proccall-fmtargs"); this._el.fmtargsList = this._require(".proccall-fmtargs-list"); this._el.addFmtArgBtn = this._require(".proccall-add-fmtarg"); this._el.addFmtArgBtn.addEventListener("click", () => this._addFormatArg()); const openBrowser = () => { const current = this._selectedProc; showProcedureBrowser(current, (qname) => { if (qname) this.setProcedure(qname); }); }; this._el.natName.addEventListener("click", openBrowser); this._el.changeBtn.addEventListener("click", openBrowser); this._initTimer = setTimeout(() => this._rebuildParams(), 0); } setProcedure(qname) { this._selectedProc = qname || ""; const libName = qname ? qname.split("/")[0] : ""; if (this._el.natName) { if (qname) { let natName = qname.split("/")[1] || qname; let techName = qname.split("/")[1] || qname; for (const proc of document.querySelectorAll("wc-procedure")) { if (proc.getQualifiedName() === qname) { const nat = proc.querySelector(".proc-natural-name"); if (nat && nat.value) natName = nat.value; techName = (proc.querySelector(".proc-tech-name") || {}).textContent || techName; break; } } this._el.natName.textContent = natName; this._el.natName.title = qname; this._el.techName.textContent = techName; this._el.techName.title = qname; this._el.icon.src = LIB_ICONS[libName] || ""; this._el.icon.alt = libName; } else { this._el.natName.textContent = "(select procedure)"; this._el.natName.title = ""; this._el.techName.textContent = ""; this._el.techName.title = ""; this._el.icon.src = ""; this._el.icon.alt = ""; } } this._rebuildParams(); } _rebuildParams() { const qname = this._selectedProc; for (const container of [this._el.inputs, this._el.outputs]) { for (const hook of container.querySelectorAll(".hook")) { const paths = [...document.querySelectorAll("path.flow, path.flow-data")]; for (const path of paths) { if (path.fromHook === hook || path.toHook === hook) { deleteLink(path); } } } } this._el.inputs.innerHTML = ""; this._el.outputs.innerHTML = ""; let returnStyle = "pointer"; if (qname) { for (const proc of document.querySelectorAll("wc-procedure")) { if (proc.getQualifiedName() === qname) { returnStyle = proc.dataset.returnStyle || "pointer"; this._cachedReturnStyle = returnStyle; for (const p of proc.getInputParams()) { this._createArgField(this._el.inputs, p, "in"); } if (returnStyle !== "error") { for (const p of proc.getOutputParams()) { this._createArgField(this._el.outputs, p, "out"); } } break; } } } this._setupFormatStringArgs(); const isError = returnStyle === "error"; const hasReturnValue = returnStyle === "error" || returnStyle === "value"; if (this._el.errHook) { this._el.errHook.classList.toggle("visible", hasReturnValue); } if (this._el.nameRow) { this._el.nameRow.style.display = isError ? "none" : ""; } } _setupFormatStringArgs() { const qname = this._selectedProc; let fmtProc = null; if (qname) { for (const proc of document.querySelectorAll("wc-procedure")) { if (proc.getQualifiedName() === qname && proc.dataset.formatString !== undefined) { fmtProc = proc; break; } } } this._fmtProc = fmtProc; if (this._fmtObserver) { this._fmtObserver.disconnect(); this._fmtObserver = null; } if (!fmtProc) { this._el.fmtargs.style.display = "none"; this._el.addFmtArgBtn.style.display = "none"; this._el.fmtargsList.innerHTML = ""; return; } this._el.fmtargs.style.display = ""; this._fmtInput = this._el.inputs.querySelector(".proccall-arg-input"); if (this._fmtInput) { this._fmtInput.addEventListener("input", () => this._rebuildFormatArgs()); } this._fmtObserver = new MutationObserver(() => { if (this._isFmtDynamic()) this._ensureFmtUnconnected(); }); this._fmtObserver.observe(this._el.fmtargsList, { attributes: true, attributeFilter: ["data-connected"], subtree: true }); this._rebuildFormatArgs(); } _isFmtDynamic() { if (!this._fmtInput) return false; const hook = this._fmtInput.parentElement.querySelector(".hook"); return hook ? findDataSource(hook) !== null : false; } _rebuildFormatArgs() { this._el.fmtargsList.innerHTML = ""; const dynamic = this._isFmtDynamic(); this._el.addFmtArgBtn.style.display = dynamic ? "" : "none"; if (dynamic) { this._ensureFmtUnconnected(); return; } const fmt = this._fmtInput ? this._fmtInput.value : ""; const specs = this._parseFormatSpecifiers(fmt); for (let i = 0; i < specs.length; i++) { this._createFormatArg(specs[i], i); } } _parseFormatSpecifiers(fmt) { const specs = []; let m; _FMT_SPEC_RE.lastIndex = 0; while ((m = _FMT_SPEC_RE.exec(fmt)) !== null) { if (m[1] === "%") continue; specs.push({ full: m[0], spec: m[1] }); } return specs; } _createFormatArg(spec, index) { const row = document.createElement("div"); row.className = "proccall-arg-row"; const hook = document.createElement("button"); hook.className = "hook"; hook.dataset.direction = "in"; hook.dataset.linkType = "data"; hook.dataset.type = _specDataType(spec.spec); hook.dataset.exit = "left"; hook.title = `expects ${_specDataType(spec.spec)}`; const label = document.createElement("span"); label.className = "proccall-arg-label"; label.textContent = `arg${index + 1}:`; const input = document.createElement("input"); input.className = "proccall-arg-input"; input.dataset.dir = "in"; input.dataset.paramName = `fmtArg${index}`; input.placeholder = spec.full; row.appendChild(hook); row.appendChild(label); row.appendChild(input); this._el.fmtargsList.appendChild(row); this._setupHook(hook); this._wrapArgInput(input); } _addFormatArg() { const idx = this._el.fmtargsList.querySelectorAll(".proccall-arg-row").length; const row = document.createElement("div"); row.className = "proccall-arg-row"; const hook = document.createElement("button"); hook.className = "hook"; hook.dataset.direction = "in"; hook.dataset.linkType = "data"; hook.dataset.type = "scalar"; hook.dataset.exit = "left"; hook.title = "expects scalar"; const label = document.createElement("span"); label.className = "proccall-arg-label"; label.textContent = `arg${idx + 1}:`; const input = document.createElement("input"); input.className = "proccall-arg-input"; input.dataset.dir = "in"; input.dataset.paramName = `fmtArg${idx}`; input.placeholder = `arg${idx + 1}`; row.appendChild(hook); row.appendChild(label); row.appendChild(input); this._el.fmtargsList.appendChild(row); this._setupHook(hook); this._wrapArgInput(input); this._ensureFmtUnconnected(); } _ensureFmtUnconnected() { if (!this._isFmtDynamic()) return; const hooks = this._el.fmtargsList.querySelectorAll(".hook"); const hasFree = [...hooks].some(h => !h.dataset.connected); if (!hasFree) this._addFormatArg(); } _wrapArgInput(input) { if (input.style.display === "none") return; const display = document.createElement("span"); display.className = "field-display"; const update = () => { display.textContent = input.value || input.placeholder || ""; }; update(); input.addEventListener("change", update); input.addEventListener("input", update); input.parentNode.insertBefore(display, input); input.style.display = "none"; } _createArgField(container, param, dir) { const row = document.createElement("div"); row.className = "proccall-arg-row"; const label = document.createElement("span"); label.className = "proccall-arg-label"; label.textContent = param.name + ":"; const input = document.createElement("input"); input.className = "proccall-arg-input"; input.placeholder = param.name; input.dataset.dir = dir; input.dataset.paramName = param.name; input.dataset.paramType = param.type; const hook = document.createElement("button"); hook.className = "hook"; hook.dataset.direction = dir === "in" ? "in" : "out"; hook.dataset.linkType = "data"; hook.dataset.type = param.type; hook.dataset.exit = dir === "in" ? "left" : "right"; hook.title = (dir === "in" ? "expects " : "") + param.type; if (dir === "in") { row.appendChild(hook); row.appendChild(label); row.appendChild(input); } else { row.appendChild(label); row.appendChild(input); row.appendChild(hook); } container.appendChild(row); this._setupHook(hook); this._wrapArgInput(input); } _sourceName(block) { const name = resolveSourceName(block); if (!name) return ""; return block.dataType === "string" ? name + ".data" : name; } generateOwnCode() { const qname = this._selectedProc; if (!qname) return ""; const parts = qname.split("/"); const procName = parts[1] || qname; let returnStyle = this._cachedReturnStyle || "pointer"; const inputs = []; const outputs = []; for (const row of this._el.inputs.querySelectorAll(".proccall-arg-row")) { const hook = row.querySelector(".hook"); const inp = row.querySelector(".proccall-arg-input"); const source = hook ? findDataSource(hook) : null; if (source) { inputs.push(this._sourceName(source)); } else { let val = inp.value || inp.placeholder; if (inp.dataset.paramType === "string") val = `"${escapeString(val)}"`; inputs.push(val); } } for (const row of this._el.outputs.querySelectorAll(".proccall-arg-row")) { const inp = row.querySelector(".proccall-arg-input"); outputs.push("&" + (inp.value || inp.placeholder)); } if (this._fmtProc) { for (const row of this._el.fmtargsList.querySelectorAll(".proccall-arg-row")) { const hook = row.querySelector(".hook"); const inp = row.querySelector(".proccall-arg-input"); const source = hook ? findDataSource(hook) : null; if (source) { inputs.push(this._sourceName(source)); } else { let val = inp.value || inp.placeholder; if (hook.dataset.type === "string") val = `"${escapeString(val)}"`; inputs.push(val); } } } const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") && Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook); // For error-capable calls, the call is emitted inside generateFlowCode // (as part of the if-condition) to avoid double execution. if (returnStyle === "error" && hasErrPath) return ""; if (returnStyle === "value" && outputs.length === 1 && hasErrPath) { const outVar = outputs[0].replace(/^&/, ""); return ` ${outVar} = ${procName}(${inputs.join(", ")});\n`; } if (returnStyle === "error") { return ` ${procName}(${inputs.join(", ")});\n`; } if (returnStyle === "value" && outputs.length === 1) { return ` ${procName}(${inputs.join(", ")});\n`; } const args = [...inputs, ...outputs].join(", "); return ` ${procName}(${args});\n`; } generateFlowCode() { const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") && Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook); if (!hasErrPath) return WCBlock.prototype.generateFlowCode.call(this); const qname = this._selectedProc; if (!qname) return ""; const parts = qname.split("/"); const procName = parts[1] || qname; let returnStyle = this._cachedReturnStyle || "pointer"; const inputs = []; const outputs = []; for (const row of this._el.inputs.querySelectorAll(".proccall-arg-row")) { const hook = row.querySelector(".hook"); const inp = row.querySelector(".proccall-arg-input"); const source = hook ? findDataSource(hook) : null; if (source) { inputs.push(this._sourceName(source)); } else { inputs.push(inp.value || inp.placeholder); } } for (const row of this._el.outputs.querySelectorAll(".proccall-arg-row")) { const inp = row.querySelector(".proccall-arg-input"); outputs.push("&" + (inp.value || inp.placeholder)); } if (this._fmtProc) { for (const row of this._el.fmtargsList.querySelectorAll(".proccall-arg-row")) { const hook = row.querySelector(".hook"); const inp = row.querySelector(".proccall-arg-input"); const source = hook ? findDataSource(hook) : null; if (source) { inputs.push(this._sourceName(source)); } else { inputs.push(inp.value || inp.placeholder); } } } const args = inputs.join(", "); const errTarget = Array.from(this.wcNextFlow) .filter(p => p.fromHook === this._el.errHook) .map(p => p.toElement)[0]; const okTarget = Array.from(this.wcNextFlow) .filter(p => p.fromHook !== this._el.errHook) .map(p => p.toElement)[0]; let code = ""; if (returnStyle === "error") { code += ` if (${procName}(${args}) != 0) goto ${errTarget.id};\n`; } else if (returnStyle === "value" && outputs.length === 1) { const outVar = outputs[0].replace(/^&/, ""); code += ` if (${outVar} != 0) goto ${errTarget.id};\n`; } if (okTarget) code += ` goto ${okTarget.id};\n`; return code; } saveData() { const args = {}; for (const inp of this.querySelectorAll(".proccall-arg-input")) { args[inp.dataset.dir + ":" + inp.dataset.paramName] = inp.value; } return { selected: this._selectedProc, name: this._el.nameInput.value, args }; } restoreData(d) { if (this._initTimer) { clearTimeout(this._initTimer); this._initTimer = null; } this.setProcedure(d.selected || ""); this._el.nameInput.value = d.name || ""; if (d.args) { for (const inp of this.querySelectorAll(".proccall-arg-input")) { const key = inp.dataset.dir + ":" + inp.dataset.paramName; if (d.args[key] !== undefined) inp.value = d.args[key]; } } if (this._fmtProc && d.args) { this._rebuildFormatArgs(); for (const inp of this.querySelectorAll(".proccall-arg-input")) { const key = inp.dataset.dir + ":" + inp.dataset.paramName; if (key.startsWith("in:fmtArg") && d.args[key] !== undefined) { inp.value = d.args[key]; } } } } }