Skip to content

improvement!: replace Localize.Unit with a generated, dependency-free BB.Unit - #227

Draft
jimsynz wants to merge 1 commit into
mainfrom
spike/localize-free-units
Draft

improvement!: replace Localize.Unit with a generated, dependency-free BB.Unit#227
jimsynz wants to merge 1 commit into
mainfrom
spike/localize-free-units

Conversation

@jimsynz

@jimsynz jimsynz commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Spike: drop Localize from the runtime and ship a generated conversion module instead.

Not for merge yet — the generator is unreleased, so :localize is pinned to a git ref. Opening this to look at the shape and to give Kip a real consumer of mix localize.unit.gen_conversions.

Why

Localize is ~231 modules and 4.1 MB of CLDR locale data, plus decimal, gettext, nimble_parsec and an optional c_src NIF. All of it compiles and ships so that a robot can convert metres, radians and newton-metres. On a Nerves target that is more build cost than the project itself.

What the generator does

mix localize.unit.gen_conversions (elixir-localize/localize@b77cb38c, one commit past v1.0.1) emits a self-contained module. It takes no unit selection at all: rather than tabulating a chosen set, it inlines Localize's own unit parser — already written as a NimbleParsec template — expanded via mix nimble_parsec.compile, with the Localize.* references rewritten out. Parity with Localize.Unit is structural rather than maintained: there is no second grammar to keep in step.

Checked in here as lib/bb/unit/conversions.ex (4,670 lines, 154 KB, 3 modules). Regeneration is byte-identical across runs — the task strips its own path/timestamp banner deliberately.

Verification

All 23 distinct units the ecosystem uses resolve to the same base unit and factor as Localize itself, including the compounds that no --types-style selection could have reached:

Unit Base Agrees
newton-meter kilogram-square-meter-per-square-second
kilogram-square-meter kilogram-square-meter
newton-second-per-meter kilogram-per-second
newton-meter-second-per-{degree,radian} kilogram-square-meter-per-second-revolution
{degree,radian}-per-square-second revolution-per-square-second
…all 23

Cross-unit conversion was checked against Localize on 13 pairs (including newton-meter-second-per-degree → newton-meter-second-per-radian = 57.2957…, and meter → second which both refuse). Edge cases route correctly too: "bogus", "", "per-", a 5,000-character input (byte cap), "foot-and-inch" and "beaufort" all return {:error, {:unknown_unit, _}}.

mix check --no-retry is green: 1327 tests, credo --strict, dialyzer, reuse, formatter, ex_doc, spark cheat sheets. The test suite needed no changes beyond renaming the struct — no numerical drift.

API change

%Localize.Unit{} becomes %BB.Unit{name:, value:}, same two fields. compare/2, compatible?/2 and convert/2 are derived from the generated resolve/1. Unit errors are now BB.Error.Invalid.Unit.

to_string!/2 loses its :style option — localised rendering is the one thing genuinely not reconstructible. All five call sites were error messages, which now read 1.5 meter-per-second instead of 1.5 m/s.

Satellite impact is two lines: bb_servo_feetech and bb_servo_pigpio each call Localize.Unit.convert! once.

Cost

The prod dependency tree loses localize, decimal, gettext, nimble_parsec and expo. A target carries 3 modules / 177 KB in their place.

Against that, 154 KB of generated code is now committed and shows up in every CLDR-bump diff, and it needed two tooling exemptions:

  • credo — 37 readability hits, all CLDR constants like 149597870700.0 tripping the underscore check. New .credo.exs excludes the file.
  • dialyzer — one pattern_match_cov on an unreachable catch-all in parse_identifier/1. It is in the generator's hand-written runtime body, not the NimbleParsec expansion. Cosmetic (unknown identifiers are rejected during decomposition, not by the parser), but it fails a --warnings-as-errors gate. Parked in .dialyzer_ignore.exs.

The formatter needed nothing — the generator emits formatted code.

For Kip

Three things worth fixing upstream, none blocking:

  1. The dead clause above, which trips dialyzer in a file consumers are told to commit.
  2. No SPDX header on the generated file, so reuse lint fails until the consumer adds a .license sidecar. A --header option, or just emitting the consumer's licence, would help.
  3. Parser.parse/1 keeps @spec ... :: {:error, Exception.t()} after localize_free/2 rewrites the constructor to return a plain {:parse_error, bindings} tuple — likely the cause of (1).

Open questions

  • Persisted parameters. BB.Parameter.Store.Dets serialises unit values; anything already on disk holds %Localize.Unit{} and will not load into %BB.Unit{}. Needs a migration or a documented reset.
  • Whether losing localised unit rendering in validation errors matters.

…ee `BB.Unit`

Localize is ~231 modules plus 4.1MB of CLDR locale data, all of which had to
compile and ship for a robot that only ever converts metres, radians and
newton-metres. On a Nerves target that costs more than the project itself.

`mix localize.unit.gen_conversions` emits a self-contained conversion module:
CLDR's conversion tables plus Localize's own unit parser, expanded by
`mix nimble_parsec.compile` so the grammar is shared rather than
reimplemented. The result is checked in as `lib/bb/unit/conversions.ex` and
Localize becomes a `:dev`-only dependency, pinned to a git ref because the
task is not yet released.

`%Localize.Unit{}` is replaced throughout by `%BB.Unit{name:, value:}`, which
carries the same two fields. `compare/2`, `compatible?/2` and `convert/2` are
derived from the generated `resolve/1`, and agree with Localize on every unit
the DSL uses. `to_string!/2` loses its `:style` option — its five call sites
are all error messages — and an unknown or incompatible unit now raises
`BB.Error.Invalid.Unit` rather than a Localize exception.

The generated file is excluded from credo, whose underscore-in-large-numbers
check objects to CLDR constants such as `149597870700.0`, and carries one
dialyzer exemption for an unreachable clause in the generator's own runtime
body. Both are the generator's to fix; neither is editable here, since any
change is lost on regeneration.

The prod dependency tree loses localize, decimal, gettext, nimble_parsec and
expo; a target carries 3 modules and 177KB in their place.

BREAKING CHANGE: DSL unit values are `%BB.Unit{}` rather than `%Localize.Unit{}`.
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