lumbda/tests/portal-formats.lsp
russell@unturf.com 57f3c9fab1 asm/c/py: add (load), ports, write-file/file->string — full cross-impl parity
Asm gains the file I/O surface Python and C already had, unlocking
9/9 cells of the portal producer×consumer matrix (previously 6/9).

asm:
- (load "path") — mmaps file, swaps input source, loops scheme_read+eval,
  restores on exit. Nestable. Uses SYS_LSEEK + SYS_MUNMAP.
- Output ports: (open-output-file), (close-port), (port?). Encoded as
  SPECIAL values ≥ 1000 (fd = (val>>3) − PORT_SPECIAL_BASE), no tag-bit
  expansion needed.
- (display), (write), (newline) accept optional port arg; printer
  writes via output_fd global, swapped by port-aware builtins.
- (write-file path content) / (file->string path) — bytes in/out.

c, py: (write-file) / (file->string) added for parity.

tests: 131 asm (up 23), 189 functional (up 8, shared py+c),
tests/portal-cross-test.sh exercises 3×3 save×load matrix.
2026-04-16 16:37:40 -04:00

139 lines
6.4 KiB
Text

;;; portal-formats.lsp — Benchmark three portal formats
;;;
;;; 1. S-expression (portable — any implementation can read)
;;; 2. JSON (Python + C)
;;; 3. Binary heap dump (assembly only, fastest)
;;;
;;; Run in Python: python3 uncommonlisp.py --fast tests/portal-formats.lsp
;;; Run in C: ./c/uncommonlisp tests/portal-formats.lsp
;;; Asm: asm/uncommonlisp < tests/portal-cross-load.lsp (no `load` builtin)
;;;
;;; ── Observed results (2026-04-16, same laptop) ─────────────────
;;;
;;; | save time | load time | file size
;;; Python S-exp | ~0.55 ms | ~0.38 ms | 310 B
;;; Python JSON | ~3.07 ms | (n/a) | 25,855 B
;;; C S-exp | ~0.17 ms | ~0.03 ms | 310 B
;;; Asm binary | ~0.08 ms | ~0.08 ms | ~10 KB
;;;
;;; Insight: S-expression is ~80x smaller than JSON for the same
;;; state, and every implementation already has a parser for it —
;;; because the language IS the interchange format.
;;;
;;; Cross-impl verified: Python→C, C→Python, Python→asm, C→asm
;;; (see tests/portal-cross-save.lsp + portal-cross-load.lsp)
;;; ═══════════════════════════════════════════════════════════════
;;; Setup: create some state to serialize
;;; ═══════════════════════════════════════════════════════════════
(define my-int 42)
(define my-list (list 1 2 3 4 5 6 7 8 9 10))
(define my-string "hello world")
(define my-nested (list (list 1 2) (list 3 4) (list 5 6)))
(define my-alist (list (cons 'a 1) (cons 'b 2) (cons 'c 3)))
(define (my-fib n)
(let loop ((a 0) (b 1) (i 0))
(if (= i n) a (loop b (+ a b) (+ i 1)))))
(define my-result (my-fib 30))
;;; ═══════════════════════════════════════════════════════════════
;;; Format 1: S-expression (portable)
;;; Write values as Scheme (define ...) forms. Any impl can (load) it.
;;; ═══════════════════════════════════════════════════════════════
(define (write-sexp-portal filename)
(let ((port (open-output-file filename)))
;; Header
(display ";; uncommonlisp portable state (S-expression format)" port)
(newline port)
(display ";; resume: (load \"" port)
(display filename port)
(display "\")" port)
(newline port)
(newline port)
;; Values
(display "(define my-int " port) (write my-int port) (display ")" port) (newline port)
(display "(define my-list '" port) (write my-list port) (display ")" port) (newline port)
(display "(define my-string " port) (write my-string port) (display ")" port) (newline port)
(display "(define my-nested '" port) (write my-nested port) (display ")" port) (newline port)
(display "(define my-alist (list" port)
(for-each (lambda (p)
(display " (cons '" port) (write (car p) port)
(display " " port) (write (cdr p) port) (display ")" port))
my-alist)
(display "))" port) (newline port)
(display "(define my-result " port) (write my-result port) (display ")" port) (newline port)
(close-port port)))
(define t0 (current-time))
(write-sexp-portal "/tmp/state.sexp")
(define t1 (current-time))
(display "S-expression save: ")
(display (* (- t1 t0) 1000))
(display "ms") (newline)
;;; ═══════════════════════════════════════════════════════════════
;;; Verify: load it back
;;; ═══════════════════════════════════════════════════════════════
;; Clear the values
(set! my-int 0)
(set! my-list '())
(set! my-string "")
(set! my-result 0)
(define t2 (current-time))
(load "/tmp/state.sexp")
(define t3 (current-time))
(display "S-expression load: ")
(display (* (- t3 t2) 1000))
(display "ms") (newline)
;; Verify
(display "Verify: my-int=") (display my-int)
(display " my-result=") (display my-result)
(display " my-list=") (display my-list)
(newline)
(if (and (= my-int 42) (= my-result 832040) (= (length my-list) 10))
(display "PASS: S-expression portal round-trip")
(display "FAIL: S-expression portal round-trip"))
(newline)
;;; ═══════════════════════════════════════════════════════════════
;;; Format 2: JSON (via portal-save builtin if available)
;;; Python binds `portal-save` directly. C binds `portal-save!` as a
;;; VM-level checkpoint (deferred save). We probe by procedure? so this
;;; stays portable and never invokes an unbound callable.
;;; ═══════════════════════════════════════════════════════════════
(define json-saver
(guard (e (#t #f))
(if (procedure? portal-save) portal-save #f)))
(if json-saver
(let ((t4 (current-time)))
(json-saver "/tmp/state.json")
(let ((t5 (current-time)))
(display "JSON save: ")
(display (* (- t5 t4) 1000))
(display "ms") (newline)))
(display "JSON save: skipped (no direct portal-save in this impl)\n"))
;;; ═══════════════════════════════════════════════════════════════
;;; Summary
;;; ═══════════════════════════════════════════════════════════════
(newline)
(display "Portal format comparison:") (newline)
(display " S-expression: portable — every impl parses it") (newline)
(display " JSON: Python + C, graph-aware, preserves sharing") (newline)
(display " Binary: asm only — raw heap dump, same-arch restore") (newline)
(newline)
(display "Files left at /tmp/state.sexp and /tmp/state.json for size comparison.") (newline)
(display "Run `wc -c /tmp/state.*` in shell to compare sizes.") (newline)