import { fnv1a64 } from "./replay.mjs"; const INTERPRETER_SIZE = 648; const STATE_SIZE = 120; const MAX_PROGRAM_BYTES = 1024 * 1024; const MAX_PROGRAM_LINES = 10000; const MAX_CONTROL_DEPTH = 32; const MAX_CONTROL_STEPS = 100000; const MAX_EXPANDED_LINES = 100000; const DEFAULT_STATE = Object.freeze({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0, feed: 0, speed: 0, line: 0, units: 21, distance_mode: 90, plane: 17, motion_mode: 1, spindle_mode: 5, program_end: 0, status: 0, }); const DEFAULT_MOTION_CONTROL = Object.freeze({ mode: "continuous", path_tolerance: 0, naive_cam_tolerance: 0 }); function call(core, name, returnType, argTypes, args) { if (typeof core.ccall === "function") return core.ccall(name, returnType, argTypes, args); const direct = core[`_${name}`] ?? core[name]; if (typeof direct === "function") return direct(...args); throw new Error(`missing core export: ${name}`); } function cloneState(state) { return { ...state }; } function readInterpreterState(core, pointer) { const bytes = core.HEAPU8; if (!bytes || !core.HEAPF64 || !core.HEAP32) throw new Error("wasm_memory_unavailable"); const offset = pointer >>> 0; if (offset % 8 !== 0 || offset + STATE_SIZE > bytes.byteLength) throw new Error("wasm_state_pointer_invalid"); const doubles = core.HEAPF64.subarray(offset / 8, offset / 8 + 11); const ints = core.HEAP32.subarray(offset / 4 + 22, offset / 4 + 30); return { x: doubles[0], y: doubles[1], z: doubles[2], a: doubles[3], b: doubles[4], c: doubles[5], u: doubles[6], v: doubles[7], w: doubles[8], feed: doubles[9], speed: doubles[10], line: core.HEAPU32[offset / 4 + 22], units: ints[1], distance_mode: ints[2], plane: ints[3], motion_mode: ints[4], spindle_mode: ints[5], program_end: ints[6], status: ints[7], }; } function readCanonical(core, interpreter) { if (typeof core._sim_interpreter_canonical !== "function") return null; const required = call(core, "sim_interpreter_canonical", "number", ["number", "number", "number"], [interpreter, 0, 0]); if (!Number.isSafeInteger(required) || required <= 1 || required > 4096) throw new Error("wasm_canonical_size_invalid"); const pointer = core._malloc(required); if (!pointer) throw new Error("wasm_canonical_allocation_failed"); try { const written = call(core, "sim_interpreter_canonical", "number", ["number", "number", "number"], [interpreter, pointer, required]); return new TextDecoder().decode(core.HEAPU8.subarray(pointer, pointer + written - 1)); } finally { core._free(pointer); } } function nonEmptyLines(source) { const lines = source.split(/\r?\n/); while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); return lines; } function controlDirective(line) { const match = /^\s*O\s*?\s+([A-Za-z]+)(?:\s+(.*?))?\s*$/i.exec(line); if (!match) return null; return { id: match[1], command: match[2].toUpperCase(), expression: match[3] ?? "" }; } function motionControlDirective(line) { const compact = String(line ?? "").replace(/\s+/g, "").toUpperCase(); let match = /^G64(?:P([+-]?(?:\d+(?:\.\d*)?|\.\d+)))?(?:Q([+-]?(?:\d+(?:\.\d*)?|\.\d+)))?$/.exec(compact); if (match) { const path = match[1] === undefined ? 0 : Number(match[1]); const naive = match[2] === undefined ? 0 : Number(match[2]); if (!Number.isFinite(path) || !Number.isFinite(naive) || path < 0 || naive < 0) return { error: "program_motion_control_range" }; return { mode: "continuous", path_tolerance: path, naive_cam_tolerance: naive }; } if (compact === "G61") return { mode: "exact_path", path_tolerance: 0, naive_cam_tolerance: 0 }; if (compact === "G61.1") return { mode: "exact_stop", path_tolerance: 0, naive_cam_tolerance: 0 }; if (/^G(?:61(?:\.\d+)?|64)/.test(compact)) return { error: "program_motion_control_syntax" }; return null; } function parameterValues(core, interpreter, expression) { const values = new Map(); for (const token of expression.match(/#\d+/g) ?? []) { const index = Number(token.slice(1)); if (!Number.isSafeInteger(index) || index < 1 || index > 5399) throw new Error("control_parameter_range"); if (typeof core._sim_interpreter_parameter_get !== "function") throw new Error("control_parameter_api_unavailable"); const pointer = core._malloc(8); if (!pointer) throw new Error("control_parameter_allocation_failed"); try { const result = call(core, "sim_interpreter_parameter_get", "number", ["number", "number", "number"], [interpreter, index, pointer]); if (result !== 0) throw new Error(`control_parameter_${result}`); values.set(index, core.HEAPF64[pointer / 8]); } finally { core._free(pointer); } } return values; } function evaluateControlValue(core, interpreter, source, namedValues = new Map()) { let expression = String(source ?? "").trim(); if (expression.startsWith("[") && expression.endsWith("]")) expression = expression.slice(1, -1); const values = parameterValues(core, interpreter, expression); const tokens = expression.match(/#<[^>]+>|#\d+|(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|\*\*|<=|>=|==|!=|[<>()+\-*/%]|\b(?:MOD|LT|LE|GT|GE|EQ|NE|AND|OR|XOR)\b/gi); if (!tokens || tokens.join("").replace(/\s/g, "") !== expression.replace(/\s/g, "")) throw new Error("control_expression_syntax"); let cursor = 0; const peek = () => tokens[cursor]?.toUpperCase() ?? null; const take = () => tokens[cursor++]; const primary = () => { const token = take(); if (token === undefined) throw new Error("control_expression_operand"); if (token === "(") { const value = logicalOr(); if (take() !== ")") throw new Error("control_expression_parenthesis"); return value; } if (token === "+") return primary(); if (token === "-") return -primary(); if (token.startsWith("#<")) return namedValues.get(token.slice(2, -1).toLowerCase()) ?? 0; if (token.startsWith("#")) return values.get(Number(token.slice(1))) ?? 0; const value = Number(token); if (!Number.isFinite(value)) throw new Error("control_expression_nonfinite"); return value; }; const power = () => { let value = primary(); while (peek() === "**") { take(); value **= power(); } return value; }; const product = () => { let value = power(); while (["*", "/", "%", "MOD"].includes(peek())) { const operator = take(); const right = power(); if ((operator === "/" || operator === "%" || operator === "MOD") && right === 0) throw new Error("control_expression_divide_zero"); value = operator === "*" ? value * right : operator === "/" ? value / right : value % right; } return value; }; const sum = () => { let value = product(); while (["+", "-"].includes(peek())) { const operator = take(); const right = product(); value = operator === "+" ? value + right : value - right; } return value; }; const comparison = () => { let value = sum(); while (["LT", "LE", "GT", "GE", "EQ", "NE", "<", "<=", ">", ">=", "==", "!=", "AND", "OR", "XOR"].includes(peek())) { const operator = take(); const right = sum(); if (["LT", "<"].includes(operator)) value = value < right ? 1 : 0; else if (["LE", "<="].includes(operator)) value = value <= right ? 1 : 0; else if (["GT", ">"].includes(operator)) value = value > right ? 1 : 0; else if (["GE", ">="].includes(operator)) value = value >= right ? 1 : 0; else if (["EQ", "=="].includes(operator)) value = value === right ? 1 : 0; else if (["NE", "!="].includes(operator)) value = value !== right ? 1 : 0; else if (operator === "AND") value = value !== 0 && right !== 0 ? 1 : 0; else if (operator === "OR") value = value !== 0 || right !== 0 ? 1 : 0; else value = (Boolean(value) !== Boolean(right)) ? 1 : 0; } return value; }; const logicalOr = () => comparison(); const result = logicalOr(); if (cursor !== tokens.length || !Number.isFinite(result)) throw new Error("control_expression_syntax"); return result; } function evaluateControlExpression(core, interpreter, source, namedValues = new Map()) { return evaluateControlValue(core, interpreter, source, namedValues) !== 0; } export class ProgramRunner { constructor(core) { this.core = core; this.interpreter = 0; this.stateBuffer = 0; this.blockBuffer = 0; this.blockCapacity = 0; this.source = ""; this.lines = []; this.programCounter = 0; this.control = null; this.controlStack = []; this.controlSteps = 0; this.expandedLines = 0; this.namedParameters = new Map(); this.path = null; this.status = "idle"; this.currentLine = 0; this.lastError = null; this.state = cloneState(DEFAULT_STATE); this.motionControl = { ...DEFAULT_MOTION_CONTROL }; } buildControlMap(lines) { const subStack = []; const ifStack = []; const loopStack = []; const doStack = []; const matching = new Map(); const branches = new Map(); const subroutines = new Map(); const directives = new Map(); const doLoopsByWhile = new Map(); const doLoopsByDo = new Map(); const fail = (index, code) => { const error = new Error(code); error.line = index + 1; throw error; }; lines.forEach((line, index) => { const directive = controlDirective(line); if (!directive) return; directives.set(index, directive); switch (directive.command) { case "SUB": if (subroutines.has(directive.id) || subStack.length >= MAX_CONTROL_DEPTH) fail(index, "program_subroutine_duplicate_or_depth"); subStack.push({ id: directive.id, start: index }); break; case "ENDSUB": { const sub = subStack.pop(); if (!sub || sub.id !== directive.id) fail(index, "program_subroutine_mismatch"); subroutines.set(sub.id, { start: sub.start, end: index }); matching.set(sub.start, index); matching.set(index, sub.start); break; } case "IF": if (ifStack.length >= MAX_CONTROL_DEPTH) fail(index, "program_control_depth"); ifStack.push({ branches: [index], end: null }); break; case "ELSEIF": case "ELSE": { const branch = ifStack[ifStack.length - 1]; if (!branch) fail(index, "program_if_without_if"); branch.branches.push(index); break; } case "ENDIF": { const branch = ifStack.pop(); if (!branch) fail(index, "program_endif_without_if"); branch.end = index; for (let position = 0; position < branch.branches.length; position += 1) { const current = branch.branches[position]; const next = branch.branches[position + 1] ?? index; branches.set(current, { next, end: index }); matching.set(current, next); } matching.set(index, branch.branches[0]); break; } case "DO": if (doStack.length >= MAX_CONTROL_DEPTH) fail(index, "program_control_depth"); doStack.push({ id: directive.id, start: index }); break; case "WHILE": case "ENDDO": { const pending = doStack.at(-1); if ((directive.command === "WHILE" || directive.command === "ENDDO") && pending?.id === directive.id) { doStack.pop(); const loop = { id: directive.id, start: pending.start, end: index, command: directive.command }; doLoopsByDo.set(pending.start, loop); doLoopsByWhile.set(index, loop); break; } if (directive.command === "ENDDO") fail(index, "program_enddo_without_do"); if (loopStack.length >= MAX_CONTROL_DEPTH) fail(index, "program_control_depth"); loopStack.push({ start: index, command: directive.command }); break; } case "ENDWHILE": case "ENDREPEAT": { const loop = loopStack.pop(); if (!loop || (directive.command === "ENDWHILE" && loop.command !== "WHILE") || (directive.command === "ENDREPEAT" && loop.command !== "REPEAT")) fail(index, "program_loop_mismatch"); matching.set(loop.start, index); matching.set(index, loop.start); break; } default: break; } }); if (subStack.length || ifStack.length || loopStack.length || doStack.length) throw Object.assign(new Error("program_control_unclosed"), { line: lines.length }); return { directives, matching, branches, subroutines, doLoopsByDo, doLoopsByWhile }; } get loaded() { return this.source.length > 0 || this.lines.length > 0; } ensureMemory() { if (this.interpreter) return; if (typeof this.core._malloc !== "function" || typeof this.core._free !== "function") throw new Error("wasm_interpreter_memory_unavailable"); const interpreterSize = typeof this.core._sim_interpreter_size === "function" ? this.core._sim_interpreter_size() : INTERPRETER_SIZE; if (!Number.isSafeInteger(interpreterSize) || interpreterSize < INTERPRETER_SIZE || interpreterSize > 4096) throw new Error("wasm_interpreter_size_invalid"); this.interpreter = this.core._malloc(interpreterSize); this.stateBuffer = this.core._malloc(STATE_SIZE); if (!this.interpreter || !this.stateBuffer) throw new Error("wasm_interpreter_allocation_failed"); const result = call(this.core, "sim_interpreter_init", "number", ["number"], [this.interpreter]); if (result !== 0) throw new Error(`interpreter_init_${result}`); } ensureBlockBuffer(byteLength) { if (byteLength <= this.blockCapacity) return; if (this.blockBuffer) this.core._free(this.blockBuffer); this.blockCapacity = Math.max(byteLength + 1, 256); this.blockBuffer = this.core._malloc(this.blockCapacity); if (!this.blockBuffer) throw new Error("wasm_block_allocation_failed"); } load(source, path = null) { if (typeof source !== "string") return { status: "error", code: "program_source_required" }; const bytes = new TextEncoder().encode(source); if (bytes.length === 0 || !source.trim()) return { status: "error", code: "program_empty" }; if (bytes.length > MAX_PROGRAM_BYTES) return { status: "error", code: "program_size_limit", limit: MAX_PROGRAM_BYTES }; const lines = nonEmptyLines(source); if (lines.length > MAX_PROGRAM_LINES) return { status: "error", code: "program_line_limit", limit: MAX_PROGRAM_LINES }; let control; try { this.ensureMemory(); const result = call(this.core, "sim_interpreter_reset", "number", ["number"], [this.interpreter]); if (result !== 0) return { status: "error", code: `interpreter_reset_${result}`, result }; control = this.buildControlMap(lines); } catch (error) { return { status: "error", code: error.message, line: error.line ?? null }; } this.source = source; this.lines = lines; this.programCounter = 0; this.control = control; this.controlStack = []; this.controlSteps = 0; this.expandedLines = 0; this.namedParameters = new Map(); this.path = typeof path === "string" && path.length > 0 ? path : null; this.status = "loaded"; this.currentLine = 0; this.lastError = null; this.state = cloneState(DEFAULT_STATE); this.motionControl = { ...DEFAULT_MOTION_CONTROL }; return this.snapshot(); } start() { if (!this.loaded) return { status: "error", code: "program_not_loaded" }; if (this.status === "completed") return { status: "error", code: "program_completed" }; if (this.status === "error") return { status: "error", code: "program_faulted", error: this.lastError }; this.status = "running"; return this.snapshot(); } pause() { if (this.status !== "running") return { status: "error", code: "program_not_running" }; this.status = "paused"; return this.snapshot(); } resume() { if (this.status !== "paused") return { status: "error", code: "program_not_paused" }; this.status = "running"; return this.snapshot(); } abort() { if (!this.loaded) return { status: "error", code: "program_not_loaded" }; this.status = "aborted"; return this.snapshot(); } restart() { if (!this.loaded) return { status: "error", code: "program_not_loaded" }; const source = this.source; const path = this.path; const result = this.load(source, path); if (result.status === "error") return result; this.status = "loaded"; return this.snapshot(); } seek(line) { if (!Number.isSafeInteger(line) || line < 0 || line > this.lines.length) return { status: "error", code: "program_line_invalid" }; if (line < this.currentLine) { const loaded = this.load(this.source, this.path); if (loaded.status === "error") return loaded; } while (this.currentLine < line) { const result = this.executeNext(true); if (result.status !== "verified") return result; } this.status = "paused"; return this.snapshot(); } step() { if (!this.loaded) return { status: "error", code: "program_not_loaded" }; if (this.status === "aborted" || this.status === "completed" || this.status === "error") return { status: "error", code: `program_${this.status}` }; return this.executeNext(false); } executeBlock(block) { if (typeof block !== "string" || !block.trim()) return { status: "error", code: "mdi_block_required" }; const bytes = new TextEncoder().encode(block); if (bytes.length > 65536) return { status: "error", code: "mdi_block_limit" }; const motionControl = motionControlDirective(block); if (motionControl) { if (motionControl.error) return { status: "error", code: motionControl.error }; this.motionControl = motionControl; this.state.line += 1; return { status: "verified", block, state: cloneState(this.state), runner: this.snapshot() }; } try { this.ensureMemory(); this.ensureBlockBuffer(bytes.length); this.core.HEAPU8.set(bytes, this.blockBuffer); const result = call(this.core, "sim_interpreter_execute", "number", ["number", "number", "number"], [this.interpreter, this.blockBuffer, bytes.length]); if (result !== 0) return { status: "error", code: "mdi_interpreter", result, column: this.errorColumn(), error_code: this.errorCode() }; const stateResult = call(this.core, "sim_interpreter_read_state", "number", ["number", "number"], [this.interpreter, this.stateBuffer]); if (stateResult !== 0) return { status: "error", code: "mdi_state_read", result: stateResult }; this.state = readInterpreterState(this.core, this.stateBuffer); return { status: "verified", block, state: cloneState(this.state), runner: this.snapshot() }; } catch (error) { return { status: "error", code: error.message }; } } tick(cycles) { const results = []; if (this.status !== "running") return results; for (let index = 0; index < cycles && this.status === "running"; index += 1) results.push(this.executeNext(false)); return results; } controlResult(index, text) { return { status: "verified", line: index + 1, text, state: cloneState(this.state), program_status: this.status, control: true }; } controlError(index, code, text) { this.status = "error"; this.lastError = { code, source_line: index + 1, text }; return { status: "error", code: "program_control", detail: code, line: index + 1, text }; } callSubroutine(id, index, text, repeat = 1) { const subroutine = this.control?.subroutines.get(String(id)); if (!subroutine) return this.controlError(index, "program_subroutine_missing", text); if (!Number.isSafeInteger(repeat) || repeat < 1 || repeat > MAX_CONTROL_STEPS) return this.controlError(index, "program_subroutine_repeat_limit", text); const depth = this.controlStack.filter((frame) => frame.type === "call").length; if (depth >= MAX_CONTROL_DEPTH) return this.controlError(index, "program_subroutine_depth", text); this.controlStack.push({ type: "call", returnPc: index + 1, end: subroutine.end, repeat, id: String(id) }); this.programCounter = subroutine.start + 1; this.currentLine = this.programCounter; this.enteringSub = subroutine.start; return this.controlResult(index, text); } executeDirective(index, directive, text) { const next = () => { this.currentLine = this.programCounter; return this.controlResult(index, text); }; const jump = (target) => { this.programCounter = target; this.currentLine = target; return this.controlResult(index, text); }; try { switch (directive.command) { case "SUB": if (this.enteringSub === index) { this.enteringSub = null; return next(); } return jump((this.control.matching.get(index) ?? index) + 1); case "ENDSUB": { const frame = this.controlStack[this.controlStack.length - 1]; if (!frame || frame.type !== "call" || frame.end !== index) return this.controlError(index, "program_endsub_without_call", text); if (frame.repeat > 1) { frame.repeat -= 1; this.programCounter = this.control.subroutines.get(frame.id).start + 1; this.currentLine = this.programCounter; return this.controlResult(index, text); } this.controlStack.pop(); return jump(frame.returnPc); } case "CALL": return this.callSubroutine(directive.id, index, text, 1); case "RETURN": { const frame = this.controlStack.pop(); if (!frame || frame.type !== "call") return this.controlError(index, "program_return_without_call", text); return jump(frame.returnPc); } case "IF": { const branch = this.control.branches.get(index); if (!branch) return this.controlError(index, "program_if_unmatched", text); const taken = evaluateControlExpression(this.core, this.interpreter, directive.expression); this.controlStack.push({ type: "if", end: branch.end, taken }); if (taken) return next(); if (branch.next === branch.end) { this.controlStack.pop(); return jump(branch.end + 1); } return jump(branch.next); } case "ELSEIF": { const frame = this.controlStack[this.controlStack.length - 1]; const branch = this.control.branches.get(index); if (!frame || frame.type !== "if" || !branch) return this.controlError(index, "program_elseif_unmatched", text); if (frame.taken) return jump(frame.end + 1); frame.taken = evaluateControlExpression(this.core, this.interpreter, directive.expression); if (frame.taken) return next(); if (branch.next === branch.end) { this.controlStack.pop(); return jump(branch.end + 1); } return jump(branch.next); } case "ELSE": { const frame = this.controlStack[this.controlStack.length - 1]; if (!frame || frame.type !== "if") return this.controlError(index, "program_else_unmatched", text); if (frame.taken) return jump(frame.end + 1); frame.taken = true; return next(); } case "ENDIF": { const frame = this.controlStack[this.controlStack.length - 1]; if (!frame || frame.type !== "if" || frame.end !== index) return this.controlError(index, "program_endif_unmatched", text); this.controlStack.pop(); return next(); } case "WHILE": case "ENDDO": case "REPEAT": { const doLoop = this.control.doLoopsByWhile.get(index); if (doLoop) { const frame = this.controlStack.at(-1); if (!frame || frame.type !== "do" || frame.end !== index) return this.controlError(index, "program_do_without_frame", text); frame.iterations += 1; this.controlSteps += 1; if (frame.iterations > MAX_CONTROL_STEPS || this.controlSteps > MAX_CONTROL_STEPS) return this.controlError(index, "program_loop_budget", text); if (evaluateControlExpression(this.core, this.interpreter, directive.expression, this.namedParameters)) return jump(frame.start + 1); this.controlStack.pop(); return next(); } const pair = this.control.matching.get(index); if (!Number.isSafeInteger(pair)) return this.controlError(index, "program_loop_unmatched", text); const active = this.controlStack[this.controlStack.length - 1]; if (active?.type === "loop" && active.start === index) { if (!evaluateControlExpression(this.core, this.interpreter, directive.expression)) { this.controlStack.pop(); return jump(pair + 1); } return next(); } if (!evaluateControlExpression(this.core, this.interpreter, directive.expression)) return jump(pair + 1); this.controlStack.push({ type: "loop", start: index, end: pair, iterations: 0 }); return next(); } case "ENDWHILE": case "ENDREPEAT": { const frame = this.controlStack[this.controlStack.length - 1]; if (!frame || frame.type !== "loop" || frame.end !== index) return this.controlError(index, "program_endloop_unmatched", text); frame.iterations += 1; this.controlSteps += 1; if (frame.iterations > MAX_CONTROL_STEPS || this.controlSteps > MAX_CONTROL_STEPS) return this.controlError(index, "program_loop_budget", text); return jump(frame.start); } case "DO": { const loop = this.control.doLoopsByDo.get(index); if (!loop) return this.controlError(index, "program_do_unmatched", text); this.controlStack.push({ type: "do", id: loop.id, start: loop.start, end: loop.end, iterations: 0 }); return next(); } case "BREAK": { const position = this.controlStack.map((frame) => frame.type).lastIndexOf("do") >= 0 ? this.controlStack.map((frame) => frame.type).lastIndexOf("do") : this.controlStack.map((frame) => frame.type).lastIndexOf("loop"); if (position < 0) return this.controlError(index, "program_break_without_loop", text); const frame = this.controlStack[position]; this.controlStack.splice(position, 1); return jump(frame.end + 1); } case "CONTINUE": { const frame = this.controlStack[this.controlStack.length - 1]; if (!frame || !["loop", "do"].includes(frame.type)) return this.controlError(index, "program_continue_without_loop", text); return jump(frame.end); } default: return this.controlError(index, "program_control_unsupported", text); } } catch (error) { return this.controlError(index, error.message, text); } } executeNext(seeking) { while (this.programCounter < this.lines.length) { const index = this.programCounter; const sourceLine = this.lines[index]; this.programCounter += 1; this.currentLine = this.programCounter; if (!sourceLine.trim()) continue; this.controlSteps += 1; this.expandedLines += 1; if (this.controlSteps > MAX_CONTROL_STEPS || this.expandedLines > MAX_EXPANDED_LINES) return this.controlError(index, "program_execution_budget", sourceLine); const directive = this.control?.directives.get(index); if (directive) return this.executeDirective(index, directive, sourceLine); if (/^\s*M99(?:\s|$)/i.test(sourceLine)) { const frame = this.controlStack.pop(); if (!frame || frame.type !== "call") return this.controlError(index, "program_return_without_call", sourceLine); this.programCounter = frame.returnPc; this.currentLine = this.programCounter; return this.controlResult(index, sourceLine); } const fanucCall = /^\s*M98\s+P(\d+)(?:\s+L(\d+))?/i.exec(sourceLine); if (fanucCall) return this.callSubroutine(fanucCall[1], index, sourceLine, Number(fanucCall[2] ?? 1)); const motionControl = motionControlDirective(sourceLine); if (motionControl) { if (motionControl.error) return this.controlError(index, motionControl.error, sourceLine); this.motionControl = motionControl; this.state.line += 1; return { status: "verified", line: index + 1, text: sourceLine, state: cloneState(this.state), program_status: this.status, motion_control: { ...this.motionControl } }; } const bytes = new TextEncoder().encode(sourceLine); this.ensureBlockBuffer(bytes.length); this.core.HEAPU8.set(bytes, this.blockBuffer); const result = call(this.core, "sim_interpreter_execute", "number", ["number", "number", "number"], [this.interpreter, this.blockBuffer, bytes.length]); if (result !== 0) { this.status = "error"; this.lastError = { result, source_line: index + 1, text: sourceLine }; return { status: "error", code: "program_interpreter", result, line: index + 1, text: sourceLine, column: this.errorColumn(), error_code: this.errorCode() }; } const stateResult = call(this.core, "sim_interpreter_read_state", "number", ["number", "number"], [this.interpreter, this.stateBuffer]); if (stateResult !== 0) { this.status = "error"; this.lastError = { result: stateResult, source_line: index + 1, text: sourceLine }; return { status: "error", code: "program_state_read", result: stateResult, line: index + 1 }; } this.state = readInterpreterState(this.core, this.stateBuffer); const namedAssignment = /^\s*#<([^>]+)>\s*=\s*(.+?)\s*$/i.exec(sourceLine); if (namedAssignment) { const value = evaluateControlValue(this.core, this.interpreter, namedAssignment[2], this.namedParameters); if (!Number.isFinite(value)) return this.controlError(index, "control_expression_nonfinite", sourceLine); this.namedParameters.set(namedAssignment[1].toLowerCase(), value); } if (this.state.program_end) this.status = "completed"; else if (this.programCounter >= this.lines.length && !seeking) this.status = "completed"; return { status: "verified", line: index + 1, text: sourceLine, state: cloneState(this.state), program_status: this.status }; } this.status = "completed"; return { status: "verified", line: this.currentLine, text: null, state: cloneState(this.state), program_status: this.status }; } snapshot() { let canonical = null; let interpreter_state_hash = null; let block_hash = null; let modal_hash = null; let error_column = 0; let error_code = 0; if (this.interpreter) { try { canonical = readCanonical(this.core, this.interpreter); const stateHash = call(this.core, "sim_interpreter_state_hash", "number", ["number"], [this.interpreter]); const blockHash = call(this.core, "sim_interpreter_block_hash", "number", ["number"], [this.interpreter]); if (typeof this.core._sim_interpreter_modal_hash === "function") { const modalHash = call(this.core, "sim_interpreter_modal_hash", "number", ["number"], [this.interpreter]); modal_hash = `fnv1a64:${BigInt.asUintN(64, BigInt(modalHash >>> 0)).toString(16).padStart(16, "0")}`; } error_column = this.errorColumn(); error_code = this.errorCode(); interpreter_state_hash = `fnv1a64:${BigInt.asUintN(64, BigInt(stateHash >>> 0)).toString(16).padStart(16, "0")}`; block_hash = `fnv1a64:${BigInt.asUintN(64, BigInt(blockHash >>> 0)).toString(16).padStart(16, "0")}`; } catch (error) { this.lastError = this.lastError ?? { code: error.message }; } } return { status: "verified", program_status: this.status, path: this.path, line: this.currentLine, line_count: this.lines.length, source_hash: this.loaded ? fnv1a64(this.source) : null, state: cloneState(this.state), canonical, interpreter_state_hash, block_hash, modal_hash, motion_control: { ...this.motionControl }, motion_control_hash: fnv1a64(this.motionControl), error_column, error_code, error: this.lastError, }; } errorColumn() { if (!this.interpreter || typeof this.core._sim_interpreter_error_column !== "function") return 0; return call(this.core, "sim_interpreter_error_column", "number", ["number"], [this.interpreter]); } errorCode() { if (!this.interpreter || typeof this.core._sim_interpreter_error_code !== "function") return 0; return call(this.core, "sim_interpreter_error_code", "number", ["number"], [this.interpreter]); } exportState() { return { ...this.snapshot(), source: this.source, motion_control: { ...this.motionControl } }; } restoreState(value) { if (!value || typeof value.source !== "string") { this.status = "idle"; return { status: "verified", program_status: this.status }; } const loaded = this.load(value.source, value.path); if (loaded.status === "error") return loaded; const target = Number.isSafeInteger(value.line) ? value.line : 0; const restored = this.seek(Math.max(0, Math.min(target, this.lines.length))); if (restored.status === "error") return restored; this.status = value.program_status === "running" ? "paused" : (value.program_status ?? this.status); if (value.motion_control && typeof value.motion_control.mode === "string") this.motionControl = { ...DEFAULT_MOTION_CONTROL, ...value.motion_control }; this.lastError = value.error ?? null; return this.snapshot(); } destroy() { if (this.blockBuffer && typeof this.core._free === "function") this.core._free(this.blockBuffer); if (this.stateBuffer && typeof this.core._free === "function") this.core._free(this.stateBuffer); if (this.interpreter && typeof this.core._free === "function") this.core._free(this.interpreter); this.blockBuffer = 0; this.stateBuffer = 0; this.interpreter = 0; } } export { DEFAULT_STATE };