From 34522fffadb90ab2b7d058046dc95ca46ca1881e Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 2 Jun 2025 13:43:45 +0200 Subject: [PATCH 01/50] parse: begin stdlib-compliant parse refactor Reimplements parsing logic in src/parse.ts to match the parsing logic for atom literals used in the hoon standard library. Includes regexes for all supported atom formats for validity checking. Relies on existing aura parsing logic for "complex" auras, even if that logic isn't stdlib-compliant yet. (Notably, `@da` and `@q` fail some of their tests due to this.) Includes some tests. More thorough tests forthcoming. --- src/parse.ts | 282 ++++++++++++++++++++++++++++++++++++++++++ test/parse.test.ts | 299 +++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 3 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 src/parse.ts create mode 100644 test/parse.test.ts diff --git a/src/parse.ts b/src/parse.ts new file mode 100644 index 0000000..f37201a --- /dev/null +++ b/src/parse.ts @@ -0,0 +1,282 @@ +// parse: parse atom literals +// +// atom literal parsing from hoon 137 (and earlier). +// stdlib arm names are included for ease of cross-referencing. +// + +//TODO unsupported auras: @r*, @if, @is +//TODO unsupported coins: %blob, %many + +import { parseDa } from "./da"; +import { isValidPatp, patp2bn } from "./p"; +import { isValidPatq, patq2bn } from "./q"; +import { parseUv } from "./uv"; +import { parseUw } from "./uw"; + +export type aura = 'c' + | 'da' + | 'dr' + | 'f' + | 'n' + | 'p' + | 'q' + | 'sb' + | 'sd' + | 'si' + | 'sv' + | 'sw' + | 'sx' + | 't' + | 'ta' + | 'tas' + | 'ub' + | 'ud' + | 'ui' + | 'uv' + | 'uw' + | 'ux'; +export type dime = { aura: aura, atom: bigint } +export type coin = ({ type: 'dime' } & dime) + | { type: 'blob', noun: any } //TODO could do jam: bigint if we don't want nockjs dependency? + | { type: 'many', list: coin[] } + +//TODO for deduplicating @u vs @s +// function integerRegex(a: string, b: string, c: string, d: boolean = false): RegExp { +// return new RegExp(`^${d ? '\\-\\-?' : ''}xx$`); +// } + +//TODO rewrite with eye towards capturing groups? +export const regex: { [key in aura]: RegExp } = { + 'c': /^~\-((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, + 'da': /^~(0|[1-9][0-9]*)\-?\.([1-9]|1[0-2])\.([1-9]|[1-3][0-9])(\.\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(\.[0-9a-f]{4})+)?)?$/, + 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))?(\.(\.[0-9a-f]{4})+)?$/, + 'f': /^\.(y|n)$/, + 'n': /^~$/, + 'p': /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/, //NOTE matches shape but not syllables + 'q': /^\.~(([a-z]{3}|[a-z]{6})(\-[a-z]{6})*)$/, //NOTE matches shape but not syllables + 'sb': /^\-\-?0b(0|1[01]{0,3}(\.[01]{4})*)$/, + 'sd': /^\-\-?(0|[1-9][0-9]{0,3}(\.[0-9]{4})*)$/, + 'si': /^\-\-?0i(0|[1-9][0-9]*)$/, + 'sv': /^\-\-?0v(0|[1-9a-v][0-9a-v]{0,4}(\.[0-9a-v]{5})*)$/, + 'sw': /^\-\-?0w(0|[1-9a-zA-Z~-][0-9a-zA-Z~-]{0,4}(\.[0-9a-zA-Z~-]{5})*)$/, + 'sx': /^\-\-?0x(0|[1-9a-f][0-9a-f]{0,3}(\.[0-9a-f]{4})*)$/, + 't': /^~~((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, + 'ta': /^~\.[0-9a-z\-\.~_]*$/, + 'tas': /^[a-z][a-z0-9\-]*$/, + 'ub': /^0b(0|1[01]{0,3}(\.[01]{4})*)$/, + 'ud': /^(0|[1-9][0-9]{0,2}(\.[0-9]{3})*)$/, + 'ui': /^0i(0|[1-9][0-9]*)$/, + 'uv': /^0v(0|[1-9a-v][0-9a-v]{0,4}(\.[0-9a-v]{5})*)$/, + 'uw': /^0w(0|[1-9a-zA-Z~-][0-9a-zA-Z~-]{0,4}(\.[0-9a-zA-Z~-]{5})*)$/, + 'ux': /^0x(0|[1-9a-f][0-9a-f]{0,3}(\.[0-9a-f]{4})*)$/, +}; + +// parse(): slaw() +// slaw(): parse string as specific aura, null if that fails +// +export const parse = slaw; +export default parse; +export function slaw(aura: aura, str: string): bigint | null { + // if the aura has a regex, test with that first + //TODO does double work with checks in nuck? + // + if (aura in regex && !regex[aura as aura].test(str)) { + return null; + } + // proceed into parsing the string into a coin, + // producing a result if the aura matches + // + //TODO further short-circuit based on aura? + const coin = nuck(str); + if (coin && coin.type === 'dime' && coin.aura === aura) { + return coin.atom; + } else { + return null; + } +} + +// slav(): slaw() but throwing on failure +// +export function slav(aura: aura, str: string): bigint { + const out = slaw(aura, str); + if (!out) { + throw new Error('slav: failed to parse @' + aura + ' from string: ' + str); + } + return out; +} + +// nuck(): parse string into coin, or null if that fails +// +export function nuck(str: string): coin | null { + if (str === '') return null; + + // narrow options down by the first character, before doing regex texts + // and trying to parse for real + // + const c = str[0]; + if (c >= 'a' && c <= 'z') { // "sym" + if (regex['tas'].test(str)) { + return { type: 'dime', aura: 'tas', atom: fromString(str) }; + } else { + return null; + } + } else + if (c >= '0' && c <= '9') { // "bisk" + const dim = bisk(str); + if (!dim) { + return null; + } else { + return { type: 'dime', ...dim }; + } + } else + if (c === '-') { // "tash" + let pos = true; + if (str[1] == '-') { + str = str.slice(2); + } else { + str = str.slice(1); + pos = false; + } + const dim = bisk(str); + if (dim) { + // `@s`?:(pos (mul 2 b) ?:(=(0 b) 0 +((mul 2 (dec b))))) + if (pos) { + dim.atom = 2n * dim.atom; + } else if (dim.atom !== 0n) { + dim.atom = 1n + (2n * (dim.atom - 1n)); + } + //NOTE assumes bisk always returns u* auras + return { type: 'dime', aura: dim.aura.replace('u', 's') as aura, atom: dim.atom }; + } else { + return null; + } + } else + if (c === '.') { // "perd" + if (str === '.y') { + return { type: 'dime', aura: 'f', atom: 0n }; + } else + if (str === '.n') { + return { type: 'dime', aura: 'f', atom: 1n }; + } else + //REVIEW entering the branch this way assumes regexes for sequentially-tested auras don't overlap... + // going down the list of options this way matches hoon parser behavior the closest, but is slow for the "miss" case. + // should probably run some perf tests + if (str[1] === '~' && regex['q'].test(str)) { + const q = str.slice(1); //NOTE q.ts insanity, need to strip leading . + if (!isValidPatq(q)) { + console.log('invalid @q', q); + return null; + } else { + return { type: 'dime', aura: 'q', atom: patq2bn(q) } + } + } + //TODO %is, %if, %r* // "zust" + //TODO nusk for %many support + return null; + } else + if (c === '~') { + if (str === '~') { + return { type: 'dime', aura: 'n', atom: 0n } + } else { // "twid" + if (regex['da'].test(str)) { + return { type: 'dime', aura: 'da', atom: parseDa(str) }; + } else + if (regex['dr'].test(str)) { + //TODO support @dr + console.log('aura-js: @dr unsupported (nuck)'); + return null; + } else + if (regex['p'].test(str)) { + if (!isValidPatp(str)) { + return null; + } else { + return { type: 'dime', aura: 'p', atom: patp2bn(str) }; + } + } else + //TODO test if these single-character checks affect performance or no, + // or if we want to move them further up, etc. + if (str[1] === '.' && regex['ta'].test(str)) { + return { type: 'dime', aura: 'ta', atom: fromString(str.slice(2)) }; + } else + if (str[1] === '~' && regex['t'].test(str)) { + //TODO known-wrong! extract encoded byte-sequences + return { type: 'dime', aura: 't', atom: fromString(str.slice(2)) }; + } else + if (str[1] === '-' && regex['c'].test(str)) { + //TODO known-wrong! extract encoded byte-sequences and call +taft + return { type: 'dime', aura: 'c', atom: fromString(str.slice(2)) }; + } + } + //TODO twid for %blob support + return null; + } + return null; +} + +// bisk(): parse string into dime of integer aura, or null if that fails +// +export function bisk(str: string): dime | null { + switch (str.slice(0, 2)) { + case '0b': // "bay" + if (regex['ub'].test(str)) { + return { aura: 'ub', atom: BigInt(str.replaceAll('.', '')) }; + } else { + return null; + } + + case '0c': // "fim" + //TODO support base58 + console.log('aura-js: @uc unsupported (bisk)'); + return null; + + case '0i': // "dim" + if (regex['ui'].test(str)) { + return { aura: 'ui', atom: BigInt(str.slice(2)) } + } else { + return null; + } + + case '0x': // "hex" + if (regex['ux'].test(str)) { + return { aura: 'ux', atom: BigInt(str.replaceAll('.', '')) }; + } else { + return null; + } + + case '0v': // "viz" + if (regex['uv'].test(str)) { + return { aura: 'uv', atom: parseUv(str) }; + } else { + return null; + } + + case '0w': // "wiz" + if (regex['uw'].test(str)) { + return { aura: 'uw', atom: parseUw(str) }; + } else { + return null; + } + + default: // "dem" + if (regex['ud'].test(str)) { + return { aura: 'ud', atom: BigInt(str.replaceAll('.', '')) } + } else { + return null; + } + } +} + +//TODO this is basically nockjs' Atom.fromCord +function fromString(str: string): bigint { + if (str.length === 0) return 0n; + let i, + j, + octs = Array(str.length); + for (i = 0, j = octs.length - 1; i < octs.length; ++i, --j) { + const charByte = (str.charCodeAt(i) & 0xff).toString(16); + octs[j] = charByte.length === 1 ? "0" + charByte : charByte; + } + return BigInt('0x' + octs.join('')); + // if (str.length > 4) return BigInt('0x' + octs.join('')); + // else return BigInt(parseInt(octs.join(""), 16)); +} diff --git a/test/parse.test.ts b/test/parse.test.ts new file mode 100644 index 0000000..f02b15b --- /dev/null +++ b/test/parse.test.ts @@ -0,0 +1,299 @@ +import { parse, aura } from '../src/parse'; + +// most test cases generated from snippets similar to the following: +// +// =| n=@ud +// ^- (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q]) +// %- zing +// |- ^- (list (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q])) +// ?: (gth n 5) ~ +// :_ $(n +(n), eny (shaz +(eny))) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// [. . . . . . .]~:(end 2^(bex n) eny) +// + +//TODO test for parse failures: leading zeroes, date out of range, etc +//TODO test for non-standard-but-accepted cases: leading 0 in hex chars, weird dates, etc. + +function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { + describe(`${desc} auras`, () => { + auras.map((a) => { + describe(`@${a} parsing`, () => { + tests.map((test) => { + // @ts-ignore we know this is sane/safe + describe(test[a], () => { + it('parses', () => { + // @ts-ignore we know this is sane/safe + const res = parse(a, test[a]); + expect(res).toEqual(test.n); + }) + }); + }); + }); + }); + }); +} + +const INTEGER_AURAS: aura[] = [ 'ub', 'ud', 'ui', 'uv', 'uw', 'ux' ]; +const INTEGER_TESTS: { + n: bigint, + ub: string, + ud: string, + ui: string, + uv: string, + uw: string, + ux: string, +}[] = [ + { 'n': 0n, + 'ub': '0b0', + 'ud': '0', + 'ui': '0i0', + 'uv': '0v0', + 'uw': '0w0', + 'ux': '0x0', + }, + { 'n': 7n, + 'ub': '0b111', + 'ud': '7', + 'ui': '0i7', + 'uv': '0v7', + 'uw': '0w7', + 'ux': '0x7', + }, + { 'n': 5n, + 'ub': '0b101', + 'ud': '5', + 'ui': '0i5', + 'uv': '0v5', + 'uw': '0w5', + 'ux': '0x5', + }, + { 'n': 4n, + 'ub': '0b100', + 'ud': '4', + 'ui': '0i4', + 'uv': '0v4', + 'uw': '0w4', + 'ux': '0x4', + }, + { 'n': 171n, + 'ub': '0b1010.1011', + 'ud': '171', + 'ui': '0i171', + 'uv': '0v5b', + 'uw': '0w2H', + 'ux': '0xab', + }, + { 'n': 53n, + 'ub': '0b11.0101', + 'ud': '53', + 'ui': '0i53', + 'uv': '0v1l', + 'uw': '0wR', + 'ux': '0x35', + }, + { 'n': 77n, + 'ub': '0b100.1101', + 'ud': '77', + 'ui': '0i77', + 'uv': '0v2d', + 'uw': '0w1d', + 'ux': '0x4d', + }, + { 'n': 64491n, + 'ub': '0b1111.1011.1110.1011', + 'ud': '64.491', + 'ui': '0i64491', + 'uv': '0v1uvb', + 'uw': '0wfLH', + 'ux': '0xfbeb', + }, + { 'n': 51765n, + 'ub': '0b1100.1010.0011.0101', + 'ud': '51.765', + 'ui': '0i51765', + 'uv': '0v1ihl', + 'uw': '0wcER', + 'ux': '0xca35', + }, + { 'n': 46444n, + 'ub': '0b1011.0101.0110.1100', + 'ud': '46.444', + 'ui': '0i46444', + 'uv': '0v1dbc', + 'uw': '0wblI', + 'ux': '0xb56c', + }, + { 'n': 384265565n, + 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', + 'ud': '384.265.565', + 'ui': '0i384265565', + 'uv': '0vb.eerat', + 'uw': '0wmVSRt', + 'ux': '0x16e7.6d5d', + }, + { 'n': 2456897374n, + 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', + 'ud': '2.456.897.374', + 'ui': '0i2456897374', + 'uv': '0v29.72hqu', + 'uw': '0w2.isktu', + 'ux': '0x9271.475e', + }, + { 'n': 38583115n, + 'ub': '0b10.0100.1100.1011.1011.0100.1011', + 'ud': '38.583.115', + 'ui': '0i38583115', + 'uv': '0v1.4peqb', + 'uw': '0w2jbJb', + 'ux': '0x24c.bb4b', + }, + { 'n': 13604104043154737885n, + 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', + 'ud': '13.604.104.043.154.737.885', + 'ui': '0i13604104043154737885', + 'uv': '0vbpi.rship.r32mt', + 'uw': '0wb.Pbv8O.PIoHt', + 'ux': '0xbccb.7c8c.b3b1.8add', + }, + { 'n': 18441444580797368868n, + 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', + 'ud': '18.441.444.580.797.368.868', + 'ui': '0i18441444580797368868', + 'uv': '0vfvr.9c4ej.nlih4', + 'uw': '0wf.~Jb2e.DuIEA', + 'ux': '0xffed.2c23.a77a.ca24', + }, + { 'n': 7643844662312245512n, + 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', + 'ud': '7.643.844.662.312.245.512', + 'ui': '0i7643844662312245512', + 'uv': '0v6k5.3472r.m6688', + 'uw': '0w6.Ekp3y.ToNA8', + 'ux': '0x6a14.6438.b763.1908', + }, + { 'n': 293389376720547819362821033486028091527n, + 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', + 'ud': '293.389.376.720.547.819.362.821.033.486.028.091.527', + 'ui': '0i293389376720547819362821033486028091527', + 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', + 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', + 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', + }, + { 'n': 11826418988767709295206418976840492314n, + 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', + 'ud': '11.826.418.988.767.709.295.206.418.976.840.492.314', + 'ui': '0i11826418988767709295206418976840492314', + 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', + 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', + 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', + }, + { 'n': 75341289328899252391918368331716799250n, + 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', + 'ud': '75.341.289.328.899.252.391.918.368.331.716.799.250', + 'ui': '0i75341289328899252391918368331716799250', + 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', + 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', + 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', + }, +]; +testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); + +// test cases generated in similar fashion as the integer aura tests + +const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; +const PHONETIC_TESTS: { + n: bigint, + p: string, + q: string +}[] = [ + { 'n': 7n, 'p': '~let', 'q': '.~let' }, + { 'n': 0n, 'p': '~zod', 'q': '.~zod' }, + { 'n': 8n, 'p': '~ful', 'q': '.~ful' }, + { 'n': 117n, 'p': '~deg', 'q': '.~deg' }, + { 'n': 83n, 'p': '~tev', 'q': '.~tev' }, + { 'n': 223n, 'p': '~lud', 'q': '.~lud' }, + { 'n': 39995n, 'p': '~hapwyc', 'q': '.~hapwyc' }, + { 'n': 50426n, 'p': '~mitrep', 'q': '.~mitrep' }, + { 'n': 11415n, 'p': '~torryx', 'q': '.~torryx' }, + { 'n': 1863930458n, 'p': '~mogteg-botfex', 'q': '.~ligput-motfus' }, + { 'n': 3284934632n, 'p': '~loplet-nosnyx', 'q': '.~fasryd-mirlyn' }, + { 'n': 3833668n, 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, + { 'n': 9260427482306755094n, + 'p': '~lasrum-pindyt-nimnym-fotmeg', + 'q': '.~lasrum-pindyt-tadtem-lodlup', + }, + { 'n': 6363574354411289343n, + 'p': '~nopnet-rostem-navteb-fodbep', + 'q': '.~nopnet-rostem-nimfel-monfes', + }, + { 'n': 17571387016818844998n, + 'p': '~namler-folwet-bictes-wormec', + 'q': '.~namler-folwet-samwet-sarrul', + }, + { 'n': 241760151623976361741451001031931477015n, + 'p': '~dablys-minwed-mosreb-mictyn--nostyv-nimdul-hanbyl-bisdep', + 'q': '.~dablys-minwed-mosreb-mictyn-nostyv-nimdul-hanbyl-bisdep', + }, + { 'n': 148310954517291502180858368907816435627n, + 'p': '~ligryn-lomnem-fintes-davsyr--pacdel-wolpex-ripdev-paldeb', + 'q': '.~ligryn-lomnem-fintes-davsyr-pacdel-wolpex-ripdev-paldeb', + }, + { 'n': 97100713129464593177912155425728457718n, + 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', + 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', + }, +]; +testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); + +const DATE_AURAS: aura[] = [ 'da' ]; +const DATE_TESTS: { + n: bigint, + da: string +}[] = [ + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.1.1' + }, + { 'n': 170141182164706681340023325049697075200n, + 'da': '~2000-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~0.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170213050367437966468743593413155225600n, + 'da': '~123456789.12.12' + }, + { 'n': 170141184492712641901540060096049971200n, + 'da': '~2000.2.31' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..7.7.7' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..007.007.007' + }, + { 'n': 170141184492622106013428863442102517760n, + 'da': '~2000.1.1..99.99.99..abcd' + }, + { 'n': 170141184492616163050404573632566132736n, + 'da': '~2000.1.1..11.11.11..0000' + }, + { 'n': 170141184492616163050404761352701739008n, + 'da': '~2000.1.1..11.11.11..0000.aabb' + }, + { 'n': 170141184492616163062707000439658774528n, + 'da': '~2000.1.1..11.11.11..aabb.0000' + }, + { 'n': 170141184492615487727406687186543706111n, + 'da': '~2000.1.1..1.1.1..aabb.ccdd.eeff.ffff' + } +]; +testAuras('phonetic', DATE_AURAS, DATE_TESTS); + + diff --git a/tsconfig.json b/tsconfig.json index 0b99bf5..8978696 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,6 @@ "compilerOptions": { "module": "esnext", "moduleResolution": "nodenext", - "target": "es2020" + "target": "es2021" } } From d50fb46a223407d9a40b95cb3b7e27f37f0fa75e Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 2 Jun 2025 13:50:45 +0200 Subject: [PATCH 02/50] tests: add `@n` and `@f` parsing tests --- test/parse.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/parse.test.ts b/test/parse.test.ts index f02b15b..c565d58 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -18,6 +18,23 @@ import { parse, aura } from '../src/parse'; //TODO test for parse failures: leading zeroes, date out of range, etc //TODO test for non-standard-but-accepted cases: leading 0 in hex chars, weird dates, etc. +describe('limited auras', () => { + describe(`@n parsing`, () => { + it('parses', () => { + const res = parse('n', '~'); + expect(res).toEqual(0n); + }); + }); + describe(`@f parsing`, () => { + it('parses', () => { + const yea = parse('f', '.y'); + expect(yea).toEqual(0n); + const nay = parse('f', '.n'); + expect(nay).toEqual(1n); + }); + }); +}); + function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { describe(`${desc} auras`, () => { auras.map((a) => { @@ -29,7 +46,7 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { // @ts-ignore we know this is sane/safe const res = parse(a, test[a]); expect(res).toEqual(test.n); - }) + }); }); }); }); From 4ee5073a200f885d7209e30c1695aa1dcb183f6d Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 2 Jun 2025 18:10:36 +0200 Subject: [PATCH 03/50] render: begin stdlib-compliant render refactor Reimplements rendering logic in src/render.ts to match the rendering logic for atoms used in the hoon standard library. Includes some tests. `@q` and `@da` in particular continue to fail. More thorough testing forthcoming. --- src/render.ts | 166 ++++++++++++++++++++++ test/render.test.ts | 334 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 src/render.ts create mode 100644 test/render.test.ts diff --git a/src/render.ts b/src/render.ts new file mode 100644 index 0000000..c3cf021 --- /dev/null +++ b/src/render.ts @@ -0,0 +1,166 @@ +// render: serialize into atom literal strings +// +// atom literal rendering from hoon 137 (and earlier). +// stdlib arm names are included for ease of cross-referencing. +// + +import { formatDa } from "./da"; +import { patp } from "./p"; +import { patq } from "./q"; +import { formatUw } from "./uw"; + +//TODO unsupported auras: @r*, @if, @is +//TODO unsupported coins: %blob, %many + +//TODO dedupe with parse +export type aura = 'c' + | 'da' + | 'dr' + | 'f' + | 'n' + | 'p' + | 'q' + | 'sb' + | 'sd' + | 'si' + | 'sv' + | 'sw' + | 'sx' + | 't' + | 'ta' + | 'tas' + | 'ub' + | 'ud' + | 'ui' + | 'uv' + | 'uw' + | 'ux'; +export type dime = { aura: aura, atom: bigint } +export type coin = ({ type: 'dime' } & dime) + | { type: 'blob', noun: any } //TODO could do jam: bigint if we don't want nockjs dependency? + | { type: 'many', list: coin[] } + +// render(): scot() +// scot(): render atom as specific aura +// +export const render = scot; +export default render; +export function scot(aura: aura, atom: bigint): string { + return rend({ type: 'dime', aura, atom }); +} + +export function rend(coin: coin): string { + switch (coin.type) { + case 'blob': + throw new Error('aura-js: %blob rendering unsupported'); //TODO + + case 'many': + return '.' + coin.list.reduce((acc: string, item: coin) => { + return acc + '_' + wack(rend(item)); + }, '') + '__'; + throw new Error('aura-js: %many rendering unsupported'); //TODO + + case 'dime': + switch(coin.aura[0]) { + case 'c': + throw new Error('aura-js: @c rendering unsupported'); //TODO + case 'd': + switch(coin.aura[1]) { + case 'a': + return formatDa(coin.atom); + case 'r': + throw new Error('aura-js: @dr rendering unsupported'); //TODO + default: + return zco(coin.atom); + } + case 'f': + switch(coin.atom) { + case 0n: return '.y'; + case 1n: return '.n'; + default: return zco(coin.atom); + } + case 'n': + return '~'; + case 'i': + switch(coin.aura[1]) { + case 'f': throw new Error('aura-js: @if rendering unsupported'); //TODO + case 's': throw new Error('aura-js: @is rendering unsupported'); //TODO + default: return zco(coin.atom); + } + case 'p': + return patp(coin.atom); + case 'q': + return '.' + patq(coin.atom); + case 'r': + switch(coin.aura[1]) { + case 'd': throw new Error('aura-js: @rd rendering unsupported'); //TODO + case 'h': throw new Error('aura-js: @rh rendering unsupported'); //TODO + case 'q': throw new Error('aura-js: @rq rendering unsupported'); //TODO + case 's': throw new Error('aura-js: @rs rendering unsupported'); //TODO + default: return zco(coin.atom); + } + case 'u': + switch(coin.aura[1]) { + case 'c': throw new Error('aura-js: @uc rendering unsupported'); //TODO; + case 'b': return '0b' + split(coin.atom.toString(2), 4); + case 'i': return '0i' + dco(1, coin.atom); + case 'x': return '0x' + split(coin.atom.toString(16), 4); + case 'v': return '0v' + split(coin.atom.toString(32), 5); + case 'w': return formatUw(coin.atom); //TODO fix 0n case + default: return split(coin.atom.toString(10), 3); + } + case 's': + const end = (coin.atom & 1n); + coin.atom = end + (coin.atom >> 1n); + coin.aura = coin.aura.replace('u', 's') as aura; + return ((end === 0n) ? '--' : '-') + rend(coin); + case 't': + if (coin.aura[1] === 'a') { + if (coin.aura[2] === 's') { + return 'coin.atom'; //TODO fromCord + } else { + return '~.' + 'coin.atom'; //TODO fromCord + } + } else { + return '~~' + 'coin.atom'; //TODO fromCord(wood(coin.atom)) + } + default: + return zco(coin.atom); + } + } +} + +function aco(atom: bigint): string { + return dco(1, atom); +} + +function dco(lent: number, atom: bigint): string { + return atom.toString(10).padStart(lent, '0'); +} + +function vco(lent: number, atom: bigint): string { + return atom.toString(32).padStart(lent, '0'); +} + +function xco(lent: number, atom: bigint): string { + return atom.toString(16).padStart(lent, '0'); +} + +function yco(atom: bigint): string { + return dco(2, atom); +} + +function zco(atom: bigint): string { + return '0x' + xco(1, atom); +} + +function wack(str: string) { + return str.replaceAll('~', '~~').replaceAll('_', '~-'); +} + +function split(str: string, group: number): string { + // insert '.' every $group characters counting from the end, + // while avoiding putting a leading dot at the start + return str.replace(new RegExp(`(?=(?:.{${group}})+$)(?!^)`, 'g'), '.'); +} + diff --git a/test/render.test.ts b/test/render.test.ts new file mode 100644 index 0000000..206efc1 --- /dev/null +++ b/test/render.test.ts @@ -0,0 +1,334 @@ +import { render, aura, coin, rend } from '../src/render'; + +// most test cases generated from snippets similar to the following: +// +// =| n=@ud +// ^- (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q]) +// %- zing +// |- ^- (list (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q])) +// ?: (gth n 5) ~ +// :_ $(n +(n), eny (shaz +(eny))) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// [. . . . . . .]~:(end 2^(bex n) eny) +// + +describe('limited auras', () => { + describe(`@n rendering`, () => { + it('renders', () => { + const gud = render('n', 0n); + expect(gud).toEqual('~'); + const bad = render('n', 1n); + expect(bad).toEqual('~'); + }); + }); + describe(`@f parsing`, () => { + it('renders', () => { + const yea = render('f', 0n); + expect(yea).toEqual('.y'); + const nay = render('f', 1n); + expect(nay).toEqual('.n'); + const bad = render('f', 2n); + expect(bad).toEqual('0x2'); + }); + }); +}); + +function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { + describe(`${desc} auras`, () => { + auras.map((a) => { + describe(`@${a} rendering`, () => { + tests.map((test) => { + // @ts-ignore we know this is sane/safe + describe(test[a], () => { + it('renders', () => { + const res = render(a, test.n); + // @ts-ignore we know this is sane/safe + expect(res).toEqual(test[a]); + }); + }); + }); + }); + }); + }); +} + +const INTEGER_AURAS: aura[] = [ 'ub', 'ud', 'ui', 'uv', 'uw', 'ux' ]; +const INTEGER_TESTS: { + n: bigint, + ub: string, + ud: string, + ui: string, + uv: string, + uw: string, + ux: string, +}[] = [ + { 'n': 0n, + 'ub': '0b0', + 'ud': '0', + 'ui': '0i0', + 'uv': '0v0', + 'uw': '0w0', + 'ux': '0x0', + }, + { 'n': 7n, + 'ub': '0b111', + 'ud': '7', + 'ui': '0i7', + 'uv': '0v7', + 'uw': '0w7', + 'ux': '0x7', + }, + { 'n': 5n, + 'ub': '0b101', + 'ud': '5', + 'ui': '0i5', + 'uv': '0v5', + 'uw': '0w5', + 'ux': '0x5', + }, + { 'n': 4n, + 'ub': '0b100', + 'ud': '4', + 'ui': '0i4', + 'uv': '0v4', + 'uw': '0w4', + 'ux': '0x4', + }, + { 'n': 171n, + 'ub': '0b1010.1011', + 'ud': '171', + 'ui': '0i171', + 'uv': '0v5b', + 'uw': '0w2H', + 'ux': '0xab', + }, + { 'n': 53n, + 'ub': '0b11.0101', + 'ud': '53', + 'ui': '0i53', + 'uv': '0v1l', + 'uw': '0wR', + 'ux': '0x35', + }, + { 'n': 77n, + 'ub': '0b100.1101', + 'ud': '77', + 'ui': '0i77', + 'uv': '0v2d', + 'uw': '0w1d', + 'ux': '0x4d', + }, + { 'n': 64491n, + 'ub': '0b1111.1011.1110.1011', + 'ud': '64.491', + 'ui': '0i64491', + 'uv': '0v1uvb', + 'uw': '0wfLH', + 'ux': '0xfbeb', + }, + { 'n': 51765n, + 'ub': '0b1100.1010.0011.0101', + 'ud': '51.765', + 'ui': '0i51765', + 'uv': '0v1ihl', + 'uw': '0wcER', + 'ux': '0xca35', + }, + { 'n': 46444n, + 'ub': '0b1011.0101.0110.1100', + 'ud': '46.444', + 'ui': '0i46444', + 'uv': '0v1dbc', + 'uw': '0wblI', + 'ux': '0xb56c', + }, + { 'n': 384265565n, + 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', + 'ud': '384.265.565', + 'ui': '0i384265565', + 'uv': '0vb.eerat', + 'uw': '0wmVSRt', + 'ux': '0x16e7.6d5d', + }, + { 'n': 2456897374n, + 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', + 'ud': '2.456.897.374', + 'ui': '0i2456897374', + 'uv': '0v29.72hqu', + 'uw': '0w2.isktu', + 'ux': '0x9271.475e', + }, + { 'n': 38583115n, + 'ub': '0b10.0100.1100.1011.1011.0100.1011', + 'ud': '38.583.115', + 'ui': '0i38583115', + 'uv': '0v1.4peqb', + 'uw': '0w2jbJb', + 'ux': '0x24c.bb4b', + }, + { 'n': 13604104043154737885n, + 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', + 'ud': '13.604.104.043.154.737.885', + 'ui': '0i13604104043154737885', + 'uv': '0vbpi.rship.r32mt', + 'uw': '0wb.Pbv8O.PIoHt', + 'ux': '0xbccb.7c8c.b3b1.8add', + }, + { 'n': 18441444580797368868n, + 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', + 'ud': '18.441.444.580.797.368.868', + 'ui': '0i18441444580797368868', + 'uv': '0vfvr.9c4ej.nlih4', + 'uw': '0wf.~Jb2e.DuIEA', + 'ux': '0xffed.2c23.a77a.ca24', + }, + { 'n': 7643844662312245512n, + 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', + 'ud': '7.643.844.662.312.245.512', + 'ui': '0i7643844662312245512', + 'uv': '0v6k5.3472r.m6688', + 'uw': '0w6.Ekp3y.ToNA8', + 'ux': '0x6a14.6438.b763.1908', + }, + { 'n': 293389376720547819362821033486028091527n, + 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', + 'ud': '293.389.376.720.547.819.362.821.033.486.028.091.527', + 'ui': '0i293389376720547819362821033486028091527', + 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', + 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', + 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', + }, + { 'n': 11826418988767709295206418976840492314n, + 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', + 'ud': '11.826.418.988.767.709.295.206.418.976.840.492.314', + 'ui': '0i11826418988767709295206418976840492314', + 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', + 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', + 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', + }, + { 'n': 75341289328899252391918368331716799250n, + 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', + 'ud': '75.341.289.328.899.252.391.918.368.331.716.799.250', + 'ui': '0i75341289328899252391918368331716799250', + 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', + 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', + 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', + }, +]; +testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); + +// test cases generated in similar fashion as the integer aura tests + +const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; +const PHONETIC_TESTS: { + n: bigint, + p: string, + q: string +}[] = [ + { 'n': 7n, 'p': '~let', 'q': '.~let' }, + { 'n': 0n, 'p': '~zod', 'q': '.~zod' }, + { 'n': 8n, 'p': '~ful', 'q': '.~ful' }, + { 'n': 117n, 'p': '~deg', 'q': '.~deg' }, + { 'n': 83n, 'p': '~tev', 'q': '.~tev' }, + { 'n': 223n, 'p': '~lud', 'q': '.~lud' }, + { 'n': 39995n, 'p': '~hapwyc', 'q': '.~hapwyc' }, + { 'n': 50426n, 'p': '~mitrep', 'q': '.~mitrep' }, + { 'n': 11415n, 'p': '~torryx', 'q': '.~torryx' }, + { 'n': 1863930458n, 'p': '~mogteg-botfex', 'q': '.~ligput-motfus' }, + { 'n': 3284934632n, 'p': '~loplet-nosnyx', 'q': '.~fasryd-mirlyn' }, + { 'n': 3833668n, 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, + { 'n': 9260427482306755094n, + 'p': '~lasrum-pindyt-nimnym-fotmeg', + 'q': '.~lasrum-pindyt-tadtem-lodlup', + }, + { 'n': 6363574354411289343n, + 'p': '~nopnet-rostem-navteb-fodbep', + 'q': '.~nopnet-rostem-nimfel-monfes', + }, + { 'n': 17571387016818844998n, + 'p': '~namler-folwet-bictes-wormec', + 'q': '.~namler-folwet-samwet-sarrul', + }, + { 'n': 241760151623976361741451001031931477015n, + 'p': '~dablys-minwed-mosreb-mictyn--nostyv-nimdul-hanbyl-bisdep', + 'q': '.~dablys-minwed-mosreb-mictyn-nostyv-nimdul-hanbyl-bisdep', + }, + { 'n': 148310954517291502180858368907816435627n, + 'p': '~ligryn-lomnem-fintes-davsyr--pacdel-wolpex-ripdev-paldeb', + 'q': '.~ligryn-lomnem-fintes-davsyr-pacdel-wolpex-ripdev-paldeb', + }, + { 'n': 97100713129464593177912155425728457718n, + 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', + 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', + }, +]; +testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); + +const DATE_AURAS: aura[] = [ 'da' ]; +const DATE_TESTS: { + n: bigint, + da: string +}[] = [ + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.1.1' + }, + { 'n': 170141182164706681340023325049697075200n, + 'da': '~2000-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170213050367437966468743593413155225600n, + 'da': '~123456789.12.12' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..07.07.07' + }, + { 'n': 170141184492616163050404573632566132736n, + 'da': '~2000.1.1..11.11.11' + }, + { 'n': 170141184492616163050404761352701739008n, + 'da': '~2000.1.1..11.11.11..0000.aabb' + }, + { 'n': 170141184492616163062707000439658774528n, + 'da': '~2000.1.1..11.11.11..aabb' + }, + { 'n': 170141184492615487727406687186543706111n, + 'da': '~2000.1.1..01.01.01..aabb.ccdd.eeff.ffff' + } +]; +testAuras('date', DATE_AURAS, DATE_TESTS); + +const MANY_COINS: { + coin: coin, + out: string +}[] = [ + { coin: { type: 'many', list: [] }, + out: '.__' + }, + { coin: { type: 'many', list: [ { type: 'many', list: [] } ] }, + out: '._.~-~-__' + }, + { coin: { type: 'many', list: [ { type: 'dime', aura: 'p', atom: 0n }, { type: 'dime', aura: 'ux', atom: 0x1234abcdn } ] }, + out: '._~~zod_0x1234.abcd__' + }, +] +describe('%many coin rendering', () => { + MANY_COINS.map((test) => { + describe(`case ${test.out}`, () => { + it('renders', () => { + const res = rend(test.coin); + expect(res).toEqual(test.out); + }); + }); + }); +}); + +//TODO render->parse roundtrip tests + From b2e7b53cad04b6998b29a3e541af1503b78599bd Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 2 Jun 2025 21:32:52 +0200 Subject: [PATCH 04/50] render: fix rendering zero `@uw` ...by reimplementing the formatUw function locally. Somehow this is _slightly_ slower than formatUw(), but only by an amount measurable across hundreds of thousands of invocations. --- src/render.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/render.ts b/src/render.ts index c3cf021..f6cee78 100644 --- a/src/render.ts +++ b/src/render.ts @@ -106,7 +106,7 @@ export function rend(coin: coin): string { case 'i': return '0i' + dco(1, coin.atom); case 'x': return '0x' + split(coin.atom.toString(16), 4); case 'v': return '0v' + split(coin.atom.toString(32), 5); - case 'w': return formatUw(coin.atom); //TODO fix 0n case + case 'w': return '0w' + split(blend(6, UW_ALPHABET, coin.atom), 5); default: return split(coin.atom.toString(10), 3); } case 's': @@ -158,6 +158,18 @@ function wack(str: string) { return str.replaceAll('~', '~~').replaceAll('_', '~-'); } +const UW_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; +function blend(bits: number, alphabet: string, atom: bigint): string { + if (atom === 0n) return alphabet[0]; + let out = ''; + const bbits = BigInt(bits); + while (atom !== 0n) { + out = alphabet[Number(BigInt.asUintN(bits, atom))] + out; + atom = atom >> bbits; + } + return out; +} + function split(str: string, group: number): string { // insert '.' every $group characters counting from the end, // while avoiding putting a leading dot at the start From 7bae0a37af2d1501c64427d63ddd746cb39cbc95 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 2 Jun 2025 23:32:18 +0200 Subject: [PATCH 05/50] render: support text auras: tas, ta, t Neither `@tas` nor `@ta` explicitly sanitize, instead rely on the atom already being "sane" for those auras. But in those cases, too, we need to take special care: string encoding in atoms is utf-8, whereas js strings are utf-16, so we must call a `TextDecoder`. For `@t` we do need to sanitize the string. We lift logic previously implemented in tloncorp/tlon-apps#3274 into the library, where it belongs. --- src/render.ts | 71 +++++++++++++++++++++++++++++++++++++++++++-- test/render.test.ts | 37 ++++++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/render.ts b/src/render.ts index f6cee78..8a5a490 100644 --- a/src/render.ts +++ b/src/render.ts @@ -117,12 +117,12 @@ export function rend(coin: coin): string { case 't': if (coin.aura[1] === 'a') { if (coin.aura[2] === 's') { - return 'coin.atom'; //TODO fromCord + return cordToString(coin.atom); } else { - return '~.' + 'coin.atom'; //TODO fromCord + return '~.' + cordToString(coin.atom); } } else { - return '~~' + 'coin.atom'; //TODO fromCord(wood(coin.atom)) + return '~~' + encodeString(cordToString(coin.atom)); } default: return zco(coin.atom); @@ -158,6 +158,52 @@ function wack(str: string) { return str.replaceAll('~', '~~').replaceAll('_', '~-'); } +// encodeString(): encode string into @ta-safe format +// +// using logic from +wood. +// for example, 'some Chars!' becomes '~.some.~43.hars~21.' +// this is url-safe encoding for arbitrary strings. +// +export function encodeString(string: string) { + let out = ''; + for (let i = 0; i < string.length; i += 1) { + const char = string[i]; + let add = ''; + switch (char) { + case ' ': + add = '.'; + break; + case '.': + add = '~.'; + break; + case '~': + add = '~~'; + break; + default: { + const codePoint = string.codePointAt(i); + if (!codePoint) break; + // js strings are encoded in UTF-16, so 16 bits per character. + // codePointAt() reads a _codepoint_ at a character index, and may + // consume up to two js string characters to do so, in the case of + // 16 bit surrogate pseudo-characters. here we detect that case, so + // we can advance the cursor to skip past the additional character. + if (codePoint > 0xffff) i += 1; + if ( + (codePoint >= 97 && codePoint <= 122) || // a-z + (codePoint >= 48 && codePoint <= 57) || // 0-9 + char === '-' + ) { + add = char; + } else { + add = `~${codePoint.toString(16)}.`; + } + } + } + out += add; + } + return out; +} + const UW_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; function blend(bits: number, alphabet: string, atom: bigint): string { if (atom === 0n) return alphabet[0]; @@ -176,3 +222,22 @@ function split(str: string, group: number): string { return str.replace(new RegExp(`(?=(?:.{${group}})+$)(?!^)`, 'g'), '.'); } +function cordToString(atom: bigint): string { + return new TextDecoder('utf-8').decode(atomToByteArray(atom).reverse()); +}; + +//NOTE from nockjs' bigIntToByteArray +//REVIEW original produced [0] for 0n... probably not correct in our contexts! +function atomToByteArray(atom: bigint): Uint8Array { + if (atom === 0n) return new Uint8Array(0); + const hexString = atom.toString(16); + const paddedHexString = hexString.length % 2 === 0 ? hexString : '0' + hexString; + const arrayLength = paddedHexString.length / 2; + const int8Array = new Uint8Array(arrayLength); + for (let i = 0; i < paddedHexString.length; i += 2) { + const hexSubstring = paddedHexString.slice(i, i + 2); + const signedInt = (parseInt(hexSubstring, 16) << 24) >> 24; + int8Array[(i / 2)] = signedInt; + } + return int8Array; +} diff --git a/test/render.test.ts b/test/render.test.ts index 206efc1..c44c683 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -305,6 +305,41 @@ const DATE_TESTS: { ]; testAuras('date', DATE_AURAS, DATE_TESTS); +const TEXT_AURAS: aura[] = [ 'tas', 'ta', 't' ]; +const TEXT_TESTS: { + n: bigint, + tas: string, + ta: string, + t: string +}[] = [ + { n: 0n, + tas: '', + ta: '~.', + t: '~~' + }, + { n: 97n, + tas: 'a', + ta: '~.a', + t: '~~a' + }, + { n: 121404708502375659064812904n, + tas: 'hello-world', + ta: '~.hello-world', + t: '~~hello-world' + }, + { n: 10334410032597741434076685640n, + tas: 'Hello World!', + ta: '~.Hello World!', + t: '~~~48.ello.~57.orld~21.' + }, + { n: 294301677938177654314463611973797746852183254758760570046179940746240825570n, + tas: 'โ˜…๐Ÿค yeehaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + ta: '~.โ˜…๐Ÿค yeehaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + t: '~~~2605.~1f920.yeehaw~1f468.~200d.~1f467.~200d.~1f466.' + } +]; +testAuras('text', TEXT_AURAS, TEXT_TESTS); + const MANY_COINS: { coin: coin, out: string @@ -321,7 +356,7 @@ const MANY_COINS: { ] describe('%many coin rendering', () => { MANY_COINS.map((test) => { - describe(`case ${test.out}`, () => { + describe(test.out, () => { it('renders', () => { const res = rend(test.coin); expect(res).toEqual(test.out); From b8a907c02883303bd6da3d9e0022cb25ba5d279c Mon Sep 17 00:00:00 2001 From: fang Date: Thu, 5 Jun 2025 23:45:34 +0200 Subject: [PATCH 06/50] parse: support escaped text in `@t` We weren't properly extracting escaped byte-sequences in cords. Now we do. We add inverses for the utilities written into render.ts, and settle on a maximally-performant `bytesToBigint` that leverages Node.js's `Buffer` if it's available for ~10x speedup on larger amounts of bytes. --- src/parse.ts | 79 ++++++++++++++++++++++++++++++++++++---------- test/parse.test.ts | 28 +++++++++++++++- 2 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index f37201a..691deed 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -116,7 +116,7 @@ export function nuck(str: string): coin | null { const c = str[0]; if (c >= 'a' && c <= 'z') { // "sym" if (regex['tas'].test(str)) { - return { type: 'dime', aura: 'tas', atom: fromString(str) }; + return { type: 'dime', aura: 'tas', atom: stringToCord(str) }; } else { return null; } @@ -196,15 +196,15 @@ export function nuck(str: string): coin | null { //TODO test if these single-character checks affect performance or no, // or if we want to move them further up, etc. if (str[1] === '.' && regex['ta'].test(str)) { - return { type: 'dime', aura: 'ta', atom: fromString(str.slice(2)) }; + return { type: 'dime', aura: 'ta', atom: stringToCord(str.slice(2)) }; } else if (str[1] === '~' && regex['t'].test(str)) { - //TODO known-wrong! extract encoded byte-sequences - return { type: 'dime', aura: 't', atom: fromString(str.slice(2)) }; + return { type: 'dime', aura: 't', atom: stringToCord(decodeString(str.slice(2))) }; } else if (str[1] === '-' && regex['c'].test(str)) { //TODO known-wrong! extract encoded byte-sequences and call +taft - return { type: 'dime', aura: 'c', atom: fromString(str.slice(2)) }; + // ...or just extract as single utf-32 character? + return { type: 'dime', aura: 'c', atom: stringToCord(decodeString(str.slice(2))) }; } } //TODO twid for %blob support @@ -266,17 +266,62 @@ export function bisk(str: string): dime | null { } } -//TODO this is basically nockjs' Atom.fromCord -function fromString(str: string): bigint { - if (str.length === 0) return 0n; - let i, - j, - octs = Array(str.length); - for (i = 0, j = octs.length - 1; i < octs.length; ++i, --j) { - const charByte = (str.charCodeAt(i) & 0xff).toString(16); - octs[j] = charByte.length === 1 ? "0" + charByte : charByte; +// decodeString(): decode string from @ta-safe format +// +// using logic from +woad. +// for example, '~.some.~43.hars~21.' becomes 'some Chars!' +// assumes +// +export function decodeString(str: string): string { + let out = ''; + let i = 0; + while (i < str.length) { + switch (str[i]) { + case '.': + out = out + ' '; + i++; continue; + case '~': + switch (str[++i]) { + case '~': + out = out + '~'; + i++; continue; + case '.': + out = out + '.'; + i++; continue; + default: + let char: number = 0; + do { + char = (char << 4) | Number.parseInt(str[i++], 16); + } while (str[i] !== '.') + out = out + String.fromCodePoint(char); + i++; continue; + } + default: + out = out + str[i++]; + continue; + } + } + return out; +} + +function stringToCord(str: string): bigint { + return bytesToBigint(new TextEncoder().encode(str)); +} + +//REVIEW should the reversal happen here or at callsites? depends on what endianness is idiomatic to js? +function bytesToBigint(bytes: Uint8Array): bigint { + // if we have node's Buffer available, use it, it's wicked fast. + // otherwise, constructing the hex string "by hand" and instantiating + // a bigint from that is the fastest thing we can do. + // + if (typeof Buffer !== 'undefined') + return BigInt('0x' + Buffer.from(bytes.reverse()).toString('hex')); + let byt: number, + parts: string[] = []; + for (var i = bytes.length - 1; i >= 0; --i) { + byt = bytes[i]; + parts.push(byt < 16 ? "0" + byt.toString(16) : byt.toString(16)); } - return BigInt('0x' + octs.join('')); - // if (str.length > 4) return BigInt('0x' + octs.join('')); - // else return BigInt(parseInt(octs.join(""), 16)); + const num = BigInt('0x' + parts.join('')); + return num; } diff --git a/test/parse.test.ts b/test/parse.test.ts index c565d58..f1c1127 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,4 +1,4 @@ -import { parse, aura } from '../src/parse'; +import { parse, aura, decodeString } from '../src/parse'; // most test cases generated from snippets similar to the following: // @@ -313,4 +313,30 @@ const DATE_TESTS: { ]; testAuras('phonetic', DATE_AURAS, DATE_TESTS); +describe('string decoding', () => { + it('decodes', () => { + expect(decodeString('a~62.c')).toEqual('abc'); + expect(decodeString('a~0a.c')).toEqual('a\nc'); + expect(decodeString('~2605.~1f920.yeehaw~1f468.~200d.~1f467.~200d.~1f466.')).toEqual('โ˜…๐Ÿค yeehaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'); + }); +}); +const TEXT_AURAS: aura[] = [ 't' ]; +const TEXT_TESTS: { + n: bigint, + t: string +}[] = [ + { 'n': 6513249n, + 't': '~~abc' + }, + { 'n': 127430240531865354190938721n, + 't': '~~abc~~def~.ghi' + }, + { 'n': 6513249n, + 't': '~~a~62.c' + }, + { 'n': 294301677938177654314463611973797746852183254758760570046179940746240825570n, + 't': '~~~2605.~1f920.yeehaw~1f468.~200d.~1f467.~200d.~1f466.' + } + ]; +testAuras('text', TEXT_AURAS, TEXT_TESTS); From 4a4a4806e8b0ed18734670978c0fea51a234bbbc Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 6 Jun 2025 15:27:37 +0200 Subject: [PATCH 07/50] render: fix `@da` rendering This brings it in line with the stdlib rendering, which ensures leading zeroes for the hours, minutes and seconds, prints those conditionally, and extracts & renders the sub-second precision accurately. --- src/da.ts | 22 ++++++++++++++-------- test/render.test.ts | 3 +++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/da.ts b/src/da.ts index c337aa8..3760799 100644 --- a/src/da.ts +++ b/src/da.ts @@ -117,10 +117,12 @@ function yell(x: bigint): Tarp { const milliMask = BigInt('0xffffffffffffffff'); const millis = milliMask & x; const ms = millis - .toString(16) - .match(/.{1,4}/g)! - .filter((x) => x !== '0000') + .toString(16).padStart(16, '0') + .match(/.{4}/g)! .map((x) => BigInt('0x'+x)); + while (ms.at(-1) === 0n) { + ms.pop(); + } let day = sec / DAY_YO; sec = sec % DAY_YO; let hor = sec / HOR_YO; @@ -203,11 +205,15 @@ export function formatDa(x: bigint | string) { if (typeof x === 'string') { x = BigInt(x); } - const { year, month, time } = yore(x); - - return `~${year}.${month}.${time.day}..${time.hour}.${time.minute}.${ - time.second - }..${time.ms.map((x) => x.toString(16).padStart(4, '0')).join('.')}`; + const { pos, year, month, time } = yore(x); + let out = `~${year}${pos ? '' : '-'}.${month}.${time.day}`; + if (time.hour !== 0n || time.minute !== 0n || time.second !== 0n || time.ms.length !== 0) { + out = out + `..${time.hour.toString().padStart(2, '0')}.${time.minute.toString().padStart(2, '0')}.${time.second.toString().padStart(2, '0')}` + if (time.ms.length !== 0) { + out = out + `..${time.ms.map((x) => x.toString(16).padStart(4, '0')).join('.')}`; + } + } + return out; } /** diff --git a/test/render.test.ts b/test/render.test.ts index c44c683..1a9fc39 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -287,6 +287,9 @@ const DATE_TESTS: { { 'n': 170213050367437966468743593413155225600n, 'da': '~123456789.12.12' }, + { 'n': 170141184507170056208381036660470579200n, + 'da': '~2025.1.1..01.00.00' + }, { 'n': 170141184492615892916284358229892268032n, 'da': '~2000.1.1..07.07.07' }, From a956bba6c68e1aae06f383900174f745e7ab6628 Mon Sep 17 00:00:00 2001 From: fang Date: Tue, 10 Jun 2025 22:12:20 +0200 Subject: [PATCH 08/50] render: fix `@q` rendering It should omit the leading byte if it's empty/zero. --- src/q.ts | 4 ++-- test/render.test.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/q.ts b/src/q.ts index 53d2d0c..645291e 100644 --- a/src/q.ts +++ b/src/q.ts @@ -33,8 +33,8 @@ function buf2patq(buf: Buffer): string { const prefixName = (byts: number[]) => byts[1] === undefined - ? prefixes[0] + suffixes[byts[0]] - : prefixes[byts[0]] + suffixes[byts[1]]; + ? suffixes[byts[0]] + : prefixes[byts[0]] + suffixes[byts[1]]; //TODO this branch unused const name = (byts: number[]) => byts[1] === undefined diff --git a/test/render.test.ts b/test/render.test.ts index 1a9fc39..978493c 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -264,6 +264,19 @@ const PHONETIC_TESTS: { 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', }, + // with zero bytes + { 'n': 3833668n, + 'p': '~nidmes-samrut', + 'q': '.~sef-palsub' + }, + { 'n': 319478973361751151n, + 'p': '~sampel-sampel-lacwyl-tirder', + 'q': '.~sampel-sampel-dozpel-sampel', + }, + { 'n': 319478973354476655n, + 'p': '~sampel-sampel-dozzod-sampel', + 'q': '.~sampel-sampel-dozzod-sampel', + }, ]; testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); From 33e696286f52d38caca1177b951fa15d7a17a79c Mon Sep 17 00:00:00 2001 From: fang Date: Tue, 10 Jun 2025 23:53:14 +0200 Subject: [PATCH 09/50] render: support character aura: c `@c` is used to represent utf-32 codepoints. Its rendering is very similar to the url-safe `@t` encoding used in most places. This should have parity with the stdlib where it matters. There are some deranged cases that will not render accurately, but those shouldn't matter in practice. (Right?) --- src/render.ts | 9 ++++++++- test/render.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/render.ts b/src/render.ts index 8a5a490..be24e78 100644 --- a/src/render.ts +++ b/src/render.ts @@ -63,7 +63,14 @@ export function rend(coin: coin): string { case 'dime': switch(coin.aura[0]) { case 'c': - throw new Error('aura-js: @c rendering unsupported'); //TODO + // this short-circuits the (wood (tuft atom)) calls that + // hoon.hoon does, leaning on wood only for ascii characters, + // and going straight to encoded representation for anything else. + // (otherwise we'd need to stringify the utf-32 bytes, ouch.) + if (coin.atom < 0x7fn) + return '~-' + encodeString(String.fromCharCode(Number(coin.atom))); + else + return '~-~' + coin.atom.toString(16) + '.'; case 'd': switch(coin.aura[1]) { case 'a': diff --git a/test/render.test.ts b/test/render.test.ts index 978493c..5b385b2 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -356,6 +356,31 @@ const TEXT_TESTS: { ]; testAuras('text', TEXT_AURAS, TEXT_TESTS); + +const CHAR_AURAS: aura[] = [ 'c' ]; +const CHAR_TESTS: { n: bigint, c: string }[] = [ + { n: 129312n, c: '~-~1f920.' }, + { n: 128104n, c: '~-~1f468.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128103n, c: '~-~1f467.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128102n, c: '~-~1f466.' }, + { n: 97n, c: '~-a' }, + { n: 33n, c: '~-~21.' }, + { n: 32n, c: '~-.' }, + { n: 126n, c: '~-~~' }, + { n: 46n, c: '~-~.' }, + { n: 1548n, c: '~-~60c.' }, + // the cases below are deranged, because the input is deranged. + // @c represents utf-32 codepoints, so if you give it not-utf-32 + // it will render bogus, drop bytes in the rendering, etc. + // we include them (disabled) here to indicate that we don't have 100% + // exact stdlib parity here, but in practice that shouldn't matter. + // { n: 478560413032n, c: '~-~c6568.o' }, // 'hello' + // { n: 36762444129640n, c: '~-~c6568.~216f.' } // 'hello!' +]; +testAuras('chars', CHAR_AURAS, CHAR_TESTS); + const MANY_COINS: { coin: coin, out: string From 6478dd02bbd8702228410b7213a454a178110a32 Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 11 Jun 2025 00:16:00 +0200 Subject: [PATCH 10/50] parse: improved da support Support leading zeroes in timeslots, absence of time and/or subseconds, and negative years (Before Christ). --- src/da.ts | 13 ++++++++++--- src/parse.ts | 2 +- test/parse.test.ts | 5 ++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/da.ts b/src/da.ts index 3760799..53b479b 100644 --- a/src/da.ts +++ b/src/da.ts @@ -93,13 +93,20 @@ export function year(det: Dat) { * @return {bigint} x The urbit date as bigint */ export function parseDa(x: string): bigint { - const [date, time, ms] = x.split('..'); - const [yer, month, day] = date.slice(1).split('.'); + let pos = true; + let [date, time, ms] = x.split('..'); + time = time || '0.0.0'; + ms = ms || '0000'; + let [yer, month, day] = date.slice(1).split('.'); + if (yer.at(-1) === '-') { + yer = yer.slice(0, -1); + pos = false; + } const [hour, minute, sec] = time.split('.'); const millis = ms.split('.').map((m) => BigInt('0x'+m)); return year({ - pos: true, + pos: pos, year: BigInt(yer), month: BigInt(month), time: { diff --git a/src/parse.ts b/src/parse.ts index 691deed..abf8f5a 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -48,7 +48,7 @@ export type coin = ({ type: 'dime' } & dime) //TODO rewrite with eye towards capturing groups? export const regex: { [key in aura]: RegExp } = { 'c': /^~\-((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, - 'da': /^~(0|[1-9][0-9]*)\-?\.([1-9]|1[0-2])\.([1-9]|[1-3][0-9])(\.\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(\.[0-9a-f]{4})+)?)?$/, + 'da': /^~(0|[1-9][0-9]*)\-?\.([1-9]|1[0-2])\.([1-9]|[1-3][0-9])(\.\.([0-9]+)\.([0-9]+)\.([0-9]+)(\.(\.[0-9a-f]{4})+)?)?$/, 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))?(\.(\.[0-9a-f]{4})+)?$/, 'f': /^\.(y|n)$/, 'n': /^~$/, diff --git a/test/parse.test.ts b/test/parse.test.ts index f1c1127..eab2edc 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -307,11 +307,14 @@ const DATE_TESTS: { { 'n': 170141184492616163062707000439658774528n, 'da': '~2000.1.1..11.11.11..aabb.0000' }, + { 'n': 170141184492616163062707000439658774528n, + 'da': '~2000.1.1..11.11.11..aabb' + }, { 'n': 170141184492615487727406687186543706111n, 'da': '~2000.1.1..1.1.1..aabb.ccdd.eeff.ffff' } ]; -testAuras('phonetic', DATE_AURAS, DATE_TESTS); +testAuras('date', DATE_AURAS, DATE_TESTS); describe('string decoding', () => { it('decodes', () => { From 6022fa8e5cfb68ed352cc663cf7a79c0310ea401 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 16:04:57 +0200 Subject: [PATCH 11/50] parse: support %blob and %many coins For %blob, because we don't want to depend on the full nockjs, we concede and say that %blobs in the aura-js contexts are the jams of nouns, instead of the nouns themselves. Nockjs should probably provide the "real" coin type and parser as a wrapper around aura-js's implementation. --- src/parse.ts | 23 ++++++++++++++++++----- test/parse.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index abf8f5a..e4c981a 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -5,7 +5,6 @@ // //TODO unsupported auras: @r*, @if, @is -//TODO unsupported coins: %blob, %many import { parseDa } from "./da"; import { isValidPatp, patp2bn } from "./p"; @@ -37,7 +36,7 @@ export type aura = 'c' | 'ux'; export type dime = { aura: aura, atom: bigint } export type coin = ({ type: 'dime' } & dime) - | { type: 'blob', noun: any } //TODO could do jam: bigint if we don't want nockjs dependency? + | { type: 'blob', jam: bigint } //NOTE nockjs for full noun | { type: 'many', list: coin[] } //TODO for deduplicating @u vs @s @@ -169,9 +168,21 @@ export function nuck(str: string): coin | null { } else { return { type: 'dime', aura: 'q', atom: patq2bn(q) } } - } + } else //TODO %is, %if, %r* // "zust" - //TODO nusk for %many support + if (str[1] === '_' && /^\.(_([0-9a-zA-Z\-\.]|~\-|~~)+)*__$/.test(str)) { // "nusk" + const coins = str.slice(1, -2).split('_').slice(1).map((s): coin | null => { + //NOTE real +wick produces null for strings w/ other ~ chars, + // but the regex above already excludes those + s = s.replaceAll('~-', '_').replaceAll('~~', '~'); // "wick" + return nuck(s); + }); + if (coins.some(c => c === null)) { + return null; + } else { + return { type: 'many', list: coins as coin[] }; + } + } return null; } else if (c === '~') { @@ -207,7 +218,9 @@ export function nuck(str: string): coin | null { return { type: 'dime', aura: 'c', atom: stringToCord(decodeString(str.slice(2))) }; } } - //TODO twid for %blob support + if ((str[1] === '0') && /^~0[0-9a-v]+$/.test(str)) { + return { type: 'blob', jam: parseUv('0v' + str.slice(2)) }; + } return null; } return null; diff --git a/test/parse.test.ts b/test/parse.test.ts index eab2edc..7a664df 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,4 +1,4 @@ -import { parse, aura, decodeString } from '../src/parse'; +import { parse, aura, decodeString, nuck } from '../src/parse'; // most test cases generated from snippets similar to the following: // @@ -343,3 +343,44 @@ const TEXT_TESTS: { } ]; testAuras('text', TEXT_AURAS, TEXT_TESTS); + +describe('blob parsing', () => { + it('parses', () => { + expect(nuck('~02')).toEqual({ type: 'blob', jam: 2n }); + expect(nuck('~097su1g7hk1')).toEqual({ type: 'blob', jam: 325350265702017n }); + }); +}); + +describe('many parsing', () => { + it('parses', () => { + expect(nuck('.__')).toEqual({ type: 'many', list: [] }); + expect( + nuck('._123__') + ).toEqual( + { type: 'many', list: [ { type: 'dime', aura: 'ud', atom: 123n }, ] } + ); + expect( + nuck('._~~~~a~~42.c_123_~~01hu32s3gu1_.~-1~-2~-~-__') + ).toEqual({ type: 'many', list: [ + { type: 'dime', aura: 't', atom: 6505057n }, + { type: 'dime', aura: 'ud', atom: 123n }, + { type: 'blob', jam: 54910179722177n }, + { type: 'many', list: [ + { type: 'dime', aura: 'ud', atom: 1n }, + { type: 'dime', aura: 'ud', atom: 2n } + ] } + ] }); + }); +}); + +describe('invalid syntax', () => { + it('fails incomplete atoms', () => { + expect(nuck('~0')).toEqual(null); + expect(nuck('~2000.1')).toEqual(null); + }); + it('fails unescaped characters', () =>{ + expect(nuck('~.aBc')).toEqual(null); + expect(nuck('._~zod__')).toEqual(null); + expect(nuck('.123__')).toEqual(null); + }); +}); From 42feb58c70de650cca916a3b185bf8e11d265fda Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 19:19:53 +0200 Subject: [PATCH 12/50] parse: refactor integer regexes They all follow a regular pattern. Constructing them through a function like this makes them more obvious. --- src/parse.ts | 33 +++++++++++++++++---------------- test/parse.test.ts | 11 +++++++---- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index e4c981a..faf199a 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -39,10 +39,11 @@ export type coin = ({ type: 'dime' } & dime) | { type: 'blob', jam: bigint } //NOTE nockjs for full noun | { type: 'many', list: coin[] } -//TODO for deduplicating @u vs @s -// function integerRegex(a: string, b: string, c: string, d: boolean = false): RegExp { -// return new RegExp(`^${d ? '\\-\\-?' : ''}xx$`); -// } +function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { + const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; + const aft = d === 0 ? `${c}*` : `(\\.${c}{${d}})*`; + return new RegExp(`^${e ? '\\-\\-?' : ''}${a}(0|${pre}${aft})$`); +} //TODO rewrite with eye towards capturing groups? export const regex: { [key in aura]: RegExp } = { @@ -53,21 +54,21 @@ export const regex: { [key in aura]: RegExp } = { 'n': /^~$/, 'p': /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/, //NOTE matches shape but not syllables 'q': /^\.~(([a-z]{3}|[a-z]{6})(\-[a-z]{6})*)$/, //NOTE matches shape but not syllables - 'sb': /^\-\-?0b(0|1[01]{0,3}(\.[01]{4})*)$/, - 'sd': /^\-\-?(0|[1-9][0-9]{0,3}(\.[0-9]{4})*)$/, - 'si': /^\-\-?0i(0|[1-9][0-9]*)$/, - 'sv': /^\-\-?0v(0|[1-9a-v][0-9a-v]{0,4}(\.[0-9a-v]{5})*)$/, - 'sw': /^\-\-?0w(0|[1-9a-zA-Z~-][0-9a-zA-Z~-]{0,4}(\.[0-9a-zA-Z~-]{5})*)$/, - 'sx': /^\-\-?0x(0|[1-9a-f][0-9a-f]{0,3}(\.[0-9a-f]{4})*)$/, + 'sb': integerRegex('0b', '1', '[01]', 4, true), + 'sd': integerRegex('', '[1-9]', '[0-9]', 3, true), + 'si': integerRegex('0i', '[1-9]', '[0-9]', 0, true), + 'sv': integerRegex('0v', '[1-9a-v]', '[0-9a-v]', 5, true), + 'sw': integerRegex('0w', '[1-9a-zA-Z~-]', '[0-9a-zA-Z~-]', 5, true), + 'sx': integerRegex('0x', '[1-9a-f]', '[0-9a-f]', 4, true), 't': /^~~((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, 'ta': /^~\.[0-9a-z\-\.~_]*$/, 'tas': /^[a-z][a-z0-9\-]*$/, - 'ub': /^0b(0|1[01]{0,3}(\.[01]{4})*)$/, - 'ud': /^(0|[1-9][0-9]{0,2}(\.[0-9]{3})*)$/, - 'ui': /^0i(0|[1-9][0-9]*)$/, - 'uv': /^0v(0|[1-9a-v][0-9a-v]{0,4}(\.[0-9a-v]{5})*)$/, - 'uw': /^0w(0|[1-9a-zA-Z~-][0-9a-zA-Z~-]{0,4}(\.[0-9a-zA-Z~-]{5})*)$/, - 'ux': /^0x(0|[1-9a-f][0-9a-f]{0,3}(\.[0-9a-f]{4})*)$/, + 'ub': integerRegex('0b', '1', '[01]', 4), + 'ud': integerRegex('', '[1-9]', '[0-9]', 3), + 'ui': integerRegex('0i', '[1-9]', '[0-9]', 0), + 'uv': integerRegex('0v', '[1-9a-v]', '[0-9a-v]', 5), + 'uw': integerRegex('0w', '[1-9a-zA-Z~-]', '[0-9a-zA-Z~-]', 5), + 'ux': integerRegex('0x', '[1-9a-f]', '[0-9a-f]', 4), }; // parse(): slaw() diff --git a/test/parse.test.ts b/test/parse.test.ts index 7a664df..ad5e882 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,4 +1,4 @@ -import { parse, aura, decodeString, nuck } from '../src/parse'; +import { parse, aura, decodeString, nuck, regex } from '../src/parse'; // most test cases generated from snippets similar to the following: // @@ -41,10 +41,13 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { describe(`@${a} parsing`, () => { tests.map((test) => { // @ts-ignore we know this is sane/safe - describe(test[a], () => { + const str = test[a]; + describe(str, () => { + it('matches regex', () => { + expect(regex[a].test(str)).toEqual(true); + }); it('parses', () => { - // @ts-ignore we know this is sane/safe - const res = parse(a, test[a]); + const res = parse(a, str); expect(res).toEqual(test.n); }); }); From 5219efbc40a9b2e96038a3d2590179e75e6938f3 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 20:34:23 +0200 Subject: [PATCH 13/50] render: fix signed atom rendering, add tests --- src/render.ts | 2 +- test/render.test.ts | 119 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/render.ts b/src/render.ts index be24e78..32ebc3c 100644 --- a/src/render.ts +++ b/src/render.ts @@ -119,7 +119,7 @@ export function rend(coin: coin): string { case 's': const end = (coin.atom & 1n); coin.atom = end + (coin.atom >> 1n); - coin.aura = coin.aura.replace('u', 's') as aura; + coin.aura = coin.aura.replace('s', 'u') as aura; return ((end === 0n) ? '--' : '-') + rend(coin); case 't': if (coin.aura[1] === 'a') { diff --git a/test/render.test.ts b/test/render.test.ts index 5b385b2..e1f5abf 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -55,7 +55,10 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { }); } -const INTEGER_AURAS: aura[] = [ 'ub', 'ud', 'ui', 'uv', 'uw', 'ux' ]; +const INTEGER_AURAS: aura[] = [ + 'ub', 'ud', 'ui', 'uv', 'uw', 'ux', + 'sb', 'sd', 'si', 'sv', 'sw', 'sx' +]; const INTEGER_TESTS: { n: bigint, ub: string, @@ -64,6 +67,12 @@ const INTEGER_TESTS: { uv: string, uw: string, ux: string, + sb: string, + sd: string, + si: string, + sv: string, + sw: string, + sx: string, }[] = [ { 'n': 0n, 'ub': '0b0', @@ -72,6 +81,12 @@ const INTEGER_TESTS: { 'uv': '0v0', 'uw': '0w0', 'ux': '0x0', + 'sb': '--0b0', + 'sd': '--0', + 'si': '--0i0', + 'sv': '--0v0', + 'sw': '--0w0', + 'sx': '--0x0', }, { 'n': 7n, 'ub': '0b111', @@ -80,6 +95,12 @@ const INTEGER_TESTS: { 'uv': '0v7', 'uw': '0w7', 'ux': '0x7', + 'sb': '-0b100', + 'sd': '-4', + 'si': '-0i4', + 'sv': '-0v4', + 'sw': '-0w4', + 'sx': '-0x4', }, { 'n': 5n, 'ub': '0b101', @@ -88,6 +109,12 @@ const INTEGER_TESTS: { 'uv': '0v5', 'uw': '0w5', 'ux': '0x5', + 'sb': '-0b11', + 'sd': '-3', + 'si': '-0i3', + 'sv': '-0v3', + 'sw': '-0w3', + 'sx': '-0x3', }, { 'n': 4n, 'ub': '0b100', @@ -96,6 +123,12 @@ const INTEGER_TESTS: { 'uv': '0v4', 'uw': '0w4', 'ux': '0x4', + 'sb': '--0b10', + 'sd': '--2', + 'si': '--0i2', + 'sv': '--0v2', + 'sw': '--0w2', + 'sx': '--0x2', }, { 'n': 171n, 'ub': '0b1010.1011', @@ -104,6 +137,12 @@ const INTEGER_TESTS: { 'uv': '0v5b', 'uw': '0w2H', 'ux': '0xab', + 'sb': '-0b101.0110', + 'sd': '-86', + 'si': '-0i86', + 'sv': '-0v2m', + 'sw': '-0w1m', + 'sx': '-0x56', }, { 'n': 53n, 'ub': '0b11.0101', @@ -112,6 +151,12 @@ const INTEGER_TESTS: { 'uv': '0v1l', 'uw': '0wR', 'ux': '0x35', + 'sb': '-0b1.1011', + 'sd': '-27', + 'si': '-0i27', + 'sv': '-0vr', + 'sw': '-0wr', + 'sx': '-0x1b', }, { 'n': 77n, 'ub': '0b100.1101', @@ -120,6 +165,12 @@ const INTEGER_TESTS: { 'uv': '0v2d', 'uw': '0w1d', 'ux': '0x4d', + 'sb': '-0b10.0111', + 'sd': '-39', + 'si': '-0i39', + 'sv': '-0v17', + 'sw': '-0wD', + 'sx': '-0x27', }, { 'n': 64491n, 'ub': '0b1111.1011.1110.1011', @@ -128,6 +179,12 @@ const INTEGER_TESTS: { 'uv': '0v1uvb', 'uw': '0wfLH', 'ux': '0xfbeb', + 'sb': '-0b111.1101.1111.0110', + 'sd': '-32.246', + 'si': '-0i32246', + 'sv': '-0vvfm', + 'sw': '-0w7TS', + 'sx': '-0x7df6', }, { 'n': 51765n, 'ub': '0b1100.1010.0011.0101', @@ -136,6 +193,12 @@ const INTEGER_TESTS: { 'uv': '0v1ihl', 'uw': '0wcER', 'ux': '0xca35', + 'sb': '-0b110.0101.0001.1011', + 'sd': '-25.883', + 'si': '-0i25883', + 'sv': '-0vp8r', + 'sw': '-0w6kr', + 'sx': '-0x651b', }, { 'n': 46444n, 'ub': '0b1011.0101.0110.1100', @@ -144,6 +207,12 @@ const INTEGER_TESTS: { 'uv': '0v1dbc', 'uw': '0wblI', 'ux': '0xb56c', + 'sb': '--0b101.1010.1011.0110', + 'sd': '--23.222', + 'si': '--0i23222', + 'sv': '--0vmlm', + 'sw': '--0w5GS', + 'sx': '--0x5ab6', }, { 'n': 384265565n, 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', @@ -152,6 +221,12 @@ const INTEGER_TESTS: { 'uv': '0vb.eerat', 'uw': '0wmVSRt', 'ux': '0x16e7.6d5d', + 'sb': '-0b1011.0111.0011.1011.0110.1010.1111', + 'sd': '-192.132.783', + 'si': '-0i192132783', + 'sv': '-0v5.n7dlf', + 'sw': '-0wbsXqL', + 'sx': '-0xb73.b6af', }, { 'n': 2456897374n, 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', @@ -160,6 +235,12 @@ const INTEGER_TESTS: { 'uv': '0v29.72hqu', 'uw': '0w2.isktu', 'ux': '0x9271.475e', + 'sb': '--0b100.1001.0011.1000.1010.0011.1010.1111', + 'sd': '--1.228.448.687', + 'si': '--0i1228448687', + 'sv': '--0v14.jh8tf', + 'sw': '--0w1.9eaeL', + 'sx': '--0x4938.a3af', }, { 'n': 38583115n, 'ub': '0b10.0100.1100.1011.1011.0100.1011', @@ -168,6 +249,12 @@ const INTEGER_TESTS: { 'uv': '0v1.4peqb', 'uw': '0w2jbJb', 'ux': '0x24c.bb4b', + 'sb': '-0b1.0010.0110.0101.1101.1010.0110', + 'sd': '-19.291.558', + 'si': '-0i19291558', + 'sv': '-0vicnd6', + 'sw': '-0w19BSC', + 'sx': '-0x126.5da6', }, { 'n': 13604104043154737885n, 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', @@ -176,6 +263,12 @@ const INTEGER_TESTS: { 'uv': '0vbpi.rship.r32mt', 'uw': '0wb.Pbv8O.PIoHt', 'ux': '0xbccb.7c8c.b3b1.8add', + 'sb': '-0b101.1110.0110.0101.1011.1110.0100.0110.0101.1001.1101.1000.1100.0101.0110.1111', + 'sd': '-6.802.052.021.577.368.943', + 'si': '-0i6802052021577368943', + 'sv': '-0v5sp.du8pc.thhbf', + 'sw': '-0w5.VBLAp.pSclL', + 'sx': '-0x5e65.be46.59d8.c56f', }, { 'n': 18441444580797368868n, 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', @@ -184,6 +277,12 @@ const INTEGER_TESTS: { 'uv': '0vfvr.9c4ej.nlih4', 'uw': '0wf.~Jb2e.DuIEA', 'ux': '0xffed.2c23.a77a.ca24', + 'sb': '--0b111.1111.1111.0110.1001.0110.0001.0001.1101.0011.1011.1101.0110.0101.0001.0010', + 'sd': '--9.220.722.290.398.684.434', + 'si': '--0i9220722290398684434', + 'sv': '--0v7vt.km279.rqp8i', + 'sw': '--0w7.~SBx7.jLmki', + 'sx': '--0x7ff6.9611.d3bd.6512', }, { 'n': 7643844662312245512n, 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', @@ -192,6 +291,12 @@ const INTEGER_TESTS: { 'uv': '0v6k5.3472r.m6688', 'uw': '0w6.Ekp3y.ToNA8', 'ux': '0x6a14.6438.b763.1908', + 'sb': '--0b11.0101.0000.1010.0011.0010.0001.1100.0101.1011.1011.0001.1000.1100.1000.0100', + 'sd': '--3.821.922.331.156.122.756', + 'si': '--0i3821922331156122756', + 'sv': '--0v3a2.hi3hd.r3344', + 'sw': '--0w3.kacxN.rIoO4', + 'sx': '--0x350a.321c.5bb1.8c84', }, { 'n': 293389376720547819362821033486028091527n, 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', @@ -200,6 +305,12 @@ const INTEGER_TESTS: { 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', + 'sb': '-0b110.1110.0101.1100.0101.1110.1000.1010.0110.1010.0111.1110.1101.1111.0001.1011.1100.1001.1110.0000.0100.0111.0010.0100.0011.1001.1101.0111.1101.1000.0100.0100', + 'sd': '-146.694.688.360.273.909.681.410.516.743.014.045.764', + 'si': '-0i146694688360273909681410516743014045764', + 'sv': '-0v3.ebhf8.kqjur.sdsjo.274gs.tfm24', + 'sw': '-0w1K.n5Waq.DXv6Y.DwhOg.VRZx4', + 'sx': '-0x6e5c.5e8a.6a7e.df1b.c9e0.4724.39d7.d844', }, { 'n': 11826418988767709295206418976840492314n, 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', @@ -208,6 +319,12 @@ const INTEGER_TESTS: { 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', + 'sb': '--0b100.0111.0010.1101.0111.1011.1100.1011.0110.0100.0110.1101.0000.1110.0001.1111.1000.0101.1000.1000.1101.0011.1001.1111.1000.1110.1011.0001.0010.1000.1101', + 'sd': '--5.913.209.494.383.854.647.603.209.488.420.246.157', + 'si': '--0i5913209494383854647603209488420246157', + 'sv': '--0v4ebbr.pdi6q.3gvgm.4d77s.em4kd', + 'sw': '--0w4.sJuYJ.ArgUv.xozjD.UWNad', + 'sx': '--0x472.d7bc.b646.d0e1.f858.8d39.f8eb.128d', }, { 'n': 75341289328899252391918368331716799250n, 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', From 1aadd88c97d5dd6bc065a49349123123b29e851c Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 20:35:09 +0200 Subject: [PATCH 14/50] parse: add signed atom tests --- test/parse.test.ts | 125 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/test/parse.test.ts b/test/parse.test.ts index ad5e882..ce78ccb 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -57,7 +57,10 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { }); } -const INTEGER_AURAS: aura[] = [ 'ub', 'ud', 'ui', 'uv', 'uw', 'ux' ]; +const INTEGER_AURAS: aura[] = [ + 'ub', 'ud', 'ui', 'uv', 'uw', 'ux', + 'sb', 'sd', 'si', 'sv', 'sw', 'sx', +]; const INTEGER_TESTS: { n: bigint, ub: string, @@ -66,6 +69,12 @@ const INTEGER_TESTS: { uv: string, uw: string, ux: string, + sb: string, + sd: string, + si: string, + sv: string, + sw: string, + sx: string, }[] = [ { 'n': 0n, 'ub': '0b0', @@ -74,6 +83,12 @@ const INTEGER_TESTS: { 'uv': '0v0', 'uw': '0w0', 'ux': '0x0', + 'sb': '--0b0', + 'sd': '--0', + 'si': '--0i0', + 'sv': '--0v0', + 'sw': '--0w0', + 'sx': '--0x0', }, { 'n': 7n, 'ub': '0b111', @@ -82,6 +97,12 @@ const INTEGER_TESTS: { 'uv': '0v7', 'uw': '0w7', 'ux': '0x7', + 'sb': '-0b100', + 'sd': '-4', + 'si': '-0i4', + 'sv': '-0v4', + 'sw': '-0w4', + 'sx': '-0x4', }, { 'n': 5n, 'ub': '0b101', @@ -90,6 +111,12 @@ const INTEGER_TESTS: { 'uv': '0v5', 'uw': '0w5', 'ux': '0x5', + 'sb': '-0b11', + 'sd': '-3', + 'si': '-0i3', + 'sv': '-0v3', + 'sw': '-0w3', + 'sx': '-0x3', }, { 'n': 4n, 'ub': '0b100', @@ -98,6 +125,12 @@ const INTEGER_TESTS: { 'uv': '0v4', 'uw': '0w4', 'ux': '0x4', + 'sb': '--0b10', + 'sd': '--2', + 'si': '--0i2', + 'sv': '--0v2', + 'sw': '--0w2', + 'sx': '--0x2', }, { 'n': 171n, 'ub': '0b1010.1011', @@ -106,6 +139,12 @@ const INTEGER_TESTS: { 'uv': '0v5b', 'uw': '0w2H', 'ux': '0xab', + 'sb': '-0b101.0110', + 'sd': '-86', + 'si': '-0i86', + 'sv': '-0v2m', + 'sw': '-0w1m', + 'sx': '-0x56', }, { 'n': 53n, 'ub': '0b11.0101', @@ -114,6 +153,12 @@ const INTEGER_TESTS: { 'uv': '0v1l', 'uw': '0wR', 'ux': '0x35', + 'sb': '-0b1.1011', + 'sd': '-27', + 'si': '-0i27', + 'sv': '-0vr', + 'sw': '-0wr', + 'sx': '-0x1b', }, { 'n': 77n, 'ub': '0b100.1101', @@ -122,6 +167,12 @@ const INTEGER_TESTS: { 'uv': '0v2d', 'uw': '0w1d', 'ux': '0x4d', + 'sb': '-0b10.0111', + 'sd': '-39', + 'si': '-0i39', + 'sv': '-0v17', + 'sw': '-0wD', + 'sx': '-0x27', }, { 'n': 64491n, 'ub': '0b1111.1011.1110.1011', @@ -130,6 +181,12 @@ const INTEGER_TESTS: { 'uv': '0v1uvb', 'uw': '0wfLH', 'ux': '0xfbeb', + 'sb': '-0b111.1101.1111.0110', + 'sd': '-32.246', + 'si': '-0i32246', + 'sv': '-0vvfm', + 'sw': '-0w7TS', + 'sx': '-0x7df6', }, { 'n': 51765n, 'ub': '0b1100.1010.0011.0101', @@ -138,6 +195,12 @@ const INTEGER_TESTS: { 'uv': '0v1ihl', 'uw': '0wcER', 'ux': '0xca35', + 'sb': '-0b110.0101.0001.1011', + 'sd': '-25.883', + 'si': '-0i25883', + 'sv': '-0vp8r', + 'sw': '-0w6kr', + 'sx': '-0x651b', }, { 'n': 46444n, 'ub': '0b1011.0101.0110.1100', @@ -146,6 +209,12 @@ const INTEGER_TESTS: { 'uv': '0v1dbc', 'uw': '0wblI', 'ux': '0xb56c', + 'sb': '--0b101.1010.1011.0110', + 'sd': '--23.222', + 'si': '--0i23222', + 'sv': '--0vmlm', + 'sw': '--0w5GS', + 'sx': '--0x5ab6', }, { 'n': 384265565n, 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', @@ -154,6 +223,12 @@ const INTEGER_TESTS: { 'uv': '0vb.eerat', 'uw': '0wmVSRt', 'ux': '0x16e7.6d5d', + 'sb': '-0b1011.0111.0011.1011.0110.1010.1111', + 'sd': '-192.132.783', + 'si': '-0i192132783', + 'sv': '-0v5.n7dlf', + 'sw': '-0wbsXqL', + 'sx': '-0xb73.b6af', }, { 'n': 2456897374n, 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', @@ -162,6 +237,12 @@ const INTEGER_TESTS: { 'uv': '0v29.72hqu', 'uw': '0w2.isktu', 'ux': '0x9271.475e', + 'sb': '--0b100.1001.0011.1000.1010.0011.1010.1111', + 'sd': '--1.228.448.687', + 'si': '--0i1228448687', + 'sv': '--0v14.jh8tf', + 'sw': '--0w1.9eaeL', + 'sx': '--0x4938.a3af', }, { 'n': 38583115n, 'ub': '0b10.0100.1100.1011.1011.0100.1011', @@ -170,6 +251,12 @@ const INTEGER_TESTS: { 'uv': '0v1.4peqb', 'uw': '0w2jbJb', 'ux': '0x24c.bb4b', + 'sb': '-0b1.0010.0110.0101.1101.1010.0110', + 'sd': '-19.291.558', + 'si': '-0i19291558', + 'sv': '-0vicnd6', + 'sw': '-0w19BSC', + 'sx': '-0x126.5da6', }, { 'n': 13604104043154737885n, 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', @@ -178,6 +265,12 @@ const INTEGER_TESTS: { 'uv': '0vbpi.rship.r32mt', 'uw': '0wb.Pbv8O.PIoHt', 'ux': '0xbccb.7c8c.b3b1.8add', + 'sb': '-0b101.1110.0110.0101.1011.1110.0100.0110.0101.1001.1101.1000.1100.0101.0110.1111', + 'sd': '-6.802.052.021.577.368.943', + 'si': '-0i6802052021577368943', + 'sv': '-0v5sp.du8pc.thhbf', + 'sw': '-0w5.VBLAp.pSclL', + 'sx': '-0x5e65.be46.59d8.c56f', }, { 'n': 18441444580797368868n, 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', @@ -186,6 +279,12 @@ const INTEGER_TESTS: { 'uv': '0vfvr.9c4ej.nlih4', 'uw': '0wf.~Jb2e.DuIEA', 'ux': '0xffed.2c23.a77a.ca24', + 'sb': '--0b111.1111.1111.0110.1001.0110.0001.0001.1101.0011.1011.1101.0110.0101.0001.0010', + 'sd': '--9.220.722.290.398.684.434', + 'si': '--0i9220722290398684434', + 'sv': '--0v7vt.km279.rqp8i', + 'sw': '--0w7.~SBx7.jLmki', + 'sx': '--0x7ff6.9611.d3bd.6512', }, { 'n': 7643844662312245512n, 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', @@ -194,6 +293,12 @@ const INTEGER_TESTS: { 'uv': '0v6k5.3472r.m6688', 'uw': '0w6.Ekp3y.ToNA8', 'ux': '0x6a14.6438.b763.1908', + 'sb': '--0b11.0101.0000.1010.0011.0010.0001.1100.0101.1011.1011.0001.1000.1100.1000.0100', + 'sd': '--3.821.922.331.156.122.756', + 'si': '--0i3821922331156122756', + 'sv': '--0v3a2.hi3hd.r3344', + 'sw': '--0w3.kacxN.rIoO4', + 'sx': '--0x350a.321c.5bb1.8c84', }, { 'n': 293389376720547819362821033486028091527n, 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', @@ -202,6 +307,12 @@ const INTEGER_TESTS: { 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', + 'sb': '-0b110.1110.0101.1100.0101.1110.1000.1010.0110.1010.0111.1110.1101.1111.0001.1011.1100.1001.1110.0000.0100.0111.0010.0100.0011.1001.1101.0111.1101.1000.0100.0100', + 'sd': '-146.694.688.360.273.909.681.410.516.743.014.045.764', + 'si': '-0i146694688360273909681410516743014045764', + 'sv': '-0v3.ebhf8.kqjur.sdsjo.274gs.tfm24', + 'sw': '-0w1K.n5Waq.DXv6Y.DwhOg.VRZx4', + 'sx': '-0x6e5c.5e8a.6a7e.df1b.c9e0.4724.39d7.d844', }, { 'n': 11826418988767709295206418976840492314n, 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', @@ -210,6 +321,12 @@ const INTEGER_TESTS: { 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', + 'sb': '--0b100.0111.0010.1101.0111.1011.1100.1011.0110.0100.0110.1101.0000.1110.0001.1111.1000.0101.1000.1000.1101.0011.1001.1111.1000.1110.1011.0001.0010.1000.1101', + 'sd': '--5.913.209.494.383.854.647.603.209.488.420.246.157', + 'si': '--0i5913209494383854647603209488420246157', + 'sv': '--0v4ebbr.pdi6q.3gvgm.4d77s.em4kd', + 'sw': '--0w4.sJuYJ.ArgUv.xozjD.UWNad', + 'sx': '--0x472.d7bc.b646.d0e1.f858.8d39.f8eb.128d', }, { 'n': 75341289328899252391918368331716799250n, 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', @@ -218,6 +335,12 @@ const INTEGER_TESTS: { 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', + 'sb': '--0b1.1100.0101.0111.0001.1010.0010.1101.1001.1010.1010.0110.1110.0010.1010.1110.0010.0011.0100.1000.1001.1011.0010.0011.1011.1101.1001.0111.0101.0101.1000.1001', + 'sd': '--37.670.644.664.449.626.195.959.184.165.858.399.625', + 'si': '--0i37670644664449626195959184165858399625', + 'sv': '--0vsasd2.r6l6s.an26i.4r4eu.pelc9', + 'sw': '--0ws.lNEJC.GryHy.d8COe.ZBRm9', + 'sx': '--0x1c57.1a2d.9aa6.e2ae.2348.9b23.bd97.5589', }, ]; testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); From 1e30add3818e02a4600d7c3fd88dddab032b86f1 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 20:50:53 +0200 Subject: [PATCH 15/50] tests: add fuzz tests We disable auras for which fuzzing doesn't make sense, or that are known-unsupported. --- test/fuzz.test.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/fuzz.test.ts diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts new file mode 100644 index 0000000..47112d2 --- /dev/null +++ b/test/fuzz.test.ts @@ -0,0 +1,48 @@ +import parse, { aura } from "../src/parse"; +import render from "../src/render"; + +const testCount = 500; + +const auras: aura[] = [ + // 'c', //TODO known-broken parsing + 'da', + // 'dr', //TODO unsupported + // 'f', // limited legitimate values + // 'n', // limited legitimate values + 'p', + 'q', + 'sb', + 'sd', + 'si', + 'sv', + 'sw', + 'sx', + // 't', // stdlib also crashes on rendering arbitrary bytes + // 'ta', // + // 'tas', // + 'ub', + 'ud', + 'ui', + 'uv', + 'uw', + 'ux' +] + +function fuzz(nom: string, arr: Uint8Array | Uint16Array | Uint32Array | BigUint64Array) { + crypto.getRandomValues(arr); + auras.forEach((a) => { + describe(nom + ' @' + a, () => { + it('round-trips losslessly', () => { + arr.forEach((n) => { + n = BigInt(n); + expect(parse(a, render(a, n))).toEqual(n); + }); + }); + }); + }); +} + +fuzz('8-bit', new Uint8Array(testCount)); +fuzz('16-bit', new Uint16Array(testCount)); +fuzz('32-bit', new Uint32Array(testCount)); +fuzz('64-bit', new BigUint64Array(testCount)); From 3d2cac0889ff1a41717ff810245a19ff130402f2 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 21:24:19 +0200 Subject: [PATCH 16/50] parse: better utf-32 codepoint parsing support Still not quite up to parity with the stdlib parsing, but should support all sane cases. --- src/parse.ts | 8 ++++++-- test/fuzz.test.ts | 2 +- test/parse.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index faf199a..4d6a8f1 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -214,8 +214,12 @@ export function nuck(str: string): coin | null { return { type: 'dime', aura: 't', atom: stringToCord(decodeString(str.slice(2))) }; } else if (str[1] === '-' && regex['c'].test(str)) { - //TODO known-wrong! extract encoded byte-sequences and call +taft - // ...or just extract as single utf-32 character? + //TODO cheeky! this doesn't support the full range of inputs that the + // hoon stdlib supports, but should work for all sane inputs. + // no guarantees about behavior for insane inputs... + if (/^~\-~[0-9a-f]+\.$/.test(str)) { + return { type: 'dime', aura: 'c', atom: BigInt('0x' + str.slice(3, -1)) }; + } return { type: 'dime', aura: 'c', atom: stringToCord(decodeString(str.slice(2))) }; } } diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index 47112d2..6cdcb7e 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -4,7 +4,7 @@ import render from "../src/render"; const testCount = 500; const auras: aura[] = [ - // 'c', //TODO known-broken parsing + 'c', 'da', // 'dr', //TODO unsupported // 'f', // limited legitimate values diff --git a/test/parse.test.ts b/test/parse.test.ts index ce78ccb..9c83ca1 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -470,6 +470,30 @@ const TEXT_TESTS: { ]; testAuras('text', TEXT_AURAS, TEXT_TESTS); +const CHAR_AURAS: aura[] = ['c']; +const CHAR_TESTS: { n: bigint, c: string }[] = [ + { n: 129312n, c: '~-~1f920.' }, + { n: 128104n, c: '~-~1f468.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128103n, c: '~-~1f467.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128102n, c: '~-~1f466.' }, + { n: 97n, c: '~-a' }, + { n: 33n, c: '~-~21.' }, + { n: 32n, c: '~-.' }, + { n: 126n, c: '~-~~' }, + { n: 46n, c: '~-~.' }, + { n: 1548n, c: '~-~60c.' }, + // the cases below are deranged, because the input is deranged. + // @c represents utf-32 codepoints, so if you give it not-utf-32 + // it will render bogus, drop bytes in the rendering, etc. + // we include them (disabled) here to indicate that we don't have 100% + // exact stdlib parity here, but in practice that shouldn't matter. + // { n: 478560413032n, c: '~-~c6568.o' }, // 'hello' + // { n: 36762444129640n, c: '~-~c6568.~216f.' } // 'hello!' +]; +testAuras('chars', CHAR_AURAS, CHAR_TESTS); + describe('blob parsing', () => { it('parses', () => { expect(nuck('~02')).toEqual({ type: 'blob', jam: 2n }); From c1187c0a879bf2a2fba627365aef1a046f6a80b1 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 21:24:55 +0200 Subject: [PATCH 17/50] parse: no bytes should give 0n bigint Calling stringToCord() with an empty string will result in zero bytes going into bytesToBigint(), but that should just give you 0n. --- src/parse.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/parse.ts b/src/parse.ts index 4d6a8f1..801f88a 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -328,6 +328,7 @@ function stringToCord(str: string): bigint { //REVIEW should the reversal happen here or at callsites? depends on what endianness is idiomatic to js? function bytesToBigint(bytes: Uint8Array): bigint { + if (bytes.length === 0) return 0n; // if we have node's Buffer available, use it, it's wicked fast. // otherwise, constructing the hex string "by hand" and instantiating // a bigint from that is the fastest thing we can do. From 679f435c37c84e031706aa9f8d04c6f7c2b9ed74 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 22:16:18 +0200 Subject: [PATCH 18/50] render: support %blob coins Here, too, with the caveat that for full Noun support in the type you should refer to the nockjs wrapper. --- src/render.ts | 5 ++--- test/render.test.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/render.ts b/src/render.ts index 32ebc3c..1c0eb72 100644 --- a/src/render.ts +++ b/src/render.ts @@ -37,7 +37,7 @@ export type aura = 'c' | 'ux'; export type dime = { aura: aura, atom: bigint } export type coin = ({ type: 'dime' } & dime) - | { type: 'blob', noun: any } //TODO could do jam: bigint if we don't want nockjs dependency? + | { type: 'blob', jam: bigint } //NOTE nockjs for full noun | { type: 'many', list: coin[] } // render(): scot() @@ -52,13 +52,12 @@ export function scot(aura: aura, atom: bigint): string { export function rend(coin: coin): string { switch (coin.type) { case 'blob': - throw new Error('aura-js: %blob rendering unsupported'); //TODO + return '~0' + coin.jam.toString(32); case 'many': return '.' + coin.list.reduce((acc: string, item: coin) => { return acc + '_' + wack(rend(item)); }, '') + '__'; - throw new Error('aura-js: %many rendering unsupported'); //TODO case 'dime': switch(coin.aura[0]) { diff --git a/test/render.test.ts b/test/render.test.ts index e1f5abf..badb7a6 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -523,5 +523,9 @@ describe('%many coin rendering', () => { }); }); -//TODO render->parse roundtrip tests - +describe('blob rendering', () => { + it('parses', () => { + expect(rend({ type: 'blob', jam: 2n })).toEqual('~02'); + expect(rend({ type: 'blob', jam: 325350265702017n })).toEqual('~097su1g7hk1'); + }); +}); From f0122b2649d676275af3357a32b011223608babf Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 22:23:45 +0200 Subject: [PATCH 19/50] types: consolidate --- src/parse.ts | 29 ++--------------------------- src/render.ts | 31 ++----------------------------- src/types.ts | 28 ++++++++++++++++++++++++++++ test/fuzz.test.ts | 3 ++- test/parse.test.ts | 3 ++- test/render.test.ts | 3 ++- 6 files changed, 38 insertions(+), 59 deletions(-) create mode 100644 src/types.ts diff --git a/src/parse.ts b/src/parse.ts index 801f88a..0ab0d74 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -4,7 +4,7 @@ // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @r*, @if, @is +import { aura, dime, coin } from './types'; import { parseDa } from "./da"; import { isValidPatp, patp2bn } from "./p"; @@ -12,32 +12,7 @@ import { isValidPatq, patq2bn } from "./q"; import { parseUv } from "./uv"; import { parseUw } from "./uw"; -export type aura = 'c' - | 'da' - | 'dr' - | 'f' - | 'n' - | 'p' - | 'q' - | 'sb' - | 'sd' - | 'si' - | 'sv' - | 'sw' - | 'sx' - | 't' - | 'ta' - | 'tas' - | 'ub' - | 'ud' - | 'ui' - | 'uv' - | 'uw' - | 'ux'; -export type dime = { aura: aura, atom: bigint } -export type coin = ({ type: 'dime' } & dime) - | { type: 'blob', jam: bigint } //NOTE nockjs for full noun - | { type: 'many', list: coin[] } +//TODO unsupported auras: @r*, @if, @is function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; diff --git a/src/render.ts b/src/render.ts index 1c0eb72..2d0e842 100644 --- a/src/render.ts +++ b/src/render.ts @@ -4,41 +4,14 @@ // stdlib arm names are included for ease of cross-referencing. // +import { aura, coin } from './types'; + import { formatDa } from "./da"; import { patp } from "./p"; import { patq } from "./q"; import { formatUw } from "./uw"; //TODO unsupported auras: @r*, @if, @is -//TODO unsupported coins: %blob, %many - -//TODO dedupe with parse -export type aura = 'c' - | 'da' - | 'dr' - | 'f' - | 'n' - | 'p' - | 'q' - | 'sb' - | 'sd' - | 'si' - | 'sv' - | 'sw' - | 'sx' - | 't' - | 'ta' - | 'tas' - | 'ub' - | 'ud' - | 'ui' - | 'uv' - | 'uw' - | 'ux'; -export type dime = { aura: aura, atom: bigint } -export type coin = ({ type: 'dime' } & dime) - | { type: 'blob', jam: bigint } //NOTE nockjs for full noun - | { type: 'many', list: coin[] } // render(): scot() // scot(): render atom as specific aura diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0f32fc6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,28 @@ +export type aura = 'c' + | 'da' + | 'dr' + | 'f' + | 'n' + | 'p' + | 'q' + | 'sb' + | 'sd' + | 'si' + | 'sv' + | 'sw' + | 'sx' + | 't' + | 'ta' + | 'tas' + | 'ub' + | 'ud' + | 'ui' + | 'uv' + | 'uw' + | 'ux'; + +export type dime = { aura: aura, atom: bigint } + +export type coin = ({ type: 'dime' } & dime) + | { type: 'blob', jam: bigint } //NOTE nockjs for full noun + | { type: 'many', list: coin[] } diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index 6cdcb7e..a3a329b 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -1,4 +1,5 @@ -import parse, { aura } from "../src/parse"; +import { aura } from '../src/types'; +import parse from "../src/parse"; import render from "../src/render"; const testCount = 500; diff --git a/test/parse.test.ts b/test/parse.test.ts index 9c83ca1..3911c56 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,4 +1,5 @@ -import { parse, aura, decodeString, nuck, regex } from '../src/parse'; +import { aura } from '../src/types'; +import { parse, decodeString, nuck, regex } from '../src/parse'; // most test cases generated from snippets similar to the following: // diff --git a/test/render.test.ts b/test/render.test.ts index badb7a6..f70ed1e 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -1,4 +1,5 @@ -import { render, aura, coin, rend } from '../src/render'; +import { aura, coin } from '../src/types'; +import { render, rend } from '../src/render'; // most test cases generated from snippets similar to the following: // From b99b9f04acb4436fdecd9f7f19f50cb9f273b986 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 22:47:36 +0200 Subject: [PATCH 20/50] tests: consolidate static test atoms --- test/data/atoms.ts | 460 ++++++++++++++++++++++++++++++++++++++++++++ test/parse.test.ts | 435 ++--------------------------------------- test/render.test.ts | 458 +------------------------------------------ 3 files changed, 483 insertions(+), 870 deletions(-) create mode 100644 test/data/atoms.ts diff --git a/test/data/atoms.ts b/test/data/atoms.ts new file mode 100644 index 0000000..c567163 --- /dev/null +++ b/test/data/atoms.ts @@ -0,0 +1,460 @@ +import { aura } from '../../src/types'; + +// most test cases generated from snippets similar to the following: +// +// =| n=@ud +// ^- (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q]) +// %- zing +// |- ^- (list (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q])) +// ?: (gth n 5) ~ +// :_ $(n +(n), eny (shaz +(eny))) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// [. . . . . . .]~:(end 2^(bex n) eny) +// + +export const INTEGER_AURAS: aura[] = [ + 'ub', 'ud', 'ui', 'uv', 'uw', 'ux', + 'sb', 'sd', 'si', 'sv', 'sw', 'sx', +]; +export const INTEGER_TESTS: { + n: bigint, + ub: string, + ud: string, + ui: string, + uv: string, + uw: string, + ux: string, + sb: string, + sd: string, + si: string, + sv: string, + sw: string, + sx: string, +}[] = [ + { 'n': 0n, + 'ub': '0b0', + 'ud': '0', + 'ui': '0i0', + 'uv': '0v0', + 'uw': '0w0', + 'ux': '0x0', + 'sb': '--0b0', + 'sd': '--0', + 'si': '--0i0', + 'sv': '--0v0', + 'sw': '--0w0', + 'sx': '--0x0', + }, + { 'n': 7n, + 'ub': '0b111', + 'ud': '7', + 'ui': '0i7', + 'uv': '0v7', + 'uw': '0w7', + 'ux': '0x7', + 'sb': '-0b100', + 'sd': '-4', + 'si': '-0i4', + 'sv': '-0v4', + 'sw': '-0w4', + 'sx': '-0x4', + }, + { 'n': 5n, + 'ub': '0b101', + 'ud': '5', + 'ui': '0i5', + 'uv': '0v5', + 'uw': '0w5', + 'ux': '0x5', + 'sb': '-0b11', + 'sd': '-3', + 'si': '-0i3', + 'sv': '-0v3', + 'sw': '-0w3', + 'sx': '-0x3', + }, + { 'n': 4n, + 'ub': '0b100', + 'ud': '4', + 'ui': '0i4', + 'uv': '0v4', + 'uw': '0w4', + 'ux': '0x4', + 'sb': '--0b10', + 'sd': '--2', + 'si': '--0i2', + 'sv': '--0v2', + 'sw': '--0w2', + 'sx': '--0x2', + }, + { 'n': 171n, + 'ub': '0b1010.1011', + 'ud': '171', + 'ui': '0i171', + 'uv': '0v5b', + 'uw': '0w2H', + 'ux': '0xab', + 'sb': '-0b101.0110', + 'sd': '-86', + 'si': '-0i86', + 'sv': '-0v2m', + 'sw': '-0w1m', + 'sx': '-0x56', + }, + { 'n': 53n, + 'ub': '0b11.0101', + 'ud': '53', + 'ui': '0i53', + 'uv': '0v1l', + 'uw': '0wR', + 'ux': '0x35', + 'sb': '-0b1.1011', + 'sd': '-27', + 'si': '-0i27', + 'sv': '-0vr', + 'sw': '-0wr', + 'sx': '-0x1b', + }, + { 'n': 77n, + 'ub': '0b100.1101', + 'ud': '77', + 'ui': '0i77', + 'uv': '0v2d', + 'uw': '0w1d', + 'ux': '0x4d', + 'sb': '-0b10.0111', + 'sd': '-39', + 'si': '-0i39', + 'sv': '-0v17', + 'sw': '-0wD', + 'sx': '-0x27', + }, + { 'n': 64491n, + 'ub': '0b1111.1011.1110.1011', + 'ud': '64.491', + 'ui': '0i64491', + 'uv': '0v1uvb', + 'uw': '0wfLH', + 'ux': '0xfbeb', + 'sb': '-0b111.1101.1111.0110', + 'sd': '-32.246', + 'si': '-0i32246', + 'sv': '-0vvfm', + 'sw': '-0w7TS', + 'sx': '-0x7df6', + }, + { 'n': 51765n, + 'ub': '0b1100.1010.0011.0101', + 'ud': '51.765', + 'ui': '0i51765', + 'uv': '0v1ihl', + 'uw': '0wcER', + 'ux': '0xca35', + 'sb': '-0b110.0101.0001.1011', + 'sd': '-25.883', + 'si': '-0i25883', + 'sv': '-0vp8r', + 'sw': '-0w6kr', + 'sx': '-0x651b', + }, + { 'n': 46444n, + 'ub': '0b1011.0101.0110.1100', + 'ud': '46.444', + 'ui': '0i46444', + 'uv': '0v1dbc', + 'uw': '0wblI', + 'ux': '0xb56c', + 'sb': '--0b101.1010.1011.0110', + 'sd': '--23.222', + 'si': '--0i23222', + 'sv': '--0vmlm', + 'sw': '--0w5GS', + 'sx': '--0x5ab6', + }, + { 'n': 384265565n, + 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', + 'ud': '384.265.565', + 'ui': '0i384265565', + 'uv': '0vb.eerat', + 'uw': '0wmVSRt', + 'ux': '0x16e7.6d5d', + 'sb': '-0b1011.0111.0011.1011.0110.1010.1111', + 'sd': '-192.132.783', + 'si': '-0i192132783', + 'sv': '-0v5.n7dlf', + 'sw': '-0wbsXqL', + 'sx': '-0xb73.b6af', + }, + { 'n': 2456897374n, + 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', + 'ud': '2.456.897.374', + 'ui': '0i2456897374', + 'uv': '0v29.72hqu', + 'uw': '0w2.isktu', + 'ux': '0x9271.475e', + 'sb': '--0b100.1001.0011.1000.1010.0011.1010.1111', + 'sd': '--1.228.448.687', + 'si': '--0i1228448687', + 'sv': '--0v14.jh8tf', + 'sw': '--0w1.9eaeL', + 'sx': '--0x4938.a3af', + }, + { 'n': 38583115n, + 'ub': '0b10.0100.1100.1011.1011.0100.1011', + 'ud': '38.583.115', + 'ui': '0i38583115', + 'uv': '0v1.4peqb', + 'uw': '0w2jbJb', + 'ux': '0x24c.bb4b', + 'sb': '-0b1.0010.0110.0101.1101.1010.0110', + 'sd': '-19.291.558', + 'si': '-0i19291558', + 'sv': '-0vicnd6', + 'sw': '-0w19BSC', + 'sx': '-0x126.5da6', + }, + { 'n': 13604104043154737885n, + 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', + 'ud': '13.604.104.043.154.737.885', + 'ui': '0i13604104043154737885', + 'uv': '0vbpi.rship.r32mt', + 'uw': '0wb.Pbv8O.PIoHt', + 'ux': '0xbccb.7c8c.b3b1.8add', + 'sb': '-0b101.1110.0110.0101.1011.1110.0100.0110.0101.1001.1101.1000.1100.0101.0110.1111', + 'sd': '-6.802.052.021.577.368.943', + 'si': '-0i6802052021577368943', + 'sv': '-0v5sp.du8pc.thhbf', + 'sw': '-0w5.VBLAp.pSclL', + 'sx': '-0x5e65.be46.59d8.c56f', + }, + { 'n': 18441444580797368868n, + 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', + 'ud': '18.441.444.580.797.368.868', + 'ui': '0i18441444580797368868', + 'uv': '0vfvr.9c4ej.nlih4', + 'uw': '0wf.~Jb2e.DuIEA', + 'ux': '0xffed.2c23.a77a.ca24', + 'sb': '--0b111.1111.1111.0110.1001.0110.0001.0001.1101.0011.1011.1101.0110.0101.0001.0010', + 'sd': '--9.220.722.290.398.684.434', + 'si': '--0i9220722290398684434', + 'sv': '--0v7vt.km279.rqp8i', + 'sw': '--0w7.~SBx7.jLmki', + 'sx': '--0x7ff6.9611.d3bd.6512', + }, + { 'n': 7643844662312245512n, + 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', + 'ud': '7.643.844.662.312.245.512', + 'ui': '0i7643844662312245512', + 'uv': '0v6k5.3472r.m6688', + 'uw': '0w6.Ekp3y.ToNA8', + 'ux': '0x6a14.6438.b763.1908', + 'sb': '--0b11.0101.0000.1010.0011.0010.0001.1100.0101.1011.1011.0001.1000.1100.1000.0100', + 'sd': '--3.821.922.331.156.122.756', + 'si': '--0i3821922331156122756', + 'sv': '--0v3a2.hi3hd.r3344', + 'sw': '--0w3.kacxN.rIoO4', + 'sx': '--0x350a.321c.5bb1.8c84', + }, + { 'n': 293389376720547819362821033486028091527n, + 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', + 'ud': '293.389.376.720.547.819.362.821.033.486.028.091.527', + 'ui': '0i293389376720547819362821033486028091527', + 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', + 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', + 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', + 'sb': '-0b110.1110.0101.1100.0101.1110.1000.1010.0110.1010.0111.1110.1101.1111.0001.1011.1100.1001.1110.0000.0100.0111.0010.0100.0011.1001.1101.0111.1101.1000.0100.0100', + 'sd': '-146.694.688.360.273.909.681.410.516.743.014.045.764', + 'si': '-0i146694688360273909681410516743014045764', + 'sv': '-0v3.ebhf8.kqjur.sdsjo.274gs.tfm24', + 'sw': '-0w1K.n5Waq.DXv6Y.DwhOg.VRZx4', + 'sx': '-0x6e5c.5e8a.6a7e.df1b.c9e0.4724.39d7.d844', + }, + { 'n': 11826418988767709295206418976840492314n, + 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', + 'ud': '11.826.418.988.767.709.295.206.418.976.840.492.314', + 'ui': '0i11826418988767709295206418976840492314', + 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', + 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', + 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', + 'sb': '--0b100.0111.0010.1101.0111.1011.1100.1011.0110.0100.0110.1101.0000.1110.0001.1111.1000.0101.1000.1000.1101.0011.1001.1111.1000.1110.1011.0001.0010.1000.1101', + 'sd': '--5.913.209.494.383.854.647.603.209.488.420.246.157', + 'si': '--0i5913209494383854647603209488420246157', + 'sv': '--0v4ebbr.pdi6q.3gvgm.4d77s.em4kd', + 'sw': '--0w4.sJuYJ.ArgUv.xozjD.UWNad', + 'sx': '--0x472.d7bc.b646.d0e1.f858.8d39.f8eb.128d', + }, + { 'n': 75341289328899252391918368331716799250n, + 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', + 'ud': '75.341.289.328.899.252.391.918.368.331.716.799.250', + 'ui': '0i75341289328899252391918368331716799250', + 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', + 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', + 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', + 'sb': '--0b1.1100.0101.0111.0001.1010.0010.1101.1001.1010.1010.0110.1110.0010.1010.1110.0010.0011.0100.1000.1001.1011.0010.0011.1011.1101.1001.0111.0101.0101.1000.1001', + 'sd': '--37.670.644.664.449.626.195.959.184.165.858.399.625', + 'si': '--0i37670644664449626195959184165858399625', + 'sv': '--0vsasd2.r6l6s.an26i.4r4eu.pelc9', + 'sw': '--0ws.lNEJC.GryHy.d8COe.ZBRm9', + 'sx': '--0x1c57.1a2d.9aa6.e2ae.2348.9b23.bd97.5589', + }, +]; + +export const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; +export const PHONETIC_TESTS: { + n: bigint, + p: string, + q: string +}[] = [ + { 'n': 7n, 'p': '~let', 'q': '.~let' }, + { 'n': 0n, 'p': '~zod', 'q': '.~zod' }, + { 'n': 8n, 'p': '~ful', 'q': '.~ful' }, + { 'n': 117n, 'p': '~deg', 'q': '.~deg' }, + { 'n': 83n, 'p': '~tev', 'q': '.~tev' }, + { 'n': 223n, 'p': '~lud', 'q': '.~lud' }, + { 'n': 39995n, 'p': '~hapwyc', 'q': '.~hapwyc' }, + { 'n': 50426n, 'p': '~mitrep', 'q': '.~mitrep' }, + { 'n': 11415n, 'p': '~torryx', 'q': '.~torryx' }, + { 'n': 1863930458n, 'p': '~mogteg-botfex', 'q': '.~ligput-motfus' }, + { 'n': 3284934632n, 'p': '~loplet-nosnyx', 'q': '.~fasryd-mirlyn' }, + { 'n': 3833668n, 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, + { 'n': 9260427482306755094n, + 'p': '~lasrum-pindyt-nimnym-fotmeg', + 'q': '.~lasrum-pindyt-tadtem-lodlup', + }, + { 'n': 6363574354411289343n, + 'p': '~nopnet-rostem-navteb-fodbep', + 'q': '.~nopnet-rostem-nimfel-monfes', + }, + { 'n': 17571387016818844998n, + 'p': '~namler-folwet-bictes-wormec', + 'q': '.~namler-folwet-samwet-sarrul', + }, + { 'n': 241760151623976361741451001031931477015n, + 'p': '~dablys-minwed-mosreb-mictyn--nostyv-nimdul-hanbyl-bisdep', + 'q': '.~dablys-minwed-mosreb-mictyn-nostyv-nimdul-hanbyl-bisdep', + }, + { 'n': 148310954517291502180858368907816435627n, + 'p': '~ligryn-lomnem-fintes-davsyr--pacdel-wolpex-ripdev-paldeb', + 'q': '.~ligryn-lomnem-fintes-davsyr-pacdel-wolpex-ripdev-paldeb', + }, + { 'n': 97100713129464593177912155425728457718n, + 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', + 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', + }, + // with zero bytes + { 'n': 3833668n, + 'p': '~nidmes-samrut', + 'q': '.~sef-palsub' + }, + { 'n': 319478973361751151n, + 'p': '~sampel-sampel-lacwyl-tirder', + 'q': '.~sampel-sampel-dozpel-sampel', + }, + { 'n': 319478973354476655n, + 'p': '~sampel-sampel-dozzod-sampel', + 'q': '.~sampel-sampel-dozzod-sampel', + }, +]; + +export const DATE_AURAS: aura[] = [ 'da' ]; +export const DATE_TESTS: { + n: bigint, + da: string +}[] = [ + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.1.1' + }, + { 'n': 170141182164706681340023325049697075200n, + 'da': '~2000-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170213050367437966468743593413155225600n, + 'da': '~123456789.12.12' + }, + { 'n': 170141184507170056208381036660470579200n, + 'da': '~2025.1.1..01.00.00' + }, + { 'n': 170141184507169989800102652781061472256n, + 'da': '~2025.1.1..00.00.00..0001' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..07.07.07' + }, + { 'n': 170141184492616163050404573632566132736n, + 'da': '~2000.1.1..11.11.11' + }, + { 'n': 170141184492616163050404761352701739008n, + 'da': '~2000.1.1..11.11.11..0000.aabb' + }, + { 'n': 170141184492616163062707000439658774528n, + 'da': '~2000.1.1..11.11.11..aabb' + }, + { 'n': 170141184492615487727406687186543706111n, + 'da': '~2000.1.1..01.01.01..aabb.ccdd.eeff.ffff' + } +]; + +export const TEXT_AURAS: aura[] = [ 't' ]; +export const TEXT_TESTS: { + n: bigint, + t: string, + ta: string, + tas: string, +}[] = [ + { n: 0n, + tas: '', + ta: '~.', + t: '~~' + }, + { n: 97n, + tas: 'a', + ta: '~.a', + t: '~~a' + }, + { n: 121404708502375659064812904n, + tas: 'hello-world', + ta: '~.hello-world', + t: '~~hello-world' + }, + { n: 10334410032597741434076685640n, + tas: 'Hello World!', + ta: '~.Hello World!', + t: '~~~48.ello.~57.orld~21.' + }, + { n: 19287354765356410753152687274314809137704681783870144927392762099049413935995106n, + tas: 'โ˜…๐Ÿค yeeุŒhaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + ta: '~.โ˜…๐Ÿค yeeุŒhaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + t: '~~~2605.~1f920.yee~60c.haw~1f468.~200d.~1f467.~200d.~1f466.' + } +]; + +export const CHAR_AURAS: aura[] = [ 'c' ]; +export const CHAR_TESTS: { n: bigint, c: string }[] = [ + { n: 129312n, c: '~-~1f920.' }, + { n: 128104n, c: '~-~1f468.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128103n, c: '~-~1f467.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128102n, c: '~-~1f466.' }, + { n: 97n, c: '~-a' }, + { n: 33n, c: '~-~21.' }, + { n: 32n, c: '~-.' }, + { n: 126n, c: '~-~~' }, + { n: 46n, c: '~-~.' }, + { n: 1548n, c: '~-~60c.' }, + // the cases below are deranged, because the input is deranged. + // @c represents utf-32 codepoints, so if you give it not-utf-32 + // it will render bogus, drop bytes in the rendering, etc. + // we include them (disabled) here to indicate that we don't have 100% + // exact stdlib parity, but in practice that shouldn't matter. + // { n: 478560413032n, c: '~-~c6568.o' }, // 'hello' + // { n: 36762444129640n, c: '~-~c6568.~216f.' } // 'hello!' +]; diff --git a/test/parse.test.ts b/test/parse.test.ts index 3911c56..0958aa7 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,20 +1,11 @@ import { aura } from '../src/types'; import { parse, decodeString, nuck, regex } from '../src/parse'; - -// most test cases generated from snippets similar to the following: -// -// =| n=@ud -// ^- (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q]) -// %- zing -// |- ^- (list (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q])) -// ?: (gth n 5) ~ -// :_ $(n +(n), eny (shaz +(eny))) -// :- [. . . . . . .]:(end 2^(bex n) eny) -// =. eny (shaz eny) -// :- [. . . . . . .]:(end 2^(bex n) eny) -// =. eny (shaz eny) -// [. . . . . . .]~:(end 2^(bex n) eny) -// +import { INTEGER_AURAS, INTEGER_TESTS, + PHONETIC_AURAS, PHONETIC_TESTS, + DATE_AURAS, DATE_TESTS, + TEXT_AURAS, TEXT_TESTS, + CHAR_AURAS, CHAR_TESTS, + } from './data/atoms'; //TODO test for parse failures: leading zeroes, date out of range, etc //TODO test for non-standard-but-accepted cases: leading 0 in hex chars, weird dates, etc. @@ -58,361 +49,17 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { }); } -const INTEGER_AURAS: aura[] = [ - 'ub', 'ud', 'ui', 'uv', 'uw', 'ux', - 'sb', 'sd', 'si', 'sv', 'sw', 'sx', -]; -const INTEGER_TESTS: { - n: bigint, - ub: string, - ud: string, - ui: string, - uv: string, - uw: string, - ux: string, - sb: string, - sd: string, - si: string, - sv: string, - sw: string, - sx: string, -}[] = [ - { 'n': 0n, - 'ub': '0b0', - 'ud': '0', - 'ui': '0i0', - 'uv': '0v0', - 'uw': '0w0', - 'ux': '0x0', - 'sb': '--0b0', - 'sd': '--0', - 'si': '--0i0', - 'sv': '--0v0', - 'sw': '--0w0', - 'sx': '--0x0', - }, - { 'n': 7n, - 'ub': '0b111', - 'ud': '7', - 'ui': '0i7', - 'uv': '0v7', - 'uw': '0w7', - 'ux': '0x7', - 'sb': '-0b100', - 'sd': '-4', - 'si': '-0i4', - 'sv': '-0v4', - 'sw': '-0w4', - 'sx': '-0x4', - }, - { 'n': 5n, - 'ub': '0b101', - 'ud': '5', - 'ui': '0i5', - 'uv': '0v5', - 'uw': '0w5', - 'ux': '0x5', - 'sb': '-0b11', - 'sd': '-3', - 'si': '-0i3', - 'sv': '-0v3', - 'sw': '-0w3', - 'sx': '-0x3', - }, - { 'n': 4n, - 'ub': '0b100', - 'ud': '4', - 'ui': '0i4', - 'uv': '0v4', - 'uw': '0w4', - 'ux': '0x4', - 'sb': '--0b10', - 'sd': '--2', - 'si': '--0i2', - 'sv': '--0v2', - 'sw': '--0w2', - 'sx': '--0x2', - }, - { 'n': 171n, - 'ub': '0b1010.1011', - 'ud': '171', - 'ui': '0i171', - 'uv': '0v5b', - 'uw': '0w2H', - 'ux': '0xab', - 'sb': '-0b101.0110', - 'sd': '-86', - 'si': '-0i86', - 'sv': '-0v2m', - 'sw': '-0w1m', - 'sx': '-0x56', - }, - { 'n': 53n, - 'ub': '0b11.0101', - 'ud': '53', - 'ui': '0i53', - 'uv': '0v1l', - 'uw': '0wR', - 'ux': '0x35', - 'sb': '-0b1.1011', - 'sd': '-27', - 'si': '-0i27', - 'sv': '-0vr', - 'sw': '-0wr', - 'sx': '-0x1b', - }, - { 'n': 77n, - 'ub': '0b100.1101', - 'ud': '77', - 'ui': '0i77', - 'uv': '0v2d', - 'uw': '0w1d', - 'ux': '0x4d', - 'sb': '-0b10.0111', - 'sd': '-39', - 'si': '-0i39', - 'sv': '-0v17', - 'sw': '-0wD', - 'sx': '-0x27', - }, - { 'n': 64491n, - 'ub': '0b1111.1011.1110.1011', - 'ud': '64.491', - 'ui': '0i64491', - 'uv': '0v1uvb', - 'uw': '0wfLH', - 'ux': '0xfbeb', - 'sb': '-0b111.1101.1111.0110', - 'sd': '-32.246', - 'si': '-0i32246', - 'sv': '-0vvfm', - 'sw': '-0w7TS', - 'sx': '-0x7df6', - }, - { 'n': 51765n, - 'ub': '0b1100.1010.0011.0101', - 'ud': '51.765', - 'ui': '0i51765', - 'uv': '0v1ihl', - 'uw': '0wcER', - 'ux': '0xca35', - 'sb': '-0b110.0101.0001.1011', - 'sd': '-25.883', - 'si': '-0i25883', - 'sv': '-0vp8r', - 'sw': '-0w6kr', - 'sx': '-0x651b', - }, - { 'n': 46444n, - 'ub': '0b1011.0101.0110.1100', - 'ud': '46.444', - 'ui': '0i46444', - 'uv': '0v1dbc', - 'uw': '0wblI', - 'ux': '0xb56c', - 'sb': '--0b101.1010.1011.0110', - 'sd': '--23.222', - 'si': '--0i23222', - 'sv': '--0vmlm', - 'sw': '--0w5GS', - 'sx': '--0x5ab6', - }, - { 'n': 384265565n, - 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', - 'ud': '384.265.565', - 'ui': '0i384265565', - 'uv': '0vb.eerat', - 'uw': '0wmVSRt', - 'ux': '0x16e7.6d5d', - 'sb': '-0b1011.0111.0011.1011.0110.1010.1111', - 'sd': '-192.132.783', - 'si': '-0i192132783', - 'sv': '-0v5.n7dlf', - 'sw': '-0wbsXqL', - 'sx': '-0xb73.b6af', - }, - { 'n': 2456897374n, - 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', - 'ud': '2.456.897.374', - 'ui': '0i2456897374', - 'uv': '0v29.72hqu', - 'uw': '0w2.isktu', - 'ux': '0x9271.475e', - 'sb': '--0b100.1001.0011.1000.1010.0011.1010.1111', - 'sd': '--1.228.448.687', - 'si': '--0i1228448687', - 'sv': '--0v14.jh8tf', - 'sw': '--0w1.9eaeL', - 'sx': '--0x4938.a3af', - }, - { 'n': 38583115n, - 'ub': '0b10.0100.1100.1011.1011.0100.1011', - 'ud': '38.583.115', - 'ui': '0i38583115', - 'uv': '0v1.4peqb', - 'uw': '0w2jbJb', - 'ux': '0x24c.bb4b', - 'sb': '-0b1.0010.0110.0101.1101.1010.0110', - 'sd': '-19.291.558', - 'si': '-0i19291558', - 'sv': '-0vicnd6', - 'sw': '-0w19BSC', - 'sx': '-0x126.5da6', - }, - { 'n': 13604104043154737885n, - 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', - 'ud': '13.604.104.043.154.737.885', - 'ui': '0i13604104043154737885', - 'uv': '0vbpi.rship.r32mt', - 'uw': '0wb.Pbv8O.PIoHt', - 'ux': '0xbccb.7c8c.b3b1.8add', - 'sb': '-0b101.1110.0110.0101.1011.1110.0100.0110.0101.1001.1101.1000.1100.0101.0110.1111', - 'sd': '-6.802.052.021.577.368.943', - 'si': '-0i6802052021577368943', - 'sv': '-0v5sp.du8pc.thhbf', - 'sw': '-0w5.VBLAp.pSclL', - 'sx': '-0x5e65.be46.59d8.c56f', - }, - { 'n': 18441444580797368868n, - 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', - 'ud': '18.441.444.580.797.368.868', - 'ui': '0i18441444580797368868', - 'uv': '0vfvr.9c4ej.nlih4', - 'uw': '0wf.~Jb2e.DuIEA', - 'ux': '0xffed.2c23.a77a.ca24', - 'sb': '--0b111.1111.1111.0110.1001.0110.0001.0001.1101.0011.1011.1101.0110.0101.0001.0010', - 'sd': '--9.220.722.290.398.684.434', - 'si': '--0i9220722290398684434', - 'sv': '--0v7vt.km279.rqp8i', - 'sw': '--0w7.~SBx7.jLmki', - 'sx': '--0x7ff6.9611.d3bd.6512', - }, - { 'n': 7643844662312245512n, - 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', - 'ud': '7.643.844.662.312.245.512', - 'ui': '0i7643844662312245512', - 'uv': '0v6k5.3472r.m6688', - 'uw': '0w6.Ekp3y.ToNA8', - 'ux': '0x6a14.6438.b763.1908', - 'sb': '--0b11.0101.0000.1010.0011.0010.0001.1100.0101.1011.1011.0001.1000.1100.1000.0100', - 'sd': '--3.821.922.331.156.122.756', - 'si': '--0i3821922331156122756', - 'sv': '--0v3a2.hi3hd.r3344', - 'sw': '--0w3.kacxN.rIoO4', - 'sx': '--0x350a.321c.5bb1.8c84', - }, - { 'n': 293389376720547819362821033486028091527n, - 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', - 'ud': '293.389.376.720.547.819.362.821.033.486.028.091.527', - 'ui': '0i293389376720547819362821033486028091527', - 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', - 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', - 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', - 'sb': '-0b110.1110.0101.1100.0101.1110.1000.1010.0110.1010.0111.1110.1101.1111.0001.1011.1100.1001.1110.0000.0100.0111.0010.0100.0011.1001.1101.0111.1101.1000.0100.0100', - 'sd': '-146.694.688.360.273.909.681.410.516.743.014.045.764', - 'si': '-0i146694688360273909681410516743014045764', - 'sv': '-0v3.ebhf8.kqjur.sdsjo.274gs.tfm24', - 'sw': '-0w1K.n5Waq.DXv6Y.DwhOg.VRZx4', - 'sx': '-0x6e5c.5e8a.6a7e.df1b.c9e0.4724.39d7.d844', - }, - { 'n': 11826418988767709295206418976840492314n, - 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', - 'ud': '11.826.418.988.767.709.295.206.418.976.840.492.314', - 'ui': '0i11826418988767709295206418976840492314', - 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', - 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', - 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', - 'sb': '--0b100.0111.0010.1101.0111.1011.1100.1011.0110.0100.0110.1101.0000.1110.0001.1111.1000.0101.1000.1000.1101.0011.1001.1111.1000.1110.1011.0001.0010.1000.1101', - 'sd': '--5.913.209.494.383.854.647.603.209.488.420.246.157', - 'si': '--0i5913209494383854647603209488420246157', - 'sv': '--0v4ebbr.pdi6q.3gvgm.4d77s.em4kd', - 'sw': '--0w4.sJuYJ.ArgUv.xozjD.UWNad', - 'sx': '--0x472.d7bc.b646.d0e1.f858.8d39.f8eb.128d', - }, - { 'n': 75341289328899252391918368331716799250n, - 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', - 'ud': '75.341.289.328.899.252.391.918.368.331.716.799.250', - 'ui': '0i75341289328899252391918368331716799250', - 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', - 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', - 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', - 'sb': '--0b1.1100.0101.0111.0001.1010.0010.1101.1001.1010.1010.0110.1110.0010.1010.1110.0010.0011.0100.1000.1001.1011.0010.0011.1011.1101.1001.0111.0101.0101.1000.1001', - 'sd': '--37.670.644.664.449.626.195.959.184.165.858.399.625', - 'si': '--0i37670644664449626195959184165858399625', - 'sv': '--0vsasd2.r6l6s.an26i.4r4eu.pelc9', - 'sw': '--0ws.lNEJC.GryHy.d8COe.ZBRm9', - 'sx': '--0x1c57.1a2d.9aa6.e2ae.2348.9b23.bd97.5589', - }, -]; -testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); - -// test cases generated in similar fashion as the integer aura tests - -const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; -const PHONETIC_TESTS: { - n: bigint, - p: string, - q: string -}[] = [ - { 'n': 7n, 'p': '~let', 'q': '.~let' }, - { 'n': 0n, 'p': '~zod', 'q': '.~zod' }, - { 'n': 8n, 'p': '~ful', 'q': '.~ful' }, - { 'n': 117n, 'p': '~deg', 'q': '.~deg' }, - { 'n': 83n, 'p': '~tev', 'q': '.~tev' }, - { 'n': 223n, 'p': '~lud', 'q': '.~lud' }, - { 'n': 39995n, 'p': '~hapwyc', 'q': '.~hapwyc' }, - { 'n': 50426n, 'p': '~mitrep', 'q': '.~mitrep' }, - { 'n': 11415n, 'p': '~torryx', 'q': '.~torryx' }, - { 'n': 1863930458n, 'p': '~mogteg-botfex', 'q': '.~ligput-motfus' }, - { 'n': 3284934632n, 'p': '~loplet-nosnyx', 'q': '.~fasryd-mirlyn' }, - { 'n': 3833668n, 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, - { 'n': 9260427482306755094n, - 'p': '~lasrum-pindyt-nimnym-fotmeg', - 'q': '.~lasrum-pindyt-tadtem-lodlup', - }, - { 'n': 6363574354411289343n, - 'p': '~nopnet-rostem-navteb-fodbep', - 'q': '.~nopnet-rostem-nimfel-monfes', - }, - { 'n': 17571387016818844998n, - 'p': '~namler-folwet-bictes-wormec', - 'q': '.~namler-folwet-samwet-sarrul', - }, - { 'n': 241760151623976361741451001031931477015n, - 'p': '~dablys-minwed-mosreb-mictyn--nostyv-nimdul-hanbyl-bisdep', - 'q': '.~dablys-minwed-mosreb-mictyn-nostyv-nimdul-hanbyl-bisdep', - }, - { 'n': 148310954517291502180858368907816435627n, - 'p': '~ligryn-lomnem-fintes-davsyr--pacdel-wolpex-ripdev-paldeb', - 'q': '.~ligryn-lomnem-fintes-davsyr-pacdel-wolpex-ripdev-paldeb', - }, - { 'n': 97100713129464593177912155425728457718n, - 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', - 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', - }, -]; -testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); - -const DATE_AURAS: aura[] = [ 'da' ]; -const DATE_TESTS: { +//NOTE we add additional tests to the data: +// parsing must be liberal and accept formats +// which the renderer would never produce +const OUR_DATE_TESTS: { n: bigint, da: string }[] = [ - { 'n': 170141184492615420181573981275213004800n, - 'da': '~2000.1.1' - }, - { 'n': 170141182164706681340023325049697075200n, - 'da': '~2000-.1.1' - }, + ...DATE_TESTS, { 'n': 170141183328369385600900416699944140800n, 'da': '~0.1.1' }, - { 'n': 170141183328369385600900416699944140800n, - 'da': '~1-.1.1' - }, - { 'n': 170213050367437966468743593413155225600n, - 'da': '~123456789.12.12' - }, { 'n': 170141184492712641901540060096049971200n, 'da': '~2000.2.31' }, @@ -428,20 +75,16 @@ const DATE_TESTS: { { 'n': 170141184492616163050404573632566132736n, 'da': '~2000.1.1..11.11.11..0000' }, - { 'n': 170141184492616163050404761352701739008n, - 'da': '~2000.1.1..11.11.11..0000.aabb' - }, { 'n': 170141184492616163062707000439658774528n, 'da': '~2000.1.1..11.11.11..aabb.0000' - }, - { 'n': 170141184492616163062707000439658774528n, - 'da': '~2000.1.1..11.11.11..aabb' - }, - { 'n': 170141184492615487727406687186543706111n, - 'da': '~2000.1.1..1.1.1..aabb.ccdd.eeff.ffff' } ]; -testAuras('date', DATE_AURAS, DATE_TESTS); + +testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); +testAuras('date', DATE_AURAS, OUR_DATE_TESTS); +testAuras('text', [ 't' ], TEXT_TESTS); +testAuras('chars', CHAR_AURAS, CHAR_TESTS); describe('string decoding', () => { it('decodes', () => { @@ -451,50 +94,6 @@ describe('string decoding', () => { }); }); -const TEXT_AURAS: aura[] = [ 't' ]; -const TEXT_TESTS: { - n: bigint, - t: string -}[] = [ - { 'n': 6513249n, - 't': '~~abc' - }, - { 'n': 127430240531865354190938721n, - 't': '~~abc~~def~.ghi' - }, - { 'n': 6513249n, - 't': '~~a~62.c' - }, - { 'n': 294301677938177654314463611973797746852183254758760570046179940746240825570n, - 't': '~~~2605.~1f920.yeehaw~1f468.~200d.~1f467.~200d.~1f466.' - } - ]; -testAuras('text', TEXT_AURAS, TEXT_TESTS); - -const CHAR_AURAS: aura[] = ['c']; -const CHAR_TESTS: { n: bigint, c: string }[] = [ - { n: 129312n, c: '~-~1f920.' }, - { n: 128104n, c: '~-~1f468.' }, - { n: 8205n, c: '~-~200d.' }, - { n: 128103n, c: '~-~1f467.' }, - { n: 8205n, c: '~-~200d.' }, - { n: 128102n, c: '~-~1f466.' }, - { n: 97n, c: '~-a' }, - { n: 33n, c: '~-~21.' }, - { n: 32n, c: '~-.' }, - { n: 126n, c: '~-~~' }, - { n: 46n, c: '~-~.' }, - { n: 1548n, c: '~-~60c.' }, - // the cases below are deranged, because the input is deranged. - // @c represents utf-32 codepoints, so if you give it not-utf-32 - // it will render bogus, drop bytes in the rendering, etc. - // we include them (disabled) here to indicate that we don't have 100% - // exact stdlib parity here, but in practice that shouldn't matter. - // { n: 478560413032n, c: '~-~c6568.o' }, // 'hello' - // { n: 36762444129640n, c: '~-~c6568.~216f.' } // 'hello!' -]; -testAuras('chars', CHAR_AURAS, CHAR_TESTS); - describe('blob parsing', () => { it('parses', () => { expect(nuck('~02')).toEqual({ type: 'blob', jam: 2n }); diff --git a/test/render.test.ts b/test/render.test.ts index f70ed1e..bff77bb 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -1,20 +1,11 @@ import { aura, coin } from '../src/types'; import { render, rend } from '../src/render'; - -// most test cases generated from snippets similar to the following: -// -// =| n=@ud -// ^- (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q]) -// %- zing -// |- ^- (list (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q])) -// ?: (gth n 5) ~ -// :_ $(n +(n), eny (shaz +(eny))) -// :- [. . . . . . .]:(end 2^(bex n) eny) -// =. eny (shaz eny) -// :- [. . . . . . .]:(end 2^(bex n) eny) -// =. eny (shaz eny) -// [. . . . . . .]~:(end 2^(bex n) eny) -// +import { INTEGER_AURAS, INTEGER_TESTS, + PHONETIC_AURAS, PHONETIC_TESTS, + DATE_AURAS, DATE_TESTS, + TEXT_AURAS, TEXT_TESTS, + CHAR_AURAS, CHAR_TESTS, + } from './data/atoms'; describe('limited auras', () => { describe(`@n rendering`, () => { @@ -56,447 +47,10 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { }); } -const INTEGER_AURAS: aura[] = [ - 'ub', 'ud', 'ui', 'uv', 'uw', 'ux', - 'sb', 'sd', 'si', 'sv', 'sw', 'sx' -]; -const INTEGER_TESTS: { - n: bigint, - ub: string, - ud: string, - ui: string, - uv: string, - uw: string, - ux: string, - sb: string, - sd: string, - si: string, - sv: string, - sw: string, - sx: string, -}[] = [ - { 'n': 0n, - 'ub': '0b0', - 'ud': '0', - 'ui': '0i0', - 'uv': '0v0', - 'uw': '0w0', - 'ux': '0x0', - 'sb': '--0b0', - 'sd': '--0', - 'si': '--0i0', - 'sv': '--0v0', - 'sw': '--0w0', - 'sx': '--0x0', - }, - { 'n': 7n, - 'ub': '0b111', - 'ud': '7', - 'ui': '0i7', - 'uv': '0v7', - 'uw': '0w7', - 'ux': '0x7', - 'sb': '-0b100', - 'sd': '-4', - 'si': '-0i4', - 'sv': '-0v4', - 'sw': '-0w4', - 'sx': '-0x4', - }, - { 'n': 5n, - 'ub': '0b101', - 'ud': '5', - 'ui': '0i5', - 'uv': '0v5', - 'uw': '0w5', - 'ux': '0x5', - 'sb': '-0b11', - 'sd': '-3', - 'si': '-0i3', - 'sv': '-0v3', - 'sw': '-0w3', - 'sx': '-0x3', - }, - { 'n': 4n, - 'ub': '0b100', - 'ud': '4', - 'ui': '0i4', - 'uv': '0v4', - 'uw': '0w4', - 'ux': '0x4', - 'sb': '--0b10', - 'sd': '--2', - 'si': '--0i2', - 'sv': '--0v2', - 'sw': '--0w2', - 'sx': '--0x2', - }, - { 'n': 171n, - 'ub': '0b1010.1011', - 'ud': '171', - 'ui': '0i171', - 'uv': '0v5b', - 'uw': '0w2H', - 'ux': '0xab', - 'sb': '-0b101.0110', - 'sd': '-86', - 'si': '-0i86', - 'sv': '-0v2m', - 'sw': '-0w1m', - 'sx': '-0x56', - }, - { 'n': 53n, - 'ub': '0b11.0101', - 'ud': '53', - 'ui': '0i53', - 'uv': '0v1l', - 'uw': '0wR', - 'ux': '0x35', - 'sb': '-0b1.1011', - 'sd': '-27', - 'si': '-0i27', - 'sv': '-0vr', - 'sw': '-0wr', - 'sx': '-0x1b', - }, - { 'n': 77n, - 'ub': '0b100.1101', - 'ud': '77', - 'ui': '0i77', - 'uv': '0v2d', - 'uw': '0w1d', - 'ux': '0x4d', - 'sb': '-0b10.0111', - 'sd': '-39', - 'si': '-0i39', - 'sv': '-0v17', - 'sw': '-0wD', - 'sx': '-0x27', - }, - { 'n': 64491n, - 'ub': '0b1111.1011.1110.1011', - 'ud': '64.491', - 'ui': '0i64491', - 'uv': '0v1uvb', - 'uw': '0wfLH', - 'ux': '0xfbeb', - 'sb': '-0b111.1101.1111.0110', - 'sd': '-32.246', - 'si': '-0i32246', - 'sv': '-0vvfm', - 'sw': '-0w7TS', - 'sx': '-0x7df6', - }, - { 'n': 51765n, - 'ub': '0b1100.1010.0011.0101', - 'ud': '51.765', - 'ui': '0i51765', - 'uv': '0v1ihl', - 'uw': '0wcER', - 'ux': '0xca35', - 'sb': '-0b110.0101.0001.1011', - 'sd': '-25.883', - 'si': '-0i25883', - 'sv': '-0vp8r', - 'sw': '-0w6kr', - 'sx': '-0x651b', - }, - { 'n': 46444n, - 'ub': '0b1011.0101.0110.1100', - 'ud': '46.444', - 'ui': '0i46444', - 'uv': '0v1dbc', - 'uw': '0wblI', - 'ux': '0xb56c', - 'sb': '--0b101.1010.1011.0110', - 'sd': '--23.222', - 'si': '--0i23222', - 'sv': '--0vmlm', - 'sw': '--0w5GS', - 'sx': '--0x5ab6', - }, - { 'n': 384265565n, - 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', - 'ud': '384.265.565', - 'ui': '0i384265565', - 'uv': '0vb.eerat', - 'uw': '0wmVSRt', - 'ux': '0x16e7.6d5d', - 'sb': '-0b1011.0111.0011.1011.0110.1010.1111', - 'sd': '-192.132.783', - 'si': '-0i192132783', - 'sv': '-0v5.n7dlf', - 'sw': '-0wbsXqL', - 'sx': '-0xb73.b6af', - }, - { 'n': 2456897374n, - 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', - 'ud': '2.456.897.374', - 'ui': '0i2456897374', - 'uv': '0v29.72hqu', - 'uw': '0w2.isktu', - 'ux': '0x9271.475e', - 'sb': '--0b100.1001.0011.1000.1010.0011.1010.1111', - 'sd': '--1.228.448.687', - 'si': '--0i1228448687', - 'sv': '--0v14.jh8tf', - 'sw': '--0w1.9eaeL', - 'sx': '--0x4938.a3af', - }, - { 'n': 38583115n, - 'ub': '0b10.0100.1100.1011.1011.0100.1011', - 'ud': '38.583.115', - 'ui': '0i38583115', - 'uv': '0v1.4peqb', - 'uw': '0w2jbJb', - 'ux': '0x24c.bb4b', - 'sb': '-0b1.0010.0110.0101.1101.1010.0110', - 'sd': '-19.291.558', - 'si': '-0i19291558', - 'sv': '-0vicnd6', - 'sw': '-0w19BSC', - 'sx': '-0x126.5da6', - }, - { 'n': 13604104043154737885n, - 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', - 'ud': '13.604.104.043.154.737.885', - 'ui': '0i13604104043154737885', - 'uv': '0vbpi.rship.r32mt', - 'uw': '0wb.Pbv8O.PIoHt', - 'ux': '0xbccb.7c8c.b3b1.8add', - 'sb': '-0b101.1110.0110.0101.1011.1110.0100.0110.0101.1001.1101.1000.1100.0101.0110.1111', - 'sd': '-6.802.052.021.577.368.943', - 'si': '-0i6802052021577368943', - 'sv': '-0v5sp.du8pc.thhbf', - 'sw': '-0w5.VBLAp.pSclL', - 'sx': '-0x5e65.be46.59d8.c56f', - }, - { 'n': 18441444580797368868n, - 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', - 'ud': '18.441.444.580.797.368.868', - 'ui': '0i18441444580797368868', - 'uv': '0vfvr.9c4ej.nlih4', - 'uw': '0wf.~Jb2e.DuIEA', - 'ux': '0xffed.2c23.a77a.ca24', - 'sb': '--0b111.1111.1111.0110.1001.0110.0001.0001.1101.0011.1011.1101.0110.0101.0001.0010', - 'sd': '--9.220.722.290.398.684.434', - 'si': '--0i9220722290398684434', - 'sv': '--0v7vt.km279.rqp8i', - 'sw': '--0w7.~SBx7.jLmki', - 'sx': '--0x7ff6.9611.d3bd.6512', - }, - { 'n': 7643844662312245512n, - 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', - 'ud': '7.643.844.662.312.245.512', - 'ui': '0i7643844662312245512', - 'uv': '0v6k5.3472r.m6688', - 'uw': '0w6.Ekp3y.ToNA8', - 'ux': '0x6a14.6438.b763.1908', - 'sb': '--0b11.0101.0000.1010.0011.0010.0001.1100.0101.1011.1011.0001.1000.1100.1000.0100', - 'sd': '--3.821.922.331.156.122.756', - 'si': '--0i3821922331156122756', - 'sv': '--0v3a2.hi3hd.r3344', - 'sw': '--0w3.kacxN.rIoO4', - 'sx': '--0x350a.321c.5bb1.8c84', - }, - { 'n': 293389376720547819362821033486028091527n, - 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', - 'ud': '293.389.376.720.547.819.362.821.033.486.028.091.527', - 'ui': '0i293389376720547819362821033486028091527', - 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', - 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', - 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', - 'sb': '-0b110.1110.0101.1100.0101.1110.1000.1010.0110.1010.0111.1110.1101.1111.0001.1011.1100.1001.1110.0000.0100.0111.0010.0100.0011.1001.1101.0111.1101.1000.0100.0100', - 'sd': '-146.694.688.360.273.909.681.410.516.743.014.045.764', - 'si': '-0i146694688360273909681410516743014045764', - 'sv': '-0v3.ebhf8.kqjur.sdsjo.274gs.tfm24', - 'sw': '-0w1K.n5Waq.DXv6Y.DwhOg.VRZx4', - 'sx': '-0x6e5c.5e8a.6a7e.df1b.c9e0.4724.39d7.d844', - }, - { 'n': 11826418988767709295206418976840492314n, - 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', - 'ud': '11.826.418.988.767.709.295.206.418.976.840.492.314', - 'ui': '0i11826418988767709295206418976840492314', - 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', - 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', - 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', - 'sb': '--0b100.0111.0010.1101.0111.1011.1100.1011.0110.0100.0110.1101.0000.1110.0001.1111.1000.0101.1000.1000.1101.0011.1001.1111.1000.1110.1011.0001.0010.1000.1101', - 'sd': '--5.913.209.494.383.854.647.603.209.488.420.246.157', - 'si': '--0i5913209494383854647603209488420246157', - 'sv': '--0v4ebbr.pdi6q.3gvgm.4d77s.em4kd', - 'sw': '--0w4.sJuYJ.ArgUv.xozjD.UWNad', - 'sx': '--0x472.d7bc.b646.d0e1.f858.8d39.f8eb.128d', - }, - { 'n': 75341289328899252391918368331716799250n, - 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', - 'ud': '75.341.289.328.899.252.391.918.368.331.716.799.250', - 'ui': '0i75341289328899252391918368331716799250', - 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', - 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', - 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', - }, -]; testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); - -// test cases generated in similar fashion as the integer aura tests - -const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; -const PHONETIC_TESTS: { - n: bigint, - p: string, - q: string -}[] = [ - { 'n': 7n, 'p': '~let', 'q': '.~let' }, - { 'n': 0n, 'p': '~zod', 'q': '.~zod' }, - { 'n': 8n, 'p': '~ful', 'q': '.~ful' }, - { 'n': 117n, 'p': '~deg', 'q': '.~deg' }, - { 'n': 83n, 'p': '~tev', 'q': '.~tev' }, - { 'n': 223n, 'p': '~lud', 'q': '.~lud' }, - { 'n': 39995n, 'p': '~hapwyc', 'q': '.~hapwyc' }, - { 'n': 50426n, 'p': '~mitrep', 'q': '.~mitrep' }, - { 'n': 11415n, 'p': '~torryx', 'q': '.~torryx' }, - { 'n': 1863930458n, 'p': '~mogteg-botfex', 'q': '.~ligput-motfus' }, - { 'n': 3284934632n, 'p': '~loplet-nosnyx', 'q': '.~fasryd-mirlyn' }, - { 'n': 3833668n, 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, - { 'n': 9260427482306755094n, - 'p': '~lasrum-pindyt-nimnym-fotmeg', - 'q': '.~lasrum-pindyt-tadtem-lodlup', - }, - { 'n': 6363574354411289343n, - 'p': '~nopnet-rostem-navteb-fodbep', - 'q': '.~nopnet-rostem-nimfel-monfes', - }, - { 'n': 17571387016818844998n, - 'p': '~namler-folwet-bictes-wormec', - 'q': '.~namler-folwet-samwet-sarrul', - }, - { 'n': 241760151623976361741451001031931477015n, - 'p': '~dablys-minwed-mosreb-mictyn--nostyv-nimdul-hanbyl-bisdep', - 'q': '.~dablys-minwed-mosreb-mictyn-nostyv-nimdul-hanbyl-bisdep', - }, - { 'n': 148310954517291502180858368907816435627n, - 'p': '~ligryn-lomnem-fintes-davsyr--pacdel-wolpex-ripdev-paldeb', - 'q': '.~ligryn-lomnem-fintes-davsyr-pacdel-wolpex-ripdev-paldeb', - }, - { 'n': 97100713129464593177912155425728457718n, - 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', - 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', - }, - // with zero bytes - { 'n': 3833668n, - 'p': '~nidmes-samrut', - 'q': '.~sef-palsub' - }, - { 'n': 319478973361751151n, - 'p': '~sampel-sampel-lacwyl-tirder', - 'q': '.~sampel-sampel-dozpel-sampel', - }, - { 'n': 319478973354476655n, - 'p': '~sampel-sampel-dozzod-sampel', - 'q': '.~sampel-sampel-dozzod-sampel', - }, -]; testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); - -const DATE_AURAS: aura[] = [ 'da' ]; -const DATE_TESTS: { - n: bigint, - da: string -}[] = [ - { 'n': 170141184492615420181573981275213004800n, - 'da': '~2000.1.1' - }, - { 'n': 170141182164706681340023325049697075200n, - 'da': '~2000-.1.1' - }, - { 'n': 170141183328369385600900416699944140800n, - 'da': '~1-.1.1' - }, - { 'n': 170141183328369385600900416699944140800n, - 'da': '~1-.1.1' - }, - { 'n': 170213050367437966468743593413155225600n, - 'da': '~123456789.12.12' - }, - { 'n': 170141184507170056208381036660470579200n, - 'da': '~2025.1.1..01.00.00' - }, - { 'n': 170141184492615892916284358229892268032n, - 'da': '~2000.1.1..07.07.07' - }, - { 'n': 170141184492616163050404573632566132736n, - 'da': '~2000.1.1..11.11.11' - }, - { 'n': 170141184492616163050404761352701739008n, - 'da': '~2000.1.1..11.11.11..0000.aabb' - }, - { 'n': 170141184492616163062707000439658774528n, - 'da': '~2000.1.1..11.11.11..aabb' - }, - { 'n': 170141184492615487727406687186543706111n, - 'da': '~2000.1.1..01.01.01..aabb.ccdd.eeff.ffff' - } -]; testAuras('date', DATE_AURAS, DATE_TESTS); - -const TEXT_AURAS: aura[] = [ 'tas', 'ta', 't' ]; -const TEXT_TESTS: { - n: bigint, - tas: string, - ta: string, - t: string -}[] = [ - { n: 0n, - tas: '', - ta: '~.', - t: '~~' - }, - { n: 97n, - tas: 'a', - ta: '~.a', - t: '~~a' - }, - { n: 121404708502375659064812904n, - tas: 'hello-world', - ta: '~.hello-world', - t: '~~hello-world' - }, - { n: 10334410032597741434076685640n, - tas: 'Hello World!', - ta: '~.Hello World!', - t: '~~~48.ello.~57.orld~21.' - }, - { n: 294301677938177654314463611973797746852183254758760570046179940746240825570n, - tas: 'โ˜…๐Ÿค yeehaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', - ta: '~.โ˜…๐Ÿค yeehaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', - t: '~~~2605.~1f920.yeehaw~1f468.~200d.~1f467.~200d.~1f466.' - } -]; testAuras('text', TEXT_AURAS, TEXT_TESTS); - - -const CHAR_AURAS: aura[] = [ 'c' ]; -const CHAR_TESTS: { n: bigint, c: string }[] = [ - { n: 129312n, c: '~-~1f920.' }, - { n: 128104n, c: '~-~1f468.' }, - { n: 8205n, c: '~-~200d.' }, - { n: 128103n, c: '~-~1f467.' }, - { n: 8205n, c: '~-~200d.' }, - { n: 128102n, c: '~-~1f466.' }, - { n: 97n, c: '~-a' }, - { n: 33n, c: '~-~21.' }, - { n: 32n, c: '~-.' }, - { n: 126n, c: '~-~~' }, - { n: 46n, c: '~-~.' }, - { n: 1548n, c: '~-~60c.' }, - // the cases below are deranged, because the input is deranged. - // @c represents utf-32 codepoints, so if you give it not-utf-32 - // it will render bogus, drop bytes in the rendering, etc. - // we include them (disabled) here to indicate that we don't have 100% - // exact stdlib parity here, but in practice that shouldn't matter. - // { n: 478560413032n, c: '~-~c6568.o' }, // 'hello' - // { n: 36762444129640n, c: '~-~c6568.~216f.' } // 'hello!' -]; testAuras('chars', CHAR_AURAS, CHAR_TESTS); const MANY_COINS: { From d41c615819fa85b3a615ecee59b934cfbd93cedd Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 22:58:12 +0200 Subject: [PATCH 21/50] various: minor cleanup --- src/parse.ts | 17 ++++++++--------- src/render.ts | 24 ++++++------------------ 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 0ab0d74..7be9995 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,18 +1,17 @@ -// parse: parse atom literals +// parse: deserialize from atom literal strings // // atom literal parsing from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // +//TODO unsupported auras: @r*, @if, @is import { aura, dime, coin } from './types'; -import { parseDa } from "./da"; -import { isValidPatp, patp2bn } from "./p"; -import { isValidPatq, patq2bn } from "./q"; -import { parseUv } from "./uv"; -import { parseUw } from "./uw"; - -//TODO unsupported auras: @r*, @if, @is +import { parseDa } from './da'; +import { isValidPatp, patp2bn } from './p'; +import { isValidPatq, patq2bn } from './q'; +import { parseUv } from './uv'; +import { parseUw } from './uw'; function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; @@ -219,7 +218,7 @@ export function bisk(str: string): dime | null { case '0c': // "fim" //TODO support base58 - console.log('aura-js: @uc unsupported (bisk)'); + console.log('aura-js: @uc parsing unsupported (bisk)'); return null; case '0i': // "dim" diff --git a/src/render.ts b/src/render.ts index 2d0e842..e729bc6 100644 --- a/src/render.ts +++ b/src/render.ts @@ -3,15 +3,13 @@ // atom literal rendering from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // +//TODO unsupported auras: @r*, @if, @is import { aura, coin } from './types'; -import { formatDa } from "./da"; -import { patp } from "./p"; -import { patq } from "./q"; -import { formatUw } from "./uw"; - -//TODO unsupported auras: @r*, @if, @is +import { formatDa } from './da'; +import { patp } from './p'; +import { patq } from './q'; // render(): scot() // scot(): render atom as specific aura @@ -22,6 +20,8 @@ export function scot(aura: aura, atom: bigint): string { return rend({ type: 'dime', aura, atom }); } +// rend(): render coin into string +// export function rend(coin: coin): string { switch (coin.type) { case 'blob': @@ -109,26 +109,14 @@ export function rend(coin: coin): string { } } -function aco(atom: bigint): string { - return dco(1, atom); -} - function dco(lent: number, atom: bigint): string { return atom.toString(10).padStart(lent, '0'); } -function vco(lent: number, atom: bigint): string { - return atom.toString(32).padStart(lent, '0'); -} - function xco(lent: number, atom: bigint): string { return atom.toString(16).padStart(lent, '0'); } -function yco(atom: bigint): string { - return dco(2, atom); -} - function zco(atom: bigint): string { return '0x' + xco(1, atom); } From 0c582218d7488ca94add29ee8be6969d0208674e Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 22:58:24 +0200 Subject: [PATCH 22/50] tests: additional parse failure tests --- test/parse.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/parse.test.ts b/test/parse.test.ts index 0958aa7..ba3363c 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -124,6 +124,11 @@ describe('many parsing', () => { }); describe('invalid syntax', () => { + it('fails leading zeroes', () => { + expect(nuck('00')).toEqual(null); + expect(nuck('01')).toEqual(null); + expect(nuck('0b01')).toEqual(null); + }); it('fails incomplete atoms', () => { expect(nuck('~0')).toEqual(null); expect(nuck('~2000.1')).toEqual(null); @@ -133,4 +138,8 @@ describe('invalid syntax', () => { expect(nuck('._~zod__')).toEqual(null); expect(nuck('.123__')).toEqual(null); }); + it('fails bogus dates', () => { + expect(nuck('~2025.1.0')).toEqual(null); + expect(nuck('~2025.13.1')).toEqual(null); + }); }); From 71ce7fa0c07130d2cf85dfcce475fe63b106364b Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 13 Jun 2025 23:22:40 +0200 Subject: [PATCH 23/50] parse: local uv/uw parsing implementation --- src/parse.ts | 22 +++++-- src/ud.ts | 25 -------- src/ux.ts | 30 ---------- test/aura.test.ts | 150 ---------------------------------------------- 4 files changed, 17 insertions(+), 210 deletions(-) delete mode 100644 src/ud.ts delete mode 100644 src/ux.ts delete mode 100644 test/aura.test.ts diff --git a/src/parse.ts b/src/parse.ts index 7be9995..bd6f890 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -10,8 +10,6 @@ import { aura, dime, coin } from './types'; import { parseDa } from './da'; import { isValidPatp, patp2bn } from './p'; import { isValidPatq, patq2bn } from './q'; -import { parseUv } from './uv'; -import { parseUw } from './uw'; function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; @@ -198,7 +196,7 @@ export function nuck(str: string): coin | null { } } if ((str[1] === '0') && /^~0[0-9a-v]+$/.test(str)) { - return { type: 'blob', jam: parseUv('0v' + str.slice(2)) }; + return { type: 'blob', jam: slurp(5, UV_ALPHABET, str.slice(2)) }; } return null; } @@ -237,14 +235,14 @@ export function bisk(str: string): dime | null { case '0v': // "viz" if (regex['uv'].test(str)) { - return { aura: 'uv', atom: parseUv(str) }; + return { aura: 'uv', atom: slurp(5, UV_ALPHABET, str.slice(2)) }; } else { return null; } case '0w': // "wiz" if (regex['uw'].test(str)) { - return { aura: 'uw', atom: parseUw(str) }; + return { aura: 'uw', atom: slurp(6, UW_ALPHABET, str.slice(2)) }; } else { return null; } @@ -300,6 +298,20 @@ function stringToCord(str: string): bigint { return bytesToBigint(new TextEncoder().encode(str)); } +const UW_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; +const UV_ALPHABET = '0123456789abcdefghijklmnopqrstuv'; +function slurp(bits: number, alphabet: string, str: string): bigint { + let out = 0n; + const bbits = BigInt(bits); + while (str !== '') { + if (str[0] !== '.') { + out = (out << bbits) + BigInt(alphabet.indexOf(str[0])); + } + str = str.slice(1); + } + return out; +} + //REVIEW should the reversal happen here or at callsites? depends on what endianness is idiomatic to js? function bytesToBigint(bytes: Uint8Array): bigint { if (bytes.length === 0) return 0n; diff --git a/src/ud.ts b/src/ud.ts deleted file mode 100644 index 6d06166..0000000 --- a/src/ud.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { chunk } from './utils'; - -/** - * Given a string representing a @ud, returns a bigint - * - * @param {string} ud the number as @ud - * @return {bigint} the number as bigint - */ -export function parseUd(ud: string): bigint { - return BigInt(ud.replace(/\./g, '')); -} - -/** - * Given a bigint representing a @ud, returns a proper @ud as string - * - * @param {bigint} ud the number as bigint - * @return {string} the number as @ud - */ -export function formatUd(ud: bigint): string { - const transform = chunk(ud.toString().split('').reverse(), 3) - .map((group) => group.reverse().join('')) - .reverse() - .join('.'); - return transform.replace(/^[0\.]+/g, ''); -} diff --git a/src/ux.ts b/src/ux.ts deleted file mode 100644 index d4d1b58..0000000 --- a/src/ux.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { dropWhile, chunk } from './utils'; - -/** - * Given a string representing a @ux, returns a string in hex - * - * @param {string} ux the number as @ux - * @return {string} the number as hex - */ -export function parseUx(ux: string) { - return ux.replace('0x', '').replace('.', ''); -} - -/** - * Given a string representing hex, returns a proper @ux - * - * @param {string} hex the number as hex - * @return {string} the number as hex - */ -export const formatUx = (hex: string): string => { - const nonZeroChars = dropWhile(hex.split(''), (y) => y === '0'); - const ux = - chunk(nonZeroChars.reverse(), 4) - .map((x) => { - return x.reverse().join(''); - }) - .reverse() - .join('.') || '0'; - - return `0x${ux}`; -}; diff --git a/test/aura.test.ts b/test/aura.test.ts deleted file mode 100644 index 4d976d7..0000000 --- a/test/aura.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - formatDa, - parseDa, - formatUw, - parseUw, - formatUv, - parseUv, - parseUd, - formatUd, - parseUx, - formatUx, -} from '../src'; - -const DA_PAIRS: [string, bigint][] = [ - [ - '~2022.5.2..15.50.20..b4cb', - BigInt('170141184505617087925707667943685357568'), - ], - [ - '~2022.5.2..18.52.34..8166.240c.0635.b423', - BigInt('170141184505617289618704043249403016227'), - ], -]; -describe('@da', () => { - DA_PAIRS.map(([da, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseDa(da); - const diff = integer - res; - expect(diff == 0n).toBe(true); - }); - it('formats', () => { - const res = formatDa(integer); - expect(res).toEqual(da); - }); - }); - }); -}); - -const UD_PAIRS: [string, bigint][] = [ - ['123', 123n], - ['7.827.527.286', 7827527286n], - [ - '927.570.172.527.456.683.282.759.587.841.913.712.910.138.850.310.449.267.827.527.286', - BigInt('927570172527456683282759587841913712910138850310449267827527286'), - ], -]; - -describe('@ud', () => { - UD_PAIRS.map(([ud, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUd(ud); - const diff = integer - res; - expect(diff === 0n).toBe(true); - }); - it('formats', () => { - const res = formatUd(integer); - expect(res).toEqual(ud); - }); - }); - }); -}); - -const UW_PAIRS: [string, bigint][] = [ - ['0wji', 1234n], - [ - '0w2.VNFPq.zLWXr.mHG98.cOSaU.jD-HK.WOAEW.icKX-.-UOti.RrLxM.BEdKI.U8j~T.rgqLe.HuVVm.m5aDi.FcUj0.z-9H9.PWYVS', - BigInt( - '9729869760580312915057700420931106632029212932045019789366559593013069886734510969807231346927570172527456683282759587841913712910138850310449267827527286' - ), - ], - [ - '0w8.~wwXK.5Jbvq.EPFfs.mWqAa.G6VLL.Hp5RZ.1ztU0.OdjK6.rwC4f.IUflm.bew2G.q2V58.Yvb-y.8D7JP.mAX5-.tTUnZ.4PIzy.fU8eX.xriTS.GcWjT.5KCF2.GxKrX.WShtv.goTu0.czkXx.CU9x3.Xe3Rl.yPE0G.CwKhi.f7O~E.y9NXs.RFeNv.Dt-5~.hcX8U.z-23K.UmQJZ.GzeAZ.NrFGg.GErC-.-JAnn.Q6dTw.38ReU.pK2og.-PwZl.oIW0a.FEbAk.zNYLW.8ysuT.dqjIn.VTvxv.QjeOe', - BigInt( - '338660688809191135992117047766620650717811482934943979674885003948246397791915632356127816874957444994283298782534439422236465196123969501940528462017413072176474702992911473379692926314882846435461316330442229390384286920909868601208813714735355172837223931275587957994082972971545840145432819726749971121524031459169847685770572005049993814978529576884322644499161452167351136603982630270130940863597682766057587354154988711969349941809951888309135835193470094' - ), - ], -]; -describe('@uw', () => { - UW_PAIRS.map(([uw, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUw(uw); - const diff = integer - res; - expect(diff === 0n).toBe(true); - }); - it('formats', () => { - const res = formatUw(integer); - expect(res).toEqual(uw); - }); - }); - }); -}); - -const UV_PAIRS: [string, bigint][] = [ - ['0v16i', 1234n], - [ - '0v1d0.l2h7n.mo1ro.s3r8e.4f6gd.dfsp1.hc5en.a0k8j.1v7vk.16jqd.oog39.5ool7.mrkdp.vvofi.gd2d6.vnmi9.a1dlt.7lbbm.iq76k.u5ivc.pp8qa', - BigInt( - '4715838753694475992579249794985609354876653107513376869107585916141874120351297535898666953377988719385257642282348313095587079274499396365843215360500554' - ), - ], - [ - '0v1q7.2j1o2.gsrac.v0lr2.4qq3l.dl4dl.geimi.ti4kn.nerpk.io8e9.fb6u8.qdo3a.f6jnl.4t0ro.mnphj.45eu3.aasog.tgnop.mgknj.vrf7c.qh8uk.uhoko.e0k76.qj7o5.eoh6m.gtbd9.3dc3k.lknch.55trm.ud4m2.3ibqp.ni6je.0qjpk.tt978.6u5lu.ccp1b.ngqin.647c5.u6dk5.5svur.pr6ka.7l7ke.563g5.1pmkp.u1bm4.9lk7a.ra8rb.0t5d5.r499f.etnj9.5ggsi.umdsh.krg6k.ud7fa.9q1nh.dfj36.8ats6.klph1.r1fhj.d19f7.vmuep.l2ht9', - BigInt( - '2192679466494434890472543084060582766548642415978526232232529756549132081077716901494847003622252677433111645469887112954835308752404322485369993198040597565692723588585723772692969275396046341198068016409658069930178326315327541379152850899800598824712189194725563892210423915200062671509436137248472305920263160462934628062386175666117414052493363024883656571948762124184585291750029792031534226654202512820124560651712985859227347538529179923696933418921183145' - ), - ], -]; -describe('@uv', () => { - UV_PAIRS.map(([uv, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUv(uv); - const diff = integer - res; - expect(diff === 0n).toBe(true); - }); - it('formats', () => { - const res = formatUv(integer); - expect(res).toEqual(uv); - }); - }); - }); -}); - -const UX_PAIRS: [string, string][] = [ - ['0x0', '0'], - ['0xff.a0e2', 'ffa0e2'], -]; - -describe('@ux', () => { - UX_PAIRS.map(([ux, hex], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUx(ux); - expect(res).toEqual(hex); - }); - it('formats', () => { - const res = formatUx(hex); - expect(res).toEqual(ux); - }); - }); - }); - - it('trims leading zeroes', () => { - const res = formatUx('00ffa0e2'); - expect(res).toEqual('0xff.a0e2'); - }); -}); From e20bae99a073bce31cad7e2738b365811eb6c101 Mon Sep 17 00:00:00 2001 From: fang Date: Tue, 16 Sep 2025 14:58:46 +0200 Subject: [PATCH 24/50] parse: make q implementation pass tests We do this by making it stop doing "valid pat" checks, which are inaccurate for the `@q` format. Instead, we catch exceptions when they get thrown and return null for those cases. At some point, we should more thoroughly refactor/clean up the p and q modules, but for now we'll take a low-touch approach. --- src/p.ts | 3 +++ src/parse.ts | 12 ++++++++---- src/q.ts | 8 ++++---- test/parse.test.ts | 10 ++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/p.ts b/src/p.ts index 93934cb..371e97b 100644 --- a/src/p.ts +++ b/src/p.ts @@ -9,6 +9,9 @@ import { } from './hoon'; import ob from './hoon/ob'; +//NOTE the logic in this file has not yet been updated for the latest broader +// aura-js implementation style. but We Make It Workโ„ข. + /** * Convert a hex-encoded string to a @p-encoded string. * diff --git a/src/parse.ts b/src/parse.ts index bd6f890..47b3d0e 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -135,11 +135,15 @@ export function nuck(str: string): coin | null { // should probably run some perf tests if (str[1] === '~' && regex['q'].test(str)) { const q = str.slice(1); //NOTE q.ts insanity, need to strip leading . - if (!isValidPatq(q)) { - console.log('invalid @q', q); + try { + if (!isValidPatq(q)) { + console.log('invalid @q', q); + return null; + } else { + return { type: 'dime', aura: 'q', atom: patq2bn(q) } + } + } catch(e) { return null; - } else { - return { type: 'dime', aura: 'q', atom: patq2bn(q) } } } else //TODO %is, %if, %r* // "zust" diff --git a/src/q.ts b/src/q.ts index 645291e..630e335 100644 --- a/src/q.ts +++ b/src/q.ts @@ -1,6 +1,9 @@ import { isValidPat, prefixes, suffixes } from './hoon'; import { chunk, splitAt } from './utils'; +//NOTE the logic in this file has not yet been updated for the latest broader +// aura-js implementation style. but We Make It Workโ„ข. + //TODO investigate whether native UintArrays are more portable // than node Buffers @@ -74,9 +77,6 @@ export function hex2patq(arg: string): string { * @return {String} */ export function patq2hex(name: string): string { - if (isValidPat(name) === false) { - throw new Error('patq2hex: not a valid @q'); - } const chunks = name.slice(1).split('-'); const dec2hex = (dec: number) => dec.toString(16).padStart(2, '0'); @@ -121,7 +121,7 @@ export function patq2dec(name: string): string { * @return {boolean} */ export const isValidPatq = (str: string): boolean => - isValidPat(str) && eqPatq(str, patq(patq2dec(str))); + eqPatq(str, patq(patq2dec(str))); /** * Remove all leading zero bytes from a sliceable value. diff --git a/test/parse.test.ts b/test/parse.test.ts index ba3363c..aa83996 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -142,4 +142,14 @@ describe('invalid syntax', () => { expect(nuck('~2025.1.0')).toEqual(null); expect(nuck('~2025.13.1')).toEqual(null); }); + it('fails bogus @p or @q', () => { + expect(nuck('~zad')).toEqual(null); + expect(nuck('.~zad')).toEqual(null); + expect(nuck('~zodbin')).toEqual(null); + expect(nuck('.~zodbin')).toEqual(null); + expect(nuck('~funpal')).toEqual(null); + expect(nuck('.~funpal')).toEqual(null); + expect(nuck('~mister--dister')).toEqual(null); + expect(nuck('.~mister--dister')).toEqual(null); + }) }); From fb88a4c585096edf954ab693b77cb400d0d981d7 Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 17 Sep 2025 13:35:28 +0200 Subject: [PATCH 25/50] various: remove unused files and utilities --- src/utils.ts | 31 ------------------------------- src/uv.ts | 28 ---------------------------- src/uw.ts | 29 ----------------------------- 3 files changed, 88 deletions(-) delete mode 100644 src/uv.ts delete mode 100644 src/uw.ts diff --git a/src/utils.ts b/src/utils.ts index 72f9e2b..d96d4b6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,20 +1,3 @@ -export function chunkFromRight(str: string, size: number) { - const numChunks = Math.ceil(str.length / size); - const chunks = new Array(numChunks); - - for (let i = numChunks - 1, o = str.length; i >= 0; --i, o -= size) { - let start = o - size; - let len = size; - if (start < 0) { - start = 0; - len = o; - } - chunks[i] = str.substr(start, len); - } - - return chunks; -} - export function chunk(arr: T[], size: number): T[][] { let chunk: T[] = []; let newArray = [chunk]; @@ -31,20 +14,6 @@ export function chunk(arr: T[], size: number): T[][] { return newArray; } -export function dropWhile(arr: T[], pred: (x: T) => boolean): T[] { - const newArray = arr.slice(); - - for (const item of arr) { - if (pred(item)) { - newArray.shift(); - } else { - return newArray; - } - } - - return newArray; -} - export function splitAt(index: number, str: string) { return [str.slice(0, index), str.slice(index)]; } diff --git a/src/uv.ts b/src/uv.ts deleted file mode 100644 index c0d38db..0000000 --- a/src/uv.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { chunkFromRight } from './utils'; - -const uvAlphabet = '0123456789abcdefghijklmnopqrstuv'; - -export function parseUv(x: string) { - let res = 0n; - x = x.slice(2); - while (x !== '') { - if (x[0] !== '.') { - res = (res << 5n) + BigInt(uvAlphabet.indexOf(x[0])); - } - x = x.slice(1); - } - return res; -} - -export function formatUv(x: bigint | string) { - if (typeof x === 'string') { - x = BigInt(x); - } - let res = ''; - while (x !== 0n) { - let nextFive = Number(BigInt.asUintN(5, x)); - res = uvAlphabet[nextFive] + res; - x = x >> 5n; - } - return `0v${chunkFromRight(res, 5).join('.')}`; -} diff --git a/src/uw.ts b/src/uw.ts deleted file mode 100644 index 6a5017a..0000000 --- a/src/uw.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { chunkFromRight } from './utils'; - -const uwAlphabet = - '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; - -export function parseUw(x: string) { - let res = 0n; - x = x.slice(2); - while (x !== '') { - if (x[0] !== '.') { - res = (res << 6n) + BigInt(uwAlphabet.indexOf(x[0])); - } - x = x.slice(1); - } - return res; -} - -export function formatUw(x: bigint | string) { - if (typeof x === 'string') { - x = BigInt(x); - } - let res = ''; - while (x !== 0n) { - let nextSix = Number(BigInt.asUintN(6, x)); - res = uwAlphabet[nextSix] + res; - x = x >> 6n; - } - return `0w${chunkFromRight(res, 5).join('.')}`; -} From c57e071a7b47c8c2be728ab7644c944841d1810c Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 17 Sep 2025 13:36:18 +0200 Subject: [PATCH 26/50] lib: refine exports Export selectively, only exposing what we actually want outside callers to use. We continue exposing some odd utilities for the time being (preSig, deSig primarily) but plan to eventually remove those. --- src/index.ts | 15 ++++++++------- src/parse.ts | 28 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2759f22..73919db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -export * from './da'; -export * from './p'; -export * from './q'; -export * from './ud'; -export * from './uv'; -export * from './uw'; -export * from './ux'; +// main +export * from './types'; +export { parse, slaw, slav, nuck } from './parse'; +export { render, scot, rend } from './render'; //TODO expose encodeString() ? + +// atom utils +export { daToUnix, unixToDa } from './da'; +export { cite, deSig, preSig } from './p'; //TODO remove deSig, preSig diff --git a/src/parse.ts b/src/parse.ts index 47b3d0e..b470e5b 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -43,11 +43,21 @@ export const regex: { [key in aura]: RegExp } = { 'ux': integerRegex('0x', '[1-9a-f]', '[0-9a-f]', 4), }; -// parse(): slaw() -// slaw(): parse string as specific aura, null if that fails +// parse(): slav() +// slav(): slaw() but throwing on failure // -export const parse = slaw; +export const parse = slav; export default parse; +export function slav(aura: aura, str: string): bigint { + const out = slaw(aura, str); + if (!out) { + throw new Error('slav: failed to parse @' + aura + ' from string: ' + str); + } + return out; +} + +// slaw(): parse string as specific aura, null if that fails +// export function slaw(aura: aura, str: string): bigint | null { // if the aura has a regex, test with that first //TODO does double work with checks in nuck? @@ -67,16 +77,6 @@ export function slaw(aura: aura, str: string): bigint | null { } } -// slav(): slaw() but throwing on failure -// -export function slav(aura: aura, str: string): bigint { - const out = slaw(aura, str); - if (!out) { - throw new Error('slav: failed to parse @' + aura + ' from string: ' + str); - } - return out; -} - // nuck(): parse string into coin, or null if that fails // export function nuck(str: string): coin | null { @@ -209,7 +209,7 @@ export function nuck(str: string): coin | null { // bisk(): parse string into dime of integer aura, or null if that fails // -export function bisk(str: string): dime | null { +function bisk(str: string): dime | null { switch (str.slice(0, 2)) { case '0b': // "bay" if (regex['ub'].test(str)) { From 85fa061a18a906423f1686bc01ff78ff354fceaf Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 3 Oct 2025 23:22:45 +0200 Subject: [PATCH 27/50] parse: add tryParse alias for slaw --- src/index.ts | 2 +- src/parse.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 73919db..b9f9990 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // main export * from './types'; -export { parse, slaw, slav, nuck } from './parse'; +export { parse, tryParse, slav, slaw, nuck } from './parse'; export { render, scot, rend } from './render'; //TODO expose encodeString() ? // atom utils diff --git a/src/parse.ts b/src/parse.ts index b470e5b..1bb5c05 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -56,8 +56,10 @@ export function slav(aura: aura, str: string): bigint { return out; } +// tryParse(): slaw() // slaw(): parse string as specific aura, null if that fails // +export const tryParse = slaw; export function slaw(aura: aura, str: string): bigint | null { // if the aura has a regex, test with that first //TODO does double work with checks in nuck? From 7e517f646fa015b73a5568ee53e0b0ae1e91a9bc Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 3 Oct 2025 23:23:29 +0200 Subject: [PATCH 28/50] tests: fix imports These had gotten borked due to index.ts' export changes. --- test/fuzz.test.ts | 2 +- test/p.test.ts | 2 +- test/parse.test.ts | 2 +- test/q.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index a3a329b..6320e47 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -1,5 +1,5 @@ import { aura } from '../src/types'; -import parse from "../src/parse"; +import { tryParse as parse } from "../src/parse"; import render from "../src/render"; const testCount = 500; diff --git a/test/p.test.ts b/test/p.test.ts index b901ef9..c8399fd 100644 --- a/test/p.test.ts +++ b/test/p.test.ts @@ -10,7 +10,7 @@ import { preSig, deSig, cite, -} from '../src'; +} from '../src/p'; const patps = jsc.uint32.smap( (num) => patp(num), diff --git a/test/parse.test.ts b/test/parse.test.ts index aa83996..b573a41 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,5 +1,5 @@ import { aura } from '../src/types'; -import { parse, decodeString, nuck, regex } from '../src/parse'; +import { tryParse as parse, decodeString, nuck, regex } from '../src/parse'; import { INTEGER_AURAS, INTEGER_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, diff --git a/test/q.test.ts b/test/q.test.ts index 33cd8e3..e6294fe 100644 --- a/test/q.test.ts +++ b/test/q.test.ts @@ -6,7 +6,7 @@ import { patq2dec, eqPatq, isValidPatq, -} from '../src'; +} from '../src/q'; const patqs = jsc.uint32.smap( (num) => patq(num), From 6692f25f248f0a15f979f4b3713faed92823c64d Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 3 Oct 2025 23:24:23 +0200 Subject: [PATCH 29/50] parse: conform `@q` behavior to old tests We had made it more lenient in e20bae9 because its failures were getting caught by its callsites, but we should probably preserve the sane parts of its original behavior for now. We do remove one old test that was testing for non-stdlib-compliant behavior. --- src/q.ts | 15 ++++++++++----- test/parse.test.ts | 2 ++ test/q.test.ts | 2 -- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/q.ts b/src/q.ts index 630e335..d4c1640 100644 --- a/src/q.ts +++ b/src/q.ts @@ -78,11 +78,14 @@ export function hex2patq(arg: string): string { */ export function patq2hex(name: string): string { const chunks = name.slice(1).split('-'); - const dec2hex = (dec: number) => dec.toString(16).padStart(2, '0'); + const dec2hex = (dec: number) => { + if (dec < 0) throw new Error('malformed @q'); + return dec.toString(16).padStart(2, '0'); + } - const splat = chunks.map((chunk) => { + const splat = chunks.map((chunk, i) => { let syls = splitAt(3, chunk); - return syls[1] === '' + return (syls[1] === '' && i === 0) // singles only at the start ? dec2hex(suffixes.indexOf(syls[0])) : dec2hex(prefixes.indexOf(syls[0])) + dec2hex(suffixes.indexOf(syls[1])); }); @@ -120,8 +123,10 @@ export function patq2dec(name: string): string { * @param {String} str a string * @return {boolean} */ -export const isValidPatq = (str: string): boolean => - eqPatq(str, patq(patq2dec(str))); +export const isValidPatq = (str: string): boolean => { + if (str === '') return false; + try { return eqPatq(str, patq(patq2dec(str))); } catch (e) { return false; } +}; /** * Remove all leading zero bytes from a sliceable value. diff --git a/test/parse.test.ts b/test/parse.test.ts index b573a41..719fe5a 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -149,6 +149,8 @@ describe('invalid syntax', () => { expect(nuck('.~zodbin')).toEqual(null); expect(nuck('~funpal')).toEqual(null); expect(nuck('.~funpal')).toEqual(null); + expect(nuck('~nidsut-dun')).toEqual(null); + expect(nuck('.~nidsut-dun')).toEqual(null); expect(nuck('~mister--dister')).toEqual(null); expect(nuck('.~mister--dister')).toEqual(null); }) diff --git a/test/q.test.ts b/test/q.test.ts index e6294fe..257bf24 100644 --- a/test/q.test.ts +++ b/test/q.test.ts @@ -48,8 +48,6 @@ describe('patq, etc.', () => { expect(input).toThrow(); input = () => patq2hex('~nidsut-tomdzn'); expect(input).toThrow(); - input = () => patq2hex('~sut-tomdun'); - expect(input).toThrow(); input = () => patq2hex('~nidsut-dun'); expect(input).toThrow(); input = () => patq2hex(null as any); From 8ef8cc1f4c44e5aceaf9c10ec3fe98da2695864c Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 3 Oct 2025 23:44:08 +0200 Subject: [PATCH 30/50] parse: add valid() syntax tester Really just a thin wrapper around slaw... --- src/index.ts | 2 +- src/parse.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b9f9990..43f6cf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // main export * from './types'; -export { parse, tryParse, slav, slaw, nuck } from './parse'; +export { parse, tryParse, valid, slav, slaw, nuck } from './parse'; export { render, scot, rend } from './render'; //TODO expose encodeString() ? // atom utils diff --git a/src/parse.ts b/src/parse.ts index 1bb5c05..fb92893 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -79,6 +79,10 @@ export function slaw(aura: aura, str: string): bigint | null { } } +export function valid(aura: aura, str: string): boolean { + return slaw(aura, str) !== null; +} + // nuck(): parse string into coin, or null if that fails // export function nuck(str: string): coin | null { From e3604681b0da20c59db28aa2f79b46d35c4899fc Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 3 Oct 2025 23:44:45 +0200 Subject: [PATCH 31/50] parse: comment touch-ups --- src/parse.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index fb92893..c9e0a66 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -88,7 +88,7 @@ export function valid(aura: aura, str: string): boolean { export function nuck(str: string): coin | null { if (str === '') return null; - // narrow options down by the first character, before doing regex texts + // narrow options down by the first character, before doing regex tests // and trying to parse for real // const c = str[0]; @@ -136,9 +136,10 @@ export function nuck(str: string): coin | null { if (str === '.n') { return { type: 'dime', aura: 'f', atom: 1n }; } else - //REVIEW entering the branch this way assumes regexes for sequentially-tested auras don't overlap... - // going down the list of options this way matches hoon parser behavior the closest, but is slow for the "miss" case. - // should probably run some perf tests + //REVIEW entering the branch this way assumes regexes for sequentially-tested auras don't overlap... + // going down the list of options this way matches hoon parser behavior the closest, but is slow for the "miss" case. + // could be optimized by hard-returning if the regex fails for cases where the lead char is unique. + // should probably run some perf tests if (str[1] === '~' && regex['q'].test(str)) { const q = str.slice(1); //NOTE q.ts insanity, need to strip leading . try { From 65364f9d5bae1616e52bc1a8f0d4ca773fc84367 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 3 Oct 2025 23:54:10 +0200 Subject: [PATCH 32/50] tests: fuzz test portability (for ci) --- test/fuzz.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index 6320e47..d49d4d6 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -1,6 +1,7 @@ import { aura } from '../src/types'; import { tryParse as parse } from "../src/parse"; -import render from "../src/render"; +import render from '../src/render'; +import { webcrypto } from 'crypto'; const testCount = 500; @@ -30,7 +31,7 @@ const auras: aura[] = [ ] function fuzz(nom: string, arr: Uint8Array | Uint16Array | Uint32Array | BigUint64Array) { - crypto.getRandomValues(arr); + webcrypto.getRandomValues(arr); auras.forEach((a) => { describe(nom + ' @' + a, () => { it('round-trips losslessly', () => { From 0b596fddacf2f42594933342136ad1155ac03e8b Mon Sep 17 00:00:00 2001 From: fang Date: Sat, 4 Oct 2025 00:04:15 +0200 Subject: [PATCH 33/50] ci: remove size-limit check As far as I can tell, this check has never run in this iteration of the repo. Now that it is trying to run, it's failing for reasons of using pnpm instead of npm, or some such thing. We don't care to spend much time on it right now, so we rip the check out entirely. We still keep it around as a script and dev dependency. For the record, I did check the sizes from pre- and post-refactor. We have gone from ~4.7kb to ~6.6kb. --- .github/workflows/size.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .github/workflows/size.yml diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml deleted file mode 100644 index 6021cda..0000000 --- a/.github/workflows/size.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: size -on: [pull_request] -jobs: - size: - runs-on: ubuntu-latest - env: - CI_JOB_NUMBER: 1 - steps: - - uses: actions/checkout@v1 - - uses: andresz1/size-limit-action@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} From 820888d8f9d3c69b2df1526d1dfcab2af5e64566 Mon Sep 17 00:00:00 2001 From: fang Date: Sun, 12 Oct 2025 21:05:26 +0200 Subject: [PATCH 34/50] parse, render: support float auras, r* Adds support for floating-point auras, `@rs`, `@rd`, `@rh` and `@rq`. This addition is sizeable, because we must match the exact behavior of the hoon stdlib. Luckily, its behavior is standard, and we don't need all of the stdlib functionality. We can make do with js-based implementation of IEEE-754 floating point conversions. To make our up-front workload lighter, we copy other people's homework. For parsing, we copy and adapt `p.encodeFloat` from this (indeed rather hellish) implementation by Jonas Raoni Soares Silva: http://jsfromhell.com/classes/binary-parser For rendering, we port Ryan Juckett's Dragon4 implementation. Truly a sight for sore eyes! https://www.ryanjuckett.com/printing-floating-point-numbers/ All of the internals are fully generic to whatever bitsizes you desire. They haven't been tested with non-standard sizes though, and the library doesn't expose direct access to them. Includes tests for all happy-path cases. Remaining TODO for adding test cases where the parser needs to truncate them down to values that fit inside the floats. --- src/parse.ts | 31 ++- src/r.ts | 530 ++++++++++++++++++++++++++++++++++++++++++++ src/render.ts | 9 +- src/types.ts | 4 + test/data/atoms.ts | 129 +++++++++++ test/parse.test.ts | 7 + test/render.test.ts | 5 + 7 files changed, 709 insertions(+), 6 deletions(-) create mode 100644 src/r.ts diff --git a/src/parse.ts b/src/parse.ts index c9e0a66..5ececb7 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -10,6 +10,7 @@ import { aura, dime, coin } from './types'; import { parseDa } from './da'; import { isValidPatp, patp2bn } from './p'; import { isValidPatq, patq2bn } from './q'; +import { parse as parseR, precision } from './r'; function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; @@ -17,6 +18,10 @@ function integerRegex(a: string, b: string, c: string, d: number, e: boolean = f return new RegExp(`^${e ? '\\-\\-?' : ''}${a}(0|${pre}${aft})$`); } +function floatRegex(a: number): RegExp { + return new RegExp(`^\\.~{${a}}(nan|\\-?(inf|(0|[1-9][0-9]*)(\\.[0-9]+)?(e\\-?(0|[1-9][0-9]*))?))$`); +} + //TODO rewrite with eye towards capturing groups? export const regex: { [key in aura]: RegExp } = { 'c': /^~\-((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, @@ -26,6 +31,10 @@ export const regex: { [key in aura]: RegExp } = { 'n': /^~$/, 'p': /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/, //NOTE matches shape but not syllables 'q': /^\.~(([a-z]{3}|[a-z]{6})(\-[a-z]{6})*)$/, //NOTE matches shape but not syllables + 'rd': floatRegex(1), + 'rh': floatRegex(2), + 'rq': floatRegex(3), + 'rs': floatRegex(0), 'sb': integerRegex('0b', '1', '[01]', 4, true), 'sd': integerRegex('', '[1-9]', '[0-9]', 3, true), 'si': integerRegex('0i', '[1-9]', '[0-9]', 0, true), @@ -129,7 +138,8 @@ export function nuck(str: string): coin | null { return null; } } else - if (c === '.') { // "perd" + if (c === '.') { // "perd", "zust" + //NOTE doesn't match stdlib parsing order, but they're easy early-outs if (str === '.y') { return { type: 'dime', aura: 'f', atom: 0n }; } else @@ -140,6 +150,23 @@ export function nuck(str: string): coin | null { // going down the list of options this way matches hoon parser behavior the closest, but is slow for the "miss" case. // could be optimized by hard-returning if the regex fails for cases where the lead char is unique. // should probably run some perf tests + if ( ( str[1] === '~' && + (regex['rd'].test(str) || regex['rh'].test(str) || regex['rq'].test(str)) ) + || regex['rs'].test(str) ) { // "royl" + let precision = 0, i = 1; + while (str[i] === '~') { + precision++; i++; + } + let aura: aura; + switch (precision) { + case 0: aura = 'rs'; break; + case 1: aura = 'rd'; break; + case 2: aura = 'rh'; break; + case 3: aura = 'rq'; break; + default: throw new Error('parsing invalid @r*'); + } + return { type: 'dime', aura, atom: parseR(aura[1] as precision, str) }; + } else if (str[1] === '~' && regex['q'].test(str)) { const q = str.slice(1); //NOTE q.ts insanity, need to strip leading . try { @@ -153,7 +180,7 @@ export function nuck(str: string): coin | null { return null; } } else - //TODO %is, %if, %r* // "zust" + //TODO %is, %if // "zust" if (str[1] === '_' && /^\.(_([0-9a-zA-Z\-\.]|~\-|~~)+)*__$/.test(str)) { // "nusk" const coins = str.slice(1, -2).split('_').slice(1).map((s): coin | null => { //NOTE real +wick produces null for strings w/ other ~ chars, diff --git a/src/r.ts b/src/r.ts new file mode 100644 index 0000000..c9db461 --- /dev/null +++ b/src/r.ts @@ -0,0 +1,530 @@ +export type precision = 'h' | 's' | 'd' | 'q' | precisionBits; +type precisionBits = { w: number, p: number, l: string }; + +// str: @r* format string including its leading . and ~s +export function parse(per: precision, str: string): bigint { + per = getPrecision(per); + return parseR(str.slice(per.l.length), per.w, per.p); +} + +export function render(per: precision, r: bigint): string { + per = getPrecision(per); + return per.l + rCo(deconstruct(r, BigInt(per.w), BigInt(per.p))); +} + +// +// helpers +// + +function getPrecision(per: precision): precisionBits { + if (per === 'h') return { w: 5, p: 10, l: '.~~' }; else + if (per === 's') return { w: 8, p: 23, l: '.' }; else + if (per === 'd') return { w: 11, p: 52, l: '.~' }; else + if (per === 'q') return { w: 15, p: 112, l: '.~~~' }; else + return per; +} + +function bitMask(bits: bigint): bigint { + return (2n ** bits) - 1n; +} + +// +// parsing and construction +// + +// str: @r* format string with its leading . and ~ stripped off +// w: exponent bits +// p: mantissa bits +function parseR(str: string, w: number, p: number): bigint { + if (str === 'nan') return makeNaN(w, p); + if (str === 'inf') return makeInf(true, w, p); + if (str === '-inf') return makeInf(false, w, p); + let i = 0; + let sign = true; + if (str[i] === '-') { + sign = false; + i++; + } + let int = ''; + while (str[i] !== '.' && str[i] !== 'e' && str[i] !== undefined) { + int += str[i++]; + } + if (str[i] === '.') i++; + let fra = ''; + while (str[i] !== 'e' && str[i] !== undefined) { + fra += str[i++]; + } + if (str[i] === 'e') i++; + let expSign = true; + if (str[i] === '-') { + expSign = false; + i++; + } + let exp = ''; + while (str[i] !== undefined) { + exp += str[i++]; + } + return BigInt('0b' + makeFloat(w, p, sign, int, fra, expSign, Number(exp))); +} + +function makeNaN(w: number, p: number): bigint { + return bitMask(BigInt(w + 1)) << BigInt(p - 1); +} + +function makeInf(s: boolean, w: number, p: number): bigint { + return bitMask(BigInt(s ? w : w + 1)) << BigInt(p); +} + +// turn into representation without exponent +function makeFloat(w: number, p: number, sign: boolean, intPart: string, floatPart: string, expSign: boolean, exp: number) { + if (exp !== 0) { + if (expSign) { + intPart = intPart + floatPart.padEnd(exp, '0').slice(0, exp); + floatPart = floatPart.slice(exp); + } else { + floatPart = intPart.padStart(exp, '0').slice(-exp) + floatPart; + intPart = intPart.slice(0, -exp); + } + } + return construct(p, w, sign, BigInt(intPart), BigInt(floatPart.length), BigInt(floatPart)); +} + +//NOTE modified from an encodeFloat() written by by Jonas Raoni Soares Silva, +// made to operate on (big)integers, without using js's float logic. +// http://jsfromhell.com/classes/binary-parser +// (yes, this code is vaguely deranged. but it works!) +function construct(precisionBits: number, exponentBits: number, + sign: boolean, intPart: bigint, floatDits: bigint, floatPart: bigint) { + //REVIEW when do we trigger this? + // inputs representing nrs too large for exponentBits? + // inputs with precision we can't match? + // add tests for those cases! should match stdlib result. + function exceed(x: string) { + console.warn(x); + return 1; + } + + const bias = 2**(exponentBits - 1) - 1, + minExp = -bias + 1, + maxExp = bias, + minUnnormExp = minExp - precisionBits, + len = 2 * bias + 1 + precisionBits + 3, + bin = new Array(len), + denom = 10n ** floatDits; + var exp = 0, + signal = !sign, + i, lastBit, rounded, j, result, n; + // zero-initialize the bit-array + for (i = len; i; bin[--i] = 0); + // integral into bits + for (i = bias + 2; intPart && i; bin[--i] = intPart & 1n, intPart = intPart >> 1n); + // fractional into bits + for (i = bias + 1; floatPart > 0n && (i < len); (bin[++i] = (((floatPart *= 2n) >= denom) ? 1 : 0)) && (floatPart = floatPart - denom)); + // walk cursor (i) to first 1-bit. + for (i = -1; ++i < len && !bin[i];); + // round if needed + if (bin[(lastBit = precisionBits - 1 + (i = (exp = bias + 1 - i) >= minExp && exp <= maxExp ? i + 1 : bias + 1 - (exp = minExp - 1))) + 1]) { + if (!(rounded = bin[lastBit])) + for (j = lastBit + 2; !rounded && j < len; rounded = bin[j++]); + for (j = lastBit + 1; rounded && --j >= 0; (bin[j] = (!bin[j] ? 1 : 0) - 0) && (rounded = 0)); + } + // walk cursor (i) to first/next(??) 1-bit + for (i = i - 2 < 0 ? -1 : i - 3; ++i < len && !bin[i];); + + // set exponent, throwing on under- and overflows + (exp = bias + 1 - i) >= minExp && exp <= maxExp ? ++i : exp < minExp && + (exp != bias + 1 - len && exp < minUnnormExp && exceed('r.construct underflow'), i = bias + 1 - (exp = minExp - 1)); + intPart && (exceed(intPart ? 'r.construct overflow' : 'r.construct'), + exp = maxExp + 1, i = bias + 2); + // exponent into bits + for (n = Math.abs(exp + bias), j = exponentBits + 1, result = ''; --j; result = (n & 1) + result, n = n >>= 1); + // final serialization: sign + exponent + mantissa + return (signal ? '1' : '0') + result + bin.slice(i, i + precisionBits).join(''); +}; + +// +// deconstruction and rendering +// + +type dn = { t: 'd', s: boolean, e: number, a: string } + | { t: 'i', s: boolean } + | { t: 'n' }; + +//NOTE not _exactly_ like +r-co due to dragon4() outExponent semantics. +// if we copy +r-co logic exactly we off-by-one all over the place. +function rCo(a: dn): string { + if (a.t === 'n') return 'nan'; + if (a.t === 'i') return a.s ? 'inf' : '-inf'; + let e: number; + if ((a.e - 4) > 0) { // 12000 -> 12e3 e>+2 + e = 1; + } else + if ((a.e + 2) < 0) { // 0.001 -> 1e-3 e<-2 + e = 1; + } else { // 1.234e2 -> '.'@3 -> 123 .4 + e = a.e + 1; + a.e = 0; + } + return (a.s ? '' : '-') + + edCo(e, a.a) + + ((a.e === 0) ? '' : ('e' + a.e.toString())); +} + +function edCo(exp: number, int: string): string { + const dig: number = Math.abs(exp); + if (exp <= 0) { + return '0.' + (''.padEnd(dig, '0')) + int; + } else { + const len = int.length; + if (dig >= len) return int + ''.padEnd((dig - len), '0'); + return int.slice(0, dig) + '.' + int.slice(dig); + } +} + +//NOTE the deconstruct() and dragon4() below are ported from Ryan Juckett's +// PrintFloat32() and Dragon4() respectively. its general structure is +// copied one-to-one and comments are preserved, but we got to drop some +// logic due to having access to native bigints. see his post series for +// a good walkthrough of the underlying algorithm and its implementation, +// as well as pointers to additional references. +// https://www.ryanjuckett.com/printing-floating-point-numbers/ +// we only use one of the cutoff modes, but have maintained support for +// the others for completeness' sake. + +// deconstruct(): binary float to $dn structure (+drg:ff) +function deconstruct(float: bigint, exponentBits: bigint, precisionBits: bigint): dn { + // deconstruct the value into its components + const mantissaMask = bitMask(precisionBits); + const exponentMask = bitMask(exponentBits); + const floatMantissa: bigint = float & mantissaMask; + const floatExponent: bigint = (float >> BigInt(precisionBits)) & exponentMask; + const sign: boolean = ((float >> BigInt(exponentBits + precisionBits)) & 1n) === 0n; + + // transform the components into the values they represent + let mantissa: bigint, exponent: bigint, mantissaHighBitIdx: number, unequalMargins: boolean; + if (floatExponent === exponentMask) { // specials + if (floatMantissa === 0n) + return { t: 'i', s: sign }; // infinity + return { t: 'n' }; // nan + } else + if (floatExponent !== 0n) { // normalized + // the floating point equation is: + // value = (1 + mantissa/2^23) * 2 ^ (exponent-127) + // we convert the integer equation by factoring a 2^23 out of the exponent + // value = (1 + mantissa/2^23) * 2^23 * 2 ^ (exponent-127-23) + // value = (2^23 + mantissa) * 2 ^ (exponent-127-23) + // because of the implied 1 in front of the mantissa we have 24 bits of precision + // m = (2^23 + mantissa) + // e = (exponent-127-23) + mantissa = (1n << BigInt(precisionBits)) | floatMantissa; + exponent = floatExponent - ((2n**(exponentBits-1n))-1n) - precisionBits; + mantissaHighBitIdx = Number(precisionBits); + unequalMargins = (floatExponent !== 1n) && (floatMantissa === 0n); + } else { // denormalized + // the floating point equation is: + // value = (mantissa/2^23) * 2 ^ (1-127) + // we convert the integer equation by factoring a 2^23 out of the exponent + // value = (mantissa/2^23) * 2^23 * 2 ^ (1-127-23) + // value = mantissa * 2 ^ (1-127-23) + // we have up to 23 bits of precision + // m = (mantissa) + // e = (1-127-23) + mantissa = floatMantissa; + exponent = 1n - ((2n**(exponentBits-1n))-1n) - precisionBits; + mantissaHighBitIdx = mantissa.toString(2).length - 1; // poor man's log2 + unequalMargins = false; + } + + const buf = (2n**precisionBits).toString(10).length + 1; + const res = dragon4(mantissa, Number(exponent), mantissaHighBitIdx, unequalMargins, 'unique', 0, buf); + return { t: 'd', s: sign, e: res.outExponent, a: res.digits }; +} + +// dragon4(): binary float to decimal digits +// +// like +drg:fl (but with slightly different outExponent semantics) +// +// mantissa: value significand +// exponent: value exponent in base 2 +// mantissaHighBitIdx: highest set mantissa bit index +// hasUnequalMargins: is the high margin twice the low margin +// cutoffMode: 'unique' | 'totalLength' | 'fractionLength' +// cutoffNumber: cutoff parameter for the selected mode +// bufferSize: max output digits +// +// digits: printed digits +// outExponent: exponent of the first digit printed +// +function dragon4( + mantissa: bigint, + exponent: number, + mantissaHighBitIdx: number, + hasUnequalMargins: boolean, + cutoffMode: 'unique' | 'totalLength' | 'fractionLength', + cutoffNumber: number, + bufferSize: number +): { digits: string, outExponent: number } { + const bexponent = BigInt(exponent); + let pCurDigit = 0; // pointer into output buffer (digit string index) + let outBuffer = new Array(bufferSize).fill('0'); + let outExponent = 0; + + // if mantissa is zero, output "0" + if (mantissa === 0n) { + outBuffer[0] = '0'; + outExponent = 0; + return { digits: outBuffer.slice(0, 1).join(''), outExponent }; + } + + // compute the initial state in integral form such that: + // value = scaledValue / scale + // marginLow = scaledMarginLow / scale + + let scale: bigint; // positive scale applied to value and margin such + // that they can be represented as whole numbers + let scaledValue: bigint; // scale * mantissa + let scaledMarginLow: bigint; // scale * 0.5 * (distance between this floating- + // point number and its immediate lower value) + + // for normalized IEEE floating point values, each time the exponent is + // incremented the margin also doubles. That creates a subset of transition + // numbers where the high margin is twice the size of the low margin. + let scaledMarginHigh: bigint; + + if (hasUnequalMargins) { + if (exponent > 0) { // no fractional component + // 1. expand the input value by multiplying out the mantissa and exponent. + // this represents the input value in its whole number representation. + // 2. apply an additional scale of 2 such that later comparisons against + // the margin values are simplified. + // 3. set the margin value to the lowest mantissa bit's scale. + + scaledValue = 4n * mantissa; + scaledValue <<= bexponent; // 2 * 2 * mantissa*2^exponent + scale = 4n; // 2 * 2 * 1 + scaledMarginLow = 1n << bexponent; // 2 * 2^(exponent-1) + scaledMarginHigh = 1n << bexponent+1n; // 2 * 2 * 2^(exponent-1) + } else { // fractional component + // in order to track the mantissa data as an integer, we store it as is + // with a large scale + + scaledValue = 4n * mantissa; // 2 * 2 * mantissa + scale = 1n << -bexponent+2n; // 2 * 2 * 2^(-exponent) + scaledMarginLow = 1n; // 2 * 2^(-1) + scaledMarginHigh = 2n; // 2 * 2 * 2^(-1) + } + } else { + if (exponent > 0) { // no fractional component + // 1. expand the input value by multiplying out the mantissa and exponent. + // this represents the input value in its whole number representation. + // 2. apply an additional scale of 2 such that later comparisons against + // the margin values are simplified. + // 3. set the margin value to the lowest mantissa bit's scale. + + scaledValue = 2n * mantissa; + scaledValue <<= bexponent; // 2 * mantissa*2^exponent + scale = 2n; // 2 * 1 + scaledMarginLow = 1n << bexponent; // 2 * 2^(exponent-1) + scaledMarginHigh = scaledMarginLow; + } else { // fractional component + // in order to track the mantissa data as an integer, we store it as is + // with a large scale + + scaledValue = 2n * mantissa; // 2 * mantissa + scale = 1n << BigInt(-exponent + 1); // 2 * 2^(-exponent) + scaledMarginLow = 1n; // 2 * 2^(-1) + scaledMarginHigh = scaledMarginLow; + } + } + + // compute an estimate for digitExponent that will be correct or undershoot + // by one. this optimization is based on the paper "Printing Floating-Point + // Numbers Quickly and Accurately" by Burger and Dybvig. + // http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.4656&rep=rep1&type=pdf + // we perform an additional subtraction of 0.69 to increase the frequency of + // a failed estimate because that lets us take a faster branch in the code. + // 0.69 is chosen because 0.69 + log10(2) is less than one by a reasonable + // epsilon that will account for any floating point error. + + // we want to set digitExponent to floor(log10(v)) + 1 + // v = mantissa*2^exponent + // log2(v) = log2(mantissa) + exponent; + // log10(v) = log2(v) * log10(2) + // floor(log2(v)) = mantissaHighBitIdx + exponent; + // log10(v) - log10(2) < (mantissaHighBitIdx + exponent) * log10(2) <= log10(v) + // log10(v) < (mantissaHighBitIdx + exponent) * log10(2) + log10(2) <= log10(v) + log10(2) + // floor( log10(v) ) < ceil( (mantissaHighBitIdx + exponent) * log10(2) ) <= floor( log10(v) ) + 1 + //NOTE loses precision! wants a 64-bit float. but seems precise enough... + const log10_2 = 0.30102999566398119521373889472449; + let digitExponent = Math.ceil((mantissaHighBitIdx + exponent) * log10_2 - 0.69); + + // if the digit exponent is smaller than the smallest desired digit for + // fractional cutoff, pull the digit back into legal range at which point we + // will round to the appropriate value. + // note that while our value for digitExponent is still an estimate, this is + // safe because it only increases the number. this will either correct + // digitExponent to an accurate value or it will clamp it above the accurate + // value. + if (cutoffMode === 'fractionLength' && digitExponent <= -cutoffNumber) { + digitExponent = -cutoffNumber + 1; + } + + // scale adjustment for digit exponent, divide value by 10^digitExponent + if (digitExponent > 0) { + // the exponent is positive creating a division so we multiply up the scale + scale *= BigInt(10) ** BigInt(digitExponent); + } else if (digitExponent < 0) { + // the exponent is negative creating a multiplication so we multiply + // up the scaledValue, scaledMarginLow and scaledMarginHigh + const pow10 = BigInt(10) ** BigInt(-digitExponent); + scaledValue *= pow10; + scaledMarginLow *= pow10; + if (scaledMarginHigh !== scaledMarginLow) { + scaledMarginHigh *= scaledMarginLow; + } + } + + // if (value >= 1), our estimate for digitExponent was too low + if (scaledValue >= scale) { + // the exponent estimate was incorrect. increment the exponent and don't + // perform the premultiply needed for the first loop iteration. + digitExponent += 1; + } else { + // the exponent estimate was correct. multiply larger by the output base + // to prepare for the first loop iteration. + scaledValue *= 10n; + scaledMarginLow *= 10n; + if (scaledMarginHigh !== scaledMarginLow) scaledMarginHigh *= 10n; + } + + // compute the cutoff exponent (the exponent of the final digit to print). + // default to the maximum size of the output buffer. + let cutoffExponent = digitExponent - bufferSize; + if (cutoffMode === 'totalLength') { + let desired = digitExponent - cutoffNumber; + if (desired > cutoffExponent) cutoffExponent = desired; + } else if (cutoffMode === 'fractionLength') { + let desired = -cutoffNumber; + if (desired > cutoffExponent) cutoffExponent = desired; + } + + // output the exponent of the first digit we will print + outExponent = digitExponent - 1; + + //NOTE thanks to native bigints, no bit block normalization needed + + // these values are used to inspect why the print loop terminated so we can properly + // round the final digit. + let low = false; // did the value get within marginLow distance from zero + let high = false; // did the value get within marginHigh distance from one + let outputDigit = 0; // current digit being output + + if (cutoffMode === 'unique') { + // for the unique cutoff mode, we will try to print until we have reached + // a level of precision that uniquely distinguishes this value from its + // neighbors. if we run out of space in the output buffer, we exit early. + + while (true) { + digitExponent -= 1; + + // extract the digit + outputDigit = Number(scaledValue / scale); + scaledValue = scaledValue % scale; + + // update the high end of the value + let scaledValueHigh = scaledValue + scaledMarginHigh; + + // stop looping if we are far enough away from our neighboring values + // or if we have reached the cutoff digit + low = scaledValue < scaledMarginLow; + high = scaledValueHigh > scale; + if (low || high || (digitExponent === cutoffExponent)) break; + + // store the output digit + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit); + pCurDigit += 1; + + // mulitply larger by the output base + scaledValue *= 10n; + scaledMarginLow *= 10n; + if (scaledMarginHigh !== scaledMarginLow) scaledMarginHigh *= 10n; + } + } else { + // for length based cutoff modes, we will try to print until we have + // exhausted all precision (i.e. all remaining digits are zeros) or until + // we reach the desired cutoff digit. + + low = false; + high = false; + while (true) { + digitExponent -= 1; + + // extract the digit + outputDigit = Number(scaledValue / scale); + scaledValue = scaledValue % scale; + + if (scaledValue === 0n || digitExponent === cutoffExponent) break; + + // store the output digit + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit); + pCurDigit += 1; + + // multiply larger by the output base + scaledValue *= 10n; + } + } + + // round off the final digit. + // default to rounding down if value got too close to 0 + let roundDown = low; + + if (low === high) { // legal to round up and down + // round to the closest digit by comparing value with 0.5. to do this we + // need to convert the inequality to large integer values. + // compare( value, 0.5 ) + // compare( scale * value, scale * 0.5 ) + // compare( 2 * scale * value, scale ) + scaledValue *= 2n; + let compare = scaledValue < scale ? -1 + : (scaledValue > scale ? 1 : 0); + roundDown = compare < 0; + // if we are directly in the middle, round towards the even digit + // (i.e. IEEE rouding rules) + if (compare === 0) roundDown = (outputDigit & 1) === 0; + } + + // print the rounded digit + if (roundDown) { + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit); + pCurDigit += 1; + } else { + // handle rounding up + if (outputDigit === 9) { + // find the first non-nine prior digit + while (true) { + if (pCurDigit === 0) { // first digit + // output 1 at the next highest exponent + outBuffer[pCurDigit] = '1'; + pCurDigit += 1; + outExponent += 1; + break; + } + pCurDigit -= 1; + if (outBuffer[pCurDigit] !== '9') { + // increment the digit + outBuffer[pCurDigit] = String.fromCharCode(outBuffer[pCurDigit].charCodeAt(0) + 1); + pCurDigit += 1; + break; + } + } + } else { + // values in the range [0,8] can perform a simple round up + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit + 1); + pCurDigit += 1; + } + } + + // trim trailing zeroes, produce output + const digits = outBuffer.slice(0, pCurDigit).join(''); + return { digits, outExponent }; +} diff --git a/src/render.ts b/src/render.ts index e729bc6..6139dda 100644 --- a/src/render.ts +++ b/src/render.ts @@ -10,6 +10,7 @@ import { aura, coin } from './types'; import { formatDa } from './da'; import { patp } from './p'; import { patq } from './q'; +import { render as renderR } from './r'; // render(): scot() // scot(): render atom as specific aura @@ -72,10 +73,10 @@ export function rend(coin: coin): string { return '.' + patq(coin.atom); case 'r': switch(coin.aura[1]) { - case 'd': throw new Error('aura-js: @rd rendering unsupported'); //TODO - case 'h': throw new Error('aura-js: @rh rendering unsupported'); //TODO - case 'q': throw new Error('aura-js: @rq rendering unsupported'); //TODO - case 's': throw new Error('aura-js: @rs rendering unsupported'); //TODO + case 'd': return renderR('d', coin.atom); + case 'h': return renderR('h', coin.atom); + case 'q': return renderR('q', coin.atom); + case 's': return renderR('s', coin.atom); default: return zco(coin.atom); } case 'u': diff --git a/src/types.ts b/src/types.ts index 0f32fc6..bb75ff5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,10 @@ export type aura = 'c' | 'n' | 'p' | 'q' + | 'rd' + | 'rh' + | 'rq' + | 'rs' | 'sb' | 'sd' | 'si' diff --git a/test/data/atoms.ts b/test/data/atoms.ts index c567163..06d4694 100644 --- a/test/data/atoms.ts +++ b/test/data/atoms.ts @@ -302,6 +302,135 @@ export const INTEGER_TESTS: { }, ]; +// float tests generated like so (swapping out 32 for each size) +// +// =| n=@ud +// |- ^- (list [n=@ui =@rs]) +// ?: (gth n 20) ~ +// :_ $(n +(n), eny (shaz +(eny))) +// [. .]:(end 0^32 eny) + +export const FLOAT_16_TESTS: { + n: bigint, + rh: string +}[] = [ + { 'n': 2794n, 'rh': '.~~2.11e-4' }, + { 'n': 56111n, 'rh': '.~~-229.9' }, + { 'n': 52968n, 'rh': '.~~-27.62' }, + { 'n': 5299n, 'rh': '.~~1.147e-3' }, + { 'n': 16599n, 'rh': '.~~2.42' }, + { 'n': 20880n, 'rh': '.~~44.5' }, + { 'n': 32827n, 'rh': '.~~-3.5e-6' }, + { 'n': 42018n, 'rh': '.~~-0.01614' }, + { 'n': 6518n, 'rh': '.~~2.666e-3' }, + { 'n': 5290n, 'rh': '.~~1.139e-3' }, + { 'n': 13267n, 'rh': '.~~0.2445' }, + { 'n': 55960n, 'rh': '.~~-211' }, + { 'n': 5180n, 'rh': '.~~1.034e-3' }, + { 'n': 57898n, 'rh': '.~~-789' }, + { 'n': 64269n, 'rh': '.~~-57760' }, + { 'n': 27075n, 'rh': '.~~2950' }, + { 'n': 52518n, 'rh': '.~~-20.6' }, + { 'n': 2871n, 'rh': '.~~2.202e-4' }, + { 'n': 14390n, 'rh': '.~~0.5264' }, + { 'n': 22479n, 'rh': '.~~124.94' }, + { 'n': 53512n, 'rh': '.~~-40.25' }, + // specials + { 'n': 32256n, 'rh': '.~~nan' }, + { 'n': 31744n, 'rh': '.~~inf' }, + { 'n': 64512n, 'rh': '.~~-inf' }, +]; +export const FLOAT_32_TESTS: { + n: bigint, + rs: string +}[] = [ + { 'n': 3801684100n, 'rs': '.-1.4120592e21' }, + { 'n': 3198448935n, 'rs': '.-0.3212063' }, + { 'n': 1493470325n, 'rs': '.2.3318207e15' }, + { 'n': 3753392125n, 'rs': '.-2.6548713e19' }, + { 'n': 2367701358n, 'rs': '.-4.9382565e-31' }, + { 'n': 3955998488n, 'rs': '.-4.926287e26' }, + { 'n': 3770285654n, 'rs': '.-1.0721795e20' }, + { 'n': 774658779n, 'rs': '.3.9188968e-11' }, + { 'n': 1839384350n, 'rs': '.6.297161e27' }, + { 'n': 3200958240n, 'rs': '.-0.39598942' }, + { 'n': 1531710513n, 'rs': '.5.74343e16' }, + { 'n': 76604734n, 'rs': '.3.4064763e-36' }, + { 'n': 3849641890n, 'rs': '.-7.227392e22' }, + { 'n': 449588378n, 'rs': '.8.444448e-23' }, + { 'n': 2200559883n, 'rs': '.-4.9922973e-37' }, + { 'n': 3233902359n, 'rs': '.-6.044811' }, + { 'n': 1826097792n, 'rs': '.2.0894205e27' }, + { 'n': 2038589591n, 'rs': '.8.463999e34' }, + { 'n': 374924072n, 'rs': '.1.7520019e-25' }, + { 'n': 2891424105n, 'rs': '.-3.0642938e-12' }, + { 'n': 1504861925n, 'rs': '.6.2758604e15' }, + // specials + { 'n': 2143289344n, 'rs': '.nan' }, + { 'n': 2139095040n, 'rs': '.inf' }, + { 'n': 4286578688n, 'rs': '.-inf' }, +]; +export const FLOAT_64_TESTS: { + n: bigint, + rd: string +}[] = [ + { 'n': 4161079059275835235n, 'rd': '.~1.5344573483999128e-30' }, + { 'n': 9946657041402327650n, 'rd': '.~-2.6040520237797157e-260' }, + { 'n': 10945498080022120923n, 'rd': '.~-1.5219889995240713e-193' }, + { 'n': 12777619852430681908n, 'rd': '.~-4.352347778495045e-71' }, + { 'n': 10832826134033707969n, 'rd': '.~-4.476445969104017e-201' }, + { 'n': 5759527968158150641n, 'rd': '.~1.083878940297261e77' }, + { 'n': 14210857183243570667n, 'rd': '.~-2.7934289125168545e25' }, + { 'n': 802481108141386491n, 'rd': '.~5.057760190438517e-255' }, + { 'n': 1275779858016506694n, 'rd': '.~2.2132215938493265e-223' }, + { 'n': 13095741331018331181n, 'rd': '.~-7.863102918443266e-50' }, + { 'n': 1888824041075399563n, 'rd': '.~2.1134816073641755e-182' }, + { 'n': 1084036463832721993n, 'rd': '.~3.350404450282384e-236' }, + { 'n': 5378671490392695147n, 'rd': '.~3.906026148423093e51' }, + { 'n': 6949924683075251658n, 'rd': '.~4.095814633904063e156' }, + { 'n': 11922669081154321283n, 'rd': '.~-3.149243846108954e-128' }, + { 'n': 3348628534449979563n, 'rd': '.~7.952461579092363e-85' }, + { 'n': 16740885817600377897n, 'rd': '.~-3.5722157011559137e194' }, + { 'n': 10947731899891474740n, 'rd': '.~-2.0655661021482118e-193' }, + { 'n': 5258441678658489674n, 'rd': '.~3.5873091546780243e43' }, + { 'n': 13356339406634822690n, 'rd': '.~-2.0992411948796013e-32' }, + { 'n': 5682954223851386361n, 'rd': '.~8.25703042630886e71' }, + // specials + { 'n': 9221120237041090560n, 'rd': '.~nan' }, + { 'n': 9218868437227405312n, 'rd': '.~inf' }, + { 'n': 18442240474082181120n, 'rd': '.~-inf' }, +]; +export const FLOAT_128_TESTS: { + n: bigint, + rq: string +}[] = [ + { 'n': 68021193693829353317549889929563141156n, 'rq': '.~~~7.350512667020030909706173370706225e-989' }, + { 'n': 302103353625220804986267666991697023063n, 'rq': '.~~~-7.9549197455275592244500492866747605e2718' }, + { 'n': 246898801142481942446083786230981830385n, 'rq': '.~~~-2.2259646667707415960533566169084728e-482' }, + { 'n': 266347262120684140485161040153719834628n, 'rq': '.~~~-8.279222993489825223543556036825673e645' }, + { 'n': 221468995426755939807482572792716215704n, 'rq': '.~~~-1.111792035542746634059251896548544e-1956' }, + { 'n': 21301800639651122378660853950525964078n, 'rq': '.~~~1.7726070616810188982779118724916924e-3697' }, + { 'n': 14097152844847057880436990765628471568n, 'rq': '.~~~3.369589719043255367636184354903706e-4115' }, + { 'n': 295177098639635213272177198060962589505n, 'rq': '.~~~-2.2206978503537075954826511134765822e2317' }, + { 'n': 110688872525300968280230769566478882214n, 'rq': '.~~~3.6397750043610061937155700733186537e1485' }, + { 'n': 61599026415237650695220681488049467053n, 'rq': '.~~~3.4057177992615989177915966678096206e-1361' }, + { 'n': 172863816977031872621427160265490779418n, 'rq': '.~~~-1.2556914150614657185267070080341778e-4774' }, + { 'n': 302266284239331446242693035229785100109n, 'rq': '.~~~-2.350663948127252855536381243313653e2728' }, + { 'n': 156268960646491515609436753671493095692n, 'rq': '.~~~1.38238485325181967793853192964508e4128' }, + { 'n': 108198906770347465279793905792970007495n, 'rq': '.~~~1.6574125869800981414994144332269465e1341' }, + { 'n': 267024674209818006533062290780699457496n, 'rq': '.~~~-1.50776910344360132341073895837773e685' }, + { 'n': 90468551431184434281606009464311982071n, 'rq': '.~~~1.8958834323964283970820423084762307e313' }, + { 'n': 142729305176167457500565258995430273841n, 'rq': '.~~~1.4432040214189287854740316305289527e3343' }, + { 'n': 130858994578269367391522528661145525849n, 'rq': '.~~~9.267801652640158022832285899427825e2654' }, + { 'n': 9047595725142263795917295497258429378n, 'rq': '.~~~6.26545010819892388144698108939643e-4408' }, + { 'n': 262829608976719100907507105384827825295n, 'rq': '.~~~-9.321356263350323349343422220817277e441' }, + { 'n': 197646596458388884873804679655774313010n, 'rq': '.~~~-8.161207090132586143046770776173326e-3338' }, + // specials + { 'n': 170138587312039964317873038467719495680n, 'rq': '.~~~nan' }, + { 'n': 170135991163610696904058773219554885632n, 'rq': '.~~~inf' }, + { 'n': 340277174624079928635746076935438991360n, 'rq': '.~~~-inf' }, +]; + export const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; export const PHONETIC_TESTS: { n: bigint, diff --git a/test/parse.test.ts b/test/parse.test.ts index 719fe5a..ba88c2a 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,6 +1,7 @@ import { aura } from '../src/types'; import { tryParse as parse, decodeString, nuck, regex } from '../src/parse'; import { INTEGER_AURAS, INTEGER_TESTS, + FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, TEXT_AURAS, TEXT_TESTS, @@ -81,6 +82,10 @@ const OUR_DATE_TESTS: { ]; testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('float16', ['rh'], FLOAT_16_TESTS); +testAuras('float32', ['rs'], FLOAT_32_TESTS); +testAuras('float64', ['rd'], FLOAT_64_TESTS); +testAuras('float128', ['rq'], FLOAT_128_TESTS); testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); testAuras('date', DATE_AURAS, OUR_DATE_TESTS); testAuras('text', [ 't' ], TEXT_TESTS); @@ -155,3 +160,5 @@ describe('invalid syntax', () => { expect(nuck('.~mister--dister')).toEqual(null); }) }); + +//TODO oversized floats diff --git a/test/render.test.ts b/test/render.test.ts index bff77bb..e25e96a 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -1,6 +1,7 @@ import { aura, coin } from '../src/types'; import { render, rend } from '../src/render'; import { INTEGER_AURAS, INTEGER_TESTS, + FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, TEXT_AURAS, TEXT_TESTS, @@ -48,6 +49,10 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { } testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('float16', ['rh'], FLOAT_16_TESTS); +testAuras('float32', ['rs'], FLOAT_32_TESTS); +testAuras('float64', ['rd'], FLOAT_64_TESTS); +testAuras('float128', ['rq'], FLOAT_128_TESTS); testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); testAuras('date', DATE_AURAS, DATE_TESTS); testAuras('text', TEXT_AURAS, TEXT_TESTS); From 6ed9422d8aba167813a5f1b09a79ea5147e863ff Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 11:37:07 +0200 Subject: [PATCH 35/50] parse, render: simplify p and q modules These hadn't been given any treatment yet. The old library exposed a myriad of convertors for various number-string formats, check helpers, etc. Here we strip most of those out, leaving only core parsing and rendering logic in place, supplemented by simple validation logic and basic stdlib utilities for `@p`. For `@p` as well, we update its logic to work directly on the bigints. The restructuring, removal of redundant checks, and tweaks to the internals, give us a modest 10% performance improval on the combined `@p` and `@q` fuzz tests. --- src/hoon/index.ts | 18 +--- src/p.ts | 264 +++++++++++++++++---------------------------- src/parse.ts | 31 ++---- src/q.ts | 122 ++++----------------- src/render.ts | 10 +- test/data/atoms.ts | 9 ++ test/p.test.ts | 196 +++++---------------------------- test/parse.test.ts | 3 +- test/q.test.ts | 111 +++---------------- 9 files changed, 188 insertions(+), 576 deletions(-) diff --git a/src/hoon/index.ts b/src/hoon/index.ts index 2bb0359..7d7b588 100644 --- a/src/hoon/index.ts +++ b/src/hoon/index.ts @@ -42,15 +42,6 @@ export const suffixes = suf.match(/.{1,3}/g) as RegExpMatchArray; export const bex = (n: bigint) => 2n ** n; -export const rsh = (a: bigint, b: bigint, c: bigint) => - c / bex(bex(a) * b); - -export const met = (a: bigint, b: bigint, c = 0n): bigint => - (b === 0n) ? c : met(a, rsh(a, 1n, b), c + 1n); - -export const end = (a: bigint, b: bigint, c: bigint) => - c % bex(bex(a) * b); - export const patp2syls = (name: string): string[] => name.replace(/[\^~-]/g, '').match(/.{1,3}/g) || []; @@ -66,16 +57,13 @@ export const patp2syls = (name: string): string[] => * @param {String} name a @p or @q value * @return {boolean} */ +//TODO rename validSyllables export function isValidPat(name: string): boolean { if (typeof name !== 'string') { throw new Error('isValidPat: non-string input'); } - - const leadingTilde = name.slice(0, 1) === '~'; - - if (leadingTilde === false || name.length < 4) { - return false; - } else { + else if (name.length < 4 || name[0] !== '~') return false; + else { const syls = patp2syls(name); const wrongLength = syls.length % 2 !== 0 && syls.length !== 1; const sylsExist = syls.reduce( diff --git a/src/p.ts b/src/p.ts index 371e97b..995b574 100644 --- a/src/p.ts +++ b/src/p.ts @@ -3,41 +3,22 @@ import { patp2syls, suffixes, prefixes, - met, - end, - rsh, } from './hoon'; import ob from './hoon/ob'; -//NOTE the logic in this file has not yet been updated for the latest broader -// aura-js implementation style. but We Make It Workโ„ข. +//NOTE matches for shape, not syllables +export const regexP = /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/; /** - * Convert a hex-encoded string to a @p-encoded string. - * - * @param {String} hex - * @return {String} + * Convert a valid `@p` literal string to a bigint. + * @param {String} str certified-sane `@p` literal string */ -export function hex2patp(hex: string): string { - if (hex === null) { - throw new Error('hex2patp: null input'); - } - return patp(BigInt('0x'+hex)); -} +export function parseP(str: string, scramble: boolean = true): bigint { + const syls = patp2syls(str); -/** - * Convert a @p-encoded string to a hex-encoded string. - * - * @param {String} name @p - * @return {String} - */ -export function patp2hex(name: string): string { - if (isValidPat(name) === false) { - throw new Error('patp2hex: not a valid @p'); + const syl2bin = (idx: number) => { + return idx.toString(2).padStart(8, '0'); //NOTE base16 isn't any faster } - const syls = patp2syls(name); - - const syl2bin = (idx: number) => idx.toString(2).padStart(8, '0'); const addr = syls.reduce( (acc, syl, idx) => @@ -47,95 +28,80 @@ export function patp2hex(name: string): string { '' ); - const bn = BigInt('0b'+addr); - const hex = ob.fynd(bn).toString(16); - return hex.length % 2 !== 0 ? hex.padStart(hex.length + 1, '0') : hex; + const num = BigInt('0b' + addr); + return scramble ? ob.fynd(num) : num; } -/** - * Convert a @p-encoded string to a bignum. - * - * @param {String} name @p - * @return {bigint} - */ -export function patp2bn(name: string): bigint { - return BigInt('0x'+patp2hex(name)); +function checkedParseP(str: string): bigint { + if (!isValidP(str)) throw new Error('invalid @p literal: ' + str); + return parseP(str); } +export type size = 'galaxy' | 'star' | 'planet' | 'moon' | 'comet'; +export type rank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn'; /** - * Convert a @p-encoded string to a decimal-encoded string. + * Determine the `$rank` of a `@p` value or literal. * - * @param {String} name @p + * @param {String} @p * @return {String} */ -export function patp2dec(name: string): string { - let bn: bigint; - try { - bn = patp2bn(name); - } catch (_) { - throw new Error('patp2dec: not a valid @p'); - } - return bn.toString(); +export function clan(who: bigint | string): rank { + let num: bigint; + if (typeof who === 'bigint') num = who; + else num = checkedParseP(who); + + return num <= 0xFFn + ? 'czar' + : num <= 0xFFFFn + ? 'king' + : num <= 0xFFFFFFFFn + ? 'duke' + : num <= 0xFFFFFFFFFFFFFFFFn + ? 'earl' + : 'pawn'; } -/** - * Determine the ship class of a @p value. - * - * @param {String} @p - * @return {String} - */ -export function clan(who: string): string { - let name: bigint; - try { - name = patp2bn(who); - } catch (_) { - throw new Error('clan: not a valid @p'); +export function kind(who: bigint | string): size { + return rankToSize(clan(who)); +} +export function rankToSize(rank: rank): size { + switch (rank) { + case 'czar': return 'galaxy'; + case 'king': return 'star'; + case 'duke': return 'planet'; + case 'earl': return 'moon'; + case 'pawn': return 'comet'; } - - const wid = met(3n, name); - return wid <= 1n - ? 'galaxy' - : wid === 2n - ? 'star' - : wid <= 4n - ? 'planet' - : wid <= 8n - ? 'moon' - : 'comet'; } /** - * Determine the parent of a @p value. + * Determine the parent of a `@p` value. Throws on invalid string inputs. * - * @param {String} @p + * @param {String | number} who `@p` value or literal string * @return {String} */ -export function sein(name: string): string { - let who: bigint; - try { - who = patp2bn(name); - } catch (_) { - throw new Error('sein: not a valid @p'); - } +export function sein(who: bigint): bigint; +export function sein(who: string): string; +export function sein(who: bigint | string): typeof who { + let num: bigint; + if (typeof who === 'bigint') num = who; + else num = checkedParseP(who); - let mir: string; - try { - mir = clan(name); - } catch (_) { - throw new Error('sein: not a valid @p'); - } + let mir = clan(num); const res = - mir === 'galaxy' - ? who - : mir === 'star' - ? end(3n, 1n, who) - : mir === 'planet' - ? end(4n, 1n, who) - : mir === 'moon' - ? end(5n, 1n, who) - : 0n; - return patp(res); + mir === 'czar' + ? num + : mir === 'king' + ? num & 0xFFn + : mir === 'duke' + ? num & 0xFFFFn + : mir === 'earl' + ? num & 0xFFFFFFFFn + : num & 0xFFFFn; + + if (typeof who === 'bigint') return res; + else return renderP(res); } /** @@ -144,97 +110,61 @@ export function sein(name: string): string { * @param {String} str a string * @return {boolean} */ -export function isValidPatp(str: string): boolean { - return isValidPat(str) && str === patp(patp2dec(str)); +export function isValidP(str: string): boolean { + return regexP.test(str) // general structure + && isValidPat(str) // valid syllables + && str === renderP(parseP(str)); // no leading zeroes //TODO can isValidPat check this? +} + +export function parseValidP(str: string): bigint | null { + if (!regexP.test(str) || !isValidPat(str)) return null; + const res = parseP(str); + return (str === renderP(res)) ? res : null; } /** * Convert a number to a @p-encoded string. * - * @param {String, Number, bigint} arg + * @param {bigint} arg * @return {String} */ -export function patp(arg: string | number | bigint) { - if (arg === null) { - throw new Error('patp: null input'); - } - const n = BigInt(arg); - - const sxz = ob.fein(n); - const dyy = met(4n, sxz); +export function renderP(arg: bigint, scramble: boolean = true) { + const sxz = scramble ? ob.fein(arg) : arg; + const dyx = Math.ceil(sxz.toString(16).length / 2); + const dyy = Math.ceil(sxz.toString(16).length / 4); - function loop(tsxz: bigint, timp: bigint, trep: string): string { - const log = end(4n, 1n, tsxz); - const pre = prefixes[Number(rsh(3n, 1n, log))]; - const suf = suffixes[Number(end(3n, 1n, log))]; - const etc = (timp % 4n) === 0n ? ((timp === 0n) ? '' : '--') : '-'; + function loop(tsxz: bigint, timp: number, trep: string): string { + const log = tsxz & 0xFFFFn; + const pre = prefixes[Number(log >> 8n)]; + const suf = suffixes[Number(log & 0xFFn)]; + const etc = (timp & 0b11) ? '-' : ((timp === 0) ? '' : '--'); const res = pre + suf + etc + trep; - return timp === dyy ? trep : loop(BigInt(rsh(4n, 1n, tsxz).toString()), timp + 1n, res); + return timp === dyy ? trep : loop(tsxz >> 16n, timp + 1, res); } - const dyx = BigInt(met(3n, sxz).toString()); - return ( - '~' + (dyx <= 1n ? suffixes[Number(sxz)] : loop(BigInt(sxz.toString()), 0n, '')) + '~' + (dyx <= 1 ? suffixes[Number(sxz)] : loop(sxz, 0, '')) ); } /** - * Ensure @p is sigged. - * - * @param {String} str a string - * @return {String} - */ -export function preSig(ship: string): string { - if (!ship) { - return ''; - } - - if (ship.trim().startsWith('~')) { - return ship.trim(); - } - - return '~'.concat(ship.trim()); -} - -/** - * Remove sig from @p - * - * @param {String} str a string - * @return {String} - */ -export function deSig(ship: string): string { - if (!ship) { - return ''; - } - - return ship.replace('~', ''); -} - -/** - * Trim @p to short form + * Render short-form ship name. Throws on invalid string inputs. * - * @param {String} str a string + * @param {String | number} who `@p` value or literal string * @return {String} */ -export function cite(ship: string): string | null { - if (!ship) { - return null; - } - - const patp = deSig(ship); - - // comet - if (patp.length === 56) { - return preSig(patp.slice(0, 6) + '_' + patp.slice(50, 56)); +export function cite(who: bigint | string): string { + let num: bigint; + if (typeof who === 'bigint') num = who; + else num = checkedParseP(who); + + if (num <= 0xFFFFFFFFn) { + return renderP(num); + } else if (num <= 0xFFFFFFFFFFFFFFFFn) { + return renderP(num & 0xFFFFFFFFn).replace('-', '^'); + } else { + return renderP(BigInt('0x'+num.toString(16).slice(0,4))) + '_' + renderP(num & 0xFFFFn).slice(1); } - - // moon - if (patp.length === 27) { - return preSig(patp.slice(14, 20) + '^' + patp.slice(21, 27)); - } - - return preSig(patp); } diff --git a/src/parse.ts b/src/parse.ts index 5ececb7..4ad2dbb 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -3,13 +3,13 @@ // atom literal parsing from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @r*, @if, @is +//TODO unsupported auras: @dr, @if, @is import { aura, dime, coin } from './types'; import { parseDa } from './da'; -import { isValidPatp, patp2bn } from './p'; -import { isValidPatq, patq2bn } from './q'; +import { parseValidP, regexP } from './p'; +import { parseQ, parseValidQ } from './q'; import { parse as parseR, precision } from './r'; function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { @@ -29,7 +29,7 @@ export const regex: { [key in aura]: RegExp } = { 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))?(\.(\.[0-9a-f]{4})+)?$/, 'f': /^\.(y|n)$/, 'n': /^~$/, - 'p': /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/, //NOTE matches shape but not syllables + 'p': regexP, //NOTE matches shape but not syllables 'q': /^\.~(([a-z]{3}|[a-z]{6})(\-[a-z]{6})*)$/, //NOTE matches shape but not syllables 'rd': floatRegex(1), 'rh': floatRegex(2), @@ -168,17 +168,9 @@ export function nuck(str: string): coin | null { return { type: 'dime', aura, atom: parseR(aura[1] as precision, str) }; } else if (str[1] === '~' && regex['q'].test(str)) { - const q = str.slice(1); //NOTE q.ts insanity, need to strip leading . - try { - if (!isValidPatq(q)) { - console.log('invalid @q', q); - return null; - } else { - return { type: 'dime', aura: 'q', atom: patq2bn(q) } - } - } catch(e) { - return null; - } + const num = parseValidQ(str); + if (num === null) return null; + return { type: 'dime', aura: 'q', atom: num }; } else //TODO %is, %if // "zust" if (str[1] === '_' && /^\.(_([0-9a-zA-Z\-\.]|~\-|~~)+)*__$/.test(str)) { // "nusk" @@ -209,11 +201,10 @@ export function nuck(str: string): coin | null { return null; } else if (regex['p'].test(str)) { - if (!isValidPatp(str)) { - return null; - } else { - return { type: 'dime', aura: 'p', atom: patp2bn(str) }; - } + //NOTE this still does the regex check twice... + const res = parseValidP(str); + if (res === null) return null; + return { type: 'dime', aura: 'p', atom: res }; } else //TODO test if these single-character checks affect performance or no, // or if we want to move them further up, etc. diff --git a/src/q.ts b/src/q.ts index d4c1640..4fe3e33 100644 --- a/src/q.ts +++ b/src/q.ts @@ -1,8 +1,5 @@ import { isValidPat, prefixes, suffixes } from './hoon'; -import { chunk, splitAt } from './utils'; - -//NOTE the logic in this file has not yet been updated for the latest broader -// aura-js implementation style. but We Make It Workโ„ข. +import { chunk, splitAt } from './utils'; //TODO inline //TODO investigate whether native UintArrays are more portable // than node Buffers @@ -13,22 +10,11 @@ import { chunk, splitAt } from './utils'; * @param {String, Number, bigint} arg * @return {String} */ -export function patq(arg: string | number | bigint) { - const bn = BigInt(arg); +export function renderQ(num: bigint): string { //NOTE stupid hack to work around bad node Buffer spec - const hex = bn.toString(16); + const hex = num.toString(16); const lex = hex.length; const buf = Buffer.from(hex.padStart(lex+lex%2, '0'), 'hex'); - return buf2patq(buf); -} - -/** - * Convert a Buffer into a @q-encoded string. - * - * @param {Buffer} buf - * @return {String} - */ -function buf2patq(buf: Buffer): string { const chunked = buf.length % 2 !== 0 && buf.length > 1 ? [[buf[0]]].concat(chunk(Array.from(buf.slice(1)), 2)) @@ -48,36 +34,19 @@ function buf2patq(buf: Buffer): string { pair.length % 2 !== 0 && chunked.length > 1 ? prefixName(pair) : name(pair); return chunked.reduce( - (acc, elem) => acc + (acc === '~' ? '' : '-') + alg(elem), - '~' + (acc, elem) => acc + (acc === '.~' ? '' : '-') + alg(elem), + '.~' ); } /** - * Convert a hex-encoded string to a @q-encoded string. - * - * Note that this preserves leading zero bytes. - * - * @param {String} hex - * @return {String} - */ -export function hex2patq(arg: string): string { - const hex = arg.length % 2 !== 0 ? arg.padStart(arg.length + 1, '0') : arg; - - const buf = Buffer.from(hex, 'hex'); - return buf2patq(buf); -} - -/** - * Convert a @q-encoded string to a hex-encoded string. - * - * Note that this preserves leading zero bytes. + * Convert a @q-encoded string to a bigint * * @param {String} name @q * @return {String} */ -export function patq2hex(name: string): string { - const chunks = name.slice(1).split('-'); +export function parseQ(str: string): bigint { + const chunks = str.slice(2).split('-'); const dec2hex = (dec: number) => { if (dec < 0) throw new Error('malformed @q'); return dec.toString(16).padStart(2, '0'); @@ -90,31 +59,16 @@ export function patq2hex(name: string): string { : dec2hex(prefixes.indexOf(syls[0])) + dec2hex(suffixes.indexOf(syls[1])); }); - return name.length === 0 ? '00' : splat.join(''); + return BigInt('0x' + (str.length === 0 ? '00' : splat.join(''))); } -/** - * Convert a @q-encoded string to a bignum. - * - * @param {String} name @q - * @return {bigint} - */ -export const patq2bn = (name: string): bigint => BigInt('0x'+patq2hex(name)); - -/** - * Convert a @q-encoded string to a decimal-encoded string. - * - * @param {String} name @q - * @return {String} - */ -export function patq2dec(name: string): string { - let bn: bigint; +export function parseValidQ(str: string): bigint | null { try { - bn = patq2bn(name); - } catch (_) { - throw new Error('patq2dec: not a valid @q'); + const num = parseQ(str); + return num; + } catch (e) { + return null; } - return bn.toString(); } /** @@ -123,48 +77,12 @@ export function patq2dec(name: string): string { * @param {String} str a string * @return {boolean} */ -export const isValidPatq = (str: string): boolean => { +export function isValidQ(str: string): boolean { if (str === '') return false; - try { return eqPatq(str, patq(patq2dec(str))); } catch (e) { return false; } -}; - -/** - * Remove all leading zero bytes from a sliceable value. - * @param {String} - * @return {String} - */ -const removeLeadingZeroBytes = (str: string): string => - str.slice(0, 2) === '00' ? removeLeadingZeroBytes(str.slice(2)) : str; - -/** - * Equality comparison, modulo leading zero bytes. - * @param {String} - * @param {String} - * @return {Bool} - */ -const eqModLeadingZeroBytes = (s: string, t: string): boolean => - removeLeadingZeroBytes(s) === removeLeadingZeroBytes(t); - -/** - * Equality comparison on @q values. - * @param {String} p a @q-encoded string - * @param {String} q a @q-encoded string - * @return {Bool} - */ -export function eqPatq(p: string, q: string): boolean { - let phex; - try { - phex = patq2hex(p); - } catch (_) { - throw new Error('eqPatq: not a valid @q'); - } - - let qhex; try { - qhex = patq2hex(q); - } catch (_) { - throw new Error('eqPatq: not a valid @q'); + parseQ(str); + return true; + } catch (e) { + return false; } - - return eqModLeadingZeroBytes(phex, qhex); -} +}; diff --git a/src/render.ts b/src/render.ts index 6139dda..c3b2ea6 100644 --- a/src/render.ts +++ b/src/render.ts @@ -3,13 +3,13 @@ // atom literal rendering from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @r*, @if, @is +//TODO unsupported auras: @dr, @if, @is import { aura, coin } from './types'; import { formatDa } from './da'; -import { patp } from './p'; -import { patq } from './q'; +import { renderP } from './p'; +import { renderQ } from './q'; import { render as renderR } from './r'; // render(): scot() @@ -68,9 +68,9 @@ export function rend(coin: coin): string { default: return zco(coin.atom); } case 'p': - return patp(coin.atom); + return renderP(coin.atom); case 'q': - return '.' + patq(coin.atom); + return renderQ(coin.atom); case 'r': switch(coin.aura[1]) { case 'd': return renderR('d', coin.atom); diff --git a/test/data/atoms.ts b/test/data/atoms.ts index 06d4694..f48a7cb 100644 --- a/test/data/atoms.ts +++ b/test/data/atoms.ts @@ -478,6 +478,10 @@ export const PHONETIC_TESTS: { 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, + { 'n': 773182725n, + 'p': '~dozreg-palfun', + 'q': '.~fospeg-fopper', + }, { 'n': 319478973361751151n, 'p': '~sampel-sampel-lacwyl-tirder', 'q': '.~sampel-sampel-dozpel-sampel', @@ -486,6 +490,11 @@ export const PHONETIC_TESTS: { 'p': '~sampel-sampel-dozzod-sampel', 'q': '.~sampel-sampel-dozzod-sampel', }, + // absurdly long + { 'n': 0x7468697320697320736f6d6520766572792068696768207175616c69747920656e74726f7079n, + 'p': '~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud', + 'q': '.~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud' + }, ]; export const DATE_AURAS: aura[] = [ 'da' ]; diff --git a/test/p.test.ts b/test/p.test.ts index c8399fd..8295a43 100644 --- a/test/p.test.ts +++ b/test/p.test.ts @@ -1,122 +1,31 @@ -import jsc from 'jsverify'; import { - patp, - patp2hex, - hex2patp, - patp2dec, - clan, + kind, sein, - isValidPatp, - preSig, - deSig, + isValidP, cite, } from '../src/p'; -const patps = jsc.uint32.smap( - (num) => patp(num), - (pp) => parseInt(patp2dec(pp)) -); - describe('@p', () => { - it('patp2dec matches expected reference values', () => { - expect(patp2dec('~zod')).toEqual('0'); - expect(patp2dec('~lex')).toEqual('200'); - expect(patp2dec('~binzod')).toEqual('512'); - expect(patp2dec('~samzod')).toEqual('1024'); - expect(patp2dec('~poldec-tonteg')).toEqual('9896704'); - expect(patp2dec('~nidsut-tomdun')).toEqual('15663360'); - expect(patp2dec('~morlyd-mogmev')).toEqual('3108299008'); - expect(patp2dec('~fipfes-morlyd')).toEqual('479733505'); - }); - - it('patp matches expected reference values', () => { - expect(patp('0')).toEqual('~zod'); - expect(patp('200')).toEqual('~lex'); - expect(patp('512')).toEqual('~binzod'); - expect(patp('1024')).toEqual('~samzod'); - expect(patp('9896704')).toEqual('~poldec-tonteg'); - expect(patp('15663360')).toEqual('~nidsut-tomdun'); - expect(patp('3108299008')).toEqual('~morlyd-mogmev'); - expect(patp('479733505')).toEqual('~fipfes-morlyd'); - }); - - it('large patp values match expected reference values', () => { - expect( - hex2patp( - '7468697320697320736f6d6520766572792068696768207175616c69747920656e74726f7079' - ) - ).toEqual( - '~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud' - ); - }); - - it('patp throws on null input', () => { - let input = () => patp(null as any); - expect(input).toThrow(); - }); - - it('hex2patp throws on null input', () => { - let input = () => hex2patp(null as any); - expect(input).toThrow(); - }); - - it('patp2hex throws on invalid patp', () => { - let input = () => patp2hex('nidsut-tomdun'); - expect(input).toThrow(); - input = () => patp2hex('~nidsut-tomdzn'); - expect(input).toThrow(); - input = () => patp2hex('~sut-tomdun'); - expect(input).toThrow(); - input = () => patp2hex('~nidsut-dun'); - expect(input).toThrow(); - input = () => patp2hex(null as any); - expect(input).toThrow(); - }); - - it('patp and patp2dec are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patp2dec(patp(num))) === num - ); - - let iso1 = jsc.forall(patps, (pp) => patp(patp2dec(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - - it('patp2hex and hex2patp are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patp2hex(hex2patp(num.toString(16))), 16) === num - ); - - let iso1 = jsc.forall(patps, (pp) => hex2patp(patp2hex(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - describe('clan/sein', () => { - it('clan works as expected', () => { - expect(clan('~zod')).toEqual('galaxy'); - expect(clan('~fes')).toEqual('galaxy'); - expect(clan('~marzod')).toEqual('star'); - expect(clan('~fassec')).toEqual('star'); - expect(clan('~dacsem-fipwex')).toEqual('planet'); - expect(clan('~fidnum-rosbyt')).toEqual('planet'); - expect(clan('~doznec-bannux-nopfen')).toEqual('moon'); - expect(clan('~dozryt--wolmep-racmyl-padpeg-mocryp')).toEqual('comet'); + it('clan/kind works as expected', () => { + expect(kind('~zod')).toEqual('galaxy'); + expect(kind('~fes')).toEqual('galaxy'); + expect(kind('~marzod')).toEqual('star'); + expect(kind('~fassec')).toEqual('star'); + expect(kind('~dacsem-fipwex')).toEqual('planet'); + expect(kind('~fidnum-rosbyt')).toEqual('planet'); + expect(kind('~doznec-bannux-nopfen')).toEqual('moon'); + expect(kind('~dozryt--wolmep-racmyl-padpeg-mocryp')).toEqual('comet'); }); it('clan throws on invalid input', () => { - let input = () => clan('~zord'); + let input = () => kind('~zord'); expect(input).toThrow(); - input = () => clan('zod'); + input = () => kind('zod'); expect(input).toThrow(); - input = () => clan('~nid-tomdun'); + input = () => kind('~nid-tomdun'); expect(input).toThrow(); - input = () => clan(null as any); + input = () => kind(null as any); expect(input).toThrow(); }); @@ -129,6 +38,7 @@ describe('@p', () => { expect(sein('~fassec')).toEqual('~sec'); expect(sein('~nidsut-tomdun')).toEqual('~marzod'); expect(sein('~sansym-ribnux')).toEqual('~marnec'); + expect(sein('~sampel-sampel-sampel-sampel--sampel-sampel-sansym-ribnux')).toEqual('~ribnux'); }); it('sein throws on invalid input', () => { @@ -138,65 +48,29 @@ describe('@p', () => { expect(input).toThrow(); input = () => sein('~nid-tomdun'); expect(input).toThrow(); - input = () => sein(null as any); - expect(input).toThrow(); }); }); - describe('isValidPatp', () => { - it('isValidPatp returns true for valid @p values', () => { - expect(isValidPatp('~zod')).toEqual(true); - expect(isValidPatp('~marzod')).toEqual(true); - expect(isValidPatp('~nidsut-tomdun')).toEqual(true); + describe('isValidP', () => { + it('isValidP returns true for valid @p values', () => { + expect(isValidP('~zod')).toEqual(true); + expect(isValidP('~marzod')).toEqual(true); + expect(isValidP('~nidsut-tomdun')).toEqual(true); }); - it('isValidPatp returns false for invalid @p values', () => { - expect(isValidPatp('')).toEqual(false); - expect(isValidPatp('~')).toEqual(false); - expect(isValidPatp('~hu')).toEqual(false); - expect(isValidPatp('~what')).toEqual(false); - expect(isValidPatp('sudnit-duntom')).toEqual(false); - }); - }); - - describe('preSig', () => { - it('preSig adds a sig if missing', () => { - expect(preSig('nocsyx-lassul')).toEqual('~nocsyx-lassul'); - }); - - it('preSig ignores sig if there already', () => { - expect(preSig('~nocsyx-lassul')).toEqual('~nocsyx-lassul'); - }); - - it('preSig ignores whitespace', () => { - expect(preSig(' nocsyx-lassul')).toEqual('~nocsyx-lassul'); - }); - - it('preSig ignores empty values safely', () => { - expect(preSig(null as any)).toEqual(''); - expect(() => preSig(null as any)).not.toThrow(); - }); - }); - - describe('deSig', () => { - it('deSig removes the sig if present', () => { - expect(deSig('~nocsyx-lassul')).toEqual('nocsyx-lassul'); - }); - - it('deSig ignores if no sig', () => { - expect(deSig('nocsyx-lassul')).toEqual('nocsyx-lassul'); - }); - - it('deSig ignores empty values safely', () => { - expect(deSig(null as any)).toEqual(''); - expect(() => deSig(null as any)).not.toThrow(); + it('isValidP returns false for invalid @p values', () => { + expect(isValidP('')).toEqual(false); + expect(isValidP('~')).toEqual(false); + expect(isValidP('~hu')).toEqual(false); + expect(isValidP('~what')).toEqual(false); + expect(isValidP('sudnit-duntom')).toEqual(false); }); }); describe('cite', () => { it('cite shortens moons', () => { expect(cite('~mister-mister-nocsyx-lassul')).toEqual('~nocsyx^lassul'); - expect(cite('~dozzod-dozzod-nocsyx-lassul')).toEqual('~nocsyx^lassul'); + expect(cite('~binzod-dozzod-nocsyx-lassul')).toEqual('~nocsyx^lassul'); }); it('cite shortens comets', () => { @@ -212,25 +86,11 @@ describe('@p', () => { expect(cite('~zod')).toEqual('~zod'); expect(cite('~marzod')).toEqual('~marzod'); expect(cite('~marzod-marzod')).toEqual('~marzod-marzod'); - expect(cite('~marzod-marzod-marzod')).toEqual('~marzod-marzod-marzod'); }); it('cite always returns sigged values', () => { expect(cite('~zod')?.at(0)).toEqual('~'); expect(cite('~zod')).toEqual('~zod'); }); - - it('cite accepts unsigged values', () => { - expect(cite('zod')).toEqual('~zod'); - expect(cite('mister-mister-nocsyx-lassul')).toEqual('~nocsyx^lassul'); - expect( - cite('roldex-navmev-biltyp-dozzod--dozzod-lapled-binnum-binzod') - ).toEqual('~roldex_binzod'); - }); - - it('cite ignores empty values safely', () => { - expect(cite(null as any)).toEqual(null); - expect(() => cite(null as any)).not.toThrow(); - }); }); }); diff --git a/test/parse.test.ts b/test/parse.test.ts index ba88c2a..e062d08 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -151,6 +151,7 @@ describe('invalid syntax', () => { expect(nuck('~zad')).toEqual(null); expect(nuck('.~zad')).toEqual(null); expect(nuck('~zodbin')).toEqual(null); + expect(nuck('~dozzod')).toEqual(null); expect(nuck('.~zodbin')).toEqual(null); expect(nuck('~funpal')).toEqual(null); expect(nuck('.~funpal')).toEqual(null); @@ -158,7 +159,7 @@ describe('invalid syntax', () => { expect(nuck('.~nidsut-dun')).toEqual(null); expect(nuck('~mister--dister')).toEqual(null); expect(nuck('.~mister--dister')).toEqual(null); - }) + }); }); //TODO oversized floats diff --git a/test/q.test.ts b/test/q.test.ts index 257bf24..ad62214 100644 --- a/test/q.test.ts +++ b/test/q.test.ts @@ -1,105 +1,20 @@ -import jsc from 'jsverify'; -import { - patq, - patq2hex, - hex2patq, - patq2dec, - eqPatq, - isValidPatq, -} from '../src/q'; - -const patqs = jsc.uint32.smap( - (num) => patq(num), - (pq) => parseInt(patq2dec(pq)) -); +import { isValidQ } from '../src/q'; describe('patq, etc.', () => { - it('patq2dec matches expected reference values', () => { - expect(patq2dec('~zod')).toEqual('0'); - expect(patq2dec('~binzod')).toEqual('512'); - expect(patq2dec('~samzod')).toEqual('1024'); - expect(patq2dec('~poldec-tonteg')).toEqual('4016240379'); - expect(patq2dec('~nidsut-tomdun')).toEqual('1208402137'); - }); - - it('patq matches expected reference values', () => { - expect(patq('0')).toEqual('~zod'); - expect(patq('512')).toEqual('~binzod'); - expect(patq('1024')).toEqual('~samzod'); - expect(patq('4016240379')).toEqual('~poldec-tonteg'); - expect(patq('1208402137')).toEqual('~nidsut-tomdun'); - }); - - it('large patq values match expected reference values', () => { - expect(hex2patq('01010101010101010102')).toEqual( - '~marnec-marnec-marnec-marnec-marbud' - ); - expect( - hex2patq( - '6d7920617765736f6d65207572626974207469636b65742c206920616d20736f206c75636b79' - ) - ).toEqual( - '~tastud-holruc-sidwet-salpel-taswet-holdeg-paddec-davdut-holdut-davwex-balwet-divwen-holdet-holruc-taslun-salpel-holtux-dacwex-baltud' - ); - }); - - it('patq2hex throws on invalid patp', () => { - let input = () => patq2hex('nidsut-tomdun'); - expect(input).toThrow(); - input = () => patq2hex('~nidsut-tomdzn'); - expect(input).toThrow(); - input = () => patq2hex('~nidsut-dun'); - expect(input).toThrow(); - input = () => patq2hex(null as any); - expect(input).toThrow(); - }); - - it('patq and patq2dec are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patq2dec(patq(num))) === num - ); - - let iso1 = jsc.forall(patqs, (pp) => patq(patq2dec(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - - it('patq2hex and hex2patq are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patq2hex(hex2patq(num.toString(16))), 16) === num - ); - - let iso1 = jsc.forall(patqs, (pp) => hex2patq(patq2hex(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - - describe('eqPatq', () => { - it('works as expected', () => { - expect(eqPatq('~dozzod-dozzod', '~zod')).toEqual(true); - expect(eqPatq('~dozzod-mardun', '~mardun')).toEqual(true); - expect(eqPatq('~dozzod-mardun', '~mardun-dozzod')).toEqual(false); - }); - }); - - describe('isValidPatq', () => { - it('isValidPatq returns true for valid @p values', () => { - expect(isValidPatq('~zod')).toEqual(true); - expect(isValidPatq('~marzod')).toEqual(true); - expect(isValidPatq('~nidsut-tomdun')).toEqual(true); - expect(isValidPatq('~dozzod-binwes-nidsut-tomdun')).toEqual(true); + describe('isValidQ', () => { + it('isValidQ returns true for valid @p values', () => { + expect(isValidQ('.~zod')).toEqual(true); + expect(isValidQ('.~marzod')).toEqual(true); + expect(isValidQ('.~nidsut-tomdun')).toEqual(true); + expect(isValidQ('.~dozzod-binwes-nidsut-tomdun')).toEqual(true); }); - it('isValidPatq returns false for invalid @p values', () => { - expect(isValidPatq('')).toEqual(false); - expect(isValidPatq('~')).toEqual(false); - expect(isValidPatq('~hu')).toEqual(false); - expect(isValidPatq('~what')).toEqual(false); - expect(isValidPatq('sudnit-duntom')).toEqual(false); + it('isValidQ returns false for invalid @p values', () => { + expect(isValidQ('')).toEqual(false); + expect(isValidQ('.~')).toEqual(false); + expect(isValidQ('.~hu')).toEqual(false); + expect(isValidQ('.~what')).toEqual(false); + expect(isValidQ('sudnit-duntom')).toEqual(false); }); }); }); From 084b9d5cb4a35e4b14afb4b6c2a1f5c6fce81811 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 14:37:53 +0200 Subject: [PATCH 36/50] lib: move code around, do renamings For coherence, we move some code around. Out of separate utility files and into the only files that import them. We rename some functions for consistency. We expose all the aura-specific utilities under objects named after their respective auras. We lightly improve doccomment consistency. --- src/da.ts | 164 +++++++++++++++++----------------- src/hoon/index.ts | 80 ----------------- src/index.ts | 6 +- src/p.ts | 219 +++++++++++++++++++++++++++++++--------------- src/parse.ts | 4 +- src/q.ts | 43 ++++++--- src/r.ts | 8 +- src/render.ts | 6 +- src/utils.ts | 19 ---- 9 files changed, 276 insertions(+), 273 deletions(-) delete mode 100644 src/hoon/index.ts delete mode 100644 src/utils.ts diff --git a/src/da.ts b/src/da.ts index 53b479b..413fe28 100644 --- a/src/da.ts +++ b/src/da.ts @@ -1,3 +1,84 @@ +/** + * Given a string formatted as a `@da`, returns a bigint representing the urbit date. + * + * @param {string} x The formatted `@da` + * @return {bigint} The urbit date as bigint + */ +export function parseDa(x: string): bigint { + let pos = true; + let [date, time, ms] = x.split('..'); + time = time || '0.0.0'; + ms = ms || '0000'; + let [yer, month, day] = date.slice(1).split('.'); + if (yer.at(-1) === '-') { + yer = yer.slice(0, -1); + pos = false; + } + const [hour, minute, sec] = time.split('.'); + const millis = ms.split('.').map((m) => BigInt('0x' + m)); + + return year({ + pos: pos, + year: BigInt(yer), + month: BigInt(month), + time: { + day: BigInt(day), + hour: BigInt(hour), + minute: BigInt(minute), + second: BigInt(sec), + ms: millis, + }, + }); +} + +/** + * Given a bigint representing an urbit date, returns a string formatted as a proper `@da`. + * + * @param {bigint} x The urbit date as bigint + * @return {string} The formatted `@da` + */ +export function renderDa(x: bigint) { + const { pos, year, month, time } = yore(x); + let out = `~${year}${pos ? '' : '-'}.${month}.${time.day}`; + if (time.hour !== 0n || time.minute !== 0n || time.second !== 0n || time.ms.length !== 0) { + out = out + `..${time.hour.toString().padStart(2, '0')}.${time.minute.toString().padStart(2, '0')}.${time.second.toString().padStart(2, '0')}` + if (time.ms.length !== 0) { + out = out + `..${time.ms.map((x) => x.toString(16).padStart(4, '0')).join('.')}`; + } + } + return out; +} + +/** + * Given a bigint representing an urbit date, returns a unix timestamp. + * + * @param {bigint} da The urbit date + * @return {number} The unix timestamp + */ +export function toUnix(da: bigint): number { + // ported from +time:enjs:format in hoon.hoon + const offset = DA_SECOND / 2000n; + const epochAdjusted = offset + (da - DA_UNIX_EPOCH); + + return Math.round( + Number(epochAdjusted * 1000n / DA_SECOND) + ); +} + +/** + * Given a unix timestamp, returns a bigint representing an urbit date + * + * @param {number} unix The unix timestamp + * @return {bigint} The urbit date + */ +export function fromUnix(unix: number): bigint { + const timeSinceEpoch = BigInt(unix) * DA_SECOND / 1000n; + return DA_UNIX_EPOCH + timeSinceEpoch; +} + +// +// internals +// interface Dat { pos: boolean; @@ -33,7 +114,7 @@ const MIT_YO = 60n; const ERA_YO = 146097n; const CET_YO = 36524n; -export function year(det: Dat) { +function year(det: Dat) { const yer = det.pos ? EPOCH + (BigInt(det.year)) : EPOCH - (BigInt(det.year) - 1n); @@ -86,39 +167,6 @@ export function year(det: Dat) { return fac | (sec << 64n); } -/** - * Given a string formatted as a @da, returns a bigint representing the urbit date. - * - * @return {string} x The formatted @da - * @return {bigint} x The urbit date as bigint - */ -export function parseDa(x: string): bigint { - let pos = true; - let [date, time, ms] = x.split('..'); - time = time || '0.0.0'; - ms = ms || '0000'; - let [yer, month, day] = date.slice(1).split('.'); - if (yer.at(-1) === '-') { - yer = yer.slice(0, -1); - pos = false; - } - const [hour, minute, sec] = time.split('.'); - const millis = ms.split('.').map((m) => BigInt('0x'+m)); - - return year({ - pos: pos, - year: BigInt(yer), - month: BigInt(month), - time: { - day: BigInt(day), - hour: BigInt(hour), - minute: BigInt(minute), - second: BigInt(sec), - ms: millis, - }, - }); -} - function yell(x: bigint): Tarp { let sec = x >> 64n; const milliMask = BigInt('0xffffffffffffffff'); @@ -201,51 +249,3 @@ function yore(x: bigint): Dat { time, }; } - -/** - * Given a bigint representing an urbit date, returns a string formatted as a proper @da. - * - * @param {bigint} x The urbit date as bigint - * @return {string} The formatted @da - */ -export function formatDa(x: bigint | string) { - if (typeof x === 'string') { - x = BigInt(x); - } - const { pos, year, month, time } = yore(x); - let out = `~${year}${pos ? '' : '-'}.${month}.${time.day}`; - if (time.hour !== 0n || time.minute !== 0n || time.second !== 0n || time.ms.length !== 0) { - out = out + `..${time.hour.toString().padStart(2, '0')}.${time.minute.toString().padStart(2, '0')}.${time.second.toString().padStart(2, '0')}` - if (time.ms.length !== 0) { - out = out + `..${time.ms.map((x) => x.toString(16).padStart(4, '0')).join('.')}`; - } - } - return out; -} - -/** - * Given a bigint representing an urbit date, returns a unix timestamp. - * - * @param {bigint} da The urbit date - * @return {number} The unix timestamp - */ -export function daToUnix(da: bigint): number { - // ported from +time:enjs:format in hoon.hoon - const offset = DA_SECOND / 2000n; - const epochAdjusted = offset + (da - DA_UNIX_EPOCH); - - return Math.round( - Number(epochAdjusted * 1000n / DA_SECOND) - ); -} - -/** - * Given a unix timestamp, returns a bigint representing an urbit date - * - * @param {number} unix The unix timestamp - * @return {bigint} The urbit date - */ -export function unixToDa(unix: number): bigint { - const timeSinceEpoch = BigInt(unix) * DA_SECOND / 1000n; - return DA_UNIX_EPOCH + timeSinceEpoch; -} diff --git a/src/hoon/index.ts b/src/hoon/index.ts deleted file mode 100644 index 7d7b588..0000000 --- a/src/hoon/index.ts +++ /dev/null @@ -1,80 +0,0 @@ - -export const pre = ` -dozmarbinwansamlitsighidfidlissogdirwacsabwissib\ -rigsoldopmodfoglidhopdardorlorhodfolrintogsilmir\ -holpaslacrovlivdalsatlibtabhanticpidtorbolfosdot\ -losdilforpilramtirwintadbicdifrocwidbisdasmidlop\ -rilnardapmolsanlocnovsitnidtipsicropwitnatpanmin\ -ritpodmottamtolsavposnapnopsomfinfonbanmorworsip\ -ronnorbotwicsocwatdolmagpicdavbidbaltimtasmallig\ -sivtagpadsaldivdactansidfabtarmonranniswolmispal\ -lasdismaprabtobrollatlonnodnavfignomnibpagsopral\ -bilhaddocridmocpacravripfaltodtiltinhapmicfanpat\ -taclabmogsimsonpinlomrictapfirhasbosbatpochactid\ -havsaplindibhosdabbitbarracparloddosbortochilmac\ -tomdigfilfasmithobharmighinradmashalraglagfadtop\ -mophabnilnosmilfopfamdatnoldinhatnacrisfotribhoc\ -nimlarfitwalrapsarnalmoslandondanladdovrivbacpol\ -laptalpitnambonrostonfodponsovnocsorlavmatmipfip\ -`; - -export const suf = ` -zodnecbudwessevpersutletfulpensytdurwepserwylsun\ -rypsyxdyrnuphebpeglupdepdysputlughecryttyvsydnex\ -lunmeplutseppesdelsulpedtemledtulmetwenbynhexfeb\ -pyldulhetmevruttylwydtepbesdexsefwycburderneppur\ -rysrebdennutsubpetrulsynregtydsupsemwynrecmegnet\ -secmulnymtevwebsummutnyxrextebfushepbenmuswyxsym\ -selrucdecwexsyrwetdylmynmesdetbetbeltuxtugmyrpel\ -syptermebsetdutdegtexsurfeltudnuxruxrenwytnubmed\ -lytdusnebrumtynseglyxpunresredfunrevrefmectedrus\ -bexlebduxrynnumpyxrygryxfeptyrtustyclegnemfermer\ -tenlusnussyltecmexpubrymtucfyllepdebbermughuttun\ -bylsudpemdevlurdefbusbeprunmelpexdytbyttyplevmyl\ -wedducfurfexnulluclennerlexrupnedlecrydlydfenwel\ -nydhusrelrudneshesfetdesretdunlernyrsebhulryllud\ -remlysfynwerrycsugnysnyllyndyndemluxfedsedbecmun\ -lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\ -`; - -export const prefixes = pre.match(/.{1,3}/g) as RegExpMatchArray; -export const suffixes = suf.match(/.{1,3}/g) as RegExpMatchArray; - -export const bex = (n: bigint) => 2n ** n; - -export const patp2syls = (name: string): string[] => - name.replace(/[\^~-]/g, '').match(/.{1,3}/g) || []; - -/** - * Weakly check if a string is a valid @p or @q value. - * - * This is, at present, a pretty weak sanity check. It doesn't confirm the - * structure precisely (e.g. dashes), and for @q, it's required that q values - * of (greater than one) odd bytelength have been zero-padded. So, for - * example, '~doznec-binwod' will be considered a valid @q, but '~nec-binwod' - * will not. - * - * @param {String} name a @p or @q value - * @return {boolean} - */ -//TODO rename validSyllables -export function isValidPat(name: string): boolean { - if (typeof name !== 'string') { - throw new Error('isValidPat: non-string input'); - } - else if (name.length < 4 || name[0] !== '~') return false; - else { - const syls = patp2syls(name); - const wrongLength = syls.length % 2 !== 0 && syls.length !== 1; - const sylsExist = syls.reduce( - (acc, syl, index) => - acc && - (index % 2 !== 0 || syls.length === 1 - ? suffixes.includes(syl) - : prefixes.includes(syl)), - true - ); - - return !wrongLength && sylsExist; - } -} diff --git a/src/index.ts b/src/index.ts index 43f6cf0..cff4a4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,7 @@ export { parse, tryParse, valid, slav, slaw, nuck } from './parse'; export { render, scot, rend } from './render'; //TODO expose encodeString() ? // atom utils -export { daToUnix, unixToDa } from './da'; -export { cite, deSig, preSig } from './p'; //TODO remove deSig, preSig +import { toUnix, fromUnix } from './da'; +export const da = { toUnix, fromUnix }; +import { cite, sein, clan, kind, rankToSize, sizeToRank } from './p'; +export const p = { cite, sein, clan, kind, rankToSize, sizeToRank }; diff --git a/src/p.ts b/src/p.ts index 995b574..29818e3 100644 --- a/src/p.ts +++ b/src/p.ts @@ -1,19 +1,21 @@ -import { - isValidPat, - patp2syls, - suffixes, - prefixes, -} from './hoon'; import ob from './hoon/ob'; +export type rank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn'; +export type size = 'galaxy' | 'star' | 'planet' | 'moon' | 'comet'; + +// +// main parsing & rendering +// + //NOTE matches for shape, not syllables export const regexP = /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/; /** * Convert a valid `@p` literal string to a bigint. - * @param {String} str certified-sane `@p` literal string + * Throws on malformed input. + * @param {String} str certified-sane `@p` literal string */ -export function parseP(str: string, scramble: boolean = true): bigint { +export function parseP(str: string): bigint { const syls = patp2syls(str); const syl2bin = (idx: number) => { @@ -29,21 +31,65 @@ export function parseP(str: string, scramble: boolean = true): bigint { ); const num = BigInt('0b' + addr); - return scramble ? ob.fynd(num) : num; + return ob.fynd(num); } -function checkedParseP(str: string): bigint { - if (!isValidP(str)) throw new Error('invalid @p literal: ' + str); - return parseP(str); +/** + * Convert a valid `@p` literal string to a bigint. + * Returns null on malformed input. + * @param {String} str `@p` literal string + */ +export function parseValidP(str: string): bigint | null { + if (!regexP.test(str) || !validSyllables(str)) return null; + const res = parseP(str); + return (str === renderP(res)) ? res : null; } -export type size = 'galaxy' | 'star' | 'planet' | 'moon' | 'comet'; -export type rank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn'; /** - * Determine the `$rank` of a `@p` value or literal. + * Convert a number to a @p-encoded string. + * @param {bigint} num + */ +export function renderP(num: bigint): string { + const sxz = ob.fein(num); + const dyx = Math.ceil(sxz.toString(16).length / 2); + const dyy = Math.ceil(sxz.toString(16).length / 4); + + function loop(tsxz: bigint, timp: number, trep: string): string { + const log = tsxz & 0xFFFFn; + const pre = prefixes[Number(log >> 8n)]; + const suf = suffixes[Number(log & 0xFFn)]; + const etc = (timp & 0b11) ? '-' : ((timp === 0) ? '' : '--'); + + const res = pre + suf + etc + trep; + + return timp === dyy ? trep : loop(tsxz >> 16n, timp + 1, res); + } + + return ( + '~' + (dyx <= 1 ? suffixes[Number(sxz)] : loop(sxz, 0, '')) + ); +} + +// +// utilities +// + +/** + * Validate a @p string. * - * @param {String} @p - * @return {String} + * @param {String} str a string + * @return {boolean} + */ +export function isValidP(str: string): boolean { + return regexP.test(str) // general structure + && validSyllables(str) // valid syllables + && str === renderP(parseP(str)); // no leading zeroes +} + +/** + * Determine the `$rank` of a `@p` value or literal. + * Throws on malformed input string. + * @param {String} who `@p` value or literal string */ export function clan(who: bigint | string): rank { let num: bigint; @@ -61,9 +107,15 @@ export function clan(who: bigint | string): rank { : 'pawn'; } +/** + * Determine the "size" of a `@p` value or literal. + * Throws on malformed input string. + * @param {String} who `@p` value or literal string + */ export function kind(who: bigint | string): size { return rankToSize(clan(who)); } + export function rankToSize(rank: rank): size { switch (rank) { case 'czar': return 'galaxy'; @@ -73,12 +125,20 @@ export function rankToSize(rank: rank): size { case 'pawn': return 'comet'; } } +export function sizeToRank(size: size): rank { + switch (size) { + case 'galaxy': return 'czar'; + case 'star': return 'king'; + case 'planet': return 'duke'; + case 'moon': return 'earl'; + case 'comet': return 'pawn'; + } +} /** - * Determine the parent of a `@p` value. Throws on invalid string inputs. - * - * @param {String | number} who `@p` value or literal string - * @return {String} + * Determine the parent of a `@p` value. + * Throws on malformed input string. + * @param {String | number} who `@p` value or literal string */ export function sein(who: bigint): bigint; export function sein(who: string): string; @@ -105,55 +165,9 @@ export function sein(who: bigint | string): typeof who { } /** - * Validate a @p string. - * - * @param {String} str a string - * @return {boolean} - */ -export function isValidP(str: string): boolean { - return regexP.test(str) // general structure - && isValidPat(str) // valid syllables - && str === renderP(parseP(str)); // no leading zeroes //TODO can isValidPat check this? -} - -export function parseValidP(str: string): bigint | null { - if (!regexP.test(str) || !isValidPat(str)) return null; - const res = parseP(str); - return (str === renderP(res)) ? res : null; -} - -/** - * Convert a number to a @p-encoded string. - * - * @param {bigint} arg - * @return {String} - */ -export function renderP(arg: bigint, scramble: boolean = true) { - const sxz = scramble ? ob.fein(arg) : arg; - const dyx = Math.ceil(sxz.toString(16).length / 2); - const dyy = Math.ceil(sxz.toString(16).length / 4); - - function loop(tsxz: bigint, timp: number, trep: string): string { - const log = tsxz & 0xFFFFn; - const pre = prefixes[Number(log >> 8n)]; - const suf = suffixes[Number(log & 0xFFn)]; - const etc = (timp & 0b11) ? '-' : ((timp === 0) ? '' : '--'); - - const res = pre + suf + etc + trep; - - return timp === dyy ? trep : loop(tsxz >> 16n, timp + 1, res); - } - - return ( - '~' + (dyx <= 1 ? suffixes[Number(sxz)] : loop(sxz, 0, '')) - ); -} - -/** - * Render short-form ship name. Throws on invalid string inputs. - * - * @param {String | number} who `@p` value or literal string - * @return {String} + * Render short-form ship name. + * Throws on malformed input string. + * @param {String | number} who `@p` value or literal string */ export function cite(who: bigint | string): string { let num: bigint; @@ -168,3 +182,68 @@ export function cite(who: bigint | string): string { return renderP(BigInt('0x'+num.toString(16).slice(0,4))) + '_' + renderP(num & 0xFFFFn).slice(1); } } + +// +// internals +// + +function checkedParseP(str: string): bigint { + if (!isValidP(str)) throw new Error('invalid @p literal: ' + str); + return parseP(str); +} + +const pre = ` +dozmarbinwansamlitsighidfidlissogdirwacsabwissib\ +rigsoldopmodfoglidhopdardorlorhodfolrintogsilmir\ +holpaslacrovlivdalsatlibtabhanticpidtorbolfosdot\ +losdilforpilramtirwintadbicdifrocwidbisdasmidlop\ +rilnardapmolsanlocnovsitnidtipsicropwitnatpanmin\ +ritpodmottamtolsavposnapnopsomfinfonbanmorworsip\ +ronnorbotwicsocwatdolmagpicdavbidbaltimtasmallig\ +sivtagpadsaldivdactansidfabtarmonranniswolmispal\ +lasdismaprabtobrollatlonnodnavfignomnibpagsopral\ +bilhaddocridmocpacravripfaltodtiltinhapmicfanpat\ +taclabmogsimsonpinlomrictapfirhasbosbatpochactid\ +havsaplindibhosdabbitbarracparloddosbortochilmac\ +tomdigfilfasmithobharmighinradmashalraglagfadtop\ +mophabnilnosmilfopfamdatnoldinhatnacrisfotribhoc\ +nimlarfitwalrapsarnalmoslandondanladdovrivbacpol\ +laptalpitnambonrostonfodponsovnocsorlavmatmipfip\ +`; + +const suf = ` +zodnecbudwessevpersutletfulpensytdurwepserwylsun\ +rypsyxdyrnuphebpeglupdepdysputlughecryttyvsydnex\ +lunmeplutseppesdelsulpedtemledtulmetwenbynhexfeb\ +pyldulhetmevruttylwydtepbesdexsefwycburderneppur\ +rysrebdennutsubpetrulsynregtydsupsemwynrecmegnet\ +secmulnymtevwebsummutnyxrextebfushepbenmuswyxsym\ +selrucdecwexsyrwetdylmynmesdetbetbeltuxtugmyrpel\ +syptermebsetdutdegtexsurfeltudnuxruxrenwytnubmed\ +lytdusnebrumtynseglyxpunresredfunrevrefmectedrus\ +bexlebduxrynnumpyxrygryxfeptyrtustyclegnemfermer\ +tenlusnussyltecmexpubrymtucfyllepdebbermughuttun\ +bylsudpemdevlurdefbusbeprunmelpexdytbyttyplevmyl\ +wedducfurfexnulluclennerlexrupnedlecrydlydfenwel\ +nydhusrelrudneshesfetdesretdunlernyrsebhulryllud\ +remlysfynwerrycsugnysnyllyndyndemluxfedsedbecmun\ +lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\ +`; + +export const prefixes = pre.match(/.{1,3}/g) as RegExpMatchArray; +export const suffixes = suf.match(/.{1,3}/g) as RegExpMatchArray; + +function patp2syls(name: string): string[] { + return name.replace(/[\^~-]/g, '').match(/.{1,3}/g) || []; +} + +// check if string contains valid syllables +function validSyllables(name: string): boolean { + const syls = patp2syls(name); + return !(syls.length % 2 !== 0 && syls.length !== 1) // wrong length + && syls.every((syl, index) => // invalid syllables + index % 2 !== 0 || syls.length === 1 + ? suffixes.includes(syl) + : prefixes.includes(syl) + ); +} diff --git a/src/parse.ts b/src/parse.ts index 4ad2dbb..fe95a86 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -9,8 +9,8 @@ import { aura, dime, coin } from './types'; import { parseDa } from './da'; import { parseValidP, regexP } from './p'; -import { parseQ, parseValidQ } from './q'; -import { parse as parseR, precision } from './r'; +import { parseValidQ } from './q'; +import { parseR, precision } from './r'; function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; diff --git a/src/q.ts b/src/q.ts index 4fe3e33..b4d8324 100644 --- a/src/q.ts +++ b/src/q.ts @@ -1,13 +1,11 @@ -import { isValidPat, prefixes, suffixes } from './hoon'; -import { chunk, splitAt } from './utils'; //TODO inline +import { prefixes, suffixes } from './p'; //TODO investigate whether native UintArrays are more portable // than node Buffers /** - * Convert a number to a @q-encoded string. - * - * @param {String, Number, bigint} arg + * Convert a number to a `@q`-encoded string. + * @param {bigint} num * @return {String} */ export function renderQ(num: bigint): string { @@ -40,9 +38,9 @@ export function renderQ(num: bigint): string { } /** - * Convert a @q-encoded string to a bigint - * - * @param {String} name @q + * Convert a `@q`-encoded string to a bigint. + * Throws on malformed input. + * @param {String} str `@q` string with leading .~ * @return {String} */ export function parseQ(str: string): bigint { @@ -72,9 +70,8 @@ export function parseValidQ(str: string): bigint | null { } /** - * Validate a @q string. - * - * @param {String} str a string + * Validate a `@q` string. + * @param {String} str a string * @return {boolean} */ export function isValidQ(str: string): boolean { @@ -86,3 +83,27 @@ export function isValidQ(str: string): boolean { return false; } }; + +// +// internals +// + +function chunk(arr: T[], size: number): T[][] { + let chunk: T[] = []; + let newArray = [chunk]; + + for (let i = 0; i < arr.length; i++) { + if (chunk.length < size) { + chunk.push(arr[i]); + } else { + chunk = [arr[i]]; + newArray.push(chunk); + } + } + + return newArray; +} + +function splitAt(index: number, str: string) { + return [str.slice(0, index), str.slice(index)]; +} diff --git a/src/r.ts b/src/r.ts index c9db461..6db7933 100644 --- a/src/r.ts +++ b/src/r.ts @@ -2,12 +2,12 @@ export type precision = 'h' | 's' | 'd' | 'q' | precisionBits; type precisionBits = { w: number, p: number, l: string }; // str: @r* format string including its leading . and ~s -export function parse(per: precision, str: string): bigint { +export function parseR(per: precision, str: string): bigint { per = getPrecision(per); - return parseR(str.slice(per.l.length), per.w, per.p); + return parse(str.slice(per.l.length), per.w, per.p); } -export function render(per: precision, r: bigint): string { +export function renderR(per: precision, r: bigint): string { per = getPrecision(per); return per.l + rCo(deconstruct(r, BigInt(per.w), BigInt(per.p))); } @@ -35,7 +35,7 @@ function bitMask(bits: bigint): bigint { // str: @r* format string with its leading . and ~ stripped off // w: exponent bits // p: mantissa bits -function parseR(str: string, w: number, p: number): bigint { +function parse(str: string, w: number, p: number): bigint { if (str === 'nan') return makeNaN(w, p); if (str === 'inf') return makeInf(true, w, p); if (str === '-inf') return makeInf(false, w, p); diff --git a/src/render.ts b/src/render.ts index c3b2ea6..93ab136 100644 --- a/src/render.ts +++ b/src/render.ts @@ -7,10 +7,10 @@ import { aura, coin } from './types'; -import { formatDa } from './da'; +import { renderDa } from './da'; import { renderP } from './p'; import { renderQ } from './q'; -import { render as renderR } from './r'; +import { renderR } from './r'; // render(): scot() // scot(): render atom as specific aura @@ -47,7 +47,7 @@ export function rend(coin: coin): string { case 'd': switch(coin.aura[1]) { case 'a': - return formatDa(coin.atom); + return renderDa(coin.atom); case 'r': throw new Error('aura-js: @dr rendering unsupported'); //TODO default: diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index d96d4b6..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function chunk(arr: T[], size: number): T[][] { - let chunk: T[] = []; - let newArray = [chunk]; - - for (let i = 0; i < arr.length; i++) { - if (chunk.length < size) { - chunk.push(arr[i]); - } else { - chunk = [arr[i]]; - newArray.push(chunk); - } - } - - return newArray; -} - -export function splitAt(index: number, str: string) { - return [str.slice(0, index), str.slice(index)]; -} From 88785eafcb3aa347d00e627bc0cab88fafb57ee9 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 14:40:33 +0200 Subject: [PATCH 37/50] lib: remove jsverify dependency We no longer use this in our tests. --- package-lock.json | 85 ----------------------------------------------- package.json | 1 - 2 files changed, 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index a206b04..92c03c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@tsconfig/recommended": "^1.0.2", "dts-cli": "^2.0.0", "husky": "^7.0.4", - "jsverify": "^0.8.4", "tslib": "^2.5.0", "typescript": "^5.0.4" }, @@ -7909,21 +7908,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsverify": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/jsverify/-/jsverify-0.8.4.tgz", - "integrity": "sha512-nUG73Sfi8L4eOkc7pv9sflgAm43v+z6XMuePGVdRoBUxBLJiVcMcf3Xgc4h19eHHF3JwsaagOkUu825UnPBLJw==", - "dev": true, - "dependencies": { - "lazy-seq": "^1.0.0", - "rc4": "~0.1.5", - "trampa": "^1.0.0", - "typify-parser": "^1.1.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -7961,15 +7945,6 @@ "language-subtag-registry": "~0.3.2" } }, - "node_modules/lazy-seq": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazy-seq/-/lazy-seq-1.0.0.tgz", - "integrity": "sha512-AQ4vRcnULa7FX6R6YTAjKQAE1MuEThidVQm0TEtTpedaBpnOwid5k6go16E5NDkafel1xAsZL73WkwdG03IzhA==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9102,15 +9077,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/rc4": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz", - "integrity": "sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10182,12 +10148,6 @@ "node": ">=12" } }, - "node_modules/trampa": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trampa/-/trampa-1.0.1.tgz", - "integrity": "sha512-93WeyHNuRggPEsfCe+yHxCgM2s6H3Q8Wmlt6b6ObJL8qc7eZlRaFjQxwTrB+zbvGtlDRnAkMoYYo3+2uH/fEwA==", - "dev": true - }, "node_modules/ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -10394,15 +10354,6 @@ "node": ">=12.20" } }, - "node_modules/typify-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/typify-parser/-/typify-parser-1.1.0.tgz", - "integrity": "sha512-p5+L1sc6Al3bcStMwiZNxDh4ii4JxL+famEbSIUuOUMVoNn9Nz27AT1jL3x7poMHxqKK0UQIUAp5lGkKbyKkFA==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -16529,18 +16480,6 @@ "universalify": "^2.0.0" } }, - "jsverify": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/jsverify/-/jsverify-0.8.4.tgz", - "integrity": "sha512-nUG73Sfi8L4eOkc7pv9sflgAm43v+z6XMuePGVdRoBUxBLJiVcMcf3Xgc4h19eHHF3JwsaagOkUu825UnPBLJw==", - "dev": true, - "requires": { - "lazy-seq": "^1.0.0", - "rc4": "~0.1.5", - "trampa": "^1.0.0", - "typify-parser": "^1.1.0" - } - }, "jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -16572,12 +16511,6 @@ "language-subtag-registry": "~0.3.2" } }, - "lazy-seq": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazy-seq/-/lazy-seq-1.0.0.tgz", - "integrity": "sha512-AQ4vRcnULa7FX6R6YTAjKQAE1MuEThidVQm0TEtTpedaBpnOwid5k6go16E5NDkafel1xAsZL73WkwdG03IzhA==", - "dev": true - }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -17418,12 +17351,6 @@ "safe-buffer": "^5.1.0" } }, - "rc4": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz", - "integrity": "sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==", - "dev": true - }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -18238,12 +18165,6 @@ "punycode": "^2.1.1" } }, - "trampa": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trampa/-/trampa-1.0.1.tgz", - "integrity": "sha512-93WeyHNuRggPEsfCe+yHxCgM2s6H3Q8Wmlt6b6ObJL8qc7eZlRaFjQxwTrB+zbvGtlDRnAkMoYYo3+2uH/fEwA==", - "dev": true - }, "ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -18371,12 +18292,6 @@ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, - "typify-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/typify-parser/-/typify-parser-1.1.0.tgz", - "integrity": "sha512-p5+L1sc6Al3bcStMwiZNxDh4ii4JxL+famEbSIUuOUMVoNn9Nz27AT1jL3x7poMHxqKK0UQIUAp5lGkKbyKkFA==", - "dev": true - }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 51a7438..6293dd3 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@tsconfig/recommended": "^1.0.2", "dts-cli": "^2.0.0", "husky": "^7.0.4", - "jsverify": "^0.8.4", "tslib": "^2.5.0", "typescript": "^5.0.4" }, From 6f26b87f77d12f96e6844e737e0a655d940e9e83 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 15:15:09 +0200 Subject: [PATCH 38/50] lib: update export shape, readme --- README.md | 92 ++++++++++++++++++++++------------------------------ src/index.ts | 8 +++++ 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 4277faf..63d4de6 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,50 @@ # `@urbit/aura` -This NPM package is intended to ease the flow of developing FE applications for urbit, by adding parsing and formatting functions for the various urbit auras +Hoon literal syntax parsing and rendering. Approaching two nines of parity with hoon stdlib. -## API +Supports all standard auras (except `@dr`, `@if` and `@is`). -```typescript -// @da manipulation -function parseDa(da: string): bigint; -function formatDa(da: bigint): string; -// Given a bigint representing an urbit date, returns a unix timestamp. -function daToUnix(da: bigint): number; -// Given a unix timestamp, returns a bigint representing an urbit date -function unixToDa(unix: number): bigint; +## API overview + +Atoms are native `bigint`s. Top-level library functions reflect the hoon stdlib. Aliases are provided for your comfort. Summary of exports below. -// @p manipulation -// Convert a number to a @p-encoded string. -function patp(arg: string | number | bigint): string; -function hex2patp(hex: string): string; -function patp2hex(name: string): string; -function patp2bn(name: string): bigint; -function patp2dec(name: string): string; -// Determine the ship class of a @p value. -function clan(who: string): string; -// Determine the parent of a @p value. -function sein(name: string): strin; -// Validate a @p string. -function isValidPatp(str: string): boolean; -// Ensure @p is sigged. -function preSig(ship: string): string; -// Remove sig from @p -function deSig(ship: string): string; -// Trim @p to short form -function cite(ship: string): string | null; +### Parsing + +```typescript +const parse = slav; +const tryParse = slaw; +function valid(:aura, :string): boolean; +function slav(:aura, :string): bigint; // throws on failure +function slaw(:aura, :string): bigint | null; +function nuck(:string): coin | null; +``` -// @q manipulation -// Convert a number to a @q-encoded string. -function patq(arg: string | number | bigint): string; -function hex2patq(arg: string): string; -function patq2hex(name: string): string; -function patq2bn(name: string): bigint; -function patq2dec(name: string): string; -// Validate a @q string. -function isValidPatq(str: string): boolean; -// Equality comparison on @q values. -function eqPatq(p: string, q: string): boolean; +### Rendering -// @ud manipulation -function parseUd(ud: string): bigint; -function formatUd(ud: bigint): string; +```typescript +const render = scot; +function scot(:aura, :bigint): string; +function rend(:coin): string; +``` -// @uv manipulation -function parseUv(x: string): bigint; -function formatUv(x: bigint | string): string; +### Utilities -// @uw manipulation -function parseUw(x: string): bigint; -function formatUw(x: bigint | string): string; +We provide some utilities for desirable operations on specific auras. These too generally match their hoon stdlib equivalents. -// @ux manipulation -function parseUx(ux: string): string; -function formatUx(hex: string): string; -``; +```typescript +const da = { + function toUnix(:bigint): number, + function fromUnix(:number): bigint, +}; +const p = { + type rank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn', + type size = 'galaxy' | 'star' | 'planet' | 'moon' | 'comet', + function cite(:bigint | string): string, + function sein(:bigint): bigint, + function sein(:string): string, // throws on bad input + function clan(:bigint | string): rank, // throws on bad input + function kind(:bigint | string): size, // throws on bad input + function rankToSize(:rank): size, + function sizeToRank(:size): rank, +}; ``` diff --git a/src/index.ts b/src/index.ts index cff4a4d..8bd65a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,18 @@ // main + export * from './types'; export { parse, tryParse, valid, slav, slaw, nuck } from './parse'; export { render, scot, rend } from './render'; //TODO expose encodeString() ? // atom utils + import { toUnix, fromUnix } from './da'; export const da = { toUnix, fromUnix }; + +import type * as pt from './p'; import { cite, sein, clan, kind, rankToSize, sizeToRank } from './p'; export const p = { cite, sein, clan, kind, rankToSize, sizeToRank }; +export namespace p { + export type rank = pt.rank; + export type size = pt.size; +} From 4837b4349155e2b0ac612d3f02fdd9d9e6bdef64 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 15:31:27 +0200 Subject: [PATCH 39/50] parse: unify r-parsing counter --- src/parse.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index fe95a86..53f800b 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -153,10 +153,8 @@ export function nuck(str: string): coin | null { if ( ( str[1] === '~' && (regex['rd'].test(str) || regex['rh'].test(str) || regex['rq'].test(str)) ) || regex['rs'].test(str) ) { // "royl" - let precision = 0, i = 1; - while (str[i] === '~') { - precision++; i++; - } + let precision = 0; + while (str[precision+1] === '~') precision++; let aura: aura; switch (precision) { case 0: aura = 'rs'; break; From a642f98216f03c03464856e7f207404c9c88620d Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 15:31:42 +0200 Subject: [PATCH 40/50] tests: correct long `@q` rendering --- test/data/atoms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data/atoms.ts b/test/data/atoms.ts index f48a7cb..e257d20 100644 --- a/test/data/atoms.ts +++ b/test/data/atoms.ts @@ -493,7 +493,7 @@ export const PHONETIC_TESTS: { // absurdly long { 'n': 0x7468697320697320736f6d6520766572792068696768207175616c69747920656e74726f7079n, 'p': '~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud', - 'q': '.~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud' + 'q': '.~divmes-davset-holdet-sallun-salpel-taswet-holtex-watmeb-tarlun-picdet-magmes-holter-dacruc-timdet-divtud-holwet-maldut-padpel-sivtud' }, ]; From 61a4c0650426c9dd90d06a2dacae33e3cbf79005 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 15:32:15 +0200 Subject: [PATCH 41/50] tests: note about `@r` aura fuzzing --- test/fuzz.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index d49d4d6..91f4fea 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -13,6 +13,10 @@ const auras: aura[] = [ // 'n', // limited legitimate values 'p', 'q', + // 'rh', // strict size limits, and NaN won't always round-trip + // 'rd', // + // 'rq', // + // 'rs', // 'sb', 'sd', 'si', From 7b8eac8fadf8915a49978c5fa5e2d8a8f3cc58ae Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 17 Oct 2025 15:39:53 +0200 Subject: [PATCH 42/50] lib: consistently note `@uc` as unsupported --- README.md | 2 +- src/parse.ts | 2 +- src/render.ts | 2 +- test/fuzz.test.ts | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 63d4de6..24cbeab 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Hoon literal syntax parsing and rendering. Approaching two nines of parity with hoon stdlib. -Supports all standard auras (except `@dr`, `@if` and `@is`). +Supports all standard auras (except `@dr`, `@if`, `@is` and `@uc`). ## API overview diff --git a/src/parse.ts b/src/parse.ts index 53f800b..154ed40 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -3,7 +3,7 @@ // atom literal parsing from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @dr, @if, @is +//TODO unsupported auras: @dr, @if, @is, @uc import { aura, dime, coin } from './types'; diff --git a/src/render.ts b/src/render.ts index 93ab136..ab9c0d5 100644 --- a/src/render.ts +++ b/src/render.ts @@ -3,7 +3,7 @@ // atom literal rendering from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @dr, @if, @is +//TODO unsupported auras: @dr, @if, @is, @uc import { aura, coin } from './types'; diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index 91f4fea..aa2c148 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -27,6 +27,7 @@ const auras: aura[] = [ // 'ta', // // 'tas', // 'ub', + // 'uc', //TODO unsupported 'ud', 'ui', 'uv', From b07fc48c1ca0bf054ff737b04730d575c91d3a8d Mon Sep 17 00:00:00 2001 From: fang Date: Sun, 19 Oct 2025 23:31:20 +0200 Subject: [PATCH 43/50] parse: support `@if`, `@is` --- README.md | 2 +- src/parse.ts | 13 +++- src/types.ts | 2 + test/data/atoms.ts | 155 +++++++++++++++++++++++++++++++++++++++++++++ test/fuzz.test.ts | 2 + test/parse.test.ts | 11 ++++ 6 files changed, 182 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 24cbeab..aa8763b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Hoon literal syntax parsing and rendering. Approaching two nines of parity with hoon stdlib. -Supports all standard auras (except `@dr`, `@if`, `@is` and `@uc`). +Supports all standard auras (except `@dr` and `@uc`). ## API overview diff --git a/src/parse.ts b/src/parse.ts index 154ed40..40fdc9f 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -3,7 +3,7 @@ // atom literal parsing from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @dr, @if, @is, @uc +//TODO unsupported auras: @dr, @uc import { aura, dime, coin } from './types'; @@ -28,6 +28,8 @@ export const regex: { [key in aura]: RegExp } = { 'da': /^~(0|[1-9][0-9]*)\-?\.([1-9]|1[0-2])\.([1-9]|[1-3][0-9])(\.\.([0-9]+)\.([0-9]+)\.([0-9]+)(\.(\.[0-9a-f]{4})+)?)?$/, 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))?(\.(\.[0-9a-f]{4})+)?$/, 'f': /^\.(y|n)$/, + 'if': /^(\.(0|[1-9][0-9]{0,2})){4}$/, + 'is': /^(\.(0|[1-9a-fA-F][0-9a-fA-F]{0,3})){8}$/, 'n': /^~$/, 'p': regexP, //NOTE matches shape but not syllables 'q': /^\.~(([a-z]{3}|[a-z]{6})(\-[a-z]{6})*)$/, //NOTE matches shape but not syllables @@ -150,6 +152,14 @@ export function nuck(str: string): coin | null { // going down the list of options this way matches hoon parser behavior the closest, but is slow for the "miss" case. // could be optimized by hard-returning if the regex fails for cases where the lead char is unique. // should probably run some perf tests + if (regex['is'].test(str)) { + const value = str.slice(1).split('.').reduce((a, v) => a + v.padStart(4, '0'), ''); + return { type: 'dime', aura: 'is', atom: BigInt('0x'+value) }; + } else + if (regex['if'].test(str)) { + const value = str.slice(1).split('.').reduce((a, v, i) => (a + (BigInt(v) << BigInt(8 * (3 - i)))), 0n); + return { type: 'dime', aura: 'if', atom: value }; + } else if ( ( str[1] === '~' && (regex['rd'].test(str) || regex['rh'].test(str) || regex['rq'].test(str)) ) || regex['rs'].test(str) ) { // "royl" @@ -170,7 +180,6 @@ export function nuck(str: string): coin | null { if (num === null) return null; return { type: 'dime', aura: 'q', atom: num }; } else - //TODO %is, %if // "zust" if (str[1] === '_' && /^\.(_([0-9a-zA-Z\-\.]|~\-|~~)+)*__$/.test(str)) { // "nusk" const coins = str.slice(1, -2).split('_').slice(1).map((s): coin | null => { //NOTE real +wick produces null for strings w/ other ~ chars, diff --git a/src/types.ts b/src/types.ts index bb75ff5..2c7a86e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,8 @@ export type aura = 'c' | 'da' | 'dr' | 'f' + | 'if' + | 'is' | 'n' | 'p' | 'q' diff --git a/test/data/atoms.ts b/test/data/atoms.ts index e257d20..9d22d49 100644 --- a/test/data/atoms.ts +++ b/test/data/atoms.ts @@ -302,6 +302,161 @@ export const INTEGER_TESTS: { }, ]; +export const IPV4_TESTS: { + n: bigint, + if: string +}[] = [ + { 'n': 807321889n, + 'if': '.48.30.193.33' + }, + { 'n': 3804317590n, + 'if': '.226.193.71.150' + }, + { 'n': 895708593n, + 'if': '.53.99.109.177' + }, + { 'n': 133621386n, + 'if': '.7.246.230.138' + }, + { 'n': 1110053789n, + 'if': '.66.42.19.157' + }, + { 'n': 1738320401n, + 'if': '.103.156.170.17' + }, + { 'n': 4231920335n, + 'if': '.252.61.250.207' + }, + { 'n': 2681308946n, + 'if': '.159.209.135.18' + }, + { 'n': 5963504n, + 'if': '.0.90.254.240' + }, + { 'n': 691080007n, + 'if': '.41.49.11.71' + }, + { 'n': 3896744529n, + 'if': '.232.67.154.81' + }, + { 'n': 4172959856n, + 'if': '.248.186.80.112' + }, + { 'n': 282345860n, + 'if': '.16.212.65.132' + }, + { 'n': 1400904024n, + 'if': '.83.128.25.88' + }, + { 'n': 3171770264n, + 'if': '.189.13.95.152' + }, + { 'n': 959932782n, + 'if': '.57.55.105.110' + }, + { 'n': 890947430n, + 'if': '.53.26.199.102' + }, + { 'n': 2083634832n, + 'if': '.124.49.190.144' + }, + { 'n': 2135483961n, + 'if': '.127.72.230.57' + }, + { 'n': 3903494749n, + 'if': '.232.170.154.93' + }, + { 'n': 588679618n, + 'if': '.35.22.137.194' + }, + // zero bytes + { 'n': 0n, + 'if': '.0.0.0.0' + }, + { 'n': 257n, + 'if': '.0.0.1.1' + }, + { 'n': 4278190335n, + 'if': '.255.0.0.255' + }, +]; + +export const IPV6_TESTS: { + n: bigint, + is: string +}[] = [ + { 'n': 337589486868456293016024144360375510656n, + 'is': '.fdf9.5ec3.442f.f80c.32eb.706e.22e2.e680' + }, + { 'n': 235657577517117307780314982160714186767n, + 'is': '.b149.ff9e.c1eb.b281.4faf.a336.9384.400f' + }, + { 'n': 325357659134260222628146827587324816556n, + 'is': '.f4c5.9b18.1578.8d04.baff.1cb5.dc9.2cac' + }, + { 'n': 226735022798529839177410190715794164949n, + 'is': '.aa93.93b5.4850.9b87.d240.94d8.1c97.cd5' + }, + { 'n': 57252987712168015925131514820989714772n, + 'is': '.2b12.863b.6962.1b1e.7831.5a7.f37e.8154' + }, + { 'n': 88629668840893973117284629411124587232n, + 'is': '.42ad.740c.a906.aac0.1de5.fb30.8289.aae0' + }, + { 'n': 248290127112293020329330966732261351214n, + 'is': '.baca.f066.e65f.a51c.bd5a.1b7d.55ec.df2e' + }, + { 'n': 37045722501599219774186597438855573164n, + 'is': '.1bde.bf22.feae.52c8.6925.6cc3.9533.eeac' + }, + { 'n': 67997883586901786122445670637270469610n, + 'is': '.3327.ea7f.fdda.e2d6.75bd.8c18.173c.bea' + }, + { 'n': 326647343852133419583482125089361985308n, + 'is': '.f5bd.fd75.bf6b.2e3b.2888.3de.fa55.7f1c' + }, + { 'n': 195717022160085584065700179326430314983n, + 'is': '.933d.ba34.4b3a.15c8.f807.1faf.9157.81e7' + }, + { 'n': 211000578906934658167753232415210617987n, + 'is': '.9ebd.3bba.dbe1.a940.4026.a247.6d6c.3c83' + }, + { 'n': 154849127606874136259068650818300161140n, + 'is': '.747e.db6f.a054.18af.e6c6.e4e3.6f7e.2c74' + }, + { 'n': 14767302673435791657373783029827412506n, + 'is': '.b1c.143c.1300.94d3.2ff7.3b36.def2.a21a' + }, + { 'n': 123971713052710237067818008482194269471n, + 'is': '.5d44.155c.7d2d.7c6.b52a.9c0b.3bbe.251f' + }, + { 'n': 262699680397970860118004915307903938281n, + 'is': '.c5a2.1e3a.e156.2210.e905.21b9.292b.6ee9' + }, + { 'n': 257209633456024567583396352939592994981n, + 'is': '.c180.c604.7996.1f58.949e.27e.eb3b.50a5' + }, + { 'n': 258801164939755730885841989240059520853n, + 'is': '.c2b3.4a93.f230.4b8c.569.ccc3.266.e755' + }, + { 'n': 270767742644940472562032354717108516079n, + 'is': '.cbb3.f869.14a2.69f8.2372.9a37.d897.b4ef' + }, + { 'n': 269795612802750900437183701002939522985n, + 'is': '.caf8.beb5.719a.f72e.5ae.2e41.a74b.dfa9' + }, + { 'n': 47520821311211709795896162188995433788n, + 'is': '.23c0.2d61.dccf.12ea.5736.4606.77e1.953c' + }, + // zero bytes + { 'n': 0n, + 'is': '.0.0.0.0.0.0.0.0' + }, + { 'n': 340277174624079928635746076935439056895n, + 'is': '.ffff.0.0.0.0.0.0.ffff' + } +]; + // float tests generated like so (swapping out 32 for each size) // // =| n=@ud diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index aa2c148..a414ace 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -10,6 +10,8 @@ const auras: aura[] = [ 'da', // 'dr', //TODO unsupported // 'f', // limited legitimate values + // 'if', // fixed size + // 'is', // fixed size // 'n', // limited legitimate values 'p', 'q', diff --git a/test/parse.test.ts b/test/parse.test.ts index e062d08..c409ba3 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1,6 +1,7 @@ import { aura } from '../src/types'; import { tryParse as parse, decodeString, nuck, regex } from '../src/parse'; import { INTEGER_AURAS, INTEGER_TESTS, + IPV4_TESTS, IPV6_TESTS, FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, @@ -82,6 +83,8 @@ const OUR_DATE_TESTS: { ]; testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('ipv4', ['if'], IPV4_TESTS); +testAuras('ipv6', ['is'], IPV6_TESTS); testAuras('float16', ['rh'], FLOAT_16_TESTS); testAuras('float32', ['rs'], FLOAT_32_TESTS); testAuras('float64', ['rd'], FLOAT_64_TESTS); @@ -162,4 +165,12 @@ describe('invalid syntax', () => { }); }); +describe('oversized inputs', () => { + it('parses oversized @if', () => { + expect(parse('if', '.255.0.0.999')).toEqual(0xff0003e7n); + expect(parse('if', '.255.0.1.999')).toEqual(0xff0004e7n); + expect(parse('if', '.256.0.0.255')).toEqual(0x1000000ffn); + }); +}); + //TODO oversized floats From 77c4a12bb44319c145f1a00d9849690d1d882f0f Mon Sep 17 00:00:00 2001 From: fang Date: Sun, 19 Oct 2025 23:31:47 +0200 Subject: [PATCH 44/50] render: support `@if`, `@is` --- src/render.ts | 19 ++++++++++++++++--- test/render.test.ts | 14 +++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/render.ts b/src/render.ts index ab9c0d5..d66cdad 100644 --- a/src/render.ts +++ b/src/render.ts @@ -3,7 +3,7 @@ // atom literal rendering from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @dr, @if, @is, @uc +//TODO unsupported auras: @dr, @uc import { aura, coin } from './types'; @@ -63,8 +63,8 @@ export function rend(coin: coin): string { return '~'; case 'i': switch(coin.aura[1]) { - case 'f': throw new Error('aura-js: @if rendering unsupported'); //TODO - case 's': throw new Error('aura-js: @is rendering unsupported'); //TODO + case 'f': return '.' + spite(coin.atom, 1, 4, 10); + case 's': return '.' + spite(coin.atom, 2, 8, 16); default: return zco(coin.atom); } case 'p': @@ -190,6 +190,19 @@ function split(str: string, group: number): string { return str.replace(new RegExp(`(?=(?:.{${group}})+$)(?!^)`, 'g'), '.'); } +// byte-level split() +function spite(atom: bigint, bytes: number, groups: number, base: number = 10): string { + let out = ''; + const size = 8n * BigInt(bytes); + const mask = (1n << size) - 1n; + while (groups-- > 0) { + if (out !== '') out = '.' + out; + out = (atom & mask).toString(base) + out; + atom = atom >> size; + } + return out; +} + function cordToString(atom: bigint): string { return new TextDecoder('utf-8').decode(atomToByteArray(atom).reverse()); }; diff --git a/test/render.test.ts b/test/render.test.ts index e25e96a..5ef4e3b 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -1,6 +1,7 @@ import { aura, coin } from '../src/types'; import { render, rend } from '../src/render'; import { INTEGER_AURAS, INTEGER_TESTS, + IPV4_TESTS, IPV6_TESTS, FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, @@ -17,7 +18,7 @@ describe('limited auras', () => { expect(bad).toEqual('~'); }); }); - describe(`@f parsing`, () => { + describe(`@f rendering`, () => { it('renders', () => { const yea = render('f', 0n); expect(yea).toEqual('.y'); @@ -49,6 +50,8 @@ function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { } testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('ipv4', ['if'], IPV4_TESTS); +testAuras('ipv6', ['is'], IPV6_TESTS); testAuras('float16', ['rh'], FLOAT_16_TESTS); testAuras('float32', ['rs'], FLOAT_32_TESTS); testAuras('float64', ['rd'], FLOAT_64_TESTS); @@ -89,3 +92,12 @@ describe('blob rendering', () => { expect(rend({ type: 'blob', jam: 325350265702017n })).toEqual('~097su1g7hk1'); }); }); + +describe('oversized inputs', () => { + it('truncates oversized @if', () => { + expect(render('if', 0x1000000ffn)).toEqual('.0.0.0.255'); + }); + it('truncates oversized @is', () => { + expect(render('is', 0xaaaaffff000000000000000000000000ffffn)).toEqual('.ffff.0.0.0.0.0.0.ffff'); + }); +}); From 359d11c18a6a80b4220e7b8940e2ec1921aa4b11 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 20 Oct 2025 22:00:44 +0200 Subject: [PATCH 45/50] tests: improve fuzz tests Can now generate values of arbitrary bit widths. We do this to make sure we cover more varied inputs more consistently, and restrict fuzzing for certain auras to inputs that make sense for that aura. (For example, no inputs >32 bits for `@if`, because any additional bits would get truncated, breaking the round-tripping that we test for.) Additionally, we make it possible to fuzz floats by letting you specify a "transform" function for the generated test values. This way, we can ensure none of our inputs are `NaN` values, which would have their mantissa ("payload") bits dropped during rendering. Unfortunately, this does uncover an off-by-one in the parsing logic for floats. In lieu of fixing that, we keep the float fuzzing disabled for now... --- test/fuzz.test.ts | 76 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index a414ace..6b9957d 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -3,19 +3,20 @@ import { tryParse as parse } from "../src/parse"; import render from '../src/render'; import { webcrypto } from 'crypto'; -const testCount = 500; +// tests per bit-size +const testCount = 50; -const auras: aura[] = [ +const simpleAuras: aura[] = [ 'c', 'da', // 'dr', //TODO unsupported // 'f', // limited legitimate values - // 'if', // fixed size - // 'is', // fixed size + // 'if', // won't round-trip above "native" size limit + // 'is', // won't round-trip above "native" size limit // 'n', // limited legitimate values 'p', 'q', - // 'rh', // strict size limits, and NaN won't always round-trip + // 'rh', // strict size limits, and NaN usually won't round-trip // 'rd', // // 'rq', // // 'rs', // @@ -37,21 +38,60 @@ const auras: aura[] = [ 'ux' ] -function fuzz(nom: string, arr: Uint8Array | Uint16Array | Uint32Array | BigUint64Array) { - webcrypto.getRandomValues(arr); - auras.forEach((a) => { - describe(nom + ' @' + a, () => { - it('round-trips losslessly', () => { - arr.forEach((n) => { - n = BigInt(n); - expect(parse(a, render(a, n))).toEqual(n); - }); +function fuzz(minBits: number, maxBits: number, auras: aura[], f: (n:bigint)=>bigint = (n)=>n) { + describe(minBits + 'โ€”' + maxBits + '-bit values', () => { + const tests: { [bits: number]: bigint[] } = {}; + for (let bits = minBits; bits <= maxBits; bits++) { + const parts = Math.ceil(bits / 64); + const src = bits <= 8 ? new Uint8Array(testCount) + : bits <= 16 ? new Uint16Array(testCount) + : bits <= 32 ? new Uint32Array(testCount) + : bits <= 64 ? new BigUint64Array(testCount) + : new BigUint64Array(testCount * parts); + webcrypto.getRandomValues(src); + const arr: bigint[] = []; + if (bits <= 64) { + src.forEach((n) => arr.push(f(BigInt(n)))); + } else { + const mask = (1n << BigInt(bits % 64)) - 1n; + for (let t = 0; t < testCount; t+=parts) { + let num = (src[t] as bigint) & mask; + for (let p = 1; p < parts; p++) { + num = (num << 64n) | (src[t+p] as bigint); + } + arr.push(f(num)); + } + } + tests[bits] = arr; + } + auras.forEach((a) => { + it(a + ' round-trips losslessly', () => { + for (let bits = minBits; bits <= maxBits; bits++) { + tests[bits].forEach((n) => { + n = BigInt(n); + expect(parse(a, render(a, n))).toEqual(n); + }); + } }); }); }); } -fuzz('8-bit', new Uint8Array(testCount)); -fuzz('16-bit', new Uint16Array(testCount)); -fuzz('32-bit', new Uint32Array(testCount)); -fuzz('64-bit', new BigUint64Array(testCount)); +fuzz( 1, 32, [...simpleAuras, 'if', 'is']); +fuzz(33, 64, [...simpleAuras, 'is']); +fuzz(65, 128, simpleAuras); + +// for floats, avoid NaNs, whose payload gets discarded during rendering, +// and as such usually won't round-trip cleanly. +// NaNs are encoded as "exponent bits all 1, non-zero significand", +// so we hard-set one pseudo-random exponent bit to 0 +function safeFloat(size: bigint, w: bigint, p: bigint) { + const full = (1n << size) - 1n; + const makemask = (n: bigint) => full ^ (1n << (p + (n % w))); + return (n: bigint) => n & makemask(n); +} +//TODO tests fail, off-by-one... +// fuzz(4, 16, ['rh'], safeFloat( 16n, 5n, 10n)); +// fuzz(4, 32, ['rs'], safeFloat( 32n, 8n, 23n)); +// fuzz(4, 64, ['rd'], safeFloat( 64n, 11n, 52n)); +// fuzz(4, 128, ['rq'], safeFloat(128n, 15n, 112n)); From a2d272c7777e6532f0791eb142168b1cd05f31d0 Mon Sep 17 00:00:00 2001 From: fang Date: Sat, 25 Oct 2025 22:28:26 +0200 Subject: [PATCH 46/50] parse: support `@dr` Splits the yule() out of year() so this can reuse the same logic used for `@da`. Makes some bignum conversions eager in the process. --- src/da.ts | 40 ++++++++++++++++++++++++++++++++-------- src/parse.ts | 10 ++++------ test/data/atoms.ts | 25 +++++++++++++++++++++++++ test/parse.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/da.ts b/src/da.ts index 413fe28..e02a134 100644 --- a/src/da.ts +++ b/src/da.ts @@ -31,6 +31,25 @@ export function parseDa(x: string): bigint { }); } +export function parseDr(x: string): bigint { + const rop: Tarp = { day: 0n, hour: 0n, minute: 0n, second: 0n, ms: [] }; + x = x.slice(1); // strip ~ + let [time, ms] = x.split('..'); + ms = ms || '0000'; + rop.ms = ms.split('.').map((m) => BigInt('0x' + m)); + time.split('.').forEach((a) => { + switch (a[0]) { + case 'd': rop.day += BigInt(a.slice(1)); break; + case 'h': rop.hour += BigInt(a.slice(1)); break; + case 'm': rop.minute += BigInt(a.slice(1)); break; + case 's': rop.second += BigInt(a.slice(1)); break; + default: throw new Error('bad dr: ' + x); + } + }); + ms = ms || '0000'; + return yule(rop); +} + /** * Given a bigint representing an urbit date, returns a string formatted as a proper `@da`. * @@ -149,19 +168,24 @@ function year(det: Dat) { return d; })(); - let sec = BigInt(det.time.second) - + (DAY_YO * day) - + (HOR_YO * BigInt(det.time.hour)) - + (MIT_YO * BigInt(det.time.minute)); + det.time.day = day; + return yule(det.time); +} + +function yule(rip: Tarp): bigint { + let sec = rip.second + + (DAY_YO * rip.day) + + (HOR_YO * rip.hour) + + (MIT_YO * rip.minute); - let ms = det.time.ms; + let ms = rip.ms; let fac = 0n; - let muc = 3; + let muc = 3n; while (ms.length !== 0) { const [first, ...rest] = ms; - fac = fac + (first << BigInt(16 * muc)); + fac = fac + (first << (16n * muc)); ms = rest; - muc -= 1; + muc -= 1n; } return fac | (sec << 64n); diff --git a/src/parse.ts b/src/parse.ts index 40fdc9f..77c074f 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -7,7 +7,7 @@ import { aura, dime, coin } from './types'; -import { parseDa } from './da'; +import { parseDa, parseDr } from './da'; import { parseValidP, regexP } from './p'; import { parseValidQ } from './q'; import { parseR, precision } from './r'; @@ -26,7 +26,7 @@ function floatRegex(a: number): RegExp { export const regex: { [key in aura]: RegExp } = { 'c': /^~\-((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, 'da': /^~(0|[1-9][0-9]*)\-?\.([1-9]|1[0-2])\.([1-9]|[1-3][0-9])(\.\.([0-9]+)\.([0-9]+)\.([0-9]+)(\.(\.[0-9a-f]{4})+)?)?$/, - 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))?(\.(\.[0-9a-f]{4})+)?$/, + 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))*(\.(\.[0-9a-f]{4})+)?$/, //TODO first ? to * mb 'f': /^\.(y|n)$/, 'if': /^(\.(0|[1-9][0-9]{0,2})){4}$/, 'is': /^(\.(0|[1-9a-fA-F][0-9a-fA-F]{0,3})){8}$/, @@ -203,9 +203,7 @@ export function nuck(str: string): coin | null { return { type: 'dime', aura: 'da', atom: parseDa(str) }; } else if (regex['dr'].test(str)) { - //TODO support @dr - console.log('aura-js: @dr unsupported (nuck)'); - return null; + return { type: 'dime', aura: 'dr', atom: parseDr(str) }; } else if (regex['p'].test(str)) { //NOTE this still does the regex check twice... @@ -251,7 +249,7 @@ function bisk(str: string): dime | null { } case '0c': // "fim" - //TODO support base58 + //TODO support base58check console.log('aura-js: @uc parsing unsupported (bisk)'); return null; diff --git a/test/data/atoms.ts b/test/data/atoms.ts index 9d22d49..b92e3c7 100644 --- a/test/data/atoms.ts +++ b/test/data/atoms.ts @@ -695,6 +695,31 @@ export const DATE_TESTS: { } ]; +export const TIME_AURAS: aura[] = ['dr']; +export const TIME_TESTS: { + n: bigint, + dr: string +}[] = [ + { 'n': 0n, + 'dr': '~s0' + }, + { 'n': 281474976710656n, + 'dr': '~s0..0001' + }, + { 'n': 18446744073709551616n, + 'dr': '~s1' + }, + { 'n': 1661332218022355928088576n, + 'dr': '~d1.h1.m1.s1' + }, + { 'n': 1968341379640822520656953344n, + 'dr': '~d1234.h23.m59.s59..ffff' + }, + { 'n': 1968341361194641392605855744n, + 'dr': '~d1234.h23.m59.s59..0000.ffff' + }, +] + export const TEXT_AURAS: aura[] = [ 't' ]; export const TEXT_TESTS: { n: bigint, diff --git a/test/parse.test.ts b/test/parse.test.ts index c409ba3..12dc32f 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -5,6 +5,7 @@ import { INTEGER_AURAS, INTEGER_TESTS, FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, + TIME_AURAS, TIME_TESTS, TEXT_AURAS, TEXT_TESTS, CHAR_AURAS, CHAR_TESTS, } from './data/atoms'; @@ -81,6 +82,27 @@ const OUR_DATE_TESTS: { 'da': '~2000.1.1..11.11.11..aabb.0000' } ]; +const OUR_TIME_TESTS: { + n: bigint, + dr: string +}[] = [ + ...TIME_TESTS, + { 'n': 9979688543876867424256n, + 'dr': '~m1.m5.s1.m3' + }, + { 'n': 18446744073709551616n, + 'dr': '~s1..0000' + }, + { 'n': 18446744073709551616n, + 'dr': '~s1..0000.0000' + }, + { 'n': 18446744073709551616n, + 'dr': '~d0.h0.s1.m0' + }, + { 'n': 69059795211765323057332224n, + 'dr': '~d1.h999.m999.s999' + }, +] testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); testAuras('ipv4', ['if'], IPV4_TESTS); @@ -91,6 +113,7 @@ testAuras('float64', ['rd'], FLOAT_64_TESTS); testAuras('float128', ['rq'], FLOAT_128_TESTS); testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); testAuras('date', DATE_AURAS, OUR_DATE_TESTS); +testAuras('time', TIME_AURAS, OUR_TIME_TESTS); testAuras('text', [ 't' ], TEXT_TESTS); testAuras('chars', CHAR_AURAS, CHAR_TESTS); From f9c0acf4de14ff9fa63ac5b963e297f7d7de07be Mon Sep 17 00:00:00 2001 From: fang Date: Sat, 25 Oct 2025 22:30:27 +0200 Subject: [PATCH 47/50] render: support `@dr` Now that both parsing and rendering are in, we can add them to fuzz tests. --- src/da.ts | 17 ++++++++++++++++- src/render.ts | 6 +++--- test/fuzz.test.ts | 2 +- test/render.test.ts | 2 ++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/da.ts b/src/da.ts index e02a134..86e0d1e 100644 --- a/src/da.ts +++ b/src/da.ts @@ -56,7 +56,7 @@ export function parseDr(x: string): bigint { * @param {bigint} x The urbit date as bigint * @return {string} The formatted `@da` */ -export function renderDa(x: bigint) { +export function renderDa(x: bigint): string { const { pos, year, month, time } = yore(x); let out = `~${year}${pos ? '' : '-'}.${month}.${time.day}`; if (time.hour !== 0n || time.minute !== 0n || time.second !== 0n || time.ms.length !== 0) { @@ -68,6 +68,21 @@ export function renderDa(x: bigint) { return out; } +export function renderDr(x: bigint): string { + if (x === 0n) return '~s0'; + const { day, hour, minute, second, ms } = yell(x); + let out: string[] = []; + if (day !== 0n) out.push('d' + day.toString()); + if (hour !== 0n) out.push('h' + hour.toString()); + if (minute !== 0n) out.push('m' + minute.toString()); + if (second !== 0n) out.push('s' + second.toString()); + if (ms.length !== 0) { + if (out.length === 0) out.push('s0'); + out.push('.' + ms.map((x) => x.toString(16).padStart(4, '0')).join('.')); + } + return '~' + out.join('.'); +} + /** * Given a bigint representing an urbit date, returns a unix timestamp. * diff --git a/src/render.ts b/src/render.ts index d66cdad..19f65e1 100644 --- a/src/render.ts +++ b/src/render.ts @@ -3,11 +3,11 @@ // atom literal rendering from hoon 137 (and earlier). // stdlib arm names are included for ease of cross-referencing. // -//TODO unsupported auras: @dr, @uc +//TODO unsupported auras: @uc import { aura, coin } from './types'; -import { renderDa } from './da'; +import { renderDa, renderDr } from './da'; import { renderP } from './p'; import { renderQ } from './q'; import { renderR } from './r'; @@ -49,7 +49,7 @@ export function rend(coin: coin): string { case 'a': return renderDa(coin.atom); case 'r': - throw new Error('aura-js: @dr rendering unsupported'); //TODO + return renderDr(coin.atom); default: return zco(coin.atom); } diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts index 6b9957d..c252ea7 100644 --- a/test/fuzz.test.ts +++ b/test/fuzz.test.ts @@ -9,7 +9,7 @@ const testCount = 50; const simpleAuras: aura[] = [ 'c', 'da', - // 'dr', //TODO unsupported + 'dr', // 'f', // limited legitimate values // 'if', // won't round-trip above "native" size limit // 'is', // won't round-trip above "native" size limit diff --git a/test/render.test.ts b/test/render.test.ts index 5ef4e3b..c72a4c0 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -5,6 +5,7 @@ import { INTEGER_AURAS, INTEGER_TESTS, FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, PHONETIC_AURAS, PHONETIC_TESTS, DATE_AURAS, DATE_TESTS, + TIME_AURAS, TIME_TESTS, TEXT_AURAS, TEXT_TESTS, CHAR_AURAS, CHAR_TESTS, } from './data/atoms'; @@ -58,6 +59,7 @@ testAuras('float64', ['rd'], FLOAT_64_TESTS); testAuras('float128', ['rq'], FLOAT_128_TESTS); testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); testAuras('date', DATE_AURAS, DATE_TESTS); +testAuras('time', TIME_AURAS, TIME_TESTS); testAuras('text', TEXT_AURAS, TEXT_TESTS); testAuras('chars', CHAR_AURAS, CHAR_TESTS); From c7a03a0bc630c63e38dde5bf16e5654516751d65 Mon Sep 17 00:00:00 2001 From: fang Date: Sat, 25 Oct 2025 22:39:25 +0200 Subject: [PATCH 48/50] lib: add some `@dr` utilities --- src/da.ts | 16 ++++++++++++++++ src/index.ts | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/da.ts b/src/da.ts index 86e0d1e..3ebfb26 100644 --- a/src/da.ts +++ b/src/da.ts @@ -110,6 +110,22 @@ export function fromUnix(unix: number): bigint { return DA_UNIX_EPOCH + timeSinceEpoch; } +/** + * Given a number of seconds, return a bigint representing its `@dr` + */ +export function fromSeconds(seconds: bigint): bigint { + return yule({ day: 0n, hour: 0n, minute: 0n, second: seconds, ms: [] }); +} + +/** + * Convert a `@dr` to the amount of seconds it represents, dropping sub- + * second precision + */ +export function toSeconds(dr: bigint): bigint { + const { day, hour, minute, second } = yell(dr); + return (((((day * 24n) + hour) * 60n) + minute) * 60n) + second; +} + // // internals // diff --git a/src/index.ts b/src/index.ts index 8bd65a8..ecc6b0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,9 @@ export { render, scot, rend } from './render'; //TODO expose encodeString() ? // atom utils -import { toUnix, fromUnix } from './da'; +import { toUnix, fromUnix, fromSeconds, toSeconds } from './da'; export const da = { toUnix, fromUnix }; +export const dr = { toSeconds, fromSeconds }; import type * as pt from './p'; import { cite, sein, clan, kind, rankToSize, sizeToRank } from './p'; From 79bfaafce14c35ae5a625557e6695db3b750b5f8 Mon Sep 17 00:00:00 2001 From: fang Date: Sat, 25 Oct 2025 22:40:49 +0200 Subject: [PATCH 49/50] lib: rename da.ts to d.ts It now deals with both `@da` and `@dr`, so should be named accordingly, similar to r.ts. --- src/{da.ts => d.ts} | 0 src/index.ts | 2 +- src/parse.ts | 2 +- src/render.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{da.ts => d.ts} (100%) diff --git a/src/da.ts b/src/d.ts similarity index 100% rename from src/da.ts rename to src/d.ts diff --git a/src/index.ts b/src/index.ts index ecc6b0e..97058fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ export { render, scot, rend } from './render'; //TODO expose encodeString() ? // atom utils -import { toUnix, fromUnix, fromSeconds, toSeconds } from './da'; +import { toUnix, fromUnix, fromSeconds, toSeconds } from './d'; export const da = { toUnix, fromUnix }; export const dr = { toSeconds, fromSeconds }; diff --git a/src/parse.ts b/src/parse.ts index 77c074f..cd9e3db 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -7,7 +7,7 @@ import { aura, dime, coin } from './types'; -import { parseDa, parseDr } from './da'; +import { parseDa, parseDr } from './d'; import { parseValidP, regexP } from './p'; import { parseValidQ } from './q'; import { parseR, precision } from './r'; diff --git a/src/render.ts b/src/render.ts index 19f65e1..4676bbb 100644 --- a/src/render.ts +++ b/src/render.ts @@ -7,7 +7,7 @@ import { aura, coin } from './types'; -import { renderDa, renderDr } from './da'; +import { renderDa, renderDr } from './d'; import { renderP } from './p'; import { renderQ } from './q'; import { renderR } from './r'; From d1efc74a4de8e30a557bb75195e7d536c458761f Mon Sep 17 00:00:00 2001 From: fang Date: Thu, 6 Nov 2025 20:41:45 +0100 Subject: [PATCH 50/50] parse: support UIP-135 style dates UIP-135 expands both parsing and rendering of `@da` literals to support leading zeroes in the month and day segments. By accepting leading zeroes in the parser, we become compatible with backends both before and after the UIP-135 change. Eventually, we'll want to switch to the "fixed-width" date rendering as well, but that doesn't make sense for this library when those backend changes haven't shipped yet. --- src/parse.ts | 2 +- test/parse.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/parse.ts b/src/parse.ts index cd9e3db..b548088 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -25,7 +25,7 @@ function floatRegex(a: number): RegExp { //TODO rewrite with eye towards capturing groups? export const regex: { [key in aura]: RegExp } = { 'c': /^~\-((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, - 'da': /^~(0|[1-9][0-9]*)\-?\.([1-9]|1[0-2])\.([1-9]|[1-3][0-9])(\.\.([0-9]+)\.([0-9]+)\.([0-9]+)(\.(\.[0-9a-f]{4})+)?)?$/, + 'da': /^~(0|[1-9][0-9]*)\-?\.0*([1-9]|1[0-2])\.0*[1-9][0-9]*(\.\.([0-9]+)\.([0-9]+)\.([0-9]+)(\.(\.[0-9a-f]{4})+)?)?$/, 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))*(\.(\.[0-9a-f]{4})+)?$/, //TODO first ? to * mb 'f': /^\.(y|n)$/, 'if': /^(\.(0|[1-9][0-9]{0,2})){4}$/, diff --git a/test/parse.test.ts b/test/parse.test.ts index 12dc32f..e880a73 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -66,6 +66,12 @@ const OUR_DATE_TESTS: { { 'n': 170141184492712641901540060096049971200n, 'da': '~2000.2.31' }, + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.01.01' + }, + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.001.001' + }, { 'n': 170141184492615892916284358229892268032n, 'da': '~2000.1.1..7.7.7' }, @@ -171,6 +177,7 @@ describe('invalid syntax', () => { }); it('fails bogus dates', () => { expect(nuck('~2025.1.0')).toEqual(null); + expect(nuck('~2025.0.1')).toEqual(null); expect(nuck('~2025.13.1')).toEqual(null); }); it('fails bogus @p or @q', () => {