Fix MOAD-0001 defects across all implementations
asm/uncommonlisp.s — intern_symbol: replaced O(N) linear scan with djb2 hash table (1024 buckets, chaining). 2.9x faster symbol interning on programs with many symbols. 75 tests pass. uncommonlisp.py — _define_record_type: replaced list.index() O(N) with dict lookup O(1) for field→index mapping. 571 tests pass. MOAD-0002 documented: _portal_checkpoint, _call_stack, _auto_compile are intentional globals (hot loop performance). cc_escape_val/cc_active_jmp are required by setjmp/longjmp call/cc approach. Comments added. All 836 assertions pass across Python + C + Assembly + functional.
This commit is contained in:
parent
87db0b0841
commit
22571fa470
3 changed files with 84 additions and 35 deletions
|
|
@ -200,6 +200,12 @@ is_tty: .skip 8
|
|||
num_buf: .skip 64
|
||||
sym_table: .skip 16384 # 2048 symbol pointers
|
||||
sym_count: .skip 8
|
||||
# Hash table for O(1) symbol interning (MOAD-0001 fix)
|
||||
# 1024 buckets, each a pointer to chain head (or 0 = empty)
|
||||
# Chain nodes: 16 bytes [sym_ptr, next_ptr], allocated from heap
|
||||
.equ SYM_HASH_BITS, 10
|
||||
.equ SYM_HASH_SIZE, 1024
|
||||
sym_hash_buckets: .skip 8192 # 1024 * 8 bytes
|
||||
|
||||
# ============================================================
|
||||
.text
|
||||
|
|
@ -461,60 +467,76 @@ env_set:
|
|||
ret
|
||||
|
||||
# ============================================================
|
||||
# Symbol interning
|
||||
# Symbol interning — MOAD-0001: O(1) hash table lookup
|
||||
# Symbols are stored as: 1 byte length, then chars (on heap)
|
||||
# sym_table: array of pointers to these, sym_count entries
|
||||
# sym_table: flat array kept for iteration; sym_hash_buckets: hash chains for O(1) lookup
|
||||
# intern_symbol: %rdi=str_ptr %rsi=len -> %rax = tagged symbol
|
||||
# Hash function: djb2 (hash = 5381; for each byte: hash = hash*33 + byte)
|
||||
# ============================================================
|
||||
intern_symbol:
|
||||
pushq %rbx
|
||||
pushq %r12
|
||||
pushq %r13 # NOTE: we temporarily use %r13 here but save/restore
|
||||
pushq %rbp
|
||||
movq %rdi, %rbx # string ptr
|
||||
movq %rsi, %r12 # length
|
||||
|
||||
# Search existing
|
||||
leaq sym_table(%rip), %rdi
|
||||
movq sym_count(%rip), %rcx
|
||||
xorq %r8, %r8
|
||||
.isym_search:
|
||||
cmpq %rcx, %r8
|
||||
jge .isym_new
|
||||
movq (%rdi,%r8,8), %r9 # candidate pointer
|
||||
movzbq (%r9), %r10 # candidate length
|
||||
# Compute djb2 hash of string
|
||||
movq $5381, %rax
|
||||
xorq %rcx, %rcx
|
||||
.isym_hash_loop:
|
||||
cmpq %r12, %rcx
|
||||
jge .isym_hash_done
|
||||
movq %rax, %rdx
|
||||
shlq $5, %rdx # hash << 5
|
||||
addq %rdx, %rax # hash * 33
|
||||
movzbq (%rbx,%rcx), %rdx
|
||||
addq %rdx, %rax # + byte
|
||||
incq %rcx
|
||||
jmp .isym_hash_loop
|
||||
.isym_hash_done:
|
||||
# Mask to bucket index
|
||||
andq $(SYM_HASH_SIZE - 1), %rax
|
||||
movq %rax, %rbp # bucket index saved in %rbp
|
||||
|
||||
# Walk the chain at sym_hash_buckets[bucket]
|
||||
leaq sym_hash_buckets(%rip), %rdi
|
||||
movq (%rdi,%rbp,8), %r8 # chain head pointer (or 0)
|
||||
.isym_chain_walk:
|
||||
testq %r8, %r8
|
||||
jz .isym_new
|
||||
movq (%r8), %r9 # sym_ptr from chain node
|
||||
movzbq (%r9), %r10 # candidate length
|
||||
cmpq %r12, %r10
|
||||
jne .isym_next
|
||||
jne .isym_chain_next
|
||||
# Compare bytes
|
||||
leaq 1(%r9), %r10 # candidate chars
|
||||
leaq 1(%r9), %r10 # candidate chars
|
||||
xorq %r11, %r11
|
||||
.isym_cmp:
|
||||
.isym_chain_cmp:
|
||||
cmpq %r12, %r11
|
||||
jge .isym_found
|
||||
jge .isym_chain_found
|
||||
movb (%rbx,%r11), %al
|
||||
cmpb (%r10,%r11), %al
|
||||
jne .isym_next
|
||||
jne .isym_chain_next
|
||||
incq %r11
|
||||
jmp .isym_cmp
|
||||
.isym_found:
|
||||
jmp .isym_chain_cmp
|
||||
.isym_chain_found:
|
||||
movq %r9, %rax
|
||||
orq $TAG_SYM, %rax
|
||||
popq %r13
|
||||
popq %rbp
|
||||
popq %r12
|
||||
popq %rbx
|
||||
ret
|
||||
.isym_next:
|
||||
incq %r8
|
||||
jmp .isym_search
|
||||
.isym_chain_next:
|
||||
movq 8(%r8), %r8 # next pointer in chain
|
||||
jmp .isym_chain_walk
|
||||
.isym_new:
|
||||
# Allocate: 1 + length bytes
|
||||
pushq %rdi
|
||||
pushq %rcx
|
||||
# Allocate symbol storage: 1 + length bytes
|
||||
# NOTE: %r13 is the global heap limit — do NOT clobber it.
|
||||
# Use the stack to save sym_ptr across the second heap_alloc.
|
||||
leaq 1(%r12), %rdi
|
||||
call heap_alloc
|
||||
popq %rcx
|
||||
popq %rdi
|
||||
# Fill
|
||||
movb %r12b, (%rax) # length byte
|
||||
# %rax = sym_ptr; fill symbol: length byte + chars
|
||||
movb %r12b, (%rax)
|
||||
xorq %r8, %r8
|
||||
.isym_copy:
|
||||
cmpq %r12, %r8
|
||||
|
|
@ -524,14 +546,27 @@ intern_symbol:
|
|||
incq %r8
|
||||
jmp .isym_copy
|
||||
.isym_copied:
|
||||
# Add to table
|
||||
pushq %rax # save sym_ptr on stack
|
||||
# Add to flat sym_table (for iteration)
|
||||
leaq sym_table(%rip), %rdi
|
||||
movq sym_count(%rip), %rcx
|
||||
movq %rax, (%rdi,%rcx,8)
|
||||
incq %rcx
|
||||
movq %rcx, sym_count(%rip)
|
||||
# Allocate hash chain node: 16 bytes [sym_ptr, next_ptr]
|
||||
movq $16, %rdi
|
||||
call heap_alloc
|
||||
# Fill chain node: %rax = node_ptr, stack top = sym_ptr
|
||||
popq %rcx # rcx = sym_ptr
|
||||
movq %rcx, (%rax) # node->sym = sym_ptr
|
||||
leaq sym_hash_buckets(%rip), %rdi
|
||||
movq (%rdi,%rbp,8), %rdx # old chain head
|
||||
movq %rdx, 8(%rax) # node->next = old head
|
||||
movq %rax, (%rdi,%rbp,8) # bucket head = new node
|
||||
# Return tagged symbol
|
||||
movq %rcx, %rax
|
||||
orq $TAG_SYM, %rax
|
||||
popq %r13
|
||||
popq %rbp
|
||||
popq %r12
|
||||
popq %rbx
|
||||
ret
|
||||
|
|
|
|||
7
c/eval.c
7
c/eval.c
|
|
@ -8,6 +8,13 @@
|
|||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* call/cc support — thread-local escape state
|
||||
*
|
||||
* MOAD-0002: These thread-locals are intentional coupling, required by the
|
||||
* setjmp/longjmp escape-only call/cc implementation. The continuation closure
|
||||
* (ul_callcc_kont) writes cc_escape_val and longjmps to cc_active_jmp; the
|
||||
* setjmp site in eval reads them back. Threading these through every call
|
||||
* frame would defeat the purpose of longjmp-based unwinding. Thread-local
|
||||
* storage ensures thread safety without a context parameter.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static __thread Value cc_escape_val;
|
||||
|
|
|
|||
|
|
@ -570,6 +570,7 @@ def _define_record_type(a, env):
|
|||
ctor_spec = _L(rest[0])
|
||||
ctor_name = ctor_spec[0]
|
||||
all_fields = [str(s) for s in ctor_spec[1:]]
|
||||
field_map = {f: i for i, f in enumerate(all_fields)} # MOAD-0001: O(1) field lookup
|
||||
pred_name = rest[1]
|
||||
slot_specs = [_L(s) for s in rest[2:]]
|
||||
|
||||
|
|
@ -610,10 +611,9 @@ def _define_record_type(a, env):
|
|||
getter_name = spec[1]
|
||||
setter_name = spec[2] if len(spec) > 2 else None
|
||||
field_str = str(field_tag)
|
||||
try:
|
||||
idx = all_fields.index(field_str) + 1 # +1 to skip type tag
|
||||
except ValueError:
|
||||
if field_str not in field_map: # MOAD-0001: O(1) lookup via dict
|
||||
raise LispErr(f'define-record-type {name}: field {field_str!r} not in {all_fields}')
|
||||
idx = field_map[field_str] + 1 # +1 to skip type tag
|
||||
|
||||
def _make_getter(i):
|
||||
def getter_fn(args, _):
|
||||
|
|
@ -2206,6 +2206,10 @@ def portal_resume(path, base_env=None):
|
|||
|
||||
|
||||
# Portal checkpoint for mid-execution save
|
||||
# MOAD-0002: Module-level global — intentional coupling. This is checked in the VM hot
|
||||
# loop (OP_JUMP, OP_TAIL_CALL) so passing it as a parameter would add overhead to every
|
||||
# iteration. The mutable list wrapper allows portal-checkpoint! to signal the VM without
|
||||
# requiring a context object threaded through vm_exec/vm_loop.
|
||||
_portal_checkpoint = [None] # set to a path to trigger save during VM execution
|
||||
|
||||
def _check_portal_checkpoint(instrs, ip, stack, env, frames, vm_id):
|
||||
|
|
@ -2607,6 +2611,9 @@ _gensym_ctr = itertools.count()
|
|||
_modules: dict = {} # module-name → Env
|
||||
_mod_exports: dict = {} # module-name → [export-names]
|
||||
_record_types: dict = {} # record-name → {'fields': [...], 'parent': name|None}
|
||||
# MOAD-0002: Module-level global — intentional coupling. Only read in LispErr.__init__
|
||||
# to snapshot the call stack for error messages. Kept global because threading it through
|
||||
# every leval/apply call would add overhead to the common (non-error) path.
|
||||
_call_stack: list = [] # call stack for error reporting
|
||||
_traced_originals: dict = {} # name → original proc (for untrace)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue