按规划继续工作

结论:解释器 WASM、SDK 统一入口、浏览器解释器 smoke 与兼容性文档已闭环,native 和 host/WASM/browser 验证全部通过。
This commit is contained in:
2026-06-08 07:24:06 +08:00
parent c28b629ff6
commit 706fd1e775
20 changed files with 629 additions and 122 deletions

View File

@@ -70,7 +70,12 @@ expanded into a separate implementation of G-code behavior.
- GUI. - GUI.
7. Frontend code must be implemented with web technology, not migrated from 7. Frontend code must be implemented with web technology, not migrated from
native GUI code. 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 object files, dependency files, and command fingerprints, and must not
unconditionally recompile unchanged source files. unconditionally recompile unchanged source files.

View File

@@ -9,6 +9,9 @@ Rules:
3. Port-specific code, build scripts, adapters, tests, and documentation live here. 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. 4. LinuxCNC source is consumed by reference, copy, generated snapshot, or scripted extraction.
5. The browser/WASM product is managed as a separate program. 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: Suggested layout:
@@ -119,6 +122,9 @@ Directory intent:
- `vendor/linuxcnc/`: read-only copied LinuxCNC source selected for the port. - `vendor/linuxcnc/`: read-only copied LinuxCNC source selected for the port.
- `runtime/core/`: the standalone simulation engine built around vendored LinuxCNC code. - `runtime/core/`: the standalone simulation engine built around vendored LinuxCNC code.
- `runtime/sdk/`: JavaScript/TypeScript API for calling the WASM engine. - `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/ui/`: HTML + JavaScript CNC simulation frontend.
- `runtime/opfs/`: OPFS-backed persistence layer. - `runtime/opfs/`: OPFS-backed persistence layer.
- `tests/fixtures/`: stable simulation inputs shared by native and browser tests. - `tests/fixtures/`: stable simulation inputs shared by native and browser tests.

View File

@@ -36,6 +36,12 @@ The current browser smoke validation command is:
wasm-port/tests/browser/verify_ini_panel_browser.sh 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: The current aggregate host/WASM/browser smoke command is:
```bash ```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 Confirms every manifest file is present in `vendor/linuxcnc/`, no extra
vendored file exists, and every vendored file is byte-identical to upstream. vendored file exists, and every vendored file is byte-identical to upstream.
3. `tools/verify_no_standalone_cnc_semantics.sh` 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` 4. `tools/verify_native_linuxcnc_fixture_baseline.sh`
Runs a side-by-side fixture baseline through upstream Runs a side-by-side fixture baseline through upstream
`../linuxcnc/bin/rs274` and compares normalized canonical events for `../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 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 `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. queries against a file written to the Emscripten filesystem.
The WASM interpreter-core smoke script builds The WASM interpreter-core smoke script builds
`build/wasm/core/linuxcnc_interp.js` and `linuxcnc_interp.wasm` from the same `build/wasm/core/linuxcnc_interp.js` and `linuxcnc_interp.wasm` from the same
vendored LinuxCNC interpreter source set used by the native minimal 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 through
interpreter fixture group through `Interp::execute()`, and compares emitted `runtime/sdk/src/index.js`, runs the first WASM interpreter fixture
canonical events plus required LinuxCNC `_setup` state readback with the group through `Interp::execute()`, and compares emitted canonical events plus
matching files in `tests/fixtures/canon/`. It also writes selected G-code required LinuxCNC `_setup` state readback with the matching files in
fixtures into the Emscripten filesystem and runs them through LinuxCNC `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 `Interp::open()`, `Interp::read()`, and `Interp::execute()` to validate the
file execution path. 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 round-trip store plus pure-text machine file and G-code stores without
defining CNC machine-state or file-format semantics. defining CNC machine-state or file-format semantics.
The browser smoke script serves `wasm-port/` over localhost and runs Chromium The browser INI/OPFS smoke script serves `wasm-port/` over localhost and runs
headless against a test page that imports the JS SDK, loads the INI WASM Chromium headless against a test page that imports the JS SDK, loads the INI
module, queries vendored LinuxCNC INI parsing through the SDK, and performs an WASM module, queries vendored LinuxCNC INI parsing through the SDK, and
OPFS text-file, generic session snapshot, machine file, and G-code text performs an OPFS text-file, generic session snapshot, machine file, and G-code
round trip. 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 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 artifacts once, then runs the Node WASM smokes, the Node OPFS mock smoke, and
the Chromium browser smoke. the Chromium browser smokes.
## Source Coverage ## Source Coverage
@@ -147,9 +164,10 @@ 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 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/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/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. | | `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 ## 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`, `cutter_comp_plane_change`, `g53_incremental`, `namedparam_readonly`,
`numbered_param_readonly`, `tool_not_found`, and `numbered_param_readonly`, `tool_not_found`, and
`tool_length_offset_not_found` negative fixtures. The WASM interpreter file `tool_length_offset_not_found` negative fixtures. The WASM interpreter file
path additionally covers the same canonical-event fixture group except path additionally covers the same canonical-event fixture group, plus
`namedparam_semantics` and `position_params`, plus `file_open_reset`, `namedparam_semantics` through the INI-aware file execution ABI,
`percent_file_finish`, and `oword_subroutine`. OPFS validation is limited to `file_open_reset`, `percent_file_finish`, and `oword_subroutine`.
the JavaScript host-boundary adapter plus the INI browser smoke harness. Full `position_params` uses a dedicated file-path expectation under
browser coverage, full SDK coverage, and full machine-session validation remain `tests/fixtures/canon_file/` because LinuxCNC file execution advances the
future work. 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 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

