增加解释器WASM文件入口

结论:解释器核心WASM新增基于LinuxCNC Interp::open/read/execute的文件运行入口,并覆盖file_open_reset、percent_file_finish和oword_subroutine夹具。
This commit is contained in:
2026-06-08 05:21:39 +08:00
parent 80f23ea0a1
commit fd680f7515
4 changed files with 135 additions and 15 deletions

View File

@@ -76,7 +76,10 @@ vendored LinuxCNC interpreter source set used by the native minimal
interpreter harness. It loads the module in Node, runs the first WASM interpreter harness. It loads the module in Node, runs the first WASM
interpreter fixture group through `Interp::execute()`, and compares emitted interpreter fixture group through `Interp::execute()`, and compares emitted
canonical events plus required LinuxCNC `_setup` state readback with the canonical events plus required LinuxCNC `_setup` state readback with the
matching files in `tests/fixtures/canon/`. matching files in `tests/fixtures/canon/`. It also writes selected G-code
fixtures into the Emscripten filesystem and runs them through LinuxCNC
`Interp::open()`, `Interp::read()`, and `Interp::execute()` to validate the
file execution path.
The OPFS host-boundary script validates the JavaScript file-service adapter 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 with a Node mock of the browser File System Access handles. It covers nested
@@ -144,7 +147,7 @@ The validation fails if:
| Harness | Purpose | | 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_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 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, 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/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_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/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, OPFS, and browser smoke validation with shared WASM builds. | | `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, OPFS, and browser smoke validation with shared WASM builds. |
@@ -197,9 +200,11 @@ for `minimal_linear`, `arc_semantics`, `length_units`, `modal_incremental`,
`feed_control_modes`, `position_params`, `probe_semantics`, `spindle_orient`, `feed_control_modes`, `position_params`, `probe_semantics`, `spindle_orient`,
`comment_logging`, and `numbered_params`, plus the `g1_zero_feed`, `comment_logging`, and `numbered_params`, plus the `g1_zero_feed`,
`arc_radius_mismatch`, `arc_zero_radius`, and `g53_incremental` negative `arc_radius_mismatch`, `arc_zero_radius`, and `g53_incremental` negative
fixtures. OPFS validation is limited to the JavaScript host-boundary adapter fixtures. The WASM interpreter file path additionally covers
plus the INI browser smoke harness. Full browser coverage, full SDK coverage, `file_open_reset`, `percent_file_finish`, and `oword_subroutine`. OPFS
and full machine-session validation remain future work. 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.
The current fixture expectations validate standalone behavior against both the The current fixture expectations validate standalone behavior against both the
vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for

View File

@@ -113,6 +113,19 @@ char *copy_result(const std::string &result)
return out; return out;
} }
void append_events_and_state(std::ostringstream &output, const Interp &interp)
{
for (const auto &event : standalone::canon_events()) {
output << "canon_event=" << event << "\n";
}
output << "setup.current_x=" << interp._setup.current_x << "\n";
output << "setup.current_y=" << interp._setup.current_y << "\n";
output << "setup.current_z=" << interp._setup.current_z << "\n";
output << "setup.parameter_5420=" << interp._setup.parameters[5420] << "\n";
output << "setup.parameter_5421=" << interp._setup.parameters[5421] << "\n";
output << "setup.parameter_5422=" << interp._setup.parameters[5422] << "\n";
}
} // namespace } // namespace
extern "C" { extern "C" {
@@ -143,15 +156,71 @@ char *lcinterp_run_program(const char *program_text)
} }
} }
for (const auto &event : standalone::canon_events()) { append_events_and_state(output, interp);
output << "canon_event=" << event << "\n"; return copy_result(output.str());
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file(const char *path)
{
Interp interp;
standalone::reset_canon_events();
initialize_minimal_interp(interp);
std::ostringstream output;
const int open_rc = interp.open(path);
output << "file_open=" << open_rc << "\n";
if (open_rc != INTERP_OK) {
if (open_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
interp.error_text(open_rc, error_buf, sizeof(error_buf));
output << "file_error_text=" << error_buf << "\n";
}
append_events_and_state(output, interp);
return copy_result(output.str());
} }
output << "setup.current_x=" << interp._setup.current_x << "\n";
output << "setup.current_y=" << interp._setup.current_y << "\n"; int file_read_count = 0;
output << "setup.current_z=" << interp._setup.current_z << "\n"; int file_execute_count = 0;
output << "setup.parameter_5420=" << interp._setup.parameters[5420] << "\n"; while (true) {
output << "setup.parameter_5421=" << interp._setup.parameters[5421] << "\n"; const int read_rc = interp.read();
output << "setup.parameter_5422=" << interp._setup.parameters[5422] << "\n"; if (read_rc == INTERP_ENDFILE) {
output << "file_read_eof=" << read_rc << "\n";
break;
}
++file_read_count;
output << "file_read_" << file_read_count << "=" << read_rc << "\n";
if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
if (read_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
interp.error_text(read_rc, error_buf, sizeof(error_buf));
output << "file_error_text=" << error_buf << "\n";
}
break;
}
const int execute_rc = interp.execute();
++file_execute_count;
output << "file_execute_" << file_execute_count << "=" << execute_rc << "\n";
if (execute_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
interp.error_text(execute_rc, error_buf, sizeof(error_buf));
output << "file_error_text=" << error_buf << "\n";
}
if ((execute_rc != INTERP_OK) &&
(execute_rc != INTERP_EXECUTE_FINISH) &&
(execute_rc != INTERP_EXIT)) {
break;
}
if (execute_rc == INTERP_EXIT) {
break;
}
}
output << "file_read_count=" << file_read_count << "\n";
output << "file_execute_count=" << file_execute_count << "\n";
append_events_and_state(output, interp);
return copy_result(output.str()); return copy_result(output.str());
} }

View File

@@ -27,6 +27,29 @@ function runProgram(mod, programText) {
} }
} }
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);
}
}
function verifyExpectedOutput(fixtureName, output, expectedText) { function verifyExpectedOutput(fixtureName, output, expectedText) {
const expectedLines = expectedText.split("\n").filter(Boolean); const expectedLines = expectedText.split("\n").filter(Boolean);
@@ -110,4 +133,26 @@ for (const fixtureName of errorFixtureNames) {
verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedOutput); verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedOutput);
} }
const fileFixtureNames = [
"file_open_reset",
"percent_file_finish",
"oword_subroutine",
];
ensureDir(interp, "/work");
for (const fixtureName of fileFixtureNames) {
const programPath = `/work/${fixtureName}.ngc`;
const programText = readFileSync(
resolve(rootDir, `tests/fixtures/gcode/${fixtureName}.ngc`),
"utf8",
);
const expectedEvents = readFileSync(
resolve(rootDir, `tests/fixtures/canon/${fixtureName}.events`),
"utf8",
).trimEnd();
interp.FS.writeFile(programPath, programText, { encoding: "utf8" });
verifyExpectedOutput(fixtureName, runFile(interp, programPath), expectedEvents);
}
console.log("interp_wasm_node_smoke=ok"); console.log("interp_wasm_node_smoke=ok");

View File

@@ -94,5 +94,6 @@ link_wasm_module \
-s ALLOW_MEMORY_GROWTH=1 \ -s ALLOW_MEMORY_GROWTH=1 \
-s STACK_SIZE=2MB \ -s STACK_SIZE=2MB \
-s NO_EXIT_RUNTIME=1 \ -s NO_EXIT_RUNTIME=1 \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_free_string"]' \ -s FORCE_FILESYSTEM=1 \
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8"]' -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_file","_lcinterp_free_string"]' \
-s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'