Four identities of one type
A logical name, shape, wire form, and runtime accessor have separate jobs.
- Logical type program meaning
- Shape optional • array • scalar
- Wire form encoded bytes
- Accessor runtime read/write
2027 · MerTek technology
Engineering reference / System overview
One type registry for code, storage and the wire
MerTekTypes is the one table of value types that Trident, MerDB, Rune and the tools all read. A type such as duration, guid or f64<family=length> means the same thing in a program, a database column and an encoded message. Each type's meaning is kept apart from the bytes that store and carry it.
Worked example
The Wichita transit depot inspects brakes on 220 buses. A technician records pad thickness in millimetres, labor in dollars and cents, and the next inspection 30 days out. The record is written on a tablet, stored in MerDB and sent to the regional office, and each hop must keep the same meaning.
Write the record type in Trident. Each field names a registry type: guid for the work order, timepoint<unix> for the moment of inspection, f64<family=length> for the pad, decimal<scale=2> for labor and duration for the interval. The literal 7.5mm reads at the length type, and the value keeps one coherent amount: 0.0075 metres.
outline brakeReport {
guid workOrder = 00000000-0000-0000-0000-000000000000
text bus = ""
timepoint<unix> checkedAt = 0
f64<family=length> pad = 0mm
decimal<scale=2> labor = 0.00
duration nextDue = 0s
text? note = null
}Store it with the same spellings. The MerDB trove declares its columns with the same type names, so the database keeps the work order as a guid and the interval as a duration. A nullable column writes ? and defaults to null. Nothing maps a column type back to a program type, because both read the same registry entry.
# _db/troves/brakeCheck.tri
trove brakeCheck {
guid? workOrder = null
text bus = ""
timepoint<unix> checkedAt = 0
f64<family=length> pad = 0mm
decimal<scale=2> labor = 0.00
duration nextDue = 0s
index byBus = bus
}
# a/inspect.tri, on the tablet
brakeCheck recordCheck(text bus, f64<family=length> pad, decimal<scale=2> labor) {
return brakeCheck.insert({ bus = bus, checkedAt = t.clock.now(), pad = pad, labor = labor, nextDue = 30d })
}Encode it for the office. The Rune bytes carry every field's name and complete type beside its value, so the bytes describe themselves. Listing the fields of the decoded record prints the registry spelling of each one.
text main(text[] argv) {
brakeReport check = {
workOrder = 0190f3a2-7c41-7b9e-a2d4-5f8c1e6b9a30
bus = "Bus 4127"
checkedAt = 1760000000
pad = 7.5mm
labor = 142.50
nextDue = 30d
note = null
}
chest received = t.chest.decode(t.chest.encode(check))
chest[] fields = t.chest.fields(received)
text spelled = ""
foreach (row in fields) {
text name = row.name
text spelling = row.typeSpelling
spelled = spelled + name + ":" + spelling + " "
}
return spelled
}
# bus:text checkedAt:timepoint<domain=unix> labor:decimal<scale=2>
# nextDue:duration note:text? pad:f64<family=length> workOrder:guidCheck it on arrival. The office admits a record only when each field it needs has exactly the type it expects. An older tablet build that sends the work order as a u128 and the pad as a bare f64 is refused before any field is read. Bytes that are not a document at all fail t.chest.check.
text admit(buffer bytes) {
chest verdict = t.chest.check(bytes)
bool ok = verdict.ok
if (!ok) {
return "not a document"
}
chest received = t.chest.decode(bytes)
expect { guid workOrder, f64<family=length> pad, duration nextDue } = received else {
return "refused: a field is missing or has another type"
}
return "admitted pad " + text(pad)
}
# older build { u128 workOrder, f64 pad, duration nextDue } -> refused: a field ...
# new build -> admitted pad 0.0075
# t.buffer.fromText("pad=7.5") -> not a documentThe pad thickness left the tablet as a length, sat in MerDB as a length and reached the office as a length, with no conversion code at any hop. The office refused the 9 records from tablets that still ran last year's build and named the problem: a missing or retyped field. The 30-day interval stayed an exact span of 2,592,000 seconds the whole way.
System diagram
The logical name stays stable while each subsystem uses the physical form it needs.
Inside the system
Follow the data, decisions, and access boundaries. Each diagram explains a concept used by applications.
A logical name, shape, wire form, and runtime accessor have separate jobs.
Container and qualifier syntax has one accepted order.
Identity, time, exact number, measure, vector, text, and large data remain distinct.
A new registry entry must cross language, storage, wire, and tool seams together.
Key parts
These are the main boundaries, inputs, outputs, and failure rules. The examples show a specific use of each part.
Trident's compiler, MerDB's column reader and the Rune encoder look up the same entries. A type has one spelling, and a spelling the registry does not hold refuses in a program and in a column alike.
A trove column typed duration and a Trident local typed duration read the same entry, so both hold an exact clock span and both refuse 1mo.
The logical type says what a value means. The storage form and wire form are the bytes MerDB keeps and the network carries, and in MTK they are the same canonical document bytes. A unit or a display choice lives in the type; the bytes keep one coherent amount.
7.5mm at f64<family=length> is kept as 0.0075 metres. Declaring f64<family=length,unit=mm> changes how it prints, never the stored number.
Optional, array and fixed-size shapes belong to the complete type, at the depth they are written. Two types with the same byte width stay different types.
i32?[] is an array of nullable values and i32[]? is an array that may be absent; neither assigns to the other. A guid never passes as a u128.
Code
Each example is short and complete enough to read on its own. Hover a dotted word for its definition.
A timepoint carries its time domain, and two instants in one domain subtract to a duration. Moving between domains needs a named map, and a month is a calendar span, so neither happens by accident.
text main(text[] argv) {
timepoint<unix> checkedAt = 1760000000
duration cycle = 30d
timepoint<unix> nextDue = checkedAt + cycle
duration gap = nextDue - checkedAt
bool later = nextDue > checkedAt
return text(gap) # 2592000s
}
# duration billing = 1mo refuses at compile
# timepoint<tai> atomic = checkedAt refuses: no conversion exists
# between timepoint<domain=unix> and timepoint<domain=tai>
A guid and a u128 occupy the same 16 bytes and are still two kinds. An identifier never turns into a number that arithmetic or sorting could quietly misuse.
text main(text[] argv) {
guid workOrder = 0190f3a2-7c41-7b9e-a2d4-5f8c1e6b9a30
guid copy = workOrder
bool same = copy == workOrder
return text(workOrder)
}
# u128 raw = workOrder
# refuses: no conversion exists between `guid` and `u128`
A decimal states its scale and a money value states its currency. Arithmetic stays exact, a literal with too many places refuses, and two currencies meet only through a named exchange.
text main(text[] argv) {
decimal<scale=2> labor = 142.50
money<USD> parts = 86.25
money<USD> invoice = parts + 12.40
return text(labor) + " " + text(invoice) # 142.50 98.65
}
# decimal<scale=2> rate = 142.505 refuses: invalid literal
# money<EUR> shipping = 9.00
# money<USD> total = parts + shipping
# refuses: a currency changes only at a rate that names the pair
A unit can live in the type, as f64<family=length>, or in the value, as a measure. Either way the stored amount is coherent, and a length never assigns to a mass.
text main(text[] argv) {
f64<family=length> pad = 7.5mm
f64<family=length,unit=mm> shown = 7.5mm
measure oil = 91.5degC:temperature
return text(pad) + " " + text(shown) + " " + text(oil)
}
# -> 0.0075 7.5 91.5degC:temperature
# f64<family=mass> wrong = pad
# refuses: no conversion exists between f64<family=length>
# and f64<family=mass>
vec3 holds three binary32 lanes and dvec3 three binary64 lanes. Widening is exact; narrowing either converts exactly or faults and names the first lane that would lose digits.
text main(text[] argv) {
vec3<family=acceleration> mount = (0.0, 0.2, 9.8)
vec3 single = (1.0, 2.0, 3.0)
dvec3 wide = dvec3(single)
dvec3 precise = (0.1, 0.2, 0.3)
return "ok"
}
# vec3 narrow = vec3(precise) faults: t.math.cast: lane 0: digits
# f32[3] lanes = [1.0, 2.0, 3.0]
# vec3 v = lanes refuses: no conversion between f32[3] and vec3
A ? applies at the depth where it is written. A nullable value reads as a plain one only where a null test proves it present.
text main(text[] argv) {
text? note = null
i32?[] readings = [4, null, 6]
i32[]? backlog = null
f64[3] corners = [1.0, 2.0, 3.0]
i32 present = 0
foreach (r in readings) {
if (r != null) { present++ }
}
if (note == null) {
return `{present} readings, no note`
}
return note
}
# i32[] counts = [1, null] refuses: null needs a nullable element type
Uses
Review questions
Which fields carry their units or currencies only in their names
Which identifiers are stored as plain integers or strings
Which receivers must refuse a record whose field types changed