unchained (Nim)
A compile-time-only Nim units library whose quantities are distinct float types and whose entire dimensional algebra runs inside term-rewriting macros over an integer QuantityPowerArray — so a checked value is a bare float at runtime, with no dimension data and no wrapper left behind.
| Field | Value |
|---|---|
| Language | Nim (requires "nim >= 1.6.0"; verified here on nim 2.2.4) |
| License | MIT (declared in unchained.nimble L6; there is no standalone LICENSE file in the tree) |
| Repository | SciNim/Unchained |
| Documentation | README.org (the primary reference) · generated API docs via the gen_docs nimble task |
| Key authors | Sebastian Schmidt (Vindaar) and contributors, under the SciNim scientific-computing org |
| Category | Library-level compile-time checking (no compiler support; a macro-based DSL on stock Nim) |
| Mechanism | distinct float unit types; dimensions are integer exponent arrays reduced and compared inside Nim macros at compile time |
| Exponent domain | ℤ — QuantityPower.power: int (quantities.nim L15–18) and UnitInstance.power: int (ct_unit_types.nim L31); no rationals |
| Checking time | Compile time (macros evaluated during semantic analysis); zero runtime checks and zero runtime dimension representation |
| Analyzed version | 426d72a (pinned clone, 2025-11-20; unchained.nimble version 0.4.8, git tag v0.4.8) |
| Latest release | v0.4.8 (git tag; the project publishes no GitHub "release", only the nimble/tag version) |
NOTE
unchained is this survey's Nim data point for the integer-exponent-array, compile-time-macro mechanism: the checker evaluates dimension arithmetic the author spelled out — reducing each side to a base-quantity power vector and comparing — with no compiler plugin and no Kennedy-style inference of unknown exponents (contrast F# and uom-plugin; see Kennedy's type system and the mechanism taxonomy). Its closest relatives are the type-level-integer libraries uom and dimensioned in Rust and mp-units in C++, but the comparison is instructive precisely where it breaks: unchained does its exponent arithmetic in macro code over a runtime seq rather than in a trait solver over typenum, and it keeps no kind/tag slot at all (free abelian group · type-system mechanisms). Wave-2 siblings: coulomb and squants (Scala), swift-units, measured (Kotlin), and the UCUM/QUDT data model. See the comparison capstone for the synthesis.
Overview
What it solves
unchained gives Nim programs dimensional analysis that is checked entirely at compile time and costs nothing at runtime. Every unit is a distinct float, and every operation between units is a macro that inspects the compile-time unit of each operand, does the dimensional bookkeeping, and emits ordinary float arithmetic. The README.org states the positioning in its first sentences (README.org L4–7):
Unchained is a fully type safe, compile time only units library. There is
*absolutely no* performance loss over pure float based code (aside from insertion
of possible conversion factors, but those would have to be written by hand
otherwise of course).The distinguishing ergonomic claim is that composite units need never be predeclared — arbitrary products synthesize their own type on the fly (README.org L26–33):
# unchained README.org L28-32 (illustration; nim 2.2.4)
import unchained
let x = 10.m * 10.m * 10.m * 10.m * 10.m
doAssert typeof(x) is Meter⁵Meter⁵ was never defined; the * macro built the type. This is the library's signature move and the reason its whole model is macro-driven rather than type-level.
Design philosophy
Three decisions define unchained against the other systems in this survey.
Units are distinct float, not a generic wrapper. The foundation is a short chain of distinct types (core_types.nim L17–24):
# unchained src/unchained/core_types.nim L17-24
type
Unit* = distinct FloatType
Quantity* = distinct Unit
CompoundQuantity* = distinct Quantity
Dimensionless* = distinct Quantity
UnitLess* = distinct DimensionlessMeter, KiloGram, Newton, and every composite like Meter⁵ are distinct descendants of these. Because Nim's distinct carries no representation overhead, a KiloGram occupies exactly the bytes of a float; there is no phantom field to elide (contrast uom's #[repr(transparent)] PhantomData struct — unchained needs no such guarantee because there is no wrapper to begin with). All the dimensional information lives in the name of the distinct type, and is recovered by macros re-parsing that name at compile time.
The dimension is data the macro computes, not a type the solver equates. A quantity's dimension is a QuantityPowerArray — a fixed-length seq of QuantityPower{quant, power: int}, one slot per base quantity (quantities.nim L11–23). Operators reduce both operands to this array and compare with a plain structural == (quantities.nim L123–128). Nothing is solved for; the macro evaluates the arithmetic the author wrote, exactly as uom's trait solver evaluates typenum sums — but here in imperative Nim run at compile time.
The unit system is user-declarable from scratch. The built-in SI is itself just a use of the public DSL — declareQuantities then declareUnits (si_units.nim L8–43, L53–251). A downstream module can import only the api/ct_api submodules and declare an entirely different set of base quantities and units (examples/custom_unit_system.nim), so "SI" is a default, not a hardwired assumption.
How it works
There is no procedural macro, no build step, and no runtime library of dimensions — everything is macro code executed during semantic analysis.
Declaring quantities and units (the DSL)
declareQuantities parses a Base:/Derived: block into CTQuantity objects and generates the distinct quantity types plus a QuantityKind enum (quantities.nim L187–353). The SI base set is seven quantities (si_units.nim L8–16):
# unchained src/unchained/si_units.nim L8-40 (abridged)
declareQuantities:
Base:
Time
Length
Mass
Current
Temperature
AmountOfSubstance
Luminosity
Derived:
Velocity: [(Length, 1), (Time, -1)]
Force: [(Mass, 1), (Length, 1), (Time, -2)]
Energy: [(Mass, 1), (Length, 2), (Time, -2)]
Torque: [(Mass, 1), (Length, 2), (Time, -2)]
# ...A derived quantity is a named list of (base, power) pairs — an integer exponent vector spelled out by the author. declareUnits then attaches concrete units to those quantities, marking base units and giving non-base units a conversion: to an existing unit (si_units.nim L53–251), e.g. Meter (base, quantity Length), Newton (derived, quantity Force), and Pound (conversion: 0.45359237.kg).
The value type and the parse-compute-emit loop
At runtime a unit value is a distinct float. At compile time, each arithmetic macro runs the same three-step loop:
- Parse both operands' types back into a
UnitProduct— aseqofUnitInstance{unit, prefix, power: int, value}(ct_unit_types.nimL27–39) — viaparseDefinedUnit. - Compute the result: reduce each side to a
QuantityPowerArray, compare, and (for*//) add or subtract the exponent vectors, simplifying and merging SI prefixes. - Emit a
quote do:block that (a)defUnits the freshly computed result type if it does not yet exist and (b) performs the underlyingfloatoperation, inserting literal scale factors only when a prefix/unit conversion is required.
The * macro is the canonical example (units.nim L312–353):
# unchained src/unchained/units.nim L312-327 (abridged)
macro `*`*[T: SomeUnit|SomeNumber; U: SomeUnit|SomeNumber](x: T; y: U): untyped =
var xCT = parseDefinedUnit(x)
let yCT = parseDefinedUnit(y)
# ... reduce, add exponent vectors, simplify, merge prefixes ...
let resType = xCT.simplify(mergePrefixes = true).toNimType()
result = quote do:
defUnit(`resType`)
`resType`(`xr`.FloatType * `yr`.FloatType)The emitted body is a bare float multiply cast to the computed distinct type; the defUnit(resType) line is what makes Meter⁵ (or any never-before-seen composite) materialize as a type. SomeUnit is a Nim concept — concept x: isAUnit(x) (units.nim L13–15) — so the operators match any unit type and reject non-units at overload resolution.
Dimension representation
A dimension is a fixed-length integer exponent vector, the QuantityPowerArray. Its element type is QuantityPower{quant: CTBaseQuantity, power: int} and the array has one slot per declared base quantity (quantities.nim L11–23):
# unchained src/unchained/quantities.nim L15-23
QuantityPower* = object
quant*: CTBaseQuantity
power*: int
QuantityPowerArray* {.requiresInit.} = object
data: seq[QuantityPower]Equality of dimensions is a component-wise integer comparison (quantities.nim L123–128), and commonQuantity is defined as "same reduced power array" (define_units.nim L47–54):
# unchained src/unchained/define_units.nim L47-54
proc commonQuantity*[...](a: T; b: U): bool =
let aQuant = a.toQuantityPower()
let bQuant = b.toQuantityPower()
result = aQuant == bQuantThree consequences follow directly from this representation:
- Powers are integers, full stop.
QuantityPower.powerandUnitInstance.powerare bothint. The source even flags the ceiling in a comment on the field: "we could make the power aRationaland that way supportsqrtand things in a ~ reasonable way without having to rely on float hacks" (ct_unit_types.nimL31–33). Rational exponents are absent by construction — a finding shared withuomanddimensionedand contrasted by mp-units. - Normalization is explicit and imperative. Unlike
uom's trait-object identity (where two spellings of the same exponent vector are literally the same type),unchainedmust reduce eachUnitProduct— flattening compounds, merging duplicate bases, sorting — in macro code (simplify/flatten,define_units.nimL231, L281) before comparing. Correctness rests on that reduction, not on the type system. - The derived-quantity name is not part of the dimension. Because
commonQuantitycompares only the reduced base-quantity vector, two derived quantities with the same base decomposition are dimensionally identical.TorqueandEnergyare both declared(Mass, 1), (Length, 2), (Time, -2)(si_units.nimL25–26), sounchainedtreats them as one dimension — there is no kind/tag slot to separate them (see Expressiveness edges). This is the sharpest structural contrast withuom'sKind.
Dimensionless is the all-zero vector, surfaced as the UnitLess distinct type with a converter to float so unitless ratios flow into std/math (core_types.nim L23–24, units.nim L50–53).
Checking & inference
All checking happens inside the operator macros, during semantic analysis. Each of +, -, *, /, ==, <, <= is a macro over [T: SomeUnit|SomeNumber; U: SomeUnit|SomeNumber] that parses both operands, and:
- if the two
UnitProducts are identical, emits the raw float op; - else if
commonQuantityholds (same reduced dimension, different unit/prefix), converts both to the base type and emits float ops with literal scale factors; - else calls
error(...)— a hard compile error raised from within the macro (units.nimL238–265 for+, L267–296 for-, L188–204 for==).
So the "type checking" is not the Nim type system equating types; it is the macro evaluating the dimensional arithmetic and choosing to emit code or an error. In the mechanism taxonomy this is the "checker evaluates, never solves" row, alongside uom and dimensioned and opposite the AG-unification of F# / uom-plugin.
Forward inference is good; backward inference is absent. Because the result type is computed by the macro, let v = length / time gives v its Velocity-shaped type with no annotation, and Meter⁵ appears from 10.m * ... * 10.m. What cannot happen is inferring an operand's dimension from a desired result: there is no Kennedy-style principal type, and sqrt's argument dimension is not derived from an expected output.
Dimensional polymorphism uses Nim concepts. Every quantity gets a generated concept that matches any unit of that quantity (define_units.nim L746–766):
# unchained src/unchained/define_units.nim L762-766 (generated per quantity)
type
Length* = concept x
isAUnit(x)
isQuantity(x, Length)So a function can be written against a quantity, accepting metres, kilometres, or inches alike (README.org L74–85):
# unchained README.org L78-85 (illustration; nim 2.2.4)
proc force[M: Mass, A: Acceleration](m: M, a: A): Force = m * a
let f = force(80.kg, 9.81.m•s⁻²)
doAssert typeof(f) is Newton
doAssert f == 784.8.NThis is genuinely more ergonomic than uom's seven-fold typenum where-clauses for the same generality — the concept hides the exponent bookkeeping — though it inherits the same limitation that the return dimension (Force) must be nameable, not solved for.
Extensibility
Extension is a strength, and it comes in three tiers grounded in shipped code:
- New composite units — automatic. Any product/quotient synthesizes its result type via the
defUnitemitted inside the operator macro; no declaration is needed before10.m * 10.myieldsMeter². To name a composite for use in a signature ortotarget,defUnit(km•h⁻¹)declares it explicitly (units.nimL125–185);toDefboth defines and converts in one step (units.nimL475–498). - New named units on an existing quantity — a
declareUnitsentry with aconversion:to a known unit. The shipped SI adds dozens this way (Pound,ElectronVolt,LightYear, …,si_units.nimL126–251), and SI-prefixed variants are generated in bulk bygenerateSiPrefixedUnits(si_units.nimL268–289). - A whole new system —
declareQuantities+declareUnitsfrom scratch.examples/custom_unit_system.nimbuilds a toy system with base quantitiesLine,Triangle,Circle, … and derivedCar = (Circle, 4), (Rectangle, 1), then does dimensional analysis over them (custom_unit_system.nimL11–75). The base-quantity set is not closed the wayuom's per-system exponent length is: you declare exactly the bases you want.
The one real limitation is that the definitions populate global compile-time tables — QuantityTab/BaseQuantityTab (quantities.nim L55, L57) and the global unit registry UnitTab (a UnitTable, define_units.nim L15), all {.compileTime.} vars. There is a single active unit registry per compilation, so a module that import unchained gets SI and cannot trivially host a second, independent system alongside it in the same scope; a custom system imports the bare api/ct_api instead of the SI-populated unchained.
Expressiveness edges
Fractional powers: absent. Exponents are
int, sosqrtsucceeds only when every component power is even; otherwise the macro raises a compile error (units.nimL393–416):nim# unchained src/unchained/units.nim L406-410 for u in mitems(mType.units): if u.power mod 2 == 0: # can be divided u.power = u.power div 2 else: error("Cannot take the `sqrt` of input unit " & $(typ.toNimType()) & " as it's not a perfect square!")sqrt(1.m)therefore fails at compile time (reproduced below);sqrt(1.m²)gives1.m. The library is clever enough to flatten derived units first, sosqrt(1.W•Ω⁻¹)succeeds as1.AbecauseW·Ω⁻¹reduces toA²(units.nimL399–403, test attests/tunchained.nimL1078–1083). But there is no type forL^(1/2), so noV/√Hz-style noise-density idiom.Affine / temperature quantities: absent. The only temperature base unit is
Kelvin(si_units.nimL68–70); there is noCelsius/Fahrenheitoffset unit anywhere in the tree, and every non-base unit is defined by a pure multiplicativeconversion:factor (si_units.nimL126–251) — the DSL has no slot for an additive origin. There is thus no point-vs-difference distinction, noQuantityPointanalogue; this is the torsor / affine-quantity model left entirely unimplemented (contrastuom's temperature-only affine handling and mp-units' generalquantity_point).Logarithmic quantities: absent. No decibel, neper, or level quantity exists in the unit tables; the
Bel/dBidiom is out of scope, as in most systems here (Pint being the exception).Angles are dimensionless, kept apart only as a unit.
Angleis declared as a derived quantity(Length, 1), (Length, -1)(si_units.nimL36), which reduces to the all-zero vector — so its dimension is literally dimensionless (aquantityOf(1.rad)renders empty, matchingUnitLess; verified against the pinned clone).RadianandSteradiansurvive as distinct units only because they are flaggedautoConvert: false, which stopsflattenfrom silently dropping them (si_units.nimL116–124). Sounchainedkeepsradvisible in types but does not give angle a dimension or kind of its own.Kind-vs-dimension disambiguation: none — same dimension means interchangeable. Because
commonQuantitycompares only reduced base vectors and there is no kind slot, quantities that share a dimension are freely combinable.Torque(N•m) andEnergy(J) shareM·L²·T⁻²;Frequency(Hz) andActivity(Bq) shareT⁻¹— and both pairs add without complaint [reproduced locally,nim 2.2.4(nixpkgs), 2026-07-04]:text# against the pinned clone let torque = 5.N•m let energy = 3.J echo torque + energy # => 8 N•m (typeof: Joule) echo typeof(2.Hz + 4.Bq) # => HertzThis is the exact capability
uom'sKindmechanism exists to provide, andunchaineddoes not have it: it is a pure dimension checker, not a quantity-kind checker. TheQuantityKindenum it generates (quantities.nimL311–324) is used for pretty-naming and lookup, not for gating operations.Automatic unit/prefix reconciliation on
+/-/comparison. Where a dimension does match but units differ, the macro converts to the base unit and inserts the scale factor, so5.kg + 5.lbscompiles and yields kilograms (README.orgL34–40,units.nimL249–265). This is real ergonomic value the dimension-only model still delivers.
Zero-cost story
The zero-cost claim here is stronger and cheaper to justify than most in this survey, because the mechanism guarantees it structurally:
- A unit is a
distinct float.Unit* = distinct FloatType(core_types.nimL17–18), and Nim'sdistincttypes share the representation of their base with no added storage. There is no wrapper struct and no phantom field — unlikeuom, which needs#[repr(transparent)]+PhantomDatato reach the same guarantee,unchainedhas nothing to elide. - Arithmetic emits bare float ops. Every operator macro's emitted body is a plain
floatoperation cast to the result type, e.g.`resType`(`xr`.FloatType * `yr`.FloatType)(units.nimL327). No dimension object is constructed, compared, or stored at runtime; the entire check happened in the compiler. - The only runtime cost is deliberate conversion factors, and only when the author mixes prefixes/units of the same quantity — the macro multiplies by a compile-time literal scale (
units.nimL261–263). The README frames this precisely as the sole exception ("aside from insertion of possible conversion factors, but those would have to be written by hand otherwise" —README.orgL4–7).
The honest counterweight is that the cost is not eliminated but relocated to compile time (see Ergonomics & compile-time cost): the macro loop re-parses type names and manipulates seqs on every operation, and the source notes even that caching unit-name strings "is 50% slower than just regenerating them" (ct_unit_types.nim L53). Runtime is free; the compiler pays.
Diagnostics
The mandated experiment — adding a Meter to a Second — compiled against the pinned clone with nim c --path:$REPOS/nim/unchained/src:
# reproduced locally — mps.nim
import unchained
let x = 1.m + 1.s
echo xstack trace: (most recent call last)
.../unchained/src/unchained/units.nim(265, 10) +
mps.nim(2, 13) template/generic instantiation of `+` from here
.../unchained/src/unchained/units.nim(265, 10) Error: Different quantities
cannot be added! Quantity 1: m, Quantity 2: s[reproduced locally, nim 2.2.4 (nixpkgs), 2026-07-04]
This is the best-in-class end of the survey's diagnostics: the message is written in the domain's language — "Different quantities cannot be added! Quantity 1: m, Quantity 2: s" — not the implementation's. There is no typenum binary encoding to decode (as in uom's PInt<UInt<UTerm, B1>>), no nameless positional array (as in dimensioned): the macro rendered the offending units with their short names via pretty(...) in the error(...) call (units.nim L265). The one blemish is the leaked implementation frame — the stack trace points at units.nim(265,10) (inside the + macro) rather than only at the user's line, a normal artifact of a macro-raised error. The exact-line error is encoded in the repo's own compile-fail tests, which assert the rejection with a fails(...) (when compiles negation) template (tests/tunchained.nim L4–8, L115–118):
# unchained tests/tunchained.nim L115-118
test "Math: `+` of units - different quantities cannot be added":
let a = 10.kg
let b = 5.m
check fails(a + b)The two other edges reproduce the same way. sqrt of a non-square unit [reproduced locally, nim 2.2.4, 2026-07-04]:
# sqrt(10.m)
.../unchained/src/unchained/units.nim(410, 12) Error: Cannot take the `sqrt` of
input unit Meter as it's not a perfect square!and feeding a non-dimensionless quantity to sin fails at overload resolution, because only UnitLess has a converter to float (units.nim L50) — sin(10.m / 5.kg) reports a plain type mismatch against func sin(x: float64), the m•kg⁻¹ argument having no path to float [reproduced locally, nim 2.2.4, 2026-07-04]. The valid counterpart compiles and runs [reproduced locally, nim 2.2.4, 2026-07-04]:
# reproduced locally — same dimension, mixed units and prefixes
let sum = 5.kg + 5.lbs # => KiloGram (auto-converted)
let v = 100.m / 4.s # => Meter•Second⁻¹ (Velocity-shaped)
doAssert typeof(1.m * 1.m * 1.m) is Meter³ # composite synthesized on the flyErgonomics & compile-time cost
Surface ergonomics are excellent for the common cases — import unchained, then 10.m, 9.81.m•s⁻², 5.kg + 5.lbs, f.to(kN). Composites need no declaration to use in expressions, quantity concepts make dimension-polymorphic functions short, and error messages read in domain terms. The notable friction is syntactic: the product separator is the Unicode bullet • and exponents are Unicode superscripts (m•s⁻²), chosen deliberately to sidestep Nim's identifier rules (README.org L187–224). The library offers an ASCII-in-accented-quotes escape hatch — 10.`m*s^-2` — but the README concedes it "does not allow you to write actual unit names in function arguments or return types" (README.org L226–231). The separator is configurable via -d:UnicodeSep=· (core_types.nim L47).
Compile-time cost is the real price, and it is significant. Because every unit operation runs a parse-reduce-emit macro over seqs of unit instances — with string manipulation of type names throughout — dimension-heavy code is slow and memory-hungry to compile. The project's own test suite quarantines a regression test for exactly this reason, and the nimble file is candid about it (unchained.nimble L23–26):
# unchained unchained.nimble L23-26
task regressionTests, "Run regression tests (require cligen)":
# NOTE: the following even compiled before, but took 10 GB of RAM. In a CI this
# will fail for that reason, locally we just test it by hand
exec "nim c -r tests/test_issue04_modified.nim"A single stress test taking ~10 GB of compiler RAM — excluded from CI as unaffordable — is the clearest statement of the model's cost profile: zero at runtime, potentially very large at compile time, and super-linear in the complexity of the unit expressions the compiler must reduce. For typical scientific code the cost is unremarkable; for machine-generated or deeply nested unit expressions it can dominate the build.
Strengths
- True zero runtime cost, structurally guaranteed — units are
distinct floatwith no wrapper; arithmetic emits bare float ops. The zero-cost claim needs no benchmark because there is nothing at runtime to measure (core_types.nimL17–18,units.nimL327). - Best-in-survey diagnostics — errors are raised by the macro in domain language ("Different quantities cannot be added! Quantity 1: m, Quantity 2: s"), with no type-level encoding leaking into the message (
units.nimL265). - Composite units synthesize on the fly —
10.m * 10.myieldsMeter²andMeter⁵needs no predeclaration; a genuine ergonomic edge over predefine-everything systems (README.orgL26–33). - Fully user-declarable unit systems —
declareQuantities/declareUnitsbuild arbitrary base quantities and units; the base set is not fixed (custom_unit_system.nim). - Quantity
concepts give clean dimensional polymorphism — functions overMass,Length,Velocitywithout exponent-vector boilerplate (define_units.nimL746–766). - Automatic prefix/unit reconciliation — mixed-unit same-dimension arithmetic (
5.kg + 5.lbs) converts and works (README.orgL34–40).
Weaknesses
- Integer-only exponents — no
ℚpowers, sosqrtis partial (perfect squares only) and half-integer dimensions are unwritable; the source itself notesRationalpowers as an unrealized wish (ct_unit_types.nimL31–33). - No kind mechanism — same-dimension quantities are interchangeable:
Torque + EnergyandHz + Bqboth compile, so the classic collision pairs are not distinguished (reproduced;si_units.nimL25–26). A capabilityuomhas andunchainedlacks. - No affine/temperature or logarithmic quantities — only
Kelvin, only multiplicative conversions; no Celsius offset, no decibel (si_units.nimL68–70, L126–251). - Angles are dimensionless —
Angle = Length/Lengthcollapses to the zero vector;radsurvives only as a non-auto-converting unit, not a distinct dimension (si_units.nimL36, L116–124). - Heavy, unbounded compile-time cost — the macro reduction is slow and memory-hungry; a real regression test cost ~10 GB of compiler RAM and is barred from CI (
unchained.nimbleL23–26). - Unicode-heavy surface syntax —
•and superscripts are required for unit types; the ASCII escape hatch cannot be used in signatures (README.orgL187–231). - A single global unit registry per compilation — custom systems must avoid importing the SI-populated
unchained; two live systems don't trivially coexist (define_units.nimL15).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Units as distinct float, no wrapper struct | Zero-cost by construction — a unit is its scalar; nothing to elide | All dimensional info lives in the type name, forcing macros to re-parse names on every operation |
Dimension algebra in term-rewriting macros over an integer seq | Runs on stock Nim, no plugin; composites synthesize on the fly; domain-language errors | Cost moves to compile time and can explode (~10 GB RAM on a stress test); correctness rests on reduce/simplify |
Integer QuantityPower.power | Simple, decidable exponent arithmetic; matches SI's integer dimensions | No rational powers; sqrt partial; half-integer dimensions unrepresentable |
Compare only reduced base-quantity vectors (commonQuantity) | Automatic reconciliation of any same-dimension units; simple, uniform check | No kind/tag slot — Torque≡Energy, Hz≡Bq; derived-quantity names are cosmetic |
Multiplicative-only conversion: in the DSL | Keeps every unit a pure scaling of its base; conversions fold to compile-time literals | No affine/offset units (Celsius, temperature points); no torsor/point-vs-difference model |
User-declarable systems via declareQuantities/declareUnits | Not tied to SI; arbitrary base quantities; extensible without forking | Global compile-time registry — one active system per compilation; SI import blocks a second system |
Quantity concepts for polymorphism | Short, readable dimension-generic functions (proc force[M: Mass, A: Acceleration]) | The return dimension must be nameable; no inference of unknown exponents (no Kennedy principal types) |
Unicode • + superscripts for unit types | Circumvents Nim identifier rules so composites parse unambiguously | Awkward to type; the ASCII accented-quote escape hatch is unusable in signatures |
Sources
- SciNim/Unchained — GitHub repository (pinned locally at
$REPOS/nim/unchained@426d72a, 2025-11-20) README.org— positioning, zero-cost claim, on-the-fly composites, prefix reconciliation, concept polymorphism, syntax rationalesrc/unchained/core_types.nim— thedistinct floatunit chain, SI prefixes,UnicodeSepsrc/unchained/quantities.nim—QuantityPower/QuantityPowerArray,declareQuantities,QuantityKindenumsrc/unchained/ct_unit_types.nim—UnitInstance/UnitProduct, the integerpowerfield +RationalTODOsrc/unchained/units.nim— the operator macros (+,-,*,/,==,<),sqrt,defUnit,to,SomeUnitconceptsrc/unchained/define_units.nim—commonQuantity,flatten/simplify,isAUnit,generateQuantityConceptssrc/unchained/si_units.nim— the SIdeclareQuantities/declareUnitsinvocation; base units, derived quantities, prefixed-unit generationsrc/unchained/api.nim— the public API surface for custom unit systemsexamples/custom_unit_system.nim— a from-scratch non-SI systemtests/tunchained.nim— compile-fail (fails/when compiles) tests for cross-quantity math andsqrtunchained.nimble— MIT license, version0.4.8, the ~10 GB regression-test note- Local reproductions (
m + s,sqrt(m),sin(m/kg),Torque+Energy,Hz+Bq, valid ops): scratch programs compiled against the pinned clone withnim 2.2.4(nixpkgs), 2026-07-04 - Related deep-dives in this survey: type-system mechanisms · Kennedy's type system · free abelian group · torsors & affine quantities ·
uom·dimensioned· mp-units · F# units of measure ·uom-plugin· Pint · coulomb · squants · swift-units · measured · UCUM/QUDT · the comparison capstone