Furth FortuneEngineering reference
TECHNICAL DOCUMENTATION

2027 · MerTek technology

On this page

Engineering reference / System overview

MerTekTypes

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

A brake inspection that keeps its units from tablet to office

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.

  1. 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
    }
  2. 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 })
    }
  3. 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:guid
  4. Check 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 document
What happened

The 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

One value across four seams

The logical name stays stable while each subsystem uses the physical form it needs.

  1. Trident duration retryDelay = 90s
  2. Registry logical type: duration
  3. MerDB typed duration column
  4. Rune checked wire representation

Inside the system

How the parts work together

Follow the data, decisions, and access boundaries. Each diagram explains a concept used by applications.

Map 01

Four identities of one type

A logical name, shape, wire form, and runtime accessor have separate jobs.

  1. Logical type program meaning
  2. Shape optional • array • scalar
  3. Wire form encoded bytes
  4. Accessor runtime read/write
Map 02

Canonical shape order

Container and qualifier syntax has one accepted order.

  1. Base type declared scalar
  2. Qualifier precision or dimensions
  3. Container optional or array
  4. Canonical name one stable shape
Map 03

Type families

Identity, time, exact number, measure, vector, text, and large data remain distinct.

  1. Identity + time guid • instant • duration
  2. Numbers integer • float • decimal
  3. Physical data measure • vector
  4. Content text • bytes • object ref
Map 04

Adding a type without drift

A new registry entry must cross language, storage, wire, and tool seams together.

  1. Append registry new logical type
  2. Language + DB declare + store
  3. Wire + accessor encode + read
  4. Proof all seams agree

Key parts

What the system does

These are the main boundaries, inputs, outputs, and failure rules. The examples show a specific use of each part.

One registry, every reader

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.

For example

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.

Meaning apart from bytes

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.

For example

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.

Shapes are part of the type

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.

For example

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

MerTekTypes in code

Each example is short and complete enough to read on its own. Hover a dotted word for its definition.

Trident

Durations and instants

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>
Trident

A guid is not a 16-byte integer

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`
Trident

Exact decimals and money

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
Trident

Measures with units

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>
Trident

Vectors are kinds of their own

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
Trident

Optional and array shapes

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

Example uses

  • Records that cross devices, stores and sites
  • Measurements that must keep their units
  • Identifiers and money that must stay exact

Review questions

What the team must decide

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

Contact us
FURTH FORTUNE / ENGINEERING REFERENCEBack to top ↑