- Replace all C code generation with LLVM IR (flat DFA model with bb_ prefix labels to avoid SSA value name collisions) - Remove c.min.js, add llvm.min.js highlight.js language definition - Update shell.nix: gcc -> clang, remove sdl3/mesa deps - Download LLVM Language Reference to docs/llvm/LangRef.html - Rename #c-code to #ir-code in HTML, JS, and CSS - Replace _generateIncludes with _generateLLVMDecls for extern function declarations using LLVM_DECL_MAP - Add string pool (CodeGen.allocString, stringGlobal, llvmStringArg) for LLVM IR global string constants with getelementptr references
307 lines
13 KiB
JavaScript
307 lines
13 KiB
JavaScript
// ── WCProgram — root container + block registration ──────────────────────────
|
|
//
|
|
// WCProgram is the root custom element that owns the flowchart workspace.
|
|
// Unlike other blocks, WCProgram extends HTMLElement directly — not WCBlock —
|
|
// because the program container itself is not draggable or linkable.
|
|
//
|
|
// This file also contains the block registry and registration calls.
|
|
// Must be loaded AFTER all block class definitions exist in the page.
|
|
//
|
|
// Load order dependencies:
|
|
// All block class files must be loaded first:
|
|
// wcblock.js → blocks-algo.js → blocks-data.js → blocks-proc.js
|
|
// helpers.js → toCName
|
|
// dom.js → $id
|
|
//
|
|
// This file is loaded last among the block group, before persist.js and app.js.
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// WCProgram — root container
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/** Root container custom element; owns the flowchart workspace, block creation, and code generation. */
|
|
class WCProgram extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.counter = 0;
|
|
}
|
|
|
|
build() {
|
|
const startTmpl = $id("wc-start");
|
|
const endTmpl = $id("wc-end");
|
|
if (!startTmpl || !endTmpl) {
|
|
showError("Missing wc-start or wc-end template elements");
|
|
return;
|
|
}
|
|
const startFrag = document.importNode(startTmpl.content, true);
|
|
const endFrag = document.importNode(endTmpl.content, true);
|
|
const startEl = startFrag.firstElementChild;
|
|
const endEl = endFrag.firstElementChild;
|
|
if (!startEl || !endEl) {
|
|
showError("wc-start or wc-end template has no content");
|
|
return;
|
|
}
|
|
startEl.id = "main-start";
|
|
endEl.id = "main-end";
|
|
this.appendChild(startFrag);
|
|
this.appendChild(endFrag);
|
|
|
|
const pw = this.parentElement ? this.parentElement.offsetWidth : 600;
|
|
const startW = startEl.offsetWidth || 80;
|
|
startEl.style.left = Math.max(20, (pw - startW) / 2) + "px";
|
|
startEl.style.top = "20px";
|
|
const endW = endEl.offsetWidth || 200;
|
|
endEl.style.left = Math.max(20, (pw - endW) / 2) + "px";
|
|
endEl.style.top = (startEl.offsetTop + startEl.offsetHeight + 60) + "px";
|
|
}
|
|
|
|
makeNode(tag) {
|
|
const info = BLOCK_REGISTRY[tag];
|
|
if (!info) return null;
|
|
const tmpl = document.querySelector(info.template);
|
|
if (!tmpl) { showError("Missing template: " + info.template); return null; }
|
|
const content = tmpl.content;
|
|
if (!content || !content.firstElementChild) { showError("Empty template: " + info.template); return null; }
|
|
this.counter++;
|
|
const frag = document.importNode(content, true);
|
|
const el = frag.firstElementChild;
|
|
el.id = info.prefix + this.counter;
|
|
return el;
|
|
}
|
|
|
|
newNode(tag, parent) {
|
|
const info = BLOCK_REGISTRY[tag];
|
|
if (!info) return null;
|
|
const el = this.makeNode(tag);
|
|
if (!el) return null;
|
|
(parent || (info.getParent ? info.getParent() : this)).appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
newDecisionNode() { return this.newNode("wc-decision"); }
|
|
newExpressionNode() { return this.newNode("wc-expression"); }
|
|
newVarRefNode() { return this.newNode("wc-varref"); }
|
|
newProcCallNode() { return this.newNode("wc-proccall"); }
|
|
|
|
newUnitNode() {
|
|
const el = this.newNode("wc-unit", $id("data-area"));
|
|
if (el) refreshAll();
|
|
return el;
|
|
}
|
|
newQuantityNode() {
|
|
const el = this.newNode("wc-quantity", $id("data-area"));
|
|
if (el) { refreshQuantityOptions(); refreshDefaultUnitOptions(); }
|
|
return el;
|
|
}
|
|
newBooleanNode() {
|
|
const el = this.newNode("wc-boolean", $id("variable-area"));
|
|
if (el) refreshVarRefOptions();
|
|
return el;
|
|
}
|
|
newScalarNode() {
|
|
const el = this.newNode("wc-scalar", $id("variable-area"));
|
|
if (el) { refreshUnitOptions(); refreshVarRefOptions(); }
|
|
return el;
|
|
}
|
|
newStringNode() {
|
|
const el = this.newNode("wc-string", $id("variable-area"));
|
|
if (el) refreshVarRefOptions();
|
|
return el;
|
|
}
|
|
newArrayNode() {
|
|
const el = this.newNode("wc-array", $id("variable-area"));
|
|
if (el) refreshVarRefOptions();
|
|
return el;
|
|
}
|
|
newVectorNode() {
|
|
const el = this.newNode("wc-vector", $id("variable-area"));
|
|
if (el) { refreshUnitOptions(); refreshVarRefOptions(); }
|
|
return el;
|
|
}
|
|
newProcedureNode(libName) {
|
|
const libPane = libName
|
|
? document.querySelector(`.lib-pane[data-lib-name="${libName}"]`)
|
|
: document.querySelector(".lib-pane");
|
|
if (!libPane) return null;
|
|
const el = this.newNode("wc-procedure", libPane.querySelector(".procedures-area"));
|
|
return el;
|
|
}
|
|
|
|
_gatherUsedTypes() {
|
|
const usedQuantities = new Set(), usedUnits = new Set();
|
|
const varArea = $id("variable-area");
|
|
if (!varArea) return { usedQuantities, usedUnits };
|
|
for (const d of varArea.querySelectorAll("wc-scalar, wc-vector")) {
|
|
const t = d.querySelector(".var-type");
|
|
if (t) { const v = t.value; if (v) usedQuantities.add(v); }
|
|
const u = d.querySelector(".var-unit");
|
|
if (u) { const v = u.value; if (v) usedUnits.add(v); }
|
|
}
|
|
return { usedQuantities, usedUnits };
|
|
}
|
|
|
|
_generateTypeDefs(usedQuantities) {
|
|
const dataArea = $id("data-area");
|
|
if (!dataArea) return "";
|
|
return Array.from(
|
|
dataArea.querySelectorAll("wc-quantity")
|
|
).filter(q => {
|
|
const el = q.querySelector(".quantity-name");
|
|
return el && usedQuantities.has(el.value || "unnamed");
|
|
})
|
|
.map(q => {
|
|
const el = q.querySelector(".quantity-name");
|
|
const name = el ? (el.value || "unnamed") : "unnamed";
|
|
return `typedef double ${name};`;
|
|
}).join("\n");
|
|
}
|
|
|
|
_generateUnitDefs(usedUnits) {
|
|
const dataArea = $id("data-area");
|
|
if (!dataArea) return "";
|
|
return Array.from(
|
|
dataArea.querySelectorAll("wc-unit")
|
|
).filter(u => {
|
|
const el = u.querySelector(".unit-suffix");
|
|
return el && usedUnits.has(el.value);
|
|
})
|
|
.map(u => {
|
|
const typeEl = u.querySelector(".unit-type");
|
|
const suffixEl = u.querySelector(".unit-suffix");
|
|
const fullnameEl = u.querySelector(".unit-fullname");
|
|
const siEl = u.querySelector(".unit-si");
|
|
const type = typeEl ? typeEl.value || "unknown" : "unknown";
|
|
const suffix = suffixEl ? suffixEl.value || "" : "";
|
|
const fullname = fullnameEl ? fullnameEl.value || "" : "";
|
|
const si = siEl ? siEl.value || "1" : "1";
|
|
return `// unit: ${fullname} (${suffix}) = 1 ${type} * ${si}`;
|
|
}).join("\n");
|
|
}
|
|
|
|
_generateVariableDeclarations() {
|
|
const varArea = $id("variable-area");
|
|
if (!varArea) return { boolDefs: "", scalarDefs: "", vectorDefs: "", stringDefs: "", arrayDefs: "" };
|
|
const boolDefs = Array.from(varArea.querySelectorAll("wc-boolean")).map(d => d.generateCode()).join("\n");
|
|
const scalarDefs = Array.from(varArea.querySelectorAll("wc-scalar")).map(d => d.generateCode()).join("\n");
|
|
const vectorDefs = Array.from(varArea.querySelectorAll("wc-vector")).map(d => d.generateCode()).join("\n");
|
|
const stringDefs = Array.from(varArea.querySelectorAll("wc-string")).map(d => d.generateCode()).join("\n");
|
|
const arrayDefs = Array.from(varArea.querySelectorAll("wc-array")).map(d => d.generateCode()).join("\n");
|
|
return { boolDefs, scalarDefs, vectorDefs, stringDefs, arrayDefs };
|
|
}
|
|
|
|
_generateLLVMDecls(proccalls) {
|
|
var seen = new Set();
|
|
var lines = [];
|
|
for (var pi = 0; pi < proccalls.length; pi++) {
|
|
var qname = proccalls[pi]._selectedProc;
|
|
if (!qname || seen.has(qname)) continue;
|
|
seen.add(qname);
|
|
var decl = LLVM_DECL_MAP[qname];
|
|
if (decl) {
|
|
lines.push(decl);
|
|
} else {
|
|
var fn = qname.split("/")[1] || qname;
|
|
lines.push("declare i32 @" + fn + "(...)");
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
generateCode() {
|
|
const outputEl = $id("ir-code");
|
|
if (!outputEl) { showError("Missing #ir-code output element"); return; }
|
|
|
|
const { usedQuantities, usedUnits } = this._gatherUsedTypes();
|
|
|
|
const typeDefs = this._generateTypeDefs(usedQuantities);
|
|
const unitDefs = this._generateUnitDefs(usedUnits);
|
|
const { boolDefs, scalarDefs, vectorDefs, stringDefs, arrayDefs } = this._generateVariableDeclarations();
|
|
|
|
CodeGen.resetStringPool();
|
|
|
|
const firstChild = this.firstElementChild;
|
|
const body = firstChild && typeof CodeGen.emitFlat === "function"
|
|
? CodeGen.emitFlat(firstChild)
|
|
: "; no flowchart body\n";
|
|
|
|
// Collect external function declarations from used libraries
|
|
const proccalls = this.querySelectorAll("wc-proccall");
|
|
const decls = this._generateLLVMDecls(proccalls);
|
|
|
|
// Collect string globals
|
|
const strPool = CodeGen.getStringPool();
|
|
const globals = strPool.map(function (_, i) { return CodeGen.stringGlobal(i); }).join("\n");
|
|
|
|
// Variable declarations -> alloca instructions in the prologue
|
|
const dataSection = (boolDefs || scalarDefs || vectorDefs || stringDefs || arrayDefs)
|
|
? [boolDefs, scalarDefs, vectorDefs, stringDefs, arrayDefs]
|
|
.filter(Boolean).map(function (s) { return s + "\n"; }).join("")
|
|
: "";
|
|
|
|
const raw = CodeGen.generateIRFile(firstChild, decls, globals, dataSection);
|
|
outputEl.textContent = raw;
|
|
if (typeof hljs !== "undefined") hljs.highlightElement(outputEl);
|
|
outputEl.parentElement.dataset.language = "llvm";
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Maps library/function names to their LLVM IR `declare` signature.
|
|
* Key is the proc qname, value is the declare line.
|
|
* Functions not listed get a generic `declare i32 @name(...)`.
|
|
*/
|
|
const LLVM_DECL_MAP = {
|
|
"stdio/printf": "declare i32 @printf(i8*, ...)",
|
|
"stdlib/exit": "declare void @exit(i32)",
|
|
};
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Block registry
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/** @type {Object<string,{cls:typeof WCBlock, template:string, prefix:string, getParent:(()=>HTMLElement)|null}>} */
|
|
const BLOCK_REGISTRY = {};
|
|
/** @type {Object<string,string>} Maps toolbar action names to block tag names. */
|
|
const BLOCK_ACTIONS = {};
|
|
|
|
/**
|
|
* Registers a custom element class and adds it to the block registry and action map.
|
|
* @param {string} tag Custom element tag (e.g. "wc-decision")
|
|
* @param {typeof WCBlock} cls Class constructor
|
|
* @param {string} prefix ID prefix for auto-generated block IDs
|
|
* @param {string|null} creatable Action name for toolbar buttons, or null if not toolbar-creatable
|
|
* @param {(()=>HTMLElement)|null} getParent Function returning the parent container, or null for algorithm pane
|
|
*/
|
|
function registerBlock(tag, cls, prefix, creatable, getParent) {
|
|
customElements.define(tag, cls);
|
|
BLOCK_REGISTRY[tag] = { cls, template: "#" + tag, prefix: prefix || tag.replace("wc-", ""), getParent };
|
|
if (creatable) BLOCK_ACTIONS[creatable] = tag;
|
|
}
|
|
|
|
registerBlock("wc-start", WCStart, "start", null, null);
|
|
registerBlock("wc-end", WCEnd, "end", "end", null);
|
|
registerBlock("wc-program", WCProgram, "program", null, null);
|
|
registerBlock("wc-decision", WCDecision, "decision", "decision", null);
|
|
registerBlock("wc-boolean", WCBoolean, "bool", "boolean", () => $id("variable-area"));
|
|
registerBlock("wc-scalar", WCScalar, "scalar", "scalar", () => $id("variable-area"));
|
|
registerBlock("wc-string", WCString, "str", "string", () => $id("variable-area"));
|
|
registerBlock("wc-vector", WCVector, "vec", "vector", () => $id("variable-area"));
|
|
registerBlock("wc-array", WCArray, "arr", "array", () => $id("variable-area"));
|
|
registerBlock("wc-varref", WCVarRef, "varref", "varref", null);
|
|
registerBlock("wc-expression", WCExpression, "expr", "expression", null);
|
|
registerBlock("wc-quantity", WCQuantity, "quantity", "quantity", () => $id("data-area"));
|
|
registerBlock("wc-unit", WCUnit, "unit", "unit", () => $id("data-area"));
|
|
registerBlock("wc-procedure", WCProcedure, "proc", null, null);
|
|
registerBlock("wc-proccall", WCProcCall, "proccall", "proccall", null);
|
|
registerBlock("wc-assign", WCAssign, "assign", "assign", null);
|
|
|
|
/** @type {MutationObserver} Watches SVG connections for changes and refreshes assign-block value visibility. */
|
|
const _connObserver = new MutationObserver(() => {
|
|
for (const el of document.querySelectorAll("wc-assign")) el._updateValueVisibility();
|
|
});
|
|
const _connSvg = document.getElementById("connections");
|
|
if (_connSvg) _connObserver.observe(_connSvg, { childList: true, subtree: true });
|