Skip to content

Release v0.13.0 - #178

Merged
skydread1 merged 36 commits into
mainfrom
develop
Sep 9, 2026
Merged

Release v0.13.0#178
skydread1 merged 36 commits into
mainfrom
develop

Conversation

@skydread1

Copy link
Copy Markdown
Member

v0.13.0 - 2026-09-09

nos build copies a library's C# assemblies into the build output, so shipping a compiled assembly to Unity takes no hand-written File/Copy, and Unity imports the plugin once instead of on every build. Saving a .clj, .cljc or .cljr re-evaluates it in the ClojureCLR Editor, on the one save rather than the third.

nos build also compiles with *unchecked-math* false, Clojure's default, so arithmetic and a narrowing cast throw on overflow instead of wrapping silently. Fifteen fixes across the compiler, runtime, stdlib and Mage align behaviour with JVM Clojure and ClojureCLR. The Editor runs ClojureCLR 1.11.0-flybot5.

Compiler

  • Calling something that is not a function throws a catchable InvalidCastException, the way JVM Clojure throws ClassCastException, instead of InvalidProgramException: Invalid IL code. The compiler boxes a value-typed callee before casting it to IFn, so the method it lands in verifies; (1 2) and (let [x (int 1)] (x 2)) each produced a method the JIT refused to load - #171.
  • deftype, reify and proxy bind a method to the slot it overrides rather than to one declaration of it, so a type can implement an interface that redeclares an inherited member. Writing count on clojure.lang.IPersistentMap used to fail with No match binding method, and naming the interface to get past it left the sibling declarations throwing NotImplementedException at run time. A return type hint on the method name picks between overloads that differ only in return type, as it does on ClojureCLR, and the compiler names the choices when the hint is missing - #146.
  • A narrowing cast on a type-hinted primitive throws when the value does not fit instead of discarding the high bits, so (int 4294967296) on a ^long throws ArgumentException rather than returning 0, as on ClojureCLR and JVM Clojure - #148.
  • A cast on a numeric literal behaves like the same cast at runtime: (int 1.5) returns 1 instead of 2, and an out-of-range literal like (int 4294967296) throws a catchable ArgumentException at runtime instead of aborting compilation with a bare OverflowException - #153.

Runtime

  • Casting a boxed UInt64 converts instead of throwing InvalidCastException, so (int (identity (ulong 1))) returns 1 - #151.

Stdlib

  • spit and writer truncate the file they overwrite, and :append appends, matching the JVM. A write used to open at position 0 without truncating, so shorter content produced a mix of new and old bytes, and :append was silently dropped - #155.
  • #object[...] prints the qualified type name, so (pr-str (System.Text.StringBuilder.)) names System.Text.StringBuilder instead of StringBuilder. print-tagged-object wrote .Name, which drops the namespace and cannot identify a type, while print-method on the type object already wrote .FullName - #142.
  • pr-str of an exception writes the :message value, so #error output carries the message instead of a bare :message key with nothing after it. The :via map was left with an odd number of forms - #158.
  • defn records the qualified type name in an arglist :tag, so a hint like ^Regex is stored as System.Text.RegularExpressions.Regex and resolves from any namespace instead of only where the import is in scope - #162.
  • sort and sort-by carry the collection's metadata through to the sorted seq - #163.
  • A namespace map prints its keys in the map's own order, so #:a{...} no longer reorders them once the map outgrows an array-map - #165.
  • clojure.pprint/pprint writes collection metadata when *print-meta* is true, so a pretty-printed value carries its metadata like pr does - #166.
  • clojure.repl/doc prints a special form's docstring once instead of repeating it after the "Please see" line - #167.
  • clojure.string/split drops trailing empty strings, and a negative limit returns every part. (split "a b " #" ") returned ["a" "b" ""]. split-lines gained a final "" on a trailing newline, and the negative limit threw. A pattern that matches nothing at the start no longer adds a leading "", so (split "abc" #"") returns ["a" "b" "c"] - #174.

Mage

  • il/type's short arities work, so a caller can write (il/type "Name" body) instead of spelling out attributes, interfaces, supertype, generic parameters and custom attributes every time. Every arity below the 7-arity threw ArityException - #143.

Nostrand

  • nos build compiles with *unchecked-math* false, Clojure's default, so arithmetic and narrowing casts keep their overflow checks instead of wrapping silently. A namespace that wants wrapping sets the flag itself - #149.
  • nos build copies the C# assemblies a library ships into the output dir, so a consumer no longer writes its own File/Copy to get them there. Point :csharp-out at a second dir to keep them out of :out, which :clean? deletes on every build: Unity then imports the plugin once and its GUID holds, instead of a new one on every build - #144.

Unity

  • The Editor loads ClojureCLR 1.11.0-flybot5, so both Editor runtimes agree on the stdlib fixes it carries. spit and writer truncate the file they overwrite, a printed double reads back equal, and (long x) on a boxed UInt64 converts. clojure.string/split drops trailing empty strings, a stack frame names the code that ran, and a string, map or record hashes to the JVM's value - clojure-1.11.0-flybot4, clojure-1.11.0-flybot5.
  • The package ships Magic.Unity.ClojureReloader, so saving a .clj, .cljc or .cljr re-evaluates it in the ClojureCLR Editor on the one save. A consumer's own FileSystemWatcher hook fires two or three events per save, so a reload took several saves before - #157.

…144)

