jolt/test/spec/destructuring-spec.janet
Dmitri Sotnikov 534007641e
Stage2 compile only (#12)
* core: compile macro expanders via staged bootstrap (Stage 2 Task 1)

Macros are now compiled, not interpreted, by steady state — matching
Clojure (macros are ordinary compiled fns the Java seed compiles for
clojure.core) and ClojureScript (macros compiled, invoked at compile
time). Neither reference keeps an interpreted-closure fallback.

The early macros (00-syntax) are defined while the self-hosted analyzer
is still being bootstrapped (it builds lazily after the overlay loads),
so macro-compile-hook returns nil and they get an interpreted closure.
The bootstrap compiler.janet can't substitute (it punts on syntax-quote,
which nearly every expander uses).

Fix = staged bootstrap, the same pattern as the compiler fixpoint:
defmacro stashes the expander source on the var (:macro-src) plus a
:macro-uses-env flag; once the overlay + analyzer are fully built,
backend/recompile-macros! (via ensure-macros-compiled! at the end of
load-core-overlay!) compiles each stashed expander through the now-live
analyzer and rebinds the var, marking :macro-compiled. Idempotent;
&env/&form macros keep the interpreted closure (the compiled fn* has no
such params). The interpreter is now a build-time crutch, gone by
steady state.

Rewrote if-not/if-let/if-some/assert from '& [else]' rest-destructuring
(which the analyzer punts on) to a plain rest param + (first rest), so
all 47 overlay macros compile. Analyzer rest-destructuring gap: jolt-f79.

47/47 overlay macros compiled, 0 interpreted; user macros compile
immediately post-init. Gate green: conformance 267x3, fallback-zero
31/5, bootstrap-fixpoint stage1==2==3, self-host, staged-bootstrap,
lazy-infinite 44/44, clojure-test-suite >=4034/67, sci-bootstrap,
features 78/78, all unit+spec, core-bench neutral.

* core: wrap macro expanders in fn (not fn*) so destructured arglists compile

Follow-up to the staged macro-compile change. The macro-recompile pass
wrapped each expander in the raw fn* primitive, which punts on a
destructuring rest param — so if-not/if-let/if-some/assert (using the
canonical '& [else]') couldn't compile and had been rewritten to plain
rest params as a workaround.

Root cause: fn* is the primitive; the fn MACRO is what desugars
destructuring (rest, map, nested) into the body before lowering. Wrapping
expanders in fn instead of fn* compiles any destructured macro arglist
uniformly, so the workaround is unnecessary — reverted those 4 macros to
the canonical '& [else]' forms.

Net: rest-destructuring is fully compiled for all normal code (fn/defn/
let/macro params). Only the hand-written raw fn* primitive still punts
(jolt-f79, downgraded to P4 — falls back to interpreter, still correct).

47/47 overlay macros compiled. Gate green: conformance 267x3,
fallback-zero 31/5, bootstrap-fixpoint stage1==2==3, self-host,
staged-bootstrap, lazy-infinite 44/44, clojure-test-suite >=4034/67,
sci-bootstrap, features, all unit+spec.

* core: fn*/let*/loop* are plain-symbol primitives, matching Clojure (jolt-f79)

jolt-f79 asked to compile destructuring in fn* rest params. Checking
against Clojure inverts the premise: Clojure's fn* REJECTS destructuring
at compile time ('fn params must be Symbols'; let*/loop* 'Bad binding
form, expected symbol'). So the self-hosted analyzer was already correct
— fn*/let*/loop* are plain-symbol primitives; the fn/let/loop/defn
MACROS desugar destructuring. The real defect was the interpreter
leniently destructuring raw fn*, and defn emitting raw fn* to rely on it.

Changes:
- evaluator: fn*/let*/loop* now reject non-symbol binding forms with
  Clojure's exact messages (require-symbol-params/plain-sym?), so the
  interpreter agrees with the analyzer + Clojure.
- 00-syntax: defn emits the fn MACRO (not raw fn*) so destructuring
  params desugar; unnamed, so self-recursion still resolves via the var.
- 00-syntax: completing that exposed a real gap — the overlay destructure
  fn didn't handle kwargs (a map pattern bound against a fn's sequential
  rest); it had only worked via the interpreter's destructure-bind. Added
  the seq->map coercion to the map? branch (sequential: 1 map elt => that
  map, else apply hash-map), matching destructure-bind so interpret ==
  compile.

