Conversation
if-compiler emitted (il/br end-label) after the else branch even when the else never returns (throw or recur). With both branches disregarded the fn skips its return, so end-label lands at the method end and the dead br targets past the last instruction, which the CLR rejects (VerificationException). disregard-type? and always-throws? also anded both if branches without honoring a constant test, so cond's (if :else expr nil) expansion was mis-analyzed. Emit the bridge only when the else can fall through, and collapse constant-test ifs in both predicates to match ast-type-impl.
A parameter hinted with a protocol name resolved to the protocol's generated interface, which MAGIC cast to at the invoke boundary. Extend/extend-protocol types are not instances of that interface, so the cast threw InvalidCastException, and the narrowing also broke member resolution in the body. types/resolve now keeps such a hint as Object, dispatching through the protocol fn as stock Clojure does. A qualified tag, which is what deftype/reify specs expand to, still narrows, so implementing a protocol is unaffected.
identical?/= between a named fn and its self-reference returned false where JVM and ClojureCLR return true. Two causes: reader source meta wrapped every fn in withMeta so the fn value was a MetaWrapper while the self-reference resolved to the wrapped instance, and under direct-linking the static body emitted the self-name as a fresh newobj. fn* parsing now strips line/column/file/source-span/rettag from fn meta like upstream FnExpr, and a method that uses the fn's own name as a value (not as a direct callee) keeps the instance compilation so the name resolves to this. Upstream's any-self-reference guard would strip invokeStatic from every recursive defn since MAGIC's defn names the fn form. The meta strip also fixes fn literals carrying reader source-location metadata: (meta (fn [] 1)) is now nil, matching JVM and ClojureCLR (#53).
Broad ripple: the old compiler emitted a runtime withMeta wrap for every literal fn expression, so every stdlib namespace with one changes.
read-project-deps wraps the missing/malformed deps-file read in an ex-info naming the file. Resolution warnings move to stderr so print-basis stdout stays parseable. Dedupe the ["src"] default.
fresh-type now filters all reusable TypeBuilders matching the requested shape, sorts them by type name with an ordinal comparator, and takes the first, instead of taking whatever `some` hit first in the unordered set. Ordinal rather than default string comparison because the committed DLLs are byte-diffed across machines and CLR default comparison is culture-sensitive.
compile-file reset RT._id per file-writing compile (#43) but not magic.util/*gensym-map*, which produces the type-name suffix. That counter accumulated across the build under the build driver's *ns*, so editing an unrelated nostrand namespace renumbered every committed DLL. Reset it alongside RT._id, guarded to file-writing compiles so host gensym uniqueness is untouched.
Mechanical rebuild output: each namespace's first-form type name resets to a per-file value instead of a build-accumulated one.
The MagicUnity deploy target has excluded clojure.tools.analyzer* since the monorepo's initial commit, so the nine Export copies were never refreshed (last compiled 2022, embedded /home/nasser paths) and nothing in the package references them, yet they shipped in every player build. Delete them and their metas, regenerate the dual variant, and update the coexistence-noise baseline from 46 to 37 narration lines.
CLOJURE_LOAD_PATH was joined from the raw declared :paths (relative), while *load-paths* already held absolute paths. A loader scanning CLOJURE_LOAD_PATH for a native assembly resolved relative roots against the current directory. Absolutize the entries so both load-path surfaces agree and a loader finds files from any cwd, as on stock ClojureCLR.
How to load a precompiled C# assembly on the CLR, where :import does not: a loader namespace that scans CLOJURE_LOAD_PATH and assembly-load-froms the DLL. Linked from the README and the porting guide.
Candidate types for loop bindings were reduced in hash-set iteration order, and best-type breaks ties by keeping the argument it saw first, so a tie-prone loop could infer different binding types across processes and emit different bytes from unchanged sources. Sort the candidate sets and best-type's common-ancestor seqs with an ordinal comparator before reducing, the same shape as reusable-type selection.
…erence-order fix (#68)
…ranch Issues auto-close on merge into develop (the default branch), not main. Also documents branch naming and that issue references belong in commits and PR descriptions only, never in source.
Unresolved type errors now name a load-path DLL matching the type's namespace prefix. The deferred :import path warns on stderr instead, since its message is baked into emitted bytes.
Asks the loaded Clojure.dll assembly for its own location, so the answer is the truth for the running host rather than a re-derived install convention.
…75) Replace --exclude=* with --tags --match='v*' so builds at a tag checkout stamp v0.10.0-0-g<hash> instead of a bare commit hash. --tags is needed because the release workflow checkout stores the pushed tag as a lightweight ref, invisible to git describe otherwise.
The Mono branch registered two dynamic assemblies per call and discarded the first, leaking one dead assembly per eval. Leftover debug code from upstream 7c37afe.
Export and magic-unity-dual copies.
…79) Dispatching ::missing-instance-method-arity threw an ArityException from the reporter itself, masking the diagnostic.
…ssing source (#81) The nine deps-vendored tools.analyzer namespaces are in bootstrap-namespaces but missing-src was computed before removing that set, so every run warned for them. Count bootstrap-owned DLLs in the summary instead; a genuinely sourceless DLL still warns.
#83) load-constant enumerated its supported types and threw for anything else, so #inst (System.DateTime), #uuid (System.Guid) and any custom tagged literal in compiled source could not compile. The :default method now mirrors the stock compilers' ObjExpr.EmitValue fallback: print the constant under *print-dup*, emit RT.readString on the string, and convert the result to the constant's static type (unbox.any for value types).
…#85) load-integer emitted ldc.i4 through a checked int cast, which throws for UInt32 constants above Int32/MaxValue. ldc.i4 is a bit-level load, so use unchecked-int, matching what csc emits for a uint constant.
load-constant passed the raw UInt64 to il/ldc-i8, and mage's emit fallthrough resolves the Emit overload reflectively from the operand's runtime type. No ulong overload exists, so a wrong-width overload encoded the operand and corrupted the method's instruction stream. Cast to unchecked-long so the operand hits the Int64 overload, same shape as the UInt32 fix.
…ss (#87) convert-type picked the widening conv opcode from the target type, so extension to an 8-byte integer ignored the source's signedness. UInt32 widened to Int64 via conv.i8 and sign-extended, making (long UInt32/MaxValue) return -1, and signed values widened to UInt64 via conv.u8 and zero-extended. Emit conv.u8 for unsigned sources and conv.i8 for signed ones.
convert-type only cast Object to Int32, Int64, Byte, Single and Double; Char, SByte, Int16, UInt16, UInt32 and UInt64 fell through to unbox.any and threw InvalidCastException on a boxed value of another type. Add the missing cases via the matching RT.*Cast / RT.unchecked*Cast methods.
Arithmetic computed in the operand's own type, so unsigned narrow ints used signed .ovf opcodes in 32 bits and wrapped: (inc UInt32/MaxValue) was 0. Promote Byte, UInt16 and UInt32 to Int64, and have inc and dec convert their argument to the computed type.
Arithmetic intrinsics computed in the operand's own type, so narrow integers overflowed or wrapped: (inc Int32/MaxValue) threw instead of giving the long 2147483648. Widen the whole narrow family (Byte, SByte, Int16, UInt16, Int32, UInt32) to Int64, matching ClojureCLR's long-based numeric tower. UInt64 stays as is: it cannot fit Int64, and ClojureCLR promotes it to BigInteger.
…ion (#93) Regenerate the compiler and affected stdlib DLLs across references and both magic-unity variants for the narrow integer promotion fix.
nos test selected tests only by namespace (:namespaces/:exclude/:re), so skipping one platform-specific test meant dropping its whole namespace. :exclude-vars names fully-qualified deftest vars whose :test meta is cleared after require, so clojure.test omits exactly those and still runs the rest.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v0.11.0 - 2026-07-24
Mostly compiler fixes: never-returning branches emitted invalid IL, protocol-hinted parameters threw
InvalidCastException, a named fn was notidentical?to its own self-reference, several numeric literals and integer operations emitted wrong bytes or overflowed instead of promoting, and the last known sources of nondeterministic DLL bytes are gone.Compiler
if/condforms whose branches never return (throw or recur on both sides) no longer emit a dead branch past the end of the method, which the CLR rejected with aVerificationException. Constant-test ifs, likecond's(if :else expr nil)expansion, are analyzed correctly too - #54.Objectand dispatches through the protocol fn, as stock Clojure does. The hint used to narrow to the protocol's generated interface, so passing anextend/extend-protocoltype threwInvalidCastExceptionat the invoke boundary. A qualified tag, which is whatdeftype/reifyspecs expand to, still narrows - #51.identical?/=between them returns true, matching JVM and ClojureCLR. Reader source-location metadata is stripped from fn literals like upstream, so(meta (fn [] 1))isnil- #52, #53.ArityExceptionfrom the error reporter itself, masking the diagnostic - #79.#inst,#uuid, and any other literal without a dedicated emitter no longer throwload-constant not implementedwhen embedded in compiled source; they are written as aprint-dupstring and read back withRT.readStringat load - #83.UInt32aboveInt32/MaxValueno longer throwsValue was either too large or too smallduring emission, and aUInt64constant no longer emits corrupted IL through mage's untypedEmitdispatch - #85, #88.longzero-extends instead of sign-extending, so(long UInt32/MaxValue)is4294967295, not-1; the conversion opcode is chosen from the source type's signedness - #87.Objectto a narrow numeric type (Char,SByte,Int16,UInt16,UInt32,UInt64) goes through the matchingRTcast, so(char x)or(int x)on a boxed value no longer throwsInvalidCastException- #91.longlike Clojure's numeric tower, so(inc Int32/MaxValue)is2147483648and(inc UInt32/MaxValue)is4294967296instead of overflowing or wrapping to a smaller type - #92, #93.Deterministic compilation
Runtime
DefineDynamicAssemblycall, leftover upstream debug code, is gone - #77.Nostrand
nos whereprints the directory of the runtime the running host actually loaded - #72.CLOJURE_LOAD_PATHentries are absolute, so a loader scanning it finds native assemblies from any working directory, as on stock ClojureCLR - #62.deps.ednwithout:aliasesno longer throws aNullReferenceExceptionwhen aliases are requested, undeclared aliases warn on stderr like tools.deps does, and a missing or malformed deps file is reported by name - #55.nos testcan skip individualdeftestvars via:exclude-varsinmagic.edn's:testconfig, not just whole namespaces, so a lib with a few platform-specific tests still runs the rest - #96.Unity
clojure.tools.analyzer*DLLs, excluded from the deploy target since the monorepo's first commit and last compiled in 2022, are dropped fromExport/so they stop shipping in every player build - #66.Tooling
SourceRevisionIdstamped intoClojure.dllandMagic.Runtime.dllincludes release tags, so from this release on a build at a tag checkout stamps0.11.0+v0.11.0-0-g<hash>instead of a bare commit hash. Develop builds, where no release tag exists, keep the bare hash - #75.bb refresh-stdlibno longer flags the bootstrap-owned DLLs as missing source on every run - #81.Docs
docs/native-assemblies.md: loading a precompiled C# assembly on the CLR via a loader namespace that scansCLOJURE_LOAD_PATH, and how to rebuild the committed native DLLs byte-stably - #62, #74.