@@ -49,7 +49,8 @@ semantic rewrites:
- Do not edit `../linuxcnc/`. - Do not edit `../linuxcnc/`.
- Do not patch vendored files without adding a patch under `patches/` and - Do not patch vendored files without adding a patch under `patches/` and
documenting the reason. 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 add `.c` or `.cc` manifest files without a source compile probe.
- Do not sync from a different upstream commit without updating - Do not sync from a different upstream commit without updating
`tools/upstream-baseline.txt` and `docs/scope-and-baseline.md`. `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 - No browser/full-core WASM parity tests yet. The INI parser now has Node and
Chromium smoke harnesses against vendored LinuxCNC `inifile.cc`. Chromium smoke harnesses against vendored LinuxCNC `inifile.cc`.
- JS SDK validation is currently limited to the INI WASM wrapper around - JS SDK validation now covers the INI WASM wrapper around vendored LinuxCNC
vendored LinuxCNC `inifile.cc`. `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 - OPFS validation covers a Node mock of the file-service adapter, the
host-side path model, generic session snapshot storage, pure-text machine host-side path model, generic session snapshot storage, pure-text machine
file and G-code storage, and a Chromium localhost round trip for those file and G-code storage, and a Chromium localhost round trip for those

View File

@@ -888,14 +888,20 @@ Keep these documents under `wasm-port/docs/`:
## Immediate Next Step ## Immediate Next Step
Continue expanding the standalone interpreter core from the verified minimal Continue from the current verified extracted-core baseline without adding
traverse path: project-authored CNC semantics:
1. keep `convert_g()` execution on vendored LinuxCNC interpreter conversion 1. Keep all `Interp::...` interpreter member behavior on vendored LinuxCNC
code and reject any new standalone `Interp::convert_g()` implementation. source. Standalone code may only provide documented runtime-edge stubs such
2. identify and shim the native runtime symbols blocking direct compilation of as the current Python/remap boundary.
`interp_convert.cc`, `interp_execute.cc`, and related interpreter files. 2. Expand WASM interpreter coverage by routing more existing native fixture
3. keep fixture coverage as regression protection while deleting temporary paths through `runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp`, using
hand-written semantics. the same vendored interpreter source set as the native harness.
4. keep all source changes inside `wasm-port/` and leave `../linuxcnc/` 3. Promote remaining standalone-only fixture expectations to native LinuxCNC
read-only. 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`.

View File

@@ -25,13 +25,15 @@ Current validation is intentionally mechanical:
- `tests/native/verify_native_probes.sh` checks that every manifest `.c` and - `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`. `.cc` file has a matching source probe in `build/native/source-probes.tsv`.
- `tools/verify_no_standalone_cnc_semantics.sh` rejects standalone - `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 ## Reuse Matrix
| Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation | | 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 | | 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 | | 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` | | 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. userspace genser flows are not yet established.
- Cutter compensation positive motion and negative interpreter paths are - Cutter compensation positive motion and negative interpreter paths are
fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`. 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 - Browser/WASM C ABI and JS SDK layers are now present for the INI parser and
interpreter/planner core. The INI parser has a minimal JS SDK wrapper and the current interpreter-core smoke scope. The interpreter SDK is a thin
Node/browser WASM smoke harnesses. 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 - OPFS persistence is connected to the INI panel through the host-side
`runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now `runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now
defines paths for INI, tool table, parameter file, G-code program, defines paths for INI, tool table, parameter file, G-code program,

View File