The copy takes the assemblies the build loaded, so the loader that resolved
an :import is what selects the file to ship. :csharp-out sends them to a dir
of their own, which :clean? never wipes.
…mes (#144)

The parsed log fields become keywords at the parse boundary and the CLI state
becomes one at the entry point, so the expectation maps stop being keyed by
string.
…#144)

The committed DLL carried the assembly identity `nocomment`, because it was
built under a different `-out:` basename and renamed afterwards. Unity's
IL2CPP linker resolves a reference by identity, so it looked for
`nocomment.dll` and aborted the player build. Mono loads by file path, so
`nos dotnet/run-tests` never reached it.
A method written on a deftype, reify or proxy bound to a single MethodInfo,
matched on parameter types alone, so an interface that redeclares an inherited
member gave several indistinguishable candidates and the analysis reported no
match. Candidates now group by name, parameter types and return type, and one
group is one slot that the written method fills whole. The emitters skip a
NotImplementedException default for every declaration it covers. A return type
hint on the method name picks between groups whose return types differ.
…d conv (#148)

A type hint made (int x) discard the high bits instead of throwing, because
convert-type emitted an unchecked conv opcode whenever both sides were
primitive.

Reference Clojure calls the RT cast rather than emitting conv.ovf, which throws
a different exception and rejects NaN.
…fault (#149)

production-flags bound *unchecked-math* true, so every project built with
nos build wrapped on integer overflow and skipped the narrowing range checks.

The build blocks it replaced bound the flag to *warn-on-reflection*, which
reads false, so this was never what any project compiled under. repl.clj still
had the same line, where rebinding a var to its own value is the intent.
longCast(object) now returns the ulong local its type check already
produced instead of unboxing the box a second time as Int64, which the
CLR rejects for a UInt64 box. Casting any boxed ulong threw
InvalidCastException, and intCast(object) delegates here, so (int ...)
failed the same way.
#153)

reinterpret-value now leaves a Single or Double literal alone when the
target is an integer type, and catches OverflowException, deferring
both to the checked RT cast at runtime. Convert rounds where the cast
truncates ((int 1.5) compiled to 2), throws during compilation where
no user try/catch exists yet, and on Mono saturates a floating-point
source into a silently wrong constant.
file-mode returned FileMode/OpenOrCreate for every write, so an existing
file was opened at position 0 without truncating, and :append was ignored.
A plain write now opens with FileMode/Create and :append with
FileMode/Append, matching JVM Clojure.
print-tagged-object writes (.FullName c), so
(pr-str (System.Text.StringBuilder.)) names System.Text.StringBuilder and
agrees with print-method on the type object. (.Name c) drops the namespace,
and a short name does not identify a type. .FullName is what the JVM's
.getName returns.

The array branch changes with it, though arrays dispatch to the ICollection
print-method and never reach it.
print-throwable writes the :message value after its label, so an #error via
map carries the message. The call was absent, so the label was written with
nothing after it and the map held an odd number of forms.
resolve-tag writes (.FullName c), so a hint like ^Regex is stored as
System.Text.RegularExpressions.Regex and resolves from any namespace. It
wrote (.Name c), which drops the namespace, defeating the point of the
branch: it exists to turn a short tag into a name that stands on its own.

The analyzer infers an invoke's static type from the matching arglist's
tag, so this changes emission wherever a dotless tag is recorded.
sort wraps its result with (meta coll), so the sorted seq keeps the
metadata, as Clojure 1.10 does. sort-by goes through sort and gains it too.
…165)

lift-ns collects [key value] pairs into a vector and print-prefix-map takes
those pairs, so #:a{...} prints in the order the map seqs. lift-ns used to
accumulate into a map and rebuild with (apply conj (empty m) lm), so once
the accumulator outgrew an array-map the original order was gone.
qualified-ident? replaces the (or (keyword? k) (symbol? k)) guard, which
matches what upstream did in CLJ-2469.
…form once (#166, #167)

pprint-meta writes ^{...} before a list, vector, map or set when
*print-meta* is true, and pprint-set becomes a defn so it can call it.

print-doc moves the "Please see" block out of the special-form cond arm
into its own when, so the arm no longer prints the docstring that the
(when doc ...) below it already prints.
The 5-arity called type with 6 arguments and the next arity takes 7, so
every arity below the 7-arity threw ArityException. It now passes the
missing custom-attributes as [], the way method does directly below it.

The README's Quick Example goes back to the short form, and a new
magic.test.mage checks each arity against the full one.
ifn-invoke-compiler boxes a value-typed callee, so the cast to IFn runs on
an object reference and the method verifies. castclass needs a reference,
and the callee reached it unboxed, which made the JIT refuse the whole
method. JVM Clojure emits the same shape, and the arguments beside it
already convert the same way.

The conversion is guarded on the callee being a value type: converting
unconditionally sends an interface-typed callee through castclass Object
and re-emits every invoke in the tree.
skydread1 and others added 6 commits September 2, 2026 21:22
.NET's Regex.Split has none of Java's three split rules, so they are
applied here. Java drops trailing empty strings at limit 0. It returns
every part at a negative limit. It also drops a leading empty part when
the pattern matches nothing at the start.
…ditor (#157)

DebouncedFileWatcher debounces multiple events from FileSystemWatcher
upon one save. ClojureReloader polls this debounced event and retries
reads if there is any race condition with the write
The bump crosses flybot4 and flybot5, so the Editor runtime picks up
the spit and writer truncation, the double round-trip in fp-str, the
boxed UInt64 cast, the declaring type in a printed stack frame, the
clojure.string/split fixes, and the JVM hash for strings, maps and
records.

Clojure.dll still reports assembly version 1.11.0.0, so
bb coexist-noise asserts the same clojure-versions as before.
@skydread1 skydread1 self-assigned this Sep 9, 2026
@skydread1
skydread1 merged commit cc1992a into main Sep 9, 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.

2 participants