diff --git a/.github/workflows/pr_spec.yaml b/.github/workflows/pr_spec.yaml new file mode 100644 index 000000000..08bf81c4b --- /dev/null +++ b/.github/workflows/pr_spec.yaml @@ -0,0 +1,25 @@ +name: Spec tests +on: + pull_request: + branches: + - main + - 'spec/**' + push: + branches: ["**"] + paths: ["spec/**"] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + spec_structure: + name: Spec structure test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + - run: python3 spec/tooling/chip.py spec/src/config.toml spec/src/signatures.toml spec/src/*.toml diff --git a/spec/.editorconfig b/spec/.editorconfig new file mode 100644 index 000000000..dbb9605a4 --- /dev/null +++ b/spec/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.typ] +indent_style = space +indent_size = 2 diff --git a/spec/.gitignore b/spec/.gitignore new file mode 100644 index 000000000..73218d5ba --- /dev/null +++ b/spec/.gitignore @@ -0,0 +1,2 @@ +dist/* +ebook.pdf diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..127e528c8 --- /dev/null +++ b/spec/README.md @@ -0,0 +1,11 @@ +# LambdaVM specification +This repository contains specification for [`LambdaVM`](https://github.com/yetanotherco/lambda_vm). +The specification is written in [`Typst`](https://typst.app/) and can be rendered by [`shiroa`](https://myriad-dreamin.github.io/shiroa/) as either a file (pdf) or a wiki (html). + +## Installation & Development setup +1. [Install `Typst`](https://github.com/typst/typst?tab=readme-ov-file#installation). +2. [Install `shiroa`](https://myriad-dreamin.github.io/shiroa/guide/installation.html). +3. Clone this repository. +4. Open the repository in a terminal and execute `shiroa serve`. + +At this point, the wiki version is hosted locally and is actively updated as you modify the specification files. diff --git a/spec/about_ecalls.typ b/spec/about_ecalls.typ new file mode 100644 index 000000000..ef4203610 --- /dev/null +++ b/spec/about_ecalls.typ @@ -0,0 +1,24 @@ +#import "/book.typ": book-page, aside +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_assumptions, + render_chip_padding_table, +) + +#let config = load_config() + +#show: book-page("about_ecalls.typ") + +ECALLs provide system-level functionalities to the guest program. + +When `ECALL` is executed, it is assumed that: +- register `A7` contains the system call number + #footnote([The RISC-V system call ABI; libriscv.no, #link("https://web.archive.org/web/20260128152107/https://libriscv.no/docs/concepts/syscalls/#the-risc-v-system-call-abi")[[src]]]), +- the arguments are located in registers `A0`-`A6`, and +- the return value is written to `A0`, +where `A0`-`A7` are symbolic names for the registers `x10`-`x17` +#footnote([RISC-V - Register sets; en.wikipedia.org, #link("https://web.archive.org/web/20260209053447/https://en.wikipedia.org/wiki/RISC-V#Register_sets")[[src]]]). diff --git a/spec/add.typ b/spec/add.typ new file mode 100644 index 000000000..0772aa30b --- /dev/null +++ b/spec/add.typ @@ -0,0 +1,33 @@ +#import "/book.typ": book-page, et +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_variable_table, render_chip_assumptions, render_constraint_table, set_nr_interactions, compute_nr_interactions, + +#let config = load_config() +#let chip = load_chip("src/add.toml", config) + +#show: book-page(chip.name) + +#set_nr_interactions(chip, name: "SUB") +#let nr_interactions = compute_nr_interactions(chip) + +#let add = raw(chip.name) +#let sub = raw("SUB") + +#add is a constraint template that is used to assert that $#`sum` equiv #`lhs` + #`rhs` (mod 2^64)$, under the condition that `cond` is non-zero. +For ease of notation, we moreover introduce the #sub constraint template +$ +#`SUB` := #`ADD`, +$ +in both conditional and unconditional versions. +It constrains that $#`diff` equiv #`lhs` - #`rhs` (mod 2^64)$ when the expression `cond` is non-zero. + += Variables +This template introduces #nr_interactions interaction(s). +#render_chip_variable_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Constraints +This template introduces the following constraints +#render_constraint_table(chip, config) diff --git a/spec/bitwise.typ b/spec/bitwise.typ new file mode 100644 index 000000000..82f9e36f9 --- /dev/null +++ b/spec/bitwise.typ @@ -0,0 +1,45 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, +) + +#let config = load_config() +#let chip = load_chip("src/bitwise.toml", config) + +#let bitwise = raw(chip.name) + +#show: book-page(chip.name) +#let bitwise = raw(chip.name) + +The #bitwise chips deal with precomputed lookup tables for bitwise boolean operations +and convenience functionalities over small domains. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_precomputed = ("input", "output").map(c => chip.variables.at(c)).flatten().len() + +The #bitwise chip is comprised of #nr_variables variables that are expressed using #nr_columns columns. +Of these, the _input_ and _output_ variables (#nr_precomputed in total) are precomputed. +#render_chip_variable_table(chip, config) + +*Note*: This table contains one row for every possible value of `(X, Y, Z)`. +As such, it has length $2^8 dot 2^8 dot 2^4 = 2^(20)$. + += Lookup +This chip adds the following interactions to the lookup: +#render_constraint_table(chip, config) + += Notes/Optimizations +The following ideas may prove to be optimizations for the #bitwise chip: ++ Extend `IS_BYTE[X]` to `ARE_BYTES[X, Y]`, such that two bytes are range checked at once. + When only a single check is required, one can still execute `IS_BYTE[X] := ARE_BYTES[X, 0]`. ++ Drop `MSB8` column, and instead define the `MSB8` lookup as `MSB8 := MSB16[256X]`. + Note: currently, `MSB8` also implicity range checks the input `X` (the lookup fails if `X` is not a `Byte`). + This optimization should only be executed when all chips leveraging `MSB8` do _not_ need this implicit range check. ++ Place the 16-bit (`AND`, `OR`, `XOR`, `MSB16`, etc.) and 20-bit (`HWSL`, `IS_B20`, `ZERO`) lookups in separate tables. diff --git a/spec/book.typ b/spec/book.typ new file mode 100644 index 000000000..bcc5fec19 --- /dev/null +++ b/spec/book.typ @@ -0,0 +1,185 @@ +#import "@preview/shiroa:0.3.1": * +#import "/templates/page.typ": project + +#show: book + +#let meta = ( + title: "Lambda VM specification", + authors: ("3MI Labs", "Aligned"), + version: "0.2", + summary: ( + ("PROOF SYSTEM", ( + ("logup.typ", [`LogUp` argument], ), + ("memory.typ", [Memory argument], ), + )), + ("OVERVIEW", ( + ("variables.typ", [Variables], ), + ("signatures.typ", [Signatures], ), + )), + ("TEMPLATES", ( + ("is_bit.typ", [`IS_BIT` template], ), + ("sign.typ", [`SIGN` template], ), + ("add.typ", [`ADD`/`SUB` template], ), + ("neg.typ", [`NEG` template], ), + )), + ("MEMORY", ( + ("memw.typ", [`MEMW` chip], ), + )), + ("CPU", ( + ("decode.typ", [`DECODE` table], ), + ("cpu.typ", [`CPU` chip], ), + )), + ("ALU", ( + ("shift.typ", [`SHIFT` chip], ), + ("branch.typ", [`BRANCH` chip], ), + ("lt.typ", [`LT` chip], ), + ("mul.typ", [`MUL` chip], ), + ("dvrm.typ", [`DVRM` chip], ), + ("load.typ", [`LOAD` chip], ), + ("bitwise.typ", [`BITWISE` chips], ), + )), + ("ECALLS", ( + ("about_ecalls.typ", [About `ECALL`], ), + ("halt.typ", [`HALT` chip], ), + ("commit.typ", [`COMMIT` chip], ), + )) + ) +) +#let meta_sections = meta.summary.map(m => m.at(1)).sum() +#book-meta( + title: meta.title, + authors: meta.authors, + summary: prefix-chapter("front.typ", meta.title) + + meta.summary.map( + ((title, sections)) => { + heading(depth: 1, title) + sections.map(((ch, title, _ref)) => chapter(ch, title)).join() + } + ).join() +) + +#let common-formatting(body) = { + set footnote(numbering: "[1]") + show raw.where(block: true): it => block(it, inset: 1em, width: 100%, radius: 5pt) + body +} + + +#let todo(background: white, foreground: black, name: none, body) = block(fill: background, outset: 0.4em, radius: 20%, stroke: black)[ + #set text(fill: foreground) + *TODO #if name != none { [(#name)] }*: #body +] +#let rj = todo.with(background: teal, name: "Robin") +#let et = todo.with(background: rgb("d4aa3a"), name: "Erik") +#let cdsg = todo.with(background: olive, name: "Cyprien") + +#let aside(title, body) = context figure( + block(inset: (left: 1em, right: 1em, bottom: 1em), stroke: luma(50%), breakable: false)[ + #block(inset: (left: 1em, right: 1em, top: .75em, bottom: .75em), + width: 100% + 2em, + fill: rgb("55aaff"), + stroke: luma(50%), + align(center, strong(text(fill: black, title)))) + #align(left, body) +]) + + +#let is-shiroa = "x-target" in sys.inputs + +// Strip styling to keep only "pure" content. +// This is useful to avoid errors on the `set document(...)` in `project` +// when invisibly including other chapters to resolve xrefs. +#let strip-all(content) = { + if repr(content.func()) == "sequence" { + for c in content.children { + strip-all(c) + } + } else if repr(content.func()) == "styled" { + strip-all(content.child) + } else { + content + } +} + +#let _toplevel = state("_toplevel", none) +#let _xref-included = state("_xref-included", (:)) + +// Invisibly include another chapter, so that its labels can be resolved +#let xref-include(f) = { + context { + place(hide(box(width: auto, height: 0%, strip-all(include "/" + f)))) + } +} + +// Generate a cross-link for references to other chapters. +// Leaves the ref untouched if it can't be resolved or points to the current chapter. +#let xref(rf) = { + assert(is-shiroa, message: "xref should only be used when compiling for shiroa") + let lbl = rf.target + let found = meta_sections.find(((_, _, tag)) => str(lbl).starts-with(str(tag))) + context if found != none and found.at(0) != _toplevel.final() { + let (ch, title, ref) = found + if ref == lbl { + cross-link("/" + ch, [Chapter #(meta_sections.position(x => x == found) + 1)]) + } else { + // Because shiroa does weird url escaping + let shiroa-label = label(str(lbl).replace(":", "%3A")) + context _xref-included.update(x => x + ((ch): true)) + // The ideal would be to use `rf` directly as content argument to `cross-link`, + // as that would inherit any/all formatting of the ref we want or need. + // Unfortunately the ref link seems to take precedence over the cross-link hyperlink + // when clicking. + // There may still be some way around it by messing with some html output + let link-content = context { + let fig = query(lbl).first() + let counter = if fig.has("counter") { + fig.counter + } else { + counter(fig.func()) + } + + let supplement = if rf.supplement == auto { + fig.fields().at("supplement", default: none) + } else { + rf.supplement + } + [#supplement#numbering(fig.numbering, ..counter.at(lbl))] + } + cross-link("/" + ch, reference: shiroa-label, link-content) + } + } else { + rf + } +} + +#let book-page(file, ..args) = { + if not file.ends-with(".typ") { + file = lower(file) + ".typ" + } + + assert(meta_sections.find(s => s.at(0) == file) != none, message: "Couldn't resolve typst source file " + file) + + if is-shiroa { + (body) => { + show: common-formatting + context _toplevel.update(s => { + if s == none { + file + } else { + s + } + }) + let cond() = _toplevel.final() == file + project.with(..args, title: context meta_sections.find(x => x.at(0) == _toplevel.final()).at(1), cond: cond)([ + #show ref: it => context if _toplevel.final() == file { + xref(it) + } + #context _xref-included.final().pairs().map(((key, value)) => context if value and cond() { + xref-include(key) + }).join() + #body + ]) + } + } else { + body => body + } +} diff --git a/spec/branch.typ b/spec/branch.typ new file mode 100644 index 000000000..0743ae2f9 --- /dev/null +++ b/spec/branch.typ @@ -0,0 +1,48 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + compute_nr_interactions, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_padding_table, +) + +#let config = load_config() +#let chip = load_chip("src/branch.toml", config) + +#show: book-page(chip.name) +#let branch = raw(chip.name) + +The #branch chip computes the target address of a branching instruction. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #branch chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions + +#render_chip_assumptions(chip, config) + += Constraints + +We constrain `next_pc` to be $#`base_address` + #`offset`$, +where `base_address` equals `pc` when $#`JALR` = 0$ and `register` otherwise. + +The range checks on `unmasked_low_byte` and `next_pc_low[0]` are performed implicitly by the `AND_BYTE` lookup. +#render_constraint_table(chip, config, groups: "all") + +This chip contributes the following to the lookup argument. +#render_constraint_table(chip, config, groups: "output") + += Padding + +The table can be padded to the next power of two with the following value assignments: + +#render_chip_padding_table(chip, config) diff --git a/spec/chip.typ b/spec/chip.typ new file mode 100644 index 000000000..f3e0892f7 --- /dev/null +++ b/spec/chip.typ @@ -0,0 +1,393 @@ +#import "expr.typ": expr_to_code, expr_to_math, type_to_code + +/// Computes the total number of variables in a `chip` +#let total_nr_variables(chip) = { + return chip.variables.values().flatten().len() +} + +// Computes the total number of columns instantiated by `chip` +#let total_nr_instantiated_columns(chip, config) = { + return chip + .variables + .pairs() + .filter(pair => pair.at(0) in config.variables.categories.instantiated) + .map(pair => pair.at(1)) + .flatten() + .map(var => { + let (label, factor) = if type(var.type) == array { + (var.type.at(0), var.type.at(1)) + } else { + (var.type, 1) + } + config.variables.types.filter(type => type.label == label).first().subtypes.len() * factor + }) + .sum() +} + +// Given a constraint, compute the number of interactions it induces +#let get_constraint_interaction_count(constraint) = { + let iters = if "iters" in constraint { + constraint.iters + } else if "iter" in constraint { + (constraint.iter,) + } else { + () + } + + iters.map(i => { + assert( + i.len() == 3 and type(i.at(1)) == int and type(i.at(2)) == int, + message: "invalid iter: " + repr(i), + ) + i.at(2) - i.at(1) + 1 + }) + .product(default: 1) +} + +// Compute the number of interactions performed by `chip` and +// store it as metadata under the `` label +// with tag `chip.name`. This tag is overwritten by `name` when specified. +#let set_nr_interactions(chip, name: none) = { + if name == none { + name = chip.name + } + + let constraints = chip + .constraints + .values() + .flatten() + + // nr. of direct interactions + let nr-direct-interactions = constraints + .filter(c => c.kind == "interaction") + .map(get_constraint_interaction_count) + .sum(default: 0) + + let template-constraints = constraints.filter(c => c.kind == "template") + + context { + let lookup-table = query().map(x => x.value).sum(default: (:)) + + // nr. of indirect interactions through templates + let nr-indirect-interactions = template-constraints + .map(c => { + assert(c.tag in lookup-table, message: "cannot find interaction_count for " + repr(c)) + + let template-interactions = lookup-table.at(c.tag) + let iter-size = get_constraint_interaction_count(c) + iter-size * template-interactions + }) + .sum(default: 0) + + let total-nr-interactions = nr-direct-interactions + nr-indirect-interactions + + [#metadata((str(name): total-nr-interactions)) ] + } +} + +#let compute_nr_interactions(chip) = { + set_nr_interactions(chip) + context { + let lut = query().map(c => c.value).sum(default: (:)) + assert(chip.name in lut, message: "no interaction_count specified for " + repr(chip.name)) + lut.at(chip.name) + } +} + +// Return a list of iterators needed by `obj`. Taken from `iters` or `iter`. +// Prepend `name` to every iterator, if given. +#let iters_of(obj, name: none) = { + let clean_iter(it) = { + let arr = if type(it) == array { + it + } else { + (it,) + } + if name != none { + (name,) + arr + } else { + arr + } + } + + (if "iters" in obj { + obj.iters + } else if "iter" in obj { + (obj.iter,) + } else { + () + }).map(clean_iter) +} + +#let render_chip_padding_table(chip, config) = { + // Whether `var` is a preprocessed variable. + let is_preprocessed(var) = { + let type = config.variables.types + .filter(t => t.label == var.type) + type.len() > 0 and type.all(t => t.at("preprocessed", default: false)) + } + + let instantiated_vars = config.variables.categories.instantiated.map(c => chip.variables.at(c, default: ())).flatten() + + show figure: set block(breakable: true) + figure(table( + columns: (auto, auto, auto), + inset: 6pt, + align: (right + top, center + top, left + top), + stroke: none, + table.header([*Column*], [], [*Padding value*]), + table.hline(stroke: stroke(thickness: 2pt)), + ..for var in instantiated_vars { + if not is_preprocessed(var) { + ([#raw(var.name)], [$:=$], [#expr_to_math(var.pad)],) + } + }, + )) +} + +/// Generates a table listing `chip`'s variables. +#let render_chip_variable_table(chip, config) = { + + // Render a definition's iterators + let render_def_iters(iters) = { + (..for (name, ..args) in iters { + if args.len() == 1 { + ([#raw(name) = #expr_to_code(args.at(0))],) + } else if args.len() == 2 { + ([#raw(name) #sym.in `[`#expr_to_code(args.at(0)), #expr_to_code(args.at(1))`]`],) + } else { + assert(false, message: "Invalid def range: " + repr(name, ..args)) + } + }).join("\n") + } + + // Render definition `def` + let render_definition(def, var_name) = { + if type(def) in (array, str) { + return ( + [], + table.cell(align: right, emph[definition]), + table.cell(colspan: 2, expr_to_math(def)) + ) + } + + assert(type(def) == dictionary, message: "invalid definition: " + repr(def)) + + let idx = def.at("idx", default: none) + let gather_indices(obj) = iters_of(obj, name: idx).map(it => it.first()) + let index_all(expr, indices) = { + for index in indices { + expr = ("idx", expr, index) + } + expr + } + + if "poly" in def { + ( + [], + table.cell(align: right, emph[definition]), + expr_to_math((":=", index_all(var_name, gather_indices(def)), def.poly)), + render_def_iters(iters_of(def, name: idx)) + ) + } else if "polys" in def { + assert( + def.polys.map(gather_indices).dedup().len() == 1, + message: "Can only do multiple polys if they're indexed identically" + ) + ( + [], + table.cell(align: right, emph[definition]), + table.cell(colspan: 2, expr_to_math(index_all(var_name, gather_indices(def.polys.first())))) + ) + for (i, poly) in def.polys.enumerate() { + ( + [], + [], + table.cell(inset: (left: 1.5em), expr_to_math((":=", "", poly.poly))), + render_def_iters(iters_of(poly, name: idx)), + ) + } + } else { + assert(false, message: "invalid definition: " + repr(def)) + } + } + + // Group variables by category + show figure: set block(breakable: true) + figure(table( + columns: (auto, auto, 1fr, auto), + inset: 6pt, + align: left + top, + stroke: none, + table.header([*Label*], [*Type*], table.cell(colspan: 2, [*Description*])), + table.hline(stroke: stroke(thickness: 2pt)), + ..for (cat, vars) in chip.variables.pairs() { + (table.header(level:2, table.cell(colspan: 4, emph(cat))), table.hline(stroke: .6pt)) + for var in vars { + ( + [#raw(var.name)], + [#type_to_code(var.type)], + table.cell(colspan: 2, [#eval(var.desc, mode: "markup")]) + ) + if "def" in var { + render_definition(var.def, var.name) + } + } + (table.cell(colspan: 4, []), ) + }, + )) +} + +#let cref(obj, body) = { + if "ref" in obj { + [#body#label(obj.ref)] + } else { + body + } +} + +// 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") +} + +#let args_interaction_like(input, output) = { + if output != none { + expr_to_code(output) + `; ` + } else { + `` + } + input.map(expr_to_code).join(`, `) +} + +#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: [], [])] + } + + show figure: set block(breakable: true) + figure(table( + columns: (auto, auto, 1fr), + inset: 6pt, + align: (top + left, top + left, top + left), + stroke: none, + table.header([*Tag*], [*Range*], [*Description*]), + table.hline(stroke: stroke(thickness: 2pt)), + ..for assumption in chip.assumptions { + ([#tag(assumption)], [#iters(assumption)], [#eval(assumption.desc, mode: "markup")]) + }, + )) +} + +/// Generates a table listing all interactions initiated by `chip`'s. +#let render_constraint_table(chip, config, groups: none) = { + let all_groups = chip.constraint_groups.map(group => group.name); + if groups == none { + // render all + groups = all_groups + } else if type(groups) == str { + groups = (groups,) + } + assert(groups.all(group => group in all_groups), message: "unknown group") + let selected_constraints = groups.map(g => ((g): chip.constraints.at(g))).join() + + // Find the group definition in the constraint_groups + let lookup_group(name) = chip.constraint_groups.filter((g) => g.name == name).at(0, default: (name: name)) + + /// Render the contraint's tag. + let tag(constraint, group) = { + let with_index(x) = ((x,) + iters_of(constraint).map(it => 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: [], [])] + } + + /// Generates a representation of `constraint` + let repr_constraint(constraint) = { + let kind = constraint.kind + + if kind == "interaction" { + raw(constraint.tag) + `[` + args_interaction_like(constraint.input, constraint.at("output", default: none)) + `]` + } else if kind == "arith" { + [#eval(constraint.constraint, mode: "markup")] + } else if kind == "template" { + let cond = if "cond" in constraint { + $#expr_to_math(constraint.cond) arrow.r.double$ + " " + } + cond + raw(constraint.tag) + `<` + args_interaction_like(constraint.input, constraint.at("output", default: none)) + `>` + } else { + assert(false, message: "illegal constraint format: " + kind) + } + } + + // Whether constraint has polynomial constraints + let has_polynomial_constraints(constraint) = { + constraint.kind == "arith" and ("poly" in constraint or "polys" in constraint) + } + + // Whether constraint has a "desc" field we need to render separately + let has_extra_description(constraint) = { + "desc" in constraint + } + + // Rendering polynomial constraints + let render_polynomial_constraints(constraint) = { + assert(constraint.kind == "arith", message: "Only arith needs extra rows") + let polys = if "poly" in constraint { + (constraint.poly,) + } else { + constraint.polys + } + + (..for poly in polys { + (table.cell(align: right, colspan: 2, [_polynomial constraint_]), $#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"), []) + } + + // Whether there is at least one constraint with a range + // This can be used to see whether the "Range" label should be displayed + let do_display_range = selected_constraints.values().flatten().any(x => iters_of(x).len() > 0) + + // Whether there is at least one constraint with a multiplicity + // This can be used to see whether the "Multiplicity" label should be displayed + let do_display_multiplicity = selected_constraints.values().flatten().any(x => "multiplicity" in x) + + show figure: set block(breakable: true) + figure(table( + columns: (auto, auto, 1fr, auto), + inset: 6pt, + align: (top + left, top + left, top + left, top + center), + stroke: none, + table.header( + [*Tag*], + if do_display_range {[*Range*]} else {[]}, + [*Description*], + if do_display_multiplicity {[*Multiplicity*]} else {[]}, + ), + table.hline(stroke: stroke(thickness: 2pt)), + ..for (group, group_constraints) in selected_constraints.pairs() { + for constraint in group_constraints { + ( + [#tag(constraint, lookup_group(group))], + [#iters(constraint)], + [#repr_constraint(constraint)], + [#expr_to_math(constraint.at("multiplicity", default: ""))], + ) + if has_extra_description(constraint) { + render_extra_description(constraint) + } + if has_polynomial_constraints(constraint) { + render_polynomial_constraints(constraint) + } + } + } + )) +} diff --git a/spec/commit.typ b/spec/commit.typ new file mode 100644 index 000000000..a6fea1b6c --- /dev/null +++ b/spec/commit.typ @@ -0,0 +1,111 @@ +#import "/book.typ": book-page, aside +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_assumptions, + render_chip_padding_table, +) + +#show: book-page("commit.typ") + +#let config = load_config() +#let chip = load_chip("src/commit.toml", config) +#let commit = raw(chip.name) + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #commit chip leverages #nr_variables variables, spanning #nr_columns columns and leverages #nr_interactions interactions: +#render_chip_variable_table(chip, config) + += Constraints +In this VM, committing is considered equivalent to writing a value to `stdout`. +Hence, this chip responds to `ECALL`s with system call number 64. +#footnote([RISC-V GNU-toolchain, `unistd.h`; version 2026-01-23, #link("https://github.com/riscv-collab/riscv-gnu-toolchain/blob/2026.01.23/linux-headers/include/asm-generic/unistd.h#L174")[[src]]]) +Since we do not know how many bytes are to be committed, this chip employs a recursive design: +each iteration commits one byte, and recursively "calls" itself to commit the remaining bytes. +As such, only the call from the CPU to this chip (i.e., the `first` in the recursion tree) should accept the `ECALL`; later recursive calls should not. +This is why @commit:c:receive_ecall has multiplicity $-#`first`$. +#render_constraint_table(chip, config, groups: "incoming") + +The `write` operation --- writing to a file descriptor --- has the following signature: +#footnote([Linux man-page on `write`; man7.org, version 6.16, 2025-10-29. #link("https://man7.org/linux/man-pages/man2/write.2.html")[[src]]]) + +```c +ssize_t write(size_t count; int fd, const void buf[count], size_t count); +``` + +That is to say, +- `A0` contains the file descriptor, +- `A1` contains the address of `buf`'s first byte, +- `A2` contains `count`, and +- the written count should be written to `A0`. + +@commit:c:read_address reads `address` from `x11` (=`A1`) and @commit:c:read_count reads `count` from `x12` (=`A2`). +Since we only support writing to `stdout` (which corresponds to $#`fd` = 1$ +#footnote([The Open Group Standard for Information Technology --- Portable Operating System Interface (POSIX) Base Specifications, `unistd.h`; The Open Group, issue 8, #link("https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/unistd.h.html")[[src]]])) +we assert that `x10` contains $1$ in @commit:c:read_fd_write_count. +Note that this constraint _also_ writes `count` to `A0`; +in this VM it is impossible for a commit to be interrupted or fail. +Lastly, the `index` is read from `x254`#footnote([In this VM, register 254 is reserved for containing the commitment index.]); in the same operation, $#`index` + #`count`$ is written back to this location by @commit:c:read_index. +This, too, leverages the fact that a commit will not be interrupted or fail to update the `index` for the next commit sequence. +Again, each of these memory interactions only take place when this is the `first` call in the recursion tree. + +#render_constraint_table(chip, config, groups: "read_input") + +*Note*: the observant reader will notice that @commit:c:read_index casts `count` to a `BaseField`, potentiallly losing information. +This is indeed correct. +However, since it is practically impossible to commit more than $2^64-2^32$ bytes in a single VM execution, it was decided to permit this. + +Next, we read the `value` located at buffer address `address` and commit to it under the given `index`. +This is only performed when we have not yet reached the `end` of the commit sequence. +#render_constraint_table(chip, config, groups: "commit") + +In parallel, we compute $#`address_incr` = #`address` + 1$ (@commit:c:address_incr) as address of the next byte to commit, and $#`count_decr` = #`count` - 1$ (@commit:c:count_decr) as the number of bytes that still has to be committed after committing this byte. +@commit:c:range_address_incr and @commit:c:range_count_decr are included to satisfy @add:a:sum respectively @add:a:rhs. +#render_constraint_table(chip, config, groups: "incr_decr") + +When `count` hits $0$, we should stop performing further recursive calls. +We use the `end` bit to indicate these circumstances. + +#render_constraint_table(chip, config, groups: "end") + +*Note*: ++ Rather than setting $#`end` = 1$ when $#`count` = 0$, we do so when $#`count_decr` = -1$. + This technique allows `count` to be stored in a `DWordWL` rather than a `DWordHL`, saving two columns. ++ $forall i in [0, 3]: 65535 - #`count_decr`_i >= 0$ as a result of @commit:c:range_count_decr. + Hence, + $ + sum_(i=0)^3 65535 - #`count_decr`_i = 0 arrow.l.r.double.long forall i in [0, 3]: #`count_decr`_i = 65535 + $ + +When this was not the `end` byte to commit in this recursion sequence, we recursively _Commit the Next Byte_ (`CNB`), specifying the timestamp, address to continue reading and the number of bytes that should still be committed (@commit:c:send_commit_next_byte). +Since that certainly won't be the `first` call in the sequence, we read `address_incr` and `count_decr` from the previous recursion level into `address` and `count` and continue executing the commit. +#render_constraint_table(chip, config, groups: "lookups") + +Lastly, we must make sure `first`, `end` and `μ` are bits (@commit:c:range_first, @commit:c:range_end, @commit:c:range_mu), and that when either $#`first` = 1$ or $#`end` = 1$ imply that $#`μ` = 1$ (@commit:c:first_or_end_implies_mu). +These are required to ensure the multiplicities $-(#`μ` - #`first`)$ and $#`μ` - #`end`$ are binary. +#render_constraint_table(chip, config, groups: "bits") + += Padding +To pad this chip, use the below data. +#render_chip_padding_table(chip, config) + += Notes/optimizations +- The current version only supports writing to `stdout`. + This chip could potentially be extended to support writing to arbitrary `fd`s +- One might be able to replace @commit:c:end by `end => count = 0`. + While loosening the constraint (`count = 0 => end` is no longer enforced), this should not cause any problems: + if the prover does not set `end` when `count=0`, they simply cannot complete the proof. + First of all, one would have to recursively work through all $2^64$ values of `count`, something that is practically infeasible. + Moreover, if this is done with a sequence that originally has $#`count` > 0$, one will inevitably have to read a memory address twice at the same timestamp, which is impossible to prove. + In addition to dropping the `ZERO` lookup, this optimization might also permit moving `count_decr` from a `DWordHL` to a `DWordWL`, saving two columns. +- Given that it is practically infeasible to commit more than $#`p`-1 = 2^64-2^32$ bytes in a program, it might suffice to store `count_decr` in a `BaseField`. + Note that this would probably involve having an extra (virtual) column storing `count` in `BaseField` form as well. + Moreover, one might need to add a lookup to `LT` to ensure $#`count` <= #`p`-1$ when being read from memory at the beginning of each commitment sequence. diff --git a/spec/cpu.typ b/spec/cpu.typ new file mode 100644 index 000000000..2fbd60d59 --- /dev/null +++ b/spec/cpu.typ @@ -0,0 +1,97 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_padding_table, +) + +#let config = load_config() +#let chip = load_chip("src/cpu.toml", config) + +#show: book-page(chip.name) +#let cpu = raw(chip.name) + +The #cpu chip coordinates memory accesses and dispatches to other chips for arithmetic and logical operations. +It bases its decisions on the entry of the `DECODE` table (@decode) corresponding the the current program counter (PC). + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #cpu chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Constraints +First, we perform a decoding lookup for the current PC. + +#render_constraint_table(chip, config, groups: "decode") + +== Range checks + +We constrain all columns to have the appropriate ranges. +The flags and register indices looked up from the decoding need to be checked, +as they are communicated through the interaction in a packed form. +In contrast, we know ahead of time that decoding will ensure proper range checks for `pc` and `imm`. +Similarly, since `next_pc` will propagate through the memory argument and be looked up +in the instruction decoding on the next cycle, it is forced to be in the correct range.#rj[is this true, do we need this elsewhere for chip assumptions?] +For the auxiliary columns, we need to check the limbs of `arg1`, `arg2`, and `res`. +The ranges of the other auxiliary columns are enforced through later constraints. +#rj[Make sure we argue for every column here] +#rj[is `rvd` still sufficiently constrained? (can also be done through the memory argument like `pc`?)] + +#render_constraint_table(chip, config, groups: "range") + +== ALU + +The ALU functionality is then obtained through judicious dispatching to the corresponding chips. + +#render_constraint_table(chip, config, groups: "alu") + +== Memory + +The interactions with the memory, both for register loading and storing, as for `LOAD` and `STORE` instructions are handled. +Note that since registers need no byte-addressing, we store them in the memory argument with `Word` limbs. +The timestamps are ensured to be disjoint for disjoint memory locations. +One consequence of that is that `next_pc` is written at `timestamp + 1` +to ensure the access is disjoint with the `pc` read into `rv1` as part of the `AUIPC` instruction. + +#render_constraint_table(chip, config, groups: "mem") + +== System + +The interactions with the wider system. + +#render_constraint_table(chip, config, groups: "sys") + +== Input and output to the ALU + +We constrain `arg1`, `arg2` and `rvd` to correspond to the wanted values, +including the appropriate sign/zero extension, depending on `word_instr`. + +#render_constraint_table(chip, config, groups: "ext") + +== Other constraints +For @cpu:c:is_equal, note that @cpu:c:sub sets `res` to be the difference between `arg1` and `arg2` whenever `BEQ` is $1$. +Given that this difference is $0$ when both are equal, @cpu:c:is_equal ensures `is_equal` is set to $1$ if and only if $#`arg1` = #`arg2`$ and `BEQ` is set. + +#render_constraint_table(chip, config, groups: "misc") + +#rj[Document the choice to not have a multiplicity column here for padding] + += Padding + +The CPU can be padded with the following values, which have a corresponding row +in the DECODE table, at the _odd_ address 1, only reachable through a HALT ecall. + +#render_chip_padding_table(chip, config) + +This approach minimizes the number of dependent lookups, increasing only multiplicities in the DECODE table and the IS_BYTE lookup. diff --git a/spec/decode.typ b/spec/decode.typ new file mode 100644 index 000000000..bb5d0d5a1 --- /dev/null +++ b/spec/decode.typ @@ -0,0 +1,221 @@ +#import "/book.typ": book-page, rj, xref +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_padding_table, +) + +#let config = load_config() +#let chip = load_chip("src/decode.toml", config) +#show: book-page(chip.name) + +#let decode = raw(chip.name) + +All `RV64IMC` instruction are to be decoded to a format that can be interpreted by the VM. +This section outlines the decoding table being used in the VM. +For reasons of efficiency, data in this table is significantly compressed. +Since reasoning about this compressed form is needlessly complex, the `decode (uncompressed)` section presents the same table in uncompressed form, and explains how to decode `RV64IM` assembly instructions to it. +Instructions on how to compress the uncompressed table to form the compressed decode table, can be derived from the `packed_decode` variable provided below. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The #decode table is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_variable_table(chip, config) + += Padding +The #decode table must be padded to a length that is a power of two. +Empty rows with the following content can be added to achieve this: + +#render_chip_padding_table(chip, config) + +Note that this row sets the `EBREAK` flag. +Given that `CPU` asserts that `EBREAK = 0` (see @cpu:c:ebreak_traps), using this "padding-instruction" would immediately make the CPU table unprovable. +Note moreover that the `pc` is set to $7$. +This value is the _smallest odd number_ (i.e., not reachable during regular execution) that is more than _$4$_ (i.e., the max `pc`-increment) greater than _$1$_ (i.e., the `pc`-value used in the #link()[additional instruction] referred to by `CPU`-padding lines). + += Decoding +For the purposes of explaining decoding, we decompress #decode's `packed_decode` variable into its constituent variables. +Note that the below table is _not_ used in practice: it is solely used for the purposes of this explanation. + +#let config = load_config() +#let uncompressed_chip = load_chip("src/decode_uncompressed.toml", config) + +#render_chip_variable_table(uncompressed_chip, config) + +We will illustrate how each instruction should be expressed in this (uncompressed) decoding table. +The columns of the accompanying table represent the following: +- *`operation`*: the assembly operation being encoded. +- *`op-flag`*: which of the "`ALU` selector flags" operation flags to set. Each operation sets exactly one. +- *`w_instr`*, *`signed`*: whether to set the `word_instr` and `signed` flags, respectively. +- *other*: the other flags that should be set or variables that should be given specific values. + +For the purpose of brevity and readability, the table uses the following rules-of-thumb: ++ `rd`, `rs1`, `rs2`, and `imm` are mapped to the values provided by the instruction; + when a value is not specified by an instruction it defaults to $0$. ++ `read_register1`, `read_register2` and `write_register` are set to $1$ when respectively $#`rs1` != 0$, $#`rs2` != 0$, or $#`rd` != 0$. ++ Any flag that is not listed is set to $0$, with the exception of the `c_type` flag. + *The `c_type` flag is set independently of the below table*, as explained next. + +Further clarification is provided in the notes following the table. + +== C-type instructions +The `RV64C` extension for compressed instructions specifies that \~50% of all instructions can be represented using a 16-bit instruction (rather than 32-bits), saving \~25% in code size. +This execution of assembly code is _not_ agnostic to an instruction's compression state; after executing a compressed instruction, the `pc` should be incremented by $2$ rather than $4$. +To indicate an instruction is provided in compressed form, the `c_type` flag is introduced. +*This flag should be set to $1$ whenever the decoded instruction is provided in compressed form and $0$ otherwise.* + +/// Add a reference to one or more notes following this table. +#let ref_note(..refs) = { + super("[" + refs.pos().map(r => ref(r)).join(",") + "]") +} + +#let decoding_table(lines) = { + show figure: set block(breakable: true) + + figure(table( + columns: (auto, auto, auto, auto, 1fr, auto), + stroke: 0pt, + inset: (right: .5em), + align: (left, right, center, center, left, right), + fill: (_, y) => + // Overlay a low-opacity fill color to distinguish the different rows better + if calc.odd(y) and y <= lines.len() { color.rgb(0, 0, 100, 20) } + else { color.rgb(255, 255, 255, 20) }, + table.header([*Operation*], [*op-flag*], [*`w_instr`*], [*`signed`*], [*other*], []), + table.hline(stroke: 1.5pt), + table.vline(x: 1, start: 1, end: lines.len() + 1, stroke: .5pt), + ..lines.flatten(), + table.hline(stroke: 1.5pt), + table.footer([*Operation*], [*op-flag*], [*`w_instr`*], [*`signed`*], [*other*]), + )) +} + +#let decoding = ( + // OP-IMM + ([`ADDI[W] rd, rs1, imm`], [`ADD`], [`[W]`], [], [], [#ref_note()]), + ([`SLTI[U] rd, rs1, imm`], [`SLT`], [], [#sym.not`[U]`], [], [#ref_note()]), + ([`ANDI rd, rs1, imm`], [`AND`], [], [], [], []), + ([`ORI rd, rs1, imm`], [`OR`], [], [], [], []), + ([`XORI rd, rs1, imm`], [`XOR`], [], [], [], []), + ([`SLLI[W] rd, rs1, imm`], [`SHIFT`], [`[W]`], [], [], []), + ([`SRLI[W] rd, rs1, imm`], [`SHIFT`], [`[W]`], [], [`mp_selector`], [#ref_note()]), + ([`SRAI[W] rd, rs1, imm`], [`SHIFT`], [`[W]`], [1], [`mp_selector`], [#ref_note()]), + // OP + ([`ADD[W] rd, rs1, rs2`], [`ADD`], [`[W]`], [], [], [#ref_note()]), + ([`SUB[W] rd, rs1, rs2`], [`SUB`], [`[W]`], [], [], [#ref_note()]), + ([`SLT[U] rd, rs1, rs2`], [`SLT`], [], [#sym.not`[U]`], [], [#ref_note()]), + ([`AND rd, rs1, rs2`], [`AND`], [], [], [], []), + ([`OR rd, rs1, rs2`], [`OR`], [], [], [], []), + ([`XOR rd, rs1, rs2`], [`XOR`], [], [], [], []), + ([`SLL[W] rd, rs1, rs2`], [`SHIFT`], [`[W]`], [], [], [#ref_note()]), + ([`SRL[W] rd, rs1, rs2`], [`SHIFT`], [`[W]`], [], [`mp_selector`], [#ref_note()]), + ([`SRA[W] rd, rs1, rs2`], [`SHIFT`], [`[W]`], [1], [`mp_selector`], [#ref_note()]), + // OP - M + ([`MUL[W] rd, rs1, rs2`], [`MUL`], [`[W]`], [1], [`mp_selector`], [#ref_note()]), + ([`MULH rd, rs1, rs2`], [`MUL`], [], [1], [`mp_selector`, `muldiv_selector`], []), + ([`MULHU rd, rs1, rs2`], [`MUL`], [], [], [`muldiv_selector`], []), + ([`MULHSU rd, rs1, rs2`], [`MUL`], [], [1], [`muldiv_selector`], []), + ([`DIV[U][W] rd, rs1, rs2`], [`DIVREM`], [`[W]`], [#sym.not`[U]`], [], [#ref_note(, )]), + ([`REM[U][W] rd, rs1, rs2`], [`DIVREM`], [`[W]`], [#sym.not`[U]`], [`muldiv_selector`], [#ref_note(, )]), + // LUI/AUIPC + ([`LUI rd, imm`], [`ADD`], [], [], [], [#ref_note()]), + ([`AUIPC rd, imm`], [`ADD`], [], [], [`rs1 := x255`], [#ref_note()]), + ([`JAL rd, imm`], [`JALR`], [], [], [`rs1 := x255`], [#ref_note()]), + // Branching + ([`JALR rd, rs1, imm`], [`JALR`], [], [], [], []), + ([`BEQ rs1, rs2, imm`], [`BEQ`], [], [], [], []), + ([`BNE rs1, rs2, imm`], [`BEQ`], [], [], [`mp_selector`], []), + ([`BLT[U] rs1, rs2, imm`], [`BLT`], [], [#sym.not`[U]`], [], [#ref_note()]), + ([`BGE[U] rs1, rs2, imm`], [`BLT`], [], [#sym.not`[U]`], [`mp_selector`], [#ref_note()]), + // LOAD + ([`LD rd, rs1, imm`], [`LOAD`], [], [], [`mem_8B`], []), + ([`LW[U] rd, rs1, imm`], [`LOAD`], [], [#sym.not`[U]`], [`mem_4B`], [#ref_note()]), + ([`LH[U] rd, rs1, imm`], [`LOAD`], [], [#sym.not`[U]`], [`mem_2B`], [#ref_note()]), + ([`LB[U] rd, rs1, imm`], [`LOAD`], [], [#sym.not`[U]`], [], [#ref_note()]), + // STORE + ([`SD rs1, rs2, imm`], [`STORE`], [], [], [`mem_8B`], []), + ([`SW rs1, rs2, imm`], [`STORE`], [], [], [`mem_4B`], []), + ([`SH rs1, rs2, imm`], [`STORE`], [], [], [`mem_2B`], []), + ([`SB rs1, rs2, imm`], [`STORE`], [], [], [], []), + // ECALL/EBREAK + ([`ECALL`], [`ECALL`], [], [], [$#`rs1` := #`x17`$], [#ref_note()]), + ([`EBREAK`], [`EBREAK`], [], [], [], []), + // FENCE + ([`FENCE`], [`ADD`], [], [], [], [#ref_note()]), +) + +#decoding_table(decoding) + +// Construct a note that can be referenced through `lbl` +#let referenceable_note(lbl, note) = { + show figure: (it) => align(left, [#it]) + [#figure(kind: "note", supplement: [], [#note]) #label(lbl)] +} + +== Notes +We note the following about the above decoding table: +#enum(numbering: "[1]", + enum.item( + referenceable_note( + "note_word_instr", + [`word_instr`: `[W]` indicates that $#`word_instr` = 1$ for the `W`-variant of the operation, and $0$ for the non-`W`-variant.] + ) + ), + enum.item( + referenceable_note( + "note_signed", + [`signed`: #sym.not`[U]` indicates that $#`signed` = 1$ for the *non-`U`*-variant of the operation, and $0$ for the `U`-variant.] + ) + ), + enum.item( + referenceable_note( + "note-lui", + [`LUI`: this operation loads the 20-bit `imm` in the upper bits of `rd`. + Observe that this can be represented using `ADDI rd, x0, imm`. + As such, *we expect the decoding to take care of writing the immediate in bit range $[12:32]$ of `imm` and extending it to 64 bits.*] + ) + ), + enum.item( + referenceable_note( + "note-auipc", + [`AUIPC`: this operation adds the 20-bit immediate to the upper bits of `pc` and stores the result in `rd`. + Given that the `pc` is stored in `x255`, this operation can be represented using `ADDI rd, x255, imm`. + As such, *we expect the decoding to take care of writing the immediate in bit range $[12:32]$ of `imm` and extending it to 64 bits.*] + ) + ), + enum.item( + referenceable_note( + "note-jal", + [`JAL`: this operation stores $#`pc` + 4$ in `rd` and adds two times the sign-extended 20-bit immediate to the `pc`. + Note that this can be represented using `JALR rd, x255, imm`. + As such, *we expect the decoding to take care of writing the immediate in bit range $[1:21]$ of `imm` and extending it to 64 bits; the least significant bit should always be 0.*] + ) + ), + enum.item( + referenceable_note( + "note-ecall", + [`ECALL`: + "On RISC-V a system call has its own instruction: `ECALL`. [...] A7 [= register `x17`] contains the system call number." #link("https://libriscv.no/docs/concepts/syscalls/#the-risc-v-system-call-abi")[[source]] + ] + ) + ), + enum.item( + referenceable_note( + "note-fence", + [`FENCE`: currently, the VM interprets this operation as `ADDI x0 x0 0`; a no-op.] + ) + ) +) + +== One more instruction +In addition to decoding all instructions provided in the ELF and adding a corresponding entry to the #decode table, one must include an entry that has $#`pc` = 1$ and every other variable set to $0$. +Note that this will never conflict with any entry in the ELF, since it has an odd `pc` value. + +This entry is used to pad the `CPU` table. +More details on this matter are provided in the `CPU` chip. diff --git a/spec/dvrm.typ b/spec/dvrm.typ new file mode 100644 index 000000000..1118aa10a --- /dev/null +++ b/spec/dvrm.typ @@ -0,0 +1,146 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_padding_table, + render_chip_assumptions +) + +#let config = load_config() +#let chip = load_chip("src/dvrm.toml", config) + +#show: book-page(chip.name) + +#let dvrm = raw(chip.name) + +The #dvrm chip provides division and remainder functionality, both signed and unsigned. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #dvrm chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Constraints +From the ISA, we gather five requirements for the `DIV[U][W]` and `REM[U][W]` instructions: +#enum(numbering: "R1.", + enum.item([ + _For both signed and unsigned division, except in the case of_ overflow, _it holds that $#`n` = #`q` #`d` + #`r`$._ + ]), + enum.item([ + _`DIV` and `DIVU` perform [...] signed and unsigned integer division [...] rounding towards zero._ + ]), + enum.item([ + _For `REM`, the sign of a nonzero [remainder] equals the sign of the [numerator]._ + ]), + enum.item([ + In case of _division-by-zero_, $#`r` = #`n`$ and $#`q` = 2^64-1$ (unsigned) or $#`q` = -1$ (signed). + ]), + enum.item([ + In case of _overflow_, $#`q` = #`n`$ and $#`r` = 0$ + ]), +) +where _overflow_ occurs when $#`n` = -2^(63)$ and $#`d` = -1$ (and, hence, $#`signed` = 1$), and _division-by-zero_ indicates that $#`d` = 0$. +In the following, we list the constraints associated with the #dvrm chip, and explain how these together enforce all five of these requirements. + +== R3: Sign remainder equals sign numerator +We start with R3, which is straightforwardly asserted by constraint @dvrm:c:sign_r_equals_sign_n. +#render_constraint_table(chip, config, groups:("sign_equality", )) + +== R2: rounding towards zero +R2 states that "_[in] signed and unsigned integer division [the quotient is] round[ed] towards zero._" +In other words, ++ the sign of $#`n`-#`qd`$ must match that of `n` (unless $#`qd` = #`n`$), and ++ $|#`n`-#`qd`| < |#`d`|$ (unless $#`d` = 0$). + +Leveraging R1 #footnote([Note: we need not worry about the _overflow_ case in applying this relation, since R5 requires specific values for `q` and `r` in this case.]), we can rewrite these as ++ the sign of $#`r`$ must match that of `n` (unless $#`r` = 0$), and ++ $|#`r`| < |#`d`|$ (unless $#`d` = 0$). + +Focusing on the first statement, we observe that this trivially holds when $#`signed` = 0$, +while R3 deals with the case that $#`signed` = 1$. +The second statement is enforced by @dvrm:c:abs_r_lt_abs_d. +@dvrm:c:abs_r_if_negative and @dvrm:c:abs_r_if_nonnegative (resp. @dvrm:c:abs_d_if_negative and @dvrm:c:abs_d_if_nonnegative) are included to ensure that `abs_r` (resp. `abs_d`) is the absolute values of `r` (resp. `d`). + +#render_constraint_table(chip, config, groups:("abs_diff", )) + +== R5: overflow +The ISA requires that $#`q` = #`n`$ and $#`r` = 0$ in the event of overflow (i.e., when $#`n` = -2^63$ and $#`d` = -1$). +We note that the second half of this requirement is already satisfied by R2: since $#`d` = -1 != 0$, R2 requires that $|#`r`| < |#`d`| = 1$, to which $#`r` = 0$ is the only satisfying value. + +We moreover find that R1 can be leveraged to enforce the correct value of `q`. +While $#`n` = #`qd` + #`r`$ (R1) does _not_ hold in the case of overflow, the relation $#`n` = |#`q`|#`d` + #`r`$ _does_. +We moreover note that the 64-bit _signed_ two's complement representation of $-2^63$ is identical to the 64-bit _unsigned_ representation of $|-2^63| = 2^63$. +As such, by interpreting `q` as an unsigned integer when $#`overflow` = 1$, it follows that R1 will enforce $#`q` = #`0x80...00`$. + +In summary, in case of overflow R2 enforces that $#`r` = 0$. +Moreover it suffices to interpret `q` as unsigned integer (@dvrm:c:sign_q); R1 will ensure it contains the correct value. + +#render_constraint_table(chip, config, groups:"overflow") + +We highlight @dvrm:c:overflow. +Recall that the `overflow` flag should be set if and only if (i) $#`signed` = 1$, (ii) $#`n` = #`0x80...00`$, and (iii) $#`d` = #`0xFF...FF`$. +These requirements are equivalent to the state where: +$ + forall i in [0, 3]:&& 65535 - #`d`_i &= 0,\ + forall i in [0, 2]:&& #`n`_i &= 0,\ + && #`n`_3 - 2^15 dot #`sign_n` &= 0,\ + && 1 - #`sign_n` &= 0,\ +$ +where $#`signed` = 1$ follows from the last equality. +The requirement is phrased in this way, because the left-hand sides of the above expressions are $>= 0$ by construction. +Given that the sum of these expressions does not exceed $2^19$ (and thus never wraps in the field), we can now say that the `overflow` bit should be set to $1$ if and only if their sum evaluates to $0$. +The `ZERO` lookup guarantees this to be the case. + +== R1: $#`n` = #`qd` + #`r`$ +Rewriting R1, we find the constraint $not#`overflow` => #`n` - #`r` = #`qd`$. +#footnote([Recall that @dvrm:c:sign_q allows to assert this equality even when `overflow`.]) +Since `n`, `d`, `q` and `r` are all 64-bit integers, we must assert this equality $mod 2^128$, rather than $mod 2^64$. +To this end, we introduce `extended_n_sub_r` and leverage the `MUL` chip to verify that it is equal to $#`qd` mod 2^128$ using constraints @dvrm:c:mul_lower and @dvrm:c:mul_upper; +@dvrm:c:q_range is included to uphold assumption @mul:a:rhs. + +#render_constraint_table(chip, config, groups:("equality", )) + +It now remains to enforce that `extended_n_sub_r` is the _signed_ 128-bit representation of $#`n`-#`r`$. +Here, we introduce `extended_n` and `extended_r`. +By their definition, these variables contain the signed 128-bit representations of `n` and `r`. +The `carry` variable has been defined such that it mimics those in the `ADD` chip, +except that here we add two `QuadHL`s rather than two `DWordHL`, thus needing four carry bits instead of two. +With this in place, @dvrm:c:n_sub_r (mimicking @add:c:carry) ensures `extended_n_sub_r` must contain the correct value. + +Lastly, observe that $#`n` - #`r` in (-2^64, 2^64)$, _regardless_ of the value of `signed`. +Moreover, note that the upper halves of the 128-bit representations of all values in this range are either `0xFFFFFFFF` (negative) or `0x00000000` (non-negative). +This means that we do not need to store all 128 bits of `extended_n_sub_r`. +Rather, we need only store the lower 64-bits, and a separate bit (`sign_n_sub_r`) indicating whether the top limbs are all-ones or all-zeroes. +The prover is free to select the value for `sign_n_sub_r`; only one of the two will fit the proof. + +#render_constraint_table(chip, config, groups:("n_sub_r", )) + +== R4: division-by-zero +R4 requires that $#`q` = 2^64-1$ (unsigned) or $-1$ (signed) and $#`r` = n$ when $#`d` = 0$. +Recalling R1, we see that $#`n` = #`q` #`d` + #`r` = #`r`$ when $#`d` = 0$, already enforces the latter. +Next, we note that, in two's complement, the _unsigned_ value $2^64-1$ and _signed_ value $-1$ are both represented by the bit string `0xFFFFFFFF`. +Hence, only @dvrm:c:q_if_div_by_zero is required to completely constrain R4; @dvrm:c:div_by_zero just ensures the `div_by_zero` flag is set when $#`d` = 0$. + +#render_constraint_table(chip, config, groups:("div_by_zero", )) + +== Other +The following constraints are included to enforce the values of `sign_n`, `sign_r` and `sign_d` are correct. +#render_constraint_table(chip, config, groups:("defs", )) + +== Output +Lastly, this chip contributes the following to the lookup: +#render_constraint_table(chip, config, groups:("output", )) + += Padding +To pad the #dvrm table, we use the following data, representing the unsigned division $frac(0, 0, style: "horizontal")$: +#render_chip_padding_table(chip, config) diff --git a/spec/ebook.typ b/spec/ebook.typ new file mode 100644 index 000000000..1bd691b9f --- /dev/null +++ b/spec/ebook.typ @@ -0,0 +1,44 @@ +#import "/book.typ": meta, common-formatting + +#set document(author: meta.authors, title: meta.title) + +#align(center, title(meta.title)) +#align(center, text(style: "italic", fill: luma(40%))[Version #meta.version]) +#align(center, meta.authors.join(", ")) +#pagebreak(weak: true) + +// outline +#show outline.entry.where(level: 1): set outline.entry(fill: line(length: 100%, stroke: stroke(dash: "solid"))) +#show outline.entry.where(level: 1): it => { + v(15pt, weak: true) + strong(it) + v(5pt, weak: true) +} +#show outline.entry.where(level: 2): it => { + v(10pt, weak: true) + it +} +#outline(depth: 3) + +// chapter pages +#show heading.where(level: 1): it => align(center + horizon)[#underline(it, offset: 10pt, extent: 5pt)] + +#show: common-formatting +#show heading: set heading(numbering: (..args) => { + let args = args.pos() + let skip_first = args.slice(calc.min(args.len(), 1)) + numbering("1.1", ..skip_first) +}) +#show raw.where(block: true): set block(fill: luma(230)) + +#for (ch_title, sections) in meta.summary { + pagebreak(weak: true) + heading(supplement: [Chapter], level: 1, ch_title, numbering: none) + + for (sec, sec_title, ref) in sections { + pagebreak(weak: true) + [#heading(level: 2, supplement: [Section], sec_title)#ref] + set heading(offset: 2) + include sec + } +} diff --git a/spec/expr.typ b/spec/expr.typ new file mode 100644 index 000000000..2de0d6ba3 --- /dev/null +++ b/spec/expr.typ @@ -0,0 +1,203 @@ +// Types and array types +// ::= str +// | [str, int] + +// Check that a type expression is structurally valid, without validating against a set of known base types +#let check_array_type(typ) = { + assert(type(typ.at(0)) == str, message: "Array types need to have a regular type as base") + assert(type(typ.at(1)) == int, message: "Array types need to have a constant dimension") +} + +// Render a type to code +#let type_to_code(typ) = { + if type(typ) == array { + check_array_type(typ) + return raw(typ.at(0) + "[" + str(typ.at(1)) + "]") + } else if type(typ) == str { + return raw(typ) + } else { + assert(false, message: "Unknown format for type: " + repr(typ)) + } +} + +// Render a type to math +#let type_to_math(typ) = type_to_code(typ) // The code version looks reasonable enough in math too + + +// Expression grammar +// ::= () ; "" +// | var ; str(var) +// | int ; int +// | ["arr", expr, ...] ; [expr, ...] +// | ["idx", expr1, expr2] ; expr1[expr2] +// | ["not", expr] ; !expr +// | ["+", expr1, expr2, ...] ; expr1 + expr2 + ... +// | ["sum", expr1, expr2, expr3] ; Σ_expr1^expr2 expr3 +// | ["*", expr1, expr2, ...] ; expr1 * expr2 * ... +// | ["/", expr1, expr2] ; expr1 / expr2 +// | ["^", expr1, expr2] ; expr1^expr2 +// | ["=", expr1, expr2] ; expr1 = expr2 +// | ["-", expr] ; -expr +// | ["-", expr1, expr2, ...] ; expr1 - expr2 - ... +// | ["cast", expr, type] ; expr as type +// +// +// To limit the number of parentheses that are placed in an expression, +// the formatter passes `pp` (for Parent Precedence) to each recursive subcall, +// and wraps itself in parentheses when `pp < expr.precedence`. + +#let PREC = ( + "MIN": -1, // + "idx": 0, // [] + "pow": 1, // ^ + "neg": 2, // Unary - + "cast": 3, // cast + "mul": 4, // * + "div": 5, // / + "sum": 6, // Σ + "not": 7, // not + "sub": 8, // - + "add": 9, // + + "eq": 10, // = and := + "MAX": 11, // +) + +// Mutual recursion through a trick from https://github.com/typst/typst/issues/744 +#let make_expr_formatter(dict, empty: none, var: raw, num: str) = { + let res(pp, expr) = { + if expr == none { + empty + } else if type(expr) == str { + var(expr) + } else if type(expr) == int { + num(expr) + } else if type(expr) == array { + (dict.at(expr.at(0), default: (pp, rec, e) => { + assert(false, message: "Invalid expression: " + repr(e)) + }))(pp, res, expr) + } + } + res.with(PREC.MAX) +} + +// Wrap code `expr` if `apply = true` +#let cwrap(expr, apply) = { + if apply { + `(` + expr + `)` + } else { + expr + } +} + +// Typeset an expression as code +#let expr_to_code = make_expr_formatter( + ( + "arr": (pp, rec, e) => `[` + e.slice(1).map(rec.with(PREC.MAX)).join(`, `) + `]`, + "idx": (pp, rec, e) => rec(PREC.MIN, e.at(1)) + `[` + rec(PREC.MAX, e.at(2)) + `]`, + "not": (pp, rec, e) => cwrap(rec(PREC.not, 1) + ` - ` + rec(PREC.not, e.at(1)), pp < PREC.not), + "+": (pp, rec, e) => cwrap(e.slice(1).map(rec.with(PREC.add)).join(` + `), pp < PREC.add), + "sum": (pp, rec, e) => assert(false, message: "sum is unsupported in code."), + "*": (pp, rec, e) => { + if e.len() == 3 and type(e.at(1)) == int and type(e.at(2)) == str and e.at(2).len() == 1 { + // multiplication of a constant with one-letter variable. + // Dropping the "dot" + cwrap(e.slice(1).map(rec.with(PREC.mul)).join(``), pp < PREC.mul) + } else { + cwrap(e.slice(1).map(rec.with(PREC.mul)).join(` ` + sym.dot + ` `), pp < PREC.mul) + } + }, + "/": (pp, rec, e) => cwrap(rec(PREC.div, e.at(1)), pp < PREC.div) + ` / ` + rec(PREC.div, e.at(2)), + "^": (pp, rec, e) => { + assert(type(e.at(1)) == int and type(e.at(2)) == int, message: "Can only exponentiate constants") + // technically wrong associativity, but it's a constant + rec(PREC.pow, e.at(1)) + `^` + rec(PREC.pow, e.at(2)) + }, + "=": (pp, rec, e) => rec(PREC.eq, e.at(1)) + ` = ` + rec(PREC.eq, e.at(2)), + ":=": (pp, rec, e) => rec(PREC.eq, e.at(1)) + ` := ` + rec(PREC.eq, e.at(2)), + "-": (pp, rec, e) => { + if e.len() == 2 { + // Negation + cwrap(`-` + rec(PREC.neg, e.at(1)), pp < PREC.neg) + } else { + // Subtraction + cwrap(e.slice(1).map(rec.with(PREC.sub)).join(` - `), pp < PREC.sub) + } + }, + "cast": (pp, rec, e) => { + assert(e.len() == 3, message: "Invalid type cast: " + repr(e)) + cwrap(rec(PREC.cast, e.at(1)) + ` as ` + type_to_code(e.at(2)), pp < PREC.cast) + }, + ), +) + +// Wrap math `expr` if `apply = true` +#let mwrap(expr, apply) = { + if apply { + $($ + expr + $)$ + } else { + expr + } +} + +#let flat_idxs(expr) = { + if expr.at(0) != "idx" { + (expr, ()) + } else { + let (sub, gathered) = flat_idxs(expr.at(1)) + (sub, gathered + (expr.at(2),)) + } +} + +// Typeset an expression as math +#let expr_to_math = make_expr_formatter( + ( + "arr": (pp, rec, e) => $[#e.slice(1).map(rec.with(PREC.MAX)).join($, $)]$, + "idx": (pp, rec, e) => { + let (val, idxs) = flat_idxs(e) + $#rec(PREC.idx, val)_(#idxs.map(idx => rec(PREC.idx, idx)).join($, $))$ + }, + "not": (pp, rec, e) => mwrap(rec(PREC.not, 1) + $ - #rec(PREC.not, e.at(1))$, pp < PREC.not), + "+": (pp, rec, e) => mwrap($#e.slice(1).map(rec.with(PREC.add)).join($+$)$, pp < PREC.add), + "sum": (pp, rec, e) => { + assert(e.len() == 4, message: "invalid sum:" + repr(e)) + mwrap( + $sum_(#rec(PREC.MAX, e.at(1)))^#rec(PREC.MAX, e.at(2)) #rec(if pp <= PREC.sub {PREC.MAX} else {PREC.sum}, e.at(3))$, + pp <= PREC.sub + ) + }, + "*": (pp, rec, e) => { + if e.len() == 3 and type(e.at(1)) == int and type(e.at(2)) == str and e.at(2).len() == 1 { + // multiplication of a constant with one-letter variable. + // Dropping the "dot" + mwrap($#e.slice(1).map(rec.with(PREC.mul)).join($$)$, pp < PREC.mul) + } else { + mwrap($#e.slice(1).map(rec.with(PREC.mul)).join($dot$)$, pp < PREC.mul) + } + }, + "/": (pp, rec, e) => $#rec(PREC.div, e.at(1)) / #rec(PREC.div, e.at(2))$, + "^": (pp, rec, e) => { + assert(type(e.at(1)) == int, message: "Can only exponentiate constants") + $#e.at(1)^#rec(PREC.MAX, e.at(2))$ + }, + "=": (pp, rec, e) => $#rec(PREC.eq, e.at(1)) = #rec(PREC.eq, e.at(2))$, + ":=": (pp, rec, e) => $#rec(PREC.eq, e.at(1)) := #rec(PREC.eq, e.at(2))$, + "-": (pp, rec, e) => { + if e.len() == 2 { + // Negation + mwrap($-#rec(PREC.neg, e.at(1))$, pp < PREC.neg) + } else { + // Subtraction + mwrap( + $#rec(PREC.add, e.at(1))-#e.slice(2).map(rec.with(PREC.sub)).join($-$)$, + pp <= PREC.sub + ) + } + }, + "cast": (pp, rec, e) => { + assert(e.len() == 3, message: "Invalid type cast: " + repr(e)) + cwrap($#rec(PREC.cast, e.at(1)) colon.double #type_to_math(e.at(2))$, pp < PREC.cast) + }, + ), + var: v => if v.len() == 1 { $#v$ } else { $#raw(v)$ }, + num: n => math.equation[#n], +) diff --git a/spec/front.typ b/spec/front.typ new file mode 100644 index 000000000..d78b0a38e --- /dev/null +++ b/spec/front.typ @@ -0,0 +1,11 @@ +#import "/book.typ": project, meta + +#show: project.with(title: "", cond: () => true) + +#align(center, title(meta.title)) +#align(center)[_Version #meta.version _] +#align(center, meta.authors.join(", ")) + + +This is the specification for the #link("https://github.com/yetanotherco/lambda_vm/")[Lambda verifiable vm]. + diff --git a/spec/halt.typ b/spec/halt.typ new file mode 100644 index 000000000..691154f61 --- /dev/null +++ b/spec/halt.typ @@ -0,0 +1,56 @@ +#import "/book.typ": book-page, aside +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_assumptions, + render_chip_padding_table, +) + +#show: book-page("halt.typ") + +#let config = load_config() +#let chip = load_chip("src/halt.toml", config) +#let halt = raw(chip.name) + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #halt chip leverages #nr_variables variable, spanning #nr_columns columns and leverages #nr_interactions interactions: +#render_chip_variable_table(chip, config) + += Assumptions +It is assumed the input is range checked: +#render_chip_assumptions(chip, config) + += Constraints +The #halt chip: ++ makes sure register `x10` (containing the exit code) equals $0$ (@halt:c:read_zero_exit_code), ++ writes $0$ to all other registers (@halt:c:zeroize_registers_lo/@halt:c:zeroize_registers_hi), and ++ sets `pc` equal to $1$ (@halt:c:pc). +Note that the writes performed by all these interactions are accompanied by the timestamp $2^64-1$; the maximum timestamp. +This prevents any other operation involving memory from being executed hereafter. +#render_constraint_table(chip, config, groups: "all") + +#aside("Note on register clean up", +[ + Observe that --- in its current state --- this solution puts the burden of verifying the register cleanup on the verifier inside of the lookup argument. + Alternatively, one could add 31 lookups to the "memory" table to remove the _known_ final tokens for the registers there. +]) + +== Lookup +In this VM, halting is considered equivalent to executing a `sys_exit`. +Hence, this chip responds to `ECALL`s with system call number 93. +#footnote([RISC-V GNU-toolchain, `unistd.h`; version 2026-01-23, #link("https://github.com/riscv-collab/riscv-gnu-toolchain/blob/2026.01.23/linux-headers/include/asm-generic/unistd.h#L258")[[src]]]) +The HALT chip therefore contributes the following interaction to the lookup-argument: +#render_constraint_table(chip, config, groups: "lookup") + += Padding +This chip should only contain a single row. +Given that $2^0 = 1$, this chip does not need to be padded. +As such, no padding is defined. diff --git a/spec/is_bit.typ b/spec/is_bit.typ new file mode 100644 index 000000000..5246a623a --- /dev/null +++ b/spec/is_bit.typ @@ -0,0 +1,34 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_variable_table, render_constraint_table, set_nr_interactions, total_nr_variables + +#let config = load_config() +#let chip = load_chip("src/is_bit.toml", config) + +#show: book-page(chip.name) + +#set_nr_interactions(chip) +#let nr_variables = total_nr_variables(chip) + +#let is_bit = raw(chip.name) + +#is_bit is a constraint template that is used to assert that a variable lies in the range ${0, 1}$ if some second variable is non-zero. +Barring exceptional cases, this template is used to assert that a variable of type `Bit` assumes a valid value under some condition. + += Variables +The #is_bit template operates on #nr_variables variables: +#render_chip_variable_table(chip, config) + += Constraints +It takes only one constraint to enforce that `X` must be either $0$ or $1$ whenever $#`cond` eq.not 0$: +#render_constraint_table(chip, config) +*Note*: +- In case of _unconditional_ template application, `cond` can be dropped from the constraint, simplifying it to $#`X` (1- #`X`) = 0$. +- As described earlier, the `cond` variable must be describable by a degree-1 (i.e., linear) expression. + This is to make sure that @isbit:c:isbit's expression has degree at most 3. + +== Correctness argument +If `cond` is $0$, @isbit:c:isbit is trivially satisfied: `X` can assume any value and the polynomial constraint will evaluate to $0$ regardless. +When $#`cond` eq.not 0$, it follows that the statement can only be proven when $#`X` (1-#`X`) equiv 0 mod p$, with $p$ the modulus of the field. +Because `BaseField` is a prime field, this equality is only satisfied if either $#`X` equiv 0 mod p$ or $1-#`X` equiv 0 mod p$. +Hence, it is proven that when $#`cond` eq.not 0$, @isbit:c:isbit is only satisfied if $#`X` in {0, 1}$. #align(right, $qed$) diff --git a/spec/load.typ b/spec/load.typ new file mode 100644 index 000000000..ac469ec79 --- /dev/null +++ b/spec/load.typ @@ -0,0 +1,48 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + render_chip_padding_table, + render_constraint_table, + compute_nr_interactions, + total_nr_instantiated_columns, + total_nr_variables, +) + +#let config = load_config() +#let chip = load_chip("src/load.toml", config) + +#show: book-page(chip.name) +#let load = raw(chip.name) + +The #load chip provides functionality to read values from memory and sign-extend them where appropriate. +It delegates low-level memory handling to the `MEMW` chip (@memw). + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #load chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Constraints +The chip delegates the actual memory interaction to the `MEMW` chip, +and ensures correctness of the requested sign/zero extension. +The output `res` is correctly range-checked as long as the memory contents are. + +#render_constraint_table(chip, config, groups: "all") + +The chip contributes the following to the lookup argument. + +#render_constraint_table(chip, config, groups: "output") + += Padding + +The table can be padded to the next power of two with the following value assignments: + +#render_chip_padding_table(chip, config) diff --git a/spec/logup.typ b/spec/logup.typ new file mode 100644 index 000000000..7bb9a085d --- /dev/null +++ b/spec/logup.typ @@ -0,0 +1,146 @@ +#import "/book.typ": book-page, aside, cdsg + +#show: book-page("logup") +#set heading(numbering: "1.") +#show link: underline + +#show "constraint choice": link()[constraint choice] + +The _LogUp_ proof system conducts a permutation check based on summing partial derivatives. This check ensures that whatever tuple is sent to be "looked-up" by a _source table_ is indeed received in the expected _destination table_. + += Notation + +#let BaseField = math.FF +#let ExtensionField = math.GG + +== VM Notation + +=== Preliminary notation +- $NN$: the set of non-negative natural integers. +- $BaseField$: the base finite field used by the arithmetisation. +- $ExtensionField$: a finite extension of $BaseField$ of cryptographic size. +- $[n]$ for $n in NN$: the set of integers ${0, dots, n - 1}$. +- $X[i]$ for tuple $X$: the $i$-th element of $X$, starting at $0$. + +=== Arithmetisation notation + +#let numTables = $sans(t)$ +#let Table = $T$ +#let TableSet = ${Table_i}_(i in [t])$ +#let numColumns = $sans(m)$ +#let numRows = $sans(N)$ + +- $numTables in NN$: number of tables $Table_i$ in the arithmetisation of the VM. +- $TableSet$: set of all tables $Table_i$ in the arithmetisation of the VM. +- $numColumns_i in NN$: number of _columns_ in table $Table_i$ (not the number of variables). +- $numRows_i in NN$: number of _rows_ in table $Table_i$. + +== Interaction Notation + +#let Interaction = $I$ +#let id = $sans(id)$ +#let numElements = $ell$ +#let weightFunction = $w$ +#let multiplicity = $mu$ + +The $j$-th _interaction_ $Interaction_j$ of table $Table_i$ is defined by the following tuple: + +#table( + columns: (auto, auto), + inset: 6pt, + align: horizon, + stroke: none, + table.header([*Symbol*], [*Description*]), + table.hline(stroke: 1pt), + table.vline(stroke: 1pt, x: 1), + [$id_(i,j) in FF$], + [the _type identifier_ of the interaction, usually the identifier of the chip that is constraining the relation expected to hold within the looked-up tuple.], + [$numElements_(i,j) in NN$], + [the _length_ of the tuple of elements being looked-up.], + [ + $weightFunction_(i,j) : FF^(numColumns_i) & arrow FF^(numElements_(i,j) + 1) \ + R & mapsto arrow(t)_(i,j) || mu_(i,j)$ + ], + [the _weight function_ that maps a row $R$ of table $Table_i$ to the looked-up tuple $arrow(t)_(i,j)$ and its multiplicity $mu_(i,j) in BaseField$.], +) + + += Vanilla LogUp + +== Protocol Description + +#let logupChallenge = math.alpha +#let fingerprintCoeff = math.beta + +#set enum(numbering: "1.a.i.1.a.") + ++ Prover commits to all traces. + ++ Verifier samples a random _(global) LogUp challenge_ $logupChallenge in ExtensionField$ and a random _fingerprint coefficient_ $fingerprintCoeff in ExtensionField$ and sends them to the Prover. + ++ Prover commits to (i) interaction contribution, (ii) table running sum columns, and (iii) each table's contribution: + + + For each table $Table_i$, populate the interaction contribution columns and compute the _table (LogUp) contribution_: + + + For each interaction $Interaction_j$ of table $Table_i$, initialize an empty _interaction contribution column_ of length $numRows_i$. + + + Initialise a _table running sum column_ $S_i in ExtensionField^(numRows_i)$ with the first value $S_i [0]$ populated according to the constraint choice. + + + *Constrain* the first row if required by selected constraint choice. + + + For each $j$-th row $R_j in BaseField^(numColumns_i)$ of $Table_i$, for $j in [numRows_i - 1]$: + + For each $k$-th interaction $Interaction_k$ of table $Table_i$: + + Compute the _interaction contribution numerator_ $ n_(j,k) = mu_(i,k) = w_(i,k)(R_j)[numElements_(i,k)] $ + + If $n eq.not 0$, compute the _interaction contribution denominator_ $ d_(j,k) = logupChallenge + fingerprintCoeff dot id_(i,k) + sum_(l = 0)^(numElements_(i,k) - 1) fingerprintCoeff^(l + 2) dot weightFunction_(i,k) (R_j)[l]. $ + + Save the _interaction contribution_ as $n_(j,k)/d_(j,k) in ExtensionField$ in the corresponding interaction contribution column for this interaction. + + *Constrain* the interaction contribution column according to the definitions of $n$ and~$d$. + + + Compute the _row contribution_ as the sum $s_(j) = sum_k n_(j,k) / d_(j,k)$ and compute the next row's table running sum value $S_i [j+1] = S_i [j] + s_(j)$. + + + *Constrain* the transition of the running sum column as indicated by the constraint choice. + + + *Constrain* the last row if required by selected constraint choice. + + + Batch-commit to every table's interaction contribution columns and running sum columns with the column commitment scheme and commit to the table's overall contribution $S_i [N_i - 1]$ by sending it in the clear to the verifier. + ++ Verifier checks that the sum of every table's overall contribution is equal to zero: $sum_i S_i [N_i - 1] = 0_ExtensionField$, and delegates the checks of the constraints to the STARK. + +== Running Sum Constraint Choices + +#cdsg[Write the constraints in this section more formally after STARK description has been written.] + +=== Choice 1: transitions looking back + +tl,dr: implicit $0_ExtensionField$ initial value, explicit final value. + ++ (*Boundary, first row*) Constrain first row of running sum column to equal the sum of the first row of every interaction contribution column. (This is analogous an implicit $-1$-th row initialised at $0_ExtensionField$.) ++ (*Transition, looking back, applied to rows $1, dots, numRows_i - 1$*) For each row _other than the first_, constrain the _current_ running sum value to equal the sum of every current interaction contribution column added to the _previous_ running sum value. ++ (*Boundary, last row*) Constrain last row of running sum column to equal the claimed table contribution. + +Total constraints: 2 boundary + 1 transition over $numRows_i - 1$ rows. + +=== Choice 2: transitions looking forward + +tl,dr: explicit $0_ExtensionField$ initial value, implicit final value. + ++ (*Boundary, first row*) Constrain first row of running sum column to equal $0_ExtensionField$. ++ (*Transition, looking forward, applied to rows $0, dots, numRows_i - 2$*) For each row _other than the last_, constrain the _next_ running sum value to equal the sum of every current interaction contribution column added to the _current_ running sum value. ++ (*Boundary, last row*) Constrain last row of running sum column added to sum of last row of every interaction column to equal the claimed table contribution. (That is, the claimed table contribution is implicit in the last row of the table, but not written to last value of running sum column.) + +Total constraints: 2 boundary + 1 transition over $numRows_i - 1$ rows. + +=== Choice 3: circular transitions looking back/forward + ++ For each row, constrain the _current/next_ (wrapping to first on last if "next") running sum value to equal the sum of every current interaction contribution value added to the _previous/current_ (wrapping to last on first if "previous") running sum value added to claimed table contribution divided by $numRows_i$. + +Total constraints: 1 _circular_ transition over $numRows_i$ rows. + +#aside("Justification")[ + This single circular constraint checks that each row's contribution $s_(i,j)$ is added to the running sum column, either in the current row's cell or in the next row's. + In order to avoid boundary constraints, the look-back or peek-forward into the running sum column wraps around the beginning or end of the table. + + This alone implies that difference between first and last row's values will be the table's overall real contribution $sum_j s_(i,j)$, which will be incompatible with the circularity of the constraint. + Since boundary constraints are avoided, the way to check that $sum_j s_(i,j)$ equals the claimed contribution $L_i$ is to remove a fraction of $L_i$ at each row in such a way that $L_i$ is removed completely after summing all $numRows_i$ rows; i.e., the constraint subtracts the public term $L_i / numRows_i$ from the running sum at every row. + + If the expected equality $sum_j s_(i,j) = L_i$ holds, then the circularity of the constraint will also hold. +] diff --git a/spec/lt.typ b/spec/lt.typ new file mode 100644 index 000000000..2e0ac8be4 --- /dev/null +++ b/spec/lt.typ @@ -0,0 +1,92 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + render_chip_padding_table, + render_constraint_table, + total_nr_instantiated_columns, + total_nr_variables, + compute_nr_interactions, +) + +#let config = load_config() +#let chip = load_chip("src/lt.toml", config) + +#show: book-page(chip.name) +#let lt = raw(chip.name) + +The #lt chip constrains an indicator bit for the less-than relation, signed or unsigned. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #lt chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions +We assume the inputs `lhs`, `rhs` and `signed` are partially range checked. +#render_chip_assumptions(chip, config) + += Constraints +We first constrain that all variables correspond to their definition. +For the defining constraint of `lt`, @lt:c:lt, observe that it is a choice +between two options, depending on the input flag `signed`. +In the case of unsigned comparison, we simply need `unsigned_lt`, indicating +that a wraparound (carry bit) modulo $2^64$ is needed to go from `rhs` to `lhs` via addition. +For the case of signed comparison, we first need some case analysis. + +We split $a < b$ into four disjoint cases, conditioned on the sign of $a$ and $b$. +Recall that the sign of a number in two's complement can be read off from the MSB, +being $1$ for a negative number and $0$ for a positive one. +For this analysis, we denote the MSB of $a$ as $A$ and the MSB of $b$ as $B$. +The four disjoint cases then become: + ++ $dash(A) and B and (a < b)$ ++ $A and dash(B) and (a < b)$ ++ $A and B and (a < b)$ ++ $dash(A) and dash(B) and (a < b)$ + +The first case is evidently false, while the second case simplifies to $A and dash(B)$. +For the third and fourth case, observe that when $A = B$, the $<$ relation is preserved +by the modular correspondence between $[-2^(31), 2^(31))$ and $[0, 2^(64))$. +Importantly, this modular correspondence is merely a reinterpretation of the +bits or values of $a$ and $b$, due to the representation in two's complement. +Hence, we can introduce the value $C = #`unsigned_lt`$, that accurately represents +the relation $a < b$ when $A = B$. + +Combining our three remaining cases, we obtain the boolean formula $A dash(B) or A B C or dash(A) dash(B) C$. +Since the cases are disjoint, this can be computed with the binary-valued polynomial +$P(A, B, C) = A (1 - B) + A B C + (1 - A) (1 - B) C$. + +The polynomial $P$ can be simplified to a total degree of two. +We claim that the polynomial $Q(A, B, C) = A (1 - B) + A C + (1 - B) C$ +is, for the purposes of this chip, equivalent to $P$. +An exhaustive check shows that $P(A, B, C) != Q(A, B, C)$ only for the triple $(A, B, C) = (1, 0, 1)$. +This is, however, impossible due to the correctness of `ADD`. +In more detail, if we let $s$ be the (range-checked) difference $a - b$ +(so the equivalent of the #`lhs_sub_rhs` column), +and $x'$ denote the most significant word of a variable $x$, +we need $c dot 2^32 + a' = b' + s' + #`carry[0]`$, by the definition of `carry`. +However, the left hand side of this is at least $3 dot 2^31$, as $(A, C) = (1, 1)$, +and the right hand side is at most $(2^31 - 1) + (2^32 - 1) + 1 = 3 dot 2^31 - 1$. +Therefore, we can use $Q$ to constrain `lt` when `signed = 1`. + +#render_constraint_table(chip, config, groups: "defs") + +And then we constrain the subtraction, +taking care of the remaining range checking not yet covered by the assumptions or the `MSB16` lookup. + +#render_constraint_table(chip, config, groups: "sub") + +The chip contributes the following to the lookup argument. + +#render_constraint_table(chip, config, groups: "output") + += Padding + +The table can be padded to the next power of two with the following value assignments: + +#render_chip_padding_table(chip, config) diff --git a/spec/memory.typ b/spec/memory.typ new file mode 100644 index 000000000..876884c85 --- /dev/null +++ b/spec/memory.typ @@ -0,0 +1,229 @@ +#import "/book.typ": book-page, rj, aside, xref +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + render_chip_padding_table, + render_constraint_table, + total_nr_instantiated_columns, + total_nr_variables, +) + +#let config = load_config() +#let chip = load_chip("src/page.toml", config) + +#show: book-page("memory.typ") + +As part of fully proving the correct execution of a RISC-V program, +the VM must ensure that memory reads and writes are consistent. +That is, every byte read from some address corresponds to the byte that was last written to that address +--- or the initial value if nothing has been written yet. +We consider "memory" in a broad sense here: +both RAM and the general purpose registers can be seen as instantiations of memory +and are therefore handled simultaneously. +#footnote[ + While RAM is byte addressed, we do choose to store registers as a `DWordWL` over two word addresses. +] + +On a high level, we ensure memory consistency by an interacting system of +reads and writes to a lookup argument, combined with an initialization and finalization scheme. +The initialization and finalization schemes together ensure both that (1) the necessary preconditions +for the lookup system are satisfied, and (2) the program is executed with the correct +initial memory and register contents as specified by the ELF binary and the ISA. + += Memory types + +A commonly made distinction of memory types is that of _read-only_ and _read-write_ memory, +with the more restrictive read-only variant often allowing for more efficient solutions +(be that regarding prover time, verifier time or proof size) via table lookup proofs. +Naturally, the VM’s main memory and registers should be handled by a read-write system +as the guest program/environment can issue instructions that write to memory. +While there are some subsystems that can be modelled as read-only memory +---e.g., the program memory and instruction decoding--- +we opt to integrate these into the proof system via chip interactions (relying on techniques derived from table lookup arguments). +As such, we only concern ourselves with read-write memory, moving forward. + += Memory operations + +Every memory operation has some conceptual attributes that are relevant to mention or discuss: + +- The type of operation (read or write) +- The memory address --- this is an address in the broad sense: + main memory and registers have their own dedicated part of the unified address space. +- The value being read from or written to the memory address +- When the value was read or written, see the below paragraph + +Since we will have to ensure that memory accesses are temporally consistent within the execution of the VM, +we additionally consider a _timestamp_ for every memory access, that should be strictly increasing. +As such, it should never be possible for the system to generate accesses to the same address at identical timestamps. +Multiple memory accesses can (and indeed will, consider e.g. register reads) occur in a single execution cycle of the VM, +so we cannot use the cycle counter directly as timestamp for register accesses. +We can, however, statically bound the maximal number of memory accesses made during a single execution by a granularity constant $k$ +and derive timestamps from the cycle counter. +The $i$th possible memory access in cycle $c$ will obtain as timestamp the value $k dot c + i$. +For simplicity, we will always reserve a timestamp for every possible memory access, and leave the timestamp unused if an instruction does not use it. + + +#aside[Note on "simultaneous" memory accesses][ + For reasons of completeness (since temporal integrity as discussed below is a security necessity), + we cannot deal with multiple accesses to the same address at identical timestamps. + However, if multiple accesses are guaranteed to be independent (that is, to different addresses), they can still share a timestamp + --- consider, e.g., the case of reading a word as 4 bytes with the `LW` load instruction. + This property is already taken into account where possible in the design of the system. + For instance, in the CPU chip, we can ensure that there are at most 3 memory accesses not guaranteed + to be independent, so a timestamp granularity of 4 timestamps per cycle is enough. +] + + += Permutation argument + +We can conceptually organise the state of the memory as a collection of "tokens" that represent tuples +$(serif("timestamp"), serif("address"), serif("value"))$, +meaning the current value written to $serif("address")$ is $serif("value")$, +last written to memory at $serif("timestamp")$. +Having exactly one value associated with any address will be ensured (see further down in this document) +by the interaction of memory initialization, memory finalization, and the effects of memory operations. + +Each memory operation will then do two things: + +- Consume the current token in the memory +- Emit a new token to replace it + +Naturally, for a read operation, the _values_ embedded in the consumed and emitted tokens must be identical. +From the need to consume a token even on the first memory access, +we can see the necessity for a memory initialization procedure +---in addition to having to make sure the initial memory content lines up with what the binary dictates. + +So long as we can properly constrain temporal integrity (that is, no memory operation can consume future tokens), +this "balancing" act of tokens can be integrated (with sufficient domain separation) into the existing LogUp argument (@logup): +consuming a token corresponds to a "receive" and emitting a new token is a "send". + += Temporal integrity + +To ensure temporal integrity, every memory operation needs to be constrained for the newly emitted token +to have a strictly greater timestamp than the consumed token. +This raises the question of how to represent timestamps and cleanly perform this check, +as over a finite field the “less than” relation is ill-defined +(though it is common and natural to consider it as the less than relation over the natural lift of the field into the integers). +We choose to represent timestamps as machine words, using the existing `LT` chip (@lt) functionality for comparisons. +The full implementation of the timestamp system can be seen in the `timestamp` column of the `CPU` (@cpu) and `MEMW` chips (@memw). +The `CPU` merely passes in the current timestamp, while `MEMW` can recall the previously written timestamp and constrain the correct sequencing. + +#aside[Note on options and trade-offs for timestamp representation][ + #grid(columns: (1fr, 1fr), gutter: 1em)[#align(center, emph[Machine word])][#align(center, emph[Field element])][ + - Clean definition of “less-than”, using the already existing `LT` functionality in the ALU + - Harder to perform increments, needing extra constraints beyond field arithmetic + - But this can be alleviated by providing a precomputed column that has a fixed increment per CPU row + ][ + - Comparison is more annoying, but can work by: + - Decomposition into a machine word and chip interaction with the LT chip + - Bit decomposition and comparison constraints + - Range-checking the difference to be sufficiently small w.r.t. the field characteristic. + - Increments and basic arithmetic operations are cheap + ] +] + += Initialization and Finalization + +Because the LogUp argument handling token consumption and emission needs to be fully balanced +--- every token emitted should be consumed, and vice versa --- +we need to have a system to emit the initial tokens and consume the final tokens. +This needs to ensure that every address has at most a single initializing emission, and at most one finalizing consumption. +Having at most one initialization will, through the correctness of the lookup argument, +immediately lead to having at most one correct finalization, and vice versa. + +The initialization will need to correspond to a fixed initial register state for the VM, +as well as the memory loaded from the program binary, zero-initialization of memory elsewhere, and private input provided by the prover. +The contribution of initialization with static data from the ELF executable and the initial register state to the sum +can be handled directly by the verifier, ensuring correctness corresponding to the ELF binary being proven. +This leaves only zero-initialization and prover input as prover-side concerns for initialization, +alongside the finalization of the entire used memory. + +For our chosen scheme (which we refer to as "paged initialization/finalization"), +the available memory range is split into equally (power-of-two) sized "pages". +Each address can then be represented as `address = page_base_address + page_offset`, +with `page_base_address` being "page-aligned", and `page_offset` belonging to a limited range (the page size). +As such, initialization or finalization of a page is represented by a table with columns `page`, `offset`, `value`, and ---for finalization--- `timestamp`. +The `page` column is a preprocessed, constant value (which can be entirely virtualized/inlined into the constraints for this table), +and the `offset` column is a preprocessed column containing its row index. +Depending on the type of initialization, `value` can be a prover-committed column (input data), or a precomputed, constant column containing `0` (free memory space). +This table then feeds into the LogUp system in the normal way, +emitting the initial tokens for all addresses in a page, without consuming any tokens. +Since the `offset` column is always the same, it can be reused across all paged initialization and finalization tables. + +Concretely, each page gets an associated `PAGE` table, consisting of #total_nr_variables(chip) variables +over #total_nr_instantiated_columns(chip, config) columns. +For each such table, the `page` variable is instantiated as the constant base address of the page. +The `offset` column is preprocessed, which helps the verifier ensure that each page has a single fixed size, +but the verifier should still check that no pages overlap and all `page` values are page-aligned. + +== Page initialization + +#rj[check whether we need `fini` to be range-checked] +We present here a set of constraints on the `PAGE` table that + ++ enforces the initial and final values of each address are bytes ++ adds the initial and final interaction to the LogUp argument + +For zero-initialized pages, `init` can be a constant `0`, +and hence doesn't need a column, nor a range check. + +#render_chip_variable_table(chip, config) +#render_constraint_table(chip, config) + + +#aside[Note on alternatives and trade-offs][ + We identify a few alternatives that would achieve the desired initialization/finalization functionalities, and consider their respective trade-offs. + + _"Free-zero" initialization_ + + Zero-initialization could be achieved by allowing the `MEMW` chip to output a zero + without consuming a token from the lookup argument. + This would in turn be made secure by finalization consuming at most one token per address: + if an address is initialized more than once, the proof cannot be finalized. + - This requires fewer pages (and hence tables) for zero-initialization. + - But it comes at a cost of added complexity in the `MEMW `chip, and likely some extra columns to handle this. + Keeping track of initialized addresses, and potentially having to initialize only some of the bytes in a word-read + may make bookkeeping challenging. + - This is an alternative form of sparse initialization (see below), so it is incompatible with paged finalization. + Paged finalization can be made into a compatible sparse form by adding a bit-checked multiplicity column. + + _Sparse initialization/finalization_ + + One or more STARK tables (depending on the amount of memory used) consisting of `(address, value)` columns are introduced, + where for zero-initialization, `value` can be constant zero. + Transition constraints ensure that `address` is strictly increasing, enforcing the "at most once" property; + `value` is range-checked to consist of bytes. + Similar to paged finalization, an additional `timestamp` column is added, containing the final timestamp each address was accessed. + This table is then further used to contribute to the LogUp sum as with any other interactions. + - The transition constraints can be chosen to only apply on finalization, as at-most-once finalization is enough to ensure consistency. + - Sparse initialization is incompatible with paged finalization, see also the remark under free-zero initialization above. + - This would require transition constraints, which currently are not needed elsewhere in the VM design + - Additionally, for memory use exceeding the capacity of a single initialization/finalization table, some form of transition constraint between tables is needed + - Alternatively, transition constraints could potentially be avoided by more integration into the LogUp system, but this could turn out more costly in practice + - This is compatible with the above "free zero" initialization + - Since a prover-committed address column is needed (rather than a precomputed one), the number of required columns increases. + - As an optimization, the address column could potentially be used simultaneously for initialization and finalization + - Sparse initialization/finalization reduces the cost for sparse memory access patterns, + where only a few addresses would be accessed per page. + Most programs and compilers should however favor a memory locality that makes paged initialization/finalization comparable. +] + +== Register initialization/finalization + +The initial and final state of registers can be entirely known by +the verifier, since the relevant initialization values are either zero, +or embedded in the ELF, and the final values can be set to a known value +by the `HALT` ecall (@ecall). +As additionally, the number of registers is small, the verifier can directly +add the required balancing terms to the LogUp sum. + += Notes and considerations + +- Register reads and writes may interact within a single cycle, so a correct and fixed ordering needs to be ensured +- Correctness of initialization and completeness of finalization need to be ensured + += Future topics of interest + +- Optimize memory systems after determining factual bottlenecks (e.g. taking inspiration from Twist and Shout, or other recent research) +- Double check whether IS_BYTE constraints are needed for fini diff --git a/spec/memw.typ b/spec/memw.typ new file mode 100644 index 000000000..425508597 --- /dev/null +++ b/spec/memw.typ @@ -0,0 +1,162 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_padding_table +) + +#let config = load_config() +#let chip = load_chip("src/memw.toml", config) + +#show: book-page(chip.name) + +#let memw = raw(chip.name) + +The #memw chip is used to read and write memory locations (both RAM and registers) +in chunks of 1, 2, 4 or 8 values. +It introduces the old value and last-accessed timestamps of memory addresses internally, +in order to satisfy the design of the memory argument (@memory). + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_memw_interactions = compute_nr_interactions(chip) + +The #memw chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_memw_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions + +#render_chip_assumptions(chip, config) + +Our assumptions do not explicitly cover any range checks for the `is_register` and `value` columns, +as these are not necessary for the correctness of this chip in isolation. +Still, these properties are necessary for the consistency of the system as a whole, and therefore +we document it here, keeping the type information as a reading help. + += Constraints + +Depending on the values of `write2`, `write4` and `write8`, the addresses following `base_address` need to be constructed. +Rather than computing these in full (which would require the later addresses to be instantiated), +it suffices to know the `carry`: the bit indicating whether $#`base_address`_0 + t >= 2^32$, i.e., whether adding $t in [1, 7]$ to `base_address` requires a carry from the lower to the upper limb. +Note that it is safe for the prover to chose these bits: additions for which this bit is not correctly set +will yield an address where either the lower or upper limb is out of bounds. +As such, the constructed address will not match any existing memory tokens, +which are only initialized for correctly formatted and range-checked doublewords (see @memory). + +#render_constraint_table(chip, config, groups: "consistency") + +As long as `timestamp` is properly range-checked, the presence of `old_timestamp` +in the memory argument automatically ensures it is appropriately range checked +(this assumes no external entities provide negative multiplicities without range checking the timestamp). +This ensures the assumptions for `LT` are satisfied. + +There is no need to check that the additions do not overflow, +as our address calculations are not performed modulo $2^64$ here, +and any overflow will result in an address without matching initialization. + +The chip adds the following tuples to the lookup argument, +to effectuate that part of the memory argument. +#render_constraint_table(chip, config, groups: "memory") + +This chip contributes the following to the lookup argument: +#render_constraint_table(chip, config, groups: "output") + += Padding +The table can be padded to the next power of two with the following value assignments: +#render_chip_padding_table(chip, config) + += Read-size aligned fast path + +#let alignedchip = load_chip("src/memw_aligned.toml", config) +#let aligned = raw(alignedchip.name) +#let nr_aligned_interactions = compute_nr_interactions(alignedchip) + +When a memory access happens at an address with proper alignment for its access size +(i.e., adding the access size to `base_address`'s lowest limb does not overflow), +and all accessed elements were last accessed at the same timestamp, we can +instead use the #aligned chip to save on total column count. +The saving comes from only requiring a single old timestamp to be stored, +as well as being able to guarantee that all values of `add_limb_overflow` would be zero. +A minor extra cost is introduced in the form of a check that the alignment is indeed correct, +and the corresponding decomposition of the `base_address`. + +Further logic remains essentially the same, so we briefly present the relevant tables for this chip. +#let nr_variables = total_nr_variables(alignedchip) +#let nr_columns = total_nr_instantiated_columns(alignedchip, config) + +The #aligned chip only needs #nr_variables variables, expressed through #nr_columns columns; it leverages #nr_aligned_interactions interactions. +#render_chip_variable_table(alignedchip, config) +#render_chip_assumptions(alignedchip, config) +#render_constraint_table(alignedchip, config) + +== Padding +The table can be padded to the next power of two with the following value assignments: +#render_chip_padding_table(alignedchip, config) + += Register fast-path + +#let config = load_config() +#let register_chip = load_chip("src/memw_register.toml", config) +#let reg = raw(register_chip.name) + +The #reg chip provides a fast-path for accessing registers. +This fast-path leverages that registers ++ can be addressed using a `Byte`, rather than a full `DWord`, ++ are constantly accessed, i.e., $#`timestamp` - #`old_timestamp`$ is small, and ++ have a fixed access pattern +to achieve a footprint that is significantly smaller than both #memw and #aligned. + +Note: as a result of hard optimization, this chip can only be used for register accesses for which ++ $#`timestamp` - #`old_timestamp` in [1, 2^16]$, and ++ $#`timestamp[0]` > #`old_timestamp[0]`$ +If either of these rules does not apply to your access, you should fall back to using `MEMW_A`. + +Note moreover that this chip does not guard against misaligned register access faults: to access register with a given `address`, one must provide $2 dot #`address`$ in the lookup. + +== Variables +#let nr_variables = total_nr_variables(register_chip) +#let nr_columns = total_nr_instantiated_columns(register_chip, config) +#let nr_memw_r_interactions = compute_nr_interactions(register_chip) + +The #reg chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_memw_r_interactions interactions: +#render_chip_variable_table(register_chip, config) + +== Assumptions +The following range checks are assumed to be performed/enforced outside of this chip: +#render_chip_assumptions(register_chip, config) + +== Constraints +Since most registers are frequently accessed, the difference between `timestamp` and `old_timestamp` is small most of the times. +Rather than storing their (nearly) identical upper limbs twice, it is instead assumed that +$#`old_timestamp[1]` = #`timestamp[1]`$; #aligned can be used for accesses where this is not the case. + +Verifying that $#`timestamp` > #`old_timestamp`$ now simplifies to verifying that $#`timestamp[0]` - #`old_timestamp[0]` > 0$. +For most accesses, this value will be small enough to fit in a `Half`. +This chip thus enforces this by means of the following constraint: +#render_constraint_table(register_chip, config, groups: "diff") + +With $#`old_timestamp`<#`timestamp`$ asserted, `old` is read from the register (@regw:c:read_old) and `val` is written back (@regw:c:write_val). +#render_constraint_table(register_chip, config, groups: "interactions") + +This chip can either just write ($#`μ_write` = 1$), or both read and write ($#`μ_read` = 1$) in the same cycle. +It must be asserted that at most one of these two options is selected: +#render_constraint_table(register_chip, config, groups: "multiplicities") + +Lastly, this chip contributes the following interactions to the logup: +#render_constraint_table(register_chip, config, groups: "output") + +== Padding +The table can be padded to the next power of two with the following value assignments: +#render_chip_padding_table(register_chip, config) + += Notes/optimizations +The following ideas may prove to be optimizations for the #memw/#aligned/#reg chip: +- `MEMB` chip that does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) +- Adding `μ_sum`/`w2`/`w4`/`write8` multiplicities to the `IS_HALF` lookups may make some GKR things faster if there are known zeroes. +- For the register fast-path, one may upgrade the `IS_HALF` check to an `IS_B20` check for extended range at the cost of looking through a larger table. \ No newline at end of file diff --git a/spec/mul.typ b/spec/mul.typ new file mode 100644 index 000000000..6e6de12e0 --- /dev/null +++ b/spec/mul.typ @@ -0,0 +1,111 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_assumptions, + render_chip_padding_table, +) + +#let config = load_config() +#let chip = load_chip("src/mul.toml", config) + +#show: book-page(chip.name) + +#let mul = raw(chip.name) + +The #mul chip constrains multiplication, both signed and unsigned, +as well as providing access to the low and high halfs of the multiplication result. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The #mul chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + +#let stackrel(top, bottom) = { + $mat(delim: #none, top; bottom)$ +} + += Assumptions +The following range checks are assumed to be performed/enforced outside of this chip: +#render_chip_assumptions(chip, config) + += Constraints +== Overview +When `lhs` and `rhs` are _unsigned_ integers, computing their product $mod 2^128$ comes down to evaluating +$ +(sum_(j=0)^3 2^(16j) dot #`lhs`_j) dot (sum_(i=0)^3 2^(16i) dot #`rhs`_i) mod 2^128. +$ +If `lhs` and `rhs` are signed instead, the computation remains nearly identical: +based on their signs, one must either zero or one-extend `lhs` and `rhs` --- forming `lhs_ext` and `rhs_ext` respectively --- and compute their product $mod 2^128$: +$ +(sum_(j=0)^7 2^(16j) dot #`lhs_ext`_j) dot (sum_(i=0)^7 2^(16i) dot #`rhs_ext`_i) mod 2^128. +$ +where `lhs_ext` and `rhs_ext` are treated as _unsigned_ integers. +Note that by setting the extension limbs of `lhs` and/or `rhs` to $0$ when the integer is (i) unsigned or (ii) signed and non-negative, this second formula still applies. +For the purposes of constraining the multiplication operation, we rewrite this formula as +#show math.equation: set block(breakable: true) +$ + &(sum_(j=0)^7 2^(16j) dot #`lhs_ext`_j) dot (sum_(i=0)^7 2^(16i) dot #`rhs_ext`_i) mod 2^128 \ + &equiv sum_(j=0)^7 sum_(i=0)^7 2^(16(i+j)) dot #`lhs_ext`_j dot #`rhs_ext`_i mod 2^128 \ + &stackrel(triangle, equiv) sum_(j=0)^7 sum_(i=0)^(7-j) 2^(16(i+j)) dot #`lhs_ext`_j dot #`rhs_ext`_i mod 2^128 \ + &stackrel(square, equiv) sum_(j=0)^7 sum_(i=j)^(7) 2^(16i) dot #`lhs_ext`_j dot #`rhs_ext`_(i-j) mod 2^128 \ + &stackrel(penta, equiv) sum_(i=0)^7 sum_(j=0)^(i) 2^(16i) dot #`lhs_ext`_j dot #`rhs_ext`_(i-j) mod 2^128 \ + &equiv sum_(i=0)^3 sum_(k=0)^1 sum_(j=0)^(2i+k) 2^(16(2i+k)) dot #`lhs_ext`_j dot #`rhs_ext`_(2i+k-j) mod 2^128 \ + &equiv sum_(i=0)^3 2^(32i) dot sum_(k=0)^1 2^(16k) dot sum_(j=0)^(2i+k) #`lhs_ext`_j dot #`rhs_ext`_(2i+k-j) mod 2^128 +$ +where at step +- $triangle$ we can ignore $i > 7-j$, since that makes $2^(16(i+j)) equiv 0 mod 2^128$, +- $square$ we rewrite the second summation such that $i$ iterates from $j$ to 7, rather than $0$ to $7-j$, and +- $penta$ we swap the sums. + +We let `raw_product` capture the second summation in this last formula (see @mul:c:raw_product). +By construction, $#`raw_product`_i < 2^51$ for all $i in [0, 3]$, far exceeding the 32-bits that fit in a single `Word`-limb. +What remains then is to reduce each limb of `raw_product` $mod 2^32$, carrying the overflow of each limb to the next, constructing the output `res` in doing so. + +This reduce-and-carry operation is constrained by @mul:c:range_lo/@mul:c:range_hi and @mul:c:carry, combined with `carry`'s definition. +@mul:c:carry and `carry`'s definition enforce that +$ + forall i in [0, 3]: #`raw_product`_i + #`carry`_(i-1) - #`res`_i in { k dot 2^32 | k in [0, 2^20) } +$ +with $#`carry`_(-1) = 0$ for simplicity. +In other words: $#`res`_i equiv #`raw_product`_i + #`carry`_(i-1) (mod 2^32)$. +With @mul:c:range_lo/@mul:c:range_hi forcing $#`res`_i < 2^32$, $#`res`_i$ can only assume one value: $#`raw_product`_i + #`carry`_(i-1) mod 2^32$. + +*Note*: one may have observed that @mul:c:carry requires $#`carry`_i in [0, 2^20)$, while no limb of a valid carry value would ever exceed $2^19$. +This is indeed the case. +However, there is some slack in how tight one has to constrain the `carry` values. +In fact, in this situation it suffices to assert that $#`carry`_i < frac(p, 2^32, style: "skewed") approx 2^31$, where $p$ denotes the field's modulus. +Given that other chips also use 20-bit lookups, using `IS_B20` makes for a simpler design. + +== Definitions +We constrain `lhs_is_negative` and `rhs_is_negative` according to their definition; `lo`, `hi` and `carry` are appropriately range checked. +#render_constraint_table(chip, config, groups: "def") + +== Product +@mul:c:raw_product defines `raw_product` in terms of the (sign extended) input values `lhs` and `rhs`. +#render_constraint_table(chip, config, groups: "prod") + +== Lookup +The #mul chip contributes the following to the lookup: +#render_constraint_table(chip, config, groups: "lookup") + += Padding + +The table can be padded to the next power of two with the following value assignments: + +#render_chip_padding_table(chip, config) + += Notes/optimizations +- `lo` and `hi` are stored in `DWordHL`s (rather than `DWordWL`s) because of their values being range checked. + Since it is not required that both `μ_lo` and `μ_hi` are non-zero at the same time, one cannot safely assume their range to be checked elsewhere. +- As an optimization, one might be able to use a `DWordWL` and `DWordHL` to store `lo` and `hi`, + where one would decide which to store in which based on the multiplicities `μ_lo` and `μ_hi`; + the value sent into the lookup could then be assumed range-checked by the other side of the relation. + This optimization was not included at this moment because of its negative impact on the readability and verifiability of the chip. diff --git a/spec/neg.typ b/spec/neg.typ new file mode 100644 index 000000000..336152892 --- /dev/null +++ b/spec/neg.typ @@ -0,0 +1,70 @@ +#import "/book.typ": book-page, aside, et +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_variable_table, render_chip_assumptions, render_constraint_table, compute_nr_interactions, + +#let config = load_config() +#let chip = load_chip("src/neg.toml", config) +#show: book-page(chip.name) + +#let nr_interactions = compute_nr_interactions(chip) + +#let neg = raw(chip.name) + +#neg is a constraint template that is used to assert that $#`neg` = -#`x`$, under the condition that `cond` is non-zero. +It requires `cond` to be a bit. + += Variables +This template introduces #nr_interactions interaction(s). +#render_chip_variable_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Constraints +We constrain this equality using two constraints: +#render_constraint_table(chip, config) + +== Correctness argument +The constraints force the `carry` values to be fixed. +Writing `carry`'s definition, we then find that +$ + #`neg`_0 &= 2^32 dot #`carry`_0 - (#`x as DWordWL`)_0 + = cases( + 2^32 - (#`x as DWordWL`)_0 & "if" (#`x as DWordWL`)_0 != 0, + 0 & "if" (#`x as DWordWL`)_0 = 0 + ),\ + #`neg`_1 &= 2^32 dot #`carry`_1 - (#`x as DWordWL`)_1 - #`carry`_0 = cases( + 2^32 - (#`x as DWordWL`)_1 - 1 & "if" #`x` != 0, + 0 & "if" #`x` = 0 + ) +$ +Clearly, $#`neg` = 0$ when $#`x` = 0$ (and `cond` is set). +For non-zero `x`, we distinguish two cases. +When $(#`x as DWordWL`)_0 = 0$, +$ + #`neg` + &= 2^32 dot #`neg`_1 + #`neg`_0\ + &= 2^32 dot (2^32 - (#`x as DWordWL`)_1) + 0\ + &= 2^32 dot (2^32 - (#`x as DWordWL`)_1) + (#`x as DWordWL`)_0\ + &= 2^64 - (2^32 dot (#`x as DWordWL`)_1 + (#`x as DWordWL`)_0)\ + &= 2^64 - #`x`\ + &equiv -x mod 2^64, +$ +while when $(#`x as DWordWL`)_0 != 0$, +$ + #`neg` + &= 2^32 dot #`neg`_1 + #`neg`_0\ + &= 2^32 dot (2^32 - (#`x as DWordWL`)_1 - 1) + (2^32 - (#`x as DWordWL`)_0) \ + &= 2^64 - 2^32 dot (#`x as DWordWL`)_1 - 2^32 + 2^32 - (#`x as DWordWL`)_0 \ + &= 2^64 - ((#`x as DWordWL`)_0 + 2^32 dot (#`x as DWordWL`)_1) \ + &= 2^64 - #`x`\ + &equiv -x mod 2^64 +$ +when `cond` is set. +When `cond` is not set, the two lookups are not executed, allowing `neg` to take any value in either case. + +#aside("Missing range check?")[ + It is worth noting that this construction does _not_ require the limbs of `neg` to be range checked, + thus allowing it be represented by the unrangecheckable `DWordWL` rather than a `DWordHL`. + The input value `x` is still assumed to be range-checked, however. +] diff --git a/spec/shift.typ b/spec/shift.typ new file mode 100644 index 000000000..c464d5d55 --- /dev/null +++ b/spec/shift.typ @@ -0,0 +1,168 @@ +#import "/book.typ": book-page, et +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + compute_nr_interactions, + render_constraint_table, + render_chip_assumptions, + render_chip_padding_table, +) + +#let config = load_config() +#let chip = load_chip("src/shift.toml", config) + +#let shift = raw(chip.name) + +#show: book-page(chip.name) + +The #shift chip is designed to constrain that +$ +#`shifted` := cases( + #`in` #`<<` #`s` " if" #`direction` = 0, + #`in` #`>>` #`s` " if" #`direction` = 1 and #`signed` = 0, + #`in` #`>>>` #`s` "if" #`direction` = 1 and #`signed` = 1, +) +$ +where +$ +#`s` := cases( + #`shift` mod 32 "if" #`word_instr` = 1, + #`shift` mod 64 "if" #`word_instr` = 0, +) +$ +Here, `<<` and `>>` denote the _logical_ left and right shift operations, while `>>>` denotes the _arithmetic_ right shift operation. + += Variables +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The `SHIFT` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Explanation +This chip has a rather complex design as a result of designing it to fit in as few columns possible. +We briefly discuss the intricacies of the design, attempting to illustrate its correctness. + +The chip's design revolves around a two-phase shifting process: +1. shift `in` by $x := #`shift` mod 16$ bits, +2. shift that result by $(#`shift`-x) mod 64$ (or $mod 32$ if $ #`word_instr` = 1$). +The intermediate value representing the state between the two phases is stored in the scratch variables `X` and `Y`. +The definition of `shifted` describes how one can combine the `X`, `Y` and `extension` variables to construct the output value as described using `Half`-limbs. +The output variable `out` is equivalent to `shifted`, but expressed using `Word`-limbs. + +In the following, we cover how these two phases were designed to complement one another. +Here, we start with discussing the _logical_ left/right shift operations only; the modifications required to compute the _arithmetic_ right shift will be discussed at the end. + +== First phase +We zoom in on the first step. +Here, we make use of the lookup operation `HWSL` (short for "HalfWord Shift Left"): +$ #`HWSL[x: Half, y: B4]` := [(#`x` #`<<` #`y`) mod 2^16, #`x` #`>>` (16 - #`y`)]. $ +One can use this to compute `out: Half[4] := in << y` as: +$ + #`out[`i#`]` = cases( + #`HWSL[in[`0#`], y]`_0 &"if" i = 0, + #`HWSL[in[`i#`], y]`_0 | #`HWSL[in[`i-1#`], y]`_1 &"if" i in [1, 3] + ) +$ +as long as $#`y` < 16$. +Observing that +$#`HWSL[x,` 16-#`y]`_0 = (#`x` #`<<` (16-#`y`)) mod 2^16$, and +$#`HWSL[x,` 16-#`y]`_1 = #`x` #`>>` #`y`$ for $#`y` in [1, 15]$, +one can also use it to compute `out := in >> y` as +$ + #`out[`i#`]` = cases( + #`HWSL[in[`i#`],` 16-#`y]`_1 | #`HWSL[in[`i+1#`], y]`_0 &"if" i in [0, 2], + #`HWSL[in[`3#`],` 16-#`y]`_1 &"if" i = 3 + ) +$ +as long as $0 < #`y` < 16$. + +Observe now that the values being looked up are (almost) independent from the direction of the shift: only the shift-amount varies slightly. +When we now define +$ + #`bit_shift` := cases( + #`shift` mod 16 & "when shifting left", + (16-#`shift`) mod 16 & "when shifting right" + ), +$ +it only takes some rearranging and combining of the values $#`X[`i#`] := HWSL[in[`i#`], bit_shift]`_0$ and $#`Y[`i#`] := HWSL[in[`i#`], bit_shift]`_1$ to form the limbs of $#`in <> shift` mod 16$. +In the remaining case that $#`right` = 1$ and $#`shift` = 0 mod 16$, the limbs of $#`in <> shift` mod 16$ simply match those of `in`. + +== Second phase +Since we're operating on 16-bit limbs, all the limbs in $#`in <> shift`$ must also occur somewhere in $#`in <> shift` mod 16$. +The number of full-limbs we still need to shift is determined by the fifth and sixth least significant bit of `shift`. +With `limb_shift` containing a unary decoding of the integer represented by these two bits, we find that the intermediate value needs to be shifted over by $i$ limbs (to the `left` or `right`) when $#`limb_shift[`i#`]` = 1$. +These things combined yield `shifted`'s definition. + +Of course, when $#`word_instr` = 1$ and, thus, only $#`shift` mod 32$ should be considered, the bit-mask for the lookup constraining `limb_shift` is adjusted appropriately (see @shift:c:limb_shift_lookup). + +== Arithmetic right shift +Lastly, we discuss the case of performing the _arithmetic_ right shift. +Here, `extension` is constrained to contain a repetition of `in`'s most significant bit. +Copies of this variable are used for any full limbs shifted in when $#`right` = #`signed` = 1$. +Moreover, `X[4]` contains a copy of `extension` shifted over by the right number of bits, to allow the construction of $#`in >>> shift` mod 16$ as the appropriate intermediate. + += Constraints +First, we constrain `bit_shift` based on whether we are left or right-shifting. +@shift:c:zbs makes sure `zbs` is set to `1` if and only if `bit_shift = 0`. +This flag is used to indicate the special case that $#`right` = 1$ and $#`shift` = 0 mod 16$. +#render_constraint_table(chip, config, groups: "bit_shift") + +Next, we shift the limbs of `in` left and right by the appropriate amount, storing the results in `X` and `Y` respectively. +When `zbs = 1`, the output cannot be used to compose $#`in >>/>>> shift` mod 16$. +To resolve this, we override `Y[i] := in[i]` and `X[i] := 0` in this case. + +The case of `left`-shifting and $#`bit_shift` = 0$ will be used for padding rows. +To prevent unnecessary lookups in padding rows, we override $#`X[i]` := #`in[i]`$ and $#`Y[i]` := 0$ here. +#render_constraint_table(chip, config, groups: "intra_limb_shift") + +== Full-limb shifting +Next, we constrain that `limb_shift` is a proper unary encoding of the fifth (and sixth if $#`word_instr` = 0$) bit of `shift`. +For this to be the case, three requirements must be satisfied: ++ *unary(0)*: $#`limb_shift[`i#`]` in {0, 1}$ for $i in [0, 3]$, ++ *unary(1)*: $#`limb_shift[`i#`]` = 1$ for exactly one $i$, and ++ *proper encoding*: $#`limb_shift[`i#`]` = 1 <=> 1/16 (#`shift &` (48-32 dot #`word_instr`)) = i$ +The first requirement is enforced by constraint @shift:c:limb_shift_is_bit. +To construct a constraint for the second and third requirement, observe that +$ +1/16 dot (#`shift &` (48-32 dot #`word_instr`)) in cases( + {0, 1, 2, 3} &"if" #`word_instr` = 0, + {0, 1} &"if" #`word_instr` = 1 +) +$ +Observe moreover that, assuming *unary(0)*, the expression +$ + 1/16 dot (1 + sum_(i=0)^3 (16i-1) dot #`limb_shift[`i#`]`) +$ +can evaluate to $i$ if and only if $#`limb_shift[`i#`]` = 1$, while the others are $0$. +This means that the relation +$ + 1 + sum_(i=0)^3 (16i-1) dot #`limb_shift[`i#`]` = #`shift &` (48-32 dot #`word_instr`) +$ +enforces both *unary(1)* and *proper encoding*. +This is the exact relation @shift:c:limb_shift_lookup enforces. + + +Hereafter, one must only check that `out` is the proper cast of `shifted` into a `DWordWL`. +#render_constraint_table(chip, config, groups: "limb_shifting") + +== Miscellaneous +#render_constraint_table(chip, config, groups: ("left_flag", "is_negative")) +*Note*: `is_negative` is not used when `signed = 0`. +As such, there is no problem with it being unconstrained in this case. + +== Lookups +This chip adds the following interaction to the lookup. +#render_constraint_table(chip, config, groups: "lookups") + += Padding + +The table can be padded to the next power of two with the following value assignments: + +#render_chip_padding_table(chip, config) diff --git a/spec/sign.typ b/spec/sign.typ new file mode 100644 index 000000000..14a6e4000 --- /dev/null +++ b/spec/sign.typ @@ -0,0 +1,33 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_variable_table, total_nr_variables, render_chip_assumptions, render_constraint_table, compute_nr_interactions, + +#let config = load_config() +#let chip = load_chip("src/sign.toml", config) +#show: book-page(chip.name) + +#let nr_variables = total_nr_variables(chip) +#let nr_interactions = compute_nr_interactions(chip) + +#let sign = raw(chip.name) + +#sign is a constraint template that is used to extract a `Half`word's sign. +It constrains that `sign` is set to `1` when both `X`'s most significant bit and `signed` are $1$, and $0$ otherwise. + += Variables +The #sign template introduces #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) + += Assumptions +The #sign template operates on the following assumptions: +#render_chip_assumptions(chip, config) + +If `sign` is set to $1$, `X` will be range-checked to be a halfword, and hence proving may fail if this is not ensured. + += Constraints +It takes only two constraints to compute the `sign` of `X`, given whether `X` represents a `signed` value or not. +When $#`signed` = 1$, the sign of `X` is equal to its most significant bit. +This value is extracted in @sign:c:sign_if_signed. +If `X` is unsigned (i.e., $#`signed` = 0$), its sign is always $0$. +This is constrained by @sign:c:sign_if_unsigned. +#render_constraint_table(chip, config) diff --git a/spec/signatures.typ b/spec/signatures.typ new file mode 100644 index 000000000..12d84f757 --- /dev/null +++ b/spec/signatures.typ @@ -0,0 +1,84 @@ +#import "/book.typ": book-page +#import "/src.typ": load_signatures, load_config + +#show: book-page("signatures.typ") + +#let config = load_config() +#let signatures = load_signatures(config) + +// Render a signature +#let render_signature(sig) = { + let (lb, rb) = if sig.kind == "interaction" { + (`[`, `]`) + } else if sig.kind == "template" { + (`<`, `>`) + } + + let cond = sig.at("cond", default: none) + let cond_str = if cond != none { + raw(cond) + ` => ` + } else {``} + + let input_str = sig.input.map(elt => { + if type(elt) == array { + raw(elt.at(0)) + `[` + raw(str(elt.at(1))) + `]` + } else { + raw(elt) + } + }).join(`, `) + + let output = sig.at("output", default: none) + let output_str = if output != none { + if type(output) == array { + raw(output.at(0)) + `[` + raw(str(output.at(1))) + `]` + } else { + raw(output) + } + `; ` + } else {``} + + return [#cond_str#raw(sig.tag)#lb#output_str#input_str#rb] +} + +// Compute the bus size of an interaction +#let interaction_bus_size(sig) = { + let vars = sig.input + if "output" in sig { (sig.output, )} else {()} + + return vars.map(v => { + let (label, factor) = if type(v) == array { + (v.at(0), v.at(1)) + } else { + (v, 1) + } + config.variables.types.filter(type => type.label == label).first().subtypes.len() * factor + }) + .sum() +} + +#let interactions = signatures.signatures.filter(s => s.kind == "interaction") +The following lists signatures of the #interactions.len() interactions in this VM. +#figure(table( + columns: (1fr, auto), + inset: 7pt, + align: (top+left, center), + stroke: none, + table.header([*Signature*], [*Bus size*]), + table.hline(stroke: 1pt), + table.vline(stroke: 1pt, x: 1), + ..for sig in interactions { + ([#render_signature(sig)], [#interaction_bus_size(sig)]) + }, +)) + +#let templates = signatures.signatures.filter(s => s.kind == "template") +Below, we list the signatures of the #templates.len() templates in this VM. +#figure(table( + columns: 1fr, + inset: 7pt, + align: (top+left, center), + stroke: none, + table.header([*Signature*]), + table.hline(stroke: 1pt), + ..for sig in templates { + ([#render_signature(sig)], ) + }, +)) diff --git a/spec/src.typ b/spec/src.typ new file mode 100644 index 000000000..6328c4665 --- /dev/null +++ b/spec/src.typ @@ -0,0 +1,126 @@ +/// Path to the config file. +#let CONFIG_PATH = "src/config.toml" +/// Path to the signatures file +#let SIGNATURES_PATH = "src/signatures.toml" + +/// Check the configuration object for internal consistency. +#let _check_config(config) = { + // Check that variable subtypes are listed, or "none" + let types = config.variables.types + for type in types { + for subtype in type.subtypes { + assert( + subtype in types.map(type => type.label), + message: "subtype '" + subtype + "' does not exist.", + ) + } + } + + // Check that `instantiated` variables are a subset of `all` + let categories = config.variables.categories + for category in categories.instantiated { + assert( + category in categories.all, + message: "category '" + category + "' part of `instantiated`, but not `all`.", + ) + } +} + +/// Load the configuration file. +#let load_config() = { + let config = toml(CONFIG_PATH) + _check_config(config) + return config +} + + +// Validate the `signatures` overview +#let _check_signatures(signatures, config) = { + let var_labels = config.variables.types.map(t => t.label) + + // Verify that `var` is a valid variable. + let verify_variable(var) = { + if type(var) == array { + assert(var.at(0) in var_labels, message: "Invalid var type: " + repr(var)) + assert(type(var.at(1)) == int, message: "Invalid var type: " + repr(var)) + } else if type(var) == str { + assert(var in var_labels, message: "Invalid var type: " + repr(var)) + } else { + assert(false, message: "Invalid var type: " + repr(var)) + } + } + + assert("signatures" in signatures, message: "No signatures listed") + for sig in signatures.signatures { + assert("tag" in sig, message: "No tag associated with " + repr(sig)) + assert(type(sig.tag) == str, message: "Tag is not of type str: " + repr(sig.tag)) + + assert("kind" in sig, message: "No kind associated with " + repr(sig)) + assert(type(sig.kind) == str, message: "kind is not of type str: " + repr(sig.kind)) + assert(sig.kind in ("interaction", "template"), message: "Invalid kind: " + repr(sig.kind)) + + if "cond" in sig { + assert(sig.kind != "interaction", message: "Invalid condition for interaction: " + repr(sig)) + verify_variable(sig.cond) + } + + assert("input" in sig, message: "No input associated with " + repr(sig)) + assert(type(sig.input) == array, message: "Invalid input type: " + repr(sig.input)) + sig.input.map(i => verify_variable(i)) + + if "output" in sig { + verify_variable(sig.output) + } + } +} + +// Load the signatures from file +#let load_signatures(config) = { + let signatures = toml(SIGNATURES_PATH) + _check_signatures(signatures, config) + return signatures +} + + +/// Check a chip object for internal consistency. +#let _check_chip(chip, config) = { + // Check that all variable categories are valid + for category in chip.variables.keys() { + assert( + category in config.variables.categories.all, + message: "invalid category: " + repr(category) + ) + } + + // Check that `def` is only contained in `virtual` variables + let non_virtual_vars = chip.variables.pairs().filter(x => x.first() != "virtual").map(x => x.last()).flatten(); + for var in non_virtual_vars { + assert( + "def" not in var, + message: "illegal `def` in non-virtual var: " + repr(var.name) + ) + } + + let all_vars = chip.variables.values().flatten() + let all_labels = config.variables.types.map(type => type.label); + for var in all_vars { + let type_label = if type(var.type) == array { + var.type.at(0) + } else { + var.type + } + + // Check that all variable types are valid + assert(type_label in all_labels, message: "found invalid var type:" + repr(var.type)) + } +} + +/// Load a chip object from file +/// +/// - path(str): path to file containing chip data +/// - config: configuration data this chip needs to match with +#let load_chip(path, config) = { + let chip = toml(path) + _check_chip(chip, config) + return chip +} diff --git a/spec/src/add.toml b/spec/src/add.toml new file mode 100644 index 000000000..c928a8b32 --- /dev/null +++ b/spec/src/add.toml @@ -0,0 +1,62 @@ +name = "ADD" + +# Variables + +[[variables.condition]] +name = "cond" +type = "BaseField" +desc = "Whether the relation should be enforced ($eq.not 0$) or not ($0$)." + +[[variables.input]] +name = "lhs" +type = "DWordWL" +desc = "left-hand operator" + +[[variables.input]] +name = "rhs" +type = "DWordWL" +desc = "right-hand operator" + +[[variables.output]] +name = "sum" +type = "DWordWL" +desc = "$#`lhs` + #`rhs`$" + +[[variables.virtual]] +name = "carry" +desc = "Carry values used to constrain the addition" +type = ["Bit", 2] +def = {idx="i", polys=[ + {iter=0, poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "lhs", 0], ["idx", "rhs", 0]], ["idx", "sum", 0]]]}, + {iter=1, poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "lhs", 1], ["idx", "rhs", 1], ["idx", "carry", 0]], ["idx", "sum", 1]]]}, +]} + +# Assumptions + +[[assumptions]] +desc = "`IS_WORD[lhs[i]]`" +iter = ["i", 0, 1] +ref = "add:a:lhs" + +[[assumptions]] +desc = "`IS_WORD[rhs[i]]`" +iter = ["i", 0, 1] +ref = "add:a:rhs" + +[[assumptions]] +desc = "`IS_WORD[sum[i]]`" +iter = ["i", 0, 1] +ref = "add:a:sum" + +# Constraints + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "carry", "i"]] +iter = ["i", 0, 1] +cond = "cond" +ref = "add:c:carry" diff --git a/spec/src/bitwise.toml b/spec/src/bitwise.toml new file mode 100644 index 000000000..67d73facd --- /dev/null +++ b/spec/src/bitwise.toml @@ -0,0 +1,188 @@ +name = "BITWISE" + +[[variables.input]] +name = "X" +type = "Byte" +desc = "" +precomputed = true + +[[variables.input]] +name = "Y" +type = "Byte" +desc = "" +precomputed = true + +[[variables.input]] +name = "Z" +type = "B4" +desc = "" +precomputed = true + +[[variables.output]] +name = "AND" +type = "Byte" +desc = "the binary AND of `X` and `Y`" +precomputed = true + +[[variables.output]] +name = "OR" +type = "Byte" +desc = "the binary OR of `X` and `Y`" +precomputed = true + +[[variables.output]] +name = "XOR" +type = "Byte" +desc = "the binary XOR of `X` and `Y`" +precomputed = true + +[[variables.output]] +name = "MSB8" +type = "Bit" +desc = "the most significant bit of `X`" +precomputed = true + +[[variables.output]] +name = "MSB16" +type = "Bit" +desc = "the most significant bit of `Y`" +precomputed = true + +[[variables.output]] +name = "ZERO" +type = "Bit" +desc = "whether $#`X` = 0$, $#`Y` = 0$ and $#`Z` = 0$." +precomputed = true + +[[variables.output]] +name = "SLL" +type = "Half" +desc = "`X||Y` logically left-shifted by `Z`: $((#`X` + 256#`Y`) #`<<` #`Z`) mod 2^16$" +precomputed = true + +[[variables.output]] +name = "SLLC" +type = "Half" +desc = "`X||Y` logically right-shifted by `Z`: $(#`X` + 256#`Y`) #`>>` (16 - #`Z`)$" +precomputed = true + +[[variables.multiplicity]] +name = "μ_AND" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_OR" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_XOR" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_MSB8" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_MSB16" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_ZERO" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_IS_BYTE" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_IS_HALF" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_IS_B20" +type = "BaseField" +desc = "" + +[[variables.multiplicity]] +name = "μ_HWSL" +type = "BaseField" +desc = "" + + +[[constraint_groups]] +name = "contributions" + +[[constraints.contributions]] +kind = "interaction" +tag = "AND_BYTE" +input = ["X", "Y"] +output = "AND" +multiplicity = ["-", "μ_AND"] + +[[constraints.contributions]] +kind = "interaction" +tag = "OR_BYTE" +input = ["X", "Y"] +output = "OR" +multiplicity = ["-", "μ_OR"] + +[[constraints.contributions]] +kind = "interaction" +tag = "XOR_BYTE" +input = ["X", "Y"] +output = "XOR" +multiplicity = ["-", "μ_XOR"] + +[[constraints.contributions]] +kind = "interaction" +tag = "MSB8" +input = ["X"] +output = "MSB8" +multiplicity = ["-", "μ_MSB8"] + +[[constraints.contributions]] +kind = "interaction" +tag = "MSB16" +input = [["+", "X", ["*", 256, "Y"]]] +output = "MSB16" +multiplicity = ["-", "μ_MSB16"] + +[[constraints.contributions]] +kind = "interaction" +tag = "ZERO" +input = [["+", "X", ["*", 256, "Y"], ["*", 65536, "Z"]]] +output = "ZERO" +multiplicity = ["-", "μ_ZERO"] + +[[constraints.contributions]] +kind = "interaction" +tag = "IS_BYTE" +input = ["X"] +multiplicity = ["-", "μ_IS_BYTE"] + +[[constraints.contributions]] +kind = "interaction" +tag = "IS_HALF" +input = [["+", "X", ["*", 256, "Y"]]] +multiplicity = ["-", "μ_IS_HALF"] + +[[constraints.contributions]] +kind = "interaction" +tag = "IS_B20" +input = [["+", "X", ["*", 256, "Y"], ["*", 65536, "Z"]]] +multiplicity = ["-", "μ_IS_B20"] + +[[constraints.contributions]] +kind = "interaction" +tag = "HWSL" +input = [["+", "X", ["*", 256, "Y"]], "Z"] +output = ["arr", "SLL", "SLLC"] +multiplicity = ["-", "μ_HWSL"] diff --git a/spec/src/branch.toml b/spec/src/branch.toml new file mode 100644 index 000000000..a98974678 --- /dev/null +++ b/spec/src/branch.toml @@ -0,0 +1,148 @@ +name = "BRANCH" + + +# Input + +[[variables.input]] +name = "pc" +type = "DWordWL" +desc = "The current pc, used as base address when `!JALR`" +pad = 0 + +[[variables.input]] +name = "offset" +type = "DWordWL" +desc = "The offset from the base address to jump to" +pad = 0 + +[[variables.input]] +name = "register" +type = "DWordWL" +desc = "The base address to use when `JALR`" +pad = 0 + +[[variables.input]] +name = "JALR" +type = "Bit" +desc = "Selects between `pc` and `register` as base address, needed for the `JALR` instruction" +pad = 0 + + +# Output + +[[variables.output]] +name = "next_pc_high" +type = ["Half", 3] +desc = "The upper part of the next pc" +pad = ["arr", 0, 0, 0] + +[[variables.output]] +name = "next_pc_low" +type = ["Byte", 2] +desc = "The lower part of the next pc" +pad = 0 + + +# Auxiliary + +[[variables.auxiliary]] +name = "unmasked_low_byte" +type = "Byte" +desc = "The low byte of the next pc, before masking the LSB. Used to constraint the raw addition." +pad = 0 + + +# Virtual + +[[variables.virtual]] +name = "next_pc_unmasked" +type = "DWordWL" +desc = "The combination of `next_pc_high`, `next_pc_low[1]` and `unmasked_low_byte` to constrain the addition. This is the computed value for the next pc, before masking off the LSB as required by the ISA." +def = {idx = "i", polys = [ + {iter = 0, poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], "unmasked_low_byte"]}, + {iter = 1, poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 2]], ["idx", "next_pc_high", 1]]}, +]} + +[[variables.virtual]] +name = "next_pc" +type = "DWordWL" +desc = "The computed next pc, after masking off the LSB as required by the ISA." +def = {idx = "i", polys = [ + {iter = 0, poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], ["idx", "next_pc_low", 0]]}, + {iter = 1, poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 2]], ["idx", "next_pc_high", 1]]}, +]} + + +# Multiplicity + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" +pad = 0 + + +[[assumptions]] +desc = "`pc` is range checked, `IS_WORD[pc[i]]`" +iter = ["i", 0, 1] + +[[assumptions]] +desc = "`offset` is range checked, `IS_WORD[offset]`" + +[[assumptions]] +desc = "`register` is range checked, `IS_WORD[register[i]]`" +iter = ["i", 0, 1] + +[[assumptions]] +desc = "`IS_BIT`" + + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "template" +tag = "ADD" +input = ["pc", ["cast", "offset", "DWordWL"]] +output = "next_pc_unmasked" +cond = ["not", "JALR"] + +[[constraints.all]] +kind = "template" +tag = "ADD" +input = ["register", ["cast", "offset", "DWordWL"]] +output = "next_pc_unmasked" +cond = "JALR" + +[[constraints.all]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "next_pc_low", 1]] +multiplicity = "μ" + +[[constraints.all]] +kind = "interaction" +tag = "AND_BYTE" +input = ["unmasked_low_byte", 254] +output = ["idx", "next_pc_low", 0] +multiplicity = "μ" + +[[constraints.all]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "next_pc_high", "i"]] +iter = ["i", 0, 2] +multiplicity = "μ" + + +[[constraint_groups]] +name = "output" +desc = "Each row contributes the following to the LogUp sum" + +[[constraints.output]] +kind = "interaction" +tag = "BRANCH" +input = ["pc", "offset", "register", "JALR"] +output = "next_pc" +multiplicity = ["-", "μ"] diff --git a/spec/src/commit.toml b/spec/src/commit.toml new file mode 100644 index 000000000..89fa133c6 --- /dev/null +++ b/spec/src/commit.toml @@ -0,0 +1,221 @@ +name = "COMMIT" + +# Variables + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "timestamp at which to commit" +pad = 0 + +[[variables.auxiliary]] +name = "index" +type = "BaseField" +desc = "Index of value being committed." +pad = 0 + +[[variables.auxiliary]] +name = "address" +type = "DWordWL" +desc = "Address of first byte to commit." +pad = ["arr", 0, 0, 0, 0] + +[[variables.auxiliary]] +name = "address_incr" +type = "DWordHL" +desc = "$#`address` + 1$" +pad = ["arr", 1, 0, 0, 0] + +[[variables.auxiliary]] +name = "count" +type = "DWordWL" +desc = "number of bytes to commit" +pad = ["arr", 1, 0, 0, 0] + +[[variables.auxiliary]] +name = "count_decr" +type = "DWordHL" +desc = "$#`count` - 1$" +pad = ["arr", 0, 0, 0, 0] + +[[variables.auxiliary]] +name = "first" +type = "Bit" +desc = "Whether this is the first commitment in this sequence." +pad = 0 + +[[variables.auxiliary]] +name = "end" +type = "Bit" +desc = "Whether this is the end of the commitment sequence." +pad = 0 + +[[variables.auxiliary]] +name = "value" +type = "Byte" +desc = "Byte stored at `address`." +pad = 0 + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" +pad = 0 + +# Assumptions + + +# Constraints + +[[constraint_groups]] +name = "incoming" + +[[constraints.incoming]] +kind = "interaction" +tag = "ECALL" +input = ["timestamp", ["cast", 64, "DWordWL"]] +multiplicity = ["-", "first"] +ref = "commit:c:receive_ecall" + +[[constraint_groups]] +name = "read_input" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 11], "DWordWL"], ["arr", ["idx", "address", 0], ["idx", "address", 1], 0, 0, 0, 0, 0, 0], "timestamp", 1, 0, 0] +output = ["arr", ["idx", "address", 0], ["idx", "address", 1], 0, 0, 0, 0, 0, 0] +multiplicity = "first" +ref = "commit:c:read_address" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 12], "DWordWL"], ["arr", ["idx", "count", 0], ["idx", "count", 1], 0, 0, 0, 0, 0, 0], "timestamp", 1, 0, 0] +output = ["arr", ["idx", "count", 0], ["idx", "count", 1], 0, 0, 0, 0, 0, 0] +multiplicity = "first" +ref = "commit:c:read_count" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 10], "DWordWL"], ["arr", ["idx", "count", 0], ["idx", "count", 1], 0, 0, 0, 0, 0, 0], "timestamp", 1, 0, 0] +output = ["arr", 1, 0, 0, 0, 0, 0, 0, 0] +multiplicity = "first" +ref = "commit:c:read_fd_write_count" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 254], "DWordWL"], ["arr", ["+", "index", ["cast", "count", "BaseField"]], 0, 0, 0, 0, 0, 0, 0], "timestamp", 0, 0, 0] +output = ["arr", "index", 0, 0, 0, 0, 0, 0, 0] +multiplicity = "first" +ref = "commit:c:read_index" + + +[[constraint_groups]] +name = "incr_decr" + +[[constraints.incr_decr]] +kind = "template" +tag = "ADD" +input = ["address", ["cast", 1, "DWordWL"]] +output = ["cast", "address_incr", "DWordWL"] +ref = "commit:c:address_incr" + +[[constraints.incr_decr]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "address_incr", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ" +ref = "commit:c:range_address_incr" + +[[constraints.incr_decr]] +kind = "template" +tag = "SUB" +input = ["count", ["cast", 1, "DWordWL"]] +output = ["cast", "count_decr", "DWordWL"] +ref = "commit:c:count_decr" + +[[constraints.incr_decr]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "count_decr", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ" +ref = "commit:c:range_count_decr" + + +[[constraint_groups]] +name = "commit" + +[[constraints.commit]] +kind = "interaction" +tag = "MEMW" +input = [0, "address", ["arr", "value", 0, 0, 0, 0, 0, 0, 0], "timestamp", 0, 0, 0] +output = ["arr", "value", 0, 0, 0, 0, 0, 0, 0] +multiplicity = ["-", "μ", "end"] +ref = "commit:c:read_value" + +[[constraints.commit]] +kind = "interaction" +tag = "COMMIT" +input = ["index", "value"] +multiplicity = ["-", "μ", "end"] +ref = "commit:c:commit_value" + +[[constraint_groups]] +name = "end" + +[[constraints.end]] +kind = "interaction" +tag = "ZERO" +input = [["+", ["-", 0xFFFF, ["idx", "count_decr", 0]], ["-", 0xFFFF, ["idx", "count_decr", 1]], ["-", 0xFFFF, ["idx", "count_decr", 2]], ["-", 0xFFFF, ["idx", "count_decr", 3]]]] +output = "end" +multiplicity = "μ" +ref = "commit:c:end" + +[[constraint_groups]] +name = "bits" + +[[constraints.bits]] +kind = "template" +tag = "IS_BIT" +input = ["first"] +ref = "commit:c:range_first" + +[[constraints.bits]] +kind = "template" +tag = "IS_BIT" +input = ["end"] +ref = "commit:c:range_end" + +[[constraints.bits]] +kind = "template" +tag = "IS_BIT" +input = ["μ"] +ref = "commit:c:range_mu" + +[[constraints.bits]] +kind = "arith" +constraint = "$#`first` + #`end` => #`μ` = 1$" +poly = ["*", ["+", "first", "end"], ["not", "μ"]] +ref = "commit:c:first_or_end_implies_mu" + +[[constraint_groups]] +name = "lookups" + +[[constraints.lookups]] +kind = "interaction" +tag = "CNB" +input = ["timestamp", ["+", "index", 1], ["cast", "address_incr", "DWordWL"], ["cast", "count_decr", "DWordWL"]] +multiplicity = ["-", "μ", "end"] +ref = "commit:c:send_commit_next_byte" + +[[constraints.lookups]] +kind = "interaction" +tag = "CNB" +input = ["timestamp", "index", "address", "count"] +multiplicity = ["-", ["-", "μ", "first"]] +ref = "commit:c:receive_commit_next_byte" diff --git a/spec/src/config.toml b/spec/src/config.toml new file mode 100644 index 000000000..d9dcaec37 --- /dev/null +++ b/spec/src/config.toml @@ -0,0 +1,124 @@ +[metadata] +version = 1 + +[[variables.types]] +label = "BaseField" +subtypes = ["BaseField"] +range = [0, "18446744069414584320"] +desc = "Variable that can assume any value in the base field." + +[[variables.types]] +label = "Bit" +subtypes = ["BaseField"] +range = [0, 1] +desc = "Variable that can only assume values in the set ${0,1}$." + +[[variables.types]] +label = "B4" +subtypes = ["BaseField"] +range = [0, 15] +desc = "Variable that can only assume values in the range $[0, 2^4)$." + +[[variables.types]] +label = "Byte" +subtypes = ["BaseField"] +range = [0, 255] +desc = "Variable that can only assume values in the range $[0, 2^8)$." + +[[variables.types]] +label = "Half" +subtypes = ["BaseField"] +range = [0, 65535] +desc = "Variable that can only assume values in the range $[0, 2^16)$." + +[[variables.types]] +label = "B20" +subtypes = ["BaseField"] +range = [0, 1048575] +desc = "Variable that can only assume values in the range $[0, 2^20)$." + +[[variables.types]] +label = "Word" +subtypes = ["BaseField"] +range = [0, 4294967295] +desc = "Variable that can only assume values in the range $[0, 2^32)$." + +[[variables.types]] +label = "B51" +subtypes = ["BaseField"] +range = [0, 2251799813685247] +desc = "Variable that can only assume values in the range $[0, 2^51)$." + +[[variables.types]] +label = "DWordBL" +subtypes = ["Byte", "Byte", "Byte", "Byte", "Byte", "Byte", "Byte", "Byte"] +desc = """\ + Variable that can only assume values in the range $[0, 2^64)$. \\ + Represented as an array of eight `Byte` variables.\ + """ + +[[variables.types]] +label = "DWordHL" +subtypes = ["Half", "Half", "Half", "Half"] +desc = """\ + Variable that can only assume values in the range $[0, 2^64)$. \\ + Represented as an array of four `Half` variables.\ + """ + +[[variables.types]] +label = "DWordWL" +subtypes = ["Word", "Word"] +desc = """\ + Variable that can only assume values in the range $[0, 2^64)$. \\ + Represented as an array of two `Word` variables.\ + """ + +[[variables.types]] +label = "DWordHHW" +subtypes = ["Word", "Half", "Half"] +desc = """\ + Variable that can only assume values in the range $[0, 2^64)$. \\ + Represented as a `Word` and two `Half` variables. \ + The `Word` is the *least* significant digit. + """ + +[[variables.types]] +label = "DWordWHH" +subtypes = ["Half", "Half", "Word"] +desc = """\ + Variable that can only assume values in the range $[0, 2^64)$. \\ + Represented as a `Word` and two `Half` variables. \ + The `Word` is the *most* significant digit. + """ + +[[variables.types]] +label = "QuadHL" +subtypes = ["Half", "Half", "Half", "Half", "Half", "Half", "Half", "Half"] +desc = """\ + Variable that can only assume values in the range $[0, 2^128)$. \\ + Represented as an array of eight `Half` variables.\ + """ + +[[variables.types]] +label = "QuadWL" +subtypes = ["Word", "Word", "Word", "Word"] +desc = """\ + Variable that can only assume values in the range $[0, 2^128)$. \\ + Represented as an array of four `Word` variables.\ + """ + +[[variables.types]] +label = "Timestamp" +subtypes = ["DWordWL"] +desc = "A preprocessed column holding timestamps as `DWordWL`. Row `i` of the column contains the value $2^2 dot (i + 1)$. Used in the CPU chip (@cpu), see there for more details about the magic number." +preprocessed = true + +[[variables.types]] +label = "RowIndex" +subtypes = ["Word"] +desc = "A preprocessed column holding the row index (zero-indexed)." +preprocessed = true + +[variables.categories] +all = ["constant", "input", "output", "auxiliary", "virtual", "multiplicity", "condition"] +instantiated = ["input", "output", "auxiliary", "multiplicity"] diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml new file mode 100644 index 000000000..a455b854f --- /dev/null +++ b/spec/src/cpu.toml @@ -0,0 +1,811 @@ +name = "CPU" + + +# Input +# Let's call the variables coming from DECODE input + +[[variables.input]] +name = "timestamp" +type = "Timestamp" +desc = "A preprocessed timestamp to coordinate the memory argument. Since we have at most 3 non-disjoint memory accesses (`(rs1, rs2, rd)`, `(rs1, pc, pc)`, `(LOAD)` or `(STORE)`) a maximum of 4 slots is enough." + +[[variables.input]] +name = "pc" +type = "DWordWL" +desc = "The program counter" +pad = 1 + +[[variables.input]] +name = "rs1" +type = "Byte" +desc = "Source register 1 index" +pad = 0 + +[[variables.input]] +name = "rs2" +type = "Byte" +desc = "Source register 2 index" +pad = 0 + +[[variables.input]] +name = "rd" +type = "Byte" +desc = "Destination register index" +pad = 0 + +[[variables.input]] +name = "read_register1" +type = "Bit" +desc = "Whether to read from `rs1` (1) or to place a 0 in `rv1` (0)" +pad = 0 + +[[variables.input]] +name = "read_register2" +type = "Bit" +desc = "Whether to read from `rs2` (1) or to place a 0 in `rv2` (0)" +pad = 0 + +[[variables.input]] +name = "write_register" +type = "Bit" +desc = "Whether to write back to the destination register" +pad = 0 + +# TODO: can we compress this to a single value? (1: is it worth it, 2: does it work) +[[variables.input]] +name = "memory_2bytes" +type = "Bit" +desc = "Whether the memory access (read or write) touches exactly 2 bytes" +pad = 0 + +[[variables.input]] +name = "memory_4bytes" +type = "Bit" +desc = "Whether the memory access (read or write) touches exactly 4 bytes" +pad = 0 + +[[variables.input]] +name = "memory_8bytes" +type = "Bit" +desc = "Whether the memory access (read or write) touches exactly 8 bytes" +pad = 0 + +# TODO: Are there usecases where it's nicer to just have this as a length constant? +[[variables.input]] +name = "c_type_instruction" +type = "Bit" +desc = "Whether the instruction is of C type, i.e., whether it is 2 bytes long instead of 4" +pad = 0 + +[[variables.input]] +name = "imm" +type = "DWordWL" +desc = "The fully extended 64-bit version of the immediate" +pad = 0 + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Indicates whether we're dealing with a signed or unsigned instruction" +pad = 0 + +[[variables.input]] +name = "mp_selector" +type = "Bit" +desc = """Multi-purpose selector used by different ALU operations for different purposes. Currently, it is used + - by the `MUL` chip to select between `MUL`/`MULH` and `MULH[S]U`, and + - as flag for inverting the condition of conditional branches (see `branch_cond`) + - as direction (left or right) for `SHIFT`""" +pad = 0 + +[[variables.input]] +name = "muldiv_selector" +type = "Bit" +desc = "Selects which output of `MUL` (lo/hi) or `DIV` (quo/rem) is wanted" +pad = 0 + +[[variables.input]] +name = "word_instr" +type = "Bit" +desc = "Whether the instruction is a \\*W instruction, requiring the inputs and outputs to be (sign) extended" +pad = 0 + +[[variables.input]] +name = "ADD" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "SUB" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "SLT" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "AND" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "OR" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "XOR" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "SHIFT" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "JALR" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "BEQ" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "BLT" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "LOAD" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "STORE" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "MUL" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "DIVREM" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "ECALL" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + +[[variables.input]] +name = "EBREAK" +type = "Bit" +desc = "One-hot ALU selector flag" +pad = 0 + + +# Output +[[variables.output]] +name = "next_pc" +type = "DWordWL" +desc = "The program counter for the next instruction" +pad = 5 + +[[variables.output]] +name = "rvd" +type = "DWordWL" +desc = "The value to (maybe) be written back to rvd" +pad = 0 + +# Auxiliary +[[variables.auxiliary]] +name = "rv1" +type = "DWordWHH" +desc = "The value of register `rs1`" +pad = 0 + +[[variables.auxiliary]] +name = "rv2" +type = "DWordWHH" +desc = "The value of register `rs2`" +pad = 0 + +[[variables.auxiliary]] +name = "rv1_ext_bit" +type = "Bit" +desc = "The sign bit of `rv1` if seen as a 32-bit word, used for sign extension with `word_instr`" +pad = 0 + +[[variables.auxiliary]] +name = "arg1" +type = "DWordBL" +desc = "The extended version of `rv1`, depending on `word_instr`" +pad = 0 + +[[variables.auxiliary]] +name = "rv2_ext_bit" +type = "Bit" +desc = "The sign bit of `rv2` if seen as a 32-bit word, used for sign extension with `word_instr`" +pad = 0 + +[[variables.auxiliary]] +name = "arg2" +type = "DWordBL" +desc = "A multiplexed version of `rv2` and `imm`, to be used as second argument to ALU calls" +pad = 0 + +[[variables.auxiliary]] +name = "res_ext_bit" +type = "Bit" +desc = "The sign bit of `res`, if seen as a 32-bit word, used for sign extension with `word_instr`" +pad = 0 + +[[variables.auxiliary]] +name = "res" +type = "DWordBL" +desc = "The ALU result" +pad = 0 + +[[variables.auxiliary]] +name = "is_equal" +type = "Bit" +desc = "Whether `rv1` and `arg2` are equal" +pad = 0 + +[[variables.auxiliary]] +name = "branch_cond" +type = "Bit" +desc = "Whether a branch is taken, i.e., the branch condition" +pad = 0 + +# Virtual +[[variables.virtual]] +name = "packed_decode" +type = "BaseField" +desc = "A packed representation of all bit flags and register indices obtained from the decoding" +def = ["+", + ["*", ["^", 2, 0], "read_register1"], + ["*", ["^", 2, 1], "read_register2"], + ["*", ["^", 2, 2], "write_register"], + ["*", ["^", 2, 3], "memory_2bytes"], + ["*", ["^", 2, 4], "memory_4bytes"], + ["*", ["^", 2, 5], "memory_8bytes"], + ["*", ["^", 2, 6], "c_type_instruction"], + ["*", ["^", 2, 7], "signed"], + ["*", ["^", 2, 8], "mp_selector"], + ["*", ["^", 2, 9], "muldiv_selector"], + ["*", ["^", 2, 10], "word_instr"], + ["*", ["^", 2, 11], "ADD"], + ["*", ["^", 2, 12], "SUB"], + ["*", ["^", 2, 13], "SLT"], + ["*", ["^", 2, 14], "AND"], + ["*", ["^", 2, 15], "OR"], + ["*", ["^", 2, 16], "XOR"], + ["*", ["^", 2, 17], "SHIFT"], + ["*", ["^", 2, 18], "JALR"], + ["*", ["^", 2, 19], "BEQ"], + ["*", ["^", 2, 20], "BLT"], + ["*", ["^", 2, 21], "LOAD"], + ["*", ["^", 2, 22], "STORE"], + ["*", ["^", 2, 23], "MUL"], + ["*", ["^", 2, 24], "DIVREM"], + ["*", ["^", 2, 25], "ECALL"], + ["*", ["^", 2, 26], "EBREAK"], + ["*", ["^", 2, 27], "rs1"], + ["*", ["^", 2, 35], "rs2"], + ["*", ["^", 2, 43], "rd"], +] + +[[variables.virtual]] +name = "pad" +type = "Bit" +desc = "When no flags are set, we must be in a padding row." +def = ["-", 1, "ADD", "SUB", "SLT", "AND", "OR", "XOR", "SHIFT", "JALR", "BEQ", "BLT", "LOAD", "STORE", "MUL", "DIVREM", "ECALL", "EBREAK"] + + +[[assumptions]] +desc = "At most one ALU selector flag is 1 by the decoding, and every other flag is 0." +ref = "cpu:a:one-hot" + +[[assumptions]] +desc = "When `STORE + LOAD + BEQ + BLT = 0`, either `rs2 = 0` or `imm = 0` should be enforced by the decoding. This is needed for `arg2`." +ref = "cpu:a:arg2-multiplex" + +[[constraint_groups]] +name = "decode" + +[[constraints.decode]] +kind = "interaction" +tag = "DECODE" +input = ["pc", "imm", "packed_decode"] +multiplicity = 1 + + +[[constraint_groups]] +name = "range" +prefix = "R" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["read_register1"] +ref = "cpu:c:range_read_register1" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["read_register2"] +ref = "cpu:c:range_read_register2" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["write_register"] +ref = "cpu:c:range_write_register" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["memory_2bytes"] +ref = "cpu:c:range_memory_2bytes" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["memory_4bytes"] +ref = "cpu:c:range_memory_4bytes" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["memory_8bytes"] +ref = "cpu:c:range_memory_8bytes" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["c_type_instruction"] +ref = "cpu:c:range_c_type_instruction" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["signed"] +ref = "cpu:c:range_signed" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["mp_selector"] +ref = "cpu:c:range_mp_selector" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["muldiv_selector"] +ref = "cpu:c:range_muldiv_selector" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["word_instr"] +ref = "cpu:c:range_word_instr" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["ADD"] +ref = "cpu:c:range_ADD" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["SUB"] +ref = "cpu:c:range_SUB" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["SLT"] +ref = "cpu:c:range_SLT" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["AND"] +ref = "cpu:c:range_AND" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["OR"] +ref = "cpu:c:range_OR" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["XOR"] +ref = "cpu:c:range_XOR" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["SHIFT"] +ref = "cpu:c:range_SHIFT" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["JALR"] +ref = "cpu:c:range_JALR" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["BEQ"] +ref = "cpu:c:range_BEQ" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["BLT"] +ref = "cpu:c:range_BLT" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["LOAD"] +ref = "cpu:c:range_LOAD" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["STORE"] +ref = "cpu:c:range_STORE" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["MUL"] +ref = "cpu:c:range_MUL" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["DIVREM"] +ref = "cpu:c:range_DIVREM" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["ECALL"] +ref = "cpu:c:range_ECALL" + +[[constraints.range]] +kind = "template" +tag = "IS_BIT" +input = ["EBREAK"] +ref = "cpu:c:range_EBREAK" + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = ["rs1"] +multiplicity = 1 + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = ["rs2"] +multiplicity = 1 + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = ["rd"] +multiplicity = 1 + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "arg1", "i"]] +iter = ["i", 0, 7] +multiplicity = 1 + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "arg2", "i"]] +iter = ["i", 0, 7] +multiplicity = 1 + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "res", "i"]] +iter = ["i", 0, 7] +multiplicity = 1 + + +[[constraint_groups]] +name = "alu" +prefix = "A" + +[[constraints.alu]] +kind = "template" +tag = "ADD" +cond = ["+", "ADD", "LOAD"] +input = [["cast", "arg1", "DWordWL"], ["cast", "arg2", "DWordWL"]] +output = ["cast", "res", "DWordWL"] + +[[constraints.alu]] +kind = "template" +tag = "ADD" +cond = "STORE" +input = [["cast", "arg1", "DWordWL"], "imm"] +output = ["cast", "res", "DWordWL"] + +[[constraints.alu]] +kind = "template" +tag = "SUB" +cond = ["+", "SUB", "BEQ"] +input = [["cast", "arg1", "DWordWL"], ["cast", "arg2", "DWordWL"]] +output = ["cast", "res", "DWordWL"] +ref = "cpu:c:sub" + +[[constraints.alu]] +kind = "interaction" +tag = "LT" +input = [["cast", "arg1", "DWordWL"], ["cast", "arg2", "DWordWL"], "signed"] +output = ["idx", "res", 0] +multiplicity = ["+", "SLT", "BLT"] + +[[constraints.alu]] +kind = "arith" +constraint = "$#`SLT` + #`BLT` => #`res[i]` = 0$" +poly = ["*", ["+", "SLT", "BLT"], ["idx", "res", "i"]] +iter = ["i", 1, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "AND_BYTE" +input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] +output = ["idx", "res", "i"] +multiplicity = "AND" +iter = ["i", 0, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "OR_BYTE" +input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] +output = ["idx", "res", "i"] +multiplicity = "OR" +iter = ["i", 0, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "XOR_BYTE" +input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] +output = ["idx", "res", "i"] +multiplicity = "XOR" +iter = ["i", 0, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "SHIFT" +input = [["cast", "arg1", "DWordHL"], ["idx", "arg2", 0], "mp_selector", "signed", "word_instr"] +output = ["cast", "res", "DWordWL"] +multiplicity = "SHIFT" + +[[constraints.alu]] +kind = "template" +tag = "ADD" +input = ["pc", ["*", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], ["cast", 1, "DWordWL"]]] +output = ["cast", "res", "DWordWL"] +cond = "JALR" + +[[constraints.alu]] +kind = "interaction" +tag = "MUL" +input = [["cast", "arg1", "DWordHL"], "signed", ["cast", "arg2", "DWordHL"], "mp_selector", "muldiv_selector"] +output = ["cast", "res", "DWordWL"] +multiplicity = "MUL" + +[[constraints.alu]] +kind = "interaction" +tag = "DVRM" +input = [["cast", "arg1", "DWordHL"], ["cast", "arg2", "DWordHL"], "signed", "muldiv_selector"] +output = ["cast", "res", "DWordWL"] +multiplicity = "DIVREM" + + +[[constraint_groups]] +name = "mem" +prefix = "M" + +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", ["cast", 2, "DWordWL"], "rs1"], ["arr", ["idx", ["cast", "rv1", "DWordWL"], 0], ["idx", ["cast", "rv1", "DWordWL"], 1], 0, 0, 0, 0, 0, 0], ["+", "timestamp", ["cast", 0, "DWordWL"]], 1, 0, 0] +output = ["arr", ["idx", ["cast", "rv1", "DWordWL"], 0], ["idx", ["cast", "rv1", "DWordWL"], 1], 0, 0, 0, 0, 0, 0] +multiplicity = "read_register1" + +[[constraints.mem]] +kind = "arith" +constraint = "$#`!read_register1` => #`rv1[i]` = 0$" +poly = ["*", ["not", "read_register1"], ["idx", "rv1", "i"]] +iter = ["i", 0, 2] + +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", ["cast", 2, "DWordWL"], "rs2"], ["arr", ["idx", ["cast", "rv2", "DWordWL"], 0], ["idx", ["cast", "rv2", "DWordWL"], 1], 0, 0, 0, 0, 0, 0], ["+", "timestamp", ["cast", 1, "DWordWL"]], 1, 0, 0] +output = ["arr", ["idx", ["cast", "rv2", "DWordWL"], 0], ["idx", ["cast", "rv2", "DWordWL"], 1], 0, 0, 0, 0, 0, 0] +multiplicity = "read_register2" + +[[constraints.mem]] +kind = "arith" +constraint = "$#`!read_register2` => #`rv2[i]` = 0$" +poly = ["*", ["not", "read_register2"], ["idx", "rv2", "i"]] +iter = ["i", 0, 2] + +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", ["cast", 2, "DWordWL"], "rd"], ["arr", ["idx", "rvd", 0], ["idx", "rvd", 1], 0, 0, 0, 0, 0, 0], ["+", "timestamp", ["cast", 2, "DWordWL"]], 1, 0, 0] +multiplicity = "write_register" + +[[constraints.mem]] +kind = "interaction" +tag = "LOAD" +input = [["cast", "res", "DWordWL"], ["+", "timestamp", ["cast", 0, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes", "signed"] +output = "rvd" +multiplicity = "LOAD" + +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", "res", "DWordWL"], ["cast", "arg2", ["Byte", 8]], ["+", "timestamp", ["cast", 1, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes"] +multiplicity = "STORE" + +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 255], "DWordWL"], ["arr", ["idx", "next_pc", 0], ["idx", "next_pc", 1], 0, 0, 0, 0, 0, 0], ["+", "timestamp", ["cast", 1, "DWordWL"]], 1, 0, 0] +output = ["arr", ["idx", "pc", 0], ["idx", "pc", 1], 0, 0, 0, 0, 0, 0] +multiplicity = ["not", "pad"] + + +[[constraint_groups]] +name = "sys" +prefix = "S" + +[[constraints.sys]] +kind = "arith" +constraint = "`!EBREAK`" +desc = "We treat `EBREAK` as an unprovable trap" +poly = ["not", "EBREAK"] +ref = "cpu:c:ebreak_traps" + +[[constraints.sys]] +kind = "interaction" +tag = "ECALL" +input = ["timestamp", ["cast", "rv1", "DWordWL"]] +multiplicity = "ECALL" + + +[[constraint_groups]] +name = "ext" +prefix = "E" + +[[constraints.ext]] +kind = "template" +tag = "SIGN" +input = [["idx", "rv1", 1], "word_instr"] +output = "rv1_ext_bit" + +[[constraints.ext]] +kind = "arith" +constraint = "$#`arg1[:4]` = #`rv1[:2]`$" +poly = ["-", ["idx", ["cast", "arg1", "DWordWL"], 0], ["idx", ["cast", "rv1", "DWordWL"], 0]] + +[[constraints.ext]] +kind = "arith" +constraint = "$#`arg1[4:]` = #`rv1[2]` dot (1 - #`word_instr`) + (2^(32) - 1) dot #`rv1_ext_bit` dot #`signed`$" +poly = ["-", ["idx", ["cast", "arg1", "DWordWL"], 1], ["*", ["not", "word_instr"], ["idx", "rv1", 2]], ["*", "signed", "rv1_ext_bit", ["-", ["^", 2, 32], 1]]] + +[[constraints.ext]] +kind = "template" +tag = "SIGN" +input = [["idx", "rv2", 1], "word_instr"] +output = "rv2_ext_bit" + +[[constraints.ext]] +kind = "arith" +constraint = "$#`arg2[:4]` = (1 - #`LOAD`) dot #`rv2[:2]` + (1 - #`BEQ` - #`BLT` - #`STORE`) dot #`imm[0]`$" +poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 0], ["*", ["not", "LOAD"], ["idx", ["cast", "rv2", "DWordWL"], 0]], ["*", ["-", 1, "BEQ", "BLT", "STORE"], ["idx", "imm", 0]]] + +[[constraints.ext]] +kind = "arith" +constraint = "$#`arg2[4:]` = (1 - #`LOAD`) dot ((1 - #`word_instr`) dot #`rv2[2]` + #`signed` dot #`rv2_ext_bit` dot (2^(32) - 1)) + (1 - #`BEQ` - #`BLT` - #`STORE`) dot #`imm[1]`$" +poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 1], ["*", ["not", "LOAD"], ["not", "word_instr"], ["idx", "rv2", 2]], ["*", ["not", "LOAD"], "signed", "rv2_ext_bit", ["-", ["^", 2, 32], 1]], ["*", ["-", 1, "BEQ", "BLT", "STORE"], ["idx", "imm", 1]]] + +[[constraints.ext]] +kind = "template" +tag = "SIGN" +input = [["idx", ["cast", "res", "DWordHL"], 1], "word_instr"] +output = "res_ext_bit" + +[[constraints.ext]] +kind = "arith" +constraint = "$#`!LOAD` => #`rvd[0]` = #`res[:4]`$" +poly = ["*", ["not", "LOAD"], ["-", ["idx", "rvd", 0], ["idx", ["cast", "res", "DWordWL"], 0]]] + +[[constraints.ext]] +kind = "arith" +constraint = "$#`!LOAD` => #`rvd[1]` = (1 - #`word_instr`) dot #`res[4:]` + #`res_ext_bit` dot (2^(32) - 1)$" +desc = "_Sign_ extend the output if it wasn't a `LOAD`. Only `LOAD` has both `write_register = 1` and `rvd ≠ res`. `LOAD` and `word_instr` are disjoint" +poly = ["*", ["not", "LOAD"], ["-", ["idx", "rvd", 1], ["*", ["not", "word_instr"], ["idx", ["cast", "res", "DWordWL"], 1]], ["*", "res_ext_bit", ["-", ["^", 2, 32], 1]]]] + + +[[constraint_groups]] +name = "misc" +prefix = "O" + +[[constraints.misc]] +kind = "interaction" +tag = "ZERO" +input = [["+", ["idx", "res", 0], ["idx", "res", 1], ["idx", "res", 2], ["idx", "res", 3], ["idx", "res", 4], ["idx", "res", 5], ["idx", "res", 6], ["idx", "res", 7]]] +output = "is_equal" +multiplicity = "BEQ" +ref = "cpu:c:is_equal" + +[[constraints.misc]] +kind = "arith" +constraint = "$#`branch_cond` = #`JALR` or (#`BLT` and (#`res` xor #`invert`)) or (#`BEQ` and (#`is_equal` xor #`invert`))$" +desc = "where `invert` is represented by `mp_selector`" +poly = ["+", + ["-", "branch_cond"], + "JALR", + ["*", ["idx", "res", 0], ["not", "mp_selector"], "BLT"], + ["*", ["-", 1, ["idx", "res", 0]], "mp_selector", "BLT"], + ["*", "is_equal", ["not", "mp_selector"], "BEQ"], + ["*", ["not", "is_equal"], "mp_selector", "BEQ"] + ] + +[[constraints.misc]] +kind = "interaction" +tag = "BRANCH" +input = ["pc", "imm", ["cast", "arg1", "DWordWL"], "JALR"] +output = "next_pc" +multiplicity = "branch_cond" + +[[constraints.misc]] +kind = "template" +tag = "ADD" +input = ["pc", ["*", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], ["cast", 1, "DWordWL"]]] +output = "next_pc" +desc = "Increment `pc` to `next_pc` if we're not branching" diff --git a/spec/src/decode.toml b/spec/src/decode.toml new file mode 100644 index 000000000..367db1568 --- /dev/null +++ b/spec/src/decode.toml @@ -0,0 +1,59 @@ +name = "DECODE" + +[[variables.output]] +name = "pc" +type = "DWordWL" +desc = "value of the program counter this instruction is associated with." +pad = 7 + +[[variables.output]] +name = "packed_decode" +type = "BaseField" +desc = """Ordered concatenation of several small variables. +The `decode (uncompressed)` section explains the purpose of each variable.\\ +A list of each variable and the bit(-range) in which it is located:\\ +[0] `read_register1`, \\ +[1] `read_register2`, \\ +[2] `write_register`, \\ +[3] `memory_2bytes`, \\ +[4] `memory_4bytes`, \\ +[5] `memory_8bytes`, \\ +[6] `c_type`, \\ +[7] `signed`, \\ +[8] `mp_selector`, \\ +[9] `muldiv_selector`, \\ +[10] `word_instr`, \\ +[11] `ADD`, \\ +[12] `SUB`, \\ +[13] `SLT`, \\ +[14] `AND`, \\ +[15] `OR`, \\ +[16] `XOR`, \\ +[17] `SHIFT`, \\ +[18] `JALR`, \\ +[19] `BEQ`, \\ +[20] `BLT`, \\ +[21] `LOAD`, \\ +[22] `STORE`, \\ +[23] `MUL`, \\ +[24] `DIVREM`, \\ +[25] `ECALL`, \\ +[26] `EBREAK`; \\ +[27:35] `rs1`, \\ +[35:43] `rs2`, \\ +[43:51] `rd`, \\ +the remaining bits are set to zero. +""" +pad = ["^", 2, 26] + +[[variables.output]] +name = "imm" +type = "DWordWL" +desc = "the *fully extended (!)* 64-bit version of the immediate." +pad = 0 + +[[variables.multiplicity]] +name = "μ" +type = "BaseField" +desc = "The multiplicity with which this instruction is looked up in the `CPU` table." +pad = 0 diff --git a/spec/src/decode_uncompressed.toml b/spec/src/decode_uncompressed.toml new file mode 100644 index 000000000..0f6c931c2 --- /dev/null +++ b/spec/src/decode_uncompressed.toml @@ -0,0 +1,167 @@ +name = "DECODE" + +[[variables.output]] +name = "pc" +type = "DWordWL" +desc = "value of the program counter this instruction is associated with." + +[[variables.output]] +name = "rs1" +type = "Byte" +desc = "index of source register 1." + +[[variables.output]] +name = "rs2" +type = "Byte" +desc = "index of source register 2." + +[[variables.output]] +name = "rd" +type = "Byte" +desc = "index of destination register." + +[[variables.output]] +name = "read_register1" +type = "Bit" +desc = "whether to load the contents of address `rs1` (1) or `0` (0) into `rv1`." + +[[variables.output]] +name = "read_register2" +type = "Bit" +desc = "whether to load the contents of address `rs2` (1) or `0` (0) into `rv2`." + +[[variables.output]] +name = "write_register" +type = "Bit" +desc = "whether the result should be written to `rd` ($=0$ for memory write and when $#`rd` = #`x0`$." + +[[variables.output]] +name = "mem_2B" +type = "Bit" +desc = "whether the memory access (read or write) touches exactly $2$ bytes." + +[[variables.output]] +name = "mem_4B" +type = "Bit" +desc = "whether the memory access (read or write) touches exactly $4$ bytes." + +[[variables.output]] +name = "mem_8B" +type = "Bit" +desc = "whether the memory access (read or write) touches exactly $8$ bytes." + +[[variables.output]] +name = "c_type" +type = "Bit" +desc = "Whether the instruction is of type `C`, i.e., whether it is $2$ bytes long instead of $4$." + +[[variables.output]] +name = "imm" +type = "DWordWL" +desc = "the *fully extended (!)* 64-bit version of the immediate." + +[[variables.output]] +name = "signed" +type = "Bit" +desc = "selector used to indicate signed or unsigned input interpretation." + +[[variables.output]] +name = "mp_selector" +type = "Bit" +desc = """Multi-purpose selector used by the CPU to to configure several ALU operations in different ways. + See the `CPU` chip for more details.""" + +[[variables.output]] +name = "muldiv_selector" +type = "Bit" +desc = "selects which output of `MUL` (lo/hi) or `DVRM` (quo/rem) is wanted." + +[[variables.output]] +name = "word_instr" +type = "Bit" +desc = "Whether the instruction is a `*W` instruction, requiring the inputs and outputs to be (sign) extended." + +[[variables.output]] +name = "ADD" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "SUB" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "SLT" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "AND" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "OR" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "XOR" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "SHIFT" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "JALR" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "BEQ" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "BLT" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "LOAD" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "STORE" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "MUL" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "DIVREM" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "ECALL" +type = "Bit" +desc = "ALU selector flag" + +[[variables.output]] +name = "EBREAK" +type = "Bit" +desc = "ALU selector flag" + +[[variables.multiplicity]] +name = "μ" +type = "BaseField" +desc = "The multiplicity with which this instruction is looked up in the `CPU` table." diff --git a/spec/src/dvrm.toml b/spec/src/dvrm.toml new file mode 100644 index 000000000..52583907c --- /dev/null +++ b/spec/src/dvrm.toml @@ -0,0 +1,388 @@ +name = "DVRM" + +# Input + +[[variables.input]] +name = "n" +type = "DWordHL" +desc = "The numerator" +pad = 0 + +[[variables.input]] +name = "d" +type = "DWordHL" +desc = "The denominator" +pad = 0 + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Whether to interpret the input as signed (1) or unsigned (0) integers." +pad = 0 + + +# Output + +[[variables.output]] +name = "q" +type = "DWordHL" +desc = "The quotient; $#`n` / #`d`$ rounded towards zero." +pad = 0 + +[[variables.output]] +name = "r" +type = "DWordHL" +desc = "The remainder; $#`n` - #`q` #`d`$." +pad = 0 + +# Auxiliary + +[[variables.auxiliary]] +name = "div_by_zero" +type = "Bit" +desc = "Whether $#`d`=0$." +pad = 1 + +[[variables.auxiliary]] +name = "overflow" +type = "Bit" +desc = "Whether $#`n` = -2^63$ and $#`d`=-1$." +pad = 0 + +[[variables.auxiliary]] +name = "abs_r" +type = "DWordWL" +desc = "Absolute value of `r`." +pad = 0 + +[[variables.auxiliary]] +name = "abs_d" +type = "DWordWL" +desc = "Absolute value of `d`." +pad = 0 + +[[variables.auxiliary]] +name = "n_sub_r" +type = "DWordHL" +desc = "$#`n`-#`r`$." +pad = 0 + +[[variables.auxiliary]] +name = "sign_n_sub_r" +type = "Bit" +desc = "Sign of `n_sub_r`." +pad = 0 + +[[variables.auxiliary]] +name = "sign_n" +type = "Bit" +desc = "Sign of `n`." +pad = 0 + +[[variables.auxiliary]] +name = "sign_d" +type = "Bit" +desc = "Sign of `d`." +pad = 0 + +[[variables.auxiliary]] +name = "sign_q" +type = "Bit" +desc = "Sign of `q`." +pad = 0 + +[[variables.auxiliary]] +name = "sign_r" +type = "Bit" +desc = "Sign of `r`." +pad = 0 + +# Virtual + +[[variables.virtual]] +name = "extended_n" +type = "QuadHL" +desc = "sign-extended value of `n`." +def = {idx="i", polys = [ + {iter=[0, 3], poly=["idx", "n", "i"]}, + {iter=[4, 7], poly=["*", 0xFFFF, "sign_n"]} +]} + +[[variables.virtual]] +name = "extended_r" +type = "QuadHL" +desc = "sign-extended value of `r`." +def = {idx="i", polys = [ + {iter=[0, 3], poly=["idx", "r", "i"]}, + {iter=[4, 7], poly=["*", 0xFFFF, "sign_r"]} +]} + +[[variables.virtual]] +name = "extension_n_sub_r" +type = "DWordHL" +desc = "sign-extension limbs of `n_sub_r`." +def = {idx="i", iter=[0, 3], poly=["*", 0xFFFF, "sign_n_sub_r"]} + +[[variables.virtual]] +name = "extended_n_sub_r" +type = "QuadHL" +desc = "sign-extended value of `n_sub_r`." +def = {idx="i", polys = [ + {iter=[0, 3], poly=["idx", "n_sub_r", "i"]}, + {iter=[4, 7], poly=["idx", "extension_n_sub_r", ["-", "i", 4]]} +]} + +[[variables.virtual]] +name = "carry" +type = ["Bit", 4] +desc = "carries for adding `extended_n_sub_r` to `extended_r`, forming `extended_n`." +def = {idx="i", polys = [ + {iter=0, poly=["*", + ["^", 2, -32], + ["-", + ["+", + ["idx", ["cast", "extended_n_sub_r", "QuadWL"], "i"], + ["idx", ["cast", "extended_r", "QuadWL"], "i"] + ], + ["idx", ["cast", "extended_n", "QuadWL"], "i"] + ] + ]}, + {iter=[1, 3], poly=["*", + ["^", 2, -32], + ["-", + ["+", + ["idx", ["cast", "extended_n_sub_r", "QuadWL"], "i"], + ["idx", ["cast", "extended_r", "QuadWL"], "i"], + ["idx", "carry", ["-", "i", 1]], + ], + ["idx", ["cast", "extended_n", "QuadWL"], "i"] + ] + ]}, +]} + +[[variables.virtual]] +name = "μ_sum" +type = "BaseField" +desc = "sum of multiplicities" +def = ["+", "μ_q", "μ_r"] + + +# Multiplicities + +[[variables.multiplicity]] +name = "μ_q" +type = "BaseField" +desc = "" +pad = 0 + +[[variables.multiplicity]] +name = "μ_r" +type = "BaseField" +desc = "" +pad = 0 + + +# Assumptions + +[[assumptions]] +desc = "`IS_HALF[n[i]]`" +iter = ["i", 0, 3] +ref = "lt:a:range_n" + +[[assumptions]] +desc = "`IS_HALF[d[i]]`" +iter = ["i", 0, 3] +ref = "lt:a:range_d" + +[[assumptions]] +desc = "`IS_BIT`" +ref = "lt:a:range_signed" + +# Constraints + +[[constraint_groups]] +name = "sign_equality" + +[[constraints.sign_equality]] +kind = "arith" +constraint = "$#`r` eq.not 0 => #`sign_r` = #`sign_n`$" +poly = ["*", ["sum", ["=", "i", 0], 3, ["idx", "r", "i"]], ["-", "sign_r", "sign_n"]] +ref = "dvrm:c:sign_r_equals_sign_n" + +[[constraint_groups]] +name = "abs_diff" + +[[constraints.abs_diff]] +kind = "interaction" +tag = "LT" +input = ["abs_r", "abs_d", 0] +output = ["not", "div_by_zero"] +multiplicity = "μ_sum" +ref ="dvrm:c:abs_r_lt_abs_d" + +[[constraints.abs_diff]] +kind = "template" +tag = "NEG" +input = ["r"] +output = "abs_r" +cond = "sign_r" +ref = "dvrm:c:abs_r_if_negative" + +[[constraints.abs_diff]] +kind = "arith" +constraint = "$not#`sign_r` => #`abs_r` = #`r`$" +poly = ["*", ["not", "sign_r"], ["-", ["idx", "abs_r", "i"], ["idx", ["cast", "r", "DWordWL"], "i"]]] +iter = ["i", 0, 1] +ref = "dvrm:c:abs_r_if_nonnegative" + +[[constraints.abs_diff]] +kind = "template" +tag = "NEG" +input = ["d"] +output = "abs_d" +cond = "sign_d" +ref = "dvrm:c:abs_d_if_negative" + +[[constraints.abs_diff]] +kind = "arith" +constraint = "$not#`sign_d` => #`abs_d` = #`d`$" +iter = ["i", 0, 1] +poly = ["*", ["not", "sign_d"], ["-", ["idx", "abs_d", "i"], ["idx", ["cast", "d", "DWordWL"], "i"]]] +ref = "dvrm:c:abs_d_if_nonnegative" + +[[constraint_groups]] +name = "overflow" + +[[constraints.overflow]] +kind = "arith" +constraint = "$#`sign_q` = #`signed` dot (1- #`overflow`)$" +poly = ["-", ["*", "signed", ["-", 1, "overflow"]], "sign_q"] +ref = "dvrm:c:sign_q" + +[[constraints.overflow]] +kind = "interaction" +tag = "ZERO" +input = [["+", ["idx", "n", 0], ["idx", "n", 1], ["idx", "n", 2], ["-", ["idx", "n", 3], ["*", ["^", 2, 15], "sign_n"]], ["-", 1, "sign_n"], ["-", 65535, ["idx", "d", 0]], ["-", 65535, ["idx", "d", 1]], ["-", 65535, ["idx", "d", 2]], ["-", 65535, ["idx", "d", 3]]]] +output = "overflow" +multiplicity = "μ_sum" +ref = "dvrm:c:overflow" + +[[constraint_groups]] +name = "n_sub_r" + +[[constraints.n_sub_r]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "carry", "i"]] +iter = ["i", 0, 3] +ref = "dvrm:c:n_sub_r" + +[[constraints.n_sub_r]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "r", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ_sum" +ref = "dvrm:c:r_range" + +[[constraints.n_sub_r]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "n_sub_r", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ_sum" +ref = "dvrm:c:n_sub_r_range" + +[[constraints.n_sub_r]] +kind = "template" +tag = "IS_BIT" +input = ["sign_n_sub_r"] +ref = "dvrm:c:sign_n_sub_r_is_bit" + +[[constraint_groups]] +name = "equality" + +[[constraints.equality]] +kind = "interaction" +tag = "MUL" +input = ["d", "signed", "q", "sign_q", 0] +output = ["cast", "n_sub_r", "DWordWL"] +multiplicity = "μ_sum" +ref = "dvrm:c:mul_lower" + +[[constraints.equality]] +kind = "interaction" +tag = "MUL" +input = ["d", "signed", "q", "sign_q", 1] +output = ["cast", "extension_n_sub_r", "DWordWL"] +multiplicity = "μ_sum" +ref = "dvrm:c:mul_upper" + +[[constraints.equality]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "q", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ_sum" +ref = "dvrm:c:q_range" + + +[[constraint_groups]] +name = "defs" + +[[constraints.defs]] +kind = "template" +tag = "SIGN" +input = [["idx", "n", 3], "signed"] +output = "sign_n" +ref = "dvrm:c:sign_n" + +[[constraints.defs]] +kind = "template" +tag = "SIGN" +input = [["idx", "r", 3], "signed"] +output = "sign_r" +ref = "dvrm:c:sign_r" + +[[constraints.defs]] +kind = "template" +tag = "SIGN" +input = [["idx", "d", 3], "signed"] +output = "sign_d" +ref = "dvrm:c:sign_d" + +[[constraint_groups]] +name = "div_by_zero" + +[[constraints.div_by_zero]] +kind = "arith" +iter = ["i", 0, 3] +constraint = "$#`div_by_zero` => #`q[i]` = 65535$" +poly = ["*", "div_by_zero", ["-", ["idx", "q", "i"], 65535]] +ref = "dvrm:c:q_if_div_by_zero" + +[[constraints.div_by_zero]] +kind = "interaction" +tag = "ZERO" +input = [["+", ["idx", "d", 0], ["idx", "d", 1], ["idx", "d", 2], ["idx", "d", 3]]] +output = "div_by_zero" +ref = "dvrm:c:div_by_zero" +multiplicity = "μ_sum" + +[[constraint_groups]] +name = "output" +desc = "Each row contributes the following to the LogUp sum" + +[[constraints.output]] +kind = "interaction" +tag = "DVRM" +input = ["n", "d", "signed", 0] +output = ["cast", "q", "DWordWL"] +multiplicity = ["-", "μ_q"] + +[[constraints.output]] +kind = "interaction" +tag = "DVRM" +input = ["n", "d", "signed", 1] +output = ["cast", "r", "DWordWL"] +multiplicity = ["-", "μ_r"] diff --git a/spec/src/halt.toml b/spec/src/halt.toml new file mode 100644 index 000000000..9fee04877 --- /dev/null +++ b/spec/src/halt.toml @@ -0,0 +1,56 @@ +name = "HALT" + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "timestamp at which to halt the program" + + +[[assumptions]] +desc = "`IS_WORD[timestamp[i]]`" +iter = ["i", 0, 1] + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, "i"], "DWordWL"], ["cast", 0, ["BaseField", 8]], ["cast", ["-", ["^", 2, 64], 1], "DWordWL"], 1, 0, 0] +iter = ["i", 1, 9] +multiplicity = 1 +ref = "halt:c:zeroize_registers_lo" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 10], "DWordWL"], ["cast", 0, ["BaseField", 8]], ["cast", ["-", ["^", 2, 64], 1], "DWordWL"], 1, 0, 0] +output = ["cast", 0, ["BaseField", 8]] +multiplicity = 1 +ref = "halt:c:read_zero_exit_code" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, "i"], "DWordWL"], ["cast", 0, ["BaseField", 8]], ["cast", ["-", ["^", 2, 64], 1], "DWordWL"], 1, 0, 0] +iter = ["i", 11, 31] +multiplicity = 1 +ref = "halt:c:zeroize_registers_hi" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 255], "DWordWL"], ["arr", 1, 0, 0, 0, 0, 0, 0, 0], ["cast", ["-", ["^", 2, 64], 1], "DWordWL"], 1, 0, 0] +multiplicity = 1 +ref = "halt:c:pc" + +[[constraint_groups]] +name = "lookup" + +[[constraints.lookup]] +kind = "interaction" +tag = "ECALL" +input = ["timestamp", ["cast", 93, "DWordWL"]] +multiplicity = ["-", 1] +ref = "halt:c:lookup" diff --git a/spec/src/is_bit.toml b/spec/src/is_bit.toml new file mode 100644 index 000000000..a72b5f648 --- /dev/null +++ b/spec/src/is_bit.toml @@ -0,0 +1,20 @@ +name = "IS_BIT" + +[[variables.condition]] +name = "cond" +type = "BaseField" +desc = "Whether the constraint should be applied ($eq.not 0$) or not ($0$)." + +[[variables.input]] +name = "X" +type = "BaseField" +desc = "Value for which to assert that it lies in the range ${0, 1}$." + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "arith" +constraint = "$#`cond` => #`X` (1-#`X`) = 0$" +poly = ["*", "cond", "X", ["-", 1, "X"]] +ref = "isbit:c:isbit" diff --git a/spec/src/load.toml b/spec/src/load.toml new file mode 100644 index 000000000..f8a974c9a --- /dev/null +++ b/spec/src/load.toml @@ -0,0 +1,160 @@ +name = "LOAD" + +# Input + +[[variables.input]] +name = "base_address" +type = "DWordWL" +desc = "The base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" +pad = 0 + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "The timestamp at which this memory access is said to occur" +pad = 0 + +[[variables.input]] +name = "read2" +type = "Bit" +desc = "Whether to read exactly 2 bytes" +pad = 0 + +[[variables.input]] +name = "read4" +type = "Bit" +desc = "Whether to read exactly 4 bytes" +pad = 0 + +[[variables.input]] +name = "read8" +type = "Bit" +desc = "Whether to read exactly 8 bytes" +pad = 0 + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Whether to sign-extend (1) or zero-extend (0)" +pad = 0 + +# Output + +[[variables.output]] +name = "res" +type = "DWordBL" +desc = "The result of reading (up to) 8 bytes from `base_address`, extended corresponding to `signed`." +pad = 0 + +# Auxiliary + +[[variables.auxiliary]] +name = "sign_bit" +type = "Bit" +desc = "The sign bit extracted from the bytes retrieved from memory" +pad = 0 + +# Virtual + +[[variables.virtual]] +name = "read1" +type = "Bit" +desc = "Whether to read exactly 1 byte" +def = ["-", "μ", "read2", "read4", "read8"] + +# Multiplicity + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" +pad = 0 + + +[[assumptions]] +desc = "`IS_WORD[base_address[i]]`" +iter = ["i", 0, 1] + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_WORD[timestamp[i]]`" +iter = ["i", 0, 1] + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "arith" +constraint = "$#`read2` + #`read4` + #`read8` => #`μ`$" +poly = ["*", ["+", "read2", "read4", "read8"], ["not", "μ"]] + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [0, "base_address", ["cast", "res", ["BaseField", 8]], "timestamp", "read2", "read4", "read8"] +output = "res" +multiplicity = "μ" + +[[constraints.all]] +kind = "interaction" +tag = "MSB8" +input = [["idx", "res", 0]] +output = "sign_bit" +multiplicity = "read1" + +[[constraints.all]] +kind = "interaction" +tag = "MSB8" +input = [["idx", "res", 1]] +output = "sign_bit" +multiplicity = "read2" + +[[constraints.all]] +kind = "interaction" +tag = "MSB8" +input = [["idx", "res", 3]] +output = "sign_bit" +multiplicity = "read4" + +[[constraints.all]] +kind = "arith" +constraint = "$!#`read8` => #`res`_i = #`signed` dot #`sign_bit` dot 255$" +poly = ["*", ["not", "read8"], ["-", ["idx", "res", "i"], ["*", "signed", "sign_bit", 255]]] +iter = ["i", 4, 7] + +[[constraints.all]] +kind = "arith" +constraint = "$!(#`read4` + #`read8`) => #`res`_i = #`signed` dot #`sign_bit` dot 255$" +poly = ["*", ["-", 1, "read4", "read8"], ["-", ["idx", "res", "i"], ["*", "signed", "sign_bit", 255]]] +iter = ["i", 2, 3] + +[[constraints.all]] +kind = "arith" +constraint = "$!(#`read2` + #`read4` + #`read8`) => #`res`_1 = #`signed` dot #`sign_bit` dot 255$" +poly = ["*", ["-", 1, "read2", "read4", "read8"], ["-", ["idx", "res", 1], ["*", "signed", "sign_bit", 255]]] + + +[[constraint_groups]] +name = "output" + +[[constraints.output]] +kind = "interaction" +tag = "LOAD" +input = ["base_address", "timestamp", "read2", "read4", "read8", "signed"] +output = ["cast", "res", "DWordWL"] +multiplicity = ["-", "μ"] diff --git a/spec/src/lt.toml b/spec/src/lt.toml new file mode 100644 index 000000000..70d25c919 --- /dev/null +++ b/spec/src/lt.toml @@ -0,0 +1,163 @@ +name = "LT" + + +# Input + +[[variables.input]] +name = "lhs" +type = "DWordHHW" +desc = "The left operand" +pad = 0 + +[[variables.input]] +name = "rhs" +type = "DWordHHW" +desc = "The right operand" +pad = 0 + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "whether to interpret `lhs` and `rhs` as signed integers (1) or not (0)" +pad = 0 + +# Output + +[[variables.output]] +name = "lt" +type = "Bit" +desc = "Whether $#`lhs` < #`rhs`$, taking `signed` into account" +pad = 0 + + +# Auxiliary + +[[variables.auxiliary]] +name = "lhs_sub_rhs" +type = "DWordHL" +desc = "$#`lhs` - #`rhs`$" +pad = 0 + +[[variables.auxiliary]] +name = "lhs_msb" +type = "Bit" +desc = "The most significant bit of `lhs`" +pad = 0 + +[[variables.auxiliary]] +name = "rhs_msb" +type = "Bit" +desc = "The most significant bit of `rhs`" +pad = 0 + +# Virtual + +[[variables.virtual]] +name = "carry" +type = ["Bit", 2] +desc = "The carry for adding `lhs_sub_rhs` back to `rhs`" +def = {idx = "i", polys = [ + {iter = 0, poly = ["*", ["^", 2, -32], ["-", ["+", ["idx", "rhs", 0], ["idx", ["cast", "lhs_sub_rhs", "DWordWL"], 0]], ["idx", "lhs", 0]]]}, + {iter = 1, poly = ["*", ["^", 2, -32], ["-", ["+", ["idx", ["cast", "rhs", "DWordWL"], 1], ["idx", ["cast", "lhs_sub_rhs", "DWordWL"], 1], ["idx", "carry", 0]], ["idx", ["cast", "lhs", "DWordWL"], 1]]]}, +]} + +[[variables.virtual]] +name = "unsigned_lt" +type = "Bit" +desc = "Whether $#`lhs` < #`rhs`$, as unsigned integers" +def = ["idx", "carry", 1] + + +# Multiplicity + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" +pad = 0 + + +[[assumptions]] +desc = "`IS_WORD[lhs[0]]`" +ref = "lt:a:range_lhs" + +[[assumptions]] +desc = "`IS_WORD[rhs[0]]`" +ref = "lt:a:range_rhs" + +[[assumptions]] +desc = "`IS_BIT`" +ref = "lt:a:range_signed" + + +[[constraint_groups]] +name = "defs" +desc = "Enforce that variables have been correctly computed" + +[[constraints.defs]] +kind = "interaction" +tag = "MSB16" +input = [["idx", "lhs", 2]] +output = "lhs_msb" +multiplicity = "μ" +ref = "lt:c:lhs_msb" + +[[constraints.defs]] +kind = "interaction" +tag = "MSB16" +input = [["idx", "rhs", 2]] +output = "rhs_msb" +multiplicity = "μ" +ref = "lt:c:rhs_msb" + +[[constraints.defs]] +kind = "arith" +constraint = "$#`lt` = #`signed` dot (A (1 - B) + A C + (1 - B) C) + (1 - #`signed`) dot #`unsigned_lt`$" +desc = "Where $A = #`lhs_msb`$, $B = #`rhs_msb`$ and $C = #`carry[1]`$" +poly = ["-", "lt", ["*", "signed", ["+", ["*", "lhs_msb", ["not", "rhs_msb"]], ["*", "lhs_msb", ["idx", "carry", 1]], ["*", ["not", "rhs_msb"], ["idx", "carry", 1]]]], ["*", ["-", 1, "signed"], "unsigned_lt"]] +ref = "lt:c:lt" + + +[[constraint_groups]] +name = "sub" +desc = "Constrain the subtraction" + +[[constraints.sub]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "carry", "i"]] +iter = ["i", 0, 1] + +[[constraints.defs]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "lhs", 1]] +multiplicity = "μ" +ref = "lt:c:range_lhs" + +[[constraints.defs]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "rhs", 1]] +multiplicity = "μ" +ref = "lt:c:range_rhs" + +[[constraints.sub]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "lhs_sub_rhs", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ" +ref = "lt:c:lhs_sub_rhs_range" + + +[[constraint_groups]] +name = "output" +desc = "Each row contributes the following to the LogUp sum" + +[[constraints.output]] +kind = "interaction" +tag = "LT" +input = [["cast", "lhs", "DWordWL"], ["cast", "rhs", "DWordWL"], "signed"] +output = "lt" +multiplicity = ["-", "μ"] diff --git a/spec/src/memw.toml b/spec/src/memw.toml new file mode 100644 index 000000000..1cc0dd3c2 --- /dev/null +++ b/spec/src/memw.toml @@ -0,0 +1,265 @@ +name = "MEMW" + +# Input + +[[variables.input]] +name = "is_register" +type = "Bit" +desc = "Whether the address represents a register index" +pad = 0 + +[[variables.input]] +name = "base_address" +type = "DWordWL" +desc = "The base address to read from/write to. Gets offset by $[0, 7]$ depending on the size of the access" +pad = 0 + +[[variables.input]] +name = "value" +type = ["BaseField", 8] +desc = "The values to store in memory. For RAM, these should be (up to) 8 range-checked `Byte`s; registers are stored as two range-checked `Word`s" +pad = 0 + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "The timestamp at which this memory access occurs" +pad = 0 + +[[variables.input]] +name = "write2" +type = "Bit" +desc = "Whether to write exactly 2 values" +pad = 0 + +[[variables.input]] +name = "write4" +type = "Bit" +desc = "Whether to write exactly 4 values" +pad = 0 + +[[variables.input]] +name = "write8" +type = "Bit" +desc = "Whether to write exactly 8 values" +pad = 0 + +# Output + +[[variables.output]] +name = "old" +type = ["BaseField", 8] +desc = """The old value written at `base_address`. See `value` for information about representation. +Only the elements corresponding to the `writeN` bits are guaranteed""" +pad = 0 + +# Auxiliary + +[[variables.auxiliary]] +name = "carry" +type = ["Bit", 7] +desc = "Whether `base_address[0] + i + 1` $>= 2^32$" +pad = 0 + +[[variables.auxiliary]] +name = "old_timestamp" +type = ["DWordWL", 8] +desc = "The timestamp at which address `base_address + i` was last accessed" +pad = 0 + +# Virtual + +[[variables.virtual]] +name = "w2" +type = "Bit" +desc = "writing at least 2 bytes" +def = ["+", "write2", "write4", "write8"] + +[[variables.virtual]] +name = "w4" +type = "Bit" +desc = "writing at least 4 bytes" +def = ["+", "write4", "write8"] + +[[variables.virtual]] +name = "address_add" +type = ["DWordWL", 7] +desc = "`address_add[i] = base_address + i + 1`" +def.iter = ["i", 0, 6] +def.poly = ["arr", + ["-", ["+", ["idx", "base_address", 0], "i", 1], ["*", ["^", 2, 32], ["idx", "carry", "i"]]], + ["+", ["idx", "base_address", 1], ["idx", "carry", "i"]]] + +[[variables.virtual]] +name = "μ_sum" +type = "Bit" +desc = "" +def = ["+", "μ_read", "μ_write"] + +# Multiplicity + +[[variables.multiplicity]] +name = "μ_read" +type = "Bit" +desc = "Whether we are performing a read (and hence return `out`)" +pad = 0 + +[[variables.multiplicity]] +name = "μ_write" +type = "Bit" +desc = "Whether we are performing a write (and hence not return `out`)" +pad = 0 + +[[assumptions]] +desc = "`IS_WORD[base_address[i]]`" +iter = ["i", 0, 1] + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_WORD[timestamp[i]]`" +iter = ["i", 0, 1] + + +[[constraint_groups]] +name = "consistency" + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = ["μ_read"] + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = ["μ_write"] + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = ["μ_sum"] + +[[constraints.consistency]] +kind = "arith" +constraint = "$#`w2` => #`μ_sum`$" +poly = ["*", "w2", ["not", "μ_sum"]] + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "carry", "i"]] +iter = ["i", 0, 6] + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", 0], "timestamp", 0] +output = 1 +multiplicity = "μ_sum" + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", 1], "timestamp", 0] +output = 1 +multiplicity = "w2" + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", "i"], "timestamp", 0] +output = 1 +iter = ["i", 2, 3] +multiplicity = "w4" + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", "i"], "timestamp", 0] +output = 1 +iter = ["i", 4, 7] +multiplicity = "write8" + +[[constraint_groups]] +name = "memory" +prefix = "M" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", "base_address", ["idx", "old_timestamp", 0], ["idx", "old", 0]] +multiplicity = "μ_sum" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", "base_address", "timestamp", ["idx", "value", 0]] +multiplicity = ["-", "μ_sum"] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["idx", "address_add", 0], ["idx", "old_timestamp", 1], ["idx", "old", 1]] +multiplicity = "w2" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["idx", "address_add", 0], "timestamp", ["idx", "value", 1]] +multiplicity = ["-", "w2"] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["idx", "address_add", ["-", "i", 1]], ["idx", "old_timestamp", "i"], ["idx", "old", "i"]] +multiplicity = "w4" +iter = ["i", 2, 3] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["idx", "address_add", ["-", "i", 1]], "timestamp", ["idx", "value", "i"]] +multiplicity = ["-", "w4"] +iter = ["i", 2, 3] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["idx", "address_add", ["-", "i", 1]], ["idx", "old_timestamp", "i"], ["idx", "old", "i"]] +multiplicity = "write8" +iter = ["i", 4, 7] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["idx", "address_add", ["-", "i", 1]], "timestamp", ["idx", "value", "i"]] +multiplicity = ["-", "write8"] +iter = ["i", 4, 7] + + +[[constraint_groups]] +name = "output" +prefix = "O" + +[[constraints.output]] +kind = "interaction" +tag = "MEMW" +input = ["is_register", "base_address", "value", "timestamp", "write2", "write4", "write8"] +output = "old" +multiplicity = ["-", "μ_read"] + +[[constraints.output]] +kind = "interaction" +tag = "MEMW" +input = ["is_register", "base_address", "value", "timestamp", "write2", "write4", "write8"] +multiplicity = ["-", "μ_write"] diff --git a/spec/src/memw_aligned.toml b/spec/src/memw_aligned.toml new file mode 100644 index 000000000..93a636aba --- /dev/null +++ b/spec/src/memw_aligned.toml @@ -0,0 +1,230 @@ +name = "MEMW_A" + +# Input + +[[variables.input]] +name = "is_register" +type = "Bit" +desc = "Whether the address represents a register index" +pad = 0 + +[[variables.input]] +name = "base_address" +type = "DWordWHH" +desc = "The base address to read from/write to. Gets offset by $[0, 7]$ depending on the size of the access" +pad = 0 + +[[variables.input]] +name = "value" +type = ["BaseField", 8] +desc = "The values to store in memory. For regular memory, these should be (up to) 8 range-checked `Byte`s; registers are stored as two range-checked `Word`s" +pad = 0 + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "The timestamp at which this memory access is said to occur" +pad = 0 + +[[variables.input]] +name = "write2" +type = "Bit" +desc = "Whether to write exactly 2 values" +pad = 0 + +[[variables.input]] +name = "write4" +type = "Bit" +desc = "Whether to write exactly 4 values" +pad = 0 + +[[variables.input]] +name = "write8" +type = "Bit" +desc = "Whether to write exactly 8 values" +pad = 0 + +# Output + +[[variables.output]] +name = "old" +type = ["BaseField", 8] +desc = """The old value written at `base_address + i`. See `value` for information about representation. +Only the elements corresponding to the `writeN` bits are guaranteed""" +pad = 0 + +# Auxiliary + +[[variables.auxiliary]] +name = "old_timestamp" +type = "DWordWL" +desc = "The timestamp at which the address was last accessed" +pad = 0 + +# Virtual + +[[variables.virtual]] +name = "w2" +type = "Bit" +desc = "writing at least 2 bytes" +def = ["+", "write2", "write4", "write8"] + +[[variables.virtual]] +name = "w4" +type = "Bit" +desc = "writing at least 4 bytes" +def = ["+", "write4", "write8"] + +[[variables.virtual]] +name = "μ_sum" +type = "Bit" +desc = "" +def = ["+", "μ_read", "μ_write"] + +# Multiplicity + +[[variables.multiplicity]] +name = "μ_read" +type = "Bit" +desc = "Whether we are performing a read (and hence return `out`)" +pad = 0 + +[[variables.multiplicity]] +name = "μ_write" +type = "Bit" +desc = "Whether we are performing a write (and hence not return `out`)" +pad = 0 + +[[assumptions]] +desc = "`IS_HALF[base_address[i]]`" +iter = ["i", 0, 1] + +[[assumptions]] +desc = "`IS_WORD[base_address[2]]`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_BIT`" + +[[assumptions]] +desc = "`IS_WORD[timestamp[i]]`" +iter = ["i", 0, 1] + + +[[constraint_groups]] +name = "consistency" + +[[constraints.consistency]] +kind = "interaction" +tag = "IS_HALF" +input = [["+", ["idx", "base_address", 0], "write2", ["*", 3, "write4"], ["*", 7, "write8"]]] +multiplicity = "μ_sum" + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = ["μ_read"] + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = ["μ_write"] + +[[constraints.consistency]] +kind = "template" +tag = "IS_BIT" +input = ["μ_sum"] + +[[constraints.consistency]] +kind = "arith" +constraint = "$#`w2` => #`μ_sum`$" +poly = ["*", "w2", ["not", "μ_sum"]] + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = ["old_timestamp", "timestamp", 0] +output = 1 +multiplicity = "μ_sum" + +[[constraint_groups]] +name = "memory" +prefix = "M" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["cast", "base_address", "DWordWL"], "old_timestamp", ["idx", "old", 0]] +multiplicity = "μ_sum" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["cast", "base_address", "DWordWL"], "timestamp", ["idx", "value", 0]] +multiplicity = ["-", "μ_sum"] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", 1, "DWordWL"]], "old_timestamp", ["idx", "old", 1]] +multiplicity = "w2" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", 1, "DWordWL"]], "timestamp", ["idx", "value", 1]] +multiplicity = ["-", "w2"] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", "i", "DWordWL"]], "old_timestamp", ["idx", "old", "i"]] +multiplicity = "w4" +iter = ["i", 2, 3] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", "i", "DWordWL"]], "timestamp", ["idx", "value", "i"]] +multiplicity = ["-", "w4"] +iter = ["i", 2, 3] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", "i", "DWordWL"]], "old_timestamp", ["idx", "old", "i"]] +multiplicity = "write8" +iter = ["i", 4, 7] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", "i", "DWordWL"]], "timestamp", ["idx", "value", "i"]] +multiplicity = ["-", "write8"] +iter = ["i", 4, 7] + + +[[constraint_groups]] +name = "output" +prefix = "O" + +[[constraints.output]] +kind = "interaction" +tag = "MEMW" +input = ["is_register", ["cast", "base_address", "DWordWL"], "value", "timestamp", "write2", "write4", "write8"] +output = "old" +multiplicity = ["-", "μ_read"] + +[[constraints.output]] +kind = "interaction" +tag = "MEMW" +input = ["is_register", ["cast", "base_address", "DWordWL"], "value", "timestamp", "write2", "write4", "write8"] +multiplicity = ["-", "μ_write"] diff --git a/spec/src/memw_register.toml b/spec/src/memw_register.toml new file mode 100644 index 000000000..3e7cdcf28 --- /dev/null +++ b/spec/src/memw_register.toml @@ -0,0 +1,141 @@ +name = "MEMW_R" + +# Variables + +[[variables.input]] +name = "address" +type = "Byte" +desc = "address of the register being accessed" +pad = 0 + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "timestamp at which the access takes place" +pad = 0 + +[[variables.input]] +name = "val" +type = "DWordWL" +desc = "value being written to this register" +pad = 0 + +[[variables.output]] +name = "old" +type = "DWordWL" +desc = "value of this register at `old_timestamp`." +pad = 0 + +[[variables.auxiliary]] +name = "old_timestamp_lo" +type = "Word" +desc = "the lower limb of `old_timestamp`" +pad = 0 + +[[variables.virtual]] +name = "old_timestamp" +type = "DWordWL" +desc = "timestamp at which this register was last accessed" +def = ["cast", ["arr", "old_timestamp_lo", ["idx", "timestamp", 1]], "DWordWL"] + +[[variables.virtual]] +name = "μ_sum" +type = "Bit" +desc = "" +def = ["+", "μ_read", "μ_write"] + +[[variables.multiplicity]] +name = "μ_read" +type = "Bit" +desc = "Whether we are performing a read (and hence return `out`)" +pad = 0 + +[[variables.multiplicity]] +name = "μ_write" +type = "Bit" +desc = "Whether we are performing a write (and hence not return `out`)" +pad = 0 + + + +# Assumptions + +[[assumptions]] +desc = "`IS_WORD[val[i]]`" +iter = ["i", 0, 1] +ref = "regw:a:val" + +[[assumptions]] +desc = "`IS_WORD[timestamp[i]]`" +iter = ["i", 0, 1] +ref = "regw:a:timestamp" + +# Constraints + +[[constraint_groups]] +name = "diff" + +[[constraints.diff]] +kind = "interaction" +tag = "IS_HALF" +input = [["-", ["idx", "timestamp", 0], ["idx", "old_timestamp", 0], 1]] +multiplicity = "μ_sum" +ref = "regw:c:diff" + + +[[constraint_groups]] +name = "multiplicities" + +[[constraints.multiplicities]] +kind = "template" +tag = "IS_BIT" +input = ["μ_read"] +ref = "regw:c:μ_read_is_bit" + +[[constraints.multiplicities]] +kind = "template" +tag = "IS_BIT" +input = ["μ_write"] +ref = "regw:c:μ_write_is_bit" + +[[constraints.multiplicities]] +kind = "template" +tag = "IS_BIT" +input = ["μ_sum"] +ref = "regw:c:μ_sum_is_bit" + +[[constraint_groups]] +name = "interactions" + +[[constraints.interactions]] +kind = "interaction" +tag = "memory" +input = [1, ["arr", ["cast", ["+", ["*", 2, "address"], "i"], "Word"], 0], "old_timestamp", ["idx", "old", "i"]] +iter = ["i", 0, 1] +multiplicity = "μ_sum" +ref = "regw:c:read_old" + +[[constraints.interactions]] +kind = "interaction" +tag = "memory" +input = [1, ["arr", ["cast", ["+", ["*", 2, "address"], "i"], "Word"], 0], "timestamp", ["idx", "val", "i"]] +iter = ["i", 0, 1] +multiplicity = ["-", "μ_sum"] +ref = "regw:c:write_val" + + +[[constraint_groups]] +name = "output" + +[[constraints.output]] +kind = "interaction" +tag = "MEMW" +input = [1, ["arr", ["cast", ["*", 2, "address"], "Word"], 0], ["arr", ["idx", "val", 0], ["idx", "val", 1], 0, 0, 0, 0, 0, 0], "timestamp", 1, 0, 0] +output = ["arr", ["idx", "old", 0], ["idx", "old", 1], 0, 0, 0, 0, 0, 0] +multiplicity = ["-", "μ_read"] + +[[constraints.output]] +kind = "interaction" +tag = "MEMW" +input = [1, ["arr", ["cast", ["*", 2, "address"], "Word"], 0], ["arr", ["idx", "val", 0], ["idx", "val", 1], 0, 0, 0, 0, 0, 0], "timestamp", 1, 0, 0] +multiplicity = ["-", "μ_write"] diff --git a/spec/src/mul.toml b/spec/src/mul.toml new file mode 100644 index 000000000..a798c682d --- /dev/null +++ b/spec/src/mul.toml @@ -0,0 +1,206 @@ +name = "MUL" + + +# Input + +[[variables.input]] +name = "lhs" +type = "DWordHL" +desc = "the left hand operator." +pad = 0 + +[[variables.input]] +name = "lhs_signed" +type = "Bit" +desc = "whether to interpret `lhs` as a signed integer (1) or not (0)." +pad = 0 + +[[variables.input]] +name = "rhs" +type = "DWordHL" +desc = "the right hand operator." +pad = 0 + +[[variables.input]] +name = "rhs_signed" +type = "Bit" +desc = "whether to interpret `rhs` as a signed integer (1) or not (0)." +pad = 0 + + +# Output + +[[variables.output]] +name = "lo" +type = "DWordHL" +desc = "the lower limbs of the (extended) multiplication result" +pad = 0 + +[[variables.output]] +name = "hi" +type = "DWordHL" +desc = "the upper limbs of the (extended) multiplication result" +pad = 0 + +# Auxiliary + +[[variables.auxiliary]] +name = "lhs_is_negative" +type = "Bit" +desc = "whether `lhs` is negative (1) or not (0)" +pad = 0 + +[[variables.auxiliary]] +name = "rhs_is_negative" +type = "Bit" +desc = "whether `rhs` is negative (1) or not (0)" +pad = 0 + +[[variables.auxiliary]] +name = "raw_product" +type = ["B51", 4] +desc = "raw multiplication output" +pad = 0 + +# Virtual + +[[variables.virtual]] +name = "lhs_ext" +type = ["Half", 8] +desc = "sign-extended value of `lhs`" +def = {idx="i", polys=[ + {iter=[0, 3], poly=["idx", "lhs", "i"]}, + {iter=[4, 7], poly=["*", 0xFFFF, "lhs_is_negative"]}, +]} + +[[variables.virtual]] +name = "rhs_ext" +type = ["Half", 8] +desc = "sign-extended value of `rhs`" +def = {idx="i", polys=[ + {iter=[0, 3], poly=["idx", "rhs", "i"]}, + {iter=[4, 7], poly=["*", 0xFFFF, "rhs_is_negative"]}, +]} + +[[variables.virtual]] +name = "res" +type = "QuadWL" +desc = "concatenation of `lo` and `hi`." +def = {idx="i", polys=[ + {iter=[0, 1], poly=["idx", ["cast", "lo", "DWordWL"], "i"]}, + {iter=[2, 3], poly=["idx", ["cast", "hi", "DWordWL"], ["-", "i", 2]]}, +]} + + +[[variables.virtual]] +name = "carry" +type = ["B20", 4] +desc = "carry values" +def = {idx="i", polys=[ + {iter=0, poly=["*", ["^", 2, -32], ["-", ["idx", "raw_product", 0], ["idx", "res", 0]]]}, + {iter=[1, 3], poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "raw_product", "i"], ["idx", "carry", ["-", "i", 1]]], ["idx", "res", "i"]]]}, +]} + +[[variables.virtual]] +name = "μ_sum" +type = "BaseField" +desc = "sum of multiplicies" +def = ["+", "μ_lo", "μ_hi"] + +# Multiplicity + +[[variables.multiplicity]] +name = "μ_lo" +type = "BaseField" +desc = "" +pad = 0 + +[[variables.multiplicity]] +name = "μ_hi" +type = "BaseField" +desc = "" +pad = 0 + +# Assumptions + +[[assumptions]] +desc = "`IS_HALF[lhs[i]]`" +iter = ["i", 0, 3] + +[[assumptions]] +desc = "`IS_HALF[rhs[i]]`" +iter = ["i", 0, 3] +ref = "mul:a:rhs" + +# Constraints + +[[constraint_groups]] +name = "def" + +[[constraints.def]] +kind = "template" +tag = "SIGN" +input = [["idx", "lhs", 3], "lhs_signed"] +output = "lhs_is_negative" +ref = "mul:c:lhs_is_negative" + +[[constraints.def]] +kind = "template" +tag = "SIGN" +input = [["idx", "rhs", 3], "rhs_signed"] +output = "rhs_is_negative" +ref = "mul:c:rhs_is_negative" + +[[constraints.def]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "lo", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ_sum" +ref = "mul:c:range_lo" + +[[constraints.def]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "hi", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ_sum" +ref = "mul:c:range_hi" + +[[constraints.def]] +kind = "interaction" +tag = "IS_B20" +input = [["idx", "carry", "i"]] +iter = ["i", 0, 3] +multiplicity = "μ_sum" +ref = "mul:c:carry" + +[[constraint_groups]] +name = "prod" + + +[[constraints.prod]] +kind = "arith" +constraint = "$#`raw_product[i]` = sum_(#`k`=0)^1 2^(16k) sum_(#`j`=0)^(2i+k) #`lhs_ext[j]` dot #`rhs_ext[2i+k-j]`$" +poly = ["-", ["sum", ["=", "k", 0], 1, ["*", ["^", 2, ["*", 16, "k"]], ["sum", ["=", "j", 0], ["+", ["*", 2, "i"], "k"], ["*", ["idx", "lhs_ext", "j"], ["idx", "rhs_ext", ["-", ["+", ["*", 2, "i"], "k"], "j"]]]]]], ["idx", "raw_product", "i"]] +iter = ["i", 0, 3] +ref = "mul:c:raw_product" + +[[constraint_groups]] +name = "lookup" + +[[constraints.lookup]] +kind = "interaction" +tag = "MUL" +input = ["lhs", "lhs_signed", "rhs", "rhs_signed", 0] +output = ["cast", "lo", "DWordWL"] +multiplicity = ["-", "μ_lo"] +ref = "mul:c:lookup_lo" + +[[constraints.lookup]] +kind = "interaction" +tag = "MUL" +input = ["lhs", "lhs_signed", "rhs", "rhs_signed", 1] +output = ["cast", "hi", "DWordWL"] +multiplicity = ["-", "μ_hi"] +ref = "mul:c:lookup_hi" diff --git a/spec/src/neg.toml b/spec/src/neg.toml new file mode 100644 index 000000000..2cd70f354 --- /dev/null +++ b/spec/src/neg.toml @@ -0,0 +1,53 @@ +name = "NEG" + +[[variables.condition]] +name = "cond" +type = "Bit" +desc = "condition on whether to negate x" + +[[variables.input]] +name = "x" +type = "DWordHL" +desc = "value to compute negation of" + +[[variables.output]] +name = "neg" +type = "DWordWL" +desc = "negation of `x` if $#`cond` != 0$; unconstrained otherwise." + +[[variables.virtual]] +name = "carry" +type = ["Bit", 2] +desc = "carries of the addition $#`neg` + #`x`$." +def = {idx="i", polys=[ + {iter=0, poly=["*", ["^", 2, -32], ["+", ["idx", ["cast", "x", "DWordWL"], 0], ["idx", "neg", 0]]]}, + {iter=1, poly=["*", ["^", 2, -32], ["+", ["idx", ["cast", "x", "DWordWL"], 1], ["idx", "neg", 1], ["idx", "carry", 0]]]} +]} + + +[[assumptions]] +desc = "`IS_HALF[x[i]]`" +iter = ["i", 0, 3] + +[[assumptions]] +desc = "`IS_BIT`" + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "interaction" +tag = "ZERO" +input = [["+", ["idx", "x", 0], ["idx", "x", 1]]] +output = ["not", ["idx", "carry", 0]] +multiplicity = "cond" +ref = "neg:c:carry_0" + +[[constraints.all]] +kind = "interaction" +tag = "ZERO" +input = [["+", ["idx", "x", 0], ["idx", "x", 1], ["idx", "x", 2], ["idx", "x", 3]]] +output = ["not", ["idx", "carry", 1]] +multiplicity = "cond" +ref = "neg:c:carry_1" diff --git a/spec/src/page.toml b/spec/src/page.toml new file mode 100644 index 000000000..dff939558 --- /dev/null +++ b/spec/src/page.toml @@ -0,0 +1,64 @@ +name = "PAGE" + +# Input + +[[variables.constant]] +name = "page" +type = "DWordWL" +desc = "Constant column containing the page base address; should be integrated into the constraints directly" + +[[variables.input]] +name = "offset" +type = "RowIndex" +desc = "The offset from the page base address." + +[[variables.input]] +name = "init" +type = "Byte" +desc = "The initial value of this address. Can be replaced by a constant zero for zero-initialization" + +[[variables.input]] +name = "fini" +type = "Byte" +desc = "The final value this address took" + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "The timestamp at which this address was last accessed" + +# Virtual + +[[variables.virtual]] +name = "address" +type = "DWordWL" +desc = "Adding `offset` to the page base address `page`. `page` is a constant with respect to a single instance of this table." +def = ["+", "page", ["*", "offset", ["cast", 1, "DWordWL"]]] + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "interaction" +tag = "IS_BYTE" +input = ["init"] +multiplicity = 1 + +[[constraints.all]] +kind = "interaction" +tag = "IS_BYTE" +input = ["fini"] +multiplicity = 1 + +[[constraints.all]] +kind = "interaction" +tag = "memory" +input = [0, "address", ["cast", 0, "DWordWL"], "init"] +multiplicity = -1 + +[[constraints.all]] +kind = "interaction" +tag = "memory" +input = [0, "address", "timestamp", "fini"] +multiplicity = 1 diff --git a/spec/src/shift.toml b/spec/src/shift.toml new file mode 100644 index 000000000..bbe22a5d9 --- /dev/null +++ b/spec/src/shift.toml @@ -0,0 +1,296 @@ +name = "SHIFT" + +# Input + +[[variables.input]] +name = "in" +type = "DWordHL" +desc = "The value being shifted" +pad = 0 + +[[variables.input]] +name = "shift" +type = "Byte" +desc = "Number of bits to shift `in` by." +pad = 0 + +[[variables.input]] +name = "direction" +type = "Bit" +desc = "Whether to shift left (0) or right (1)." +pad = 0 + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Whether to interpret `in` as a signed integer." +pad = 0 + +[[variables.input]] +name = "word_instr" +type = "Bit" +desc = "Whether this is a Word-instruction (1) or not (0)." +pad = 0 + + +# Output + +[[variables.output]] +name = "out" +type = "DWordWL" +desc = "$#`in <>/>>>` (#`shift` mod 32 dot (2 - #`word_instr`))$" +pad = 0 + +# Auxiliary + +[[variables.auxiliary]] +name = "is_negative" +type = "Bit" +desc = "Whether `in` is negative" +pad = 0 + +[[variables.auxiliary]] +name = "bit_shift" +type = "Byte" +desc = "Value by which to shift `in` to obtain `X` and `Y`" +pad = 0 + +[[variables.auxiliary]] +name = "zbs" +type = "Bit" +desc = "Whether `bit_shift` is zero (1) or not (0)." +pad = 1 + +[[variables.auxiliary]] +name = "X" +type = ["Half", 5] +desc = "scratch variable." +pad = ["arr", 0, 0, 0, 0, 0] + +[[variables.auxiliary]] +name = "Y" +type = ["Half", 4] +desc = "scratch variable." +pad = ["arr", 0, 0, 0, 0] + +[[variables.auxiliary]] +name = "limb_shift_raw" +type = ["Bit", 3] +desc = "One-hot vector indicating whether $floor.l #`shift` / 16 floor.r equiv i mod s$, where $s = 2$ when $#`word_instr` = 1$ and $4$ otherwise. These columns store the first 3 values, and the 4th is derived from the one-hot property." +pad = ["arr", 0, 0, 0] + +# Virtual + +[[variables.virtual]] +name = "limb_shift" +type = ["Bit", 4] +desc = "" +def = {idx = "i", polys = [ + {iter = [0, 2], poly = ["idx", "limb_shift_raw", "i"]}, + {iter = 3, poly = ["-", 1, ["sum", ["=", "j", 0], 2, ["idx", "limb_shift_raw", "j"]]]}, +]} + +[[variables.virtual]] +name = "extension" +type = "Half" +desc = "sign extension of `in`." +def = ["*", 65535, "is_negative"] + +[[variables.virtual]] +name = "left" +type = "Bit" +desc = "Whether to perform a left-shift." +def = ["-", "μ", "direction"] + +[[variables.virtual]] +name = "right" +type = "Bit" +desc = "Whether to perform a right-shift." +def = "direction" + +[[variables.virtual]] +name = "intra_limb_left" +type = "DWordHL" +desc = "`in << (shift % 16)` if `left`" +def = {idx="i", polys=[ + {iter=0, poly=["idx", "X", 0]}, + {iter=[1, 3], poly=["+", ["idx", "X", "i"], ["idx", "Y", ["-", "i", 1]]]}, +]} + +[[variables.virtual]] +name = "intra_limb_right" +type = "DWordHL" +desc = "`in >>> (shift % 16)` if `right` and `signed`;\\ `in >> (shift % 16)` if `right` and `!signed`" +def = {idx="i", iter=[0, 3], poly=["+", ["idx", "Y", "i"], ["idx", "X", ["+", "i", 1]]]} + +[[variables.virtual]] +name = "shifted" +type = "DWordHL" +desc = "$#`in <>/>>>` (#`shift` mod 32 dot (2 - #`word_instr`))$" +def = {idx="i", iter=[0, 3], poly=["+", ["*", "left", ["sum", ["=", "j", 0], "i", ["*", ["idx", "limb_shift", "j"], ["idx", "intra_limb_left", ["-", "i", "j"]]]]], ["*", "right", ["+", ["sum", ["=", "j", 0], ["-", 3, "i"], ["*", ["idx", "limb_shift", "j"], ["idx", "intra_limb_right", ["+", "i", "j"]]]], ["*", "extension", ["sum", ["=", "j", ["-", 4, "i"]], 3, ["idx", "limb_shift", "j"]]]]]]} + +# Multiplicities + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" +pad = 0 + + + +# Assumptions + +[[assumptions]] +desc = "`IS_HALF[in[i]]`" +iter = ["i", 0, 3] +ref = "shift:a:range_in" + +[[assumptions]] +desc = "`IS_BYTE[shift]`" +ref = "shift:a:range_shift" + +[[assumptions]] +desc = "`IS_BIT`" +ref = "shift:a:direction" + +[[assumptions]] +desc = "`IS_BIT`" +ref = "shift:a:signed" + +[[assumptions]] +desc = "`IS_BIT`" +ref = "shift:a:word_instr" + +# Constraints + +[[constraint_groups]] +name = "left_flag" + +[[constraints.left_flag]] +kind = "arith" +desc = "enforces `left` is `Bit`." +constraint = "$#`direction` => #`μ` = 1$" +poly = ["*", "direction", ["not", "μ"]] +ref = "shift:c:direction_implies_mu" + + +[[constraint_groups]] +name = "is_negative" + +[[constraints.is_negative]] +kind = "interaction" +tag = "MSB16" +input = [["idx", "in", 3]] +output = "is_negative" +multiplicity = "signed" +ref = "shift:c:is_negative_if_signed" + + +[[constraint_groups]] +name = "bit_shift" + +[[constraints.bit_shift]] +kind = "interaction" +tag = "AND_BYTE" +input = ["shift", 0x0F] +output = "bit_shift" +ref = "shift:c:bit_shift_if_left" +multiplicity = "left" + +[[constraints.bit_shift]] +kind = "interaction" +tag = "AND_BYTE" +input = [["-", ["^", 2, 8], ["*", 16, "zbs"], "shift"], 0x0F] +output = "bit_shift" +ref = "shift:c:bit_shift_if_right" +multiplicity = "right" + +[[constraints.bit_shift]] +kind = "interaction" +tag = "ZERO" +input = ["bit_shift"] +output = "zbs" +ref = "shift:c:zbs" +multiplicity = "μ" + + +[[constraint_groups]] +name = "intra_limb_shift" + +[[constraints.intra_limb_shift]] +kind = "interaction" +tag = "HWSL" +input = [["idx", "in", "i"], "bit_shift"] +output = ["arr", ["idx", "X", "i"], ["idx", "Y", "i"]] +iter = ["i", 0, 3] +ref = "shift:c:hwsl_if_not_zero" +multiplicity = ["not", "zbs"] + +[[constraints.intra_limb_shift]] +kind = "arith" +constraint = "$#`zbs` => #`X[i]` = #`in[i]` dot #`left`$" +poly = ["*", "zbs", ["-", ["idx", "X", "i"], ["*", ["idx", "in", "i"], "left"]]] +iter = ["i", 0, 3] +ref = "shift:c:zbs_implies_X" + +[[constraints.intra_limb_shift]] +kind = "arith" +constraint = "$#`zbs` => #`Y[i]` = #`in[i]` dot #`right`$" +poly = ["*", "zbs", ["-", ["idx", "Y", "i"], ["*", ["idx", "in", "i"], "right"]]] +iter = ["i", 0, 3] +ref = "shift:c:zbs_implies_Y" + +[[constraints.intra_limb_shift]] +kind = "interaction" +tag = "HWSL" +input = ["extension", "bit_shift"] +output = ["arr", ["idx", "X", 4], ["-", "extension", ["idx", "X", 4]]] +ref = "shift:c:hwsl_x4_if_not_zero" +multiplicity = ["not", "zbs"] + +[[constraints.intra_limb_shift]] +kind = "arith" +constraint = "$#`zbs` => #`X[4]` = 0$" +poly = ["*", "zbs", ["idx", "X", 4]] +ref = "shift:c:zbs_implies_X_4" + + +[[constraint_groups]] +name = "limb_shifting" + +[[constraints.limb_shifting]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "limb_shift", "i"]] +iter = ["i", 0, 3] +ref = "shift:c:limb_shift_is_bit" + +[[constraints.limb_shifting]] +kind = "interaction" +tag = "AND_BYTE" +input = ["shift", ["-", 0x30, ["*", 0x20, "word_instr"]]] +output = ["+", ["-", 1, ["idx", "limb_shift", 0]], ["*", 15, ["idx", "limb_shift", 1]], ["*", 31, ["idx", "limb_shift", 2]], ["*", 47, ["idx", "limb_shift", 3]]] +ref = "shift:c:limb_shift_lookup" +multiplicity = "μ" + +[[constraints.limb_shifting]] +kind = "arith" +constraint = "$#`out[:2]` = #`shifted[:4]`$" +poly = ["-", ["idx", "out", "i"], ["idx", ["cast", "shifted", "DWordWL"], "i"]] +iter = ["i", 0, 1] +ref = "shift:c:out_eq_shifted" + + +# Lookups + +[[constraint_groups]] +name = "lookups" + +[[constraints.lookups]] +kind = "interaction" +tag = "SHIFT" +input = ["in", "shift", "direction", "signed", "word_instr"] +output = "out" +multiplicity = ["-", "μ"] +ref = "shift:c:lookup" diff --git a/spec/src/sign.toml b/spec/src/sign.toml new file mode 100644 index 000000000..24e99bd0e --- /dev/null +++ b/spec/src/sign.toml @@ -0,0 +1,39 @@ +name = "SIGN" + +[[variables.input]] +name = "X" +type = "Half" +desc = "Value for which to extract its sign." + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Whether `X` represents a signed value (1) or not (0)" + +[[variables.output]] +name = "sign" +type = "Bit" +desc = "Sign of `X`" + + +[[assumptions]] +desc = "`IS_BIT`" + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "interaction" +tag = "MSB16" +input = ["X"] +output = "sign" +multiplicity = "signed" +ref = "sign:c:sign_if_signed" + +[[constraints.all]] +kind = "arith" +constraint = "$not#`signed` => #`sign` = 0$" +poly = ["*", ["not", "signed"], "sign"] +ref = "sign:c:sign_if_unsigned" + diff --git a/spec/src/signatures.toml b/spec/src/signatures.toml new file mode 100644 index 000000000..17ecd3933 --- /dev/null +++ b/spec/src/signatures.toml @@ -0,0 +1,189 @@ +# cond => IS_BIT +[[signatures]] +tag = "IS_BIT" +kind = "template" +input = ["BaseField"] +cond = "BaseField" + +# cond => ADD +[[signatures]] +tag = "ADD" +kind = "template" +input = ["DWordWL", "DWordWL"] +output = "DWordWL" +cond = "BaseField" + +# cond => SUB +[[signatures]] +tag = "SUB" +kind = "template" +input = ["DWordWL", "DWordWL"] +output = "DWordWL" +cond = "BaseField" + +# cond => NEG +[[signatures]] +tag = "NEG" +kind = "template" +input = ["DWordHL"] +output = "DWordWL" +cond = "Bit" + +# SIGN +[[signatures]] +tag = "SIGN" +kind = "template" +input = ["Half", "Bit"] +output = "Bit" + +# DECODE[pc, imm, packed_decode] +[[signatures]] +tag = "DECODE" +kind = "interaction" +input = ["DWordWL", "DWordWL", "BaseField"] + +# SHIFT[out; in, shift, direction, signed, word_instr] +[[signatures]] +tag = "SHIFT" +kind = "interaction" +input = ["DWordHL", "Byte", "Bit", "Bit", "Bit"] +output = "DWordWL" + +# BRANCH[next_pc; pc, offset, register, JALR] +[[signatures]] +tag = "BRANCH" +kind = "interaction" +input = ["DWordWL", "DWordWL", "DWordWL", "Bit"] +output = "DWordWL" + +# MEMW[old; is_register, base_address, value, timestamp, write2, write4, write8] +[[signatures]] +tag = "MEMW" +kind = "interaction" +input = ["Bit", "DWordWL", ["BaseField", 8], "DWordWL", "Bit", "Bit", "Bit"] +output = ["BaseField", 8] + +# MEMW[is_register, base_address, value, timestamp, write2, write4, write8] +[[signatures]] +tag = "MEMW" +kind = "interaction" +input = ["Bit", "DWordWL", ["BaseField", 8], "DWordWL", "Bit", "Bit", "Bit"] + +# LT[lt; lhs, rhs, signed] +[[signatures]] +tag = "LT" +kind = "interaction" +input = ["DWordWL", "DWordWL", "Bit"] +output = "Bit" + +# MUL[lo/hi; lhs, lhs_signed, rhs, rhs_signed, 0/1] +[[signatures]] +tag = "MUL" +kind = "interaction" +input = ["DWordHL", "Bit", "DWordHL", "Bit", "Bit"] +output = "DWordWL" + +# DVRM[q/r; n, d, signed, 0/1] +[[signatures]] +tag = "DVRM" +kind = "interaction" +input = ["DWordHL", "DWordHL", "Bit", "Bit"] +output = "DWordWL" + +# LOAD[res; base_address, timestamp, read2, read4, read8, signed] +[[signatures]] +tag = "LOAD" +kind = "interaction" +input = ["DWordWL", "DWordWL", "Bit", "Bit", "Bit", "Bit"] +output = "DWordWL" + +# ECALL[timestamp, syscallnr] +[[signatures]] +tag = "ECALL" +kind = "interaction" +input = ["DWordWL", "DWordWL"] + +# CNB[timestamp, index, address, count] +[[signatures]] +tag = "CNB" +kind = "interaction" +input = ["DWordWL", "BaseField", "DWordWL", "DWordWL"] + +# COMMIT[index, value] +[[signatures]] +tag = "COMMIT" +kind = "interaction" +input = ["BaseField", "Byte"] + +# AND_BYTE[res; X, Y] +[[signatures]] +tag = "AND_BYTE" +kind = "interaction" +input = ["Byte", "Byte"] +output = "Byte" + +# OR_BYTE[res; X, Y] +[[signatures]] +tag = "OR_BYTE" +kind = "interaction" +input = ["Byte", "Byte"] +output = "Byte" + +# XOR_BYTE[res; X, Y] +[[signatures]] +tag = "XOR_BYTE" +kind = "interaction" +input = ["Byte", "Byte"] +output = "Byte" + +# MSB8[msb; X] +[[signatures]] +tag = "MSB8" +kind = "interaction" +input = ["Byte"] +output = "Bit" + +# MSB16[msb; X] +[[signatures]] +tag = "MSB16" +kind = "interaction" +input = ["Half"] +output = "Bit" + +# ZERO[is_zero; X] +[[signatures]] +tag = "ZERO" +kind = "interaction" +input = ["B20"] +output = "Bit" + +# IS_BYTE[X] +[[signatures]] +tag = "IS_BYTE" +kind = "interaction" +input = ["Byte"] + +# IS_HALF[X] +[[signatures]] +tag = "IS_HALF" +kind = "interaction" +input = ["Half"] + +# IS_B20[X] +[[signatures]] +tag = "IS_B20" +kind = "interaction" +input = ["B20"] + +# HWSL[res; X, shift] +[[signatures]] +tag = "HWSL" +kind = "interaction" +input = ["Half", "B4"] +output = ["Half", 2] + +# The actual memory tokens, see MEMW and PAGE +[[signatures]] +tag = "memory" +kind = "interaction" +input = ["Bit", "DWordWL", "DWordWL", "BaseField"] diff --git a/spec/templates/page.typ b/spec/templates/page.typ new file mode 100644 index 000000000..4ec7b27ac --- /dev/null +++ b/spec/templates/page.typ @@ -0,0 +1,167 @@ +// This is important for shiroa to produce a responsive layout +// and multiple targets. +#import "@preview/shiroa:0.3.1": ( + get-page-width, is-html-target, is-pdf-target, is-web-target, plain-text, shiroa-sys-target, templates, +) +#import templates: * + +/// The site theme to use. If we renders to static HTML, it is suggested to use `starlight`. +/// otherwise, since `starlight` with dynamic SVG HTML is not supported, `mdbook` is used. +/// The `is-html-target(exclude-wrapper: true)` is currently a bit internal so you shouldn't use it other place. +#let web-theme = if is-html-target(exclude-wrapper: true) { "starlight" } else { "mdbook" } +#let is-starlight-theme = web-theme == "starlight" + +// Metadata +#let page-width = get-page-width() +#let is-html-target = is-html-target() +#let is-pdf-target = is-pdf-target() +#let is-web-target = is-web-target() +#let sys-is-html-target = ("target" in dictionary(std)) + +// Theme (Colors) +#let themes = theme-box-styles-from(toml("theme-style.toml"), read: it => read(it)) +#let ( + default-theme: ( + style: theme-style, + is-dark: is-dark-theme, + is-light: is-light-theme, + main-color: main-color, + dash-color: dash-color, + code-extra-colors: code-extra-colors, + ), +) = themes; +#let ( + default-theme: default-theme, +) = themes; +#let theme-box = theme-box.with(themes: themes) + +// Fonts +#let main-font = ( + // "Charter", + // "Source Han Serif SC", + // "Source Han Serif TC", + // shiroa's embedded font + "Libertinus Serif", +) +#let code-font = ( + // "BlexMono Nerd Font Mono", + // shiroa's embedded font + "DejaVu Sans Mono", +) + +// Sizes +#let main-size = if is-web-target { + 16pt +} else { + 10.5pt +} +#let heading-sizes = if is-web-target { + (2, 1.5, 1.17, 1, 0.83).map(it => it * main-size) +} else { + (26pt, 22pt, 14pt, 12pt, main-size) +} +#let list-indent = 0.5em + +// Put your custom CSS here. +#let extra-css = ```css +.site-title { + font-size: 1.2rem; + font-weight: 600; + font-style: italic; +} +``` + +/// The project show rule that is used by all pages. +/// +/// Example: +/// ```typ +/// #show: project +/// ``` +/// +/// - title (str): The title of the page. +/// - description (auto): The description of the page. +/// - If description is `auto`, it will be generated from the plain body. +/// - If description is `none`, an error is raised to force migration. In future, `none` will mean the description is not generated. +/// - Hint: use `""` to generate an empty description. +/// - authors (array | str): The author(s) of the page. +/// - kind (str): The kind of the page. +/// - cond (function): A predicate that can be used inside of `context` +/// to check whether display rules should be applied. +/// Useful for including other chapters invisibly to figure out information about their labels +/// - plain-body (content): The plain body of the page. +#let project(title: "Typst Book", description: auto, authors: (), kind: "page", cond: none, plain-body) = { + // set basic document metadata + set document( + author: authors, + title: title, + ) if not is-pdf-target + + // set web/pdf page properties + set page( + numbering: none, + number-align: center, + width: page-width, + ) if not (sys-is-html-target or is-html-target) + + // remove margins for web target + set page( + margin: ( + // reserved beautiful top margin + top: 20pt, + // reserved for our heading style. + // If you apply a different heading style, you may remove it. + left: 20pt, + // Typst is setting the page's bottom to the baseline of the last line of text. So bad :(. + bottom: 0.5em, + // remove rest margins. + rest: 0pt, + ), + height: auto, + ) if is-web-target and not is-html-target + + let common = ( + web-theme: web-theme, + ) + + show: template-rules.with( + book-meta: include "/book.typ", + title: title, + description: description, + plain-body: plain-body, + extra-assets: (extra-css,), + ..common, + ) + + // Set main text + set text( + font: main-font, + size: main-size, + fill: main-color, + lang: "en", + ) + + context if cond() { + // markup setting + show: markup-rules.with( + ..common, + themes: themes, + heading-sizes: heading-sizes, + list-indent: list-indent, + main-size: main-size, + ) + + // math setting + show: equation-rules.with(..common, theme-box: theme-box) + // code block setting + show: code-block-rules.with(..common, themes: themes, code-font: code-font) + + // Main body. + set par(justify: true) + + plain-body + } else { + plain-body + } +} + +#let part-style = heading diff --git a/spec/templates/theme-style.toml b/spec/templates/theme-style.toml new file mode 100644 index 000000000..128d0b171 --- /dev/null +++ b/spec/templates/theme-style.toml @@ -0,0 +1,30 @@ + +[light] +color-scheme = "light" +main-color = "#000" +dash-color = "#20609f" +code-theme = "" + +[rust] +color-scheme = "light" +main-color = "#262625" +dash-color = "#2b79a2" +code-theme = "" + +[coal] +color-scheme = "dark" +main-color = "#98a3ad" +dash-color = "#2b79a2" +code-theme = "tokyo-night.tmTheme" + +[navy] +color-scheme = "dark" +main-color = "#bcbdd0" +dash-color = "#2b79a2" +code-theme = "tokyo-night.tmTheme" + +[ayu] +color-scheme = "dark" +main-color = "#c5c5c5" +dash-color = "#0096cf" +code-theme = "tokyo-night.tmTheme" diff --git a/spec/templates/tokyo-night.tmTheme b/spec/templates/tokyo-night.tmTheme new file mode 100644 index 000000000..24829e7c4 --- /dev/null +++ b/spec/templates/tokyo-night.tmTheme @@ -0,0 +1,1308 @@ + + + + + name + Tokyo Night + settings + + + settings + + caret + #c0caf5 + selection + #515c7e4d + lineHighlight + #1e202e + foreground + #a9b1d6 + background + #1a1b26 + invisibles + #363b54 + + + + name + Italics - Comments, Storage, Keyword Flow, Vue attributes, Decorators + scope + comment,meta.var.expr storage.type,keyword.control.flow,keyword.control.return,meta.directive.vue punctuation.separator.key-value.html,meta.directive.vue entity.other.attribute-name.html,tag.decorator.js entity.name.tag.js,tag.decorator.js punctuation.definition.tag.js,storage.modifier + settings + + fontStyle + italic + + + + name + Fix YAML block scalar + scope + keyword.control.flow.block-scalar.literal + settings + + fontStyle + + + + + name + Comment + scope + comment,comment.block.documentation,punctuation.definition.comment,comment.block.documentation punctuation + settings + + foreground + #444b6a + + + + name + Comment Doc + scope + keyword.operator.assignment.jsdoc,comment.block.documentation variable,comment.block.documentation storage,comment.block.documentation keyword,comment.block.documentation support,comment.block.documentation markup,comment.block.documentation markup.inline.raw.string.markdown,meta.other.type.phpdoc.php keyword.other.type.php,meta.other.type.phpdoc.php support.other.namespace.php,meta.other.type.phpdoc.php punctuation.separator.inheritance.php,meta.other.type.phpdoc.php support.class,keyword.other.phpdoc.php,log.date + settings + + foreground + #5a638c + + + + name + Comment Doc Emphasized + scope + meta.other.type.phpdoc.php support.class,comment.block.documentation storage.type,comment.block.documentation punctuation.definition.block.tag,comment.block.documentation entity.name.type.instance + settings + + foreground + #646e9c + + + + name + Number, Boolean, Undefined, Null + scope + variable.other.constant,punctuation.definition.constant,constant.language,constant.numeric,support.constant + settings + + foreground + #ff9e64 + + + + name + String, Symbols + scope + string,constant.other.symbol,constant.other.key,meta.attribute-selector + settings + + fontStyle + + foreground + #9ece6a + + + + name + Colors + scope + constant.other.color,constant.other.color.rgb-value.hex punctuation.definition.constant + settings + + foreground + #9aa5ce + + + + name + Invalid + scope + invalid,invalid.illegal + settings + + foreground + #ff5370 + + + + name + Invalid deprecated + scope + invalid.deprecated + settings + + foreground + #bb9af7 + + + + name + Storage Type + scope + storage.type + settings + + foreground + #bb9af7 + + + + name + Storage - modifier, var, const, let + scope + meta.var.expr storage.type,storage.modifier + settings + + foreground + #9d7cd8 + + + + name + Interpolation, PHP tags, Smarty tags + scope + punctuation.definition.template-expression,punctuation.section.embedded,meta.embedded.line.tag.smarty,support.constant.handlebars,punctuation.section.tag.twig + settings + + foreground + #7dcfff + + + + name + Blade, Twig, Smarty Handlebars keywords + scope + keyword.control.smarty,keyword.control.twig,support.constant.handlebars keyword.control,keyword.operator.comparison.twig,keyword.blade,entity.name.function.blade + settings + + foreground + #0db9d7 + + + + name + Spread + scope + keyword.operator.spread,keyword.operator.rest + settings + + foreground + #f7768e + fontStyle + bold + + + + name + Operator, Misc + scope + keyword.operator,keyword.control.as,keyword.other,keyword.operator.bitwise.shift,punctuation,expression.embbeded.vue punctuation.definition.tag,text.html.twig meta.tag.inline.any.html,meta.tag.template.value.twig meta.function.arguments.twig,meta.directive.vue punctuation.separator.key-value.html,punctuation.definition.constant.markdown,punctuation.definition.string,punctuation.support.type.property-name,text.html.vue-html meta.tag,meta.attribute.directive,punctuation.definition.keyword,punctuation.terminator.rule,punctuation.definition.entity,punctuation.separator.inheritance.php,keyword.other.template,keyword.other.substitution,entity.name.operator,meta.property-list punctuation.separator.key-value,meta.at-rule.mixin punctuation.separator.key-value,meta.at-rule.function variable.parameter.url + settings + + foreground + #89ddff + + + + name + Import, Export, From, Default + scope + keyword.control.import,keyword.control.export,keyword.control.from,keyword.control.default,meta.import keyword.other + settings + + foreground + #7dcfff + + + + name + Keyword + scope + keyword,keyword.control,keyword.other.important + settings + + foreground + #bb9af7 + + + + name + Keyword SQL + scope + keyword.other.DML + settings + + foreground + #7dcfff + + + + name + Keyword Operator Logical, Arrow, Ternary, Comparison + scope + keyword.operator.logical,storage.type.function,keyword.operator.bitwise,keyword.operator.ternary,keyword.operator.comparison,keyword.operator.relational,keyword.operator.or.regexp + settings + + foreground + #bb9af7 + + + + name + Tag + scope + entity.name.tag + settings + + foreground + #f7768e + + + + name + Tag - Custom + scope + entity.name.tag support.class.component,meta.tag.custom entity.name.tag,meta.tag + settings + + foreground + #de5971 + + + + name + Tag Punctuation + scope + punctuation.definition.tag + settings + + foreground + #ba3c97 + + + + name + Globals, PHP Constants, etc + scope + constant.other.php,variable.other.global.safer,variable.other.global.safer punctuation.definition.variable,variable.other.global,variable.other.global punctuation.definition.variable,constant.other + settings + + foreground + #e0af68 + + + + name + Variables + scope + variable,support.variable,string constant.other.placeholder,variable.parameter.handlebars,variable.other.object + settings + + foreground + #c0caf5 + + + + name + Variable Array Key + scope + meta.array.literal variable + settings + + foreground + #7dcfff + + + + name + Object Key + scope + meta.object-literal.key,entity.name.type.hcl,string.alias.graphql,string.unquoted.graphql,string.unquoted.alias.graphql,meta.group.braces.curly constant.other.object.key.js string.unquoted.label.js,meta.field.declaration.ts variable.object.property,meta.block entity.name.label + settings + + foreground + #73daca + + + + name + Object Property + scope + variable.other.property,support.variable.property,support.variable.property.dom,meta.function-call variable.other.object.property + settings + + foreground + #7dcfff + + + + name + Object Property + scope + variable.other.object.property + settings + + foreground + #c0caf5 + + + + name + Object Literal Member lvl 3 (Vue Prop Validation) + scope + meta.objectliteral meta.object.member meta.objectliteral meta.object.member meta.objectliteral meta.object.member meta.object-literal.key + settings + + foreground + #41a6b5 + + + + name + C-related Block Level Variables + scope + source.cpp meta.block variable.other + settings + + foreground + #f7768e + + + + name + Other Variable + scope + support.other.variable + settings + + foreground + #f7768e + + + + name + Methods + scope + meta.class-method.js entity.name.function.js,entity.name.method.js,variable.function.constructor,keyword.other.special-method,storage.type.cs + settings + + foreground + #7aa2f7 + + + + name + Function Definition + scope + entity.name.function,variable.other.enummember,meta.function-call,meta.function-call entity.name.function,variable.function,meta.definition.method entity.name.function,meta.object-literal entity.name.function + settings + + foreground + #7aa2f7 + + + + name + Function Argument + scope + variable.parameter.function.language.special,variable.parameter,meta.function.parameters punctuation.definition.variable,meta.function.parameter variable + settings + + foreground + #e0af68 + + + + name + Constant, Tag Attribute + scope + keyword.other.type.php,storage.type.php,constant.character,constant.escape,keyword.other.unit + settings + + foreground + #bb9af7 + + + + name + Variable Definition + scope + meta.definition.variable variable.other.constant,meta.definition.variable variable.other.readwrite,variable.declaration.hcl variable.other.readwrite.hcl,meta.mapping.key.hcl variable.other.readwrite.hcl,variable.other.declaration + settings + + foreground + #bb9af7 + + + + name + Inherited Class + scope + entity.other.inherited-class + settings + + fontStyle + + foreground + #bb9af7 + + + + name + Class, Support, DOM, etc + scope + support.class,support.type,variable.other.readwrite.alias,support.orther.namespace.use.php,meta.use.php,support.other.namespace.php,support.type.sys-types,support.variable.dom,support.constant.math,support.type.object.module,support.constant.json,entity.name.namespace,meta.import.qualifier,variable.other.constant.object + settings + + foreground + #0db9d7 + + + + name + Class Name + scope + entity.name + settings + + foreground + #c0caf5 + + + + name + Support Function + scope + support.function + settings + + foreground + #0db9d7 + + + + name + CSS Class and Support + scope + source.css support.type.property-name,source.sass support.type.property-name,source.scss support.type.property-name,source.less support.type.property-name,source.stylus support.type.property-name,source.postcss support.type.property-name,support.type.property-name.css,support.type.vendored.property-name,support.type.map.key + settings + + foreground + #7aa2f7 + + + + name + CSS Font + scope + support.constant.font-name,meta.definition.variable + settings + + foreground + #9ece6a + + + + name + CSS Class + scope + entity.other.attribute-name.class,meta.at-rule.mixin.scss entity.name.function.scss + settings + + foreground + #9ece6a + + + + name + CSS ID + scope + entity.other.attribute-name.id + settings + + foreground + #fc7b7b + + + + name + CSS Tag + scope + entity.name.tag.css + settings + + foreground + #0db9d7 + + + + name + CSS Tag Reference, Pseudo & Class Punctuation + scope + entity.other.attribute-name.pseudo-class punctuation.definition.entity,entity.other.attribute-name.pseudo-element punctuation.definition.entity,entity.other.attribute-name.class punctuation.definition.entity,entity.name.tag.reference + settings + + foreground + #e0af68 + + + + name + CSS Punctuation + scope + meta.property-list + settings + + foreground + #9abdf5 + + + + name + CSS at-rule fix + scope + meta.property-list meta.at-rule.if,meta.at-rule.return variable.parameter.url,meta.property-list meta.at-rule.else + settings + + foreground + #ff9e64 + + + + name + CSS Parent Selector Entity + scope + entity.other.attribute-name.parent-selector-suffix punctuation.definition.entity.css + settings + + foreground + #73daca + + + + name + CSS Punctuation comma fix + scope + meta.property-list meta.property-list + settings + + foreground + #9abdf5 + + + + name + SCSS @ + scope + meta.at-rule.mixin keyword.control.at-rule.mixin,meta.at-rule.include entity.name.function.scss,meta.at-rule.include keyword.control.at-rule.include + settings + + foreground + #bb9af7 + + + + name + SCSS Mixins, Extends, Include Keyword + scope + keyword.control.at-rule.include punctuation.definition.keyword,keyword.control.at-rule.mixin punctuation.definition.keyword,meta.at-rule.include keyword.control.at-rule.include,keyword.control.at-rule.extend punctuation.definition.keyword,meta.at-rule.extend keyword.control.at-rule.extend,entity.other.attribute-name.placeholder.css punctuation.definition.entity.css,meta.at-rule.media keyword.control.at-rule.media,meta.at-rule.mixin keyword.control.at-rule.mixin,meta.at-rule.function keyword.control.at-rule.function,keyword.control punctuation.definition.keyword + settings + + foreground + #9d7cd8 + + + + name + SCSS Include Mixin Argument + scope + meta.property-list meta.at-rule.include + settings + + foreground + #c0caf5 + + + + name + CSS value + scope + support.constant.property-value + settings + + foreground + #ff9e64 + + + + name + Sub-methods + scope + entity.name.module.js,variable.import.parameter.js,variable.other.class.js + settings + + foreground + #c0caf5 + + + + name + Language methods + scope + variable.language + settings + + foreground + #f7768e + + + + name + Variable punctuation + scope + variable.other punctuation.definition.variable + settings + + foreground + #c0caf5 + + + + name + Keyword this with Punctuation, ES7 Bind Operator + scope + source.js constant.other.object.key.js string.unquoted.label.js,variable.language.this punctuation.definition.variable,keyword.other.this + settings + + foreground + #f7768e + + + + name + HTML Attributes + scope + entity.other.attribute-name,text.html.basic entity.other.attribute-name.html,text.html.basic entity.other.attribute-name + settings + + foreground + #bb9af7 + + + + name + HTML Character Entity + scope + text.html constant.character.entity + settings + + foreground + #0DB9D7 + + + + name + Vue (Vetur / deprecated) Template attributes + scope + entity.other.attribute-name.id.html,meta.directive.vue entity.other.attribute-name.html + settings + + foreground + #bb9af7 + + + + name + CSS ID's + scope + source.sass keyword.control + settings + + foreground + #7aa2f7 + + + + name + CSS psuedo selectors + scope + entity.other.attribute-name.pseudo-class,entity.other.attribute-name.pseudo-element,entity.other.attribute-name.placeholder,meta.property-list meta.property-value + settings + + foreground + #bb9af7 + + + + name + Inserted + scope + markup.inserted + settings + + foreground + #449dab + + + + name + Deleted + scope + markup.deleted + settings + + foreground + #914c54 + + + + name + Changed + scope + markup.changed + settings + + foreground + #6183bb + + + + name + Regular Expressions + scope + string.regexp + settings + + foreground + #b4f9f8 + + + + name + Regular Expressions - Punctuation + scope + punctuation.definition.group + settings + + foreground + #f7768e + + + + name + Regular Expressions - Character Class + scope + constant.other.character-class.regexp + settings + + foreground + #bb9af7 + + + + name + Regular Expressions - Character Class Set + scope + constant.other.character-class.set.regexp,punctuation.definition.character-class.regexp + settings + + foreground + #e0af68 + + + + name + Regular Expressions - Quantifier + scope + keyword.operator.quantifier.regexp + settings + + foreground + #89ddff + + + + name + Regular Expressions - Backslash + scope + constant.character.escape.backslash + settings + + foreground + #c0caf5 + + + + name + Escape Characters + scope + constant.character.escape + settings + + foreground + #89ddff + + + + name + Decorators + scope + tag.decorator.js entity.name.tag.js,tag.decorator.js punctuation.definition.tag.js + settings + + foreground + #7aa2f7 + + + + name + CSS Units + scope + keyword.other.unit + settings + + foreground + #f7768e + + + + name + JSON Key - Level 0 + scope + source.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #7aa2f7 + + + + name + JSON Key - Level 1 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #0db9d7 + + + + name + JSON Key - Level 2 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #7dcfff + + + + name + JSON Key - Level 3 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #bb9af7 + + + + name + JSON Key - Level 4 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #e0af68 + + + + name + JSON Key - Level 5 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #0db9d7 + + + + name + JSON Key - Level 6 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #73daca + + + + name + JSON Key - Level 7 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #f7768e + + + + name + JSON Key - Level 8 + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json support.type.property-name.json + settings + + foreground + #9ece6a + + + + name + Plain Punctuation + scope + punctuation.definition.list_item.markdown + settings + + foreground + #9abdf5 + + + + name + Block Punctuation + scope + meta.block,meta.brace,punctuation.definition.block,punctuation.definition.use,punctuation.definition.class,punctuation.definition.begin.bracket,punctuation.definition.end.bracket,punctuation.definition.switch-expression.begin.bracket,punctuation.definition.switch-expression.end.bracket,punctuation.definition.section.switch-block.begin.bracket,punctuation.definition.section.switch-block.end.bracket,punctuation.definition.group.shell,punctuation.definition.parameters,punctuation.definition.arguments,punctuation.definition.dictionary,punctuation.definition.array,punctuation.section + settings + + foreground + #9abdf5 + + + + name + Markdown - Plain + scope + meta.jsx.children,meta.embedded.block + settings + + foreground + #c0caf5 + + + + name + HTML text + scope + text.html,text.log + settings + + foreground + #9aa5ce + + + + name + Markdown - Markup Raw Inline + scope + text.html.markdown markup.inline.raw.markdown + settings + + foreground + #bb9af7 + + + + name + Markdown - Markup Raw Inline Punctuation + scope + text.html.markdown markup.inline.raw.markdown punctuation.definition.raw.markdown + settings + + foreground + #4E5579 + + + + name + Markdown - Heading 1 + scope + heading.1.markdown entity.name,heading.1.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #89ddff + + + + name + Markdown - Heading 2 + scope + heading.2.markdown entity.name,heading.2.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #61bdf2 + + + + name + Markdown - Heading 3 + scope + heading.3.markdown entity.name,heading.3.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #7aa2f7 + + + + name + Markdown - Heading 4 + scope + heading.4.markdown entity.name,heading.4.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #6d91de + + + + name + Markdown - Heading 5 + scope + heading.5.markdown entity.name,heading.5.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #9aa5ce + + + + name + Markdown - Heading 6 + scope + heading.6.markdown entity.name,heading.6.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #747ca1 + + + + name + Markup - Italic + scope + markup.italic,markup.italic punctuation + settings + + fontStyle + italic + foreground + #c0caf5 + + + + name + Markup - Bold + scope + markup.bold,markup.bold punctuation + settings + + fontStyle + bold + foreground + #c0caf5 + + + + name + Markup - Bold-Italic + scope + markup.bold markup.italic,markup.bold markup.italic punctuation + settings + + fontStyle + bold italic + foreground + #c0caf5 + + + + name + Markup - Underline + scope + markup.underline,markup.underline punctuation + settings + + fontStyle + underline + + + + name + Markdown - Blockquote + scope + markup.quote punctuation.definition.blockquote.markdown + settings + + foreground + #4e5579 + + + + name + Markup - Quote + scope + markup.quote + settings + + fontStyle + italic + + + + name + Markdown - Link + scope + string.other.link,markup.underline.link,constant.other.reference.link.markdown,string.other.link.description.title.markdown + settings + + foreground + #73daca + + + + name + Markdown - Fenced Code Block + scope + markup.fenced_code.block.markdown,markup.inline.raw.string.markdown,variable.language.fenced.markdown + settings + + foreground + #89ddff + + + + name + Markdown - Separator + scope + meta.separator + settings + + fontStyle + bold + foreground + #444b6a + + + + name + Markup - Table + scope + markup.table + settings + + foreground + #c0cefc + + + + name + Token - Info + scope + token.info-token + settings + + foreground + #0db9d7 + + + + name + Token - Warn + scope + token.warn-token + settings + + foreground + #ffdb69 + + + + name + Token - Error + scope + token.error-token + settings + + foreground + #db4b4b + + + + name + Token - Debug + scope + token.debug-token + settings + + foreground + #b267e6 + + + + name + Apache Tag + scope + entity.tag.apacheconf + settings + + foreground + #f7768e + + + + name + Preprocessor + scope + meta.preprocessor + settings + + foreground + #73daca + + + + name + ENV value + scope + source.env + settings + + foreground + #7aa2f7 + + + + + diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py new file mode 100644 index 000000000..d597d2274 --- /dev/null +++ b/spec/tooling/chip.py @@ -0,0 +1,1073 @@ +import copy +import sys +import tomllib +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from pathlib import Path +from typing import Never, Optional, Self + + +class ErrorReporter: + reported: bool + location: str + + def __init__(self, location: str): + self.reported = False + self.location = location + + def update_location(self, loc: str): + self.reported = False + self.location = loc + + def error(self, message: str): + self.reported = True + print(f"ERROR {self.location}: {message}", file=sys.stderr) + + def asserts(self, condition: bool, message: str): + if not condition: + self.error(message) + + +reporter = ErrorReporter("unknown") + + +def assert_no_unexpected(data: dict, possible_keys: Iterable[str]): + for key in data.keys(): + reporter.asserts(key in possible_keys, f"Unexpected key: {key!r}") + + +@dataclass(frozen=True) +class Range: + low: int + high: int + + @classmethod + def const(cls, x: int) -> Self: + return cls(x, x) + + def is_bool(self): + return self.low >= 0 and self.high <= 1 + + def is_const(self): + return self.low == self.high + + def get_const(self) -> int: + assert self.is_const() + return self.low + + +type Type = list[Type] | Range + +DEFAULT_TYPE: Type = Range.const(0) + + +def structure_matches(a: Type, b: Type) -> bool: + if isinstance(a, Range) and isinstance(b, (Range, type(None))): + return True + elif isinstance(a, list) and isinstance(b, list): + return len(a) == len(b) and all(structure_matches(x, y) for x, y in zip(a, b)) + else: + return False + + +def constant_fits(cst: int, target: Type) -> bool: + if isinstance(target, Range): + return target.low <= cst <= target.high + else: + return constant_fits(cst, target[0]) + + +type Expr = ( + LitExpr + | VarExpr + | ArrExpr + | IdxExpr + | CastExpr + | MulExpr + | AddExpr + | SubExpr + | PowExpr + | SumExpr + | NotExpr + | DummyExpr +) + + +@dataclass +class Environment: + config: "Config" + valmap: dict[str, Range] + typemap: dict[str, Type] + + def with_val(self, key: str, val: Range) -> Self: + return type(self)(self.config, {**self.valmap, key: val}, self.typemap) + + +@dataclass +class LitExpr: + lit: int + + def typecheck(self, _env: Environment) -> Type: + return Range.const(self.lit) + + +@dataclass +class VarExpr: + name: str + + def typecheck(self, env: Environment) -> Type: + if self.name in env.valmap: + return env.valmap[self.name] + if self.name in env.typemap: + return env.typemap[self.name] + reporter.error(f"Unknown variable: {self.name!r}") + return DEFAULT_TYPE + + +@dataclass +class ArrExpr: + elems: list[Expr] + + def typecheck(self, env: Environment) -> Type: + reporter.asserts(self.elems != [], f"Empty array: {self!r}") + return [e.typecheck(env) for e in self.elems] + + +@dataclass +class IdxExpr: + base: Expr + idx: Expr + + def typecheck(self, env: Environment) -> Type: + base = self.base.typecheck(env) + idx = self.idx.typecheck(env) + if not isinstance(idx, Range) or not idx.is_const(): + reporter.error(f"Invalid index: {idx!r}") + return Range.const(-1) + idxconst = idx.get_const() + if isinstance(base, Range): + reporter.error(f"Indexing into non-array type: {self!r}") + return DEFAULT_TYPE + if not (0 <= idxconst < len(base)): + reporter.error(f"Index out of range {self!r}") + idxconst = 0 + return base[idxconst] + + +@dataclass +class CastExpr: + base: Expr + type: Type + + def typecheck(self, env: Environment) -> Type: + base = self.base.typecheck(env) + # TODO? Detect more sorts of invalid casts + baselen = len(base) if isinstance(base, list) else 1 + castlen = len(self.type) if isinstance(self.type, list) else 1 + reporter.asserts( + baselen >= castlen or (isinstance(base, Range) and base.is_const()), + f"Casting from fewer columns to more: {self!r} {base} {self.type}", + ) + if isinstance(base, Range) and base.is_const(): + reporter.asserts( + constant_fits(base.get_const(), self.type), + f"Casting const to type it doesn't fit: {self!r}", + ) + return self.type + + +@dataclass +class MulExpr: + factors: list[Expr] + + def type_match(self, a: Type, b: Type) -> Type: + if isinstance(a, list) and isinstance(b, list): + reporter.error(f"Multiplication of non-scalar types: {self!r}") + return DEFAULT_TYPE + elif not isinstance(a, Range): + return [self.type_match(x, b) for x in a] + elif isinstance(b, list): + return self.type_match(b, a) + else: + extrema = [x * y for x in [a.low, a.high] for y in [b.low, b.high]] + return Range(min(extrema), max(extrema)) + + def typecheck(self, env: Environment) -> Type: + reporter.asserts(self.factors != [], f"Empty product: {self!r}") + t: Type = Range.const(1) + for f in self.factors: + t = self.type_match(t, f.typecheck(env)) + return t + + +@dataclass +class AddExpr: + terms: list[Expr] + + def type_match(self, a: Type, b: Type) -> Type: + if isinstance(a, list) and isinstance(b, list): + if len(a) != len(b): + reporter.error(f"Adding array types of different length {self!r}") + return [DEFAULT_TYPE for _ in b] + return [self.type_match(x, y) for x, y in zip(a, b)] + elif isinstance(a, list) or isinstance(b, list): + reporter.error(f"Adding of scalar and array types {self!r}") + return DEFAULT_TYPE + else: + return Range(a.low + b.low, a.high + b.high) + + def typecheck(self, env: Environment) -> Type: + if not self.terms: + reporter.error("Empty add") + return Range.const(0) + t: Type = self.terms[0].typecheck(env) + for term in self.terms[1:]: + t = self.type_match(t, term.typecheck(env)) + return t + + +@dataclass +class SubExpr: + head: Expr + subs: list[Expr] + + def type_match(self, a: Type, b: Type) -> Type: + if isinstance(a, list) and isinstance(b, list): + if len(a) != len(b): + reporter.error(f"Subtracting array types of different length {self!r}") + return [DEFAULT_TYPE for _ in a] + return [self.type_match(x, y) for x, y in zip(a, b)] + elif isinstance(a, list) or isinstance(b, list): + reporter.error(f"Subtraction of scalar and array types {self!r}") + return DEFAULT_TYPE + else: + return Range(a.low - b.high, a.high - b.low) + + def typecheck(self, env: Environment) -> Type: + t = self.head.typecheck(env) + if not self.subs: + if not isinstance(t, Range): + reporter.error(f"Negating a non-scalar type: {self!r}") + return t + return Range(-t.high, -t.low) + for term in self.subs: + t = self.type_match(t, term.typecheck(env)) + return t + + +@dataclass +class PowExpr: + base: Expr + exp: Expr + + def typecheck(self, env: Environment) -> Type: + base = self.base.typecheck(env) + exp = self.exp.typecheck(env) + if isinstance(base, list) or not base.is_const(): + reporter.error(f"Invalid exponentiation with non-const base: {self.base!r}") + return DEFAULT_TYPE + if isinstance(exp, list) or not exp.is_const(): + reporter.error( + f"Invalid exponentiation with non-const exponent: {self.exp!r}" + ) + return DEFAULT_TYPE + val = pow(base.get_const(), exp.get_const(), env.config.variables.prime) + return Range.const(val) + + +@dataclass +class SumExpr: + iter: "Iter" + terms: Expr + + def type_match(self, a: Type, b: Type) -> Type: + if isinstance(a, list) and isinstance(b, list): + if len(a) != len(b): + reporter.error(f"Summing array types of different length {self!r}") + return [DEFAULT_TYPE for _ in b] + return [self.type_match(x, y) for x, y in zip(a, b)] + elif isinstance(a, list) or isinstance(b, list): + reporter.error(f"Summing of scalar and array types {self!r}") + return DEFAULT_TYPE + else: + return Range(a.low + b.low, a.high + b.high) + + def typecheck(self, env: Environment) -> Type: + t: Type = Range.const(0) + for tc in self.iter.typecheck(env, lambda e: [self.terms.typecheck(e)]): + t = self.type_match(t, tc) + return t + + +@dataclass +class NotExpr: + inner: Expr + + def typecheck(self, env: Environment) -> Type: + inner = self.inner.typecheck(env) + if isinstance(inner, list) or not inner.is_bool(): + reporter.error(f"Not a bool passed to `not`: {self.inner!r}") + return Range(0, 1) + return Range(1 - inner.high, 1 - inner.low) + + +@dataclass +class DummyExpr: + def typecheck(self, _env: Environment) -> Type: + return DEFAULT_TYPE + + +def build_expr(config: Optional["Config"], data: object) -> Expr: + # Does this need config, or do we delay any config-checking to when we use the expr? + match data: + case int(x): + return LitExpr(x) + case str(x): + reporter.asserts( + x.isidentifier(), f"Invalid identifier name for variable {x!r}" + ) + return VarExpr(x) + case ["arr", *elems]: + return ArrExpr([build_expr(config, e) for e in elems]) + case ["idx", x, y]: + return IdxExpr(build_expr(config, x), build_expr(config, y)) + case ["cast", x, t]: + assert config is not None + assert isinstance(t, (list, str)) + return CastExpr(build_expr(config, x), build_type(config, t)) + case ["*", *factors]: + return MulExpr([build_expr(config, f) for f in factors]) + case ["+", *terms]: + return AddExpr([build_expr(config, t) for t in terms]) + case ["-", head, *subs]: + return SubExpr( + build_expr(config, head), [build_expr(config, s) for s in subs] + ) + case ["^", base, exp]: + return PowExpr(build_expr(config, base), build_expr(config, exp)) + case ["sum", ["=", str(var), start], stop, terms]: + assert config is not None + return SumExpr(Iter(config, var, start, stop), build_expr(config, terms)) + case ["not", e]: + return NotExpr(build_expr(config, e)) + case other: + reporter.error(f"Unknown expression: {other!r}") + return DummyExpr() + + +@dataclass +class Iter: + name: str + start: Expr + stop: Expr + + def __init__(self, config: "Config", name: str, start: object, stop: object): + self.name = name + reporter.asserts( + isinstance(self.name, str), f"iter name is not a string: {self.name!r}" + ) + reporter.asserts( + self.name.isidentifier(), f"Not a valid identifier: {self.name!r}" + ) + self.start = build_expr(config, start) + self.stop = build_expr(config, stop) + + def typecheck[T]( + self, env: Environment, callback: Callable[[Environment], Iterable[T]] + ) -> Iterable[T]: + start = self.start.typecheck(env) + if isinstance(start, list) or not start.is_const(): + reporter.error(f"Starting value of iterator not a const: {self!r}") + start = Range.const(0) + stop = self.stop.typecheck(env) + if isinstance(stop, list) or not stop.is_const(): + reporter.error(f"Ending value of iterator not a const: {self!r}") + stop = Range.const(start.get_const()) + + # While it's tempting to replace this loop by an assignment of Range(start, stop + 1) to self.name + # that would break both detection of consts, and narrowing down to the correct type for indexing + # heterogenous array types + for i in range(start.get_const(), stop.get_const() + 1): + yield from callback(env.with_val(self.name, Range.const(i))) + + +def iters_of(obj: dict, config, name=None) -> list[Iter]: + """Return a list of iterators needed by `obj`. Taken from `iters` or `iter`. + Prepend `name` to every iterator, if given. + Adapted from the corresponding typst implementation.""" + + def clean_iter(it): + arr = it if isinstance(it, list) else [it] + if name is not None: + arr = [name] + arr + + if len(arr) == 2: + # Assume single-element range + arr.append(arr[-1]) + + if len(arr) != 3: + reporter.error(f"Invalid length iter: {arr!r}") + return Iter(config, "_", 0, 0) + return Iter(config, *arr) + + if "iters" in obj: + reporter.asserts( + "iter" not in obj, f"Object has both `iters` and `iter`: {obj!r}" + ) + return [clean_iter(it) for it in obj["iters"]] + elif "iter" in obj: + return [clean_iter(obj["iter"])] + else: + return [] + + +@dataclass +class TypeConfig: + label: str + subtypes: list[Type] + range: Optional[Range] + desc: str + preprocessed: bool + + def __init__(self, default_name: str, lookup: Callable[[str], Type], data: dict): + assert_no_unexpected(data, type(self).__annotations__.keys()) + self.label = data["label"] + if "range" in data: + reporter.asserts( + data["subtypes"] == [default_name], + f"Specified a range on a non-base composite type: {data!r}", + ) + reporter.asserts( + isinstance(data["range"], list) and len(data["range"]) == 2, + f"Invalid range: {data!r}", + ) + start, stop = data["range"] + if not isinstance(start, int) and not ( + isinstance(start, str) and start.isdigit() + ): + reporter.error(f"Range start not an int: {data!r}") + start = 0 + if not isinstance(stop, int) and not ( + isinstance(stop, str) and stop.isdigit() + ): + reporter.error(f"Range end not an int: {data!r}") + stop = start + reporter.asserts(int(start) <= int(stop), f"Inverted range: {data!r}") + self.range = Range(int(start), int(stop)) + self.subtypes = [] + else: + self.range = None + self.subtypes = [lookup(tp) for tp in data["subtypes"]] + self.desc = data["desc"] + self.preprocessed = data.get("preprocessed", False) + + def as_type(self) -> Type: + return self.range or self.subtypes[:] + + +@dataclass +class ConfigCategories: + all: list[str] + instantiated: list[str] + + def __init__(self, data: dict): + assert_no_unexpected(data, type(self).__annotations__.keys()) + self.all = data["all"] + self.instantiated = data["instantiated"] + reporter.asserts( + all(isinstance(v, str) for v in self.all), + f"Something's not a string: {self.all}", + ) + reporter.asserts( + all(isinstance(v, str) for v in self.instantiated), + f"Something's not a string: {self.instantiated}", + ) + reporter.asserts( + set(self.instantiated) <= set(self.all), + f"Instantiated not a subset of all: {self!r}", + ) + + +@dataclass +class ConfigVariables: + types: list[TypeConfig] + categories: ConfigCategories + prime: int + + def __init__(self, data: dict): + assert_no_unexpected(data, type(self).__annotations__.keys()) + self.types = [] + base_type = data["types"][0]["label"] + for tp in data["types"]: + self.types.append(TypeConfig(base_type, self.lookup_type, tp)) + self.categories = ConfigCategories(data["categories"]) + basefield = self.lookup_type(base_type) + assert isinstance(basefield, Range) + self.prime = basefield.high + 1 + + def lookup_type(self, typename: str) -> Type: + matches = [t for t in self.types if t.label == typename] + if len(matches) != 1: + reporter.error(f"Couldn't lookup type by name: {typename!r}") + return DEFAULT_TYPE + return matches[0].as_type() + + +@dataclass +class ConfigMetadata: + version: int + + def __init__(self, data: dict): + assert_no_unexpected(data, type(self).__annotations__.keys()) + self.version = data["version"] + reporter.asserts( + isinstance(self.version, int), f"version {self.version!r} is not an int" + ) + + +@dataclass +class Config: + metadata: ConfigMetadata + variables: ConfigVariables + + def __init__(self, data: dict): + """Construct a Config from toml-parsed data""" + assert_no_unexpected(data, type(self).__annotations__.keys()) + self.metadata = ConfigMetadata(data["metadata"]) + self.variables = ConfigVariables(data["variables"]) + + @classmethod + def from_file(cls, filename: str | Path) -> Self: + reporter.update_location(str(filename)) + with open(filename, "rb") as fp: + return cls(tomllib.load(fp)) + + @classmethod + def from_string(cls, s: str) -> Self: + reporter.update_location("") + return cls(tomllib.loads(s)) + + +def build_type(config: Config, data: list | str): + if isinstance(data, list): + if len(data) != 2: + reporter.error(f"Invalid type: {data!r}") + return DEFAULT_TYPE + return [build_type(config, data[0]) for _ in range(data[1])] + else: + return config.variables.lookup_type(data) + + +@dataclass +class Variable: + category: str + name: str + type: Type + desc: str + pad: Expr + precomputed: bool + + def __init__(self, config: Config, category: str, data: dict): + self.category = category + assert_no_unexpected(data, Variable.__annotations__.keys()) + self.name = data["name"] + reporter.asserts(isinstance(self.name, str), f"{self.name!r} is not a string") + reporter.asserts(self.name.isidentifier(), f"Invalid identifier: {self.name!r}") + self.type = build_type(config, data["type"]) + self.desc = data["desc"] + reporter.asserts(isinstance(self.desc, str), f"{self.desc!r} is not a string") + self.pad = build_expr(None, data.get("pad", 0)) + self.precomputed = data.get("precomputed", False) + reporter.asserts( + isinstance(self.precomputed, bool), + f"precomputed is not a bool: {self.precomputed!r}", + ) + + +def all_iters[T]( + its: list[Iter], env: Environment, callback: Callable[[Environment], Iterable[T]] +) -> Iterable[T]: + if not its: + yield from callback(env) + else: + yield from its[0].typecheck(env, lambda e: all_iters(its[1:], e, callback)) + + +@dataclass +class PolyWithIters: + poly: Expr + iters: list[Iter] + + +@dataclass +class VirtualDef: + # A list of polynomials with each a set of iters they range over + defs: list[PolyWithIters] + + def __init__(self, config: Config, name: str, tp: Type, data: dict): + if "poly" in data: + idx = data.get("idx", None) + self.defs = [ + PolyWithIters( + build_expr(config, data["poly"]), iters_of(data, config, name=idx) + ) + ] + elif "polys" in data: + idx = data.get("idx", None) + self.defs = [ + PolyWithIters( + build_expr(config, poly["poly"]), iters_of(poly, config, name=idx) + ) + for poly in data["polys"] + ] + else: + self.defs = [PolyWithIters(build_expr(config, data), [])] + + +@dataclass +class VirtualVariable(Variable): + def_: VirtualDef + + def __init__(self, config: Config, category: str, data: dict): + assert_no_unexpected(data, (set(Variable.__annotations__.keys()) | {"def"}) - {"pad"}) + reporter.asserts("def" in data, f"Missing def for virtual column: {data!r}") + data = copy.deepcopy(data) + def_ = data.pop("def", {}) + super().__init__(config, category, data) + self.def_ = VirtualDef(config, self.name, self.type, def_) + + def typecheck(self, env: Environment) -> Type: + def handle_iters( + env: Environment, + iters: list[Iter], + poly: Expr, + expected: Type, + indices: list[int], + seen: set[tuple], + ): + if not iters: + # Check not doubly defined + for s in seen: + ln = min(len(s), len(indices)) + if s[:ln] == tuple(indices[:ln]): + reporter.error( + f"Double definition for virtual column: {self!r} at index {indices}" + ) + break + + val = poly.typecheck(env) + # check val structure matches assigned + reporter.asserts( + structure_matches(val, expected), + f"Invalid structure for definition to virtual column: {self!r}", + ) + # Check type fits? + + seen.add(tuple(indices)) + else: + it, *its = iters + # Some duplicated code/concepts from Iter.typecheck + # But threading the extra needed state through overly complicates everything + start = it.start.typecheck(env) + if isinstance(start, list) or not start.is_const(): + reporter.error( + f"Starting value of virtual def iter not a const: {self!r}" + ) + start = Range.const(0) + stop = it.stop.typecheck(env) + if isinstance(stop, list) or not stop.is_const(): + reporter.error( + f"Ending value of virtual def iter not a const: {self!r}" + ) + stop = Range.const(start.get_const()) + + if isinstance(expected, Range): + reporter.error( + f"Virtual definition has an iter for a scalar: {self!r}" + ) + return + + if not 0 <= start.get_const() <= stop.get_const() < len(expected): + reporter.error( + f"Virtual definition index [{start.get_const()}, {stop.get_const()}] out of range for {expected}: {self!r}" + ) + return + + for i in range(start.get_const(), stop.get_const() + 1): + handle_iters( + env.with_val(it.name, Range.const(i)), + its, + poly, + expected[i], + indices + [i], + seen, + ) + + def is_covered(seen: set[tuple], indices: list[int]) -> bool: + for s in seen: + if len(s) <= len(indices) and s == tuple(indices[: len(s)]): + return True + return False + + def check_covered(t: Type, seen: set[tuple], indices: list[int]): + if isinstance(t, Range): + reporter.asserts( + is_covered(seen, indices), + f"Virtual column {self.name!r} not completely defined", + ) + else: + for i, elt in enumerate(t): + check_covered(elt, seen, indices + [i]) + + # Special case for better error messages + if isinstance(self.type, Range): + reporter.asserts( + len(self.def_.defs) == 1 and not self.def_.defs[0].iters, + f"Invalid def for scalar column: {self!r}", + ) + assigned_type = self.def_.defs[0].poly.typecheck(env) + if not isinstance(assigned_type, Range): + reporter.error( + f"Assigning non-scalar type to scalar virtual column: {self!r}" + ) + return self.type + # Check type fits? + # Leaving this out because it produces too much noise with one-hot assumptions + # reporter.asserts(self.type.low <= assigned_type.low <= assigned_type.high <= self.type.high, f"Definition may not fit in virtual column: {self!r}") + else: + # Check no indices are covered twice + seen: set[tuple] = set() + for poly_iters in self.def_.defs: + handle_iters( + env, poly_iters.iters, poly_iters.poly, self.type, [], seen + ) + # Check everything is covered + check_covered(self.type, seen, []) + return self.type + + +@dataclass +class Assumption: + desc: str + iters: list[Iter] + + def __init__(self, config: Config, data: dict): + assert_no_unexpected( + data, set(self.__annotations__.keys()) | {"iter", "iters", "ref"} + ) + self.desc = data["desc"] + self.iters = iters_of(data, config) + + +@dataclass +class ArithConstraint: + constraint: str + desc: str + poly: Expr + iters: list[Iter] + + def __init__(self, config: Config, data: dict): + assert_no_unexpected( + data, set(self.__annotations__.keys()) | {"kind", "ref", "iter", "iters"} + ) + assert data["kind"] == "arith" + self.constraint = data["constraint"] + reporter.asserts( + isinstance(self.constraint, str), + f"Constraint not a string: {self.constraint!r}", + ) + self.desc = data.get("desc", "") + reporter.asserts( + isinstance(self.desc, str), f"desc is not a string: {self.desc!r}" + ) + self.poly = build_expr(config, data["poly"]) + self.iters = iters_of(data, config) + + def typecheck(self, env: Environment) -> Iterable[Never]: + # TODO? Should we check that there's no overflow of the modulus? + # This would probably struggle due to things like one-hot invariants + + def check_includes_zero(t: Type): + if isinstance(t, Range): + reporter.asserts( + t.low <= 0 <= t.high, + f"Unsatisfiable constraint, 0 not in range: {self!r} {t}", + ) + else: + reporter.error( + f"Non-scalar value for polynomial constraint: {self!r} {t}" + ) + + for t in all_iters(self.iters, env, lambda e: [self.poly.typecheck(e)]): + check_includes_zero(t) + return [] + + +@dataclass +class Signature: + tag: str + input: list[Type] + output: Optional[Type] + + def matches(self, other: Self) -> bool: + if not isinstance(other, type(self)): + return False + if self.tag != other.tag: + return False + if (self.output is None) != (other.output is None): + return False + if ( + self.output is not None + and other.output is not None + and not structure_matches(self.output, other.output) + ): + return False + return structure_matches(self.input, other.input) + + +@dataclass +class InteractionLike: + kind: str + conditional_name: str + conditional_required: bool + signature: type[Signature] + + tag: str + desc: str + input: list[Expr] + output: Optional[Expr] + conditional: Optional[Expr] + iters: list[Iter] + + def __init__(self, config: Config, data: dict): + assert_no_unexpected( + data, + { + "tag", + "desc", + "input", + "output", + self.conditional_name, + "kind", + "ref", + "iter", + "iters", + }, + ) + assert data["kind"] == self.kind + self.tag = data["tag"] + reporter.asserts( + isinstance(self.tag, str), f"tag is not a string: {self.tag!r}" + ) + self.desc = data.get("desc", "") + reporter.asserts( + isinstance(self.desc, str), f"Description is not a string: {self.desc!r}" + ) + self.input = [build_expr(config, inp) for inp in data["input"]] + if "output" in data: + self.output = build_expr(config, data["output"]) + else: + self.output = None + if self.conditional_name in data: + self.conditional = build_expr(config, data[self.conditional_name]) + else: + reporter.asserts( + not self.conditional_required, + f"Missing {self.conditional_name}: {data!r}", + ) + self.conditional = None + self.iters = iters_of(data, config) + + def typecheck(self, env: Environment) -> Iterable[Signature]: + def callback(e: Environment) -> Iterable[Signature]: + # TODO: Should we be able to check cond/multiplicity somehow? + if self.conditional is not None: + self.conditional.typecheck(e) + return [ + self.signature( + self.tag, + [inp.typecheck(e) for inp in self.input], + self.output.typecheck(e) if self.output else None, + ) + ] + + return all_iters(self.iters, env, callback) + + +class TemplateSignature(Signature): + pass + + +class TemplateConstraint(InteractionLike): + kind = "template" + conditional_name = "cond" + conditional_required = False + signature = TemplateSignature + + +class InteractionSignature(Signature): + pass + + +class InteractionConstraint(InteractionLike): + kind = "interaction" + conditional_name = "multiplicity" + conditional_required = True + signature = InteractionSignature + + +@dataclass +class DummyConstraint: + def typecheck(self, env: Environment) -> list[Never]: + return [] + + +type Constraint = ( + ArithConstraint | TemplateConstraint | InteractionConstraint | DummyConstraint +) + + +def build_constraint(config, data: dict) -> Constraint: + match data["kind"]: + case "arith": + return ArithConstraint(config, data) + case "template": + return TemplateConstraint(config, data) + case "interaction": + return InteractionConstraint(config, data) + case other: + reporter.error(f"Unknown constraint kind: {other!r}") + return DummyConstraint() + + +@dataclass +class Chip: + config: Config + name: str + variables: list[Variable] + assumptions: list[Assumption] + constraints: list[Constraint] + + def __init__(self, config: Config, data: dict): + """Construct a chip from toml-parsed data""" + assert_no_unexpected( + data, set(type(self).__annotations__.keys()) | {"constraint_groups"} + ) + assert_no_unexpected(data["variables"], config.variables.categories.all) + self.config = config + self.name = data["name"] + reporter.asserts( + 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.variables = [ + (Variable if cat != "virtual" else VirtualVariable)(config, cat, var) + for cat, vars in data["variables"].items() + for var in vars + ] + self.assumptions = [Assumption(config, a) for a in data.get("assumptions", [])] + constraint_groups = [grp["name"] for grp in data.get("constraint_groups", [])] + assert_no_unexpected(data.get("constraints", {}), constraint_groups) + self.constraints = [ + build_constraint(config, constraint) + for group in data.get("constraints", {}).values() + for constraint in group + ] + + @classmethod + def from_file(cls, config: Config, filename: str | Path) -> Self: + reporter.update_location(str(filename)) + with open(filename, "rb") as fp: + return cls(config, tomllib.load(fp)) + + @classmethod + def from_string(cls, config: Config, s: str) -> Self: + reporter.update_location("") + return cls(config, tomllib.loads(s)) + + def typecheck(self) -> Iterable[Signature]: + typemap = {} + for v in self.variables: + if isinstance(v.type, list) and len(v.type) == 1: + t = v.type[0] + else: + t = v.type + typemap[v.name] = t + + env = Environment(self.config, {}, typemap) + for v in self.variables: + if isinstance(v, VirtualVariable): + v.typecheck(env) + for c in self.constraints: + yield from c.typecheck(env) + + +def build_signature(config: Config, data: dict) -> Signature: + assert_no_unexpected( + data, {"tag", "kind", "input", "output", "cond", "multiplicity"} + ) + Sig: type[Signature] + match data["kind"]: + case "template": + reporter.asserts( + "multiplicity" not in data, + f"Template signature with multiplicity: {data!r}", + ) + Sig = TemplateSignature + case "interaction": + reporter.asserts( + "cond" not in data, f"Template signature with cond: {data!r}" + ) + Sig = InteractionSignature + case other: + reporter.error(f"Signature of invalid kind '{other}': {data!r}") + Sig = Signature + tag = data["tag"] + reporter.asserts(isinstance(tag, str), f"Signature tag not a string: {tag!r}") + input = [build_type(config, inp) for inp in data["input"]] + if "output" in data: + output = build_type(config, data["output"]) + else: + output = None + return Sig(tag, input, output) + + +def read_signatures(config, filename) -> list[Signature]: + with open(filename, "rb") as fp: + data = tomllib.load(fp) + assert_no_unexpected(data, {"signatures"}) + return [build_signature(config, sig) for sig in data["signatures"]] + + +def check_signatures(found: Iterable[Signature], expected: list[Signature]): + for sig in found: + reporter.asserts( + any(sig.matches(exp) for exp in expected), f"Unexpected signature: {sig}" + ) + + +if __name__ == "__main__": + config = Config.from_file(sys.argv[1]) + signatures = read_signatures(config, sys.argv[2]) + if reporter.reported: + sys.exit(1) + + reported = False + chips: list[Chip] = [] + for file in sys.argv[3:]: + if file in sys.argv[1:3]: + continue + chips.append(Chip.from_file(config, file)) + reported |= reporter.reported + if reported: + sys.exit(1) + + for chip in chips: + reporter.update_location(f"Chip {chip.name}") + check_signatures(chip.typecheck(), signatures) + reported |= reporter.reported + if reported: + sys.exit(1) + else: + print("No issues were found.") + sys.exit(0) diff --git a/spec/variables.typ b/spec/variables.typ new file mode 100644 index 000000000..d62fec7ac --- /dev/null +++ b/spec/variables.typ @@ -0,0 +1,19 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config + +#show: book-page("variables.typ") + +#let config = load_config() + +While this VM operates on 64-bit words, the proving system's base field has fewer than $2^64$ elements available and thus cannot represent all words natively. +To this end, we introduce the concept of "variables" as an abstraction layer on top of the VM's field elements. The following table lists all variable types used in this VM. + +#table( + columns: (auto, 1fr, auto), + inset: 7pt, + align: (top+left, top+left, top+center, ), + table.header([*Name*], [*Description*], [*\#Columns*]), + ..for type in config.variables.types { + ([#raw(type.label)], [#eval(type.desc, mode: "markup")], [#type.subtypes.len()]) + }, +)