Tags and units¶
ll-lang has a single tag mechanism that covers two distinct use cases:
- Newtype-style labels — distinguish strings that happen to look alike
(
UserIdvsEmail) without runtime overhead. - Units of measure — dimensional analysis on numeric types at compile
time (
Float[m],Float[s], derivedFloat[m/s]).
Tags applied to the same base type with different names are incompatible — passing one where the other is expected is a compile error.
Declaring tags¶
tag UserId
tag Email
tag m
tag s
tag kg
A tag has no body — it's a pure type-level label.
Applying tags to values¶
Postfix [Tag] syntax:
uid = "user-42"[UserId] -- Str[UserId]
dist = 5.0[m] -- Float[m]
The base type is whatever the inner expression had; the tag is attached on
top. Internally this is TyTagged(baseType, UName tagName).
Tagged parameters¶
Write the full BaseType[Tag] in the annotation:
getUser(id Str[UserId]) Maybe[Str] = Some "alice"
sendEmail(to Str[Email]) = to
Calling getUser "raw-string" is a compile error (E005 TagViolation):
the argument has base type Str but no UserId tag. You must explicitly
tag the value: getUser "u1"[UserId].
Unit algebra (numeric tags)¶
When a tag is applied to a numeric base (Int or Float), the compiler
tracks units through arithmetic operations:
tag m
tag s
speed(d Float[m])(t Float[s]) = d / t
-- inferred return: Float[m/s]
Supported unit operations (compile-time only):
- Multiplication:
Float[m] * Float[s]→Float[m*s] - Division:
Float[m] / Float[s]→Float[m/s] - Addition / subtraction require matching units;
Float[m] + Float[kg]raisesE004 UnitMismatch - Exponentiation via
^:Float[m]^2→Float[m^2]
Non-numeric tags (Str[UserId], etc.) never compose — only identity check.
Phantom types for state machines¶
A type with a bracketed phantom parameter can carry state information with zero runtime cost:
tag Validated
tag Raw
Email[state] = Str
validate(s Str) Result[Email[Validated] Str] =
if s != ""
Ok s
else Err "empty"
Email[Validated] and Email[Raw] are distinct types even though both
erase to Str at runtime. A function declared as
send(e Email[Validated]) ... cannot be called with an unvalidated
email — the compiler enforces the state transition.
What's enforced¶
| Situation | Result |
|---|---|
Passing Str where Str[UserId] expected |
E005 TagViolation |
Passing Str[Email] where Str[UserId] |
E001 TypeMismatch (different tags, same base, non-numeric) |
Passing Float[kg] where Float[m] |
E004 UnitMismatch |
Adding Float[m] + Float[s] |
E004 UnitMismatch |
Float[m] / Float[s] |
OK, produces Float[m/s] |
Worked example from the corpus¶
From spec/examples/valid/03-tags.lll:
module Examples.Tags
Maybe A = Some A | None
Result A E = Ok A | Err E
tag UserId
tag Email
tag m
tag s
tag kg
uid = "user-42"[UserId]
getUser(id Str[UserId]) Maybe[Str] = Some "alice"
sendEmail(to Str[Email]) = to
speed(d Float[m])(t Float[s]) = d / t
tag Validated
tag Raw
Email[state] = Str
validate(s Str) Result[Email[Validated] Str] =
if s != ""
Ok s
else Err "empty"
Codegen note¶
Tags erase completely in the emitted F# source. Float[m] becomes float.
The checks all happen at compile time — runtime has no awareness of units.