From 2074516cd75be5f7808a7c0ee881afc98bfd1957 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Thu, 23 Apr 2026 17:26:25 +0200 Subject: [PATCH 01/19] spec/constraint_id: introduce FNV-1a hash --- spec/src.typ | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/spec/src.typ b/spec/src.typ index d553629ff..dc253fae4 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -115,6 +115,38 @@ } } +/// Fowler-Noll-Vo (FNV) hash function, version 1a +/// Src: https://en.wikipedia.org/wiki/Fowler-Noll-Vo_hash_function +/// +/// Note: this is a non-cryptographic hash function; it is optimized +/// for speed at the expense of unpredictability. +/// +/// This implementation operates on two 32-bit limbs, rather than a single +/// 64-bit limb, since Typst does not support u64s. +#let FNV-1a(bytes) = { + // FNV_prime := 0x00000100000001B3 + let prime = (0x000001B3, 0x00000100) + + // hash := FNV_offset_basis = 0xCBF29CE484222325 + let hash = (0x84222325, 0xCBF29CE4) + for b in bytes { + // hash := hash XOR byte_of_data + hash.at(0) = hash.at(0).bit-xor(b) + + // hash := hash × FNV_prime + let lo = hash.at(0) * prime.at(0) + let hi = hash.at(0) * prime.at(1) + hash.at(1) * prime.at(0) + + // Carry result + let carry = lo.bit-rshift(32) + let lo = lo.bit-and(0xFFFFFFFF) + let hi = (hi + carry).bit-and(0xFFFFFFFF) + hash = (lo, hi) + } + + hash.map(int.to-bytes).join() +} + /// Load a chip object from file /// /// - path(str): path to file containing chip data From a3e59509c2b82b258d228c0cd411284001a3e767 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Thu, 23 Apr 2026 15:19:26 +0200 Subject: [PATCH 02/19] spec/constraint_id: compute constraint ID upon load --- spec/src.typ | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/spec/src.typ b/spec/src.typ index dc253fae4..61ba8a90f 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -147,6 +147,114 @@ hash.map(int.to-bytes).join() } +/// Converts a byte array to a hexadecimal string +#let bytes-to-hex(bytes) = { + /// Pads a string with 0s on the left to reach a certain length + let z-fill(string) = "0" * (2 - string.len()) + string + + array(bytes) + .map(b => str(b, base: 16)) + .map(z-fill) + .sum() +} + +/// Tag constraints with an identifier +#let _add_constraint_ids(chip) = { + + /// A NON-CRYPTOGRAPHIC hash function. + let nchf(str) = FNV-1a(bytes(str)) + + // number of characters in constraint ID + let CONSTRAINT_ID_CHAR_COUNT = 4; + + /// Digests a variable based on its location and type. + let digest_variable(chip, group, idx, var) = { + /// Flatten the type of a variable into a string + let flatten_vartype(typ) = { + if type(typ) == array { + "(" + typ.map(flatten_vartype).join(",") + ")" + } else { + str(typ) + } + } + + let flattened_type = lower(flatten_vartype(var.type)) + let input = (chip, group, str(idx), flattened_type).join(",") + let digest = bytes-to-hex(nchf(input)) + digest.slice(0, count: 8) + } + + // Map variables to their ID + let variable_to_ID = chip + .variables + .pairs() + .map(((group, variables)) => { + variables + .enumerate() + .map(((idx, var)) => { + (var.name: digest_variable(chip.name, group, idx, var)) + }).sum() + }).sum() + + // replace variable with ID in LISP + let replace_variable_with_ID(lisp) = { + if type(lisp) == array { + "(" + lisp.map(replace_variable_with_ID).join(",") + ")" + } else { + variable_to_ID.at(str(lisp), default: str(lisp)) + } + } + + // Replace variable names with their ID + let digestable_constraint(c) = { + let CONSTRAINT_CAT_TO_SCOPE = ( + "interaction": ("iter", "input", "output", "multiplicity"), + "template": ("iter", "input", "output", "cond"), + "arith": ("iter", "poly") + ) + + assert(c.kind in CONSTRAINT_CAT_TO_SCOPE) + let id_tagged = CONSTRAINT_CAT_TO_SCOPE + .at(c.kind) + .filter(cat => cat in c.keys()) + .map(cat => (str(cat): replace_variable_with_ID(c.at(cat)))) + .sum(default: (:)) + + repr(id_tagged) + .replace("\n", "") + .replace(" ", "") + } + + // Map hash digest to ID + let digest_to_id(hash_bytes) = { + let CHARS = "123456789ABDEFGHJKLMNPQRSTUVWXYZ".codepoints() + assert(CHARS.len() == 32, message: "invalid CHARS length") + + let int = int.from-bytes(hash_bytes.slice(0, count: 8)) + for i in range(CONSTRAINT_ID_CHAR_COUNT) { + let idx = int.bit-and(31) + int = int.bit-rshift(5) + (CHARS.at(idx), ) + }.sum() + } + + // Add an ID to each constraint + chip.constraints = chip.at("constraints", default: (:)) + .pairs() + .map(((group, constraints)) => { + ( + str(group): + constraints + .map(c => { + c.id = digest_to_id(nchf(digestable_constraint(c))) + c + }) + ) + }).sum(default: (:)) + + chip +} + /// Load a chip object from file /// /// - path(str): path to file containing chip data @@ -154,5 +262,5 @@ #let load_chip(path, config) = { let chip = toml(path) _check_chip(chip, config) - return chip + return _add_constraint_ids(chip) } From acddf3a374d144b7945e608ccebc50388790da17 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 24 Apr 2026 11:54:18 +0200 Subject: [PATCH 03/19] spec/constraint_id: attempt at new id --- spec/chip.typ | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index c6cce5073..01e8c30cb 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -301,11 +301,11 @@ /// Render the contraint's tag. let tag(constraint, group) = { - let with_index(x) = ((x,) + iters_of(constraint).map(it => it.at(0))).join(".") + let with_index(x) = ((raw(x),) + iters_of(constraint).map(it => raw(it.at(0)))).join(".") let prefix = if "prefix" in group { group.prefix } - let lbl = [#chip.name\-C#prefix] - show figure: (it) => align(left, block[#lbl#context with_index(it.counter.display())]) - cref(constraint)[#figure(kind: chip.name + "constraint", numbering: (i) => [#lbl#i], supplement: [], [])] + let tag(x) = [#raw(chip.name + "-" + prefix + "" + constraint.id + "/")#with_index(x)] + show figure: (it) => align(left, context tag(it.counter.display())) + cref(constraint)[#figure(kind: chip.name + "constraint", numbering: (i) => tag(str(i)), supplement: [], [])] } /// Generates a representation of `constraint` From 58ac836485bdc2ffa65be11af35e98b75b112947 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 24 Apr 2026 14:57:59 +0200 Subject: [PATCH 04/19] spec: drop "constraint" from "polynomial constraint" --- spec/chip.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/chip.typ b/spec/chip.typ index 01e8c30cb..6b6c6058d 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -346,7 +346,7 @@ } (..for poly in polys { - (table.cell(align: right, colspan: 2, [_polynomial constraint_]), $#expr_to_math(poly) = 0$, []) + (table.cell(align: right, colspan: 2, [_polynomial_]), $#expr_to_math(poly) = 0$, []) },) } From b635dbdb699b8402f6b78b8723ff382a74bc06a6 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 14:50:46 +0200 Subject: [PATCH 05/19] spec/constraint_id: refactor ID --- spec/chip.typ | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 6b6c6058d..c526db21c 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -301,11 +301,20 @@ /// Render the contraint's tag. let tag(constraint, group) = { - let with_index(x) = ((raw(x),) + iters_of(constraint).map(it => raw(it.at(0)))).join(".") - let prefix = if "prefix" in group { group.prefix } - let tag(x) = [#raw(chip.name + "-" + prefix + "" + constraint.id + "/")#with_index(x)] - show figure: (it) => align(left, context tag(it.counter.display())) - cref(constraint)[#figure(kind: chip.name + "constraint", numbering: (i) => tag(str(i)), supplement: [], [])] + let counter-kind = chip.name + "constraint" + let tag = chip.name + "-" + constraint.id + + let indices = (("",) + iters_of(constraint).map(it => it.at(0))).join(".") + + let z-fill(s) = "0" * (2 - s.len()) + s + let ref-tag(i) = raw(tag) + sub("/" + z-fill(str(i))) + return ( + context super[#emph(z-fill(str(counter(figure.where(kind: counter-kind)).get().at(0) + 1)))], + [ + #show figure: (it) => align(left, raw(tag + indices)) + #cref(constraint)[#figure(kind: counter-kind, numbering: (i) => ref-tag(i), supplement: [], [])] + ], + ) } /// Generates a representation of `constraint` @@ -346,13 +355,23 @@ } (..for poly in polys { - (table.cell(align: right, colspan: 2, [_polynomial_]), $#expr_to_math(poly) = 0$, []) + ( + [], + table.cell(align: right, colspan: 2, [_polynomial_]), + table.cell(align: left, colspan: 1, $#expr_to_math(poly) = 0$), + [] + ) },) } // Rendering the additional "desc" field for arith constraints let render_extra_description(constraint) = { - (table.cell(align: right, colspan: 2, [_description_]), eval(constraint.desc, mode: "markup"), []) + ( + [], + table.cell(align: right, colspan: 2, [_description_]), + table.cell(align: left, colspan: 1, eval(constraint.desc, mode: "markup")), + [] + ) } // Whether there is at least one constraint with a range @@ -365,11 +384,12 @@ show figure: set block(breakable: true) figure(table( - columns: (auto, auto, 1fr, auto), + columns: (auto, auto, auto, 1fr, auto), inset: 6pt, - align: (top + left, top + left, top + left, top + center), + align: (top + left, top + left, top + left, top + left, top + center), stroke: none, table.header( +[], [*Tag*], if do_display_range {[*Range*]} else {[]}, [*Description*], @@ -379,7 +399,7 @@ ..for (group, group_constraints) in selected_constraints.pairs() { for constraint in group_constraints { ( - [#tag(constraint, lookup_group(group))], + ..tag(constraint, lookup_group(group)), [#iters(constraint)], [#repr_constraint(constraint)], [#expr_to_math(constraint.at("multiplicity", default: ""))], From 89037517758ebedc00e4f047a7373fac63b9867d Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 14:52:28 +0200 Subject: [PATCH 06/19] spec: completely hide unused constraint table columns --- spec/chip.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/chip.typ b/spec/chip.typ index c526db21c..1f3c1e594 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -384,7 +384,7 @@ show figure: set block(breakable: true) figure(table( - columns: (auto, auto, auto, 1fr, auto), + columns: (auto, auto, if do_display_range {auto} else {0pt}, 1fr, if do_display_multiplicity {auto} else {0pt}), inset: 6pt, align: (top + left, top + left, top + left, top + left, top + center), stroke: none, From 37468f1f3f61ec8e1341409017c9bbfed604bc63 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 14:52:58 +0200 Subject: [PATCH 07/19] spec: squish column table iter notation --- spec/chip.typ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 1f3c1e594..a2a1027bb 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -251,7 +251,7 @@ // Render the iterators of `obj`. #let iters(obj) = { - iters_of(obj).map(iter => [#raw(iter.at(0)) #sym.in `[`#expr_to_code(iter.at(1)), #expr_to_code(iter.at(2))`]`]).join("\n") + iters_of(obj).map(iter => [#raw(iter.at(0))#sym.in`[`#expr_to_code(iter.at(1)),#expr_to_code(iter.at(2))`]`]).join("\n") } #let args_interaction_like(input, output) = { @@ -389,7 +389,7 @@ align: (top + left, top + left, top + left, top + left, top + center), stroke: none, table.header( -[], + [], [*Tag*], if do_display_range {[*Range*]} else {[]}, [*Description*], From 08fab18472532711ffa2cb1cef0dbd8a3669b8f2 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 14:58:00 +0200 Subject: [PATCH 08/19] spec: reduce some constraint table column widths --- spec/chip.typ | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index a2a1027bb..d39863ef5 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -385,7 +385,12 @@ show figure: set block(breakable: true) figure(table( columns: (auto, auto, if do_display_range {auto} else {0pt}, 1fr, if do_display_multiplicity {auto} else {0pt}), - inset: 6pt, + inset: (x,_) => ( + left: if x == 0 or x == 1 {0pt} else {6pt}, + right: if x == 4 {0pt} else {6pt}, + top: 6pt, + bottom: 6pt + ), align: (top + left, top + left, top + left, top + left, top + center), stroke: none, table.header( @@ -393,7 +398,7 @@ [*Tag*], if do_display_range {[*Range*]} else {[]}, [*Description*], - if do_display_multiplicity {[*Multiplicity*]} else {[]}, + if do_display_multiplicity {[*Multip.*]} else {[]}, ), table.hline(stroke: stroke(thickness: 2pt)), ..for (group, group_constraints) in selected_constraints.pairs() { From 65bd6489c2d0c411c4ce8c2b3c8b87f29e63d04d Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 14:58:43 +0200 Subject: [PATCH 09/19] spec: introduce thin lines between constraints --- spec/chip.typ | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/chip.typ b/spec/chip.typ index d39863ef5..ceb16ad53 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -415,6 +415,7 @@ if has_polynomial_constraints(constraint) { render_polynomial_constraints(constraint) } + (table.hline(stroke: stroke(thickness: .25pt)),) } } )) From d4680af6174c83f33de31da5cec419a4901a29bb Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 15:08:44 +0200 Subject: [PATCH 10/19] spec: use chip codes as shorthand in constraint notation --- spec/chip.typ | 5 +++-- spec/src/branch.toml | 1 + spec/src/commit.toml | 1 + spec/src/keccak.toml | 1 + spec/src/keccak_rc.toml | 1 + spec/src/keccak_round.toml | 1 + spec/src/load.toml | 1 + spec/src/memw.toml | 1 + spec/src/memw_aligned.toml | 1 + spec/src/memw_register.toml | 1 + spec/src/rotxor.toml | 1 + spec/src/sha256.toml | 1 + spec/src/sha256consts.toml | 1 + spec/src/sha256msgsched.toml | 1 + spec/src/sha256round.toml | 1 + spec/src/shift.toml | 1 + spec/src/sign.toml | 1 + 17 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index ceb16ad53..8e70f1366 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -301,8 +301,9 @@ /// Render the contraint's tag. let tag(constraint, group) = { - let counter-kind = chip.name + "constraint" - let tag = chip.name + "-" + constraint.id + let code = chip.at("code", default: chip.name) + let counter-kind = code + "constraint" + let tag = code + "-" + constraint.id let indices = (("",) + iters_of(constraint).map(it => it.at(0))).join(".") diff --git a/spec/src/branch.toml b/spec/src/branch.toml index a98974678..7e296aa93 100644 --- a/spec/src/branch.toml +++ b/spec/src/branch.toml @@ -1,4 +1,5 @@ name = "BRANCH" +code = "BRH" # Input diff --git a/spec/src/commit.toml b/spec/src/commit.toml index 89fa133c6..912b61369 100644 --- a/spec/src/commit.toml +++ b/spec/src/commit.toml @@ -1,4 +1,5 @@ name = "COMMIT" +code = "CMT" # Variables diff --git a/spec/src/keccak.toml b/spec/src/keccak.toml index b8f2d91c2..64cb4b6cd 100644 --- a/spec/src/keccak.toml +++ b/spec/src/keccak.toml @@ -1,4 +1,5 @@ name = "KECCAK" +code = "KCK" [[variables.input]] name = "timestamp" diff --git a/spec/src/keccak_rc.toml b/spec/src/keccak_rc.toml index 7844dfbee..c15566a25 100644 --- a/spec/src/keccak_rc.toml +++ b/spec/src/keccak_rc.toml @@ -1,4 +1,5 @@ name = "KECCAK_RC" +code = "KCC" [[variables.input]] name = "round" diff --git a/spec/src/keccak_round.toml b/spec/src/keccak_round.toml index 548cb5151..ba5d1e160 100644 --- a/spec/src/keccak_round.toml +++ b/spec/src/keccak_round.toml @@ -1,4 +1,5 @@ name = "KECCAK_RND" +code = "KCR" [[variables.input]] name = "timestamp" diff --git a/spec/src/load.toml b/spec/src/load.toml index f8a974c9a..1e346a3e9 100644 --- a/spec/src/load.toml +++ b/spec/src/load.toml @@ -1,4 +1,5 @@ name = "LOAD" +code = "LD" # Input diff --git a/spec/src/memw.toml b/spec/src/memw.toml index 1cc0dd3c2..5ff6d3b9b 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -1,4 +1,5 @@ name = "MEMW" +code = "MMW" # Input diff --git a/spec/src/memw_aligned.toml b/spec/src/memw_aligned.toml index 93a636aba..6903e8066 100644 --- a/spec/src/memw_aligned.toml +++ b/spec/src/memw_aligned.toml @@ -1,4 +1,5 @@ name = "MEMW_A" +code = "MWA" # Input diff --git a/spec/src/memw_register.toml b/spec/src/memw_register.toml index 3e7cdcf28..6858c4ca9 100644 --- a/spec/src/memw_register.toml +++ b/spec/src/memw_register.toml @@ -1,4 +1,5 @@ name = "MEMW_R" +code = "MWR" # Variables diff --git a/spec/src/rotxor.toml b/spec/src/rotxor.toml index 730e9bda5..2e1e99e1f 100644 --- a/spec/src/rotxor.toml +++ b/spec/src/rotxor.toml @@ -1,4 +1,5 @@ name = "ROTXOR" +code = "RTXR" [[variables.input]] name = "a" diff --git a/spec/src/sha256.toml b/spec/src/sha256.toml index 4cd4de9ba..9aaaaf24b 100644 --- a/spec/src/sha256.toml +++ b/spec/src/sha256.toml @@ -1,4 +1,5 @@ name = "SHA256" +code = "SHA" [[variables.input]] name = "timestamp" diff --git a/spec/src/sha256consts.toml b/spec/src/sha256consts.toml index 17fe6fb0f..ab9354801 100644 --- a/spec/src/sha256consts.toml +++ b/spec/src/sha256consts.toml @@ -1,4 +1,5 @@ name = "SHA256_K" +code = "SHK" [[variables.input]] name = "index" diff --git a/spec/src/sha256msgsched.toml b/spec/src/sha256msgsched.toml index 79664a797..e32359a2b 100644 --- a/spec/src/sha256msgsched.toml +++ b/spec/src/sha256msgsched.toml @@ -1,4 +1,5 @@ name = "SHA256MSGSCHED" +code = "SHM" [[variables.input]] name = "timestamp" diff --git a/spec/src/sha256round.toml b/spec/src/sha256round.toml index 8ec93ea36..4ec70f563 100644 --- a/spec/src/sha256round.toml +++ b/spec/src/sha256round.toml @@ -1,4 +1,5 @@ name = "SHA256ROUND" +code = "SHR" [[variables.input]] name = "timestamp" diff --git a/spec/src/shift.toml b/spec/src/shift.toml index bbe22a5d9..f441ccbe8 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -1,4 +1,5 @@ name = "SHIFT" +code = "SHF" # Input diff --git a/spec/src/sign.toml b/spec/src/sign.toml index 24e99bd0e..929404208 100644 --- a/spec/src/sign.toml +++ b/spec/src/sign.toml @@ -1,4 +1,5 @@ name = "SIGN" +code = "SGN" [[variables.input]] name = "X" From 331547f3c999d30d34d128631bc5c18b60b71eea Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 15:26:30 +0200 Subject: [PATCH 11/19] spec: fix tooling --- spec/tooling/chip.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index 7f7ecca81..34ba13565 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -970,6 +970,7 @@ def build_constraint(config, data: dict) -> Constraint: class Chip: config: Config name: str + code: str variables: list[Variable] assumptions: list[Assumption] constraints: list[Constraint] @@ -986,6 +987,11 @@ def __init__(self, config: Config, data: dict): isinstance(self.name, str), f"name is not a string: {self.name!r}" ) reporter.asserts(self.name.isidentifier(), f"Invalid identifier: {self.name!r}") + self.code = data.get("name", "") + reporter.asserts( + isinstance(self.code, str), f"code is not a string: {self.code!r}" + ) + reporter.asserts(self.code.isidentifier(), f"Invalid identifier: {self.name!r}") self.variables = [ (Variable if cat != "virtual" else VirtualVariable)(config, cat, var) for cat, vars in data["variables"].items() From 2d989fe72b4b9deeb3ecf2793ca2ea8797babc25 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 15:37:42 +0200 Subject: [PATCH 12/19] spec: update assumption naming --- spec/chip.typ | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 8e70f1366..f010cd7a1 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -264,10 +264,12 @@ #let render_chip_assumptions(chip, config) = { let tag(assumption) = { - let with_index(x) = ((x,) + iters_of(assumption).map(it => it.at(0))).join(".") - let lbl = [#chip.name\-A] - show figure: (it) => align(left, block[#lbl#context with_index(it.counter.display())]) - cref(assumption)[#figure(kind: chip.name + "assumption", numbering: (i) => [#lbl#i], supplement: [], [])] + let code = if "code" in chip {chip.code} else {chip.name} + let index = (("",) + iters_of(assumption).map(it => it.at(0))).join(`.`) + let lbl(idx) = raw(code + "-A" + str(idx)) + + show figure: (it) => align(left, block[#context lbl(it.counter.get().at(0))#index]) + cref(assumption)[#figure(kind: code + "assumption", numbering: (i) => lbl(i), supplement: [], [])] } show figure: set block(breakable: true) From b0f2694169d745e0b8d51955daaf204ffa07c379 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 15:43:25 +0200 Subject: [PATCH 13/19] spec/chip: fix missing sum defaults --- spec/chip.typ | 2 +- spec/signatures.typ | 2 +- spec/src.typ | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index f010cd7a1..2688e52ad 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -24,7 +24,7 @@ config.variables.types.filter(type => type.label == var_type).first().subtypes.len() * factor }) - .sum() + .sum(default: 0) } // Given a constraint, compute the number of interactions it induces diff --git a/spec/signatures.typ b/spec/signatures.typ index 2839a74c6..4d0840a66 100644 --- a/spec/signatures.typ +++ b/spec/signatures.typ @@ -43,7 +43,7 @@ let lbl = v config.variables.types.filter(type => type.label == lbl).first().subtypes.len() * factor }) - .sum() + .sum(default: 0) } #let interactions = signatures.signatures.filter(s => s.kind == "interaction") diff --git a/spec/src.typ b/spec/src.typ index 61ba8a90f..981816141 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -193,8 +193,8 @@ .enumerate() .map(((idx, var)) => { (var.name: digest_variable(chip.name, group, idx, var)) - }).sum() - }).sum() + }).sum(default: (:)) + }).sum(default: (:)) // replace variable with ID in LISP let replace_variable_with_ID(lisp) = { @@ -230,6 +230,7 @@ let CHARS = "123456789ABDEFGHJKLMNPQRSTUVWXYZ".codepoints() assert(CHARS.len() == 32, message: "invalid CHARS length") + assert(hash_bytes.len() >= 8, message: "too few bytes to digest: " + repr(hash_bytes)) let int = int.from-bytes(hash_bytes.slice(0, count: 8)) for i in range(CONSTRAINT_ID_CHAR_COUNT) { let idx = int.bit-and(31) From 35bc74b6f24114a44bfa2b1440593ad631dc7a1d Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 15:46:38 +0200 Subject: [PATCH 14/19] spec: fix z-fill bug --- spec/chip.typ | 2 +- spec/src.typ | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 2688e52ad..8a280f6ea 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -309,7 +309,7 @@ let indices = (("",) + iters_of(constraint).map(it => it.at(0))).join(".") - let z-fill(s) = "0" * (2 - s.len()) + s + let z-fill(s) = "0" * calc.max(2 - s.len(), 0) + s let ref-tag(i) = raw(tag) + sub("/" + z-fill(str(i))) return ( context super[#emph(z-fill(str(counter(figure.where(kind: counter-kind)).get().at(0) + 1)))], diff --git a/spec/src.typ b/spec/src.typ index 981816141..12951e389 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -150,7 +150,7 @@ /// Converts a byte array to a hexadecimal string #let bytes-to-hex(bytes) = { /// Pads a string with 0s on the left to reach a certain length - let z-fill(string) = "0" * (2 - string.len()) + string + let z-fill(str) = "0" * calc.max(2 - str.len(), 0) + str array(bytes) .map(b => str(b, base: 16)) From 764c8d74ccec73396caef36f27ebb5f0a993df8c Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:49:13 +0200 Subject: [PATCH 15/19] spec/constraint_id: use \x00 as domain separator Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- spec/src.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/src.typ b/spec/src.typ index 12951e389..2557b5333 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -179,7 +179,7 @@ } let flattened_type = lower(flatten_vartype(var.type)) - let input = (chip, group, str(idx), flattened_type).join(",") + let input = (chip, group, str(idx), flattened_type).join("\x00") let digest = bytes-to-hex(nchf(input)) digest.slice(0, count: 8) } From 9f6a43abed3811e4bcf77dce715b0d58c6b518cb Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Tue, 28 Apr 2026 15:51:13 +0200 Subject: [PATCH 16/19] spec/chip: fix reading "code" --- spec/tooling/chip.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index 34ba13565..6b8e6a565 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -987,11 +987,12 @@ def __init__(self, config: Config, data: dict): isinstance(self.name, str), f"name is not a string: {self.name!r}" ) reporter.asserts(self.name.isidentifier(), f"Invalid identifier: {self.name!r}") - self.code = data.get("name", "") - reporter.asserts( - isinstance(self.code, str), f"code is not a string: {self.code!r}" - ) - reporter.asserts(self.code.isidentifier(), f"Invalid identifier: {self.name!r}") + self.code = data.get("code", "") + if self.code: + reporter.asserts( + isinstance(self.code, str), f"code is not a string: {self.code!r}" + ) + reporter.asserts(self.code.isidentifier(), f"Invalid identifier: {self.code!r}") self.variables = [ (Variable if cat != "virtual" else VirtualVariable)(config, cat, var) for cat, vars in data["variables"].items() From b824b4ba1c69b6bb46d8f526853d8f5c6cd68057 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:54:24 +0200 Subject: [PATCH 17/19] spec/constraint_id: include `tag` in constraint-ID derivation --- spec/src.typ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/src.typ b/spec/src.typ index 2557b5333..dd7aa0021 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -208,8 +208,8 @@ // Replace variable names with their ID let digestable_constraint(c) = { let CONSTRAINT_CAT_TO_SCOPE = ( - "interaction": ("iter", "input", "output", "multiplicity"), - "template": ("iter", "input", "output", "cond"), + "interaction": ("tag", "iter", "input", "output", "multiplicity"), + "template": ("tag", "iter", "input", "output", "cond"), "arith": ("iter", "poly") ) From 95216ecc4739596354589a11c66753915d3dbcba Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Wed, 29 Apr 2026 10:05:59 +0200 Subject: [PATCH 18/19] spec/constraint_id: support indexing for chips beyond 99 constraints --- spec/chip.typ | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 8a280f6ea..29dc02387 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -309,10 +309,11 @@ let indices = (("",) + iters_of(constraint).map(it => it.at(0))).join(".") - let z-fill(s) = "0" * calc.max(2 - s.len(), 0) + s - let ref-tag(i) = raw(tag) + sub("/" + z-fill(str(i))) + let pad-width() = calc.max(calc.ceil(calc.log(counter(figure.where(kind: counter-kind)).final().at(0))), 2) + let z-pad(s) = context "0" * calc.max(pad-width() - s.len(), 0) + s + let ref-tag(i) = raw(tag) + sub("/" + z-pad(str(i))) return ( - context super[#emph(z-fill(str(counter(figure.where(kind: counter-kind)).get().at(0) + 1)))], + context super[#emph(z-pad(str(counter(figure.where(kind: counter-kind)).get().at(0) + 1)))], [ #show figure: (it) => align(left, raw(tag + indices)) #cref(constraint)[#figure(kind: counter-kind, numbering: (i) => ref-tag(i), supplement: [], [])] From a27e78f7771144339de9c5302ab55ef7497d6afe Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Wed, 29 Apr 2026 10:19:07 +0200 Subject: [PATCH 19/19] spec/constraint_id: drop bytes-to-hex --- spec/src.typ | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/spec/src.typ b/spec/src.typ index dd7aa0021..456a6a132 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -167,6 +167,26 @@ // number of characters in constraint ID let CONSTRAINT_ID_CHAR_COUNT = 4; + // Map hash digest to ID + let digest_to_id(hash_bytes) = { + // Character set used to represent ID + let CHARS = "123456789ABDEFGHJKLMNPQRSTUVWXYZ".codepoints() + assert(CHARS.len() == 32, message: "invalid CHARS length") + + let min_bytes_len = 2 * CONSTRAINT_ID_CHAR_COUNT + assert( + hash_bytes.len() >= min_bytes_len, + message: "too few bytes to digest: " + repr(hash_bytes) + " has " + str(hash_bytes.len()) + " where " + str(min_bytes_len) + " is required." + ) + + let int = int.from-bytes(hash_bytes.slice(0, count: min_bytes_len)) + for _ in range(CONSTRAINT_ID_CHAR_COUNT) { + let idx = int.bit-and(31) + int = int.bit-rshift(5) + (CHARS.at(idx), ) + }.sum() + } + /// Digests a variable based on its location and type. let digest_variable(chip, group, idx, var) = { /// Flatten the type of a variable into a string @@ -180,8 +200,7 @@ let flattened_type = lower(flatten_vartype(var.type)) let input = (chip, group, str(idx), flattened_type).join("\x00") - let digest = bytes-to-hex(nchf(input)) - digest.slice(0, count: 8) + digest_to_id(nchf(input)) } // Map variables to their ID @@ -225,20 +244,6 @@ .replace(" ", "") } - // Map hash digest to ID - let digest_to_id(hash_bytes) = { - let CHARS = "123456789ABDEFGHJKLMNPQRSTUVWXYZ".codepoints() - assert(CHARS.len() == 32, message: "invalid CHARS length") - - assert(hash_bytes.len() >= 8, message: "too few bytes to digest: " + repr(hash_bytes)) - let int = int.from-bytes(hash_bytes.slice(0, count: 8)) - for i in range(CONSTRAINT_ID_CHAR_COUNT) { - let idx = int.bit-and(31) - int = int.bit-rshift(5) - (CHARS.at(idx), ) - }.sum() - } - // Add an ID to each constraint chip.constraints = chip.at("constraints", default: (:)) .pairs()