Skip to content

Release v0.11.0#98

Merged
skydread1 merged 47 commits into
mainfrom
develop
Jul 24, 2026
Merged

Release v0.11.0#98
skydread1 merged 47 commits into
mainfrom
develop

Conversation

@skydread1

Copy link
Copy Markdown
Member

v0.11.0 - 2026-07-24

Mostly compiler fixes: never-returning branches emitted invalid IL, protocol-hinted parameters threw InvalidCastException, a named fn was not identical? 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/cond forms 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 a VerificationException. Constant-test ifs, like cond's (if :else expr nil) expansion, are analyzed correctly too - #54.
  • A parameter hinted with a protocol name stays Object and dispatches through the protocol fn, as stock Clojure does. The hint used to narrow to the protocol's generated interface, so passing an extend / extend-protocol type threw InvalidCastException at the invoke boundary. A qualified tag, which is what deftype / reify specs expand to, still narrows - #51.
  • A named fn's self-reference is the fn value itself, so identical? / = between them returns true, matching JVM and ClojureCLR. Reader source-location metadata is stripped from fn literals like upstream, so (meta (fn [] 1)) is nil - #52, #53.
  • When a type fails to resolve, the error hints at load-path DLLs matching the type's namespace prefix that are not loaded yet - #70.
  • Reporting a wrong-arity instance-method call no longer throws an ArityException from the error reporter itself, masking the diagnostic - #79.
  • #inst, #uuid, and any other literal without a dedicated emitter no longer throw load-constant not implemented when embedded in compiled source; they are written as a print-dup string and read back with RT.readString at load - #83.
  • Unsigned integer constants keep their exact bits: a UInt32 above Int32/MaxValue no longer throws Value was either too large or too small during emission, and a UInt64 constant no longer emits corrupted IL through mage's untyped Emit dispatch - #85, #88.
  • Widening an unsigned value to long zero-extends instead of sign-extending, so (long UInt32/MaxValue) is 4294967295, not -1; the conversion opcode is chosen from the source type's signedness - #87.
  • Casting an Object to a narrow numeric type (Char, SByte, Int16, UInt16, UInt32, UInt64) goes through the matching RT cast, so (char x) or (int x) on a boxed value no longer throws InvalidCastException - #91.
  • Integer arithmetic promotes narrow operands to long like Clojure's numeric tower, so (inc Int32/MaxValue) is 2147483648 and (inc UInt32/MaxValue) is 4294967296 instead of overflowing or wrapping to a smaller type - #92, #93.

Deterministic compilation

  • Reusable-type selection and loop binding-type inference iterate their candidate sets in a sorted order instead of hash order, removing the last known ways unchanged sources could compile to different bytes across processes - #60, #68.
  • The type-name gensym counter resets per file-writing compile unit, so editing an unrelated nostrand namespace no longer renumbers every committed DLL - #64.

Runtime

  • Every eval on Mono registered a dead extra dynamic assembly; the duplicate DefineDynamicAssembly call, leftover upstream debug code, is gone - #77.

Nostrand

  • New nos where prints the directory of the runtime the running host actually loaded - #72.
  • CLOJURE_LOAD_PATH entries are absolute, so a loader scanning it finds native assemblies from any working directory, as on stock ClojureCLR - #62.
  • A deps.edn without :aliases no longer throws a NullReferenceException when 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 test can skip individual deftest vars via :exclude-vars in magic.edn's :test config, not just whole namespaces, so a lib with a few platform-specific tests still runs the rest - #96.

Unity

  • Nine orphaned clojure.tools.analyzer* DLLs, excluded from the deploy target since the monorepo's first commit and last compiled in 2022, are dropped from Export/ so they stop shipping in every player build - #66.

Tooling

  • The SourceRevisionId stamped into Clojure.dll and Magic.Runtime.dll includes release tags, so from this release on a build at a tag checkout stamps 0.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-stdlib no longer flags the bootstrap-owned DLLs as missing source on every run - #81.

Docs

  • New docs/native-assemblies.md: loading a precompiled C# assembly on the CLR via a loader namespace that scans CLOJURE_LOAD_PATH, and how to rebuild the committed native DLLs byte-stably - #62, #74.

skydread1 added 30 commits July 17, 2026 11:22
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.
)

merge-aliases invoked the possibly-absent :aliases map as the lookup
fn, a bare NRE when deps.edn has no :aliases, and silently dropped
undeclared alias keywords. Nil-safe get lookup plus the tools.deps
warning on stderr.
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.
…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.
…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.
…hints under IL2CPP (#51 #54)

#54: if/cond where every branch throws used to emit a dead br past the method end.
#51: a ^Protocol param used to cast to the generated interface, which extend-protocol implementers fail.
#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).
skydread1 added 17 commits July 22, 2026 23:05
…#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.
@skydread1 skydread1 self-assigned this Jul 24, 2026
@skydread1
skydread1 merged commit 49163bc into main Jul 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant