compiler: fix compile-mode correctness (conformance 87->218/218)

Running the conformance suite under compile mode surfaced many forms that
silently miscompiled (the hybrid fallback only catches compile-time errors,
not wrong results). Fixes:

- Global var resolution now mirrors the interpreter's resolve-var: current ns
  (which holds refers) then clojure.core, instead of interning an empty var in
  the current ns. This was the big one — every core fn not in core-renames
  (iterate, update, subvec, reductions, ...) derefed to nil from a user ns.
- Map literals evaluate their keys and values (were emitted as quoted data);
  build via build-map-literal, mirroring the interpreter (struct, or phm when a
  key is a collection).
- Vector literals build a mode-appropriate jolt vector via make-vec (pvec when
  immutable, array when mutable) instead of a bare Janet tuple, so compiled and
  interpreted vectors share one representation (type-strict ops like rseq
  rejected tuples).
- Core fn values resolve dynamically from the runtime env instead of a
  hand-maintained table that had drifted: core-apply mapped to Janet's native
  apply (rejects pvec tails), core-some mapped to core-some?. Removed the table
  and the bogus some rename.
- analyze-form throws uncompilable on interpreter special forms it doesn't
  implement and on definitional/host macros (deftype, defprotocol, reify,
  binding, letfn, read-string, regex/tagged literals, ...), so they fall back
  to the interpreter instead of miscompiling — including nested in compiled
  forms.

conformance-test.janet now runs every case under both interpreter and compiler
so compile-mode correctness is guarded in CI.
This commit is contained in:
Yogthos 2026-06-06 02:41:18 -04:00
parent acf724b638
commit 6297a65617
2 changed files with 137 additions and 132 deletions

View file

@ -299,27 +299,38 @@
["map literal in fn" "6" "(do (defn mk [a b] {:sum (+ a b)}) (:sum (mk 2 4)))"]
])
(var pass 0)
(def fails @[])
(each [name expected actual] cases
(def ctx (init))
(def prog (string "(= " expected " " actual ")"))
(def res (protect (eval-string ctx prog)))
(cond
(not= (res 0) true)
(array/push fails [name "ERROR" (string (res 1))])
(= (res 1) true)
(++ pass)
# not equal: re-eval actual alone to show what we got
(let [got (protect (eval-string (init) actual))]
(array/push fails [name "MISMATCH"
(string "want=" expected
" got=" (if (= (got 0) true) (string/format "%q" (got 1)) (string "ERR:" (got 1))))]))))
# Run every case under a given context factory and return the failures. The same
# cases run under both the interpreter and the compiler: results must match real
# Clojure semantics either way, so the compile path (hybrid: hot compiles,
# unsupported forms fall back to the interpreter) must not diverge.
(defn- run-cases [opts]
(def fails @[])
(each [name expected actual] cases
(def ctx (init opts))
(def prog (string "(= " expected " " actual ")"))
(def res (protect (eval-string ctx prog)))
(cond
(not= (res 0) true)
(array/push fails [name "ERROR" (string (res 1))])
(= (res 1) true)
nil
(let [got (protect (eval-string (init opts) actual))]
(array/push fails [name "MISMATCH"
(string "want=" expected
" got=" (if (= (got 0) true) (string/format "%q" (got 1)) (string "ERR:" (got 1))))]))))
fails)
(printf "\n=== CONFORMANCE: %d/%d passed ===" pass (length cases))
(unless (empty? fails)
(print "\n--- Failures ---")
(each [name kind detail] fails
(printf "[%s] %s: %s" kind name detail)))
(defn- report [label fails]
(printf "=== CONFORMANCE (%s): %d/%d passed ===" label (- (length cases) (length fails)) (length cases))
(unless (empty? fails)
(print "--- Failures ---")
(each [name kind detail] fails
(printf "[%s] %s: %s" kind name detail))))
(def interp-fails (run-cases {}))
(report "interpret" interp-fails)
(def compile-fails (run-cases {:compile? true}))
(report "compile" compile-fails)
(print)
(when (pos? (length fails)) (os/exit 1))
(when (or (pos? (length interp-fails)) (pos? (length compile-fails)))
(os/exit 1))