diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index a2730c5..e06b982 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -87,7 +87,10 @@ 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. +file execution path. The same Node smoke writes LinuxCNC-format parameter +files into the Emscripten filesystem and validates vendored +`Interp::restore_parameters()` and `Interp::save_parameters()`, including the +saved parameter values and `.bak` backup file boundary. The OPFS host-boundary script validates the JavaScript file-service adapter with a Node mock of the browser File System Access handles. It covers nested @@ -164,7 +167,7 @@ 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, 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/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()`, match the native canonical event plus required state readback fixtures, and run parameter-file restore/save through vendored LinuxCNC `Interp::restore_parameters()` and `Interp::save_parameters()`. | | `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. | @@ -230,6 +233,11 @@ path additionally covers the same canonical-event fixture group, plus `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. +The Node WASM interpreter smoke also covers LinuxCNC parameter-file +restore/save behavior through the exported C ABI, including out-of-order file +rejection, missing-file success, required numeric parameter writeback, removal +of named-parameter-only lines from saved output, and the `.bak` backup produced +by vendored `save_parameters()`. 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. diff --git a/wasm-port/docs/drift-report.md b/wasm-port/docs/drift-report.md index 62ee433..f9247f5 100644 --- a/wasm-port/docs/drift-report.md +++ b/wasm-port/docs/drift-report.md @@ -32,6 +32,7 @@ semantic rewrites: | Interpreter state tags | LinuxCNC `UPDATE_TAG` callbacks are captured from the state tags packed by vendored interpreter code; standalone code does not derive modal state. | | Rotary indexer lock state | LinuxCNC `UNLOCK_ROTARY` and `LOCK_ROTARY` callbacks are captured from the vendored single-axis indexer path; standalone code does not implement rotary-indexing semantics. | | File flush | LinuxCNC `FINISH` is captured as a canonical test event on the vendored `%`-delimited file-reading path. | +| WASM parameter-file backup | LinuxCNC `save_parameters()` calls `link()` to create the `.bak` parameter-file backup; `runtime/core/linuxcnc_wrap/linuxcnc_wasm_posix_stubs.cpp` maps that WASM/MEMFS edge to a file copy so the vendored save path can run unchanged. | | Interpreter reset | LinuxCNC `ON_RESET` is captured as a canonical test event on the vendored interpreter reset/file-open path. | | Interpreter init | LinuxCNC `INIT_CANON` and the canonical initialization sequence from vendored `Interp::init()` are captured as test events. | | Comment logging | LinuxCNC `LOGOPEN`, `LOG`, `LOGAPPEND`, and `LOGCLOSE` callbacks are captured as canonical test events instead of writing host log files. | diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index b6c708a..086670c 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -53,7 +53,7 @@ Current validation is intentionally mechanical: | Dependency | LinuxCNC files that expose it | Standalone treatment | | --- | --- | --- | -| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, and pure-text machine-file persistence in `runtime/opfs/machine-file-store.js` | +| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; the interpreter WASM C ABI now validates parameter-file restore/save by calling vendored `Interp::restore_parameters()` and `Interp::save_parameters()` against Emscripten filesystem paths; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, and pure-text machine-file persistence in `runtime/opfs/machine-file-store.js` | | RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` | | NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes | | HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` | @@ -73,15 +73,17 @@ Current validation is intentionally mechanical: - 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. + paths, including parameter-file restore/save through vendored + `rs274ngc_pre.cc`; it does not define G-code or parameter 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, preview-cache, and session-snapshot targets. `runtime/opfs/snapshot-store.js` adds a generic JSON session snapshot envelope. `runtime/opfs/machine-file-store.js` adds pure-text storage for INI, tool - table, parameter file, and G-code program content; semantic loading for tool - tables, parameter files, G-code programs, and machine-state restoration - remains future work. + table, parameter file, and G-code program content; browser session plumbing + that feeds OPFS-backed parameter files into the existing WASM + `restore_parameters()`/`save_parameters()` ABI, plus semantic loading for + tool tables and machine-state restoration, remains future work. - Native LinuxCNC GUI code remains out of scope for implementation. 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 5406cda..6814ae4 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp @@ -136,6 +136,36 @@ void append_events_and_state(std::ostringstream &output, const Interp &interp) output << "post_execute.flood=" << interp._setup.flood << "\n"; } +void append_parameter_state(std::ostringstream &output, const Interp &interp) +{ + output << "parameter_5161=" << interp._setup.parameters[5161] << "\n"; + output << "parameter_5162=" << interp._setup.parameters[5162] << "\n"; + output << "parameter_5220=" << interp._setup.parameters[5220] << "\n"; + output << "parameter_5221=" << interp._setup.parameters[5221] << "\n"; + output << "parameter_5399=" << interp._setup.parameters[5399] << "\n"; +} + +void append_error_text(std::ostringstream &output, Interp &interp, const char *prefix, int rc) +{ + if (rc > INTERP_MIN_ERROR) { + char error_buf[LINELEN] = {0}; + interp.error_text(rc, error_buf, sizeof(error_buf)); + output << prefix << error_buf << "\n"; + } +} + +void apply_parameter_assignments(Interp &interp, const char *assignments) +{ + std::istringstream input(assignments ? assignments : ""); + int parameter = 0; + double value = 0.0; + while (input >> parameter >> value) { + if ((parameter > 0) && (parameter < interp_param_global::RS274NGC_MAX_PARAMETERS)) { + interp._setup.parameters[parameter] = value; + } + } +} + } // namespace extern "C" { @@ -258,6 +288,35 @@ char *lcinterp_run_file(const char *path) return lcinterp_run_file_with_ini(path, nullptr); } +EMSCRIPTEN_KEEPALIVE +char *lcinterp_restore_parameters(const char *path) +{ + Interp interp; + initialize_minimal_interp(interp); + + std::ostringstream output; + const int rc = interp.restore_parameters(path); + output << "restore_parameters=" << rc << "\n"; + append_error_text(output, interp, "restore_error_text=", rc); + append_parameter_state(output, interp); + return copy_result(output.str()); +} + +EMSCRIPTEN_KEEPALIVE +char *lcinterp_save_parameters(const char *path, const char *assignments) +{ + Interp interp; + initialize_minimal_interp(interp); + apply_parameter_assignments(interp, assignments); + + std::ostringstream output; + const int rc = interp.save_parameters(path, interp._setup.parameters); + output << "save_parameters=" << rc << "\n"; + append_error_text(output, interp, "save_error_text=", rc); + append_parameter_state(output, interp); + return copy_result(output.str()); +} + EMSCRIPTEN_KEEPALIVE void lcinterp_free_string(char *value) { diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_wasm_posix_stubs.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_wasm_posix_stubs.cpp index 756a395..91ebcf3 100644 --- a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_wasm_posix_stubs.cpp +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_wasm_posix_stubs.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include @@ -44,4 +46,59 @@ void wordfree(wordexp_t *pwordexp) pwordexp->we_offs = 0; } +int link(const char *oldpath, const char *newpath) +{ + FILE *source = std::fopen(oldpath, "rb"); + if (!source) { + return -1; + } + + FILE *target = std::fopen(newpath, "rb"); + if (target) { + std::fclose(target); + std::fclose(source); + errno = EEXIST; + return -1; + } + + target = std::fopen(newpath, "wb"); + if (!target) { + const int saved_errno = errno; + std::fclose(source); + errno = saved_errno; + return -1; + } + + char buffer[4096]; + while (true) { + const std::size_t read = std::fread(buffer, 1, sizeof(buffer), source); + if (read > 0 && std::fwrite(buffer, 1, read, target) != read) { + const int saved_errno = errno; + std::fclose(target); + std::fclose(source); + errno = saved_errno; + return -1; + } + if (read < sizeof(buffer)) { + if (std::ferror(source)) { + const int saved_errno = errno; + std::fclose(target); + std::fclose(source); + errno = saved_errno; + return -1; + } + break; + } + } + + if (std::fclose(target) != 0) { + const int saved_errno = errno; + std::fclose(source); + errno = saved_errno; + return -1; + } + std::fclose(source); + return 0; +} + } // extern "C" diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index b0fac3d..6e5df5e 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -38,4 +38,7 @@ vendored LinuxCNC RS274NGC sources and exposes: - `runProgramWithIni(programText, iniPath)` - `runFile(path)` - `runFileWithIni(path, iniPath)` +- `restoreParameters(path)` +- `saveParameters(path, values)` - `writeTextFile(path, text)` +- `readTextFile(path)` diff --git a/wasm-port/runtime/sdk/src/linuxcnc-interp.js b/wasm-port/runtime/sdk/src/linuxcnc-interp.js index b691690..d4c6ed3 100644 --- a/wasm-port/runtime/sdk/src/linuxcnc-interp.js +++ b/wasm-port/runtime/sdk/src/linuxcnc-interp.js @@ -54,6 +54,10 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) { mod.FS.writeFile(path, text, { encoding: "utf8" }); }, + readTextFile(path) { + return mod.FS.readFile(path, { encoding: "utf8" }); + }, + runProgram(programText) { return callStringResult(mod, "lcinterp_run_program", programText); }, @@ -69,5 +73,16 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) { runFileWithIni(path, iniPath) { return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath); }, + + restoreParameters(path) { + return callStringResult(mod, "lcinterp_restore_parameters", path); + }, + + saveParameters(path, values) { + const assignments = Object.entries(values) + .map(([parameter, value]) => `${parameter} ${value}`) + .join("\n"); + return callStringResult(mod, "lcinterp_save_parameters", path, assignments); + }, }; } diff --git a/wasm-port/tests/wasm/node/verify_interp_wasm.mjs b/wasm-port/tests/wasm/node/verify_interp_wasm.mjs index ee97a05..7af944e 100644 --- a/wasm-port/tests/wasm/node/verify_interp_wasm.mjs +++ b/wasm-port/tests/wasm/node/verify_interp_wasm.mjs @@ -134,4 +134,73 @@ verifyExpectedOutput( "utf8", ).trimEnd(), ); + +const parameterFilePath = "/work/rs274ngc.var"; +interp.writeTextFile( + parameterFilePath, + [ + "5161 10.5", + "5162 20.25", + "5220 1", + "5221 2.25", + "5399 44", + "<_named_param> 123", + "", + ].join("\n"), +); +verifyExpectedOutput( + "restore_parameters", + interp.restoreParameters(parameterFilePath), + [ + "restore_parameters=0", + "parameter_5161=10.5", + "parameter_5162=20.25", + "parameter_5220=1", + "parameter_5221=2.25", + "parameter_5399=44", + ].join("\n"), +); +verifyExpectedOutput( + "restore_parameters_missing_file", + interp.restoreParameters("/work/missing.var"), + "restore_parameters=0", +); + +const outOfOrderParameterFilePath = "/work/out-of-order.var"; +interp.writeTextFile(outOfOrderParameterFilePath, "5220 1\n5161 2\n"); +verifyExpectedOutput( + "restore_parameters_out_of_order", + interp.restoreParameters(outOfOrderParameterFilePath), + [ + "restore_parameters=5", + "restore_error_text=Parameter file out of order", + ].join("\n"), +); + +verifyExpectedOutput( + "save_parameters", + interp.saveParameters(parameterFilePath, { + 5161: 12.34, + 5162: 56.78, + 5220: 1.0, + 5221: 9.87, + 5399: 66.6, + }), + [ + "save_parameters=0", + "parameter_5161=12.34", + "parameter_5162=56.78", + "parameter_5220=1", + "parameter_5221=9.87", + "parameter_5399=66.6", + ].join("\n"), +); +const savedParameterFile = interp.readTextFile(parameterFilePath); +const backupParameterFile = interp.readTextFile(`${parameterFilePath}.bak`); +assert.equal(savedParameterFile.includes("5161\t12.340000"), true); +assert.equal(savedParameterFile.includes("5162\t56.780000"), true); +assert.equal(savedParameterFile.includes("5221\t9.870000"), true); +assert.equal(savedParameterFile.includes("5399\t66.600000"), true); +assert.equal(savedParameterFile.includes("_named_param"), false); +assert.equal(backupParameterFile.includes("5161 10.5"), true); 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 fc24c6f..60a9dce 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_run_file_with_ini","_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_restore_parameters","_lcinterp_save_parameters","_lcinterp_free_string"]' \ -s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'