asm-gc portal v1: explicit version header + resume-side version check
GC-build portal files now start with ";; lumbda-portal v1\n". The line is a Scheme comment the reader already skips, so loading a v1 file via bi_load works unchanged. What's new is that portal-resume actively validates the header before delegating to bi_load: file starts with ";; lumbda-portal v1\n" -> load normally file starts with ";;" but different text -> return #f (rejected) file does not start with ";;" at all -> load as legacy (back-compat) The check reads the first 32 bytes of the file, compares the first two bytes against ";;", and on match compares the full 20-byte v1 prefix. Closes the versioning-friction concern a SEW reviewer raised after reading §7.4.1: we can now add a v2 format with new syntax (complex numbers, records, whatever) without older consumers silently parsing new files into garbage — they will cleanly return #f. Verified: v1 portal -> resume loads all bindings, returns #<void> v2 portal -> resume returns #f without evaluating any forms legacy portal (no ;; header) -> resume loads normally missing file -> resume returns #f The deeper framing worth writing down: this is a migration format for handoff across process / tier / machine, not an archive format. If archival becomes a real use case it earns its own format with proper schema evolution and a builtin-rename table. The v1 tag is the minimum hook that lets v2 happen cleanly when someone needs it. 137 asm no-GC + 137 asm GC still pass. §6.6.4 HTTP cells still green at 10K: GC + no-snapshot at 543 req/s, peak 1.1 MB, 972 KB growth (one chunk, steady state).
This commit is contained in:
parent
a606b6087e
commit
9e54c7aaf1
5 changed files with 477 additions and 358 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -416,6 +416,14 @@ s_rparen_nl: .ascii ")\n"
|
|||
s_dotsp: .ascii " . "
|
||||
s_minus: .ascii "-"
|
||||
s_hashparen: .ascii "#("
|
||||
# Portal v1 header. GC-build portal-save emits this as the first
|
||||
# line of every output file; portal-resume reads the first bytes
|
||||
# and verifies the prefix before loading. A file starting with ";;"
|
||||
# that does NOT match is rejected as an unsupported version. A file
|
||||
# that does not start with ";;" at all is accepted as legacy
|
||||
# (pre-v1) and loaded normally for back-compat.
|
||||
s_portal_v1: .ascii ";; lumbda-portal v1\n"
|
||||
.equ s_portal_v1_len, 20
|
||||
|
||||
# Interned special form symbols (filled at init)
|
||||
.align 8
|
||||
|
|
@ -4992,6 +5000,14 @@ bi_portal_save:
|
|||
js .ps_gc_fail
|
||||
movq %rax, %rbx # fd
|
||||
|
||||
# Emit v1 header. resume() will read this first line and bail
|
||||
# if it doesn't match, letting us evolve the format cleanly.
|
||||
movq $SYS_WRITE, %rax
|
||||
movq %rbx, %rdi
|
||||
leaq s_portal_v1(%rip), %rsi
|
||||
movq $s_portal_v1_len, %rdx
|
||||
syscall
|
||||
|
||||
# Redirect printer to the portal file.
|
||||
movq output_fd(%rip), %r12 # save caller's fd
|
||||
movq %rbx, output_fd(%rip)
|
||||
|
|
@ -5052,11 +5068,103 @@ bi_portal_save:
|
|||
.endif
|
||||
|
||||
.ifdef GC_NAIVE
|
||||
# GC build: portal-resume is equivalent to (load "filename") since
|
||||
# the GC portal format is S-expressions. No magic header, no binary
|
||||
# heap dump to splice back in — just read+eval every form.
|
||||
# GC build: portal-resume reads the v1 header, then delegates to
|
||||
# bi_load for the actual form-by-form evaluation. Header check
|
||||
# lets us reject unknown future versions cleanly instead of
|
||||
# getting a mysterious parse error on new syntax.
|
||||
#
|
||||
# Policy:
|
||||
# file starts with ";; lumbda-portal v1\n" -> load
|
||||
# file starts with ";;" but different -> VAL_FALSE (reject)
|
||||
# file does not start with ";;" at all -> load as legacy
|
||||
bi_portal_resume:
|
||||
# Peek at arg without consuming %r12 — bi_load needs its own
|
||||
# GETARG to re-read the filename after we've validated the
|
||||
# header.
|
||||
movq %r12, %rax
|
||||
andq $-8, %rax
|
||||
movq (%rax), %rbx # filename (tagged string)
|
||||
andq $-8, %rbx
|
||||
movq (%rbx), %rcx # string length
|
||||
leaq 8(%rbx), %rsi # bytes
|
||||
|
||||
# Null-terminate on stack (256-byte slot).
|
||||
subq $256, %rsp
|
||||
movq %rsp, %rdi
|
||||
movq %rcx, %rdx
|
||||
.prg_cp:
|
||||
testq %rdx, %rdx
|
||||
jz .prg_cp_done
|
||||
movb (%rsi), %al
|
||||
movb %al, (%rdi)
|
||||
incq %rsi
|
||||
incq %rdi
|
||||
decq %rdx
|
||||
jmp .prg_cp
|
||||
.prg_cp_done:
|
||||
movb $0, (%rdi)
|
||||
|
||||
# Open read-only.
|
||||
movq $SYS_OPEN, %rax
|
||||
movq %rsp, %rdi
|
||||
movq $O_RDONLY, %rsi
|
||||
xorq %rdx, %rdx
|
||||
syscall
|
||||
addq $256, %rsp
|
||||
testq %rax, %rax
|
||||
js .prg_fail
|
||||
movq %rax, %rbx # fd
|
||||
|
||||
# Read up to 32 bytes to inspect the header.
|
||||
subq $32, %rsp
|
||||
movq $SYS_READ, %rax
|
||||
movq %rbx, %rdi
|
||||
movq %rsp, %rsi
|
||||
movq $32, %rdx
|
||||
syscall
|
||||
# rax = bytes actually read. If <2 we can't tell what it is — accept.
|
||||
cmpq $2, %rax
|
||||
jl .prg_close_accept
|
||||
# Is this a comment line (starts with ";;")?
|
||||
movzwl (%rsp), %eax
|
||||
cmpl $0x3b3b, %eax # ";;"
|
||||
jne .prg_close_accept # no header marker — legacy file, accept
|
||||
# Header present. Must match s_portal_v1 exactly for the first
|
||||
# s_portal_v1_len bytes.
|
||||
leaq s_portal_v1(%rip), %rdi
|
||||
movq %rsp, %rsi
|
||||
movq $s_portal_v1_len, %rcx
|
||||
.prg_match:
|
||||
movb (%rdi), %al
|
||||
cmpb (%rsi), %al
|
||||
jne .prg_bad_version
|
||||
incq %rdi
|
||||
incq %rsi
|
||||
decq %rcx
|
||||
jnz .prg_match
|
||||
# Fall through — header matches.
|
||||
|
||||
.prg_close_accept:
|
||||
addq $32, %rsp
|
||||
movq $SYS_CLOSE, %rax
|
||||
movq %rbx, %rdi
|
||||
syscall
|
||||
# Delegate to bi_load, which re-opens the file and reads every
|
||||
# form. The v1 header itself is a ";; comment" line the reader
|
||||
# already skips.
|
||||
jmp bi_load
|
||||
|
||||
.prg_bad_version:
|
||||
addq $32, %rsp
|
||||
movq $SYS_CLOSE, %rax
|
||||
movq %rbx, %rdi
|
||||
syscall
|
||||
movq $VAL_FALSE, %rax
|
||||
RET_VAL
|
||||
|
||||
.prg_fail:
|
||||
movq $VAL_FALSE, %rax
|
||||
RET_VAL
|
||||
.endif
|
||||
|
||||
.ifndef GC_NAIVE
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -786,6 +786,8 @@ Every non-builtin, non-closure binding round-trips. Closures and builtins are sk
|
|||
|
||||
**Trade-off.** The GC build's portal is slower than the binary dump (walks each binding through the printer) and stricter about what it can preserve (data only, no closures or continuations). In exchange: **every portal produced by the GC build resumes on every other tier**, with no MAP_FIXED trick, no architecture constraint, no "same binary" requirement. A GC-asm producer can hand a portal to a Python consumer — we added the asm-gc column to the §7.2 matrix and all 16 cells are green.
|
||||
|
||||
**Version tag.** Every v1 portal starts with a single comment line — ``;; lumbda-portal v1`` — that Scheme readers already skip but the resume path actively parses. The matching policy: a file beginning with ``;;`` is required to match the v1 prefix exactly, or ``portal-resume`` returns ``#f`` instead of trying to evaluate forms that may use syntax the current reader doesn't understand. A file that doesn't begin with ``;;`` at all is accepted as legacy (pre-v1) for back-compat. The S-expression portal is a **migration format**, not an archive format — it exists to hand live data across a process boundary at handoff time, not to carry state across years of language evolution. When we need the latter, it earns its own format with proper schema evolution and a builtin-rename mapping table; the current portal stays simple and the version tag is the hook that lets v2 happen cleanly when someone actually needs it.
|
||||
|
||||
7.5 Cross-Process Benchmarks
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue