boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit 6ff8a39efb43086b165a90851556c3e44e185295
parent ab60ce5e5c26d78e7328d3a8f7f99c54f3142e00
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 25 Apr 2026 09:10:26 -0700

Add scheme1 define-record-type and parameterized PRIM data slot

PRIM grows from 16 bytes to 24: header word, entry-label word, and a
new per-instance data slot (offset 13 from the tagged pointer). apply's
PRIM dispatch now passes the prim itself in a1 alongside args in a0,
so a parameterized PRIM can read the data word and dispatch on a
specific TD or field index without indirecting through s-expressions.

define-record-type allocates one TD plus one PRIM per ctor / predicate /
accessor / mutator name, binding each name's global directly:

  ctor     -- data = TD; allocates a fresh REC with the args inline
  pred     -- data = TD; checks tag + HDR.REC + matching td slot
  accessor -- data = tagged field index; reads rec[idx]
  mutator  -- data = tagged field index; writes rec[idx]

The original `%record-*` internal primitives are also exposed in
prim_table for direct use, parallel to the parameterized entries.
Plain primitives never touch the data slot and stay zero-initialized.

Diffstat:
Mscheme1/scheme1.P1pp | 394++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atests/scheme1/35-record-basic.expected-exit | 1+
Atests/scheme1/35-record-basic.scm | 9+++++++++
Atests/scheme1/36-record-mutator.expected-exit | 1+
Atests/scheme1/36-record-mutator.scm | 10++++++++++
Atests/scheme1/37-record-predicate.expected-exit | 1+
Atests/scheme1/37-record-predicate.scm | 11+++++++++++
Atests/scheme1/38-record-internal-prims.expected-exit | 1+
Atests/scheme1/38-record-internal-prims.scm | 7+++++++
9 files changed, 430 insertions(+), 5 deletions(-)

diff --git a/scheme1/scheme1.P1pp b/scheme1/scheme1.P1pp @@ -46,8 +46,10 @@ # Layout helpers. %struct stride is 8 bytes per field. %struct PAIR { car cdr } # .SIZE = 16 %struct SYMENT { name_ptr name_len global_val pad } # .SIZE = 32 -%struct PRIM { hdr entry_w } # .SIZE = 16 +%struct PRIM { hdr entry_w data } # .SIZE = 24 %struct CLOSURE { hdr params body env } # .SIZE = 32 +%struct TD { hdr name nfields } # .SIZE = 24 +# Records are variable width: header + td slot + N field slots. # BSS arena offsets from :ELF_end. Each arena is 64 KiB; the three are # packed back-to-back below ELF_end + 192 KiB. p1_main's startup loop @@ -819,6 +821,7 @@ %dispatch_form(&sym_let, &::do_let) %dispatch_form(&sym_letstar, &::do_letstar) %dispatch_form(&sym_letrec, &::do_letrec) + %dispatch_form(&sym_define_record_type, &::do_define_record_type) # head = eval(car(expr), env) %ld(a0, sp, 0) @@ -861,6 +864,8 @@ %tail_to_handler(&eval_letstar) ::do_letrec %tail_to_handler(&eval_letrec) + ::do_define_record_type + %tail_to_handler(&eval_define_record_type) }) # eval_args(args=a0, env=a1) -> evaluated args list (cons-built). @@ -916,8 +921,12 @@ %die(msg_not_proc, 8) ::prim - %ld(t0, a0, 5) ; t0 = entry word (offset = -3 + 8) - %ld(a0, sp, 0) ; args list -> a0 + # Primitives are called with args list in a0 and the prim ptr itself + # in a1, so parameterized prims (eg record accessors) can read their + # data slot via a1+13. Plain primitives ignore a1. + %mov(a1, a0) + %ld(t0, a0, 5) + %ld(a0, sp, 0) %tailr(t0) ::closure @@ -960,6 +969,7 @@ %intern_form(&name_let, 3, &sym_let) %intern_form(&name_letstar, 4, &sym_letstar) %intern_form(&name_letrec, 6, &sym_letrec) + %intern_form(&name_define_record_type, 18, &sym_define_record_type) }) # eval_if(rest=a0, env=a1) -> value (a0). `rest` is (test then else). @@ -1602,8 +1612,11 @@ %ld(t1, sp, 16) %beq(t0, t1, &::done) - # alloc_hdr(16, HDR.PRIM) -> HEAP-tagged a0. - %li(a0, 16) + # alloc_hdr(24, HDR.PRIM) -> HEAP-tagged a0. The third slot (offset 13 + # from tagged) holds per-instance data and stays zero for the + # primitives registered here -- only parameterized prims (record + # ctor/predicate/accessor/mutator) read it. + %li(a0, 24) %li(a1, %HDR.PRIM) %call(&alloc_hdr) %st(a0, sp, 0) @@ -2129,6 +2142,363 @@ %car(a0, a0) }) +# Records: TDs (type descriptors) and instances. A TD is a 24-byte heap +# object [HDR.TD][name_sym][nfields_raw]. A record is a variable-width +# heap object [HDR.REC][td][field_0]...[field_{n-1}], so field i lives at +# tagged + 13 + 8*i. define-record-type allocates one TD plus one +# parameterized PRIM per ctor/predicate/accessor/mutator, all pointing +# into the same TD via the prim's data slot. + +# make_param_prim(entry=a0, data=a1) -> prim (a0). Allocates a 24-byte +# PRIM, sets the entry label and data word. +%fn(make_param_prim, 16, { + %st(a0, sp, 0) + %st(a1, sp, 8) + + %li(a0, 24) + %li(a1, %HDR.PRIM) + %call(&alloc_hdr) + + %ld(t0, sp, 0) + %st(t0, a0, 5) + %ld(t1, sp, 8) + %st(t1, a0, 13) +}) + +# (%make-record-td name nfields) -- args = (name nfields). +%fn(prim_make_record_td_entry, 16, { + %st(a0, sp, 0) + %li(a0, 24) + %li(a1, %HDR.TD) + %call(&alloc_hdr) + + %ld(t0, sp, 0) + %car(t1, t0) ; name sym + %st(t1, a0, 5) + %cdr(t0, t0) + %car(t0, t0) + %sari(t0, t0, 3) ; raw nfields + %st(t0, a0, 13) +}) + +# (%make-record td f0 f1 ...) -- args = (td . fields). +%fn(prim_make_record_entry, 24, { + %st(a0, sp, 0) + + # Count fields = length(args) - 1. We'll use the args walker for the + # actual fill, just count to size the allocation. + %cdr(t0, a0) + %li(t1, 0) + ::count_loop + %li(t2, %imm_val(%IMM.NIL)) + %beq(t0, t2, &::count_done) + %addi(t1, t1, 1) + %cdr(t0, t0) + %b(&::count_loop) + ::count_done + + %shli(a0, t1, 3) + %addi(a0, a0, 16) + %li(a1, %HDR.REC) + %call(&alloc_hdr) + %st(a0, sp, 8) + + # td (offset +5) = car(args) + %ld(t0, sp, 0) + %car(t0, t0) + %st(t0, a0, 5) + + # Walk fields, filling +13, +21, ... + %ld(t0, sp, 0) + %cdr(t0, t0) + %addi(t1, a0, 13) + + ::fill_loop + %li(t2, %imm_val(%IMM.NIL)) + %beq(t0, t2, &::fill_done) + %car(t2, t0) + %st(t2, t1, 0) + %addi(t1, t1, 8) + %cdr(t0, t0) + %b(&::fill_loop) + + ::fill_done + %ld(a0, sp, 8) +}) + +# (%record-ref rec idx) +:prim_record_ref_entry + %car(t0, a0) ; rec + %cdr(t1, a0) + %car(t1, t1) ; tagged idx (= idx << 3) + %addi(t1, t1, 13) + %add(t1, t1, t0) + %ld(a0, t1, 0) + %ret + +# (%record-set! rec idx val) +:prim_record_set_entry +%scope prim_record_set + %car(t0, a0) + %cdr(t1, a0) + %car(t2, t1) ; tagged idx + %cdr(t1, t1) + %car(t1, t1) ; val + %addi(t2, t2, 13) + %add(t2, t2, t0) + %st(t1, t2, 0) + %li(a0, %imm_val(%IMM.UNSPEC)) + %ret +%endscope + +# (%record-is-a? rec td) +:prim_record_is_a_entry +%scope prim_record_is_a + %car(t0, a0) + %cdr(t1, a0) + %car(t1, t1) + %tagof(t2, t0) + %li(a0, %imm_val(%IMM.FALSE)) + %li(a2, %TAG.HEAP) + %bne(t2, a2, &::end) + %hdr_type(t2, t0) + %li(a2, %HDR.REC) + %bne(t2, a2, &::end) + %ld(t2, t0, 5) + %bne(t2, t1, &::end) + %li(a0, %imm_val(%IMM.TRUE)) + ::end + %ret +%endscope + +# (%record-td rec) -- returns rec's TD pointer. +:prim_record_td_entry + %car(t0, a0) + %ld(a0, t0, 5) + %ret + +# Parameterized PRIM entries used by define-record-type. Each receives +# args in a0 and the prim itself in a1; the prim's data slot (offset 13 +# from tagged) holds either the TD or a tagged field index. + +# ctor: prim.data = TD (HEAP); args = (f0 f1 ...). Inlines the +# %make-record body so we don't have to cons (TD . args) first. +%fn(prim_ctor_entry, 24, { + # +0 args + # +8 td (from prim.data) + # +16 record + %st(a0, sp, 0) + %ld(t0, a1, 13) + %st(t0, sp, 8) + + # Count = length(args). + %ld(t1, sp, 0) + %li(t2, 0) + ::count_loop + %li(a2, %imm_val(%IMM.NIL)) + %beq(t1, a2, &::count_done) + %addi(t2, t2, 1) + %cdr(t1, t1) + %b(&::count_loop) + ::count_done + + %shli(a0, t2, 3) + %addi(a0, a0, 16) + %li(a1, %HDR.REC) + %call(&alloc_hdr) + %st(a0, sp, 16) + + %ld(t0, sp, 8) + %st(t0, a0, 5) + + %ld(t0, sp, 0) + %addi(t1, a0, 13) + + ::fill_loop + %li(t2, %imm_val(%IMM.NIL)) + %beq(t0, t2, &::fill_done) + %car(t2, t0) + %st(t2, t1, 0) + %addi(t1, t1, 8) + %cdr(t0, t0) + %b(&::fill_loop) + + ::fill_done + %ld(a0, sp, 16) +}) + +# predicate: prim.data = TD; args = (rec). +:prim_predicate_entry +%scope prim_predicate + %car(t0, a0) + %ld(t1, a1, 13) + %tagof(t2, t0) + %li(a0, %imm_val(%IMM.FALSE)) + %li(a2, %TAG.HEAP) + %bne(t2, a2, &::end) + %hdr_type(t2, t0) + %li(a2, %HDR.REC) + %bne(t2, a2, &::end) + %ld(t2, t0, 5) + %bne(t2, t1, &::end) + %li(a0, %imm_val(%IMM.TRUE)) + ::end + %ret +%endscope + +# accessor: prim.data = tagged field index; args = (rec). +:prim_accessor_entry + %car(t0, a0) + %ld(t1, a1, 13) + %addi(t1, t1, 13) + %add(t1, t1, t0) + %ld(a0, t1, 0) + %ret + +# mutator: prim.data = tagged field index; args = (rec val). +:prim_mutator_entry +%scope prim_mutator + %car(t0, a0) + %cdr(t1, a0) + %car(t1, t1) + %ld(t2, a1, 13) + %addi(t2, t2, 13) + %add(t2, t2, t0) + %st(t1, t2, 0) + %li(a0, %imm_val(%IMM.UNSPEC)) + %ret +%endscope + +# eval_define_record_type(rest=a0, env=a1) -> UNSPEC. +# rest = (name (ctor f1 ...) pred clause1 clause2 ...) +# Each clause is (field-name accessor) or (field-name accessor mutator). +# Allocates one TD + one parameterized PRIM per name introduced (ctor, +# predicate, accessor, mutator) and binds each to the symbol's global. +# +# Frame: 56 bytes +# +0 rest +# +8 env (unused, but the dispatcher passes it) +# +16 td +# +24 walk (clauses, advancing) +# +32 idx (raw counter) +# +40 nfields +%fn(eval_define_record_type, 56, { + %st(a0, sp, 0) + %st(a1, sp, 8) + + # clauses = cdddr(rest); count them while we're at it. + %ld(t0, sp, 0) + %cdr(t0, t0) + %cdr(t0, t0) + %cdr(t0, t0) + %st(t0, sp, 24) + %li(t1, 0) + ::count_loop + %li(t2, %imm_val(%IMM.NIL)) + %beq(t0, t2, &::count_done) + %addi(t1, t1, 1) + %cdr(t0, t0) + %b(&::count_loop) + ::count_done + %st(t1, sp, 40) + + # td = alloc_hdr(24, HDR.TD); td.name = type-name; td.nfields = nfields. + %li(a0, 24) + %li(a1, %HDR.TD) + %call(&alloc_hdr) + %st(a0, sp, 16) + %ld(t0, sp, 0) + %car(t0, t0) + %st(t0, a0, 5) + %ld(t1, sp, 40) + %st(t1, a0, 13) + + # ctor-prim = make_param_prim(prim_ctor_entry, td); bind ctor-name. + %la(a0, &prim_ctor_entry) + %ld(a1, sp, 16) + %call(&make_param_prim) + %ld(t0, sp, 0) + %cdr(t0, t0) + %car(t0, t0) + %car(t0, t0) + %untag_sym(t0, t0) + %mov(a1, a0) + %mov(a0, t0) + %call(&sym_set_global) + + # pred-prim = make_param_prim(prim_predicate_entry, td); bind pred. + %la(a0, &prim_predicate_entry) + %ld(a1, sp, 16) + %call(&make_param_prim) + %ld(t0, sp, 0) + %cdr(t0, t0) + %cdr(t0, t0) + %car(t0, t0) + %untag_sym(t0, t0) + %mov(a1, a0) + %mov(a0, t0) + %call(&sym_set_global) + + # Iterate clauses: bind accessor + optional mutator per clause. + %li(t0, 0) + %st(t0, sp, 32) + + ::clause_loop + %ld(t0, sp, 24) + %li(t1, %imm_val(%IMM.NIL)) + %beq(t0, t1, &::done) + + # accessor-prim with data = tagged idx; bind cadr(clause). + %ld(a1, sp, 32) + %shli(a1, a1, 3) + %la(a0, &prim_accessor_entry) + %call(&make_param_prim) + + %ld(t0, sp, 24) + %car(t0, t0) + %cdr(t0, t0) + %car(t0, t0) + %untag_sym(t0, t0) + %mov(a1, a0) + %mov(a0, t0) + %call(&sym_set_global) + + # Mutator? If cddr(clause) is a pair, bind it. + %ld(t0, sp, 24) + %car(t0, t0) + %cdr(t0, t0) + %cdr(t0, t0) + %li(t1, %imm_val(%IMM.NIL)) + %beq(t0, t1, &::no_mutator) + + %ld(a1, sp, 32) + %shli(a1, a1, 3) + %la(a0, &prim_mutator_entry) + %call(&make_param_prim) + + %ld(t0, sp, 24) + %car(t0, t0) + %cdr(t0, t0) + %cdr(t0, t0) + %car(t0, t0) + %untag_sym(t0, t0) + %mov(a1, a0) + %mov(a0, t0) + %call(&sym_set_global) + + ::no_mutator + %ld(t0, sp, 24) + %cdr(t0, t0) + %st(t0, sp, 24) + %ld(t0, sp, 32) + %addi(t0, t0, 1) + %st(t0, sp, 32) + %b(&::clause_loop) + + ::done + %li(a0, %imm_val(%IMM.UNSPEC)) +}) + # ========================================================================= # Read-only data # ========================================================================= @@ -2146,6 +2516,7 @@ :name_let "let" :name_letstar "let*" :name_letrec "letrec" +:name_define_record_type "define-record-type" # Primitive surface names. :name_sys_exit "sys-exit" @@ -2173,6 +2544,12 @@ :name_bv_copy "bytevector-copy" :name_bv_copy_b "bytevector-copy!" :name_bv_grow "bytevector-grow!" +:name_make_rt "%make-record-td" +:name_make_rec "%make-record" +:name_record_ref "%record-ref" +:name_record_set "%record-set!" +:name_record_isa "%record-is-a?" +:name_record_td "%record-td" # Primitive registration table. Each entry: 8-byte name_ptr (4-byte label # ref + 4 pad), 8-byte name_len, 8-byte entry_label (4 ref + 4 pad). @@ -2202,6 +2579,12 @@ &name_bv_copy %(0) $(15) &prim_bv_copy_entry %(0) &name_bv_copy_b %(0) $(16) &prim_bv_copy_bang_entry %(0) &name_bv_grow %(0) $(16) &prim_bv_grow_entry %(0) +&name_make_rt %(0) $(15) &prim_make_record_td_entry %(0) +&name_make_rec %(0) $(12) &prim_make_record_entry %(0) +&name_record_ref %(0) $(11) &prim_record_ref_entry %(0) +&name_record_set %(0) $(12) &prim_record_set_entry %(0) +&name_record_isa %(0) $(13) &prim_record_is_a_entry %(0) +&name_record_td %(0) $(10) &prim_record_td_entry %(0) :prim_table_end :msg_usage "scheme1: usage: scheme1 SOURCE.scm" '0a' '00' @@ -2252,6 +2635,7 @@ :sym_let $(0) :sym_letstar $(0) :sym_letrec $(0) +:sym_define_record_type $(0) # Pointer slots for the past-:ELF_end arenas. :readbuf_buf_ptr $(0) diff --git a/tests/scheme1/35-record-basic.expected-exit b/tests/scheme1/35-record-basic.expected-exit @@ -0,0 +1 @@ +18 diff --git a/tests/scheme1/35-record-basic.scm b/tests/scheme1/35-record-basic.scm @@ -0,0 +1,9 @@ +; define-record-type with constructor and accessors. +(define-record-type point + (make-point x y) + point? + (x point-x) + (y point-y set-point-y!)) + +(define p (make-point 7 11)) +(sys-exit (+ (point-x p) (point-y p))) diff --git a/tests/scheme1/36-record-mutator.expected-exit b/tests/scheme1/36-record-mutator.expected-exit @@ -0,0 +1 @@ +38 diff --git a/tests/scheme1/36-record-mutator.scm b/tests/scheme1/36-record-mutator.scm @@ -0,0 +1,10 @@ +; Mutators update the field in place; later accessor reads the new value. +(define-record-type point + (make-point x y) + point? + (x point-x) + (y point-y set-point-y!)) + +(define p (make-point 5 5)) +(set-point-y! p 33) +(sys-exit (+ (point-x p) (point-y p))) diff --git a/tests/scheme1/37-record-predicate.expected-exit b/tests/scheme1/37-record-predicate.expected-exit @@ -0,0 +1 @@ +22 diff --git a/tests/scheme1/37-record-predicate.scm b/tests/scheme1/37-record-predicate.scm @@ -0,0 +1,11 @@ +; Predicates distinguish disjoint record types; rejects non-records too. +(define-record-type point (make-point x y) point? (x point-x) (y point-y)) +(define-record-type box (make-box w) box? (w box-w)) + +(define p (make-point 1 2)) +(define b (make-box 99)) +(sys-exit (cond ((point? b) 1) + ((box? p) 2) + ((point? '()) 3) + ((point? p) 22) + (else 99))) diff --git a/tests/scheme1/38-record-internal-prims.expected-exit b/tests/scheme1/38-record-internal-prims.expected-exit @@ -0,0 +1 @@ +33 diff --git a/tests/scheme1/38-record-internal-prims.scm b/tests/scheme1/38-record-internal-prims.scm @@ -0,0 +1,7 @@ +; Internal record primitives are accessible directly: build a TD, build +; a record, ref/set/predicate via %record-*. +(define td (%make-record-td 'foo 2)) +(define r (%make-record td 11 22)) +(sys-exit (if (%record-is-a? r td) + (+ (%record-ref r 0) (%record-ref r 1)) + 999))