1717// which oracle.
1818//
1919//## ABI SCOPE (read this)
20- // This runner handles the **scalar i32 ABI** only: every argument is an i32 and
21- // every result is an i32. That is exactly the boundary the DESIGN-VISION
22- // prescribes ("they pass primitives across the wasm boundary") and it covers
23- // pure-integer co-processors such as SecurityRank. Array/string parameters use
24- // a different convention -- `[len:i32 LE][utf8 bytes]` written into linear
25- // memory plus an exported `__affine_alloc` -- and are a deliberate follow-on,
26- // NOT implemented here. Exports taking [Int]/String params therefore can only be
27- // invoked here in their nullary form (e.g. Kernel_IO's `main`, whose array
28- // arguments are inlined inside the .affine source).
20+ // This runner handles i32 results over i32 / f64 / String arguments:
21+ // * i32 args -- swept as integer ranges/values (the original scalar ABI),
22+ // * f64 args -- any non-integer values in a `{ values: [...] }` spec are
23+ // passed through verbatim as wasm f64 (args are not coerced),
24+ // * String args -- written into linear memory as `[len:i32 LE][utf8 bytes]`
25+ // and passed BY POINTER (an i32). The string layout matches
26+ // the compiler's (codegen `gen_literal`). The oracle still
27+ // receives the original JS string, not the pointer.
28+ // Every result is read back as an i32 (`| 0`).
29+ //
30+ // String scratch memory: after instantiation the runner grows the module's
31+ // linear memory by one page (64 KiB) and writes string args into that fresh
32+ // page (reset before each call). Because the page sits above all of the
33+ // module's data and bump-heap base, a read-only string consumer (the common
34+ // case -- a classifier that reads its String arg and returns an Int) never
35+ // collides with it. A module that *allocates heavily during the call* before
36+ // reading its String arg could in principle reach the scratch page; such cases
37+ // should size their sweeps modestly or write a bespoke driver.
2938//
3039//## CONFIG SHAPE
3140// A config is a `.mjs` module with a default export:
@@ -126,11 +135,44 @@ function expandArgSpec(spec, paramIndex) {
126135 return out ;
127136 }
128137 if ( spec && Array . isArray ( spec . values ) ) return spec . values . slice ( ) ;
138+ if ( spec && Array . isArray ( spec . strings ) ) return spec . strings . slice ( ) ;
129139 throw new Error (
130- `arg spec #${ paramIndex } must be a number, a [lo,hi] range, or { values : [...] }` ,
140+ `arg spec #${ paramIndex } must be a number, a [lo,hi] range, { values: [...] }, or { strings : [...] }` ,
131141 ) ;
132142}
133143
144+ // Build a writer for String args. Grows the module's linear memory by one page
145+ // and bump-allocates `[len:i32 LE][utf8]` strings into it, 4-byte aligned, so
146+ // each is a valid AffineScript string the wasm can read by pointer. Returns
147+ // null when the module exports no memory (then String args are a usage error).
148+ function makeStringWriter ( memory ) {
149+ if ( ! ( memory instanceof WebAssembly . Memory ) ) return null ;
150+ const oldPages = memory . grow ( 1 ) ; // fresh page above all module data + heap
151+ const base = oldPages * 65536 ;
152+ const enc = new TextEncoder ( ) ;
153+ let bump = base ;
154+ return {
155+ reset ( ) {
156+ bump = base ;
157+ } ,
158+ write ( s ) {
159+ const u = enc . encode ( s ) ;
160+ const need = 4 + u . length ;
161+ // Grow if a long string would overrun the scratch page.
162+ if ( bump + need > memory . buffer . byteLength ) {
163+ memory . grow ( Math . ceil ( need / 65536 ) + 1 ) ;
164+ }
165+ const ptr = bump ;
166+ const dv = new DataView ( memory . buffer ) ; // re-read: grow detaches the buffer
167+ dv . setInt32 ( ptr , u . length , true ) ;
168+ new Uint8Array ( memory . buffer , ptr + 4 , u . length ) . set ( u ) ;
169+ bump = ptr + need ;
170+ bump += ( 4 - ( bump & 3 ) ) & 3 ; // 4-byte align the next string
171+ return ptr ;
172+ } ,
173+ } ;
174+ }
175+
134176// Cartesian product of a list of value-lists. [] -> [[]] (the single empty tuple).
135177function cartesian ( lists ) {
136178 return lists . reduce (
@@ -141,7 +183,7 @@ function cartesian(lists) {
141183
142184// Run a single case (one export + its arg sweep + its oracle). Returns
143185// { name, total, pass, failures: [{args, got, want}] }.
144- function runCase ( exports , kase , idx ) {
186+ function runCase ( exports , kase , idx , stringWriter ) {
145187 const name = kase . name || kase . export || `case#${ idx } ` ;
146188 const fn = exports [ kase . export ] ;
147189 if ( typeof fn !== "function" ) {
@@ -159,7 +201,22 @@ function runCase(exports, kase, idx) {
159201 let pass = 0 ;
160202 const failures = [ ] ;
161203 for ( const tuple of tuples ) {
162- const got = fn ( ...tuple ) | 0 ; // normalise to i32
204+ // String args are written into scratch linear memory as [len][utf8] and
205+ // passed by pointer; the oracle still sees the original JS string. i32 /
206+ // f64 args pass through unchanged.
207+ let wasmArgs = tuple ;
208+ if ( tuple . some ( ( v ) => typeof v === "string" ) ) {
209+ if ( ! stringWriter ) {
210+ throw new Error (
211+ `case "${ name } " passes a String arg but the module exports no "memory"` ,
212+ ) ;
213+ }
214+ stringWriter . reset ( ) ;
215+ wasmArgs = tuple . map ( ( v ) =>
216+ typeof v === "string" ? stringWriter . write ( v ) : v
217+ ) ;
218+ }
219+ const got = fn ( ...wasmArgs ) | 0 ; // normalise result to i32
163220 const want = kase . oracle ( ...tuple ) | 0 ;
164221 if ( got === want ) {
165222 pass ++ ;
@@ -206,12 +263,13 @@ export async function runParity(configPath, { verbose = false } = {}) {
206263 }
207264
208265 const exports = await instantiate ( outPath ) ;
266+ const stringWriter = makeStringWriter ( exports . memory ) ;
209267
210268 let grandTotal = 0 ;
211269 let grandPass = 0 ;
212270 const results = [ ] ;
213271 cfg . cases . forEach ( ( kase , i ) => {
214- const r = runCase ( exports , kase , i ) ;
272+ const r = runCase ( exports , kase , i , stringWriter ) ;
215273 results . push ( r ) ;
216274 grandTotal += r . total ;
217275 grandPass += r . pass ;
0 commit comments