diff --git a/crates/cli/tests/fixtures/apollo.mjs b/crates/cli/tests/fixtures/apollo.mjs index 35aad76..b7fd189 100644 --- a/crates/cli/tests/fixtures/apollo.mjs +++ b/crates/cli/tests/fixtures/apollo.mjs @@ -54,7 +54,7 @@ if (fetched.url !== "https://example.test/") { } const request = { url: "https://example.test/" }; -await init(new URL("./app.wasm", import.meta.url), { +const exports = await init(new URL("./app.wasm", import.meta.url), { "regulus/js": { request_text(input) { return `${input} from JS`; @@ -71,15 +71,30 @@ await init(new URL("./app.wasm", import.meta.url), { }, }); +const initialMark = exports.__regulus_arena_mark(); const result = callString("main", "hello"); if (result !== "hello from JS") { throw new Error(`unexpected result: ${result}`); } +if (exports.__regulus_arena_mark() !== initialMark) { + throw new Error("adapter callString did not reset the arena"); +} + +const secondResult = callString("main", "again"); +if (secondResult !== "again from JS") { + throw new Error(`unexpected second result: ${secondResult}`); +} +if (exports.__regulus_arena_mark() !== initialMark) { + throw new Error("repeated adapter callString did not reuse arena memory"); +} const described = call("describe_from_js", 7n, 2.5, true, "shape"); if (described !== "shape:7:2.5:true") { throw new Error(`unexpected described result: ${described}`); } +if (exports.__regulus_arena_mark() !== initialMark) { + throw new Error("adapter call did not reset scalar/string argument allocations"); +} const kept = call("keep_bool", true); if (kept !== true) { @@ -90,6 +105,9 @@ const response = call("response"); if (response.tag !== "Response" || response.fields.status !== 200n || response.fields.body !== "ok") { throw new Error(`unexpected response: ${response.tag}`); } +if (exports.__regulus_arena_mark() !== initialMark) { + throw new Error("adapter call did not reset structured return allocations"); +} const names = call("names"); if (names.join(",") !== "Ada,Joe") { diff --git a/crates/core/src/adapter.js b/crates/core/src/adapter.js index b18b19b..417ee42 100644 --- a/crates/core/src/adapter.js +++ b/crates/core/src/adapter.js @@ -213,8 +213,13 @@ export function callExport(name, paramTypes = [], returnType = "Nil", ...args) { `Regulus export "${name}" expects ${paramTypes.length} argument(s), got ${args.length}`, ); } - const wasmArgs = args.map((arg, index) => toWasmValue(paramTypes[index], arg)); - return fromWasmValue(returnType, fn(...wasmArgs)); + const mark = instance.exports.__regulus_arena_mark(); + try { + const wasmArgs = args.map((arg, index) => toWasmValue(paramTypes[index], arg)); + return fromWasmValue(returnType, fn(...wasmArgs)); + } finally { + instance.exports.__regulus_arena_reset(mark); + } } /** diff --git a/crates/core/src/wasm/codegen.rs b/crates/core/src/wasm/codegen.rs index b380e00..2dd0ca8 100644 --- a/crates/core/src/wasm/codegen.rs +++ b/crates/core/src/wasm/codegen.rs @@ -371,6 +371,8 @@ impl<'a> StructuredEmitter<'a> { fn emit_js_host_abi_helpers(&mut self) { self.ensure_memory(); self.runtime_helper_roots.insert("__alloc".into()); + self.runtime_helper_roots.insert("__arena_mark".into()); + self.runtime_helper_roots.insert("__arena_reset".into()); self.runtime_helper_roots.insert("__string_new".into()); self.runtime_helper_roots.insert("__string_len".into()); self.runtime_helper_roots.insert("__string_data".into()); @@ -382,6 +384,15 @@ impl<'a> StructuredEmitter<'a> { call $__alloc ) (export "__regulus_alloc" (func $__regulus_alloc)) + (func $__regulus_arena_mark (result i32) + call $__arena_mark + ) + (export "__regulus_arena_mark" (func $__regulus_arena_mark)) + (func $__regulus_arena_reset (param $mark i32) + local.get $mark + call $__arena_reset + ) + (export "__regulus_arena_reset" (func $__regulus_arena_reset)) (func $__regulus_string_new (param $data i32) (param $len i32) (result i32) local.get $data local.get $len diff --git a/crates/core/src/wasm/tests.rs b/crates/core/src/wasm/tests.rs index 0c57309..91474a4 100644 --- a/crates/core/src/wasm/tests.rs +++ b/crates/core/src/wasm/tests.rs @@ -874,6 +874,8 @@ pub fn main(input: String) -> String { request_text(input) }"#, ); for name in [ "__regulus_alloc", + "__regulus_arena_mark", + "__regulus_arena_reset", "__regulus_string_new", "__regulus_string_len", "__regulus_string_data", diff --git a/docs/website/development/runtime-memory.md b/docs/website/development/runtime-memory.md index c085702..0ad0b4b 100644 --- a/docs/website/development/runtime-memory.md +++ b/docs/website/development/runtime-memory.md @@ -123,9 +123,15 @@ Reset invalidates every dynamic object allocated after the mark. Static data and dynamic objects allocated before the mark remain valid. Later allocations may reuse reset-owned memory. -Automatic call/request reset boundaries are not yet generated. Host adapters -and compiler-generated code must not return or retain pointers allocated after -a mark that will be reset. +Generated JavaScript adapters wrap exported Gleam calls in an arena scope. The +adapter marks before encoding JS arguments, calls the Wasm export, decodes the +return into JS-owned data, and resets in a `finally` block. Raw Wasm and +Wasmtime exports are not automatically reset because those callers may inspect +borrowed managed pointers after the call. + +Compiler-generated code must not return or retain pointers allocated after a +mark that will be reset. General internal reset scopes still require escape +analysis or region tracking. Reference counting is not the selected strategy for this milestone. It would require generated retain/release operations for every managed assignment, field