Problem
Loop binding-type inference reduces its candidate types in hash-set iteration order, which varies from process to process. A namespace with a tie-prone loop/recur can therefore infer different binding types on different runs and compile to different DLL bytes from unchanged sources, breaking the byte-reproducibility guarantee that bb check-drift and CI enforce.
There are two layers to it. First, the candidate reduce itself, in magic.analyzer.loop-bindings/infer-binding-types (and the same shape again in the :loop case of typed_passes.clj):
candidate-types (->> binding-types
(conj recur-expr-types)
(apply map hash-set) ;; one candidate set per binding position
(map #(reduce best-type %)) ;; folded in hash-set iteration order
vec)
Second, best-type itself breaks ties by keeping whichever argument it saw first, and its inputs also come out of set operations (ancestors returns a hash set, s/intersection keeps that order):
heaviest-interface (when-not (empty? common-interfaces)
(reduce
(fn [t1 t2]
(if (> (count (.GetInterfaces t2))
(count (.GetInterfaces t1)))
t2 t1)) ;; equal counts keep t1, so the winner
common-interfaces)) ;; depends on set iteration order
Concretely: if two candidate interfaces implement the same number of interfaces, the "heaviest" one is just whichever the hash set yielded first, and .NET hashes Type objects by runtime handle, so that order changes across processes.
This is the same identity-hash-order family as #60, in the inference path instead of the type-reuse path. No failing namespace is known yet (while pinning #60, instrumentation showed inference outcomes were stable for clojure.math.combinatorics), so this is a latent hazard rather than an observed failure, but the tie condition is reachable by ordinary code.
Suggestion
Sort every one of these type collections by name before reducing, exactly what #61 did for fresh-type in generated_types.clj. One helper in loop_bindings.clj:
(defn ordinal-sort [types]
(sort-by str #(String/CompareOrdinal %1 %2) types))
applied at the three order-sensitive points:
;; infer-binding-types (and the :loop pass in typed_passes.clj)
(map #(reduce best-type (ordinal-sort %)))
;; inside best-type
common-types (ordinal-sort (remove #(.IsInterface %) common-ancestors))
common-interfaces (ordinal-sort (filter #(.IsInterface %) common-ancestors))
Ordinal comparison rather than default compare, because CLR string comparison is culture-sensitive and check-drift byte-diffs across machines with different locales. Ties still exist after this, but they resolve the same way in every process, which is all determinism needs.
Problem
Loop binding-type inference reduces its candidate types in hash-set iteration order, which varies from process to process. A namespace with a tie-prone
loop/recurcan therefore infer different binding types on different runs and compile to different DLL bytes from unchanged sources, breaking the byte-reproducibility guarantee thatbb check-driftand CI enforce.There are two layers to it. First, the candidate reduce itself, in
magic.analyzer.loop-bindings/infer-binding-types(and the same shape again in the:loopcase oftyped_passes.clj):Second,
best-typeitself breaks ties by keeping whichever argument it saw first, and its inputs also come out of set operations (ancestorsreturns a hash set,s/intersectionkeeps that order):Concretely: if two candidate interfaces implement the same number of interfaces, the "heaviest" one is just whichever the hash set yielded first, and .NET hashes
Typeobjects by runtime handle, so that order changes across processes.This is the same identity-hash-order family as #60, in the inference path instead of the type-reuse path. No failing namespace is known yet (while pinning #60, instrumentation showed inference outcomes were stable for
clojure.math.combinatorics), so this is a latent hazard rather than an observed failure, but the tie condition is reachable by ordinary code.Suggestion
Sort every one of these type collections by name before reducing, exactly what #61 did for
fresh-typeingenerated_types.clj. One helper inloop_bindings.clj:applied at the three order-sensitive points:
Ordinal comparison rather than default
compare, because CLR string comparison is culture-sensitive and check-drift byte-diffs across machines with different locales. Ties still exist after this, but they resolve the same way in every process, which is all determinism needs.