Files
avcf/blocks-proc.js
T
adrien 439cc6e7ba feat: loop support via goto labels on each block
- Every block now emits a C label (its id) before its code
- generateNextCode emits goto label; for back-edges (visited targets)
- _emitBranch delegates to generateCode (not generateOwnCode), so labels
  are emitted consistently in both main and branch traversals
- generateCallWithError threads a shared visited set across all three
  emitBranch calls, preventing duplicate label emission for shared blocks
- emitBranch skips roots already visited by previous roots in the same
  branch (prevents spurious gotos from shared-block traversal)
2026-06-23 01:16:14 +02:00

619 lines
21 KiB
JavaScript

// ── 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<string,string>} 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 =
`<input class="proc-param-name" placeholder="name" value=""${readonly ? " readonly" : ""}>` +
`<input class="proc-param-type" placeholder="type" value="scalar"${readonly ? " readonly" : ""}>` +
(readonly ? "" : `<button class="proc-param-del" title="remove">&times;</button>`);
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";
}
/**
* Adapter: wraps CodeGen.branchDescendants with the browser's instanceof check.
* @param {HTMLElement[]} roots
* @returns {Set<HTMLElement>}
*/
function _branchDescendants(roots) {
return CodeGen.branchDescendants(roots, function (b) {
return b instanceof WCDecision;
});
}
/**
* Adapter: wraps CodeGen.emitBranch with the browser's instanceof check.
* @param {HTMLElement[]} roots
* @param {Set<HTMLElement>} stop
* @param {Set<HTMLElement>} visited
* @returns {string}
*/
function _emitBranch(roots, stop, visited) {
return CodeGen.emitBranch(roots, stop, visited, function (b) {
return b instanceof WCDecision;
});
}
/** 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;
}
generateCode(visited = new Set()) {
if (visited.has(this)) return "\n goto " + this.id + ";\n";
visited.add(this);
const own = "\n" + this.id + ":\n" + this.generateOwnCode();
if (this._el.errHook && this._el.errHook.classList.contains("visible") &&
Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook)) {
return own;
}
return own + this.generateNextCode(visited);
}
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 errPaths = this._el.errHook && this._el.errHook.classList.contains("visible")
? Array.from(this.wcNextFlow).filter(p => p.fromHook === this._el.errHook)
: [];
const hasErrPath = errPaths.length > 0;
if (returnStyle === "error") {
const call = `${procName}(${inputs.join(", ")})`;
if (hasErrPath) {
return this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
}
return `\n${call};\n`;
}
if (returnStyle === "value" && outputs.length === 1) {
const outVar = outputs[0].replace(/^&/, "");
if (hasErrPath) {
return this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
}
return `\n${procName}(${inputs.join(", ")});\n`;
}
const args = [...inputs, ...outputs].join(", ");
return `\n${procName}(${args});\n`;
}
_generateCallWithError(ifHeader, errPaths) {
const errTargets = errPaths.map(p => p.toElement);
const okTargets = Array.from(this.wcNextFlow)
.filter(p => p.fromHook !== this._el.errHook)
.map(p => p.toElement);
return CodeGen.generateCallWithError(ifHeader, errTargets, okTargets, function (b) {
return b instanceof WCDecision;
});
}
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];
}
}
}
}
}