*unchecked-math* on macro-emitted arithmetic + local shadowing a bare native op

Two general fixes shaken out by clojure.test.check's own suite (its splittable
PRNG mixes 64-bit longs and binds locals named min/max).

- *unchecked-math* now wraps arithmetic a macro emits. The analyzer rewrote a
  bare (+/-/*) to its wrapping unchecked-* under *unchecked-math*, but a macro's
  syntax-quote produces clojure.core/* (qualified), which was skipped — so e.g.
  test.check's mix-64 multiply grew to a bignum instead of a 64-bit long. The
  rewrite now also fires on the clojure.core-qualified form.
- A local binding named like a bare-emitted native op no longer shadows it. ops
  where native-ops maps the name to itself (+ - * / < > min max …) emit as the
  bare Scheme name; a local `max` emitted the same token, so
  (fn [max] (clojure.core/max …)) called the param. munge-name now prefixes such
  locals, like reserved words (derived from native-ops so they can't drift).

make test green (+1 corpus row, 0 new divergences), shakesmoke byte-identical.
One re-mint (analyzer + backend).
This commit is contained in:
Yogthos 2026-06-27 18:19:14 -04:00
parent 75652de1ad
commit 992fc0af34
5 changed files with 131 additions and 114 deletions

View file

@ -636,9 +636,14 @@
(form-sym-name head)))
shadowed (and hname (local? env hname))
;; under *unchecked-math*, a core +/-/*/inc/dec becomes its wrapping
;; unchecked-* (computed once; nil when off or not such an op).
unm (when (and hname (not shadowed) (unchecked-math?))
(unchecked-arith hname (count items)))]
;; unchecked-* (computed once; nil when off or not such an op). The op
;; may arrive bare (+) or clojure.core-qualified (clojure.core/*), the
;; latter from a macro's syntax-quote — both must wrap.
unm (when (unchecked-math?)
(let [opn (cond (and hname (not shadowed)) hname
(and (form-sym? head) (= "clojure.core" (form-sym-ns head)))
(form-sym-name head))]
(when opn (unchecked-arith opn (count items)))))]
(cond
;; *unchecked-math* rewrite, before macro/special dispatch (these are
;; ordinary core fns). The unchecked-* form re-analyzes normally.

View file

@ -188,10 +188,19 @@
"unquote" "set!" "define" "define-syntax" "cond" "case" "when" "unless"
"and" "or" "do" "else" "guard" "parameterize" "delay" "values"})
;; clojure.core ops emitted as a BARE Scheme name (where native-ops maps the op
;; to itself: + - * / < > min max …). A local binding with one of these names
;; would otherwise shadow the emitted prim — e.g. (fn [max] (clojure.core/max …))
;; emits (max …) calling the param — so such locals are prefixed, like reserved
;; words. Derived from native-ops so the two never drift.
(def ^:private bare-native-names
(set (keep (fn [[k v]] (when (= k v) k)) native-ops)))
;; Most jolt names are already valid Scheme identifiers. The one that isn't is
;; `#`, which jolt auto-gensyms use as a suffix (p1__0000X4# from #(...)) — `#`
;; starts a datum in Scheme, so replace it with `_`. A name that collides with a
;; Scheme keyword is prefixed with `_` so it can never shadow the emitted form.
;; Scheme keyword OR a bare-emitted native op is prefixed with `_` so it can never
;; shadow the emitted form.
(defn- munge-name [s]
;; A Clojure symbol may contain chars that break a Scheme identifier: ' is the
;; quote reader macro (a bare f' would read as f then 'rest), # already maps to
@ -200,7 +209,7 @@
(let [s (-> s
(str/replace "#" "_")
(str/replace "'" "_PRIME_"))]
(if (contains? scheme-reserved s) (str "_" s) s)))
(if (or (contains? scheme-reserved s) (contains? bare-native-names s)) (str "_" s) s)))
(declare emit)