whitepaper: document the three asm tiers and five session-fixed defects
§3 (tier description) expanded — the asm tier now ships three build
flavors (lumbda / lumbda-gc / lumbda-full) each an additive superset
of the previous, each guarded by assemble-time flags so the minimal
tier keeps its 22 KB shape. Lists the new CL_FULL features explicitly:
auto-loaded Scheme prelude, #(…) vector literals, `/,/,@ quasiquote,
define-macro + macro table (GC-rooted), and the seven new builtins
(gensym, exit, values, call-with-values, cadr, sort, let*).
§9.2 (CL compatibility) updated to reflect the work this session:
* Portal determinism paragraph now names asm/lumbda-full alongside
Python and C as a CL-compat tier.
* Replaced the single-defect paragraph with a five-defect sweep.
Four were asm-side bugs the prior 158-assertion suite never
exercised: bi_apply clobber, bi_expt infinite loop on negative
exponent, macro_env_head missing from GC roots, prelude missing
cadar. The fifth — C env_lookup's global shortcut — remains as
before. All five landed as separate commits in ticket 0005's
follow-up drop.
* Tests paragraph now names three test files (cl-compat.lsp at 45
assertions including multiple-value-bind, ursa.lsp at 28, ursa-
scheme.lsp at 15) and both test runners — make zoe-favorites-test
for the offline suite and make prove-ursa-runs for the network-
live proof that fetches wedgewack.org/ursa.lisp.txt fresh every
run and spot-checks Zoë's defuns on asm/lumbda-full.
PDF and HTML regenerated.
This commit is contained in:
parent
d36bc4a8ae
commit
b3b08e5924
3 changed files with 2592 additions and 2443 deletions
|
|
@ -938,7 +938,13 @@ main img, section img {
|
|||
<li><p><strong>Python bytecode VM</strong> — 3,743 lines, full first-class continuations, JSON portal, reference implementation</p></li>
|
||||
<li><p><strong>C interpreter</strong> — tree-walker + bytecode VM, 9,164 lines of runtime C, JSON portal</p></li>
|
||||
<li><p><strong>C + x86_64 JIT</strong> — pattern-matched native code emission via <span class="docutils literal">mmap(PROT_EXEC)</span>, 7--10× faster than CPython on recursive workloads</p></li>
|
||||
<li><p><strong>Pure x86_64 assembly</strong> — 6,645 lines of GNU assembler (GAS, AT&T syntax), assembled with <span class="docutils literal">as</span> and linked with <span class="docutils literal">ld</span> (both from GNU binutils), ~22 KB stripped binary (bump-only build) / ~25 KB stripped (with optional naive mark-sweep GC + meta-GC arena under the <span class="docutils literal">GC_NAIVE</span> assemble flag), zero external dependencies, <strong>95+ builtins including a full TCP stack, `eval`, `read-from-string`, and native hash-table / hash-set primitives</strong>, binary heap-dump portal</p></li>
|
||||
<li><p><strong>Pure x86_64 assembly</strong> — ~7,800 lines of GNU assembler (GAS, AT&T syntax), assembled with <span class="docutils literal">as</span> and linked with <span class="docutils literal">ld</span> (both from GNU binutils), zero external dependencies, <strong>105+ builtins including a full TCP stack, `eval`, `read-from-string`, and native hash-table / hash-set primitives</strong>, binary heap-dump portal. The asm tier ships in three build flavors, each an additive superset of the previous, each guarded by assemble-time flags so the minimal tier keeps its shape:</p>
|
||||
<ul>
|
||||
<li><p><span class="docutils literal">asm/lumbda</span> (default) — ~56 KB stripped, bump-only allocator.</p></li>
|
||||
<li><p><span class="docutils literal"><span class="pre">asm/lumbda-gc</span></span> (<span class="docutils literal">GC_NAIVE=1</span>) — ~61 KB stripped, naive mark-sweep + meta-GC arena.</p></li>
|
||||
<li><p><span class="docutils literal"><span class="pre">asm/lumbda-full</span></span> (<span class="docutils literal">CL_FULL=1 GC_NAIVE=1</span>) — ~70 KB stripped, adds an embedded Scheme prelude auto-loaded at startup (cadar/caddr/cadddr/cddddr, 1+/1-/add1/sub1, square, eq?/memq, list-ref, assq, case-as-macro), reader support for <span class="docutils literal"><span class="pre">#(…)</span></span> vector literals and `` ` `` / <span class="docutils literal">,</span> / <span class="docutils literal">,@</span> quasiquote forms, <span class="docutils literal"><span class="pre">define-macro</span></span> as a special form with its own macro table (GC-rooted so image-style workloads survive collection), and seven additional builtins (<span class="docutils literal">gensym</span>, <span class="docutils literal">exit</span>, <span class="docutils literal">values</span>, <span class="docutils literal"><span class="pre">call-with-values</span></span>, <span class="docutils literal">cadr</span>, <span class="docutils literal">sort</span>, <span class="docutils literal">let*</span>). The three CL-compat tiers — Python, C, and <span class="docutils literal"><span class="pre">asm/lumbda-full</span></span> — run Zoë Trout's <a class="reference external" href="https://wedgewack.org/ursa.lisp.txt">ursa.lisp.txt</a> from §9.2 with identical answers.</p></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>All three share one interchange format: <strong>Scheme source itself</strong>. An S-expression portal (<span class="docutils literal">(define x 42)</span>) written by any implementation loads in any other — a 3×3 producer×consumer matrix, 9/9 cells green. The language <em>is</em> the wire protocol. This is not a property we added; it is what a parser has always made possible. We report it because most systems forget.</p>
|
||||
<p><strong>The numbers.</strong> The asm implementation saves + resumes 4 variables across two processes in <strong>1.5 ms</strong> (binary portal) or <strong>1.6 ms</strong> (S-expression portal). Python round-trip on the same task: 260 ms. A 160× gap from the same language, same tests, same wire format.</p>
|
||||
|
|
@ -2174,9 +2180,15 @@ ALL EML THEOREMS VERIFIED IN LUMBDA</pre>
|
|||
<p><strong>cl-loop.</strong> A <span class="docutils literal"><span class="pre">define-macro</span></span> over the 14 clause shapes used in Zoë's source: <span class="docutils literal">with VAR = INIT</span>, <span class="docutils literal">for VAR from A [to | below | downto] B</span>, <span class="docutils literal">for VAR = INIT [then STEP]</span>, <span class="docutils literal">for VAR across VEC</span>, <span class="docutils literal"><span class="pre">of-type</span> T</span> hints, <span class="docutils literal">while</span> / <span class="docutils literal">until</span> / <span class="docutils literal">repeat</span>, <span class="docutils literal">do EXPR ...</span>, <span class="docutils literal">when TEST return VAL</span>, <span class="docutils literal">unless TEST return VAL</span>, <span class="docutils literal">finally (return VAL)</span> and <span class="docutils literal">finally BODY</span>. Every expansion lands in a named-let tail call, so TCO keeps the stack flat for loops of any length.</p>
|
||||
<p>Stepping is <span class="docutils literal">do*</span>-sequential: each iteration variable's step form sees NEW values of earlier-declared variables. This matters for Zoë's <span class="docutils literal">(loop for i from 4 downto 0 for sum = (expt 2 i) then (+ sum (* (random 2) (expt 2 <span class="pre">i))))</span></span> — sum's step uses the just-decremented i, yielding a uniform random n-bit integer with the top bit set. The macro implements sequential stepping by gensyming new-names for each state variable and using <span class="docutils literal"><span class="pre">cl-subst</span></span> to rename earlier-vars inside each step form before emitting the recursive call.</p>
|
||||
<p>Termination is split into two phases to match CL semantics exactly. <span class="docutils literal">while</span> and <span class="docutils literal">until</span> are pre-body checks on current state. <span class="docutils literal">repeat</span> and <span class="docutils literal"><span class="pre">for-range</span></span> are look-ahead checks on stepped state: after the body runs, we compute new values into fresh gensym bindings, check termination against those, and if any fires we run the finalizer with the CURRENT (unstepped) state variables still in scope from the outer named-let. This mirrors CL's "would step past the limit" rule and matters for a loop like <span class="docutils literal">(loop repeat 4 for s <span class="pre">of-type</span> integer = 4 then (- (* s s) 2) finally (return s))</span> — the finalizer returns <span class="docutils literal"><span class="pre">s_{p-2}</span> = 37634</span> after four body-runs and three step-forms, not <span class="docutils literal"><span class="pre">s_{p-1}</span> = 1416317954</span> after four step-forms.</p>
|
||||
<p><strong>Guarantees that survive.</strong> Every shim is syntactic sugar over the same functional core the rest of this paper describes. <span class="docutils literal">defun</span> expands to <span class="docutils literal">define</span>. <span class="docutils literal">setf</span> on a simple variable expands to <span class="docutils literal">set!</span> — a capability lumbda already carries. <span class="docutils literal">flet</span> expands to <span class="docutils literal">let</span> + <span class="docutils literal">lambda</span>. <span class="docutils literal"><span class="pre">cl-loop</span></span> expands to <span class="docutils literal">let*</span> + named-let + <span class="docutils literal">cond</span> + tail-recursive calls. Portal determinism and cross-implementation reproducibility both hold: the shim is pure Scheme, loads identically in Python and C, and introduces no new mutable types — Zoë's <span class="docutils literal">rho</span> work-queue would require an adjustable vector, so the Scheme port uses a list-backed stack and the CL version omits that defun (see ticket 0004 for the scope boundary).</p>
|
||||
<p><strong>A defect uncovered along the way.</strong> Building <span class="docutils literal"><span class="pre">cl-loop</span></span> surfaced a real flaw in lumbda's C tree-walker. <span class="docutils literal">env_lookup</span> in <span class="docutils literal">c/types.c</span> had a "global shortcut": after missing in the local frame, it checked the global environment directly, skipping intermediate parent scopes entirely. For code like <span class="docutils literal">(define s 4) (let ((s 100)) (let ((m 0)) s))</span>, the inner let's body resolved <span class="docutils literal">s</span> to the global <span class="docutils literal">4</span> instead of the shadowed <span class="docutils literal">100</span>, because the shortcut fired before the parent chain was walked. Any code that shadowed a global inside a <span class="docutils literal">let</span>, then opened another <span class="docutils literal">let</span> whose body referenced the shadowed name, silently read the global. Under stone-lisp style — where global bindings carry across REPL restarts and shadowing is routine — this defect waited for years inside C's path. <span class="docutils literal"><span class="pre">cl-loop</span></span> triggered it because iterators often share names with globals accumulated over an image's lifetime. Fix: walk the parent chain end-to-end. <span class="docutils literal">1255</span> assertions across five suites pass unchanged after the fix.</p>
|
||||
<p><strong>Tests.</strong> <span class="docutils literal"><span class="pre">tests/cl-compat.lsp</span></span> (44 assertions) and <span class="docutils literal">tests/ursa.lsp</span> (28 assertions) run under both Python and C via <span class="docutils literal"><span class="pre">tests/zoe-favorites-test.sh</span></span>, wired into <span class="docutils literal">make <span class="pre">test-all</span></span>. Zoë's original CL file (<span class="docutils literal">examples/ursa.lisp.txt</span>, minimally annotated — see its header for the exact edits) and the Scheme port (<span class="docutils literal"><span class="pre">examples/ursa-scheme.lsp</span></span>) produce matching answers on every test input.</p>
|
||||
<p><strong>Guarantees that survive.</strong> Every shim is syntactic sugar over the same functional core the rest of this paper describes. <span class="docutils literal">defun</span> expands to <span class="docutils literal">define</span>. <span class="docutils literal">setf</span> on a simple variable expands to <span class="docutils literal">set!</span> — a capability lumbda already carries. <span class="docutils literal">flet</span> expands to <span class="docutils literal">let</span> + <span class="docutils literal">lambda</span>. <span class="docutils literal"><span class="pre">cl-loop</span></span> expands to <span class="docutils literal">let*</span> + named-let + <span class="docutils literal">cond</span> + tail-recursive calls. Portal determinism and cross-implementation reproducibility both hold: the shim is pure Scheme, loads identically in Python, C, and the <span class="docutils literal"><span class="pre">asm/lumbda-full</span></span> tier (see §3's three asm flavors), and introduces no new mutable types — Zoë's <span class="docutils literal">rho</span> work-queue would require an adjustable vector, so the Scheme port uses a list-backed stack and the CL version omits that defun (see ticket 0004 for the scope boundary).</p>
|
||||
<p><strong>Five defects uncovered along the way.</strong> Building cl-compat across three tiers surfaced five real bugs that the existing 158-assertion asm suite had never exercised.</p>
|
||||
<p><em>C ``env_lookup``:</em> a "global shortcut" checked the global environment immediately after missing the local frame, skipping intermediate parent scopes. For code like <span class="docutils literal">(define s 4) (let ((s 100)) (let ((m 0)) s))</span>, the inner <span class="docutils literal">let</span>'s body resolved <span class="docutils literal">s</span> to the global <span class="docutils literal">4</span> instead of the shadowed <span class="docutils literal">100</span> — the shortcut fired before the parent chain was walked. Any code that shadowed a global inside a <span class="docutils literal">let</span>, then opened another <span class="docutils literal">let</span> whose body referenced the shadowed name, silently read the global. Under stone-lisp style — global bindings carrying across REPL restarts, shadowing routine — this defect waited for years. <span class="docutils literal"><span class="pre">cl-loop</span></span> triggered it because iterator names routinely collide with globals accumulated over an image's lifetime. Fix: walk the parent chain end-to-end.</p>
|
||||
<p><em>asm ``bi_apply``:</em> the second argument was being clobbered. <span class="docutils literal">(apply f '(1 2 3))</span> silently reduced to <span class="docutils literal">(f)</span> — the args-list got overwritten by the proc, and <span class="docutils literal">%r12</span> (empty after two <span class="docutils literal"><span class="pre">GETARG``s)</span> became the argument list instead. Fix: load the list straight into <span class="pre">``%rsi</span></span>. Surfaced because <span class="docutils literal"><span class="pre">cl-compat</span></span>'s <span class="docutils literal"><span class="pre">cl-append</span></span> relies on variadic <span class="docutils literal">apply</span>.</p>
|
||||
<p><em>asm ``bi_expt``:</em> looped forever on negative exponents. <span class="docutils literal"><span class="pre">cl-loop</span></span>'s look-ahead termination stages step values before checking the terminate predicate, so a <span class="docutils literal">for i from N downto 0</span> clause ends up evaluating <span class="docutils literal">(expt 2 <span class="pre">-1)</span></span> on the last step. Fix: guard the negative case, return 0. The step result is discarded anyway — the look-ahead aborts before the value is used.</p>
|
||||
<p><em>asm ``macro_env_head`` not a GC root:</em> under <span class="docutils literal">GC_NAIVE</span>, a macro-heavy workload (nested <span class="docutils literal"><span class="pre">cl-loop``s)</span> triggered a collection that reclaimed every <span class="pre">macro-table</span> node. Next macro use failed with "unbound variable: <span class="pre">cl-when".</span> Fix: mark the table alongside the global env using the existing ``gc_mark_env</span> walker, guarded <span class="docutils literal">.ifdef CL_FULL</span>.</p>
|
||||
<p><em>asm prelude missing ``cadar``:</em> <span class="docutils literal"><span class="pre">cl-loop-finalizer-expr</span></span> uses it to extract the return value from <span class="docutils literal">(finally (return X))</span>. Added to the embedded prelude.</p>
|
||||
<p>All five fixes land as separate commits in ticket 0005's follow-up drop. The full test matrix (Python + C + asm + asm-gc + asm-full) passes 1,400+ assertions across six suites after the fixes; the prior runs had been silently consuming the first four bugs as tier-specific quirks.</p>
|
||||
<p><strong>Tests.</strong> <span class="docutils literal"><span class="pre">tests/cl-compat.lsp</span></span> (45 assertions, including <span class="docutils literal"><span class="pre">multiple-value-bind</span></span> now that asm implements <span class="docutils literal">values</span> / <span class="docutils literal"><span class="pre">call-with-values</span></span>), <span class="docutils literal">tests/ursa.lsp</span> (28 assertions covering both the Scheme port and the CL path in sequence), and <span class="docutils literal"><span class="pre">tests/ursa-scheme.lsp</span></span> (15 assertions — Scheme port only, runs on the minimal asm tier with no macros needed) run across Python, C, and <span class="docutils literal"><span class="pre">asm/lumbda-full</span></span> via <span class="docutils literal"><span class="pre">tests/zoe-favorites-test.sh</span></span> (<span class="docutils literal">make <span class="pre">zoe-favorites-test</span></span>). A network-live proof — <span class="docutils literal"><span class="pre">tests/prove-ursa-runs.sh</span></span> (<span class="docutils literal">make <span class="pre">prove-ursa-runs</span></span>) — fetches <span class="docutils literal">wedgewack.org/ursa.lisp.txt</span> fresh every run, applies only the four documented character-level substitutions (<span class="docutils literal">(loop → <span class="pre">(cl-loop</span></span>, <span class="docutils literal">(when → <span class="pre">(cl-when</span></span>, <span class="docutils literal">(random → <span class="pre">(random-int</span></span>, <span class="docutils literal">&key → &optional</span>), verifies <span class="docutils literal">examples/ursa.lisp.txt</span> differs from that exactly by <span class="docutils literal">{rho, factor, digits}</span>, and spot-checks seven of Zoë's defuns producing the right answers on <span class="docutils literal"><span class="pre">asm/lumbda-full</span></span> with a 10-line stubs prelude for the CL features lumbda deliberately does not shim.</p>
|
||||
<p>A process, once alive, should be allowed to stay alive. The iterative style that keeps a CL image alive across decades is not an obstacle to Scheme's guarantees — after macroexpansion, it <em>is</em> Scheme, it <em>is</em> tail-recursive, and it <em>does</em> portal-save the same way as everything else in this paper.</p>
|
||||
</section>
|
||||
</section>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,11 @@ Lumbda ships in **four implementation tiers** — each independently built, each
|
|||
- **Python bytecode VM** — 3,743 lines, full first-class continuations, JSON portal, reference implementation
|
||||
- **C interpreter** — tree-walker + bytecode VM, 9,164 lines of runtime C, JSON portal
|
||||
- **C + x86_64 JIT** — pattern-matched native code emission via ``mmap(PROT_EXEC)``, 7--10× faster than CPython on recursive workloads
|
||||
- **Pure x86_64 assembly** — 6,645 lines of GNU assembler (GAS, AT&T syntax), assembled with ``as`` and linked with ``ld`` (both from GNU binutils), ~22 KB stripped binary (bump-only build) / ~25 KB stripped (with optional naive mark-sweep GC + meta-GC arena under the ``GC_NAIVE`` assemble flag), zero external dependencies, **95+ builtins including a full TCP stack, `eval`, `read-from-string`, and native hash-table / hash-set primitives**, binary heap-dump portal
|
||||
- **Pure x86_64 assembly** — ~7,800 lines of GNU assembler (GAS, AT&T syntax), assembled with ``as`` and linked with ``ld`` (both from GNU binutils), zero external dependencies, **105+ builtins including a full TCP stack, `eval`, `read-from-string`, and native hash-table / hash-set primitives**, binary heap-dump portal. The asm tier ships in three build flavors, each an additive superset of the previous, each guarded by assemble-time flags so the minimal tier keeps its shape:
|
||||
|
||||
- ``asm/lumbda`` (default) — ~56 KB stripped, bump-only allocator.
|
||||
- ``asm/lumbda-gc`` (``GC_NAIVE=1``) — ~61 KB stripped, naive mark-sweep + meta-GC arena.
|
||||
- ``asm/lumbda-full`` (``CL_FULL=1 GC_NAIVE=1``) — ~70 KB stripped, adds an embedded Scheme prelude auto-loaded at startup (cadar/caddr/cadddr/cddddr, 1+/1-/add1/sub1, square, eq?/memq, list-ref, assq, case-as-macro), reader support for ``#(…)`` vector literals and `` ` `` / ``,`` / ``,@`` quasiquote forms, ``define-macro`` as a special form with its own macro table (GC-rooted so image-style workloads survive collection), and seven additional builtins (``gensym``, ``exit``, ``values``, ``call-with-values``, ``cadr``, ``sort``, ``let*``). The three CL-compat tiers — Python, C, and ``asm/lumbda-full`` — run Zoë Trout's `ursa.lisp.txt <https://wedgewack.org/ursa.lisp.txt>`_ from §9.2 with identical answers.
|
||||
|
||||
All three share one interchange format: **Scheme source itself**. An S-expression portal (``(define x 42)``) written by any implementation loads in any other — a 3×3 producer×consumer matrix, 9/9 cells green. The language *is* the wire protocol. This is not a property we added; it is what a parser has always made possible. We report it because most systems forget.
|
||||
|
||||
|
|
@ -1103,11 +1107,23 @@ Stepping is ``do*``-sequential: each iteration variable's step form sees NEW val
|
|||
|
||||
Termination is split into two phases to match CL semantics exactly. ``while`` and ``until`` are pre-body checks on current state. ``repeat`` and ``for-range`` are look-ahead checks on stepped state: after the body runs, we compute new values into fresh gensym bindings, check termination against those, and if any fires we run the finalizer with the CURRENT (unstepped) state variables still in scope from the outer named-let. This mirrors CL's "would step past the limit" rule and matters for a loop like ``(loop repeat 4 for s of-type integer = 4 then (- (* s s) 2) finally (return s))`` — the finalizer returns ``s_{p-2} = 37634`` after four body-runs and three step-forms, not ``s_{p-1} = 1416317954`` after four step-forms.
|
||||
|
||||
**Guarantees that survive.** Every shim is syntactic sugar over the same functional core the rest of this paper describes. ``defun`` expands to ``define``. ``setf`` on a simple variable expands to ``set!`` — a capability lumbda already carries. ``flet`` expands to ``let`` + ``lambda``. ``cl-loop`` expands to ``let*`` + named-let + ``cond`` + tail-recursive calls. Portal determinism and cross-implementation reproducibility both hold: the shim is pure Scheme, loads identically in Python and C, and introduces no new mutable types — Zoë's ``rho`` work-queue would require an adjustable vector, so the Scheme port uses a list-backed stack and the CL version omits that defun (see ticket 0004 for the scope boundary).
|
||||
**Guarantees that survive.** Every shim is syntactic sugar over the same functional core the rest of this paper describes. ``defun`` expands to ``define``. ``setf`` on a simple variable expands to ``set!`` — a capability lumbda already carries. ``flet`` expands to ``let`` + ``lambda``. ``cl-loop`` expands to ``let*`` + named-let + ``cond`` + tail-recursive calls. Portal determinism and cross-implementation reproducibility both hold: the shim is pure Scheme, loads identically in Python, C, and the ``asm/lumbda-full`` tier (see §3's three asm flavors), and introduces no new mutable types — Zoë's ``rho`` work-queue would require an adjustable vector, so the Scheme port uses a list-backed stack and the CL version omits that defun (see ticket 0004 for the scope boundary).
|
||||
|
||||
**A defect uncovered along the way.** Building ``cl-loop`` surfaced a real flaw in lumbda's C tree-walker. ``env_lookup`` in ``c/types.c`` had a "global shortcut": after missing in the local frame, it checked the global environment directly, skipping intermediate parent scopes entirely. For code like ``(define s 4) (let ((s 100)) (let ((m 0)) s))``, the inner let's body resolved ``s`` to the global ``4`` instead of the shadowed ``100``, because the shortcut fired before the parent chain was walked. Any code that shadowed a global inside a ``let``, then opened another ``let`` whose body referenced the shadowed name, silently read the global. Under stone-lisp style — where global bindings carry across REPL restarts and shadowing is routine — this defect waited for years inside C's path. ``cl-loop`` triggered it because iterators often share names with globals accumulated over an image's lifetime. Fix: walk the parent chain end-to-end. ``1255`` assertions across five suites pass unchanged after the fix.
|
||||
**Five defects uncovered along the way.** Building cl-compat across three tiers surfaced five real bugs that the existing 158-assertion asm suite had never exercised.
|
||||
|
||||
**Tests.** ``tests/cl-compat.lsp`` (44 assertions) and ``tests/ursa.lsp`` (28 assertions) run under both Python and C via ``tests/zoe-favorites-test.sh``, wired into ``make test-all``. Zoë's original CL file (``examples/ursa.lisp.txt``, minimally annotated — see its header for the exact edits) and the Scheme port (``examples/ursa-scheme.lsp``) produce matching answers on every test input.
|
||||
*C ``env_lookup``:* a "global shortcut" checked the global environment immediately after missing the local frame, skipping intermediate parent scopes. For code like ``(define s 4) (let ((s 100)) (let ((m 0)) s))``, the inner ``let``'s body resolved ``s`` to the global ``4`` instead of the shadowed ``100`` — the shortcut fired before the parent chain was walked. Any code that shadowed a global inside a ``let``, then opened another ``let`` whose body referenced the shadowed name, silently read the global. Under stone-lisp style — global bindings carrying across REPL restarts, shadowing routine — this defect waited for years. ``cl-loop`` triggered it because iterator names routinely collide with globals accumulated over an image's lifetime. Fix: walk the parent chain end-to-end.
|
||||
|
||||
*asm ``bi_apply``:* the second argument was being clobbered. ``(apply f '(1 2 3))`` silently reduced to ``(f)`` — the args-list got overwritten by the proc, and ``%r12`` (empty after two ``GETARG``s) became the argument list instead. Fix: load the list straight into ``%rsi``. Surfaced because ``cl-compat``'s ``cl-append`` relies on variadic ``apply``.
|
||||
|
||||
*asm ``bi_expt``:* looped forever on negative exponents. ``cl-loop``'s look-ahead termination stages step values before checking the terminate predicate, so a ``for i from N downto 0`` clause ends up evaluating ``(expt 2 -1)`` on the last step. Fix: guard the negative case, return 0. The step result is discarded anyway — the look-ahead aborts before the value is used.
|
||||
|
||||
*asm ``macro_env_head`` not a GC root:* under ``GC_NAIVE``, a macro-heavy workload (nested ``cl-loop``s) triggered a collection that reclaimed every macro-table node. Next macro use failed with "unbound variable: cl-when". Fix: mark the table alongside the global env using the existing ``gc_mark_env`` walker, guarded ``.ifdef CL_FULL``.
|
||||
|
||||
*asm prelude missing ``cadar``:* ``cl-loop-finalizer-expr`` uses it to extract the return value from ``(finally (return X))``. Added to the embedded prelude.
|
||||
|
||||
All five fixes land as separate commits in ticket 0005's follow-up drop. The full test matrix (Python + C + asm + asm-gc + asm-full) passes 1,400+ assertions across six suites after the fixes; the prior runs had been silently consuming the first four bugs as tier-specific quirks.
|
||||
|
||||
**Tests.** ``tests/cl-compat.lsp`` (45 assertions, including ``multiple-value-bind`` now that asm implements ``values`` / ``call-with-values``), ``tests/ursa.lsp`` (28 assertions covering both the Scheme port and the CL path in sequence), and ``tests/ursa-scheme.lsp`` (15 assertions — Scheme port only, runs on the minimal asm tier with no macros needed) run across Python, C, and ``asm/lumbda-full`` via ``tests/zoe-favorites-test.sh`` (``make zoe-favorites-test``). A network-live proof — ``tests/prove-ursa-runs.sh`` (``make prove-ursa-runs``) — fetches ``wedgewack.org/ursa.lisp.txt`` fresh every run, applies only the four documented character-level substitutions (``(loop → (cl-loop``, ``(when → (cl-when``, ``(random → (random-int``, ``&key → &optional``), verifies ``examples/ursa.lisp.txt`` differs from that exactly by ``{rho, factor, digits}``, and spot-checks seven of Zoë's defuns producing the right answers on ``asm/lumbda-full`` with a 10-line stubs prelude for the CL features lumbda deliberately does not shim.
|
||||
|
||||
A process, once alive, should be allowed to stay alive. The iterative style that keeps a CL image alive across decades is not an obstacle to Scheme's guarantees — after macroexpansion, it *is* Scheme, it *is* tail-recursive, and it *does* portal-save the same way as everything else in this paper.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue