commit 0b9f5898c39962558247b3c14ed85a9bafbdbe18 Author: rvba Date: Mon Aug 31 10:12:17 2026 +0200 release: 0.0.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c2c3de --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/pkg +Cargo.lock +.claude/ +.opencode/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0b54a04 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md — bimr-wasm + +WASM adapter for the [`bimr-engine`](../bimr-engine) eval — the +`compile()` surface consumed by bimr-web. Plan: +[`task/refactor/2026-08-13/white_wasm_surface.md`](../task/refactor/2026-08-13/white_wasm_surface.md). + +## Commands + +```bash +make test # native cargo test (wasm_bindgen fns are plain Rust) +make pkg # wasm32 build -> wasm-bindgen web pkg -> wasm-opt +make smoke # pkg + Node smoke test (node/smoke.mjs) +``` + +## Rules + +- **Whole-source only** (D8, batch-DSL / Alternative A): the surface is + exactly **one export**, `compile(src)` — no session API, no per-call + construction, no side-query projections. Python emits `.bimr` text and + calls `compile()` once, the same path as the CLI. The incumbent session + bridge was retired with the classic stack (bimr-black). +- The adapter stays **thin**: no engine logic here — no geometry pre-pass + (meshes compute at serialize time), no lazy materialisation, no parameter + parsing. Everything goes through `eval`'s public surface. +- Errors return `{"error": "..."}` JSON strings. +- Dependencies on `eval`/`parser` are git pins on + `bimr-engine` with local `[patch]` overrides — same pattern as the + engine's kernel pin. +- `pkg/` and `target/` are build artifacts — never commit them. + +*This document should be updated as the codebase evolves.* diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4adf22f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bimr-wasm" +version = "0.0.1" +edition = "2024" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +eval = { git = "ssh://git@git.rvba.fr:229/rvba/bimr-engine.git" } +parser = { git = "ssh://git@git.rvba.fr:229/rvba/bimr-engine.git" } +# Pinned to the factory's wasm-bindgen CLI (Makefile WASM_BINDGEN_VERSION). +wasm-bindgen = "=0.2.114" + +# Local checkouts override the forge pins (same pattern as bimr-engine's +# kernel patch) so the factory builds against sibling repos. +[patch."ssh://git@git.rvba.fr:229/rvba/bimr-engine.git"] +eval = { path = "../bimr-engine/eval" } +parser = { path = "../bimr-engine/parser" } + +[patch."ssh://git@git.rvba.fr:229/rvba/bimr-kernel.git"] +bimr-kernel = { path = "../bimr-kernel" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9e1af9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Milovann Yanatchkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bde4be5 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: test pkg clean + +# Native tests — wasm_bindgen fns are plain Rust; sessions work on the host. +test: + cargo test + +# wasm-bindgen web pkg (consumed by the factory `build-wasm-white` and +# node/smoke.mjs). Pins mirror the factory: wasm-bindgen 0.2.114, Binaryen. +pkg: + rustup target add wasm32-unknown-unknown + cargo build --release --target wasm32-unknown-unknown + $(HOME)/.cargo/bin/wasm-bindgen --target web --out-dir pkg \ + target/wasm32-unknown-unknown/release/bimr_wasm.wasm + command -v wasm-opt >/dev/null && \ + wasm-opt -O pkg/bimr_wasm_bg.wasm -o pkg/bimr_wasm_bg.wasm || true + +# Node smoke test over the web pkg. +smoke: pkg + node node/smoke.mjs + +clean: + cargo clean + rm -rf pkg diff --git a/node/smoke.mjs b/node/smoke.mjs new file mode 100644 index 0000000..ff942f8 --- /dev/null +++ b/node/smoke.mjs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 Milovann Yanatchkov +// SPDX-License-Identifier: MIT + +// smoke.mjs — Node test of the wasm-bindgen web pkg (engine.test.mjs pattern +// from bimr-web). Run via `make smoke` after `make pkg`. + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +function section(name) { + console.log(`\n# ${name}`); +} + +async function loadWasm() { + const jsUrl = new URL("../pkg/bimr_wasm.js", import.meta.url).href; + const bimrWasm = await import(jsUrl); + const wasmBytes = readFileSync(new URL("../pkg/bimr_wasm_bg.wasm", import.meta.url)); + await bimrWasm.default({ module_or_path: wasmBytes }); + return bimrWasm; +} + +const WALL_SRC = `p1 = Point(0,0,0) +p2 = Point(500,0,0) +l1 = Line(p1,p2) +w1 = Wall(l1,20,300)`; + +async function main() { + const wasm = await loadWasm(); + + section("surface — whole-source only (D8)"); + { + const exports = Object.keys(wasm).filter((k) => typeof wasm[k] === "function" && !k.startsWith("__")); + assert.deepEqual( + exports.sort(), + ["compile", "default", "initSync"], + "no session exports, no side queries", + ); + } + + section("compile() — wall to IFCX"); + { + const out = wasm.compile(WALL_SRC); + assert.match(out, /wall-001/); + assert.match(out, /ifcx_alpha/); + assert.match(out, /faceVertexIndices/); + } + + section("compile() — error handling"); + { + const out = wasm.compile("w1 = Wall(missing)"); + assert.match(out, /error/); + } + + console.log("\nAll smoke tests passed."); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f75f592 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2026 Milovann Yanatchkov +// SPDX-License-Identifier: MIT + +//! bimr-wasm — browser adapter for the [`bimr-engine`](https://git.rvba.fr/rvba/bimr-engine) eval. +//! +//! **Whole-source only**: the adapter exposes exactly one export — +//! `compile(src)`. There is no session layer, no per-call construction, and +//! no side queries: Python emits `.bimr` text and calls `compile()` once, +//! the same path as the CLI. The `.bimr` intermediate is the compatibility +//! layer; whole-source is the single entry point for the CLI, web Python, +//! and LLM paths. +//! +//! Errors return `{"error": "..."}` JSON strings. + +use wasm_bindgen::prelude::*; + +fn parse_or_error(src: &str) -> Result { + parser::parse(src).map_err(|errors| { + let msg = errors + .iter() + .map(|e| format!("{e:?}")) + .collect::>() + .join("; "); + format!("{{\"error\": \"{}\"}}", msg.replace('"', "'")) + }) +} + +/// Compile a `.bimr` source string to IFCX JSON. +/// +/// Returns a JSON string — either an IFCX document on success, or +/// `{"error": "..."}` on parse/eval failure. +#[wasm_bindgen] +pub fn compile(src: &str) -> String { + let program = match parse_or_error(src) { + Ok(p) => p, + Err(e) => return e, + }; + match eval::eval_program(&program, "model.bimr") { + Ok(store) => eval::to_ifcx(&store), + Err(e) => format!("{{\"error\": \"{e}\"}}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const WALL_SRC: &str = "\ +p1 = Point(0,0,0) +p2 = Point(500,0,0) +l1 = Line(p1,p2) +w1 = Wall(l1,20,300)"; + + #[test] + fn compile_wall_to_ifcx() { + let out = compile(WALL_SRC); + assert!(out.contains("wall-001"), "missing wall-001: {out}"); + assert!(out.contains("ifcx_alpha")); + assert!(out.contains("faceVertexIndices")); + } + + #[test] + fn compile_errors_are_error_json() { + let out = compile("w1 = Wall(missing)"); + assert!(out.contains("error"), "expected error json: {out}"); + } +}