main.bend source
main.bend on the hub · documented module
# eztoml: TOML for Bend 2, a proven parser and renderer for TOML documents.## eztoml reads and writes TOML documents. Values are strings, integers, floats,# booleans, datetimes, arrays, inline tables, tables, and arrays of tables.# Comments and blank lines are passed over.## parse, render, get, at, root, bad, string, digits, flag# str, integer, float, boolean, array, inline, table, pair# key, wfimport Base# the sign of an integer, a float, or a zone offsettype Sign is Data: Plus{} Minus{}# a calendar date: year, month, daytype Date is Data: Date{y: U32, mo: U32, d: U32}# a time of day. frac is the fractional second, empty when there is nonetype Clock is Data: Clock{h: U32, mi: U32, s: U32, frac: String}# a zone offset: Z, or a numeric offsettype Zone is Data: Zulu{} Shift{sign: Sign, h: U32, mi: U32}# the four TOML datetime formstype When is Data: WOff{date: Date, clock: Clock, zone: Zone} WLocal{date: Date, clock: Clock} WDate{date: Date} WTime{clock: Clock}# one TOML value. Tables remember the dotted path they were opened under.# An inline table is VInl. An array of tables is VAots, and each element is# VAot. VPair is a key in a table. VSpan is a plain string: the first n# characters of a source suffix.type Val is Data: VStr{text: String} VSpan{src: String, n: U32} VInt{sign: Sign, digits: String} VFlo{sign: Sign, text: String} VBool{bit: Bool} VWhen{when: When} VArr{items: List<&2, Val>} VInl{rows: List<&2, Val>} VHead{path: String, rows: List<&2, Val>} VAot{path: String, rows: List<&2, Val>} VAots{path: String, elems: List<&2, Val>} VPair{name: String, val: Val}# a key found in a table, or nottype Hit is Data: Found{val: Val} Miss{}# a parsed document: the first error, and the root table in ordertype Doc is Data: Doc{bad: String, rows: List<&2, Val>}# a value read from a bare word, or the reason it is not onetype Got is Data: GotVal{val: Val} GotBad{why: String}# digits taken from the front of a character listtype Dig is Data: DigOk{n: U32, cs: List<&2, Char>} DigBad{}# what a datetime scan producedtype WhenOut is Data: WSome{when: When} WFail{why: String} WSkip{}# a decimal numeral being built, least-significant digit firsttype NK is Data: NLead{} NSign{} NZero{} NDig{} NBase{} NFrac{} NExp{} NEDig{} NBad{}# the numeral scantype Num is Data: Num{nk: NK, sign: Sign, base: U32, ds: List<&2, U32>, us: Bool, saw: Bool, why: String}# how a basic-string character is writtentype BK is Data: BKCh{} BKHex{} BKEsc{mark: Char}# where the scanner is in a documenttype Lex is Data: LTop{} LLine{} LKey{} LKeyDot{} LAfter{} LKeyQ{} LKeyE{} LKeyU{} LVal{} LQ{} LQ2{} LStr{} LEsc{} LUni{} LFold{} LMq{} LMqEnd{} LTrim{} LTrim2{} LBare{} LDate{} LCom{} LHead{} LHeadGap{} LHeadDot{} LHeadQ{} LHeadE{} LHeadU{} LHeadR{} LArr{} LArrC{} LInl{} LInlK{} LInlC{} LSink{}# which quotes a string is usingtype QKind is Data: QBasic{} QLit{} QMulti{} QMultiLit{}# one character to read againtype Hold is Data: HNone{} HChar{c: Char}# an open array or inline tabletype Ctx is Data: CArr{items: List<&2, Val>, parts: List<&2, String>, here: List<&2, String>} CInl{rows: List<&2, Val>, parts: List<&2, String>, here: List<&2, String>}# the scannertype St is Data: St{bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, hold: Hold, uni: U32, left: U32, arrtab: Bool, lead: Bool}# an escape in a basic stringtype Esc is Data: EChar{c: Char} EUni{n: U32} EFold{saw: Bool} EBad{}# a character a bare key may holddef bare.at(+cc: Char) -> Bool: Bool.or(Char.is_alpha(cc), Bool.or(Char.is_digit(cc), Bool.or(Char.is_eq(cc, '_'), Char.is_eq(cc, '-'))))# one step of the bare-key walk, with the head's answer in handdef bare.all.step(here: Bool, rest: Unit -> Bool) -> Bool: match here: case True{}: rest(Unit{}) case False{}: False{}# whether every character of a key is one a bare key may holddef bare.all(cs: List<&2, Char>) -> Bool: match cs: case []: True{} case +h <> t: bare.all.step(bare.at(h), _u => bare.all(t))# whether a key can be written without quotesdef bare(+ss: String) -> Bool: Bool.and(Bool.not(String.is_empty(ss)), bare.all(String.to_list(ss)))# one hex nibble as a characterdef hex.char.go(dec: Bool, nn: U32) -> Char: match dec: case True{}: Chr{U32.add(48, nn)} case False{}: Chr{U32.add(87, nn)}# one hex nibble as a characterdef hex.char(+nn: U32) -> Char: hex.char.go(U32.is_lt(nn, 10), nn)# four lowercase hex digits of a code point, then the \u introducer, reverseddef basic.hex(+uu: U32, out: List<&2, Char>) -> List<&2, Char>: hex.char(U32.mod(uu, 16)) <> (hex.char(U32.mod(U32.shrn(uu, 4n), 16)) <> (hex.char(U32.mod(U32.shrn(uu, 8n), 16)) <> (hex.char(U32.mod(U32.shrn(uu, 12n), 16)) <> ('u' <> ('\\' <> out)))))# an escape mark reversed onto the output: the mark, then the backslashdef basic.esc(mark: Char, out: List<&2, Char>) -> List<&2, Char>: mark <> ('\\' <> out)# other controls are \u escapes; the rest are themselvesdef basic.cls.hex(hit: Bool) -> BK: match hit: case True{}: BKHex{} case False{}: BKCh{}# backslashdef basic.cls.bslash(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'\\'} case False{}: basic.cls.hex(Bool.or(U32.is_lt(uu, 32), U32.is_eq(uu, 127)))# quotation markdef basic.cls.quote(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'"'} case False{}: basic.cls.bslash(U32.is_eq(uu, 92), uu)# carriage returndef basic.cls.cr(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'r'} case False{}: basic.cls.quote(U32.is_eq(uu, 34), uu)# form feeddef basic.cls.ff(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'f'} case False{}: basic.cls.cr(U32.is_eq(uu, 13), uu)# line feeddef basic.cls.lf(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'n'} case False{}: basic.cls.ff(U32.is_eq(uu, 12), uu)# tabdef basic.cls.tab(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'t'} case False{}: basic.cls.lf(U32.is_eq(uu, 10), uu)# backspace, then the restdef basic.cls.bs(hit: Bool, +uu: U32) -> BK: match hit: case True{}: BKEsc{'b'} case False{}: basic.cls.tab(U32.is_eq(uu, 9), uu)# the class of a code point in a basic stringdef basic.cls(+uu: U32) -> BK: basic.cls.bs(U32.is_eq(uu, 8), uu)# one character of a basic string, reversed onto the outputdef basic.disp(kk: BK, cc: Char, uu: U32, out: List<&2, Char>) -> List<&2, Char>: match kk: case BKCh{}: cc <> out case BKEsc{mark}: basic.esc(mark, out) case BKHex{}: basic.hex(uu, out)# one character of a basic stringdef basic.one(+cc: Char, out: List<&2, Char>) -> List<&2, Char>: +u = Char.to_u32(cc) basic.disp(basic.cls(u), cc, u, out)# the escaped characters of a basic string, reverseddef basic.go(cs: List<&2, Char>, out: List<&2, Char>) -> List<&2, Char>: match cs: case []: out case +h <> t: basic.go(t, basic.one(h, out))# a basic string: quotes, with controls and quotes escapeddef key.basic(+ss: String) -> String: "\"" ++ String.from_list(List.reverse(&2, Char, basic.go(String.to_list(ss), []))) ++ "\""# bare when the name allows it, otherwise a basic stringdef key.go(ok: Bool, +ss: String) -> String: match ok: case True{}: ss case False{}: key.basic(ss)# a key as TOML writes itdef key(+ss: String) -> String: key.go(bare(ss), ss)# the overflow of a decimal step, least-significant digit firstdef dec.carry.c0(zz: Bool, cc: U32) -> List<&2, U32>: match zz: case True{}: [] case False{}: [U32.mod(cc, 10)]# the overflow of a decimal stepdef dec.carry.c(+cc: U32) -> List<&2, U32>: dec.carry.c0(U32.is_zero(cc), cc)# the next digit of an overflowdef dec.carry.b0(zz: Bool, +cc: U32) -> List<&2, U32>: match zz: case True{}: [] case False{}: U32.mod(cc, 10) <> dec.carry.c(U32.div(cc, 10))# the next digit of an overflowdef dec.carry.b(+cc: U32) -> List<&2, U32>: dec.carry.b0(U32.is_zero(cc), cc)# the first digit of an overflowdef dec.carry.a(zz: Bool, +cc: U32) -> List<&2, U32>: match zz: case True{}: [] case False{}: U32.mod(cc, 10) <> dec.carry.b(U32.div(cc, 10))# the digits of a leftover carrydef dec.carry(+cc: U32) -> List<&2, U32>: dec.carry.a(U32.is_zero(cc), cc)# multiply a decimal numeral by a radix and add a digitdef dec.go(ds: List<&2, U32>, +radix: U32, +carry: U32) -> List<&2, U32>: match ds: case []: dec.carry(carry) case +d <> t: U32.mod(U32.add(U32.mul(d, radix), carry), 10) <> dec.go(t, radix, U32.div(U32.add(U32.mul(d, radix), carry), 10))# a digit characterdef dec.ch(dd: U32) -> Char: Chr{U32.add(48, dd)}# digits as characters, in the order givendef dec.chars(ds: List<&2, U32>) -> List<&2, Char>: match ds: case []: [] case d <> t: dec.ch(d) <> dec.chars(t)# whether a leading zero is still being skippeddef dec.drop.lead(lead: Bool, +hh: Char) -> Bool: match lead: case False{}: False{} case True{}: Char.is_eq(hh, '0')# a leading zero is skipped; any other character is keptdef dec.drop.outz(zz: Bool, hh: Char, out: List<&2, Char>) -> List<&2, Char>: match zz: case True{}: out case False{}: hh <> out# the output after one character, reverseddef dec.drop.out(lead: Bool, +hh: Char, out: List<&2, Char>) -> List<&2, Char>: match lead: case False{}: hh <> out case True{}: dec.drop.outz(Char.is_eq(hh, '0'), hh, out)# the numeral, or 0 when every digit was zerodef dec.drop.end(lead: Bool, out: List<&2, Char>) -> String: match lead: case True{}: "0" case False{}: String.from_list(List.reverse(&2, Char, out))# drop leading zeros from a most-significant-first digit listdef dec.drop(cs: List<&2, Char>, +lead: Bool, out: List<&2, Char>) -> String: match cs: case []: dec.drop.end(lead, out) case +h <> t: dec.drop(t, dec.drop.lead(lead, h), dec.drop.out(lead, h, out))# a decimal numeral. The digit list is least-significant firstdef dec.text(ds: List<&2, U32>) -> String: dec.drop(dec.chars(List.reverse(&2, U32, ds)), True{}, [])# a code point's digit value, or 99 when the character is not a digitdef dig.up(ok: Bool, uu: U32) -> U32: match ok: case True{}: U32.sub(uu, 55) case False{}: 99# a lowercase hex digit, otherwise an uppercase onedef dig.low(ok: Bool, +uu: U32) -> U32: match ok: case True{}: U32.sub(uu, 87) case False{}: dig.up(Bool.and(U32.is_ge(uu, 65), U32.is_le(uu, 70)), uu)# a decimal digit, otherwise a hex letterdef dig.dec(ok: Bool, +uu: U32) -> U32: match ok: case True{}: U32.sub(uu, 48) case False{}: dig.low(Bool.and(U32.is_ge(uu, 97), U32.is_le(uu, 102)), uu)# the value of a digit character, or 99def dig.val(+cc: Char) -> U32: +u = Char.to_u32(cc) dig.dec(Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)), u)# whether a digit value fits a basedef dig.ok(dd: U32, base: U32) -> Bool: U32.is_lt(dd, base)# exactly k digits, or DigBaddef take.acc.d(digit: Bool, nn: U32, +cc: Char) -> U32: match digit: case False{}: nn case True{}: U32.add(U32.mul(nn, 10), U32.sub(Char.to_u32(cc), 48))# the accumulator after one characterdef take.acc(ok: Bool, nn: U32, +cc: Char) -> U32: match ok: case False{}: nn case True{}: take.acc.d(Char.is_digit(cc), nn, cc)# the scan stays well only while every character is a digitdef take.ok(ok: Bool, +cc: Char) -> Bool: match ok: case False{}: False{} case True{}: Char.is_digit(cc)# the finished takedef take.fin(ok: Bool, nn: U32, cs: List<&2, Char>) -> Dig: match ok: case True{}: DigOk{nn, cs} case False{}: DigBad{}# exactly k digits from the front of a listdef take.n(kk: Nat, cs: List<&2, Char>, nn: U32, +ok: Bool) -> Dig: match kk: case 0n: take.fin(ok, nn, cs) case 1n+p: match cs: case []: DigBad{} case +h <> t: take.n(p, t, take.acc(ok, nn, h), take.ok(ok, h))# a number is inside the inclusive boundsdef bound.rng(+nn: U32, lo: U32, hi: U32) -> Bool: Bool.and(U32.is_ge(nn, lo), U32.is_le(nn, hi))# two digits, zero paddeddef pad2.go(small: Bool, nn: U32) -> String: match small: case True{}: "0" ++ U32.show(nn) case False{}: U32.show(nn)# two digits, zero paddeddef text.pad2(+nn: U32) -> String: pad2.go(U32.is_lt(nn, 10), nn)# the one-zero pad, or the digits themselvesdef pad4.big(nn: U32, big: Bool) -> String: match big: case True{}: "0" ++ U32.show(nn) case False{}: U32.show(nn)# the three-digit and two-digit padsdef pad4.mid(nn: U32, mid: Bool, big: Bool) -> String: match mid: case True{}: "00" ++ U32.show(nn) case False{}: pad4.big(nn, big)# four digits, zero paddeddef pad4.go(nn: U32, small: Bool, mid: Bool, big: Bool) -> String: match small: case True{}: "000" ++ U32.show(nn) case False{}: pad4.mid(nn, mid, big)# four digits, zero paddeddef text.pad4(+nn: U32) -> String: pad4.go(nn, U32.is_lt(nn, 10), U32.is_lt(nn, 100), U32.is_lt(nn, 1000))# the sign of a zone offsetdef zone.mark(sign: Sign) -> String: match sign: case Plus{}: "+" case Minus{}: "-"# a numeric zone offsetdef zone.shift(sign: Sign, hh: U32, mi: U32) -> String: zone.mark(sign) ++ text.pad2(hh) ++ ":" ++ text.pad2(mi)# a zone suffixdef zone.text(zz: Zone) -> String: match zz: case Zulu{}: "Z" case Shift{sign, h, mi}: zone.shift(sign, h, mi)# the fractional second, or nothingdef clock.frac.go(empty: Bool, frac: String) -> String: match empty: case True{}: "" case False{}: "." ++ frac# the fractional second, or nothingdef clock.frac(+frac: String) -> String: clock.frac.go(String.is_empty(frac), frac)# a clock, with a fractional second when it has onedef clock.text(cc: Clock) -> String: Clock{h, mi, s, frac} = cc text.pad2(h) ++ ":" ++ text.pad2(mi) ++ ":" ++ text.pad2(s) ++ clock.frac(frac)# a datedef date.text(dd: Date) -> String: Date{y, mo, day} = dd text.pad4(y) ++ "-" ++ text.pad2(mo) ++ "-" ++ text.pad2(day)# a datetime in TOML spellingdef when.text(ww: When) -> String: match ww: case WOff{date, clock, zone}: date.text(date) ++ "T" ++ clock.text(clock) ++ zone.text(zone) case WLocal{date, clock}: date.text(date) ++ "T" ++ clock.text(clock) case WDate{date}: date.text(date) case WTime{clock}: clock.text(clock)# the sign of an integer or a float, empty when it is positivedef sign.text(sign: Sign) -> String: match sign: case Plus{}: "" case Minus{}: "-"# whether the list still has a characterdef cs.some(cs: List<&2, Char>) -> Bool: match cs: case []: False{} case _ <> _: True{}# one character, when the list has onedef cs.head(cs: List<&2, Char>) -> Char: match cs: case []: ' ' case h <> _: h# the tail of a listdef cs.tail(cs: List<&2, Char>) -> List<&2, Char>: match cs: case []: [] case _ <> t: t# a clock with its fraction filled indef clock.with(cc: Clock, frac: String) -> Clock: Clock{h, mi, s, _f} = cc Clock{h, mi, s, frac}# a finished local datetime, or one with a zone still to readdef when.done(_date: Date, _clock: Clock, ww: When) -> WhenOut: WSome{ww}# Z ends the tokendef when.zone.z(tt: List<&2, Char>, clock: Clock, date: Date) -> WhenOut: match tt: case []: WSome{WOff{date, clock, Zulu{}}} case _ <> _: WFail{"invalid zone"}# the offset minute is in rangedef when.zone.mm.ok2(mi_ok: Bool, sign: Sign, hh: U32, mi: U32, clock: Clock, date: Date) -> WhenOut: match mi_ok: case False{}: WFail{"zone minute is out of range"} case True{}: WSome{WOff{date, clock, Shift{sign, hh, mi}}}# the offset fields are in rangedef when.zone.mm.ok(h_ok: Bool, mi_ok: Bool, sign: Sign, hh: U32, mi: U32, clock: Clock, date: Date) -> WhenOut: match h_ok: case False{}: WFail{"zone hour is out of range"} case True{}: when.zone.mm.ok2(mi_ok, sign, hh, mi, clock, date)# nothing follows an offset, and its fields are in rangedef when.zone.mm.end( cs: List<&2, Char>, h_ok: Bool, mi_ok: Bool, sign: Sign, hh: U32, mi: U32, clock: Clock, date: Date) -> WhenOut: match cs: case []: when.zone.mm.ok(h_ok, mi_ok, sign, hh, mi, clock, date) case _ <> _: WFail{"invalid zone"}# the offset minute is in rangedef when.zone.mm.go(dd: Dig, sign: Sign, +hh: U32, clock: Clock, date: Date) -> WhenOut: match dd: case DigBad{}: WFail{"invalid zone"} case DigOk{+n, cs}: when.zone.mm.end(cs, bound.rng(hh, 0, 23), bound.rng(n, 0, 59), sign, hh, n, clock, date)# the offset minutedef when.zone.mm(hit: Bool, dd: Dig, sign: Sign, hh: U32, clock: Clock, date: Date) -> WhenOut: match hit: case False{}: WFail{"invalid zone"} case True{}: when.zone.mm.go(dd, sign, hh, clock, date)# the colon in an offsetdef when.zone.hh.colon(cs: List<&2, Char>, sign: Sign, hh: U32, clock: Clock, date: Date) -> WhenOut: match cs: case []: WFail{"invalid zone"} case +c <> t: when.zone.mm(Char.is_eq(c, ':'), take.n(2n, t, 0, True{}), sign, hh, clock, date)# the offset hourdef when.zone.hh(dd: Dig, sign: Sign, clock: Clock, date: Date) -> WhenOut: match dd: case DigBad{}: WFail{"invalid zone"} case DigOk{n, cs}: when.zone.hh.colon(cs, sign, n, clock, date)# HH:MM of a numeric offsetdef when.zone.off(sign: Sign, tt: List<&2, Char>, clock: Clock, date: Date) -> WhenOut: when.zone.hh(take.n(2n, tt, 0, True{}), sign, clock, date)# a minus starts a numeric offsetdef when.zone.minus(minus: Bool, tt: List<&2, Char>, clock: Clock, date: Date) -> WhenOut: match minus: case True{}: when.zone.off(Minus{}, tt, clock, date) case False{}: WFail{"invalid zone"}# a plus or a minus starts a numeric offsetdef when.zone.sign(plus: Bool, minus: Bool, tt: List<&2, Char>, clock: Clock, date: Date) -> WhenOut: match plus: case True{}: when.zone.off(Plus{}, tt, clock, date) case False{}: when.zone.minus(minus, tt, clock, date)# Z, or a signed numeric offsetdef when.zone.go(zee: Bool, plus: Bool, minus: Bool, tt: List<&2, Char>, clock: Clock, date: Date) -> WhenOut: match zee: case True{}: when.zone.z(tt, clock, date) case False{}: when.zone.sign(plus, minus, tt, clock, date)# a zone, or the end of a local datetimedef when.zone(cs: List<&2, Char>, clock: Clock, date: Date) -> WhenOut: match cs: case []: WSome{WLocal{date, clock}} case +c <> t: when.zone.go(Bool.or(Char.is_eq(c, 'Z'), Char.is_eq(c, 'z')), Char.is_eq(c, '+'), Char.is_eq(c, '-'), t, clock, date)# the fraction must hold a digitdef when.frac.end.go(some: Bool, rev: List<&2, Char>, clock: Clock, date: Date, rest: List<&2, Char>) -> WhenOut: match some: case False{}: WFail{"invalid fraction"} case True{}: when.zone(rest, clock.with(clock, String.from_list(List.reverse(&2, Char, rev))), date)# the fraction must hold a digitdef when.frac.end(+rev: List<&2, Char>, clock: Clock, date: Date, rest: List<&2, Char>) -> WhenOut: when.frac.end.go(cs.some(rev), rev, clock, date, rest)# one fractional digit, or the end of the fractiondef when.frac.digit( digit: Bool, +cc: Char, tt: List<&2, Char>, rev: List<&2, Char>, clock: Clock, date: Date, more: Unit -> WhenOut) -> WhenOut: match digit: case True{}: more(Unit{}) case False{}: when.frac.end(rev, clock, date, cc <> tt)# fractional digitsdef when.frac.digits(cs: List<&2, Char>, +rev: List<&2, Char>, +clock: Clock, +date: Date) -> WhenOut: match cs: case []: when.frac.end(rev, clock, date, []) case +c <> +t: when.frac.digit(Char.is_digit(c), c, t, rev, clock, date, _u => when.frac.digits(t, c <> rev, clock, date))# a dot starts a fraction; anything else is a zonedef when.frac.go(dot: Bool, tt: List<&2, Char>, clock: Clock, date: Date, +cc: Char) -> WhenOut: match dot: case True{}: when.frac.digits(tt, [], clock, date) case False{}: when.zone(cc <> tt, clock, date)# an optional fractional seconddef when.frac(cs: List<&2, Char>, +clock: Clock, +date: Date) -> WhenOut: match cs: case []: when.done(date, clock, WLocal{date, clock}) case +c <> t: when.frac.go(Char.is_eq(c, '.'), t, clock, date, c)# the second is in range, then the fraction and the zonedef when.clock.range.s(s_ok: Bool, hh: U32, mi: U32, ss: U32, cs: List<&2, Char>, date: Date) -> WhenOut: match s_ok: case False{}: WFail{"second is out of range"} case True{}: when.frac(cs, Clock{hh, mi, ss, ""}, date)# minute and second are in rangedef when.clock.range.m(mi_ok: Bool, s_ok: Bool, hh: U32, mi: U32, ss: U32, cs: List<&2, Char>, date: Date) -> WhenOut: match mi_ok: case False{}: WFail{"minute is out of range"} case True{}: when.clock.range.s(s_ok, hh, mi, ss, cs, date)# hour, minute, and second are in rangedef when.clock.range( h_ok: Bool, mi_ok: Bool, s_ok: Bool, hh: U32, mi: U32, ss: U32, cs: List<&2, Char>, date: Date) -> WhenOut: match h_ok: case False{}: WFail{"hour is out of range"} case True{}: when.clock.range.m(mi_ok, s_ok, hh, mi, ss, cs, date)# seconds, then an optional fraction and zonedef when.clock.ss.go(dd: Dig, +hh: U32, +mi: U32, date: Date) -> WhenOut: match dd: case DigBad{}: WFail{"invalid time"} case DigOk{+n, cs}: when.clock.range(bound.rng(hh, 0, 23), bound.rng(mi, 0, 59), bound.rng(n, 0, 60), hh, mi, n, cs, date)# seconds after the colondef when.clock.ss(hit: Bool, dd: Dig, hh: U32, mi: U32, date: Date) -> WhenOut: match hit: case False{}: WFail{"invalid time"} case True{}: when.clock.ss.go(dd, hh, mi, date)# the colon after the minutesdef when.clock.mm.colon(cs: List<&2, Char>, hh: U32, mi: U32, date: Date) -> WhenOut: match cs: case []: WFail{"invalid time"} case +c <> t: when.clock.ss(Char.is_eq(c, ':'), take.n(2n, t, 0, True{}), hh, mi, date)# minutes, then secondsdef when.clock.mm(dd: Dig, hh: U32, date: Date) -> WhenOut: match dd: case DigBad{}: WFail{"invalid time"} case DigOk{n, cs}: when.clock.mm.colon(cs, hh, n, date)# the colon after the hourdef when.clock.colon.go(hit: Bool, hh: U32, tt: List<&2, Char>, date: Date) -> WhenOut: match hit: case False{}: WFail{"invalid time"} case True{}: when.clock.mm(take.n(2n, tt, 0, True{}), hh, date)# the colon after the hourdef when.clock.colon(cs: List<&2, Char>, hh: U32, date: Date) -> WhenOut: match cs: case []: WFail{"invalid time"} case +c <> t: when.clock.colon.go(Char.is_eq(c, ':'), hh, t, date)# a clock following a datedef when.clock.of(dd: Dig, date: Date) -> WhenOut: match dd: case DigBad{}: WFail{"invalid time"} case DigOk{n, cs}: when.clock.colon(cs, n, date)# a space separates a date from a timedef when.after.sp(sp: Bool, date: Date, tt: List<&2, Char>) -> WhenOut: match sp: case True{}: when.clock.of(take.n(2n, tt, 0, True{}), date) case False{}: WFail{"invalid datetime"}# T or a space separates a date from a timedef when.after.sep(tee: Bool, sp: Bool, date: Date, tt: List<&2, Char>) -> WhenOut: match tee: case True{}: when.clock.of(take.n(2n, tt, 0, True{}), date) case False{}: when.after.sp(sp, date, tt)# the character after a date: nothing, T, or a space, and then a timedef when.after.date(date: Date, cs: List<&2, Char>) -> WhenOut: match cs: case []: WSome{WDate{date}} case +c <> t: when.after.sep(Bool.or(Char.is_eq(c, 'T'), Char.is_eq(c, 't')), Char.is_eq(c, ' '), date, t)# the day is in range, then the rest of the token is a time or nothingdef when.date.ok.d(d_ok: Bool, yy: U32, mo: U32, dd: U32, cs: List<&2, Char>) -> WhenOut: match d_ok: case False{}: WFail{"day is out of range"} case True{}: when.after.date(Date{yy, mo, dd}, cs)# a date whose fields are in rangedef when.date.ok.go(mo_ok: Bool, d_ok: Bool, yy: U32, mo: U32, dd: U32, cs: List<&2, Char>) -> WhenOut: match mo_ok: case False{}: WFail{"month is out of range"} case True{}: when.date.ok.d(d_ok, yy, mo, dd, cs)# February in a year divisible by 4 and by 100: 29 days only when the year# is also divisible by 400def when.feb.quad(four_hundred: Bool) -> U32: match four_hundred: case True{}: 29 case False{}: 28# February in a year divisible by 4: 29 days unless the year is a centurydef when.feb.cent(hundred: Bool, four_hundred: Bool) -> U32: match hundred: case False{}: 29 case True{}: when.feb.quad(four_hundred)# February: 29 days in a leap year and 28 otherwise. A leap year is divisible# by 4, except by 100 unless also by 400 (RFC 3339, Appendix C)def when.feb.go(four: Bool, hundred: Bool, four_hundred: Bool) -> U32: match four: case False{}: 28 case True{}: when.feb.cent(hundred, four_hundred)# the days of February in a yeardef when.feb(+yy: U32) -> U32: when.feb.go(U32.is_eq(U32.mod(yy, 4), 0), U32.is_eq(U32.mod(yy, 100), 0), U32.is_eq(U32.mod(yy, 400), 0))# April, June, September and November have 30 daysdef when.short(+mo: U32) -> Bool: Bool.or(Bool.or(U32.is_eq(mo, 4), U32.is_eq(mo, 6)), Bool.or(U32.is_eq(mo, 9), U32.is_eq(mo, 11)))# 30 days in a short month and 31 in the othersdef when.days.short(short: Bool) -> U32: match short: case True{}: 30 case False{}: 31# the days of a month, by whether it is February or a short monthdef when.days.go(feb: Bool, short: Bool, +yy: U32) -> U32: match feb: case True{}: when.feb(yy) case False{}: when.days.short(short)# the days of month mo in year yydef when.days(+yy: U32, +mo: U32) -> U32: when.days.go(U32.is_eq(mo, 2), when.short(mo), yy)# a date whose fields are in range: the day within its month in that yeardef when.date.ok(+yy: U32, +mo: U32, +dd: U32, cs: List<&2, Char>) -> WhenOut: when.date.ok.go(bound.rng(mo, 1, 12), bound.rng(dd, 1, when.days(yy, mo)), yy, mo, dd, cs)# a local time: HH:MM:SS, with the hour already takendef when.time.rest(tt: List<&2, Char>, hh: U32) -> WhenOut: when.clock.mm(take.n(2n, tt, 0, True{}), hh, Date{0, 1, 1})# a local time uses a dummy date and then keeps only the clockdef when.time.only(ww: WhenOut) -> WhenOut: match ww: case WSome{WLocal{_date, clock}}: WSome{WTime{clock}} case WSome{WOff{_date, clock, zone}}: WFail{"invalid time"} case WSome{other}: WFail{"invalid time"} case WFail{why}: WFail{why} case WSkip{}: WFail{"invalid time"}# a complete date, then whatever follows itdef when.day.end(dd: Dig, yy: U32, mo: U32) -> WhenOut: match dd: case DigBad{}: WFail{"invalid date"} case DigOk{n, cs}: when.date.ok(yy, mo, n, cs)# the day is two digitsdef when.day.go(hit: Bool, dd: Dig, yy: U32, mo: U32) -> WhenOut: match hit: case False{}: WFail{"invalid date"} case True{}: when.day.end(dd, yy, mo)# the daydef when.day(cs: List<&2, Char>, yy: U32, mo: U32) -> WhenOut: match cs: case []: WFail{"invalid date"} case +c <> t: when.day.go(Char.is_eq(c, '-'), take.n(2n, t, 0, True{}), yy, mo)# the dash after the monthdef when.month.go(dd: Dig, yy: U32) -> WhenOut: match dd: case DigBad{}: WFail{"invalid date"} case DigOk{n, cs}: when.day(cs, yy, n)# the monthdef when.month(hit: Bool, dd: Dig, yy: U32) -> WhenOut: match hit: case False{}: WFail{"invalid date"} case True{}: when.month.go(dd, yy)# the dash after the yeardef when.year.dash(cs: List<&2, Char>, yy: U32) -> WhenOut: match cs: case []: WFail{"invalid date"} case +c <> t: when.month(Char.is_eq(c, '-'), take.n(2n, t, 0, True{}), yy)# the fourth year digit, then the monthdef when.year4(dd: Dig) -> WhenOut: match dd: case DigBad{}: WFail{"invalid date"} case DigOk{n, cs}: when.year.dash(cs, n)# two more year digits, then a dashdef when.year.more(digit: Bool, nn: U32, +cc: Char, tt: List<&2, Char>) -> WhenOut: match digit: case False{}: WFail{"invalid datetime"} case True{}: when.year4(take.n(1n, tt, U32.add(U32.mul(nn, 10), dig.val(cc)), True{}))# a colon means a local time; another digit continues a yeardef when.branch(colon: Bool, digit: Bool, nn: U32, +cc: Char, tt: List<&2, Char>) -> WhenOut: match colon: case True{}: when.time.only(when.time.rest(tt, nn)) case False{}: when.year.more(digit, nn, cc, tt)# two digits decide a time or a datedef when.after2.go(cs: List<&2, Char>, nn: U32) -> WhenOut: match cs: case []: WFail{"invalid datetime"} case +c <> t: when.branch(Char.is_eq(c, ':'), Char.is_digit(c), nn, c, t)# two digits decide a time (HH:) or a date (YYYY-)def when.after2(dd: Dig) -> WhenOut: match dd: case DigBad{}: WFail{"invalid datetime"} case DigOk{n, cs}: when.after2.go(cs, n)# a datetime tokendef when.read(cs: List<&2, Char>) -> WhenOut: when.after2(take.n(2n, cs, 0, True{}))# a dash after the first characterdef word.dash.hit(first: Bool, +cc: Char) -> Bool: match first: case True{}: False{} case False{}: Char.is_eq(cc, '-')# either flagdef word.or(aa: Bool, bb: Bool) -> Bool: match aa: case True{}: True{} case False{}: bb# a colon anywhere in the tokendef word.colon(cs: List<&2, Char>) -> Bool: match cs: case []: False{} case +c <> t: word.or(Char.is_eq(c, ':'), word.colon(t))# e or E: an exponent, so the token is a numeral rather than a datetimedef word.ee.hit(+cc: Char) -> Bool: Bool.or(Char.is_eq(cc, 'e'), Char.is_eq(cc, 'E'))# whether the token holds an exponent markerdef word.ee(cs: List<&2, Char>) -> Bool: match cs: case []: False{} case +c <> t: word.or(word.ee.hit(c), word.ee(t))# a datetime scan only when the token is not an exponent numeraldef word.as_date(date: Bool, exp: Bool) -> Bool: match exp: case True{}: False{} case False{}: date# a dash after the first characterdef word.dash(cs: List<&2, Char>, first: Bool) -> Bool: match cs: case []: False{} case +c <> t: word.or(word.dash.hit(first, c), word.dash(t, False{}))# a negative nan, or not a specialdef word.spec.nan3(cc: Bool) -> Got: match cc: case True{}: GotVal{VFlo{Minus{}, "nan"}} case False{}: GotBad{""}# a signed nandef word.spec.nan2(bb: Bool, cc: Bool) -> Got: match bb: case True{}: GotVal{VFlo{Plus{}, "nan"}} case False{}: word.spec.nan3(cc)# nandef word.spec.nan(aa: Bool, bb: Bool, cc: Bool) -> Got: match aa: case True{}: GotVal{VFlo{Plus{}, "nan"}} case False{}: word.spec.nan2(bb, cc)# character-for-character equality. a mismatch drops the rest and stopsdef str.eq.step(aa: String, bb: String, same: Bool) -> Bool: match aa bb same: case SNil{} SNil{} True{}: True{} case SCon{x, xt} SCon{y, yt} True{}: str.eq.step(xt, yt, Char.is_eq(x, y)) case _a _b _s: False{}# the search only needs the bool, not a rebuilt copy of either stringdef str.eq(aa: String, bb: String) -> Bool: str.eq.step(aa, bb, True{})# a negative inf, or nandef word.spec.inf3(cc: Bool, +ss: String) -> Got: match cc: case True{}: GotVal{VFlo{Minus{}, "inf"}} case False{}: word.spec.nan(str.eq(ss, "nan"), str.eq(ss, "+nan"), str.eq(ss, "-nan"))# a signed inf, or nandef word.spec.inf2(bb: Bool, cc: Bool, +ss: String) -> Got: match bb: case True{}: GotVal{VFlo{Plus{}, "inf"}} case False{}: word.spec.inf3(cc, ss)# inf, or nandef word.spec.inf(aa: Bool, bb: Bool, cc: Bool, +ss: String) -> Got: match aa: case True{}: GotVal{VFlo{Plus{}, "inf"}} case False{}: word.spec.inf2(bb, cc, ss)# false, or the rest of the specialsdef word.spec.f(hit: Bool, +ss: String) -> Got: match hit: case True{}: GotVal{VBool{False{}}} case False{}: word.spec.inf(str.eq(ss, "inf"), str.eq(ss, "+inf"), str.eq(ss, "-inf"), ss)# true, or the rest of the specialsdef word.spec.t(hit: Bool, other: Bool, +ss: String) -> Got: match hit: case True{}: GotVal{VBool{True{}}} case False{}: word.spec.f(other, ss)# true, false, inf, and nan, including a sign on inf and nandef word.spec(+ss: String) -> Got: word.spec.t(str.eq(ss, "true"), str.eq(ss, "false"), ss)# a datetime result as a valuedef word.from.when(ww: WhenOut) -> Got: match ww: case WSome{when}: GotVal{VWhen{when}} case WFail{why}: GotBad{why} case WSkip{}: GotBad{"invalid datetime"}# the spelling of a numeral with the leading sign and underscores removeddef word.body.put.sign(is_sign: Bool, cc: Char, out: List<&2, Char>) -> List<&2, Char>: match is_sign: case True{}: out case False{}: cc <> out# an underscore is dropped; every other character is keptdef word.body.put.us(is_us: Bool, cc: Char, out: List<&2, Char>) -> List<&2, Char>: match is_us: case True{}: out case False{}: cc <> out# one character of a numeral spellingdef word.body.put(lead: Bool, +cc: Char, out: List<&2, Char>) -> List<&2, Char>: match lead: case True{}: word.body.put.sign(Bool.or(Char.is_eq(cc, '+'), Char.is_eq(cc, '-')), cc, out) case False{}: word.body.put.us(Char.is_eq(cc, '_'), cc, out)# the spelling, reversed, then put back in order at the enddef word.body.go(cs: List<&2, Char>, lead: Bool, out: List<&2, Char>) -> List<&2, Char>: match cs: case []: List.reverse(&2, Char, out) case +c <> t: word.body.go(t, False{}, word.body.put(lead, c, out))# the spelling of a numeral with the leading sign and underscores removeddef word.body(cs: List<&2, Char>) -> String: String.from_list(word.body.go(cs, True{}, []))# a fresh numeral scandef num.start() -> Num: Num{NLead{}, Plus{}, 10, [], False{}, False{}, ""}# a scan that has faileddef num.bad(_st: Num, why: String) -> Num: Num{NBad{}, Plus{}, 10, [], False{}, False{}, why}# set the sign and wait for a digitdef num.sign(_st: Num, sign: Sign) -> Num: Num{NSign{}, sign, 10, [], False{}, False{}, ""}# a digit that fits the basedef num.digit.ok(ok: Bool, +cc: Char, nk: NK, sign: Sign, +base: U32, ds: List<&2, U32>) -> Num: match ok: case False{}: num.bad(Num{NLead{}, Plus{}, 10, [], False{}, False{}, ""}, "invalid digit") case True{}: Num{nk, sign, base, dec.go(ds, base, dig.val(cc)), False{}, True{}, ""}# a decimal digit joined to the numeraldef num.digit.go(st: Num, +cc: Char, nk: NK) -> Num: Num{_nk, sign, +base, ds, _us, _saw, _why} = st num.digit.ok(dig.ok(dig.val(cc), base), cc, nk, sign, base, ds)# a decimal digit joined to the numeraldef num.digit(st: Num, +cc: Char, nk: NK) -> Num: num.digit.go(st, cc, nk)# an underscore between digitsdef num.us.go(ok: Bool, nk: NK, sign: Sign, base: U32, ds: List<&2, U32>, why: String) -> Num: match ok: case False{}: num.bad(Num{NLead{}, Plus{}, 10, [], False{}, False{}, ""}, "invalid underscore") case True{}: Num{nk, sign, base, ds, True{}, True{}, why}# an underscore between digitsdef num.us(st: Num) -> Num: Num{nk, sign, base, ds, us, saw, why} = st num.us.go(Bool.and(saw, Bool.not(us)), nk, sign, base, ds, why)# the base prefix after a leading zerodef num.base(st: Num, base: U32) -> Num: Num{_nk, sign, _b, _ds, _us, _saw, _why} = st Num{NBase{}, sign, base, [], False{}, False{}, ""}# a dot begins the fractiondef num.dot.go(ok: Bool, sign: Sign) -> Num: match ok: case False{}: num.bad(Num{NLead{}, Plus{}, 10, [], False{}, False{}, ""}, "invalid fraction") case True{}: Num{NFrac{}, sign, 10, [], False{}, False{}, ""}# a dot begins the fractiondef num.dot(st: Num) -> Num: Num{_nk, sign, _b, _ds, us, saw, _why} = st num.dot.go(Bool.and(saw, Bool.not(us)), sign)# an exponent markerdef num.exp.go(ok: Bool, sign: Sign) -> Num: match ok: case False{}: num.bad(Num{NLead{}, Plus{}, 10, [], False{}, False{}, ""}, "invalid exponent") case True{}: Num{NExp{}, sign, 10, [], False{}, False{}, ""}# an exponent markerdef num.exp(st: Num) -> Num: Num{_nk, sign, _b, _ds, us, saw, _why} = st num.exp.go(Bool.and(saw, Bool.not(us)), sign)# the sign already chosendef num.sign.of(st: Num) -> Sign: Num{_nk, sign, _b, _ds, _us, _saw, _why} = st sign# a first digitdef num.lead.digit(digit: Bool, st: Num, +cc: Char) -> Num: match digit: case True{}: num.digit(st, cc, NDig{}) case False{}: num.bad(st, "invalid number")# a leading zero, or another digitdef num.lead.zero(zero: Bool, digit: Bool, st: Num, +cc: Char) -> Num: match zero: case True{}: Num{NZero{}, num.sign.of(st), 10, [], False{}, True{}, ""} case False{}: num.lead.digit(digit, st, cc)# a minus, a zero, or a digitdef num.lead.minus(minus: Bool, zero: Bool, digit: Bool, st: Num, +cc: Char) -> Num: match minus: case True{}: num.sign(st, Minus{}) case False{}: num.lead.zero(zero, digit, st, cc)# a sign, a zero, or a digitdef num.lead.go(plus: Bool, minus: Bool, zero: Bool, digit: Bool, st: Num, +cc: Char) -> Num: match plus: case True{}: num.sign(st, Plus{}) case False{}: num.lead.minus(minus, zero, digit, st, cc)# the first character after an optional signdef num.lead(st: Num, +cc: Char) -> Num: num.lead.go(Char.is_eq(cc, '+'), Char.is_eq(cc, '-'), Char.is_eq(cc, '0'), Char.is_digit(cc), st, cc)# a second sign is refused; any other character is read as the first one of# an unsigned numeral, with the sign keptdef num.after.sign.go(twice: Bool, sign: Sign, +cc: Char) -> Num: match twice: case True{}: num.bad(num.start(), "invalid number") case False{}: num.lead(Num{NLead{}, sign, 10, [], False{}, False{}, ""}, cc)# the character after a signdef num.after.sign(st: Num, +cc: Char) -> Num: num.after.sign.go(Bool.or(Char.is_eq(cc, '+'), Char.is_eq(cc, '-')), num.sign.of(st), cc)# a capital exponent; any other character is refused, since a digit or an# underscore after a leading zero (`01`, `0_1`) would hide itdef num.zero.exp2(ee: Bool, st: Num, _c: Char) -> Num: match ee: case True{}: num.exp(st) case False{}: num.bad(st, "invalid number")# an exponent after zero, or a rejected extra digitdef num.zero.exp(eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match eee: case True{}: num.exp(st) case False{}: num.zero.exp2(ee, st, cc)# a fraction or an exponent after zerodef num.zero.dot(dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match dot: case True{}: num.dot(st) case False{}: num.zero.exp(eee, ee, st, cc)# a binary prefix, or a floatdef num.zero.b(bb: Bool, dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match bb: case True{}: num.base(st, 2) case False{}: num.zero.dot(dot, eee, ee, st, cc)# an octal prefix, or the restdef num.zero.o(oo: Bool, bb: Bool, dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match oo: case True{}: num.base(st, 8) case False{}: num.zero.b(bb, dot, eee, ee, st, cc)# a base prefix, a fraction, or an exponentdef num.zero.go(xx: Bool, oo: Bool, bb: Bool, dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match xx: case True{}: num.base(st, 16) case False{}: num.zero.o(oo, bb, dot, eee, ee, st, cc)# the character after a lone zerodef num.zero(st: Num, +cc: Char) -> Num: num.zero.go(Char.is_eq(cc, 'x'), Char.is_eq(cc, 'o'), Char.is_eq(cc, 'b'), Char.is_eq(cc, '.'), Char.is_eq(cc, 'e'), Char.is_eq(cc, 'E'), st, cc)# a capital exponent, or a rejected characterdef num.digs.exp2(ee: Bool, st: Num, _c: Char) -> Num: match ee: case True{}: num.exp(st) case False{}: num.bad(st, "invalid number")# an exponentdef num.digs.exp(eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match eee: case True{}: num.exp(st) case False{}: num.digs.exp2(ee, st, cc)# a dot or an exponentdef num.digs.dot(dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match dot: case True{}: num.dot(st) case False{}: num.digs.exp(eee, ee, st, cc)# an underscore, a dot, or an exponentdef num.digs.us(us: Bool, dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match us: case True{}: num.us(st) case False{}: num.digs.dot(dot, eee, ee, st, cc)# a decimal digit, an underscore, a dot, or an exponentdef num.digs.go(digit: Bool, us: Bool, dot: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match digit: case True{}: num.digit(st, cc, NDig{}) case False{}: num.digs.us(us, dot, eee, ee, st, cc)# a decimal digit, an underscore, a dot, or an exponentdef num.digs(st: Num, +cc: Char) -> Num: num.digs.go(Char.is_digit(cc), Char.is_eq(cc, '_'), Char.is_eq(cc, '.'), Char.is_eq(cc, 'e'), Char.is_eq(cc, 'E'), st, cc)# an underscore, or a digit of the basedef num.based.go(us: Bool, st: Num, +cc: Char) -> Num: match us: case True{}: num.us(st) case False{}: num.digit(st, cc, NBase{})# a digit of a prefixed integer, or an underscoredef num.based(st: Num, +cc: Char) -> Num: num.based.go(Char.is_eq(cc, '_'), st, cc)# a capital exponent, or a rejected characterdef num.frac.exp2(ee: Bool, st: Num, _c: Char) -> Num: match ee: case True{}: num.exp(st) case False{}: num.bad(st, "invalid fraction")# an exponent in a fractiondef num.frac.exp(eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match eee: case True{}: num.exp(st) case False{}: num.frac.exp2(ee, st, cc)# an underscore or an exponent in a fractiondef num.frac.us(us: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match us: case True{}: num.us(st) case False{}: num.frac.exp(eee, ee, st, cc)# a fraction digit, an underscore, or an exponentdef num.frac.go(digit: Bool, us: Bool, eee: Bool, ee: Bool, st: Num, +cc: Char) -> Num: match digit: case True{}: num.digit(st, cc, NFrac{}) case False{}: num.frac.us(us, eee, ee, st, cc)# a fraction digit, an underscore, or an exponentdef num.frac(st: Num, +cc: Char) -> Num: num.frac.go(Char.is_digit(cc), Char.is_eq(cc, '_'), Char.is_eq(cc, 'e'), Char.is_eq(cc, 'E'), st, cc)# the exponent sign is part of the spelling; digits followdef num.exp.sign(st: Num) -> Num: Num{_nk, sign, _b, _ds, _us, _saw, _why} = st Num{NEDig{}, sign, 10, [], False{}, False{}, ""}# a digit of the exponentdef num.expon.digit(digit: Bool, st: Num, +cc: Char) -> Num: match digit: case True{}: num.digit(st, cc, NEDig{}) case False{}: num.bad(st, "invalid exponent")# a minus or a digit in an exponentdef num.expon.minus(minus: Bool, digit: Bool, st: Num, +cc: Char) -> Num: match minus: case True{}: num.exp.sign(st) case False{}: num.expon.digit(digit, st, cc)# the sign or the first digit of an exponentdef num.expon.go(plus: Bool, minus: Bool, digit: Bool, st: Num, +cc: Char) -> Num: match plus: case True{}: num.exp.sign(st) case False{}: num.expon.minus(minus, digit, st, cc)# the sign or the first digit of an exponentdef num.expon(st: Num, +cc: Char) -> Num: num.expon.go(Char.is_eq(cc, '+'), Char.is_eq(cc, '-'), Char.is_digit(cc), st, cc)# an underscore in an exponentdef num.edig.us(us: Bool, st: Num, _c: Char) -> Num: match us: case True{}: num.us(st) case False{}: num.bad(st, "invalid exponent")# an exponent digit or an underscoredef num.edig.go(digit: Bool, us: Bool, st: Num, +cc: Char) -> Num: match digit: case True{}: num.digit(st, cc, NEDig{}) case False{}: num.edig.us(us, st, cc)# an exponent digit or an underscoredef num.edig(st: Num, +cc: Char) -> Num: num.edig.go(Char.is_digit(cc), Char.is_eq(cc, '_'), st, cc)# one character of a numeraldef num.on(nk: NK, st: Num, +cc: Char) -> Num: match nk: case NLead{}: num.lead(st, cc) case NSign{}: num.after.sign(st, cc) case NZero{}: num.zero(st, cc) case NDig{}: num.digs(st, cc) case NBase{}: num.based(st, cc) case NFrac{}: num.frac(st, cc) case NExp{}: num.expon(st, cc) case NEDig{}: num.edig(st, cc) case NBad{}: st# one character of a numeraldef num.step.go(st: Num, +cc: Char) -> Num: Num{+nk, sign, base, ds, us, saw, why} = st num.on(nk, Num{nk, sign, base, ds, us, saw, why}, cc)# one character of a numeraldef num.step(st: Num, +cc: Char) -> Num: num.step.go(st, cc)# the walk over a numeraldef num.go(cs: List<&2, Char>, st: Num) -> Num: match cs: case []: st case +h <> t: num.go(t, num.step(st, h))# the positive limit is 2^63-1; the negative limit is 2^63def i64.limit(sign: Sign) -> String: match sign: case Plus{}: "9223372036854775807" case Minus{}: "9223372036854775808"# the first differing digit stands; a tie takes the new orderdef i64.ord(seen: Cmp, now: Cmp) -> Cmp: match seen now: case EQ{} n: n case LT{} _n: LT{} case GT{} _n: GT{}# a shorter digit string fits and a longer one does not. equal length uses# digit order. the walk does not rebuild either stringdef i64.step(+digits: String, +limit: String, ord: Cmp) -> Bool: match digits limit: case SNil{} SNil{}: Cmp.is_le(ord) case SNil{} SCon{_h, _t}: True{} case SCon{_h, _t} SNil{}: False{} case SCon{x, xt} SCon{y, yt}: i64.step(xt, yt, i64.ord(ord, U32.cmp(Char.to_u32(x), Char.to_u32(y))))# whether the integer fits in a signed 64-bit integerdef i64.fits(+sign: Sign, +digits: String) -> Bool: i64.step(digits, i64.limit(sign), EQ{})# an integer inside the signed 64-bit rangedef num.fin.range(ok: Bool, sign: Sign, digits: String) -> Got: match ok: case True{}: GotVal{VInt{sign, digits}} case False{}: GotBad{"out of range"}# a finished integer, rejected when it falls outside signed 64-bitdef num.fin.int(saw: Bool, +sign: Sign, +digits: String) -> Got: match saw: case False{}: GotBad{"invalid number"} case True{}: num.fin.range(i64.fits(sign, digits), sign, digits)# a numeral needs a digit in its last rundef num.fin.saw(saw: Bool, val: Val) -> Got: match saw: case False{}: GotBad{"invalid number"} case True{}: GotVal{val}# a prefixed integer may not carry a signdef num.signed(cs: List<&2, Char>) -> Bool: match cs: case []: False{} case +c <> _: Bool.or(Char.is_eq(c, '+'), Char.is_eq(c, '-'))# a prefixed integer, rejected when a sign was writtendef num.fin.base(signed: Bool, saw: Bool, sign: Sign, ds: List<&2, U32>) -> Got: match signed: case True{}: GotBad{"invalid number"} case False{}: num.fin.int(saw, sign, dec.text(ds))# the kind of numeral the scan finished indef num.fin.nk(nk: NK, sign: Sign, ds: List<&2, U32>, saw: Bool, why: String, cs: List<&2, Char>) -> Got: match nk: case NBad{}: GotBad{why} case NLead{}: GotBad{"invalid number"} case NSign{}: GotBad{"invalid number"} case NZero{}: GotVal{VInt{sign, "0"}} case NDig{}: num.fin.int(saw, sign, dec.text(ds)) case NBase{}: num.fin.base(num.signed(cs), saw, sign, ds) case NFrac{}: num.fin.saw(saw, VFlo{sign, word.body(cs)}) case NExp{}: GotBad{"invalid exponent"} case NEDig{}: num.fin.saw(saw, VFlo{sign, word.body(cs)})# a trailing underscore is rejecteddef num.fin.us(us: Bool, nk: NK, sign: Sign, ds: List<&2, U32>, saw: Bool, why: String, cs: List<&2, Char>) -> Got: match us: case True{}: GotBad{"invalid underscore"} case False{}: num.fin.nk(nk, sign, ds, saw, why, cs)# the value of a finished numeraldef num.fin(st: Num, cs: List<&2, Char>) -> Got: Num{nk, sign, _base, ds, us, saw, why} = st num.fin.us(us, nk, sign, ds, saw, why, cs)# a numeral tokendef word.num(+cs: List<&2, Char>) -> Got: num.fin(num.go(cs, num.start()), cs)# a datetime when the token has a colon or a dash, otherwise a numeraldef word.route.date(date: Bool, cs: List<&2, Char>) -> Got: match date: case True{}: word.from.when(when.read(cs)) case False{}: word.num(cs)# a special word, or a numeraldef word.route(gg: Got, date: Bool, +cs: List<&2, Char>) -> Got: match gg: case GotVal{val}: GotVal{val} case GotBad{_}: word.route.date(word.as_date(date, word.ee(cs)), cs)# '-' can start a signed inf or nandef word.special.minus(+cc: Char) -> Bool: Char.is_eq(cc, '-')# '+' or '-'def word.special.plus(hit: Bool, +cc: Char) -> Bool: match hit: case True{}: True{} case False{}: word.special.minus(cc)# 'n', or a signdef word.special.n(hit: Bool, +cc: Char) -> Bool: match hit: case True{}: True{} case False{}: word.special.plus(Char.is_eq(cc, '+'), cc)# 'i', or nan, or a signdef word.special.i(hit: Bool, +cc: Char) -> Bool: match hit: case True{}: True{} case False{}: word.special.n(Char.is_eq(cc, 'n'), cc)# 'f', or inf, or nan, or a signdef word.special.f(hit: Bool, +cc: Char) -> Bool: match hit: case True{}: True{} case False{}: word.special.i(Char.is_eq(cc, 'i'), cc)# 't', or false, or inf, or nan, or a signdef word.special.t(hit: Bool, +cc: Char) -> Bool: match hit: case True{}: True{} case False{}: word.special.f(Char.is_eq(cc, 'f'), cc)# whether this character can start a special bare worddef word.special(+cc: Char) -> Bool: word.special.t(Char.is_eq(cc, 't'), cc)# the first character, when the word has onedef word.lead(cs: List<&2, Char>) -> Bool: match cs: case []: False{} case +h <> _t: word.special(h)# a numeral or a datetime. the token cannot be a keyworddef word.val.num(+cs: List<&2, Char>) -> Got: word.route(GotBad{""}, word.or(word.colon(cs), word.dash(cs, True{})), cs)# a special word, or a numeral. a digit does not build a string to comparedef word.val.pick(special: Bool, +cs: List<&2, Char>) -> Got: match special: case True{}: word.route(word.spec(String.from_list(cs)), word.or(word.colon(cs), word.dash(cs, True{})), cs) case False{}: word.val.num(cs)# a bare word: a special, a datetime, or a numeraldef word.val(+cs: List<&2, Char>) -> Got: word.val.pick(word.lead(cs), cs)# a change applied at the end of a key pathtype Job is Data: JPut{val: Val} JDef{} JAot{}# one table the walk has enteredtype Fr is Data: FrNew{name: String, parent: List<&2, Val>, path: String} FrRep{name: String, parent: List<&2, Val>, path: String} FrAot{name: String, parent: List<&2, Val>, pre: List<&2, Val>, path: String}# the walk: the table being edited, and the frames above ittype Tw is Data: Tw{vs: List<&2, Val>, job: Job, stack: List<&2, Fr>, bad: String, pre: String, heads: List<&2, String>, inl: Bool}# the root table after a walktype End is Data: End{vs: List<&2, Val>, bad: String, heads: List<&2, String>}# what a key in a table refers totype Look is Data: KMiss{} KHead{path: String, rows: List<&2, Val>} KInl{} KAots{path: String, elems: List<&2, Val>} KNo{why: String}# the last element of an array of tablestype Last is Data: LastSome{path: String, pre: List<&2, Val>, rows: List<&2, Val>} LastNo{why: String}# the first error is keptdef err.first.go(empty: Bool, bad: String, why: String) -> String: match empty: case True{}: why case False{}: bad# the first error is keptdef err.first(+bad: String, why: String) -> String: err.first.go(String.is_empty(bad), bad, why)# a key found earlier winsdef find.step(eq: Bool, vv: Val, more: Unit -> Hit) -> Hit: match eq: case True{}: Found{vv} case False{}: more(Unit{})# the first pair of this namedef rows.find(rows: List<&2, Val>, +name: String) -> Hit: match rows: case []: Miss{} case VPair{n, v} <> t: find.step(str.eq(n, name), v, _u => rows.find(t, name)) case _ <> t: rows.find(t, name)# the value written for a key, or Missdef get(rows: List<&2, Val>, +name: String) -> Hit: rows.find(rows, name)# replace the value of one pairdef rows.repl.step(eq: Bool, nn: String, old: Val, val: Val, more: Unit -> List<&2, Val>) -> List<&2, Val>: match eq: case True{}: VPair{nn, val} <> more(Unit{}) case False{}: VPair{nn, old} <> more(Unit{})# replace every pair of this namedef rows.repl(rows: List<&2, Val>, +name: String, +val: Val) -> List<&2, Val>: match rows: case []: [] case VPair{+n, old} <> t: rows.repl.step(str.eq(n, name), n, old, val, _u => rows.repl(t, name, val)) case h <> t: h <> rows.repl(t, name, val)# either flagdef heads.or(aa: Bool, bb: Bool) -> Bool: match aa: case True{}: True{} case False{}: bb# heads records each table the document has defined as its path behind one# mark: `[` when a header defined it, `.` when dotted keys diddef heads.by.head(path: String) -> String: SCon{'[', path}# a table dotted keys defined, as heads records itdef heads.by.dot(path: String) -> String: SCon{'.', path}# whether a table path was defined, by a header or by dotted keysdef heads.has(hs: List<&2, String>, +pp: String) -> Bool: match hs: case []: False{} case SCon{_mark, h} <> t: heads.or(str.eq(h, pp), heads.has(t, pp)) case SNil{} <> t: heads.has(t, pp)# whether a table path was defined by a headerdef heads.head(hs: List<&2, String>, +pp: String) -> Bool: match hs: case []: False{} case SCon{mark, h} <> t: heads.or(Bool.and(Char.is_eq(mark, '['), str.eq(h, pp)), heads.head(t, pp)) case SNil{} <> t: heads.head(t, pp)# a recorded table under the prefix is droppeddef heads.drop.step( under: Bool, mark: Char, path: String, rest: List<&2, String>) -> List<&2, String>: match under: case True{}: rest case False{}: SCon{mark, path} <> rest# forget every table whose path starts with the prefix. A new element of an# array of tables starts with none of the tables the element before defineddef heads.drop(hs: List<&2, String>, +pre: String) -> List<&2, String>: match hs: case []: [] case SCon{mark, +path} <> t: heads.drop.step(String.starts_with(path, pre), mark, path, heads.drop(t, pre)) case SNil{} <> t: heads.drop(t, pre)# a table defined by a dotted key is recorded oncedef heads.define.new(seen: Bool, path: String, heads: List<&2, String>) -> List<&2, String>: match seen: case True{}: heads case False{}: heads.by.dot(path) <> heads# a dotted key defines each table before its last segmentdef heads.define(job: Job, +path: String, +heads: List<&2, String>) -> List<&2, String>: match job: case JPut{_val}: heads.define.new(heads.has(heads, path), path, heads) case _: heads# a dotted path from a prefix and one segmentdef tree.pre.go(empty: Bool, pre: String, name: String) -> String: match empty: case True{}: key(name) case False{}: pre ++ "." ++ key(name)# a dotted path from a prefix and one segmentdef tree.pre(+pre: String, name: String) -> String: tree.pre.go(String.is_empty(pre), pre, name)# the value a key already holdsdef look.of(vv: Val) -> Look: match vv: case VHead{path, rows}: KHead{path, rows} case VInl{_rows}: KInl{} case VAots{path, elems}: KAots{path, elems} case _: KNo{"not a table"}# the matching pair, or the rest of the tabledef look.step(eq: Bool, vv: Val, more: Unit -> Look) -> Look: match eq: case True{}: look.of(vv) case False{}: more(Unit{})# what a key in this table refers todef rows.look(rows: List<&2, Val>, +name: String) -> Look: match rows: case []: KMiss{} case VPair{n, v} <> t: look.step(str.eq(n, name), v, _u => rows.look(t, name)) case _ <> t: rows.look(t, name)# the newest element. rows stay newest-first until the document is sealeddef look.last.one(vv: Val, path: String, older: List<&2, Val>) -> Last: match vv: case VAot{_p, rows}: LastSome{path, older, rows} case _: LastNo{"not a table"}# the newest element of an array of tables, and the older elements behind itdef look.last(elems: List<&2, Val>, path: String) -> Last: match elems: case []: LastNo{"empty array of tables"} case h <> t: look.last.one(h, path, t)# put the child rows back into the parent table. A new implicit table is# consed, not copied on. Inside an inline table it is a table too until the# inline table closes, so the next dotted key can go on in it (inl.seal)def tree.wrap(fr: Fr, child: List<&2, Val>) -> List<&2, Val>: match fr: case FrNew{name, parent, path}: VPair{name, VHead{path, child}} <> parent case FrRep{name, parent, path}: rows.repl(parent, name, VHead{path, child}) case FrAot{name, parent, pre, +path}: rows.repl(parent, name, VAots{path, VAot{path, child} <> pre})# walk the frames back to the rootdef tree.rebuild(stack: List<&2, Fr>, vs: List<&2, Val>, bad: String, heads: List<&2, String>) -> End: match stack: case []: End{vs, bad, heads} case fr <> t: tree.rebuild(t, tree.wrap(fr, vs), bad, heads)# a key that is already present is a duplicatedef tree.leaf.put( hit: Hit, name: String, vs: List<&2, Val>, val: Val, stack: List<&2, Fr>, bad: String, heads: List<&2, String>) -> End: match hit: case Found{_v}: tree.rebuild(stack, vs, err.first(bad, "duplicate key"), heads) case Miss{}: tree.rebuild(stack, VPair{name, val} <> vs, bad, heads)# an explicit header for a table that already existsdef tree.leaf.def.head( seen: Bool, name: String, vs: List<&2, Val>, rows: List<&2, Val>, stack: List<&2, Fr>, bad: String, +path: String, heads: List<&2, String>) -> End: match seen: case True{}: tree.rebuild(stack, vs, err.first(bad, "duplicate table"), heads) case False{}: tree.rebuild(stack, rows.repl(vs, name, VHead{path, rows}), bad, heads.by.head(path) <> heads)# open a standard tabledef tree.leaf.def.hit( hit: Look, name: String, vs: List<&2, Val>, stack: List<&2, Fr>, bad: String, +path: String, +heads: List<&2, String>) -> End: match hit: case KMiss{}: tree.rebuild(stack, VPair{name, VHead{path, []}} <> vs, bad, heads.by.head(path) <> heads) case KHead{_p, rows}: tree.leaf.def.head(heads.has(heads, path), name, vs, rows, stack, bad, path, heads) case KInl{}: tree.rebuild(stack, vs, err.first(bad, "inline table is closed"), heads) case KAots{_p, _elems}: tree.rebuild(stack, vs, err.first(bad, "not a table"), heads) case KNo{why}: tree.rebuild(stack, vs, err.first(bad, why), heads)# open a standard tabledef tree.leaf.def( hit: Look, +name: String, vs: List<&2, Val>, stack: List<&2, Fr>, bad: String, pre: String, heads: List<&2, String>) -> End: tree.leaf.def.hit(hit, name, vs, stack, bad, tree.pre(pre, name), heads)# append one element of an array of tablesdef tree.leaf.aot.hit( hit: Look, name: String, vs: List<&2, Val>, stack: List<&2, Fr>, bad: String, +path: String, heads: List<&2, String>) -> End: match hit: case KMiss{}: tree.rebuild(stack, VPair{name, VAots{path, [VAot{path, []}]}} <> vs, bad, heads) case KAots{_p, elems}: tree.rebuild(stack, rows.repl(vs, name, VAots{path, VAot{path, []} <> elems}), bad, heads.drop(heads, path ++ ".")) case KHead{_p, _rows}: tree.rebuild(stack, vs, err.first(bad, "not a table"), heads) case KInl{}: tree.rebuild(stack, vs, err.first(bad, "inline table is closed"), heads) case KNo{why}: tree.rebuild(stack, vs, err.first(bad, why), heads)# append one element of an array of tablesdef tree.leaf.aot( hit: Look, +name: String, vs: List<&2, Val>, stack: List<&2, Fr>, bad: String, pre: String, heads: List<&2, String>) -> End: tree.leaf.aot.hit(hit, name, vs, stack, bad, tree.pre(pre, name), heads)# the key at the end of the pathdef tree.leaf.job( job: Job, +name: String, +vs: List<&2, Val>, stack: List<&2, Fr>, bad: String, pre: String, heads: List<&2, String>) -> End: match job: case JPut{val}: tree.leaf.put(get(vs, name), name, vs, val, stack, bad, heads) case JDef{}: tree.leaf.def(rows.look(vs, name), name, vs, stack, bad, pre, heads) case JAot{}: tree.leaf.aot(rows.look(vs, name), name, vs, stack, bad, pre, heads)# a failed walk rebuilds what it hasdef tree.leaf.bad( ok: Bool, name: String, vs: List<&2, Val>, job: Job, stack: List<&2, Fr>, bad: String, pre: String, heads: List<&2, String>) -> End: match ok: case False{}: tree.rebuild(stack, vs, bad, heads) case True{}: tree.leaf.job(job, name, vs, stack, bad, pre, heads)# the key at the end of the pathdef tree.leaf(name: String, tw: Tw) -> End: Tw{vs, job, stack, +bad, pre, heads, _inl} = tw tree.leaf.bad(String.is_empty(bad), name, vs, job, stack, bad, pre, heads)# enter the last element of an array of tablesdef tree.sub.aots( last: Last, +name: String, +vs: List<&2, Val>, job: Job, stack: List<&2, Fr>, +pre: String, heads: List<&2, String>, inl: Bool, path: String) -> Tw: match last: case LastSome{_p, prefix, rows}: Tw{rows, job, FrAot{name, vs, prefix, path} <> stack, "", tree.pre(pre, name), heads, inl} case LastNo{why}: Tw{vs, job, stack, why, pre, heads, inl}# a prefix of the current table is navigation; a dotted segment defines a tabledef tree.sub.job(mark: Bool, job: Job) -> Job: match mark: case True{}: job case False{}: JDef{}# a segment of a dotted key, which defines tables as it goesdef tree.sub.dot(job: Job) -> Bool: match job: case JPut{_val}: True{} case _: False{}# enter a table, unless a dotted key would add to a table a header defineddef tree.sub.head.go( shut: Bool, path: String, rows: List<&2, Val>, +name: String, vs: List<&2, Val>, +job: Job, stack: List<&2, Fr>, +pre: String, heads: List<&2, String>, inl: Bool, mark: Bool) -> Tw: match shut: case True{}: Tw{vs, job, stack, "duplicate table", pre, heads, inl} case False{}: Tw{rows, job, FrRep{name, vs, path} <> stack, "", tree.pre(pre, name), heads.define(tree.sub.job(mark, job), tree.pre(pre, name), heads), inl}# a dotted key asks whether a header defined the table; a header walk does notdef tree.sub.head( dot: Bool, path: String, rows: List<&2, Val>, +name: String, vs: List<&2, Val>, job: Job, stack: List<&2, Fr>, +pre: String, +heads: List<&2, String>, inl: Bool, mark: Bool) -> Tw: match dot: case True{}: tree.sub.head.go(heads.head(heads, tree.pre(pre, name)), path, rows, name, vs, job, stack, pre, heads, inl, mark) case False{}: tree.sub.head.go(False{}, path, rows, name, vs, job, stack, pre, heads, inl, mark)# a header enters the last element of an array of tables; a dotted key cannot# add to an array of tablesdef tree.sub.arr( dot: Bool, +path: String, elems: List<&2, Val>, +name: String, +vs: List<&2, Val>, job: Job, stack: List<&2, Fr>, +pre: String, heads: List<&2, String>, inl: Bool) -> Tw: match dot: case True{}: Tw{vs, job, stack, "duplicate table", pre, heads, inl} case False{}: tree.sub.aots(look.last(elems, path), name, vs, job, stack, pre, heads, inl, path)# descend into one key, or record why it cannot be entereddef tree.sub.look( hit: Look, +name: String, +vs: List<&2, Val>, +job: Job, stack: List<&2, Fr>, +pre: String, heads: List<&2, String>, +inl: Bool, +mark: Bool) -> Tw: match hit: case KMiss{}: Tw{[], job, FrNew{name, vs, tree.pre(pre, name)} <> stack, "", tree.pre(pre, name), heads.define(tree.sub.job(mark, job), tree.pre(pre, name), heads), inl} case KHead{path, rows}: tree.sub.head(tree.sub.dot(tree.sub.job(mark, job)), path, rows, name, vs, job, stack, pre, heads, inl, mark) case KInl{}: Tw{vs, job, stack, "inline table is closed", pre, heads, inl} case KAots{path, elems}: tree.sub.arr(tree.sub.dot(tree.sub.job(mark, job)), path, elems, name, vs, job, stack, pre, heads, inl) case KNo{why}: Tw{vs, job, stack, why, pre, heads, inl}# descend into one key when the walk is still welldef tree.sub.bad( ok: Bool, +name: String, +vs: List<&2, Val>, job: Job, stack: List<&2, Fr>, bad: String, pre: String, heads: List<&2, String>, inl: Bool, mark: Bool) -> Tw: match ok: case False{}: Tw{vs, job, stack, bad, pre, heads, inl} case True{}: tree.sub.look(rows.look(vs, name), name, vs, job, stack, pre, heads, inl, mark)# descend into one keydef tree.sub(name: String, tw: Tw, mark: Bool) -> Tw: Tw{+vs, job, stack, +bad, pre, heads, inl} = tw tree.sub.bad(String.is_empty(bad), name, vs, job, stack, bad, pre, heads, inl, mark)# how many segments are already the current tabledef path.len(xs: List<&2, String>, nn: U32) -> U32: match xs: case []: nn case _ <> t: path.len(t, U32.add(nn, 1))# one fewer prefix segment, staying at zerodef path.dec.z(zz: Bool, skip: U32) -> U32: match zz: case True{}: 0 case False{}: U32.sub(skip, 1)# one fewer prefix segment, staying at zerodef path.dec(+skip: U32) -> U32: path.dec.z(U32.is_zero(skip), skip)# an empty path records an errordef tree.walk.none(tw: Tw) -> End: Tw{vs, _job, stack, bad, _pre, heads, _inl} = tw tree.rebuild(stack, vs, err.first(bad, "empty key"), heads)# follow a key path and apply the job at its end. skip is the current tabledef tree.walk(path: List<&2, String>, tw: Tw, +skip: U32) -> End: match path: case []: tree.walk.none(tw) case name <> rest: match rest: case []: tree.leaf(name, tw) case _ <> _: tree.walk(rest, tree.sub(name, tw, U32.is_zero(skip)), path.dec(skip))# space or tabdef ch.sp(+cc: Char) -> Bool: Bool.or(U32.is_eq(Char.to_u32(cc), 32), U32.is_eq(Char.to_u32(cc), 9))# the space character itself, U+0020, the only one a datetime may holddef ch.space(+cc: Char) -> Bool: U32.is_eq(Char.to_u32(cc), 32)# line feed or carriage returndef ch.nl(+cc: Char) -> Bool: Bool.or(U32.is_eq(Char.to_u32(cc), 10), U32.is_eq(Char.to_u32(cc), 13))# a control: U+0000 to U+001F, or U+007Fdef ch.ctl(+cc: Char) -> Bool: Bool.or(U32.is_lt(Char.to_u32(cc), 32), U32.is_eq(Char.to_u32(cc), 127))# a character that ends a bare valuedef ch.delim(+cc: Char) -> Bool: Bool.or(ch.sp(cc), Bool.or(ch.nl(cc), Bool.or(Char.is_eq(cc, '#'), Bool.or(Char.is_eq(cc, ','), Bool.or(Char.is_eq(cc, ']'), Char.is_eq(cc, '}'))))))# whitespace that a bare value swallowsdef ch.skip(+cc: Char) -> Bool: Bool.or(ch.sp(cc), ch.nl(cc))# the quote character of a string kinddef q.char(kk: QKind) -> Char: match kk: case QLit{}: '\'' case QMultiLit{}: '\'' case _: '"'# a multiline stringdef q.multi(kk: QKind) -> Bool: match kk: case QMulti{}: True{} case QMultiLit{}: True{} case _: False{}# a basic string, which has escapesdef q.basic(kk: QKind) -> Bool: match kk: case QBasic{}: True{} case QMulti{}: True{} case _: False{}# the multiline kind of a one-line kinddef q.as.multi(kk: QKind) -> QKind: match kk: case QLit{}: QMultiLit{} case _: QMulti{}# a pattern character that stands for a digitdef date.mode(pp: Char) -> Bool: U32.is_eq(Char.to_u32(pp), 68)# whether the rest of the shape matchesdef date.eq.go(ok: Bool, more: Unit -> Bool) -> Bool: match ok: case False{}: False{} case True{}: more(Unit{})# one character of a date shapedef date.step(mode: Bool, digit: Bool, same: Bool, more: Unit -> Bool) -> Bool: match mode: case True{}: date.eq.go(digit, more) case False{}: date.eq.go(same, more)# both lists ended togetherdef date.eq.nil(pat: List<&2, Char>) -> Bool: match pat: case []: True{} case _ <> _: False{}# YYYY-MM-DD, with D standing for a digitdef date.eq(cs: List<&2, Char>, pat: List<&2, Char>) -> Bool: match cs: case []: date.eq.nil(pat) case +h <> t: match pat: case []: False{} case +p <> r: date.step(date.mode(p), Char.is_digit(h), Char.is_eq(h, p), _u => date.eq(t, r))# the buffer is a calendar datedef date.shaped(buf: List<&2, Char>) -> Bool: date.eq(List.reverse(&2, Char, buf), String.to_list("DDDD-DD-DD"))# the buffer is reversed, so each head goes on the front of the textdef buf.str.go(buf: List<&2, Char>, acc: String) -> String: match buf: case []: acc case +h <> t: buf.str.go(t, SCon{h, acc})# the buffer, in orderdef buf.str(buf: List<&2, Char>) -> String: buf.str.go(buf, "")# the first n characters of a source spandef span.str(src: String, +nn: U32, zz: Bool) -> String: match src zz: case _s True{}: "" case SNil{} False{}: "" case SCon{+h, t} False{}: SCon{h, span.str(t, U32.sub(nn, 1), U32.is_zero(U32.sub(nn, 1)))}# the first n characters, reversed, so an escape can keep copyingdef span.rev(src: String, +nn: U32, zz: Bool, acc: List<&2, Char>) -> List<&2, Char>: match src zz: case _s True{}: acc case SNil{} False{}: acc case SCon{h, t} False{}: span.rev(t, U32.sub(nn, 1), U32.is_zero(U32.sub(nn, 1)), h <> acc)# the unread suffix of a span that will not be storeddef span.drop(src: String) -> Unit: match src: case SNil{}: Unit{} case SCon{_h, t}: span.drop(t)# the key path, document table then this keydef path.cat(here: List<&2, String>, parts: List<&2, String>) -> List<&2, String>: List.append(&2, String, here, List.reverse(&2, String, parts))# a multiline string keeps a newlinedef str.bad.nl(nl: Bool, multi: Bool) -> Bool: match nl: case False{}: True{} case True{}: Bool.not(multi)# tab is keptdef str.bad.tab(tab: Bool, nl: Bool, multi: Bool) -> Bool: match tab: case True{}: False{} case False{}: str.bad.nl(nl, multi)# a raw control is rejecteddef str.bad.go(ctrl: Bool, tab: Bool, nl: Bool, multi: Bool) -> Bool: match ctrl: case False{}: False{} case True{}: str.bad.tab(tab, nl, multi)# a raw control is rejected. Tab is kept. A multiline string also keeps newlinesdef str.bad(+uu: U32, multi: Bool) -> Bool: str.bad.go(Bool.or(U32.is_lt(uu, 32), U32.is_eq(uu, 127)), U32.is_eq(uu, 9), Bool.or(U32.is_eq(uu, 10), U32.is_eq(uu, 13)), multi)# the scanner has faileddef st.err(st: St, why: String) -> St: St{+bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, _hold, uni, left, arrtab, lead} = st St{err.first(bad, why), rows, heads, here, parts, buf, LSink{}, back, qkind, stack, HNone{}, uni, left, arrtab, lead}# replace the lex statedef st.lex(st: St, lex: Lex) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead}# hold one character and set the lex statedef st.hold(st: St, +lex: Lex, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, _lex, _back, qkind, stack, _hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, lex, lex, qkind, stack, HChar{cc}, uni, left, arrtab, lead}# append one characterdef st.push(st: St, cc: Char, lex: Lex) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, cc <> buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead}# the segment in the buffer joins the key, and the buffer is cleareddef seg.push(st: St, lex: Lex) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, buf.str(buf) <> parts, [], lex, back, qkind, stack, hold, uni, left, arrtab, lead}# a finished value at the document rootdef take.root( end: End, here: List<&2, String>, back: Lex, qkind: QKind, uni: U32, left: U32, arrtab: Bool, lead: Bool) -> St: End{rows, bad, heads} = end St{bad, rows, heads, here, [], [], LLine{}, back, qkind, [], HNone{}, uni, left, arrtab, lead}# a finished value inside an inline tabledef take.inl( end: End, rows: List<&2, Val>, heads: List<&2, String>, +here: List<&2, String>, +parts: List<&2, String>, rest: List<&2, Ctx>, back: Lex, qkind: QKind, uni: U32, left: U32, arrtab: Bool, lead: Bool) -> St: End{inl, bad, _h} = end St{bad, rows, heads, here, [], [], LInlC{}, back, qkind, CInl{inl, parts, here} <> rest, HNone{}, uni, left, arrtab, lead}# place a finished value into the current table, array, or inline tabledef take.top( stack: List<&2, Ctx>, val: Val, bad: String, rows: List<&2, Val>, heads: List<&2, String>, +here: List<&2, String>, parts: List<&2, String>, back: Lex, qkind: QKind, uni: U32, left: U32, arrtab: Bool, lead: Bool) -> St: match stack: case []: take.root(tree.walk(path.cat(here, parts), Tw{rows, JPut{val}, [], bad, "", heads, False{}}, path.len(here, 0)), here, back, qkind, uni, left, arrtab, lead) case CArr{items, +saved, +at} <> rest: St{bad, rows, heads, at, saved, [], LArrC{}, back, qkind, CArr{val <> items, saved, at} <> rest, HNone{}, uni, left, arrtab, lead} case CInl{inl, +saved, +at} <> rest: take.inl(tree.walk(List.reverse(&2, String, parts), Tw{inl, JPut{val}, [], bad, "", [], True{}}, 0), rows, heads, at, saved, rest, back, qkind, uni, left, arrtab, lead)# place a finished valuedef val.take(st: St, val: Val) -> St: St{bad, rows, heads, here, parts, _buf, _lex, back, qkind, stack, _hold, uni, left, arrtab, lead} = st take.top(stack, val, bad, rows, heads, here, parts, back, qkind, uni, left, arrtab, lead)# hold the delimiter when the following state must see itdef bare.hold.on(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, _h, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, HChar{cc}, uni, left, arrtab, lead}# hold the delimiter when the following state must see it# a newline that ended a root value already separates the next expressiondef bare.line.lex(lex: Lex, st: St) -> St: match lex: case LLine{}: st.lex(st, LTop{}) case _: st# a consumed newline returns to the top level after a root valuedef bare.line.go(st: St) -> St: St{bad, rows, heads, here, parts, buf, +lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st bare.line.lex(lex, St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead})# a consumed newline returns to the top level after a root valuedef bare.line(st: St, nl: Bool) -> St: match nl: case True{}: bare.line.go(st) case False{}: st# hold the delimiter when the following state must see itdef bare.hold(st: St, cc: Char, hold: Bool) -> St: match hold: case False{}: bare.line(st, ch.nl(cc)) case True{}: bare.hold.on(st, cc)# a bare word has been classifieddef bare.got(gg: Got, st: St, cc: Char, hold: Bool) -> St: match gg: case GotBad{why}: st.err(st, why) case GotVal{val}: bare.hold(val.take(st, val), cc, hold)# finish a bare word. hold is true when the delimiter is not whitespacedef bare.finish(st: St, cc: Char, hold: Bool) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, h, uni, left, arrtab, lead} = st bare.got(word.val(List.reverse(&2, Char, buf)), St{bad, rows, heads, here, parts, [], lex, back, qkind, stack, h, uni, left, arrtab, lead}, cc, hold)# close the header that was just readdef head.apply(end: End, segs: List<&2, String>, back: Lex, qkind: QKind, uni: U32, left: U32, lead: Bool) -> St: End{rows, bad, heads} = end St{bad, rows, heads, segs, [], [], LLine{}, back, qkind, [], HNone{}, uni, left, False{}, lead}# an array-of-tables header, or a table headerdef head.job( arr: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, +segs: List<&2, String>, back: Lex, qkind: QKind, uni: U32, left: U32, lead: Bool) -> St: match arr: case True{}: head.apply(tree.walk(segs, Tw{rows, JAot{}, [], bad, "", heads, False{}}, 0), segs, back, qkind, uni, left, lead) case False{}: head.apply(tree.walk(segs, Tw{rows, JDef{}, [], bad, "", heads, False{}}, 0), segs, back, qkind, uni, left, lead)# open the table or the array of tables named by the headerdef head.on(st: St, segs: List<&2, String>) -> St: St{bad, rows, heads, _here, _parts, _buf, _lex, back, qkind, _stack, _hold, uni, left, arrtab, lead} = st head.job(arrtab, bad, rows, heads, segs, back, qkind, uni, left, lead)# the header key is readydef head.go(empty: Bool, st: St, segs: List<&2, String>) -> St: match empty: case True{}: st.err(st, "empty key") case False{}: head.on(st, segs)# the segment was pusheddef head.use(st: St) -> St: St{bad, rows, heads, here, +parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st head.go(List.is_empty(&2, String, parts), St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead}, List.reverse(&2, String, parts))# the buffer is the last segment when it holds onedef head.fin.buf(empty: Bool, st: St) -> St: match empty: case True{}: head.use(st) case False{}: head.use(seg.push(st, LTop{}))# push a trailing segment, then open the headerdef head.fin(st: St) -> St: St{bad, rows, heads, here, parts, +buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st head.fin.buf(List.is_empty(&2, Char, buf), St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead})# close an array and place itdef arr.top( stack: List<&2, Ctx>, bad: String, rows: List<&2, Val>, heads: List<&2, String>, back: Lex, qkind: QKind, uni: U32, left: U32, arrtab: Bool, lead: Bool) -> St: match stack: case CArr{items, parts, here} <> rest: val.take(St{bad, rows, heads, here, parts, [], LTop{}, back, qkind, rest, HNone{}, uni, left, arrtab, lead}, VArr{List.reverse(&2, Val, items)}) case _: St{err.first(bad, "unclosed"), rows, heads, [], [], [], LSink{}, back, qkind, [], HNone{}, uni, left, arrtab, lead}# close an array and place itdef arr.close(st: St) -> St: St{bad, rows, heads, _here, _parts, _buf, _lex, back, qkind, stack, _hold, uni, left, arrtab, lead} = st arr.top(stack, bad, rows, heads, back, qkind, uni, left, arrtab, lead)# an inline table is closed once written: the tables its dotted keys made,# open while it was read, close with itdef inl.seal(rows: List<&2, Val>) -> List<&2, Val>: match rows: case []: [] case VPair{name, VHead{_path, sub}} <> t: VPair{name, VInl{inl.seal(sub)}} <> inl.seal(t) case h <> t: h <> inl.seal(t)# close an inline table and place itdef inl.top( stack: List<&2, Ctx>, bad: String, rows: List<&2, Val>, heads: List<&2, String>, back: Lex, qkind: QKind, uni: U32, left: U32, arrtab: Bool, lead: Bool) -> St: match stack: case CInl{inl, parts, here} <> rest: val.take(St{bad, rows, heads, here, parts, [], LTop{}, back, qkind, rest, HNone{}, uni, left, arrtab, lead}, VInl{inl.seal(inl)}) case _: St{err.first(bad, "unclosed"), rows, heads, [], [], [], LSink{}, back, qkind, [], HNone{}, uni, left, arrtab, lead}# close an inline table and place itdef inl.close(st: St) -> St: St{bad, rows, heads, _here, _parts, _buf, _lex, back, qkind, stack, _hold, uni, left, arrtab, lead} = st inl.top(stack, bad, rows, heads, back, qkind, uni, left, arrtab, lead)# the innermost frame is an inline tabledef inl.open(stack: List<&2, Ctx>) -> Bool: match stack: case CInl{_rows, _parts, _here} <> _rest: True{} case _: False{}# a string is finisheddef str.fin(st: St) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st val.take(St{bad, rows, heads, here, parts, [], lex, back, qkind, stack, hold, uni, left, arrtab, lead}, VStr{buf.str(buf)})# one or two quotes that did not close a multiline stringdef mq.put(one: Bool, +qq: Char, buf: List<&2, Char>) -> List<&2, Char>: match one: case True{}: qq <> buf case False{}: qq <> (qq <> buf)# a code point may be written as a characterdef uni.ok(+uu: U32) -> Bool: Bool.and(U32.is_le(uu, 1114111), Bool.not(Bool.and(U32.is_ge(uu, 55296), U32.is_le(uu, 57343))))# a finished unicode escape written into the bufferdef uni.finish.on(st: St, uu: U32, back: Lex) -> St: St{bad, rows, heads, here, parts, buf, _lex, b, qkind, stack, _hold, _uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, Char.from_u32(uu) <> buf, back, b, qkind, stack, HNone{}, 0, 0, arrtab, lead}# a finished unicode escapedef uni.finish(ok: Bool, st: St, uu: U32, back: Lex) -> St: match ok: case False{}: st.err(st, "invalid escape") case True{}: uni.finish.on(st, uu, back)# another hex digit is still requireddef uni.next.more(st: St, uu: U32, left: U32) -> St: St{bad, rows, heads, here, parts, buf, lex, b, qkind, stack, hold, _uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, lex, b, qkind, stack, hold, uu, U32.sub(left, 1), arrtab, lead}# the escape is finished, or another hex digit is requireddef uni.next(last: Bool, st: St, +uu: U32, back: Lex, left: U32) -> St: match last: case True{}: uni.finish(uni.ok(uu), st, uu, back) case False{}: uni.next.more(st, uu, left)# one hex digit of a unicode escapedef uni.go(ok: Bool, st: St, +cc: Char, back: Lex, uni: U32, +left: U32) -> St: match ok: case False{}: st.err(st, "invalid escape") case True{}: uni.next(U32.is_eq(left, 1), st, U32.add(U32.mul(uni, 16), dig.val(cc)), back, left)# one hex digit of a unicode escapedef uni.step(st: St, +cc: Char, back: Lex) -> St: St{bad, rows, heads, here, parts, buf, lex, b, qkind, stack, hold, +uni, +left, arrtab, lead} = st uni.go(dig.ok(dig.val(cc), 16), St{bad, rows, heads, here, parts, buf, lex, b, qkind, stack, hold, uni, left, arrtab, lead}, cc, back, uni, left)# a long unicode escape, or an unknown escapedef esc.uu(uu: Bool) -> Esc: match uu: case True{}: EUni{8} case False{}: EBad{}# a unicode escapedef esc.u(uuu: Bool, uu: Bool) -> Esc: match uuu: case True{}: EUni{4} case False{}: esc.uu(uu)# carriage return or a unicode escapedef esc.r(rr: Bool, uuu: Bool, uu: Bool) -> Esc: match rr: case True{}: EChar{'\r'} case False{}: esc.u(uuu, uu)# form feed or the restdef esc.f(ff: Bool, rr: Bool, uuu: Bool, uu: Bool) -> Esc: match ff: case True{}: EChar{Chr{12}} case False{}: esc.r(rr, uuu, uu)# newline or the restdef esc.n(nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool) -> Esc: match nn: case True{}: EChar{'\n'} case False{}: esc.f(ff, rr, uuu, uu)# tab, newline, form feed, carriage return, or a unicode escapedef esc.t(tt: Bool, nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool) -> Esc: match tt: case True{}: EChar{'\t'} case False{}: esc.n(nn, ff, rr, uuu, uu)# backslash, quotation mark, or a letter escapedef esc.letter(bb: Bool, tt: Bool, nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool) -> Esc: match bb: case True{}: EChar{Chr{8}} case False{}: esc.t(tt, nn, ff, rr, uuu, uu)# spaces before the newline of a line-ending backslashdef esc.sp(sp: Bool, bb: Bool, tt: Bool, nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool) -> Esc: match sp: case True{}: EFold{False{}} case False{}: esc.letter(bb, tt, nn, ff, rr, uuu, uu)# a line-ending backslash, or a letterdef esc.fold(nl: Bool, sp: Bool, bb: Bool, tt: Bool, nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool) -> Esc: match nl: case True{}: EFold{True{}} case False{}: esc.sp(sp, bb, tt, nn, ff, rr, uuu, uu)# a backslash or a letterdef esc.bs(bs: Bool, bb: Bool, tt: Bool, nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool, nl: Bool, sp: Bool) -> Esc: match bs: case True{}: EChar{'\\'} case False{}: esc.fold(nl, sp, bb, tt, nn, ff, rr, uuu, uu)# a quote, a backslash, or a letterdef esc.of.go( qq: Bool, bs: Bool, bb: Bool, tt: Bool, nn: Bool, ff: Bool, rr: Bool, uuu: Bool, uu: Bool, nl: Bool, sp: Bool) -> Esc: match qq: case True{}: EChar{'"'} case False{}: esc.bs(bs, bb, tt, nn, ff, rr, uuu, uu, nl, sp)# classify one escaped characterdef esc.of(+cc: Char) -> Esc: esc.of.go(Char.is_eq(cc, '"'), Char.is_eq(cc, '\\'), Char.is_eq(cc, 'b'), Char.is_eq(cc, 't'), Char.is_eq(cc, 'n'), Char.is_eq(cc, 'f'), Char.is_eq(cc, 'r'), Char.is_eq(cc, 'u'), Char.is_eq(cc, 'U'), ch.nl(cc), ch.sp(cc))# the lex state that reads the hex digits of an escapedef esc.uni.lex(back: Lex) -> Lex: match back: case LKeyQ{}: LKeyU{} case LHeadQ{}: LHeadU{} case _: LUni{}# a unicode escape starts with no digits seendef esc.uni(st: St, back: Lex, nn: U32) -> St: St{bad, rows, heads, here, parts, buf, _lex, b, qkind, stack, hold, _uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, esc.uni.lex(back), b, qkind, stack, hold, 0, nn, arrtab, lead}# 1 once a newline has been seendef esc.fold.bit(saw: Bool) -> U32: match saw: case True{}: 1 case False{}: 0# a line-ending backslash skips following whitespacedef esc.fold.on(st: St, saw: Bool) -> St: St{bad, rows, heads, here, parts, buf, _lex, b, qkind, stack, hold, _uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, LFold{}, b, qkind, stack, hold, 0, esc.fold.bit(saw), arrtab, lead}# a line-ending backslash is only part of a multiline stringdef esc.fold.kind(multi: Bool, st: St, saw: Bool) -> St: match multi: case True{}: esc.fold.on(st, saw) case False{}: st.err(st, "invalid escape")# a line-ending backslash is only part of a multiline stringdef esc.fold.str(st: St, saw: Bool) -> St: St{bad, rows, heads, here, parts, buf, lex, b, +qkind, stack, hold, uni, left, arrtab, lead} = st esc.fold.kind(q.multi(qkind), St{bad, rows, heads, here, parts, buf, lex, b, qkind, stack, hold, uni, left, arrtab, lead}, saw)# a line-ending backslash is only part of a stringdef esc.fold.use(back: Lex, st: St, saw: Bool) -> St: match back: case LStr{}: esc.fold.str(st, saw) case _: st.err(st, "invalid escape")# apply one escape and return to the stringdef esc.use(ee: Esc, st: St, back: Lex) -> St: match ee: case EChar{c}: st.push(st, c, back) case EUni{n}: esc.uni(st, back, n) case EFold{saw}: esc.fold.use(back, st, saw) case EBad{}: st.err(st, "invalid escape")# the lex state a comment returns todef com.enter(st: St, back: Lex) -> St: St{bad, rows, heads, here, parts, buf, _lex, _back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, LCom{}, back, qkind, stack, hold, uni, left, arrtab, lead}# a bare key, or a rejected characterdef lex.top.bare(bare: Bool, st: St, cc: Char) -> St: match bare: case True{}: st.push(st, cc, LKey{}) case False{}: st.err(st, "invalid key")# a header starts with a clear keydef lex.bracket(st: St) -> St: St{bad, rows, heads, here, _parts, _buf, _lex, back, qkind, stack, hold, uni, left, _arr, lead} = st St{bad, rows, heads, here, [], [], LHead{}, back, qkind, stack, hold, uni, left, False{}, lead}# a quoted key starts with an empty bufferdef lex.qkey(st: St, kind: QKind, lex: Lex) -> St: St{bad, rows, heads, here, parts, _buf, _lex, back, _q, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, [], lex, back, kind, stack, hold, uni, left, arrtab, lead}# a literal key or a bare keydef lex.top.lit(lit: Bool, bare: Bool, st: St, cc: Char) -> St: match lit: case True{}: lex.qkey(st, QLit{}, LKeyQ{}) case False{}: lex.top.bare(bare, st, cc)# a quoted key or a bare keydef lex.top.q(qq: Bool, lit: Bool, bare: Bool, st: St, cc: Char) -> St: match qq: case True{}: lex.qkey(st, QBasic{}, LKeyQ{}) case False{}: lex.top.lit(lit, bare, st, cc)# a header or a keydef lex.top.br(br: Bool, qq: Bool, lit: Bool, bare: Bool, st: St, cc: Char) -> St: match br: case True{}: lex.bracket(st) case False{}: lex.top.q(qq, lit, bare, st, cc)# a comment, a header, or a keydef lex.top.hash(hash: Bool, br: Bool, qq: Bool, lit: Bool, bare: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LTop{}) case False{}: lex.top.br(br, qq, lit, bare, st, cc)# a newline, a comment, a header, or a keydef lex.top.nl(nl: Bool, hash: Bool, br: Bool, qq: Bool, lit: Bool, bare: Bool, st: St, cc: Char) -> St: match nl: case True{}: st case False{}: lex.top.hash(hash, br, qq, lit, bare, st, cc)# whitespace, a comment, a header, or a keydef lex.top.go(sp: Bool, nl: Bool, hash: Bool, br: Bool, qq: Bool, lit: Bool, bare: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.top.nl(nl, hash, br, qq, lit, bare, st, cc)# top level: a key, a header, a comment, or whitespacedef lex.top(st: St, +cc: Char) -> St: lex.top.go(ch.sp(cc), ch.nl(cc), Char.is_eq(cc, '#'), Char.is_eq(cc, '['), Char.is_eq(cc, '"'), Char.is_eq(cc, '\''), bare.at(cc), st, cc)# a newline before equals is rejecteddef lex.key.nl(nl: Bool, st: St, _c: Char) -> St: match nl: case True{}: st.err(st, "newline in key") case False{}: st.err(st, "invalid key")# an empty segment is rejecteddef lex.key.end.go(empty: Bool, st: St, lex: Lex) -> St: match empty: case True{}: st.err(st, "empty key") case False{}: seg.push(st, lex)# end a nonempty segmentdef lex.key.end(st: St, next: Lex) -> St: St{bad, rows, heads, here, parts, +buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st lex.key.end.go(List.is_empty(&2, Char, buf), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, next)# a comment ends the segment and waits for equalsdef lex.key.hash(hash: Bool, nl: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(lex.key.end(st, LAfter{}), LAfter{}) case False{}: lex.key.nl(nl, st, cc)# an equals ends the keydef lex.key.eq(eq: Bool, hash: Bool, nl: Bool, st: St, +cc: Char) -> St: match eq: case True{}: lex.key.end(st, LVal{}) case False{}: lex.key.hash(hash, nl, st, cc)# a dot ends the segment and skips whitespace before the next onedef lex.key.dot(dot: Bool, eq: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match dot: case True{}: lex.key.end(st, LKeyDot{}) case False{}: lex.key.eq(eq, hash, nl, st, cc)# whitespace ends the segmentdef lex.key.sp(sp: Bool, dot: Bool, eq: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: lex.key.end(st, LAfter{}) case False{}: lex.key.dot(dot, eq, hash, nl, st, cc)# a bare character, whitespace, a dot, or an equalsdef lex.key.go(bare: Bool, sp: Bool, dot: Bool, eq: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match bare: case True{}: st.push(st, cc, LKey{}) case False{}: lex.key.sp(sp, dot, eq, hash, nl, st, cc)# inside a bare key segmentdef lex.key(st: St, +cc: Char) -> St: lex.key.go(bare.at(cc), ch.sp(cc), Char.is_eq(cc, '.'), Char.is_eq(cc, '='), Char.is_eq(cc, '#'), ch.nl(cc), st, cc)# a newline before equals is rejecteddef lex.after.nl(nl: Bool, st: St, _c: Char) -> St: match nl: case True{}: st.err(st, "newline in key") case False{}: st.err(st, "invalid key")# a comment waits for equalsdef lex.after.hash(hash: Bool, nl: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LAfter{}) case False{}: lex.after.nl(nl, st, cc)# equals starts the valuedef lex.after.eq(eq: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match eq: case True{}: st.lex(st, LVal{}) case False{}: lex.after.hash(hash, nl, st, cc)# a dot starts the next segment, after any whitespacedef lex.after.dot(dot: Bool, eq: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match dot: case True{}: st.lex(st, LKeyDot{}) case False{}: lex.after.eq(eq, hash, nl, st, cc)# whitespace, a dot, or an equalsdef lex.after.go(sp: Bool, dot: Bool, eq: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.after.dot(dot, eq, hash, nl, st, cc)# after a key segment, before equals or a dotdef lex.after(st: St, +cc: Char) -> St: lex.after.go(ch.sp(cc), Char.is_eq(cc, '.'), Char.is_eq(cc, '='), Char.is_eq(cc, '#'), ch.nl(cc), st, cc)# a value starts on the line of its keydef lex.val.nl3(inside: Bool, st: St) -> St: match inside: case True{}: st.err(st, "newline in inline table") case False{}: st.err(st, "newline before value")# a newline before a value is rejecteddef lex.val.nl2(st: St, _c: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, qkind, +stack, hold, uni, left, arrtab, lead} = st lex.val.nl3(inl.open(stack), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead})# an array starts a frame and waits for a valuedef lex.array(st: St) -> St: St{bad, rows, heads, +here, +parts, _buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, [], [], LArr{}, back, qkind, CArr{[], parts, here} <> stack, hold, uni, left, arrtab, lead}# an inline table starts a frame and waits for a keydef lex.inline(st: St) -> St: St{bad, rows, heads, +here, +parts, _buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, [], [], LInl{}, back, qkind, CInl{[], parts, here} <> stack, hold, uni, left, arrtab, lead}# an inline table, or a bare valuedef lex.val.inl(inl: Bool, st: St, cc: Char) -> St: match inl: case True{}: lex.inline(st) case False{}: st.push(st, cc, LBare{})# an array, or another valuedef lex.val.br(br: Bool, inl: Bool, st: St, cc: Char) -> St: match br: case True{}: lex.array(st) case False{}: lex.val.inl(inl, st, cc)# a literal string, or another valuedef lex.val.lit(lit: Bool, br: Bool, inl: Bool, st: St, cc: Char) -> St: match lit: case True{}: lex.qkey(st, QLit{}, LQ{}) case False{}: lex.val.br(br, inl, st, cc)# a basic string, or another valuedef lex.val.q(qq: Bool, lit: Bool, br: Bool, inl: Bool, st: St, cc: Char) -> St: match qq: case True{}: lex.qkey(st, QBasic{}, LQ{}) case False{}: lex.val.lit(lit, br, inl, st, cc)# a comment, a string, an array, an inline table, or a bare valuedef lex.val.hash(hash: Bool, qq: Bool, lit: Bool, br: Bool, inl: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LVal{}) case False{}: lex.val.q(qq, lit, br, inl, st, cc)# a newline before a value is rejecteddef lex.val.nl(nl: Bool, hash: Bool, qq: Bool, lit: Bool, br: Bool, inl: Bool, st: St, cc: Char) -> St: match nl: case True{}: lex.val.nl2(st, cc) case False{}: lex.val.hash(hash, qq, lit, br, inl, st, cc)# whitespace or the value itselfdef lex.val.go(sp: Bool, nl: Bool, hash: Bool, qq: Bool, lit: Bool, br: Bool, inl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.val.nl(nl, hash, qq, lit, br, inl, st, cc)# the start of a valuedef lex.val(st: St, +cc: Char) -> St: lex.val.go(ch.sp(cc), ch.nl(cc), Char.is_eq(cc, '#'), Char.is_eq(cc, '"'), Char.is_eq(cc, '\''), Char.is_eq(cc, '['), Char.is_eq(cc, '{'), st, cc)# a second quote may start a multiline string; otherwise the first character is helddef lex.q.go(same: Bool, st: St, cc: Char) -> St: match same: case True{}: st.lex(st, LQ2{}) case False{}: st.hold(st.lex(st, LStr{}), LStr{}, cc)# the first quote has been seendef lex.q(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, +qkind, stack, hold, uni, left, arrtab, lead} = st lex.q.go(Char.is_eq(cc, q.char(qkind)), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc)# a multiline string, about to trim one opening newlinedef lex.multi(st: St, kind: QKind) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, _q, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, LTrim{}, back, q.as.multi(kind), stack, hold, uni, left, arrtab, lead}# a third quote starts a multiline string; two quotes are an empty stringdef lex.q2.go(same: Bool, st: St, cc: Char, kind: QKind) -> St: match same: case True{}: lex.multi(st, kind) case False{}: bare.hold(val.take(st, VStr{""}), cc, True{})# two quotes have been seendef lex.q2(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, +qkind, stack, hold, uni, left, arrtab, lead} = st lex.q2.go(Char.is_eq(cc, q.char(qkind)), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc, qkind)# a line feed is trimmed; any other character is the first of the stringdef lex.trim.nl(nl: Bool, st: St, cc: Char) -> St: match nl: case True{}: st.lex(st, LStr{}) case False{}: st.hold(st.lex(st, LStr{}), LStr{}, cc)# a carriage return may still have a line feed after itdef lex.trim.go(cr: Bool, nl: Bool, st: St, cc: Char) -> St: match cr: case True{}: st.lex(st, LTrim2{}) case False{}: lex.trim.nl(nl, st, cc)# trim one opening newline of a multiline stringdef lex.trim(st: St, +cc: Char) -> St: lex.trim.go(U32.is_eq(Char.to_u32(cc), 13), ch.nl(cc), st, cc)# the line feed of a CRLF opening newlinedef lex.trim2.go(lf: Bool, st: St, cc: Char) -> St: match lf: case True{}: st.lex(st, LStr{}) case False{}: st.hold(st.lex(st, LStr{}), LStr{}, cc)# the line feed of a CRLF opening newlinedef lex.trim2(st: St, +cc: Char) -> St: lex.trim2.go(U32.is_eq(Char.to_u32(cc), 10), st, cc)# a forbidden control, or one character of the stringdef lex.str.char.go(bad: Bool, st: St, cc: Char) -> St: match bad: case True{}: st.err(st, "invalid control") case False{}: st.push(st, cc, LStr{})# a raw character, rejected when it is a forbidden controldef lex.str.char(multi: Bool, st: St, +cc: Char) -> St: lex.str.char.go(str.bad(Char.to_u32(cc), multi), st, cc)# an escape, or a plain characterdef lex.str.bs(bs: Bool, multi: Bool, st: St, cc: Char, _kind: QKind) -> St: match bs: case True{}: st.lex(st, LEsc{}) case False{}: lex.str.char(multi, st, cc)# a backslash starts an escape in a basic stringdef lex.str.esc(basic: Bool, multi: Bool, st: St, +cc: Char, kind: QKind) -> St: match basic: case True{}: lex.str.bs(Char.is_eq(cc, '\\'), multi, st, cc, kind) case False{}: lex.str.char(multi, st, cc)# count closing quotes of a multiline stringdef lex.mq(st: St, nn: U32) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, hold, _uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, LMq{}, back, qkind, stack, hold, 0, nn, arrtab, lead}# one quote closes a one-line string and may close a multiline stringdef lex.str.quote(multi: Bool, st: St) -> St: match multi: case False{}: str.fin(st) case True{}: lex.mq(st, 1)# a quote, an escape, or a characterdef lex.str.go(quote: Bool, multi: Bool, basic: Bool, st: St, +cc: Char, kind: QKind) -> St: match quote: case True{}: lex.str.quote(multi, st) case False{}: lex.str.esc(basic, multi, st, cc, kind)# inside a stringdef lex.str(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, +qkind, stack, hold, uni, left, arrtab, lead} = st lex.str.go(Char.is_eq(cc, q.char(qkind)), q.multi(qkind), q.basic(qkind), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc, qkind)# a closing delimiter has been read. up to two more quotes are still the string'sdef lex.mq.shut(st: St) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, hold, uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, LMqEnd{}, back, qkind, stack, hold, uni, 0, arrtab, lead}# the second quote waits; the third is a closing delimiterdef lex.mq.one(one: Bool, st: St) -> St: match one: case True{}: lex.mq(st, 2) case False{}: lex.mq.shut(st)# quotes that were not a closer return to the string, and the character is helddef lex.mq.back(st: St, cc: Char, buf: List<&2, Char>) -> St: St{bad, rows, heads, here, parts, _buf, _lex, back, qkind, stack, _hold, uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, LStr{}, back, qkind, stack, HChar{cc}, uni, 0, arrtab, lead}# a quote continues the closer; anything else is contentdef lex.mq.go(quote: Bool, one: Bool, st: St, cc: Char, buf: List<&2, Char>, kind: QKind, _left: U32) -> St: match quote: case True{}: lex.mq.one(one, st) case False{}: lex.mq.back(st, cc, mq.put(one, q.char(kind), buf))# one character while closing a multiline stringdef lex.mq.step(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, +buf, lx, back, +qkind, stack, hold, uni, +left, arrtab, lead} = st lex.mq.go(Char.is_eq(cc, q.char(qkind)), U32.is_eq(left, 1), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc, buf, qkind, left)# a quote after the closing delimiter: the delimiter moves one quote ondef lex.mq.more(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, cc <> buf, lx, back, qkind, stack, hold, uni, U32.add(left, 1), arrtab, lead}# the string ends before this character, which the next state readsdef lex.mq.fin(st: St, cc: Char) -> St: bare.hold(str.fin(st), cc, True{})# a third quote after the delimiter would make three in the stringdef lex.mq.room(room: Bool, st: St, cc: Char) -> St: match room: case True{}: lex.mq.more(st, cc) case False{}: lex.mq.fin(st, cc)# a quote may still belong to the string; anything else follows itdef lex.mq.end.go(quote: Bool, room: Bool, st: St, cc: Char) -> St: match quote: case True{}: lex.mq.room(room, st, cc) case False{}: lex.mq.fin(st, cc)# one character after a multiline string's closing delimiterdef lex.mq.end(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, +qkind, stack, hold, uni, +left, arrtab, lead} = st lex.mq.end.go(Char.is_eq(cc, q.char(qkind)), U32.is_lt(left, 2), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc)# one escaped characterdef lex.esc(st: St, +cc: Char) -> St: esc.use(esc.of(cc), st, LStr{})# remember that a newline was seendef lex.fold.saw(st: St) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, _left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, 1, arrtab, lead}# a character before the newline is rejecteddef lex.fold.end.go(saw: Bool, st: St, cc: Char) -> St: match saw: case True{}: st.hold(st.lex(st, LStr{}), LStr{}, cc) case False{}: st.err(st, "invalid escape")# the next character ends the fold only after a newlinedef lex.fold.end(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, +left, arrtab, lead} = st lex.fold.end.go(U32.is_eq(left, 1), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc)# a newline has been seen, so later whitespace may still be skippeddef lex.fold.nl(nl: Bool, st: St, cc: Char) -> St: match nl: case True{}: lex.fold.saw(st) case False{}: lex.fold.end(st, cc)# more whitespace, or the next character of the stringdef lex.fold.go(sp: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.fold.nl(nl, st, cc)# whitespace after a line-ending backslashdef lex.fold(st: St, +cc: Char) -> St: lex.fold.go(ch.sp(cc), ch.nl(cc), st, cc)# keep the space only when the buffer is a date; otherwise it ends the tokendef lex.bare.date.go(date: Bool, st: St, cc: Char) -> St: match date: case True{}: st.lex(st, LDate{}) case False{}: bare.finish(st, cc, False{})# a date may continue after one spacedef lex.bare.date(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, +buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st lex.bare.date.go(date.shaped(buf), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc)# a newline inside an inline table is held, and so is any other delimiterdef lex.bare.hold(skip: Bool, nl: Bool, inside: Bool) -> Bool: Bool.or(Bool.not(skip), Bool.and(nl, inside))# a delimiter other than a date-space is held unless it is whitespacedef lex.bare.end(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lx, back, qkind, +stack, hold, uni, left, arrtab, lead} = st bare.finish(St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc, lex.bare.hold(ch.skip(cc), ch.nl(cc), inl.open(stack)))# a space may be the space inside a datetimedef lex.bare.sp(sp: Bool, st: St, cc: Char) -> St: match sp: case True{}: lex.bare.date(st, cc) case False{}: lex.bare.end(st, cc)# a delimiter ends the tokendef lex.bare.go(delim: Bool, sp: Bool, st: St, cc: Char) -> St: match delim: case False{}: st.push(st, cc, LBare{}) case True{}: lex.bare.sp(sp, st, cc)# a bare valuedef lex.bare(st: St, +cc: Char) -> St: lex.bare.go(ch.delim(cc), ch.space(cc), st, cc)# the space and the digit belong to the datetimedef lex.date.digit(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, cc <> (' ' <> buf), LBare{}, back, qkind, stack, hold, uni, left, arrtab, lead}# the character after the space of a possible datetimedef lex.date.go(digit: Bool, st: St, cc: Char) -> St: match digit: case True{}: lex.date.digit(st, cc) case False{}: bare.finish(st, cc, True{})# the character after the space of a possible datetimedef lex.date(st: St, +cc: Char) -> St: lex.date.go(Char.is_digit(cc), st, cc)# the newline is read again in the state that was waitingdef lex.com.nl(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, _lex, +back, qkind, stack, _hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, back, back, qkind, stack, HChar{cc}, uni, left, arrtab, lead}# tab is the one control a comment may holddef lex.com.tab(tab: Bool, st: St) -> St: match tab: case True{}: st case False{}: st.err(st, "invalid control in comment")# any other character is part of the commentdef lex.com.ctl(ctl: Bool, tab: Bool, st: St) -> St: match ctl: case False{}: st case True{}: lex.com.tab(tab, st)# a comment runs to the end of the line and holds no control but tabdef lex.com.go(nl: Bool, ctl: Bool, tab: Bool, st: St, cc: Char) -> St: match nl: case False{}: lex.com.ctl(ctl, tab, st) case True{}: lex.com.nl(st, cc)# a comment runs to the end of the linedef lex.com(st: St, +cc: Char) -> St: lex.com.go(ch.nl(cc), ch.ctl(cc), U32.is_eq(Char.to_u32(cc), 9), st, cc)# the header is an array of tablesdef lex.head.arr(st: St) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, _arr, lead} = st St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, True{}, lead}# one extra bracketdef lex.head.br2(once: Bool, st: St) -> St: match once: case False{}: st.err(st, "invalid header") case True{}: lex.head.arr(st)# the second bracket is only legal at the startdef lex.head.br.go(fresh: Bool, once: Bool, st: St) -> St: match fresh: case False{}: st.err(st, "invalid header") case True{}: lex.head.br2(once, st)# [[ opens an array of tables when the key has not starteddef lex.head.br(st: St) -> St: St{bad, rows, heads, here, +parts, +buf, lx, back, qkind, stack, hold, uni, left, +arrtab, lead} = st lex.head.br.go(Bool.and(List.is_empty(&2, String, parts), List.is_empty(&2, Char, buf)), Bool.not(arrtab), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead})# between segments a dot is requireddef lex.head.ch.go(gap: Bool, st: St, cc: Char) -> St: match gap: case True{}: st.err(st, "invalid header") case False{}: st.push(st, cc, LHead{})# a bare character continues the segment, or starts one at the beginningdef lex.head.ch(st: St, cc: Char) -> St: St{bad, rows, heads, here, +parts, +buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st lex.head.ch.go(Bool.and(Bool.not(List.is_empty(&2, String, parts)), List.is_empty(&2, Char, buf)), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, cc)# whitespace with no segment open still needs a segment after it, so a second# bracket there is refused: `[[` is two brackets with nothing betweendef lex.head.gap.go(empty: Bool, st: St) -> St: match empty: case True{}: st.lex(st, LHeadDot{}) case False{}: seg.push(st, LHeadGap{})# whitespace ends a segment when one is opendef lex.head.gap(st: St) -> St: St{bad, rows, heads, here, parts, +buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st lex.head.gap.go(List.is_empty(&2, Char, buf), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead})# a dot with no segment is rejecteddef lex.head.dot3(empty: Bool, st: St) -> St: match empty: case True{}: st.err(st, "empty key") case False{}: seg.push(st, LHeadDot{})# a dot ends the open segmentdef lex.head.dot2(st: St) -> St: St{bad, rows, heads, here, parts, +buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st lex.head.dot3(List.is_empty(&2, Char, buf), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead})# an array of tables still needs one ]def lex.head.close2(arr: Bool, st: St) -> St: match arr: case True{}: st.lex(st, LHeadR{}) case False{}: head.fin(st)# push the last segment when the buffer holds onedef lex.head.close.go(empty: Bool, arr: Bool, st: St) -> St: match empty: case True{}: lex.head.close2(arr, st) case False{}: lex.head.close2(arr, seg.push(st, LHead{}))# ] closes, or waits for the second ] of an array of tablesdef lex.head.close(st: St) -> St: St{bad, rows, heads, here, parts, +buf, lx, back, qkind, stack, hold, uni, left, +arrtab, lead} = st lex.head.close.go(List.is_empty(&2, Char, buf), arrtab, St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead})# a comment in a headerdef lex.head.hash(hash: Bool, st: St) -> St: match hash: case True{}: com.enter(st, LHead{}) case False{}: st.err(st, "invalid header")# a quote opens a segment only where none has started. After a bare character# it would glue two keys into one segment, which toml.abnf does not allowdef lex.head.quote.go(fresh: Bool, st: St, kind: QKind) -> St: match fresh: case True{}: lex.qkey(st, kind, LHeadQ{}) case False{}: st.err(st, "invalid header")# a quoted header segment, right after the bracketdef lex.head.quote(st: St, kind: QKind) -> St: St{bad, rows, heads, here, +parts, +buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead} = st lex.head.quote.go(Bool.and(List.is_empty(&2, Char, buf), List.is_empty(&2, String, parts)), St{bad, rows, heads, here, parts, buf, lx, back, qkind, stack, hold, uni, left, arrtab, lead}, kind)# a literal header segment or a commentdef lex.head.lit(lit: Bool, hash: Bool, st: St) -> St: match lit: case True{}: lex.head.quote(st, QLit{}) case False{}: lex.head.hash(hash, st)# a quoted header segment or a commentdef lex.head.q(qq: Bool, lit: Bool, hash: Bool, st: St, _c: Char) -> St: match qq: case True{}: lex.head.quote(st, QBasic{}) case False{}: lex.head.lit(lit, hash, st)# the closing bracket, a quoted key, or a commentdef lex.head.end(end: Bool, qq: Bool, lit: Bool, hash: Bool, st: St, cc: Char) -> St: match end: case True{}: lex.head.close(st) case False{}: lex.head.q(qq, lit, hash, st, cc)# a dot, the end, or a quoted segmentdef lex.head.dot(dot: Bool, end: Bool, qq: Bool, lit: Bool, hash: Bool, st: St, cc: Char) -> St: match dot: case True{}: lex.head.dot2(st) case False{}: lex.head.end(end, qq, lit, hash, st, cc)# whitespace, a dot, or the closing bracketdef lex.head.sp(sp: Bool, dot: Bool, end: Bool, qq: Bool, lit: Bool, hash: Bool, st: St, cc: Char) -> St: match sp: case True{}: lex.head.gap(st) case False{}: lex.head.dot(dot, end, qq, lit, hash, st, cc)# a bare character, whitespace, a dot, or the enddef lex.head.bare(bare: Bool, sp: Bool, dot: Bool, end: Bool, qq: Bool, lit: Bool, hash: Bool, st: St, cc: Char) -> St: match bare: case True{}: lex.head.ch(st, cc) case False{}: lex.head.sp(sp, dot, end, qq, lit, hash, st, cc)# a second bracket, a key character, or whitespacedef lex.head.go( br: Bool, bare: Bool, sp: Bool, dot: Bool, end: Bool, qq: Bool, lit: Bool, hash: Bool, st: St, cc: Char) -> St: match br: case True{}: lex.head.br(st) case False{}: lex.head.bare(bare, sp, dot, end, qq, lit, hash, st, cc)# inside a headerdef lex.head(st: St, +cc: Char) -> St: lex.head.go(Char.is_eq(cc, '['), bare.at(cc), ch.sp(cc), Char.is_eq(cc, '.'), Char.is_eq(cc, ']'), Char.is_eq(cc, '"'), Char.is_eq(cc, '\''), Char.is_eq(cc, '#'), st, cc)# the end, or a commentdef lex.hgap.end(end: Bool, hash: Bool, st: St) -> St: match end: case True{}: lex.head.close(st) case False{}: lex.head.hash(hash, st)# a dot starts the next segmentdef lex.hgap.dot(dot: Bool, end: Bool, hash: Bool, st: St) -> St: match dot: case True{}: st.lex(st, LHeadDot{}) case False{}: lex.hgap.end(end, hash, st)# whitespace, a dot, or the enddef lex.hgap(sp: Bool, dot: Bool, end: Bool, hash: Bool, st: St, _c: Char) -> St: match sp: case True{}: st case False{}: lex.hgap.dot(dot, end, hash, st)# between header segmentsdef lex.head.gap.step(st: St, +cc: Char) -> St: lex.hgap(ch.sp(cc), Char.is_eq(cc, '.'), Char.is_eq(cc, ']'), Char.is_eq(cc, '#'), st, cc)# a literal segment, or a rejected characterdef lex.hdot.lit(lit: Bool, st: St, _c: Char) -> St: match lit: case True{}: lex.qkey(st, QLit{}, LHeadQ{}) case False{}: st.err(st, "invalid header")# a quoted segmentdef lex.hdot.q(qq: Bool, lit: Bool, st: St, cc: Char) -> St: match qq: case True{}: lex.qkey(st, QBasic{}, LHeadQ{}) case False{}: lex.hdot.lit(lit, st, cc)# a bare segment or a quoted onedef lex.hdot.bare(bare: Bool, qq: Bool, lit: Bool, st: St, cc: Char) -> St: match bare: case True{}: st.push(st, cc, LHead{}) case False{}: lex.hdot.q(qq, lit, st, cc)# whitespace or the next segmentdef lex.hdot(sp: Bool, bare: Bool, qq: Bool, lit: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.hdot.bare(bare, qq, lit, st, cc)# after a dot, a segment must startdef lex.head.dot.step(st: St, +cc: Char) -> St: lex.hdot(ch.sp(cc), bare.at(cc), Char.is_eq(cc, '"'), Char.is_eq(cc, '\''), st, cc)# a forbidden control, or one character of the keydef lex.kq.char.go(bad: Bool, st: St, cc: Char, stay: Lex) -> St: match bad: case True{}: st.err(st, "invalid control") case False{}: st.push(st, cc, stay)# one character of a quoted keydef lex.kq.char(st: St, +cc: Char, stay: Lex) -> St: lex.kq.char.go(str.bad(Char.to_u32(cc), False{}), st, cc, stay)# a newline in a key is rejecteddef lex.kq.nl(nl: Bool, st: St, +cc: Char, stay: Lex) -> St: match nl: case True{}: st.err(st, "newline in key") case False{}: lex.kq.char(st, cc, stay)# the escape state for a quoted keydef lex.esc.lex(stay: Lex) -> Lex: match stay: case LHeadQ{}: LHeadE{} case _: LKeyE{}# the escape returns to the quoted keydef lex.kesc(st: St, +stay: Lex) -> St: St{bad, rows, heads, here, parts, buf, _lex, _b, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, parts, buf, lex.esc.lex(stay), stay, qkind, stack, hold, uni, left, arrtab, lead}# only a basic key treats the backslash as an escapedef lex.kq.esc(basic: Bool, st: St, stay: Lex) -> St: match basic: case True{}: lex.kesc(st, stay) case False{}: st.push(st, '\\', stay)# an escape in a basic keydef lex.kq.bs(bs: Bool, basic: Bool, nl: Bool, st: St, cc: Char, stay: Lex) -> St: match bs: case True{}: lex.kq.esc(basic, st, stay) case False{}: lex.kq.nl(nl, st, cc, stay)# a quoted segment may be emptydef lex.qkey.end(st: St, next: Lex) -> St: seg.push(st, next)# the closer, an escape, or a characterdef lex.kq.go(quote: Bool, bs: Bool, basic: Bool, nl: Bool, st: St, cc: Char, back: Lex, stay: Lex) -> St: match quote: case True{}: lex.qkey.end(st, back) case False{}: lex.kq.bs(bs, basic, nl, st, cc, stay)# a quoted key segment. back is the state after the closer; stay is this quotedef lex.kq(st: St, +cc: Char, back: Lex, stay: Lex) -> St: St{bad, rows, heads, here, parts, buf, lex, b, +qkind, stack, hold, uni, left, arrtab, lead} = st lex.kq.go(Char.is_eq(cc, q.char(qkind)), Char.is_eq(cc, '\\'), q.basic(qkind), ch.nl(cc), St{bad, rows, heads, here, parts, buf, lex, b, qkind, stack, hold, uni, left, arrtab, lead}, cc, back, stay)# the second ] of an array of tablesdef lex.head.r.go(end: Bool, st: St) -> St: match end: case True{}: head.fin(st) case False{}: st.err(st, "invalid header")# the second ] of an array of tablesdef lex.head.r(st: St, +cc: Char) -> St: lex.head.r.go(Char.is_eq(cc, ']'), st)# ] closes; anything else is a valuedef lex.arr.end(end: Bool, st: St, cc: Char) -> St: match end: case True{}: arr.close(st) case False{}: st.hold(st.lex(st, LVal{}), LVal{}, cc)# a comment, the end, or a valuedef lex.arr.hash(hash: Bool, end: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LArr{}) case False{}: lex.arr.end(end, st, cc)# a newline is whitespace in an arraydef lex.arr.nl(nl: Bool, hash: Bool, end: Bool, st: St, cc: Char) -> St: match nl: case True{}: st case False{}: lex.arr.hash(hash, end, st, cc)# whitespace, a comment, the end, or a valuedef lex.arr.go(sp: Bool, nl: Bool, hash: Bool, end: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.arr.nl(nl, hash, end, st, cc)# an array waits for a value or ]def lex.arr(st: St, +cc: Char) -> St: lex.arr.go(ch.sp(cc), ch.nl(cc), Char.is_eq(cc, '#'), Char.is_eq(cc, ']'), st, cc)# ] closes the arraydef lex.arrc.end(end: Bool, st: St) -> St: match end: case True{}: arr.close(st) case False{}: st.err(st, "invalid array")# a comma waits for another valuedef lex.arrc.comma(comma: Bool, end: Bool, st: St) -> St: match comma: case True{}: st.lex(st, LArr{}) case False{}: lex.arrc.end(end, st)# a comment, a comma, or the enddef lex.arrc.hash(hash: Bool, comma: Bool, end: Bool, st: St, _c: Char) -> St: match hash: case True{}: com.enter(st, LArrC{}) case False{}: lex.arrc.comma(comma, end, st)# a newline is whitespacedef lex.arrc.nl(nl: Bool, hash: Bool, comma: Bool, end: Bool, st: St, cc: Char) -> St: match nl: case True{}: st case False{}: lex.arrc.hash(hash, comma, end, st, cc)# whitespace, a comment, a comma, or the enddef lex.arrc.go(sp: Bool, nl: Bool, hash: Bool, comma: Bool, end: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.arrc.nl(nl, hash, comma, end, st, cc)# after an array value: a comma, ], or whitespacedef lex.arrc(st: St, +cc: Char) -> St: lex.arrc.go(ch.sp(cc), ch.nl(cc), Char.is_eq(cc, '#'), Char.is_eq(cc, ','), Char.is_eq(cc, ']'), st, cc)# a new key starts with an empty segment listdef lex.key.clear(st: St) -> St: St{bad, rows, heads, here, _parts, _buf, _lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st St{bad, rows, heads, here, [], [], LKey{}, back, qkind, stack, hold, uni, left, arrtab, lead}# a bare character opens the keydef lex.key.bare(ok: Bool, st: St, cc: Char) -> St: match ok: case True{}: st.push(st, cc, LKey{}) case False{}: st.err(st, "invalid key")# a literal quote or a bare character opens the keydef lex.key.lit(lit: Bool, st: St, +cc: Char) -> St: match lit: case True{}: lex.qkey(st, QLit{}, LKeyQ{}) case False{}: lex.key.bare(bare.at(cc), st, cc)# a quote or a bare character opens the keydef lex.key.q(qq: Bool, st: St, +cc: Char) -> St: match qq: case True{}: lex.qkey(st, QBasic{}, LKeyQ{}) case False{}: lex.key.lit(Char.is_eq(cc, '\''), st, cc)# the first character of a key inside an inline tabledef lex.key.start(st: St, +cc: Char) -> St: lex.key.q(Char.is_eq(cc, '"'), lex.key.clear(st), cc)# a newline is rejected; anything else is a keydef lex.inl.nl(nl: Bool, st: St, cc: Char) -> St: match nl: case True{}: st.err(st, "newline in inline table") case False{}: lex.key.start(st, cc)# } closes an empty inline tabledef lex.inl.end(end: Bool, nl: Bool, st: St, cc: Char) -> St: match end: case True{}: inl.close(st) case False{}: lex.inl.nl(nl, st, cc)# a comment, the end, or a keydef lex.inl.hash(hash: Bool, end: Bool, nl: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LInl{}) case False{}: lex.inl.end(end, nl, st, cc)# whitespace, a comment, the end, or a keydef lex.inl.go(sp: Bool, hash: Bool, end: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.inl.hash(hash, end, nl, st, cc)# an inline table waits for a key or }def lex.inl(st: St, +cc: Char) -> St: lex.inl.go(ch.sp(cc), Char.is_eq(cc, '#'), Char.is_eq(cc, '}'), ch.nl(cc), st, cc)# a comment or a keydef lex.inlk.hash(hash: Bool, nl: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LInlK{}) case False{}: lex.inl.nl(nl, st, cc)# whitespace or a keydef lex.inlk.go(sp: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.inlk.hash(hash, nl, st, cc)# after a comma, a key is requireddef lex.inlk(st: St, +cc: Char) -> St: lex.inlk.go(ch.sp(cc), Char.is_eq(cc, '#'), ch.nl(cc), st, cc)# a newline is rejecteddef lex.inlc.nl(nl: Bool, st: St) -> St: match nl: case True{}: st.err(st, "newline in inline table") case False{}: st.err(st, "invalid inline table")# } closes the inline tabledef lex.inlc.end(end: Bool, nl: Bool, st: St, _c: Char) -> St: match end: case True{}: inl.close(st) case False{}: lex.inlc.nl(nl, st)# a comma requires another keydef lex.inlc.comma(comma: Bool, end: Bool, nl: Bool, st: St, cc: Char) -> St: match comma: case True{}: st.lex(st, LInlK{}) case False{}: lex.inlc.end(end, nl, st, cc)# a comment, a comma, or the enddef lex.inlc.hash(hash: Bool, comma: Bool, end: Bool, nl: Bool, st: St, cc: Char) -> St: match hash: case True{}: com.enter(st, LInlC{}) case False{}: lex.inlc.comma(comma, end, nl, st, cc)# whitespace, a comment, a comma, or the enddef lex.inlc.go(sp: Bool, hash: Bool, comma: Bool, end: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.inlc.hash(hash, comma, end, nl, st, cc)# after an inline value: a comma or }def lex.inlc(st: St, +cc: Char) -> St: lex.inlc.go(ch.sp(cc), Char.is_eq(cc, '#'), Char.is_eq(cc, ','), Char.is_eq(cc, '}'), ch.nl(cc), st, cc)# the rest of a failed document is ignoreddef lex.sink(st: St, _c: Char) -> St: st# anything else on the same line is another expressiondef lex.line.nl(nl: Bool, st: St) -> St: match nl: case True{}: st.lex(st, LTop{}) case False{}: st.err(st, "newline required")# a comment may follow an expressiondef lex.line.hash(hash: Bool, nl: Bool, st: St, _c: Char) -> St: match hash: case True{}: com.enter(st, LLine{}) case False{}: lex.line.nl(nl, st)# whitespace, a comment, or a newline after an expressiondef lex.line.go(sp: Bool, hash: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.line.hash(hash, nl, st, cc)# after an expression, the rest of the line is whitespace or a commentdef lex.line(st: St, +cc: Char) -> St: lex.line.go(ch.sp(cc), Char.is_eq(cc, '#'), ch.nl(cc), st, cc)# a newline in a dotted key is rejecteddef lex.kdot.nl(nl: Bool, st: St) -> St: match nl: case True{}: st.err(st, "newline in key") case False{}: st.err(st, "invalid key")# a second dot is an empty segmentdef lex.kdot.dot(dot: Bool, nl: Bool, st: St) -> St: match dot: case True{}: st.err(st, "empty key") case False{}: lex.kdot.nl(nl, st)# a bare character starts the segmentdef lex.kdot.bare(bare: Bool, dot: Bool, nl: Bool, st: St, cc: Char) -> St: match bare: case True{}: st.push(st, cc, LKey{}) case False{}: lex.kdot.dot(dot, nl, st)# a literal quote starts the segmentdef lex.kdot.lit(lit: Bool, dot: Bool, nl: Bool, st: St, +cc: Char) -> St: match lit: case True{}: lex.qkey(st, QLit{}, LKeyQ{}) case False{}: lex.kdot.bare(bare.at(cc), dot, nl, st, cc)# a quote starts the segmentdef lex.kdot.q(qq: Bool, lit: Bool, dot: Bool, nl: Bool, st: St, cc: Char) -> St: match qq: case True{}: lex.qkey(st, QBasic{}, LKeyQ{}) case False{}: lex.kdot.lit(lit, dot, nl, st, cc)# whitespace or the next segmentdef lex.kdot.go(sp: Bool, qq: Bool, lit: Bool, dot: Bool, nl: Bool, st: St, cc: Char) -> St: match sp: case True{}: st case False{}: lex.kdot.q(qq, lit, dot, nl, st, cc)# after a dot, whitespace and then a segmentdef lex.kdot(st: St, +cc: Char) -> St: lex.kdot.go(ch.sp(cc), Char.is_eq(cc, '"'), Char.is_eq(cc, '\''), Char.is_eq(cc, '.'), ch.nl(cc), st, cc)# a quoted header segmentdef lex.hq(st: St, +cc: Char) -> St: lex.kq(st, cc, LHeadGap{}, LHeadQ{})# one character, dispatched on the lex statedef read.on(lex: Lex, st: St, cc: Char) -> St: match lex: case LTop{}: lex.top(st, cc) case LLine{}: lex.line(st, cc) case LKey{}: lex.key(st, cc) case LKeyDot{}: lex.kdot(st, cc) case LAfter{}: lex.after(st, cc) case LKeyQ{}: lex.kq(st, cc, LAfter{}, LKeyQ{}) case LKeyE{}: esc.use(esc.of(cc), st, LKeyQ{}) case LKeyU{}: uni.step(st, cc, LKeyQ{}) case LVal{}: lex.val(st, cc) case LQ{}: lex.q(st, cc) case LQ2{}: lex.q2(st, cc) case LStr{}: lex.str(st, cc) case LEsc{}: lex.esc(st, cc) case LUni{}: uni.step(st, cc, LStr{}) case LFold{}: lex.fold(st, cc) case LMq{}: lex.mq.step(st, cc) case LMqEnd{}: lex.mq.end(st, cc) case LTrim{}: lex.trim(st, cc) case LTrim2{}: lex.trim2(st, cc) case LBare{}: lex.bare(st, cc) case LDate{}: lex.date(st, cc) case LCom{}: lex.com(st, cc) case LHead{}: lex.head(st, cc) case LHeadGap{}: lex.head.gap.step(st, cc) case LHeadDot{}: lex.head.dot.step(st, cc) case LHeadQ{}: lex.hq(st, cc) case LHeadE{}: esc.use(esc.of(cc), st, LHeadQ{}) case LHeadU{}: uni.step(st, cc, LHeadQ{}) case LHeadR{}: lex.head.r(st, cc) case LArr{}: lex.arr(st, cc) case LArrC{}: lex.arrc(st, cc) case LInl{}: lex.inl(st, cc) case LInlK{}: lex.inlk(st, cc) case LInlC{}: lex.inlc(st, cc) case LSink{}: lex.sink(st, cc)# read the lex state and stepdef read.on.st(st: St, cc: Char) -> St: St{bad, rows, heads, here, parts, buf, +lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st read.on(lex, St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead}, cc)# one character, after a leading mark has been decideddef read.step.bom(bom: Bool, st: St, cc: Char) -> St: match bom: case True{}: st case False{}: read.on.st(st, cc)# the first character may be a byte-order markdef read.step.lead(lead: Bool, st: St, +cc: Char) -> St: match lead: case False{}: read.on.st(st, cc) case True{}: read.step.bom(U32.is_eq(Char.to_u32(cc), 65279), st, cc)# one character of the documentdef read.step(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, +lead} = st read.step.lead(lead, St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, False{}}, cc)# a held character is read oncedef read.once.hold(hold: Hold, st: St) -> St: match hold: case HNone{}: st case HChar{c}: read.step(st, c)# a held character is read oncedef read.once2(st: St) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st read.once.hold(hold, St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, HNone{}, uni, left, arrtab, lead})# one new character, then a held character if the step asked for onedef read.once(st: St, cc: Char) -> St: read.once2(read.step(st, cc))# a bare token, an empty string or a closed multiline string still open at the enddef read.fin.lex(lex: Lex, st: St) -> St: match lex: case LBare{}: bare.finish(st, ' ', False{}) case LDate{}: bare.finish(st, ' ', False{}) case LQ2{}: val.take(st, VStr{""}) case LMqEnd{}: str.fin(st) case _: st# a bare token still open at the end is closeddef read.fin.close(st: St) -> St: St{bad, rows, heads, here, parts, buf, +lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st read.fin.lex(lex, St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead})# builder order is newest-first. a kept list is reversed back; a builder list is notdef rows.seal.fin(rev: Bool, acc: List<&2, Val>) -> List<&2, Val>: match rev: case True{}: acc case False{}: List.reverse(&2, Val, acc)# put every table back in insertion order. one pass, no append along the waydef rows.seal.go(vs: List<&2, Val>, rev: Bool, acc: List<&2, Val>) -> List<&2, Val>: match vs: case []: rows.seal.fin(rev, acc) case VPair{name, VHead{path, rows}} <> t: rows.seal.go(t, rev, VPair{name, VHead{path, rows.seal.go(rows, True{}, [])}} <> acc) case VPair{name, VAots{path, elems}} <> t: rows.seal.go(t, rev, VPair{name, VAots{path, rows.seal.go(elems, True{}, [])}} <> acc) case VPair{name, VInl{rows}} <> t: rows.seal.go(t, rev, VPair{name, VInl{rows.seal.go(rows, True{}, [])}} <> acc) case VPair{name, VArr{items}} <> t: rows.seal.go(t, rev, VPair{name, VArr{rows.seal.go(items, False{}, [])}} <> acc) case VAot{path, rows} <> t: rows.seal.go(t, rev, VAot{path, rows.seal.go(rows, True{}, [])} <> acc) case VAots{path, elems} <> t: rows.seal.go(t, rev, VAots{path, rows.seal.go(elems, True{}, [])} <> acc) case VInl{rows} <> t: rows.seal.go(t, rev, VInl{rows.seal.go(rows, True{}, [])} <> acc) case VArr{items} <> t: rows.seal.go(t, rev, VArr{rows.seal.go(items, False{}, [])} <> acc) case h <> t: rows.seal.go(t, rev, h <> acc)# insertion order of a table built newest-firstdef rows.seal(rows: List<&2, Val>) -> List<&2, Val>: rows.seal.go(rows, True{}, [])# an open frame at the end is an errordef read.fin.stack(stack: List<&2, Ctx>, bad: String, rows: List<&2, Val>) -> Doc: match stack: case []: Doc{bad, rows.seal(rows)} case _ <> _: Doc{err.first(bad, "unclosed"), rows.seal(rows)}# the document from a finished statedef read.fin.doc(st: St) -> Doc: St{bad, rows, _heads, _here, _parts, _buf, _lex, _back, _q, stack, _hold, _uni, _left, _arr, _lead} = st read.fin.stack(stack, bad, rows)# an unfinished document keeps the first errordef read.fin.unclosed(st: St) -> Doc: St{bad, rows, _heads, _here, _parts, _buf, _lex, _back, _q, _stack, _hold, _uni, _left, _arr, _lead} = st Doc{err.first(bad, "unclosed"), rows.seal(rows)}# a comment may end a document only where the line already coulddef read.fin.end(lex: Lex) -> Bool: match lex: case LTop{}: True{} case LLine{}: True{} case LSink{}: True{} case _: False{}# a comment at the end is unfinished when it interrupted a key or a valuedef read.fin.com(ok: Bool, st: St) -> Doc: match ok: case True{}: read.fin.doc(st) case False{}: read.fin.unclosed(st)# a comment at the end is unfinished when it interrupted a key or a valuedef read.fin.com.st(st: St) -> Doc: St{bad, rows, heads, here, parts, buf, lex, +back, qkind, stack, hold, uni, left, arrtab, lead} = st read.fin.com(read.fin.end(back), St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead})# any other state is unfinisheddef read.fin.bad(st: St) -> Doc: read.fin.unclosed(st)# the states that may end a documentdef read.fin.ok(lex: Lex, st: St) -> Doc: match lex: case LTop{}: read.fin.doc(st) case LLine{}: read.fin.doc(st) case LCom{}: read.fin.com.st(st) case LSink{}: read.fin.doc(st) case _: read.fin.bad(st)# the document once the last token has been closeddef read.fin2(st: St) -> Doc: St{bad, rows, heads, here, parts, buf, +lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st read.fin.ok(lex, St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead})# the document at the end of the textdef read.fin(st: St) -> Doc: read.fin2(read.fin.close(st))# one scanner state, with the hold cleared and the first character already takendef read.fast.st( bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, HNone{}, uni, left, arrtab, False{}}# the character joins the buffer and the state staysdef read.fast.cons( bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: read.fast.st(bad, rows, heads, here, parts, cc <> buf, lex, back, qkind, stack, uni, left, arrtab)# a quote, an escape, or a state this step does not special-casedef read.fast.old( bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: read.once2(read.on.st(read.fast.st(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab), cc))# a forbidden control takes the full step; a plain character is storeddef read.fast.push( ctrl: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match ctrl: case False{}: read.fast.cons(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab, cc) case True{}: read.fast.old(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab, cc)# a basic-string character that is not a quotedef read.fast.basic.bs( bs: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match bs: case True{}: read.fast.old(bad, rows, heads, here, parts, buf, LStr{}, back, QBasic{}, stack, uni, left, arrtab, cc) case False{}: read.fast.push(str.bad(Char.to_u32(cc), False{}), bad, rows, heads, here, parts, buf, LStr{}, back, QBasic{}, stack, uni, left, arrtab, cc)# a basic-string character: a quote or a backslash still takes the full stepdef read.fast.basic( quote: Bool, bs: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match quote: case True{}: read.fast.old(bad, rows, heads, here, parts, buf, LStr{}, back, QBasic{}, stack, uni, left, arrtab, cc) case False{}: read.fast.basic.bs(bs, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)# a literal-string character: a quote still takes the full stepdef read.fast.lit( quote: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match quote: case True{}: read.fast.old(bad, rows, heads, here, parts, buf, LStr{}, back, QLit{}, stack, uni, left, arrtab, cc) case False{}: read.fast.push(str.bad(Char.to_u32(cc), False{}), bad, rows, heads, here, parts, buf, LStr{}, back, QLit{}, stack, uni, left, arrtab, cc)# one character of a stringdef read.fast.str( qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match qkind: case QBasic{}: read.fast.basic(Char.is_eq(cc, '"'), Char.is_eq(cc, '\\'), bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc) case QLit{}: read.fast.lit(Char.is_eq(cc, '\''), bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc) case other: read.fast.old(bad, rows, heads, here, parts, buf, LStr{}, back, other, stack, uni, left, arrtab, cc)# one character of a bare keydef read.fast.key( ok: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match ok: case True{}: read.fast.cons(bad, rows, heads, here, parts, buf, LKey{}, back, qkind, stack, uni, left, arrtab, cc) case False{}: read.fast.old(bad, rows, heads, here, parts, buf, LKey{}, back, qkind, stack, uni, left, arrtab, cc)# one character of a bare valuedef read.fast.bare( delim: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match delim: case False{}: read.fast.cons(bad, rows, heads, here, parts, buf, LBare{}, back, qkind, stack, uni, left, arrtab, cc) case True{}: read.fast.old(bad, rows, heads, here, parts, buf, LBare{}, back, qkind, stack, uni, left, arrtab, cc)# one character of a comment. a control, a newline among them, takes the full stepdef read.fast.com( ctl: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match ctl: case False{}: read.fast.st(bad, rows, heads, here, parts, buf, LCom{}, back, qkind, stack, uni, left, arrtab) case True{}: read.fast.old(bad, rows, heads, here, parts, buf, LCom{}, back, qkind, stack, uni, left, arrtab, cc)# whitespace that this state ignores. keep is true when a newline is also ignoreddef read.fast.ws.keep( keep: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match keep: case True{}: read.fast.st(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab) case False{}: read.fast.old(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab, cc)# a newline this state ignores, or the full stepdef read.fast.ws.nl( nl: Bool, keep: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match nl: case True{}: read.fast.ws.keep(keep, bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab, cc) case False{}: read.fast.old(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab, cc)# space stays in this state; a newline stays only when keep is setdef read.fast.ws( sp: Bool, nl: Bool, keep: Bool, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, lex: Lex, back: Lex, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match sp: case True{}: read.fast.st(bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab) case False{}: read.fast.ws.nl(nl, keep, bad, rows, heads, here, parts, buf, lex, back, qkind, stack, uni, left, arrtab, cc)# the states whose plain characters do not change the scannerdef read.fast.lex( lex: Lex, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match lex: case LStr{}: read.fast.str(qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc) case LKey{}: read.fast.key(bare.at(cc), bad, rows, heads, here, parts, buf, back, qkind, stack, uni, left, arrtab, cc) case LBare{}: read.fast.bare(ch.delim(cc), bad, rows, heads, here, parts, buf, back, qkind, stack, uni, left, arrtab, cc) case LCom{}: read.fast.com(ch.ctl(cc), bad, rows, heads, here, parts, buf, back, qkind, stack, uni, left, arrtab, cc) case LTop{}: read.fast.ws(ch.sp(cc), ch.nl(cc), True{}, bad, rows, heads, here, parts, buf, LTop{}, back, qkind, stack, uni, left, arrtab, cc) case LArr{}: read.fast.ws(ch.sp(cc), ch.nl(cc), True{}, bad, rows, heads, here, parts, buf, LArr{}, back, qkind, stack, uni, left, arrtab, cc) case LArrC{}: read.fast.ws(ch.sp(cc), ch.nl(cc), True{}, bad, rows, heads, here, parts, buf, LArrC{}, back, qkind, stack, uni, left, arrtab, cc) case LLine{}: read.fast.ws(ch.sp(cc), ch.nl(cc), False{}, bad, rows, heads, here, parts, buf, LLine{}, back, qkind, stack, uni, left, arrtab, cc) case LVal{}: read.fast.ws(ch.sp(cc), ch.nl(cc), False{}, bad, rows, heads, here, parts, buf, LVal{}, back, qkind, stack, uni, left, arrtab, cc) case LAfter{}: read.fast.ws(ch.sp(cc), ch.nl(cc), False{}, bad, rows, heads, here, parts, buf, LAfter{}, back, qkind, stack, uni, left, arrtab, cc) case LInl{}: read.fast.ws(ch.sp(cc), ch.nl(cc), False{}, bad, rows, heads, here, parts, buf, LInl{}, back, qkind, stack, uni, left, arrtab, cc) case LInlK{}: read.fast.ws(ch.sp(cc), ch.nl(cc), False{}, bad, rows, heads, here, parts, buf, LInlK{}, back, qkind, stack, uni, left, arrtab, cc) case LInlC{}: read.fast.ws(ch.sp(cc), ch.nl(cc), False{}, bad, rows, heads, here, parts, buf, LInlC{}, back, qkind, stack, uni, left, arrtab, cc) case other: read.fast.old(bad, rows, heads, here, parts, buf, other, back, qkind, stack, uni, left, arrtab, cc)# the first character may be a byte-order mark; later characters take the fast stepdef read.fast.lead( lead: Bool, lex: Lex, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match lead: case True{}: read.once(St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, HNone{}, uni, left, arrtab, True{}}, cc) case False{}: read.fast.lex(lex, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)# a held character is still read by the full step, then this characterdef read.fast.hold( hold: Hold, lead: Bool, lex: Lex, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> St: match hold: case HChar{held}: read.once(read.once2(St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, HChar{held}, uni, left, arrtab, lead}), cc) case HNone{}: read.fast.lead(lead, lex, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)# one source characterdef read.fast(st: St, +cc: Char) -> St: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st read.fast.hold(hold, lead, lex, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)# where the next source character is read. MEat is the body of a plain string, and# MCr follows a carriage return, which only a line feed may followtype Mode is Data: MScan{st: St} MCr{st: St} MOpen{basic: Bool, q: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool} MEat{basic: Bool, q: Char, cut: String, n: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool}# an unclosed stringdef eat.fin( _n: U32, _u: Unit, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Doc: read.fin.unclosed(St{bad, rows, heads, here, parts, [], LStr{}, LStr{}, qkind, stack, HNone{}, uni, left, arrtab, False{}})# a plain string is the first n characters of the source suffixdef eat.close( cut: String, nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> St: val.take(St{bad, rows, heads, here, parts, [], LStr{}, LStr{}, qkind, stack, HNone{}, uni, left, arrtab, False{}}, VSpan{cut, nn})# an escape or a forbidden control copies the span so far and continuesdef eat.copy( +cc: Char, cut: String, +nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: MScan{lex.str(St{bad, rows, heads, here, parts, span.rev(cut, nn, U32.is_zero(nn), []), LStr{}, LStr{}, qkind, stack, HNone{}, uni, left, arrtab, False{}}, cc)}# a forbidden control leaves the span; a plain character extends itdef eat.ctrl( badc: Bool, basic: Bool, qq: Char, cc: Char, cut: String, nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match badc: case True{}: eat.copy(cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab) case False{}: MEat{basic, qq, cut, U32.add(nn, 1), bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab}# a backslash in a basic string leaves the spandef eat.bs( bs: Bool, basic: Bool, qq: Char, +cc: Char, cut: String, nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match bs: case True{}: eat.copy(cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab) case False{}: eat.ctrl(str.bad(Char.to_u32(cc), False{}), basic, qq, cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab)# a literal string keeps a backslash; a basic string does notdef eat.basic( basic: Bool, qq: Char, +cc: Char, cut: String, nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match basic: case False{}: eat.ctrl(str.bad(Char.to_u32(cc), False{}), False{}, qq, cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab) case True{}: eat.bs(Char.is_eq(cc, '\\'), True{}, qq, cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab)# a quote closes the spandef eat.feed.q( quote: Bool, basic: Bool, qq: Char, cc: Char, cut: String, nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match quote: case True{}: MScan{eat.close(cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab)} case False{}: eat.basic(basic, qq, cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab)# one character of a plain string. cut is the suffix where the text begandef eat.feed( basic: Bool, +qq: Char, cut: String, nn: U32, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, qkind: QKind, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char) -> Mode: eat.feed.q(Char.is_eq(cc, qq), basic, qq, cc, cut, nn, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab)# the opening quote's back state is not kept on the spandef doc.open.eat(_back: Lex, mode: Mode) -> Mode: mode# a suffix the span does not keepdef doc.drop(_tail: String, mode: Mode) -> Mode: mode# an empty buffer starts a span; a buffer already in use stays on the old pathdef doc.open.buf( basic: Bool, qq: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, cc: Char, tail: String) -> Mode: match buf: case []: doc.open.eat(back, MEat{basic, qq, SCon{cc, tail}, 1, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab}) case h <> t: doc.drop(tail, MScan{read.fast.lex(LQ{}, qkind, bad, rows, heads, here, parts, h <> t, back, stack, uni, left, arrtab, cc)})# a forbidden control leaves the spandef doc.open.ok( badc: Bool, basic: Bool, qq: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, cc: Char, tail: String) -> Mode: match badc: case True{}: doc.drop(tail, MScan{read.fast.lex(LQ{}, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)}) case False{}: doc.open.buf(basic, qq, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc, tail)# a backslash in a basic string leaves the spandef doc.open.bs( bs: Bool, qq: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char, tail: String) -> Mode: match bs: case True{}: doc.drop(tail, MScan{read.fast.lex(LQ{}, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)}) case False{}: doc.open.ok(str.bad(Char.to_u32(cc), False{}), True{}, qq, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc, tail)# a literal string keeps a backslash; a basic string does notdef doc.open.b( basic: Bool, qq: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char, tail: String) -> Mode: match basic: case False{}: doc.open.ok(str.bad(Char.to_u32(cc), False{}), False{}, qq, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc, tail) case True{}: doc.open.bs(Char.is_eq(cc, '\\'), qq, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc, tail)# a second quote is an empty string or the start of a multiline stringdef doc.open.q( same: Bool, basic: Bool, qq: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, cc: Char, tail: String) -> Mode: match same: case True{}: doc.drop(tail, MScan{read.fast.lex(LQ{}, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc)}) case False{}: doc.open.b(basic, qq, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc, tail)# the character after an opening quotedef doc.open( basic: Bool, +qq: Char, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool, +cc: Char, tail: String) -> Mode: doc.open.q(Char.is_eq(cc, qq), basic, qq, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, cc, tail)# a one-line string may be a span; a multiline string stays on the old pathdef doc.scan.kind( qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match qkind: case QBasic{}: MOpen{True{}, '"', QBasic{}, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab} case QLit{}: MOpen{False{}, '\'', QLit{}, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab} case other: MScan{St{bad, rows, heads, here, parts, buf, LQ{}, back, other, stack, HNone{}, uni, left, arrtab, False{}}}# the first character of the file is still the old stepdef doc.scan.lead( lead: Bool, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match lead: case True{}: MScan{St{bad, rows, heads, here, parts, buf, LQ{}, back, qkind, stack, HNone{}, uni, left, arrtab, True{}}} case False{}: doc.scan.kind(qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab)# a held character is not the start of a spandef doc.scan.hold( hold: Hold, lead: Bool, qkind: QKind, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, stack: List<&2, Ctx>, uni: U32, left: U32, arrtab: Bool) -> Mode: match hold: case HChar{held}: MScan{St{bad, rows, heads, here, parts, buf, LQ{}, back, qkind, stack, HChar{held}, uni, left, arrtab, lead}} case HNone{}: doc.scan.lead(lead, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab)# a quote opened a one-line string, or the state is rebuilt around that quotedef doc.wrap.lex( lex: Lex, bad: String, rows: List<&2, Val>, heads: List<&2, String>, here: List<&2, String>, parts: List<&2, String>, buf: List<&2, Char>, back: Lex, qkind: QKind, stack: List<&2, Ctx>, hold: Hold, uni: U32, left: U32, arrtab: Bool, lead: Bool) -> Mode: match lex: case LQ{}: doc.scan.hold(hold, lead, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab) case other: MScan{St{bad, rows, heads, here, parts, buf, other, back, qkind, stack, hold, uni, left, arrtab, lead}}# a quote may have opened a stringdef doc.wrap(st: St) -> Mode: St{bad, rows, heads, here, parts, buf, lex, back, qkind, stack, hold, uni, left, arrtab, lead} = st doc.wrap.lex(lex, bad, rows, heads, here, parts, buf, back, qkind, stack, hold, uni, left, arrtab, lead)# a carriage return must be followed by a line feeddef doc.scan.cr(cr: Bool, st: St, cc: Char) -> Mode: match cr: case True{}: MCr{read.fast(st, cc)} case False{}: MScan{read.fast(st, cc)}# an apostrophe may open a literal stringdef doc.scan.sq(sq: Bool, cr: Bool, st: St, cc: Char) -> Mode: match sq: case True{}: doc.wrap(read.fast(st, cc)) case False{}: doc.scan.cr(cr, st, cc)# a quote may open a basic string. any other character is the old stepdef doc.scan.q(dq: Bool, sq: Bool, cr: Bool, st: St, cc: Char) -> Mode: match dq: case True{}: doc.wrap(read.fast(st, cc)) case False{}: doc.scan.sq(sq, cr, st, cc)# one source character outside a plain stringdef doc.scan(st: St, +cc: Char) -> Mode: doc.scan.q(Char.is_eq(cc, '"'), Char.is_eq(cc, '\''), U32.is_eq(Char.to_u32(cc), 13), st, cc)# the character after a carriage return: a line feed ends the line, anything else failsdef doc.cr(lf: Bool, st: St, cc: Char) -> Mode: match lf: case True{}: MScan{read.fast(st, cc)} case False{}: MScan{st.err(st, "carriage return without line feed")}# one character at a time. a plain string keeps a span of this suffixdef doc.read(text: String, mode: Mode) -> Doc: match text mode: case SNil{} MScan{st}: read.fin(read.once2(st)) case SNil{} MCr{st}: read.fin(read.once2(st.err(st, "carriage return without line feed"))) case SNil{} MOpen{_basic, _q, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab}: read.fin(read.once2(St{bad, rows, heads, here, parts, buf, LQ{}, back, qkind, stack, HNone{}, uni, left, arrtab, False{}})) case SNil{} MEat{_basic, _q, cut, n, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab}: eat.fin(n, span.drop(cut), bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab) case SCon{+h, +t} MOpen{basic, q, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab}: doc.read(t, doc.open(basic, q, qkind, bad, rows, heads, here, parts, buf, back, stack, uni, left, arrtab, h, t)) case SCon{+h, t} MEat{basic, q, cut, n, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab}: doc.read(t, eat.feed(basic, q, cut, n, bad, rows, heads, here, parts, qkind, stack, uni, left, arrtab, h)) case SCon{+h, t} MScan{st}: doc.read(t, doc.scan(st, h)) case SCon{+h, t} MCr{st}: doc.read(t, doc.cr(Char.is_eq(h, '\n'), st, h))# a scanner at the start of a documentdef st.start() -> St: St{"", [], [], [], [], [], LTop{}, LTop{}, QBasic{}, [], HNone{}, 0, 0, False{}, True{}}# a span walked for quoting. SqOk is the body reversed, with the closing quote already ontype Sq is Data: SqOk{rev: List<&2, Char>} SqNo{}# the reversed body while a span is still plain. SqStop means an escape was seentype SqAcc is Data: SqRun{rev: List<&2, Char>} SqStop{}# quote, backslash, or a control. those still take the escaping writerdef span.esc(+uu: U32) -> Bool: Bool.or(U32.is_lt(uu, 32), Bool.or(U32.is_eq(uu, 34), Bool.or(U32.is_eq(uu, 92), U32.is_eq(uu, 127))))# cons one plain character. an escape, or a stop already seen, stays stoppeddef span.quote.push(esc: Bool, acc: SqAcc, +hh: Char) -> SqAcc: match esc acc: case True{} _a: SqStop{} case False{} SqStop{}: SqStop{} case False{} SqRun{rev}: SqRun{hh <> rev}# the reversed body, or a stop, as the quoting resultdef span.quote.end(acc: SqAcc) -> Sq: match acc: case SqStop{}: SqNo{} case SqRun{rev}: SqOk{rev}# the first n characters, reversed onto acc. a character that needs an escape stopsdef span.quote.go(src: String, +nn: U32, zz: Bool, acc: SqAcc) -> Sq: match src zz: case _s True{}: span.quote.end(acc) case SNil{} False{}: span.quote.end(acc) case SCon{+h, t} False{}: span.quote.go(t, U32.sub(nn, 1), U32.is_zero(U32.sub(nn, 1)), span.quote.push(span.esc(Char.to_u32(h)), acc, h))# quotes around a plain span, or the escaping writer when a character needs itdef span.quote.fin(hit: Sq, src: String, +nn: U32) -> String: match hit: case SqNo{}: key.basic(span.str(src, nn, U32.is_zero(nn))) case SqOk{rev}: String.from_list(List.reverse(&2, Char, '"' <> rev))# a plain string span, quoted without rebuilding an escaped copydef span.quote(+src: String, +nn: U32) -> String: span.quote.fin(span.quote.go(src, nn, U32.is_zero(nn), SqRun{'"' <> []}), src, nn)# true or falsedef render.bool(bit: Bool) -> String: match bit: case True{}: "true" case False{}: "false"# a scalar valuedef render.atom(vv: Val) -> String: match vv: case VStr{text}: key.basic(text) case VSpan{src, +n}: span.quote(src, n) case VInt{sign, digits}: sign.text(sign) ++ digits case VFlo{sign, text}: sign.text(sign) ++ text case VBool{bit}: render.bool(bit) case VWhen{w}: when.text(w) case _: ""# a separator that is omitted after the last itemdef render.suf.last(last: Bool, gap: String) -> String: match last: case True{}: "" case False{}: gap# a separator. trail keeps the separator after the last itemdef render.suf(+gap: String, trail: Bool, last: Bool) -> String: match trail: case True{}: gap case False{}: render.suf.last(last, gap)# the values of a table, an array, or an inline tabledef render.go(vs: List<&2, Val>, +gap: String, +trail: Bool) -> String: match vs: case []: "" case VPair{_name, VHead{path, rows}} <> +t: "[" ++ path ++ "]\n" ++ render.go(rows, "\n", True{}) ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VPair{_name, VAots{_path, elems}} <> +t: render.go(elems, "\n", True{}) ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VAot{path, rows} <> +t: "[[" ++ path ++ "]]\n" ++ render.go(rows, "\n", True{}) ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VPair{name, VArr{items}} <> +t: key(name) ++ " = [" ++ render.go(items, ", ", False{}) ++ "]" ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VPair{name, VInl{rows}} <> +t: key(name) ++ " = {" ++ render.go(rows, ", ", False{}) ++ "}" ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VPair{name, val} <> +t: key(name) ++ " = " ++ render.atom(val) ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VArr{items} <> +t: "[" ++ render.go(items, ", ", False{}) ++ "]" ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case VInl{rows} <> +t: "{" ++ render.go(rows, ", ", False{}) ++ "}" ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail) case v <> +t: render.atom(v) ++ render.suf(gap, trail, List.is_empty(&2, Val, t)) ++ render.go(t, gap, trail)# a nested table rendering that this pass does not printdef render.skip( _path: String, _rows: List<&2, Val>, _tail: List<&2, Val>, _gap: String, _trail: Bool, nest: Unit -> String, rest: Unit -> String) -> String: _n = nest rest(Unit{})# the rows are already closed over by the nested renderingdef render.use(_rows: List<&2, Val>, text: String) -> String: text# a header already ends in a newlinedef render.rest(_tail: List<&2, Val>, _gap: String, _trail: Bool, rest: Unit -> String) -> String: rest(Unit{})# a table header. Direct keys are printed by the other passdef render.head( tabs: Bool, path: String, rows: List<&2, Val>, tail: List<&2, Val>, gap: String, trail: Bool, nest: Unit -> String, rest: Unit -> String) -> String: match tabs: case False{}: render.skip(path, rows, tail, gap, trail, nest, rest) case True{}: "[" ++ path ++ "]\n" ++ render.use(rows, nest(Unit{})) ++ render.rest(tail, gap, trail, rest)# an array of tables. Its elements are headersdef render.aots( tabs: Bool, elems: List<&2, Val>, tail: List<&2, Val>, gap: String, trail: Bool, nest: Unit -> String, rest: Unit -> String) -> String: match tabs: case False{}: render.skip("", elems, tail, gap, trail, nest, rest) case True{}: render.use(elems, nest(Unit{})) ++ render.rest(tail, gap, trail, rest)# one element of an array of tablesdef render.aot( tabs: Bool, path: String, rows: List<&2, Val>, tail: List<&2, Val>, gap: String, trail: Bool, nest: Unit -> String, rest: Unit -> String) -> String: match tabs: case False{}: render.skip(path, rows, tail, gap, trail, nest, rest) case True{}: "[[" ++ path ++ "]]\n" ++ render.use(rows, nest(Unit{})) ++ render.rest(tail, gap, trail, rest)# a key, an array, or an inline table. The text is built only by the pass that writes keysdef render.late( tabs: Bool, text: Unit -> String, tail: List<&2, Val>, gap: String, trail: Bool, rest: Unit -> String) -> String: match tabs: case True{}: rest(Unit{}) case False{}: text(Unit{}) ++ render.suf(gap, trail, List.is_empty(&2, Val, tail)) ++ rest(Unit{})# one pass: keys, or the tables that follow themdef render.pass(vs: List<&2, Val>, +gap: String, +trail: Bool, +tabs: Bool) -> String: match vs: case []: "" case VPair{_name, VHead{path, +rows}} <> +t: render.head(tabs, path, rows, t, gap, trail, _u => render.pass(rows, "\n", True{}, False{}) ++ render.pass(rows, "\n", True{}, True{}), _u => render.pass(t, gap, trail, tabs)) case VPair{_name, VAots{_path, +elems}} <> +t: render.aots(tabs, elems, t, gap, trail, _u => render.pass(elems, "\n", True{}, False{}) ++ render.pass(elems, "\n", True{}, True{}), _u => render.pass(t, gap, trail, tabs)) case VAot{path, +rows} <> +t: render.aot(tabs, path, rows, t, gap, trail, _u => render.pass(rows, "\n", True{}, False{}) ++ render.pass(rows, "\n", True{}, True{}), _u => render.pass(t, gap, trail, tabs)) case VPair{name, VArr{items}} <> +t: render.late(tabs, _u => key(name) ++ " = [" ++ render.go(items, ", ", False{}) ++ "]", t, gap, trail, _v => render.pass(t, gap, trail, tabs)) case VPair{name, VInl{rows}} <> +t: render.late(tabs, _u => key(name) ++ " = {" ++ render.go(rows, ", ", False{}) ++ "}", t, gap, trail, _v => render.pass(t, gap, trail, tabs)) case VPair{name, val} <> +t: render.late(tabs, _u => key(name) ++ " = " ++ render.atom(val), t, gap, trail, _v => render.pass(t, gap, trail, tabs)) case VArr{items} <> +t: render.late(tabs, _u => "[" ++ render.go(items, ", ", False{}) ++ "]", t, gap, trail, _v => render.pass(t, gap, trail, tabs)) case VInl{rows} <> +t: render.late(tabs, _u => "{" ++ render.go(rows, ", ", False{}) ++ "}", t, gap, trail, _v => render.pass(t, gap, trail, tabs)) case v <> +t: render.late(tabs, _u => render.atom(v), t, gap, trail, _v => render.pass(t, gap, trail, tabs))# keys first, then tables and arrays of tablesdef render.both(+vs: List<&2, Val>, +gap: String, +trail: Bool) -> String: render.pass(vs, gap, trail, False{}) ++ render.pass(vs, gap, trail, True{})# the text of a string valuedef string(vv: Val) -> String: match vv: case VStr{text}: text case VSpan{src, +n}: span.str(src, n, U32.is_zero(n)) case _: ""# the digits of an integer valuedef digits(vv: Val) -> String: match vv: case VInt{_sign, text}: text case _: ""# the bit of a boolean valuedef flag(vv: Val) -> Bool: match vv: case VBool{bit}: bit case _: False{}# the rows of a table valuedef at.rows(vv: Val) -> List<&2, Val>: match vv: case VHead{_path, rows}: rows case VInl{rows}: rows case VAot{_path, rows}: rows case _: []# the rows of a hit, when it is a tabledef at.rows_of(hh: Hit) -> List<&2, Val>: match hh: case Found{val}: at.rows(val) case Miss{}: []# the rest of a path after the first keydef at.step(rest: List<&2, String>, hit: Hit) -> Hit: match rest: case []: hit case name <> t: at.step(t, get(at.rows_of(hit), name))# the value at a dotted pathdef at(rows: List<&2, Val>, path: List<&2, String>) -> Hit: match path: case []: Miss{} case name <> rest: at.step(rest, get(rows, name))# the root tabledef root(doc: Doc) -> List<&2, Val>: Doc{_bad, rows} = doc rows# the first error, or "" when the document is well formeddef bad(doc: Doc) -> String: Doc{text, _rows} = doc text# a string valuedef str(text: String) -> Val: VStr{text}# an integer valuedef integer(sign: Sign, text: String) -> Val: VInt{sign, text}# a float valuedef float(sign: Sign, text: String) -> Val: VFlo{sign, text}# a boolean valuedef boolean(bit: Bool) -> Val: VBool{bit}# an array valuedef array(items: List<&2, Val>) -> Val: VArr{items}# an inline tabledef inline(rows: List<&2, Val>) -> Val: VInl{rows}# a tabledef table(path: String, rows: List<&2, Val>) -> Val: VHead{path, rows}# a key and a valuedef pair(name: String, val: Val) -> Val: VPair{name, val}# a document written back as TOMLdef render(doc: Doc) -> String: Doc{_bad, rows} = doc render.both(rows, "\n", True{})# a document read from TOML textdef parse(text: String) -> Doc: doc.read(text, MScan{st.start()})# Well-formed documents: the shapes parse returns. The builders make any shape,# and render writes TOML only for these.# where a list of values sits: a table's rows under its path (the root's is# ""), an inline table's rows, an array's items, or the elements of an array of# tables under its pathtype WfAt is Data: WfRows{path: String} WfInl{} WfItems{} WfElems{path: String}# how far a float's spelling has been read: toml.abnf's float with no sign# and no underscore, `dec-int ( exp / frac [ exp ] )`type Fl is Data: FlStart{} FlZero{} FlInt{} FlDot{} FlFrac{} FlE{} FlESign{} FlExp{} FlNo{}# the rest when this part holds, and False otherwisedef wf.then(ok: Bool, more: Unit -> Bool) -> Bool: match ok: case True{}: more(Unit{}) case False{}: False{}# a Unicode scalar value: no surrogate, and nothing past U+10FFFFdef wf.char(+cc: Char) -> Bool: uni.ok(Char.to_u32(cc))# every character of a string or a key is a Unicode scalar valuedef wf.text(ss: String) -> Bool: match ss: case SNil{}: True{} case SCon{+h, t}: wf.then(wf.char(h), _u => wf.text(t))# a span's source holds its n characters, each a Unicode scalar valuedef wf.span(src: String, +nn: U32, zz: Bool) -> Bool: match src zz: case _s True{}: True{} case SNil{} False{}: False{} case SCon{+h, t} False{}: wf.then(wf.char(h), _u => wf.span(t, U32.sub(nn, 1), U32.is_zero(U32.sub(nn, 1))))# every character is an ASCII digitdef wf.digits(ss: String) -> Bool: match ss: case SNil{}: True{} case SCon{+h, t}: wf.then(Char.is_digit(h), _u => wf.digits(t))# a lone 0, or a first digit that is not 0 and more digitsdef wf.dec.lead(zero: Bool, +cc: Char, rest: String) -> Bool: match zero: case True{}: String.is_empty(rest) case False{}: Bool.and(Char.is_digit(cc), wf.digits(rest))# decimal digits with no leading zero, except 0 itselfdef wf.dec(ss: String) -> Bool: match ss: case SNil{}: False{} case SCon{+h, t}: wf.dec.lead(Char.is_eq(h, '0'), h, t)# an integer: decimal digits within signed 64 bitsdef wf.int(+sign: Sign, +digits: String) -> Bool: Bool.and(wf.dec(digits), i64.fits(sign, digits))# e or Edef wf.flo.is_e(+cc: Char) -> Bool: Bool.or(Char.is_eq(cc, 'e'), Char.is_eq(cc, 'E'))# after the integer part: a fraction or an exponentdef wf.flo.tail(+cc: Char) -> Fl: Bool.pick(Fl, Char.is_eq(cc, '.'), FlDot{}, Bool.pick(Fl, wf.flo.is_e(cc), FlE{}, FlNo{}))# one character of a float's spellingdef wf.flo.step(st: Fl, +cc: Char) -> Fl: match st: case FlStart{}: Bool.pick(Fl, Char.is_eq(cc, '0'), FlZero{}, Bool.pick(Fl, Char.is_digit(cc), FlInt{}, FlNo{})) case FlZero{}: wf.flo.tail(cc) case FlInt{}: Bool.pick(Fl, Char.is_digit(cc), FlInt{}, wf.flo.tail(cc)) case FlDot{}: Bool.pick(Fl, Char.is_digit(cc), FlFrac{}, FlNo{}) case FlFrac{}: Bool.pick(Fl, Char.is_digit(cc), FlFrac{}, Bool.pick(Fl, wf.flo.is_e(cc), FlE{}, FlNo{})) case FlE{}: Bool.pick(Fl, Bool.or(Char.is_eq(cc, '+'), Char.is_eq(cc, '-')), FlESign{}, Bool.pick(Fl, Char.is_digit(cc), FlExp{}, FlNo{})) case FlESign{}: Bool.pick(Fl, Char.is_digit(cc), FlExp{}, FlNo{}) case FlExp{}: Bool.pick(Fl, Char.is_digit(cc), FlExp{}, FlNo{}) case FlNo{}: FlNo{}# a float's spelling read from the given statedef wf.flo.go(cs: List<&2, Char>, st: Fl) -> Fl: match cs: case []: st case +h <> t: wf.flo.go(t, wf.flo.step(st, h))# a spelling ends after a fraction's digit or an exponent's digitdef wf.flo.end(st: Fl) -> Bool: match st: case FlFrac{}: True{} case FlExp{}: True{} case _: False{}# a float's spelling: inf, nan, or toml.abnf's float with no sign and no `_`def wf.flo(+text: String) -> Bool: Bool.or(str.eq(text, "inf"), Bool.or(str.eq(text, "nan"), wf.flo.end(wf.flo.go(String.to_list(text), FlStart{}))))# a date: a four-digit year, a month, and a day within that month in that yeardef wf.date(dd: Date) -> Bool: Date{+y, +mo, +day} = dd Bool.and(U32.is_le(y, 9999), Bool.and(bound.rng(mo, 1, 12), bound.rng(day, 1, when.days(y, mo))))# a time of day: hour, minute and second in range, and a fraction of digitsdef wf.clock(cc: Clock) -> Bool: Clock{+h, +mi, +s, frac} = cc Bool.and(U32.is_le(h, 23), Bool.and(U32.is_le(mi, 59), Bool.and(U32.is_le(s, 60), wf.digits(frac))))# a zone offset in rangedef wf.zone(zz: Zone) -> Bool: match zz: case Zulu{}: True{} case Shift{_sign, +h, +mi}: Bool.and(U32.is_le(h, 23), U32.is_le(mi, 59))# a datetime whose fields are in rangedef wf.when(ww: When) -> Bool: match ww: case WOff{date, clock, zone}: Bool.and(wf.date(date), Bool.and(wf.clock(clock), wf.zone(zone))) case WLocal{date, clock}: Bool.and(wf.date(date), wf.clock(clock)) case WDate{date}: wf.date(date) case WTime{clock}: wf.clock(clock)# a string, a number, a boolean or a datetime, well-formed. Nothing else is a# scalardef wf.atom(vv: Val) -> Bool: match vv: case VStr{text}: wf.text(text) case VSpan{src, +n}: wf.span(src, n, U32.is_zero(n)) case VInt{+sign, +digits}: wf.int(sign, digits) case VFlo{_sign, +text}: wf.flo(text) case VBool{_bit}: True{} case VWhen{w}: wf.when(w) case _: False{}# a name no later pair of the table hasdef wf.fresh(hit: Hit) -> Bool: match hit: case Found{_v}: False{} case Miss{}: True{}# a pair's key: Unicode scalar values, and no later pair of the same namedef wf.key(+name: String, +rest: List<&2, Val>) -> Bool: Bool.and(wf.text(name), wf.fresh(get(rest, name)))# a table as a pair's value, in a table's rows, at the path its name givesdef wf.head(at: WfAt, +name: String, +path: String, +rest: List<&2, Val>, more: Unit -> Bool) -> Bool: match at: case WfRows{+here}: wf.then(Bool.and(wf.key(name, rest), str.eq(path, tree.pre(here, name))), more) case _: False{}# an array of tables as a pair's value, in a table's rows, at the path its# name gives, with at least one elementdef wf.aots( at: WfAt, +name: String, +path: String, some: Bool, +rest: List<&2, Val>, more: Unit -> Bool) -> Bool: match at: case WfRows{+here}: wf.then(Bool.and(some, Bool.and(wf.key(name, rest), str.eq(path, tree.pre(here, name)))), more) case _: False{}# an element of an array of tables, at the array's pathdef wf.aot(at: WfAt, +path: String, more: Unit -> Bool) -> Bool: match at: case WfElems{here}: wf.then(str.eq(path, here), more) case _: False{}# a pair of any other value, in a table's or an inline table's rowsdef wf.pair(at: WfAt, +name: String, +rest: List<&2, Val>, ok: Bool, more: Unit -> Bool) -> Bool: match at: case WfRows{_here}: wf.then(Bool.and(ok, wf.key(name, rest)), more) case WfInl{}: wf.then(Bool.and(ok, wf.key(name, rest)), more) case _: False{}# a value that is not a pair, as an array's itemdef wf.item(at: WfAt, ok: Bool, more: Unit -> Bool) -> Bool: match at: case WfItems{}: wf.then(ok, more) case _: False{}# a list of values, well-formed where it sitsdef wf.go(vs: List<&2, Val>, +at: WfAt) -> Bool: match vs: case []: True{} case VPair{+name, VHead{+path, rows}} <> +t: wf.head(at, name, path, t, _u => wf.then(wf.go(rows, WfRows{path}), _v => wf.go(t, at))) case VPair{+name, VAots{+path, +elems}} <> +t: wf.aots(at, name, path, Bool.not(List.is_empty(&2, Val, elems)), t, _u => wf.then(wf.go(elems, WfElems{path}), _v => wf.go(t, at))) case VPair{+name, VArr{items}} <> +t: wf.pair(at, name, t, True{}, _u => wf.then(wf.go(items, WfItems{}), _v => wf.go(t, at))) case VPair{+name, VInl{rows}} <> +t: wf.pair(at, name, t, True{}, _u => wf.then(wf.go(rows, WfInl{}), _v => wf.go(t, at))) case VPair{+name, val} <> +t: wf.pair(at, name, t, wf.atom(val), _u => wf.go(t, at)) case VAot{+path, rows} <> t: wf.aot(at, path, _u => wf.then(wf.go(rows, WfRows{path}), _v => wf.go(t, at))) case VArr{items} <> t: wf.item(at, True{}, _u => wf.then(wf.go(items, WfItems{}), _v => wf.go(t, at))) case VInl{rows} <> t: wf.item(at, True{}, _u => wf.then(wf.go(rows, WfInl{}), _v => wf.go(t, at))) case val <> t: wf.item(at, wf.atom(val), _u => wf.go(t, at))# whether a document is well-formed: the shapes parse returns. Tables and# arrays of tables sit only as a pair's value in a table's rows, each at the# path its parent's path and its name give; a table's keys are distinct;# numbers, datetimes and strings are as parse reads them. The error is not# looked atdef wf(doc: Doc) -> Bool: Doc{_bad, rows} = doc wf.go(rows, WfRows{""})