747 lines
36 KiB
JavaScript
747 lines
36 KiB
JavaScript
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 readInterpreterMotion(core, interpreter, sourceLine = null) {
|
|
if (typeof core._sim_interpreter_read_motion !== "function" || typeof core._sim_interpreter_motion_size !== "function") return null;
|
|
const size = core._sim_interpreter_motion_size();
|
|
if (!Number.isSafeInteger(size) || size < 216 || size > 4096) throw new Error("wasm_motion_size_invalid");
|
|
const pointer = core._malloc(size);
|
|
if (!pointer) throw new Error("wasm_motion_allocation_failed");
|
|
try {
|
|
const result = call(core, "sim_interpreter_read_motion", "number", ["number", "number"], [interpreter, pointer]);
|
|
if (result !== 0) return null;
|
|
const doubles = core.HEAPF64.subarray(pointer / 8, pointer / 8 + 26);
|
|
const ints = core.HEAP32.subarray(pointer / 4 + 52, pointer / 4 + 54);
|
|
const pose = (offset) => ({
|
|
x: doubles[offset], y: doubles[offset + 1], z: doubles[offset + 2],
|
|
a: doubles[offset + 3], b: doubles[offset + 4], c: doubles[offset + 5],
|
|
u: doubles[offset + 6], v: doubles[offset + 7], w: doubles[offset + 8],
|
|
});
|
|
return {
|
|
source_line: sourceLine,
|
|
start: pose(0),
|
|
end: pose(9),
|
|
center: { x: doubles[18], y: doubles[19], z: doubles[20] },
|
|
normal: { x: doubles[21], y: doubles[22], z: doubles[23] },
|
|
velocity: doubles[24],
|
|
acceleration: doubles[25],
|
|
motion_type: ints[0],
|
|
turn: ints[1],
|
|
source: "wasm_interpreter_canonical_motion",
|
|
};
|
|
} finally {
|
|
core._free(pointer);
|
|
}
|
|
}
|
|
|
|
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*<?([A-Za-z0-9_-]+)>?\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();
|
|
}
|
|
|
|
preview() {
|
|
if (!this.loaded) return { status: "error", code: "program_not_loaded" };
|
|
const source = this.source;
|
|
const path = this.path;
|
|
const segments = [];
|
|
let result = { status: "verified" };
|
|
try {
|
|
for (let steps = 0; steps < MAX_EXPANDED_LINES && this.programCounter < this.lines.length; steps += 1) {
|
|
result = this.executeNext(true);
|
|
if (result.status !== "verified") break;
|
|
if (result.motion) {
|
|
const motion = result.motion;
|
|
const linearNoop = motion.motion_type === 1
|
|
&& ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => motion.start[axis] === motion.end[axis]);
|
|
if (!linearNoop) segments.push(motion);
|
|
}
|
|
}
|
|
if (result.status !== "verified") return { ...result, preview: { status: "error", segments } };
|
|
return { status: "verified", preview: { status: "verified", segments } };
|
|
} catch (error) {
|
|
return { status: "error", code: error.message, preview: { status: "error", segments } };
|
|
} finally {
|
|
this.load(source, path);
|
|
}
|
|
}
|
|
|
|
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;
|
|
const interpreterState = cloneState(this.state);
|
|
return { status: "verified", block, state: interpreterState, interpreter_state: interpreterState, 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);
|
|
const interpreterState = cloneState(this.state);
|
|
return { status: "verified", block, state: interpreterState, interpreter_state: interpreterState, motion: readInterpreterMotion(this.core, this.interpreter, this.state.line), 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, motion: readInterpreterMotion(this.core, this.interpreter, index + 1) };
|
|
}
|
|
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 };
|
|
}
|
|
}
|
|
const unitsCode = this.state.units === 20 ? "G20" : "G21";
|
|
const distanceCode = this.state.distance_mode === 91 ? "G91" : "G90";
|
|
const planeCode = this.state.plane === 18 ? "G18" : this.state.plane === 19 ? "G19" : "G17";
|
|
const motionCode = `G${this.state.motion_mode}`;
|
|
const spindleCode = this.state.spindle_mode === 5 ? "M5" : `M${this.state.spindle_mode}`;
|
|
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),
|
|
active_gcodes: `${unitsCode} ${distanceCode} ${planeCode} ${motionCode} ${spindleCode}`,
|
|
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 };
|