@@ -183,9 +183,13 @@ char *lcinterp_run_program(const char *program_text)
} }
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file(const char *path) char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
{ {
if (ini_path && ini_path[0] != '\0') {
setenv("INI_FILE_NAME", ini_path, 1);
} else {
unsetenv("INI_FILE_NAME"); unsetenv("INI_FILE_NAME");
}
Interp interp; Interp interp;
standalone::reset_canon_events(); standalone::reset_canon_events();
@@ -248,6 +252,12 @@ char *lcinterp_run_file(const char *path)
return copy_result(output.str()); return copy_result(output.str());
} }
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file(const char *path)
{
return lcinterp_run_file_with_ini(path, nullptr);
}
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
void lcinterp_free_string(char *value) void lcinterp_free_string(char *value)
{ {

View File

@@ -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)`

View File

@@ -0,0 +1,2 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";

View File

@@ -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);
},
};
}

View File

@@ -1,4 +1,4 @@
import { createLinuxCncIniSdk } from "../../sdk/src/linuxcnc-ini.js"; import { createLinuxCncIniSdk } from "../../sdk/src/index.js";
import { import {
getOpfsRoot, getOpfsRoot,
loadTextFile, loadTextFile,

View File

@@ -7,7 +7,7 @@
<body> <body>
<pre id="status">running</pre> <pre id="status">running</pre>
<script type="module"> <script type="module">
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/linuxcnc-ini.js"; import { createLinuxCncIniSdk } from "../../runtime/sdk/src/index.js";
import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js"; import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
import { machineIniPath } from "../../runtime/opfs/path-model.js"; import { machineIniPath } from "../../runtime/opfs/path-model.js";
import { import {

View File

@@ -0,0 +1,138 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>LinuxCNC Interpreter Browser Smoke</title>
</head>
<body>
<pre id="status">running</pre>
<script type="module">
import { createLinuxCncInterpSdk } from "../../runtime/sdk/src/index.js";
const status = document.getElementById("status");
async function fetchText(path) {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`${path}: HTTP ${response.status}`);
}
return response.text();
}
function verifyExpectedOutput(fixtureName, output, expectedText) {
const expectedLines = expectedText.split("\n").filter(Boolean);
for (const expectedLine of expectedLines) {
if (expectedLine.startsWith("absent=")) {
const forbidden = expectedLine.slice("absent=".length);
if (output.includes(forbidden)) {
throw new Error(`${fixtureName}: unexpected ${forbidden}`);
}
continue;
}
if (!output.includes(expectedLine)) {
throw new Error(`${fixtureName}: missing ${expectedLine}`);
}
}
}
try {
const interp = await createLinuxCncInterpSdk({
locateFile(path) {
if (path === "linuxcnc_interp.wasm") {
return "../../build/wasm/core/linuxcnc_interp.wasm";
}
return path;
},
print() {},
printErr(message) {
console.error(message);
},
});
for (const fixtureName of [
"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",
]) {
const programText = await fetchText(`../fixtures/gcode/${fixtureName}.ngc`);
const expectedText = await fetchText(`../fixtures/canon/${fixtureName}.events`);
verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedText.trimEnd());
}
for (const errorFixtureName of [
"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",
]) {
verifyExpectedOutput(
errorFixtureName,
interp.runProgram(await fetchText(`../fixtures/gcode_errors/${errorFixtureName}.ngc`)),
(await fetchText(`../fixtures/canon_errors/${errorFixtureName}.expected`)).trimEnd(),
);
}
const positionParamsText = await fetchText("../fixtures/gcode/position_params.ngc");
const positionParamsPath = "/work/position_params.ngc";
interp.writeTextFile(positionParamsPath, positionParamsText);
verifyExpectedOutput(
"position_params_file",
interp.runFile(positionParamsPath),
(await fetchText("../fixtures/canon_file/position_params.events")).trimEnd(),
);
for (const fileFixtureName of ["file_open_reset", "percent_file_finish", "oword_subroutine"]) {
const programPath = `/work/${fileFixtureName}.ngc`;
interp.writeTextFile(programPath, await fetchText(`../fixtures/gcode/${fileFixtureName}.ngc`));
verifyExpectedOutput(
`${fileFixtureName}_file`,
interp.runFile(programPath),
(await fetchText(`../fixtures/canon/${fileFixtureName}.events`)).trimEnd(),
);
}
const namedParamIniPath = "/work/namedparams.ini";
interp.writeTextFile(namedParamIniPath, await fetchText("../fixtures/ini/namedparams.ini"));
const namedParamPath = "/work/namedparam_semantics.ngc";
interp.writeTextFile(
namedParamPath,
await fetchText("../fixtures/gcode/namedparam_semantics.ngc"),
);
verifyExpectedOutput(
"namedparam_semantics_file",
interp.runFileWithIni(namedParamPath, namedParamIniPath),
(await fetchText("../fixtures/canon/namedparam_semantics.events")).trimEnd(),
);
status.textContent = "browser_interp_smoke=ok";
} catch (error) {
status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`;
}
</script>
</body>
</html>

