diff --git a/wasm-port/AGENTS.md b/wasm-port/AGENTS.md index a731254..e124eb3 100644 --- a/wasm-port/AGENTS.md +++ b/wasm-port/AGENTS.md @@ -70,7 +70,12 @@ expanded into a separate implementation of G-code behavior. - GUI. 7. Frontend code must be implemented with web technology, not migrated from native GUI code. -8. All build scripts must be incremental. Native and WASM builds must reuse +8. Frontend, browser tests, and Node WASM tests should call generated WASM + modules through `runtime/sdk/src/index.js`. SDK code is a host-boundary + layer only: it may load modules, manage strings, write Emscripten files, + and call exported C ABI functions, but it must not implement G-code, + canonical motion, tool, parameter, kinematics, or planner semantics. +9. All build scripts must be incremental. Native and WASM builds must reuse object files, dependency files, and command fingerprints, and must not unconditionally recompile unchanged source files. diff --git a/wasm-port/README.md b/wasm-port/README.md index 69f3976..781237f 100644 --- a/wasm-port/README.md +++ b/wasm-port/README.md @@ -9,6 +9,9 @@ Rules: 3. Port-specific code, build scripts, adapters, tests, and documentation live here. 4. LinuxCNC source is consumed by reference, copy, generated snapshot, or scripted extraction. 5. The browser/WASM product is managed as a separate program. +6. Browser UI and tests should call WASM modules through `runtime/sdk/src/index.js`. + The SDK is a thin module-loading, string-allocation, Emscripten-FS, and C + ABI wrapper; it must not implement CNC semantics. Suggested layout: @@ -119,6 +122,9 @@ Directory intent: - `vendor/linuxcnc/`: read-only copied LinuxCNC source selected for the port. - `runtime/core/`: the standalone simulation engine built around vendored LinuxCNC code. - `runtime/sdk/`: JavaScript/TypeScript API for calling the WASM engine. + Stable imports should come from `runtime/sdk/src/index.js`. SDK code may + adapt host/runtime edges, but G-code, tool, parameter, kinematics, and + planner behavior must remain in vendored LinuxCNC source. - `runtime/ui/`: HTML + JavaScript CNC simulation frontend. - `runtime/opfs/`: OPFS-backed persistence layer. - `tests/fixtures/`: stable simulation inputs shared by native and browser tests. diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index fb1c62a..245e481 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -36,6 +36,12 @@ The current browser smoke validation command is: wasm-port/tests/browser/verify_ini_panel_browser.sh ``` +The current browser interpreter smoke validation command is: + +```bash +wasm-port/tests/browser/verify_interp_browser.sh +``` + The current aggregate host/WASM/browser smoke command is: ```bash @@ -53,7 +59,8 @@ The native validation script runs these checks in order: Confirms every manifest file is present in `vendor/linuxcnc/`, no extra vendored file exists, and every vendored file is byte-identical to upstream. 3. `tools/verify_no_standalone_cnc_semantics.sh` - Confirms standalone code has not reintroduced `Interp::convert_g()`. + Confirms standalone code has not introduced project-owned `Interp::...` + member definitions outside the documented Python/remap runtime-edge stubs. 4. `tools/verify_native_linuxcnc_fixture_baseline.sh` Runs a side-by-side fixture baseline through upstream `../linuxcnc/bin/rs274` and compares normalized canonical events for @@ -67,17 +74,18 @@ The native validation script runs these checks in order: The WASM INI smoke script builds `runtime/ui/ini-panel/linuxcnc_ini.js` and `linuxcnc_ini.wasm` from vendored LinuxCNC `inifile.cc`, then loads that -module through `runtime/sdk/src/linuxcnc-ini.js` in Node and verifies INI +module through `runtime/sdk/src/index.js` in Node and verifies INI queries against a file written to the Emscripten filesystem. The WASM interpreter-core smoke script builds `build/wasm/core/linuxcnc_interp.js` and `linuxcnc_interp.wasm` from the same vendored LinuxCNC interpreter source set used by the native minimal -interpreter harness. It loads the module in Node, runs the first WASM -interpreter fixture group through `Interp::execute()`, and compares emitted -canonical events plus required LinuxCNC `_setup` state readback with the -matching files in `tests/fixtures/canon/`. It also writes selected G-code -fixtures into the Emscripten filesystem and runs them through LinuxCNC +interpreter harness. It loads the module in Node through +`runtime/sdk/src/index.js`, runs the first WASM interpreter fixture +group through `Interp::execute()`, and compares emitted canonical events plus +required LinuxCNC `_setup` state readback with the matching files in +`tests/fixtures/canon/`. It also writes selected G-code fixtures into the +Emscripten filesystem through the SDK and runs them through LinuxCNC `Interp::open()`, `Interp::read()`, and `Interp::execute()` to validate the file execution path. @@ -90,15 +98,24 @@ targets. It also validates the host-side session snapshot JSON envelope and round-trip store plus pure-text machine file and G-code stores without defining CNC machine-state or file-format semantics. -The browser smoke script serves `wasm-port/` over localhost and runs Chromium -headless against a test page that imports the JS SDK, loads the INI WASM -module, queries vendored LinuxCNC INI parsing through the SDK, and performs an -OPFS text-file, generic session snapshot, machine file, and G-code text -round trip. +The browser INI/OPFS smoke script serves `wasm-port/` over localhost and runs +Chromium headless against a test page that imports the JS SDK, loads the INI +WASM module, queries vendored LinuxCNC INI parsing through the SDK, and +performs an OPFS text-file, generic session snapshot, machine file, and G-code +text round trip. + +The browser interpreter smoke script serves `wasm-port/` over localhost and +runs Chromium headless against a test page that loads the interpreter-core +WASM module through `runtime/sdk/src/index.js`, writes no CNC +behavior in JavaScript, and verifies existing canonical fixtures through the +exported C ABI backed by vendored LinuxCNC `Interp::execute()` and +`Interp::open()`/`read()`/`execute()` paths, including the INI-aware +named-parameter file path and negative interpreter fixtures with expected +error text plus absent canonical motion output. The aggregate host smoke script builds the INI and interpreter-core WASM artifacts once, then runs the Node WASM smokes, the Node OPFS mock smoke, and -the Chromium browser smoke. +the Chromium browser smokes. ## Source Coverage @@ -147,9 +164,10 @@ The validation fails if: | Harness | Purpose | | --- | --- | | `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. | -| `tests/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, and match the native canonical event plus required state readback fixtures. | +| `tests/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, loaded through the interpreter JS SDK, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, and match the native canonical event plus required state readback fixtures. | | `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, session snapshot store, machine file store, and G-code text store used by the browser INI panel without moving file persistence into the WASM core. | | `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, OPFS text-file round trip, generic session snapshot round trip, machine file text round trip, and G-code text round trip in a real browser runtime. | +| `tests/browser/verify_interp_browser.sh` | Validates the interpreter-core WASM module loads through the interpreter JS SDK in a real browser runtime and runs selected positive and negative canonical fixtures through vendored LinuxCNC `Interp::execute()` plus `Interp::open()`/`read()`/`execute()` via the exported C ABI. | | `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, OPFS, and browser smoke validation with shared WASM builds. | ## Fixture Coverage @@ -206,12 +224,127 @@ plus the `g1_zero_feed`, `arc_radius_mismatch`, `arc_zero_radius`, `cutter_comp_plane_change`, `g53_incremental`, `namedparam_readonly`, `numbered_param_readonly`, `tool_not_found`, and `tool_length_offset_not_found` negative fixtures. The WASM interpreter file -path additionally covers the same canonical-event fixture group except -`namedparam_semantics` and `position_params`, plus `file_open_reset`, -`percent_file_finish`, and `oword_subroutine`. OPFS validation is limited to -the JavaScript host-boundary adapter plus the INI browser smoke harness. Full -browser coverage, full SDK coverage, and full machine-session validation remain -future work. +path additionally covers the same canonical-event fixture group, plus +`namedparam_semantics` through the INI-aware file execution ABI, +`file_open_reset`, `percent_file_finish`, and `oword_subroutine`. +`position_params` uses a dedicated file-path expectation under +`tests/fixtures/canon_file/` because LinuxCNC file execution advances the +post-execute position parameters differently than the line-by-line MDI smoke. +OPFS validation is limited to the JavaScript host-boundary adapter plus the INI +browser smoke harness. Full browser coverage, full SDK coverage, and full +machine-session validation remain future work. + +## WASM/Browser Fixture Matrix + +Node WASM `Interp::execute()` coverage currently includes: + +- `minimal_linear` +- `arc_semantics` +- `length_units` +- `modal_incremental` +- `plane_selection` +- `coordinate_offsets` +- `g53_machine_coordinates` +- `feed_control_modes` +- `position_params` +- `probe_semantics` +- `spindle_orient` +- `comment_logging` +- `numbered_params` +- `tool_semantics` +- `tool_table_setup` +- `tool_reload` +- `canned_cycles` +- `cutter_comp_motion` +- `threading_sync` +- `nurbs_g5_semantics` +- `nurbs_g6_semantics` +- `state_tag_motion` +- `canon_runtime_edges` +- `program_end_modal_reset` +- `namedparam_semantics` through the INI-aware program ABI + +Node WASM file-path coverage currently includes: + +- `minimal_linear` +- `arc_semantics` +- `length_units` +- `modal_incremental` +- `plane_selection` +- `coordinate_offsets` +- `g53_machine_coordinates` +- `feed_control_modes` +- `probe_semantics` +- `spindle_orient` +- `comment_logging` +- `numbered_params` +- `tool_semantics` +- `tool_table_setup` +- `tool_reload` +- `canned_cycles` +- `cutter_comp_motion` +- `threading_sync` +- `nurbs_g5_semantics` +- `nurbs_g6_semantics` +- `state_tag_motion` +- `canon_runtime_edges` +- `program_end_modal_reset` +- `file_open_reset` +- `percent_file_finish` +- `oword_subroutine` +- `position_params` through the dedicated `canon_file/` expectation +- `namedparam_semantics` through the INI-aware file ABI + +Browser interpreter `Interp::execute()` coverage currently includes: + +- `minimal_linear` +- `arc_semantics` +- `length_units` +- `modal_incremental` +- `plane_selection` +- `coordinate_offsets` +- `g53_machine_coordinates` +- `feed_control_modes` +- `canned_cycles` +- `numbered_params` +- `comment_logging` +- `tool_semantics` +- `tool_table_setup` +- `probe_semantics` +- `spindle_orient` +- `cutter_comp_motion` +- `threading_sync` +- `nurbs_g5_semantics` +- `nurbs_g6_semantics` +- `state_tag_motion` +- `canon_runtime_edges` +- `tool_reload` +- `program_end_modal_reset` + +Browser interpreter negative coverage currently includes every fixture under +`tests/fixtures/gcode_errors/`: + +- `g1_zero_feed` +- `arc_radius_mismatch` +- `arc_zero_radius` +- `g53_incremental` +- `cutter_comp_plane_change` +- `namedparam_readonly` +- `numbered_param_readonly` +- `tool_length_offset_not_found` +- `tool_not_found` + +Browser interpreter file-path coverage currently includes: + +- `position_params` through the dedicated `canon_file/` expectation +- `file_open_reset` +- `percent_file_finish` +- `oword_subroutine` +- `namedparam_semantics` through the INI-aware file ABI + +All current positive G-code fixtures have browser interpreter smoke coverage +through either `Interp::execute()`, the file-path ABI, or the INI-aware +file-path ABI. The current fixture expectations validate standalone behavior against both the vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for diff --git a/wasm-port/docs/drift-report.md b/wasm-port/docs/drift-report.md index cfa22fd..62ee433 100644 --- a/wasm-port/docs/drift-report.md +++ b/wasm-port/docs/drift-report.md @@ -49,7 +49,8 @@ semantic rewrites: - Do not edit `../linuxcnc/`. - Do not patch vendored files without adding a patch under `patches/` and documenting the reason. -- Do not add standalone `Interp::convert_g()`. +- Do not add standalone `Interp::...` member definitions outside the + documented Python/remap runtime-edge stubs. - Do not add `.c` or `.cc` manifest files without a source compile probe. - Do not sync from a different upstream commit without updating `tools/upstream-baseline.txt` and `docs/scope-and-baseline.md`. @@ -58,8 +59,10 @@ semantic rewrites: - No browser/full-core WASM parity tests yet. The INI parser now has Node and Chromium smoke harnesses against vendored LinuxCNC `inifile.cc`. -- JS SDK validation is currently limited to the INI WASM wrapper around - vendored LinuxCNC `inifile.cc`. +- JS SDK validation now covers the INI WASM wrapper around vendored LinuxCNC + `inifile.cc` and the interpreter-core SDK wrapper around the existing + exported C ABI. The interpreter SDK only manages strings, Emscripten file + writes, and calls into vendored LinuxCNC execution paths. - OPFS validation covers a Node mock of the file-service adapter, the host-side path model, generic session snapshot storage, pure-text machine file and G-code storage, and a Chromium localhost round trip for those diff --git a/wasm-port/docs/porting-steps-standalone.md b/wasm-port/docs/porting-steps-standalone.md index 1261aad..4ddf733 100644 --- a/wasm-port/docs/porting-steps-standalone.md +++ b/wasm-port/docs/porting-steps-standalone.md @@ -888,14 +888,20 @@ Keep these documents under `wasm-port/docs/`: ## Immediate Next Step -Continue expanding the standalone interpreter core from the verified minimal -traverse path: +Continue from the current verified extracted-core baseline without adding +project-authored CNC semantics: -1. keep `convert_g()` execution on vendored LinuxCNC interpreter conversion - code and reject any new standalone `Interp::convert_g()` implementation. -2. identify and shim the native runtime symbols blocking direct compilation of - `interp_convert.cc`, `interp_execute.cc`, and related interpreter files. -3. keep fixture coverage as regression protection while deleting temporary - hand-written semantics. -4. keep all source changes inside `wasm-port/` and leave `../linuxcnc/` - read-only. +1. Keep all `Interp::...` interpreter member behavior on vendored LinuxCNC + source. Standalone code may only provide documented runtime-edge stubs such + as the current Python/remap boundary. +2. Expand WASM interpreter coverage by routing more existing native fixture + paths through `runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp`, using + the same vendored interpreter source set as the native harness. +3. Promote remaining standalone-only fixture expectations to native LinuxCNC + baselines where possible, especially adapter-heavy paths such as INI/HAL + named parameters, tool-change host state, and richer machine session state. +4. Move browser-facing work through SDK and OPFS adapters only after the core + behavior is validated against native LinuxCNC or vendored-source harnesses. +5. Before adding any CNC feature, update `tools/source-manifest.txt`, extract + the LinuxCNC source file, add a source probe or harness, and document the + reuse boundary in `docs/source-reuse-map.md`. diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index 5fe2b80..b6c708a 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -25,13 +25,15 @@ Current validation is intentionally mechanical: - `tests/native/verify_native_probes.sh` checks that every manifest `.c` and `.cc` file has a matching source probe in `build/native/source-probes.tsv`. - `tools/verify_no_standalone_cnc_semantics.sh` rejects standalone - `Interp::convert_g()` definitions outside `vendor/linuxcnc/`. + `Interp::...` member definitions outside `vendor/linuxcnc/`, except for the + documented Python/remap runtime-edge stubs in + `runtime/core/linuxcnc_wrap/linuxcnc_interp_edge_stubs.cpp`. ## Reuse Matrix | Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation | | --- | --- | --- | --- | --- | -| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/linuxcnc-ini.js` only wraps the exported WASM C ABI | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh`, `tests/browser/verify_ini_panel_browser.sh`, `tests/host/verify_host_smokes.sh` | +| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/index.js` exports the INI SDK wrapper around the generated WASM C ABI | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh`, `tests/browser/verify_ini_panel_browser.sh`, `tests/host/verify_host_smokes.sh` | | RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes | | Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses | | Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe` | @@ -68,9 +70,11 @@ Current validation is intentionally mechanical: userspace genser flows are not yet established. - Cutter compensation positive motion and negative interpreter paths are fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`. -- Browser/WASM C ABI and JS SDK layers are not yet built for the full - interpreter/planner core. The INI parser has a minimal JS SDK wrapper and - Node/browser WASM smoke harnesses. +- Browser/WASM C ABI and JS SDK layers are now present for the INI parser and + the current interpreter-core smoke scope. The interpreter SDK is a thin + allocation, filesystem, and C ABI wrapper over vendored LinuxCNC execution + paths; it does not define G-code semantics. Full planner/session SDK + coverage remains future work. - OPFS persistence is connected to the INI panel through the host-side `runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now defines paths for INI, tool table, parameter file, G-code program, diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp index 14bf9ae..5406cda 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp @@ -183,9 +183,13 @@ char *lcinterp_run_program(const char *program_text) } EMSCRIPTEN_KEEPALIVE -char *lcinterp_run_file(const char *path) +char *lcinterp_run_file_with_ini(const char *path, const char *ini_path) { - unsetenv("INI_FILE_NAME"); + if (ini_path && ini_path[0] != '\0') { + setenv("INI_FILE_NAME", ini_path, 1); + } else { + unsetenv("INI_FILE_NAME"); + } Interp interp; standalone::reset_canon_events(); @@ -248,6 +252,12 @@ char *lcinterp_run_file(const char *path) return copy_result(output.str()); } +EMSCRIPTEN_KEEPALIVE +char *lcinterp_run_file(const char *path) +{ + return lcinterp_run_file_with_ini(path, nullptr); +} + EMSCRIPTEN_KEEPALIVE void lcinterp_free_string(char *value) { diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md new file mode 100644 index 0000000..b0fac3d --- /dev/null +++ b/wasm-port/runtime/sdk/README.md @@ -0,0 +1,41 @@ +# LinuxCNC WASM SDK + +This SDK exposes browser/Node JavaScript wrappers for standalone WASM modules +built from vendored LinuxCNC source. + +## Boundary + +The SDK is a host-boundary layer only. It may: + +- load generated Emscripten modules; +- allocate and free C strings; +- write text files into the Emscripten filesystem; +- call exported C ABI functions; +- return LinuxCNC-produced text output to JavaScript callers. + +The SDK must not implement G-code interpretation, canonical motion behavior, +tool semantics, parameter semantics, kinematics, or planner behavior. Those +behaviors must continue to come from vendored LinuxCNC source through the C ABI. + +## Entrypoints + +Use `src/index.js` for stable imports: + +```js +import { + createLinuxCncIniSdk, + createLinuxCncInterpSdk, +} from "./src/index.js"; +``` + +`createLinuxCncIniSdk()` wraps the INI parser module built from vendored +LinuxCNC `inifile.cc`. + +`createLinuxCncInterpSdk()` wraps the interpreter-core module built from +vendored LinuxCNC RS274NGC sources and exposes: + +- `runProgram(programText)` +- `runProgramWithIni(programText, iniPath)` +- `runFile(path)` +- `runFileWithIni(path, iniPath)` +- `writeTextFile(path, text)` diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js new file mode 100644 index 0000000..1959a46 --- /dev/null +++ b/wasm-port/runtime/sdk/src/index.js @@ -0,0 +1,2 @@ +export { createLinuxCncIniSdk } from "./linuxcnc-ini.js"; +export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js"; diff --git a/wasm-port/runtime/sdk/src/linuxcnc-interp.js b/wasm-port/runtime/sdk/src/linuxcnc-interp.js new file mode 100644 index 0000000..b691690 --- /dev/null +++ b/wasm-port/runtime/sdk/src/linuxcnc-interp.js @@ -0,0 +1,73 @@ +import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js"; + +function allocCString(mod, value) { + const bytes = mod.lengthBytesUTF8(value) + 1; + const ptr = mod._malloc(bytes); + mod.stringToUTF8(value, ptr, bytes); + return ptr; +} + +function ensureParentPath(mod, path) { + const parts = path.split("/").filter(Boolean); + let current = ""; + for (const part of parts.slice(0, -1)) { + current += `/${part}`; + try { + mod.FS.mkdir(current); + } catch { + // Directory already exists. + } + } +} + +function copyResultString(mod, resultPtr, functionName) { + if (!resultPtr) { + throw new Error(`${functionName} returned null`); + } + return mod.UTF8ToString(resultPtr); +} + +function callStringResult(mod, functionName, ...values) { + const ptrs = values.map((value) => allocCString(mod, value)); + let resultPtr = 0; + try { + resultPtr = mod[`_${functionName}`](...ptrs); + return copyResultString(mod, resultPtr, functionName); + } finally { + if (resultPtr) { + mod._lcinterp_free_string(resultPtr); + } + for (const ptr of ptrs) { + mod._free(ptr); + } + } +} + +export async function createLinuxCncInterpSdk(moduleOptions = {}) { + const mod = await createLinuxCncInterpModule(moduleOptions); + + return { + module: mod, + + writeTextFile(path, text) { + ensureParentPath(mod, path); + mod.FS.writeFile(path, text, { encoding: "utf8" }); + }, + + runProgram(programText) { + return callStringResult(mod, "lcinterp_run_program", programText); + }, + + runProgramWithIni(programText, iniPath) { + return callStringResult(mod, "lcinterp_run_program_with_ini", programText, iniPath); + }, + + runFile(path) { + return callStringResult(mod, "lcinterp_run_file", path); + }, + + runFileWithIni(path, iniPath) { + return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath); + }, + }; +} diff --git a/wasm-port/runtime/ui/ini-panel/app.js b/wasm-port/runtime/ui/ini-panel/app.js index 3675b8e..695654b 100644 --- a/wasm-port/runtime/ui/ini-panel/app.js +++ b/wasm-port/runtime/ui/ini-panel/app.js @@ -1,4 +1,4 @@ -import { createLinuxCncIniSdk } from "../../sdk/src/linuxcnc-ini.js"; +import { createLinuxCncIniSdk } from "../../sdk/src/index.js"; import { getOpfsRoot, loadTextFile, diff --git a/wasm-port/tests/browser/ini_panel_smoke.html b/wasm-port/tests/browser/ini_panel_smoke.html index 26c4d9b..f490c21 100644 --- a/wasm-port/tests/browser/ini_panel_smoke.html +++ b/wasm-port/tests/browser/ini_panel_smoke.html @@ -7,7 +7,7 @@
running
+ + diff --git a/wasm-port/tests/browser/verify_interp_browser.sh b/wasm-port/tests/browser/verify_interp_browser.sh new file mode 100755 index 0000000..48c4c44 --- /dev/null +++ b/wasm-port/tests/browser/verify_interp_browser.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}" + +if [[ -z "$CHROMIUM" ]]; then + echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2 + exit 1 +fi + +if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then + "$ROOT_DIR/tools/build_wasm_core.sh" +fi + +TMP_DIR="$(mktemp -d)" +PORT_FILE="$TMP_DIR/port" +SERVER_LOG="$TMP_DIR/server.log" +CHROME_PROFILE="$TMP_DIR/chrome-profile" +mkdir -p "$CHROME_PROFILE" + +cleanup() { + if [[ -n "${SERVER_PID:-}" ]]; then + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + fi + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +import functools +import http.server +import pathlib +import socketserver +import sys + +root = pathlib.Path(sys.argv[1]) +port_file = pathlib.Path(sys.argv[2]) + +handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root)) +with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd: + port_file.write_text(str(httpd.server_address[1]), encoding="ascii") + httpd.serve_forever() +PY +SERVER_PID=$! + +for _ in $(seq 1 100); do + [[ -s "$PORT_FILE" ]] && break + sleep 0.05 +done + +if [[ ! -s "$PORT_FILE" ]]; then + echo "browser interpreter smoke HTTP server did not start" >&2 + cat "$SERVER_LOG" >&2 || true + exit 1 +fi + +PORT="$(cat "$PORT_FILE")" +URL="http://127.0.0.1:$PORT/tests/browser/interp_smoke.html" +OUT="$TMP_DIR/chromium.out" + +"$CHROMIUM" \ + --headless=new \ + --disable-gpu \ + --no-sandbox \ + --user-data-dir="$CHROME_PROFILE" \ + --virtual-time-budget=10000 \ + --dump-dom \ + "$URL" >"$OUT" 2>&1 + +if ! grep -Fq "browser_interp_smoke=ok" "$OUT"; then + echo "browser interpreter smoke failed" >&2 + sed -n '1,220p' "$OUT" >&2 + exit 1 +fi + +echo "browser_interp_smoke=ok" diff --git a/wasm-port/tests/fixtures/canon_file/position_params.events b/wasm-port/tests/fixtures/canon_file/position_params.events new file mode 100644 index 0000000..c66d61d --- /dev/null +++ b/wasm-port/tests/fixtures/canon_file/position_params.events @@ -0,0 +1,15 @@ +file_open=0 +file_read_count=3 +file_execute_count=2 +canon_event=STRAIGHT_TRAVERSE line=1 x=1 y=2 z=3 a=0 b=0 c=0 u=0 v=0 w=0 +canon_event=SET_FEED_RATE rate=80 +canon_event=COMMENT: interpreter: distance mode changed to incremental +canon_event=STRAIGHT_FEED line=2 x=1.5 y=1 z=5 a=0 b=0 c=0 u=0 v=0 w=0 +setup.current_x=1.5 +setup.current_y=1 +setup.current_z=5 +setup.parameter_5420=1.5 +setup.parameter_5421=1 +setup.parameter_5422=5 +post_execute.distance_mode=1 +post_execute.motion_mode=10 diff --git a/wasm-port/tests/host/verify_host_smokes.sh b/wasm-port/tests/host/verify_host_smokes.sh index dedb690..7c6927e 100755 --- a/wasm-port/tests/host/verify_host_smokes.sh +++ b/wasm-port/tests/host/verify_host_smokes.sh @@ -9,5 +9,6 @@ SKIP_INI_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_ini_wasm.sh" SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh" "$ROOT_DIR/tests/opfs/node/verify_file_service.sh" SKIP_INI_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh" +SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh" echo "host_wasm_opfs_browser_smokes=ok" diff --git a/wasm-port/tests/wasm/node/verify_ini_wasm.mjs b/wasm-port/tests/wasm/node/verify_ini_wasm.mjs index 25f0ca5..e319302 100644 --- a/wasm-port/tests/wasm/node/verify_ini_wasm.mjs +++ b/wasm-port/tests/wasm/node/verify_ini_wasm.mjs @@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import assert from "node:assert/strict"; -import { createLinuxCncIniSdk } from "../../../runtime/sdk/src/linuxcnc-ini.js"; +import { createLinuxCncIniSdk } from "../../../runtime/sdk/src/index.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/wasm-port/tests/wasm/node/verify_interp_wasm.mjs b/wasm-port/tests/wasm/node/verify_interp_wasm.mjs index f366f21..1b52828 100644 --- a/wasm-port/tests/wasm/node/verify_interp_wasm.mjs +++ b/wasm-port/tests/wasm/node/verify_interp_wasm.mjs @@ -3,69 +3,7 @@ import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import assert from "node:assert/strict"; -import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js"; - -function allocCString(mod, value) { - const bytes = mod.lengthBytesUTF8(value) + 1; - const ptr = mod._malloc(bytes); - mod.stringToUTF8(value, ptr, bytes); - return ptr; -} - -function runProgram(mod, programText) { - const programPtr = allocCString(mod, programText); - let resultPtr = 0; - try { - resultPtr = mod._lcinterp_run_program(programPtr); - assert.notEqual(resultPtr, 0); - return mod.UTF8ToString(resultPtr); - } finally { - if (resultPtr) { - mod._lcinterp_free_string(resultPtr); - } - mod._free(programPtr); - } -} - -function runProgramWithIni(mod, programText, iniPath) { - const programPtr = allocCString(mod, programText); - const iniPathPtr = allocCString(mod, iniPath); - let resultPtr = 0; - try { - resultPtr = mod._lcinterp_run_program_with_ini(programPtr, iniPathPtr); - assert.notEqual(resultPtr, 0); - return mod.UTF8ToString(resultPtr); - } finally { - if (resultPtr) { - mod._lcinterp_free_string(resultPtr); - } - mod._free(programPtr); - mod._free(iniPathPtr); - } -} - -function ensureDir(mod, path) { - try { - mod.FS.mkdir(path); - } catch { - // Directory already exists. - } -} - -function runFile(mod, path) { - const pathPtr = allocCString(mod, path); - let resultPtr = 0; - try { - resultPtr = mod._lcinterp_run_file(pathPtr); - assert.notEqual(resultPtr, 0); - return mod.UTF8ToString(resultPtr); - } finally { - if (resultPtr) { - mod._lcinterp_free_string(resultPtr); - } - mod._free(pathPtr); - } -} +import { createLinuxCncInterpSdk } from "../../../runtime/sdk/src/index.js"; function verifyExpectedOutput(fixtureName, output, expectedText) { const expectedLines = expectedText.split("\n").filter(Boolean); @@ -94,7 +32,7 @@ const __dirname = dirname(__filename); const rootDir = resolve(__dirname, "../../.."); const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm"); -const interp = await createLinuxCncInterpModule({ +const interp = await createLinuxCncInterpSdk({ wasmBinary: readFileSync(wasmPath), print() {}, printErr(message) { @@ -139,15 +77,13 @@ for (const fixtureName of fixtureNames) { "utf8", ).trimEnd(); - verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedEvents); + verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedEvents); } -ensureDir(interp, "/work"); const namedParamIniPath = "/work/namedparams.ini"; -interp.FS.writeFile( +interp.writeTextFile( namedParamIniPath, readFileSync(resolve(rootDir, "tests/fixtures/ini/namedparams.ini"), "utf8"), - { encoding: "utf8" }, ); const namedParamProgramText = readFileSync( resolve(rootDir, "tests/fixtures/gcode/namedparam_semantics.ngc"), @@ -159,7 +95,7 @@ const namedParamExpected = readFileSync( ).trimEnd(); verifyExpectedOutput( "namedparam_semantics", - runProgramWithIni(interp, namedParamProgramText, namedParamIniPath), + interp.runProgramWithIni(namedParamProgramText, namedParamIniPath), namedParamExpected, ); @@ -185,7 +121,7 @@ for (const fixtureName of errorFixtureNames) { "utf8", ).trimEnd(); - verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedOutput); + verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedOutput); } const fileFixtureNames = [ @@ -228,7 +164,29 @@ for (const fixtureName of fileFixtureNames) { "utf8", ).trimEnd(); - interp.FS.writeFile(programPath, programText, { encoding: "utf8" }); - verifyExpectedOutput(fixtureName, runFile(interp, programPath), expectedEvents); + interp.writeTextFile(programPath, programText); + verifyExpectedOutput(fixtureName, interp.runFile(programPath), expectedEvents); } + +const namedParamFilePath = "/work/namedparam_semantics.ngc"; +interp.writeTextFile(namedParamFilePath, namedParamProgramText); +verifyExpectedOutput( + "namedparam_semantics_file", + interp.runFileWithIni(namedParamFilePath, namedParamIniPath), + namedParamExpected, +); + +const positionParamsFilePath = "/work/position_params.ngc"; +interp.writeTextFile( + positionParamsFilePath, + readFileSync(resolve(rootDir, "tests/fixtures/gcode/position_params.ngc"), "utf8"), +); +verifyExpectedOutput( + "position_params_file", + interp.runFile(positionParamsFilePath), + readFileSync( + resolve(rootDir, "tests/fixtures/canon_file/position_params.events"), + "utf8", + ).trimEnd(), +); console.log("interp_wasm_node_smoke=ok"); diff --git a/wasm-port/tools/build_wasm_core.sh b/wasm-port/tools/build_wasm_core.sh index b85ae33..fc24c6f 100755 --- a/wasm-port/tools/build_wasm_core.sh +++ b/wasm-port/tools/build_wasm_core.sh @@ -95,5 +95,5 @@ link_wasm_module \ -s STACK_SIZE=2MB \ -s NO_EXIT_RUNTIME=1 \ -s FORCE_FILESYSTEM=1 \ - -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_free_string"]' \ + -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_free_string"]' \ -s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]' diff --git a/wasm-port/tools/verify_no_standalone_cnc_semantics.sh b/wasm-port/tools/verify_no_standalone_cnc_semantics.sh index 41a63a9..cb9042e 100755 --- a/wasm-port/tools/verify_no_standalone_cnc_semantics.sh +++ b/wasm-port/tools/verify_no_standalone_cnc_semantics.sh @@ -4,7 +4,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" TMP_FILE="$(mktemp)" -trap 'rm -f "$TMP_FILE"' EXIT +VIOLATION_FILE="$(mktemp)" +trap 'rm -f "$TMP_FILE" "$VIOLATION_FILE"' EXIT find \ "$ROOT_DIR/runtime" \ @@ -15,11 +16,44 @@ find \ ! -path "$ROOT_DIR/vendor/*" \ ! -path "$ROOT_DIR/runtime/ui/ini-panel/linuxcnc_ini.js" \ -print0 | - xargs -0 grep -nE '^[[:space:]]*int[[:space:]]+Interp::convert_g[[:space:]]*\(' > "$TMP_FILE" || true + xargs -0 grep -nE '^[[:space:]]*([A-Za-z_][A-Za-z0-9_:<>~*&[:space:]]+[[:space:]]+)?Interp::[A-Za-z_~][A-Za-z0-9_]*[[:space:]]*\(' > "$TMP_FILE" || true -if [[ -s "$TMP_FILE" ]]; then - echo "standalone Interp::convert_g definitions are not allowed:" >&2 - cat "$TMP_FILE" >&2 +ALLOWED_STUB_FILE="$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_interp_edge_stubs.cpp" + +is_allowed_interp_stub() { + local line="$1" + + case "$line" in + "$ALLOWED_STUB_FILE":*Interp::is_pycallable\(* | \ + "$ALLOWED_STUB_FILE":*Interp::is_user_defined_g_code\(* | \ + "$ALLOWED_STUB_FILE":*Interp::is_any_m_code_remapped\(* | \ + "$ALLOWED_STUB_FILE":*Interp::is_user_defined_m_code\(* | \ + "$ALLOWED_STUB_FILE":*Interp::is_m_code_remappable\(* | \ + "$ALLOWED_STUB_FILE":*Interp::is_g_code_remappable\(* | \ + "$ALLOWED_STUB_FILE":*Interp::remap_in_progress\(* | \ + "$ALLOWED_STUB_FILE":*Interp::remapping\(* | \ + "$ALLOWED_STUB_FILE":*Interp::parse_remap\(* | \ + "$ALLOWED_STUB_FILE":*Interp::convert_remapped_code\(* | \ + "$ALLOWED_STUB_FILE":*Interp::add_parameters\(* | \ + "$ALLOWED_STUB_FILE":*Interp::pycall\(* | \ + "$ALLOWED_STUB_FILE":*Interp::py_execute\(* | \ + "$ALLOWED_STUB_FILE":*Interp::py_reload\(*) + return 0 + ;; + esac + + return 1 +} + +while IFS= read -r line; do + if ! is_allowed_interp_stub "$line"; then + printf '%s\n' "$line" >> "$VIOLATION_FILE" + fi +done < "$TMP_FILE" + +if [[ -s "$VIOLATION_FILE" ]]; then + echo "standalone Interp member definitions are not allowed outside documented runtime-edge stubs:" >&2 + cat "$VIOLATION_FILE" >&2 exit 1 fi