Net: fn*/let*/loop* are plain-symbol primitives across interpreter,
analyzer, and Clojure; all real destructuring (fn/defn/let/loop/macro
params, incl kwargs & {:keys}) compiles through the macros with no
interpreter fallback. Regression spec added.

Gate green: conformance 267x3, fallback-zero 31/5, bootstrap-fixpoint
stage1==2==3, self-host, staged-bootstrap, lazy-infinite 44/44,
clojure-test-suite >=4034/67, sci-bootstrap, features 78/78, all
unit+spec (destructuring 50/50).

* docs: clarify fn*/let*/loop* take plain symbols only (jolt-f79)

grammar.ebnf: the destructuring note already attributed patterns to the
binding MACROS; make the boundary explicit — the fn*/let*/loop* PRIMITIVES
they desugar to take plain symbols only (a non-symbol binding errors, as
in Clojure).

self-hosting-compiler.md: the 'compile destructuring via a shared
destructure expander instead of falling back' item is done (and its
jolt-7dl ref was stale) — destructuring now compiles through the
fn/let/loop/defn macros' desugaring; the primitives reject patterns.

* core: Stage 2 Task 2 tier 1 — compile syntax-quote + definterface/extend/proxy

First slice of moving stateful forms onto the compile path (jolt-eaa).
- loader stateful-head?: drop syntax-quote (the analyzer's `handled` set
  already compiles it; routing it to the interpreter was redundant).
- host_iface special-names: drop definterface/extend/proxy (stub macros
  expanding to def/nil — their expansions compile once unpunted).
- letfn stays interpreted: its let* expansion needs letrec semantics
  (mutual recursion between the fns), which sequential compiled let* lacks
  — a later tier.

Gate green: conformance 267x3, fallback-zero 31/5, bootstrap-fixpoint
stage1==2==3, self-host, staged-bootstrap, clojure-test-suite >=4034/67,
features 78/78, all unit + protocol/multimethod/macro specs.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-09 14:20:38 -04:00

79 lines
4.6 KiB
Text