View File

@@ -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"

View File

@@ -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

View File

@@ -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" SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh"
"$ROOT_DIR/tests/opfs/node/verify_file_service.sh" "$ROOT_DIR/tests/opfs/node/verify_file_service.sh"
SKIP_INI_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.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" echo "host_wasm_opfs_browser_smokes=ok"

View File

@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import assert from "node:assert/strict"; 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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);

View File

@@ -3,69 +3,7 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js"; import { createLinuxCncInterpSdk } from "../../../runtime/sdk/src/index.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);
}
}
function verifyExpectedOutput(fixtureName, output, expectedText) { function verifyExpectedOutput(fixtureName, output, expectedText) {
const expectedLines = expectedText.split("\n").filter(Boolean); const expectedLines = expectedText.split("\n").filter(Boolean);
@@ -94,7 +32,7 @@ const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../.."); const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm"); const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm");
const interp = await createLinuxCncInterpModule({ const interp = await createLinuxCncInterpSdk({
wasmBinary: readFileSync(wasmPath), wasmBinary: readFileSync(wasmPath),
print() {}, print() {},
printErr(message) { printErr(message) {
@@ -139,15 +77,13 @@ for (const fixtureName of fixtureNames) {
"utf8", "utf8",
).trimEnd(); ).trimEnd();
verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedEvents); verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedEvents);
} }
ensureDir(interp, "/work");
const namedParamIniPath = "/work/namedparams.ini"; const namedParamIniPath = "/work/namedparams.ini";
interp.FS.writeFile( interp.writeTextFile(
namedParamIniPath, namedParamIniPath,
readFileSync(resolve(rootDir, "tests/fixtures/ini/namedparams.ini"), "utf8"), readFileSync(resolve(rootDir, "tests/fixtures/ini/namedparams.ini"), "utf8"),
{ encoding: "utf8" },
); );
const namedParamProgramText = readFileSync( const namedParamProgramText = readFileSync(
resolve(rootDir, "tests/fixtures/gcode/namedparam_semantics.ngc"), resolve(rootDir, "tests/fixtures/gcode/namedparam_semantics.ngc"),
@@ -159,7 +95,7 @@ const namedParamExpected = readFileSync(
).trimEnd(); ).trimEnd();
verifyExpectedOutput( verifyExpectedOutput(
"namedparam_semantics", "namedparam_semantics",
runProgramWithIni(interp, namedParamProgramText, namedParamIniPath), interp.runProgramWithIni(namedParamProgramText, namedParamIniPath),
namedParamExpected, namedParamExpected,
); );
@@ -185,7 +121,7 @@ for (const fixtureName of errorFixtureNames) {
"utf8", "utf8",
).trimEnd(); ).trimEnd();
verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedOutput); verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedOutput);
} }
const fileFixtureNames = [ const fileFixtureNames = [
@@ -228,7 +164,29 @@ for (const fixtureName of fileFixtureNames) {
"utf8", "utf8",
).trimEnd(); ).trimEnd();
interp.FS.writeFile(programPath, programText, { encoding: "utf8" }); interp.writeTextFile(programPath, programText);
verifyExpectedOutput(fixtureName, runFile(interp, programPath), expectedEvents); 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"); console.log("interp_wasm_node_smoke=ok");

View File

@@ -95,5 +95,5 @@ link_wasm_module \
-s STACK_SIZE=2MB \ -s STACK_SIZE=2MB \
-s NO_EXIT_RUNTIME=1 \ -s NO_EXIT_RUNTIME=1 \
-s FORCE_FILESYSTEM=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"]' -s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'

View File

@@ -4,7 +4,8 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TMP_FILE="$(mktemp)" TMP_FILE="$(mktemp)"
trap 'rm -f "$TMP_FILE"' EXIT VIOLATION_FILE="$(mktemp)"
trap 'rm -f "$TMP_FILE" "$VIOLATION_FILE"' EXIT
find \ find \
"$ROOT_DIR/runtime" \ "$ROOT_DIR/runtime" \
@@ -15,11 +16,44 @@ find \
! -path "$ROOT_DIR/vendor/*" \ ! -path "$ROOT_DIR/vendor/*" \
! -path "$ROOT_DIR/runtime/ui/ini-panel/linuxcnc_ini.js" \ ! -path "$ROOT_DIR/runtime/ui/ini-panel/linuxcnc_ini.js" \
-print0 | -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 ALLOWED_STUB_FILE="$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_interp_edge_stubs.cpp"
echo "standalone Interp::convert_g definitions are not allowed:" >&2
cat "$TMP_FILE" >&2 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 exit 1
fi fi