lumbda/tests/portal-formats.lsp
russell@unturf.com f7352b51b0 rename: uncommonlisp -> lumbda throughout the repo
Historical internal name "uncommonlisp" retired in favor of the
public name "lumbda" ahead of lumbda.com going live. Scope of
this commit:

Source files renamed:
  uncommonlisp.py                     -> lumbda.py
  asm/uncommonlisp.s                  -> asm/lumbda.s
  c/uncommonlisp.h                    -> c/lumbda.h
  whitepaper/uncommonlisp-whitepaper  -> whitepaper/lumbda-whitepaper (.rst + .pdf)

Binaries renamed (tracked ones; c/ was always gitignored):
  asm/uncommonlisp, asm/uncommonlisp-gc, asm/uncommonlisp.o,
  asm/uncommonlisp-gc.o                -> asm/lumbda(-gc)(.o)
  c/.gitignore                          -> ignores lumbda

Internal string updates (sed pass ordered longest-first):
  asm/uncommonlisp -> asm/lumbda
  c/uncommonlisp   -> c/lumbda
  uncommonlisp.py  -> lumbda.py
  UNCOMMONLISP_BIN -> LUMBDA_BIN (asm/test.sh env var)
  "uncommonlisp> " -> "lumbda> " (asm REPL prompt baked into binary)
  UNCOMMONLISP     -> LUMBDA (macros, comments)
  uncommonlisp     -> lumbda (prose)

Binary portal magic updated:
  "ULPORTAL" -> "LUMBDAB1"   # "Lumbda Binary v1"
Old portal files are not backward-compatible — this is a deliberate
break since it's the rename moment. S-expression portals already
carry their own ";; lumbda-portal v1" header and remain cleanly
versioned.

WHITEPAPER.pdf / WHITEPAPER.rst symlinks repointed to the renamed
files. Makefile's whitepaper target targets lumbda-whitepaper.pdf.

Not changed (intentional, separate phases):
  - Filesystem directory /home/fox/git/uncommonlisp itself
    (fox renames locally and the gitlab repo URL in a follow-up)
  - tests.py hardcoded cwd=/home/fox/git/uncommonlisp
    (matches the current on-disk location; will flip when the
    directory rename ships)
  - Git history (immutable; old commits still say uncommonlisp,
    which is correct — that's what they were)

Verified:
  137 asm no-GC + 137 asm GC + 571 Python + 83 C + 189 shared
  functional tests all pass under the new names.
  bench-gc-http (2000 req): all 4 cells behave as expected
  (cells 1/2 flat, 3 leaks, 4 bounded at 1 chunk).
  Python REPL, C REPL, asm REPL all start cleanly.
2026-04-19 10:20:11 -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 lumbda.py --fast tests/portal-formats.lsp
;;; Run in C: ./c/lumbda tests/portal-formats.lsp
;;; Asm: asm/lumbda < 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 ";; lumbda 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)