# Specification: destructuring (in let, fn, loop, doseq, for).
(use ../support/harness)
(defspec "destructure / sequential"
["basic vector" "3" "(let [[a b] [1 2]] (+ a b))"]
["skip with _" "3" "(let [[_ b] [1 2]] (+ b 1))"]
["rest with &" "[3 4]" "(let [[a & more] [1 3 4]] more)"]
[":as whole" "[1 2]" "(let [[a :as v] [1 2]] v)"]
["nested" "3" "(let [[[a b]] [[1 2]]] (+ a b))"]
["fewer values nil" "nil" "(let [[a b c] [1 2]] c)"]
["over a list" "1" "(let [[a] (list 1 2)] a)"]
["over a seq" "2" "(let [[a b] (rest [9 1 2])] b)"]
["string chars" "\\a" "(let [[a] (seq \"ab\")] a)"])
(defspec "destructure / associative"
["keys" "3" "(let [{:keys [a b]} {:a 1 :b 2}] (+ a b))"]
[":as map" "{:a 1}" "(let [{:as m} {:a 1}] m)"]
[":or default" "9" "(let [{:keys [a] :or {a 9}} {}] a)"]
[":or present" "1" "(let [{:keys [a] :or {a 9}} {:a 1}] a)"]
["explicit binding" "1" "(let [{x :a} {:a 1}] x)"]
["nested map" "2" "(let [{{b :b} :a} {:a {:b 2}}] b)"]
["keys + as" "[1 {:a 1}]" "(let [{:keys [a] :as m} {:a 1}] [a m])"]
["map in vector" "1" "(let [[{:keys [a]}] [{:a 1}]] a)"])
(defspec "destructure / in forms"
["fn params" "3" "((fn [[a b]] (+ a b)) [1 2])"]
["fn map param" "1" "((fn [{:keys [a]}] a) {:a 1})"]
["defn destructure" "3" "(do (defn f [[a b]] (+ a b)) (f [1 2]))"]
["loop destructure" "3" "(loop [[a b] [1 2]] (+ a b))"]
["doseq destructure" "12" "(let [s (atom 0)] (doseq [[k v] {:a 4 :b 8}] (swap! s (fn [x] (+ x v)))) @s)"]
["for destructure" "[3 7]" "(for [[a b] [[1 2] [3 4]]] (+ a b))"]
["& rest in fn" "[2 3]" "((fn [a & more] more) 1 2 3)"])
(defspec "destructure / associative extras"
[":strs" "7" "(let [{:strs [a]} {\"a\" 7}] a)"]
[":syms" "8" "(let [{:syms [a]} {(quote a) 8}] a)"]
["namespaced :keys" "3" "(let [{:keys [x/y]} {:x/y 3}] y)"]
["namespaced :syms" "4" "(let [{:syms [p/q]} {(quote p/q) 4}] q)"]
# :keys also accepts keyword elements ({:keys [:a :b]}), binding bare locals.
["keyword :keys" "3" "(let [{:keys [:a :b]} {:a 1 :b 2}] (+ a b))"]
["keyword :keys ns" "3" "(let [{:keys [:x/y]} {:x/y 3}] y)"])
(defspec "destructure / keyword args (& {:keys})"
["fn kwargs" "[1 2]" "(do (defn f [& {:keys [a b]}] [a b]) (f :a 1 :b 2))"]
["fn kwargs + fixed" "[0 5]" "(do (defn g [x & {:keys [a]}] [x a]) (g 0 :a 5))"]
["fn kwargs :or" "9" "(do (defn h [& {:keys [a] :or {a 9}}] a) (h))"]
["fn kwargs trailing map" "7" "(do (defn k [& {:keys [a]}] a) (k {:a 7}))"])
(defspec "destructure / fn params & loop"
["fn vector param" "7" "((fn [[a b]] (+ a b)) [3 4])"]
["fn map param" "30" "((fn [{:keys [x y]}] (* x y)) {:x 5 :y 6})"]
["fn :or param" "7" "((fn [{:keys [x] :or {x 7}}] x) {})"]
["fn multi-arity destr" "15" "((fn ([[a]] a) ([[a] b] (+ a b))) [10] 5)"]
["loop vector binding" "[4 2]" "(loop [[a b] [1 2] n 0] (if (< n 3) (recur [(inc a) b] (inc n)) [a b]))"]
["loop map binding" "4" "(loop [{:keys [v]} {:v 1} n 0] (if (< n 2) (recur {:v (* v 2)} (inc n)) v))"]
["loop init sees destr" "[1 2 3]" "(loop [[a b] [1 2] c (+ a b)] [a b c])"])
# fn*/let*/loop* are PRIMITIVES: their binding forms must be plain symbols, exactly
# as in Clojure ("fn params must be Symbols" / "Bad binding form, expected symbol").
# Destructuring lives in the fn/let/loop/defn MACROS, which desugar to plain
# primitives — so the interpreter and the self-hosted analyzer agree (neither
# destructures fn*), and nothing silently falls back to a lenient interpreter path.
(defspec "destructure / primitives reject patterns (jolt-f79)"
["fn* fixed pattern" :throws "((fn* [[a b]] a) [1 2])"]
["fn* rest pattern" :throws "((fn* [a & [b]] b) 1 2 3)"]
["let* pattern" :throws "(let* [[a b] [1 2]] a)"]
["loop* pattern" :throws "(loop* [[a b] [1 2]] a)"]
# the macros still desugar the same patterns to plain primitives:
["fn desugars" "[1 2]" "((fn [[a b]] [a b]) [1 2])"]
["let desugars" "[1 2]" "(let [[a b] [1 2]] [a b])"])
(defspec "destructure / macro params"
["macro & [a & more :as all]"
"[1 [2 3] [1 2 3]]"
"(do (defmacro m [& [a & more :as all]] (list (quote quote) [a (vec more) (vec all)])) (m 1 2 3))"]
["macro fixed destructure" "[2 1]"
"(do (defmacro mm [[a b]] (list (quote quote) [b a])) (mm [1 2]))"]
["macro & {:keys}" "5"
"(do (defmacro mk [& {:keys [x]}] (list (quote quote) x)) (mk :x 5))"])