wasm asm: read_string handles \" \\ \n \t escapes — bend payload works

The WAT $read_string function scanned bytes until the first " and copied
raw, with NO escape handling. So a source string like
"(\"00\" \"01\")" got chopped at the first \" — the asm tier read only
"(\\" before terminating, producing a mangled payload that the worker
couldn't parse. The bend-gpu demo's payload built via string-append of
escaped-quote strings came out as "(cuda-shake-fanout (\ \ \) 32)" in
the asm tier, sent garbage to bend, and got nothing useful back.

Two-pass fix to match Python/C tier behavior:
  1. Scan-and-count pass: walks source-ptr to the closing ", but
     when it sees \ it skips the next byte so embedded \" doesn't
     terminate the string. Counts decoded output bytes (each \X
     contributes one byte, not two).
  2. Allocate + copy-and-decode pass: walks the same range, converts
     \n → 0x0A, \t → 0x09, and any other \X (including \" and \\)
     → X. Matches the lenient fallback the desktop tiers use.

Verified via cross-tier parity probe — 255/255 still passing.
Demo payload now constructs as
"(cuda-shake-fanout (\"00\" \"01\" \"deadbeef\") 32)" (45 bytes,
identical to Python/C reads) and the asm playground returns
(ok (HEX0 HEX1 HEX2)) from the live 3090-ai bend worker.
This commit is contained in:
russell@unturf.com 2026-06-14 19:00:17 -04:00
parent 0cf66cc3dd
commit d672ab077e
No known key found for this signature in database
4 changed files with 43 additions and 13 deletions

View file

@ -1781,35 +1781,65 @@
(i32.const 1))
;; Read a string literal (we've already consumed the opening ").
;; Read a "..." string literal. Handles escape sequences inside the
;; literal: \" → " (so a string can contain a literal quote), \\ → \,
;; \n → newline, \t → tab. Any other \X falls back to literal X
;; (same fallback the Python/C tiers use). Two-pass: first pass
;; counts output bytes & locates closing quote, second copies the
;; payload performing escape conversion.
(func $read_string (result i32)
(local $start i32)
(local $len i32)
(local $end i32)
(local $bytes i32)
(local $s i32)
(local $i i32)
(local $src i32)
(local $dst i32)
(local $ch i32)
(local $next i32)
(local.set $start (global.get $source_ptr))
(local.set $bytes (i32.const 0))
;; First pass: scan to closing ", counting decoded output bytes.
(block $done
(loop $loop
(br_if $done (i32.ge_u (global.get $source_ptr) (global.get $source_end)))
(br_if $done
(i32.eq (i32.load8_u (global.get $source_ptr)) (i32.const 34)))
(local.set $ch (i32.load8_u (global.get $source_ptr)))
(br_if $done (i32.eq (local.get $ch) (i32.const 34))) ;; closing "
(if (i32.eq (local.get $ch) (i32.const 92)) ;; \
(then
(global.set $source_ptr (i32.add (global.get $source_ptr) (i32.const 1)))
(br_if $done (i32.ge_u (global.get $source_ptr) (global.get $source_end)))))
(local.set $bytes (i32.add (local.get $bytes) (i32.const 1)))
(global.set $source_ptr (i32.add (global.get $source_ptr) (i32.const 1)))
(br $loop)))
(local.set $len (i32.sub (global.get $source_ptr) (local.get $start)))
(local.set $end (global.get $source_ptr))
;; consume closing "
(if (i32.lt_u (global.get $source_ptr) (global.get $source_end))
(then (global.set $source_ptr (i32.add (global.get $source_ptr) (i32.const 1)))))
;; Allocate a string object (same layout as symbol but tag=5).
(local.set $s (call $alloc (i32.add (i32.const 8) (local.get $len))))
(local.set $s (call $alloc (i32.add (i32.const 8) (local.get $bytes))))
(i32.store (local.get $s) (i32.const 5))
(i32.store offset=4 (local.get $s) (local.get $len))
(local.set $i (i32.const 0))
(i32.store offset=4 (local.get $s) (local.get $bytes))
;; Second pass: copy with escape conversion.
(local.set $src (local.get $start))
(local.set $dst (i32.add (local.get $s) (i32.const 8)))
(block $cdone
(loop $cloop
(br_if $cdone (i32.ge_u (local.get $i) (local.get $len)))
(i32.store8
(i32.add (i32.add (local.get $s) (i32.const 8)) (local.get $i))
(i32.load8_u (i32.add (local.get $start) (local.get $i))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $cdone (i32.ge_u (local.get $src) (local.get $end)))
(local.set $ch (i32.load8_u (local.get $src)))
(if (i32.eq (local.get $ch) (i32.const 92)) ;; \
(then
(local.set $src (i32.add (local.get $src) (i32.const 1)))
(if (i32.lt_u (local.get $src) (local.get $end))
(then
(local.set $next (i32.load8_u (local.get $src)))
(local.set $ch
(if (result i32) (i32.eq (local.get $next) (i32.const 110)) (then (i32.const 10)) ;; \n
(else
(if (result i32) (i32.eq (local.get $next) (i32.const 116)) (then (i32.const 9)) ;; \t
(else (local.get $next)))))))))) ;; \" → ", \\ → \, \X → X
(i32.store8 (local.get $dst) (local.get $ch))
(local.set $src (i32.add (local.get $src) (i32.const 1)))
(local.set $dst (i32.add (local.get $dst) (i32.const 1)))
(br $cloop)))
(local.get $s))

Binary file not shown.