From 23fa85b0848fe57c8372c431fa1820c8646bcd3d Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Wed, 17 Dec 2025 12:49:18 +0100 Subject: [PATCH 01/78] spec: initial spec commit --- spec/.gitignore | 1 + spec/README.md | 11 + spec/book.typ | 15 + spec/ebook.typ | 8 + spec/sample_page.typ | 7 + spec/templates/ebook.typ | 37 + spec/templates/page.typ | 159 ++++ spec/templates/theme-style.toml | 30 + spec/templates/tokyo-night.tmTheme | 1308 ++++++++++++++++++++++++++++ 9 files changed, 1576 insertions(+) create mode 100644 spec/.gitignore create mode 100644 spec/README.md create mode 100644 spec/book.typ create mode 100644 spec/ebook.typ create mode 100644 spec/sample_page.typ create mode 100644 spec/templates/ebook.typ create mode 100644 spec/templates/page.typ create mode 100644 spec/templates/theme-style.toml create mode 100644 spec/templates/tokyo-night.tmTheme diff --git a/spec/.gitignore b/spec/.gitignore new file mode 100644 index 000000000..4be6e160a --- /dev/null +++ b/spec/.gitignore @@ -0,0 +1 @@ +dist/* \ No newline at end of file diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..d841017cb --- /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 reposity +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. \ No newline at end of file diff --git a/spec/book.typ b/spec/book.typ new file mode 100644 index 000000000..b196e3fc6 --- /dev/null +++ b/spec/book.typ @@ -0,0 +1,15 @@ + +#import "@preview/shiroa:0.3.1": * + +#show: book + +#book-meta( + title: "Lambda VM specification", + summary: [ + #prefix-chapter("sample_page.typ")[Sample page] + ] +) + +// re-export page template +#import "/templates/page.typ": project +#let book-page = project diff --git a/spec/ebook.typ b/spec/ebook.typ new file mode 100644 index 000000000..abddf2701 --- /dev/null +++ b/spec/ebook.typ @@ -0,0 +1,8 @@ +#import "@preview/shiroa:0.3.1": * + +#import "/templates/ebook.typ" + +#show: ebook.project.with(title: "typst-book", spec: "book.typ") + +// set a resolver for inclusion +#ebook.resolve-inclusion(it => include it) diff --git a/spec/sample_page.typ b/spec/sample_page.typ new file mode 100644 index 000000000..6eb300c4e --- /dev/null +++ b/spec/sample_page.typ @@ -0,0 +1,7 @@ +#import "/book.typ": book-page + +#show: book-page.with(title: "Hello, typst") + += Hello, typst + +Sample page diff --git a/spec/templates/ebook.typ b/spec/templates/ebook.typ new file mode 100644 index 000000000..44e0312d3 --- /dev/null +++ b/spec/templates/ebook.typ @@ -0,0 +1,37 @@ +#import "@preview/shiroa:0.3.1": * +#import "/templates/page.typ": part-style, project + +#let _page-project = project + +#let _resolve-inclusion-state = state("_resolve-inclusion", none) + +#let resolve-inclusion(inc) = _resolve-inclusion-state.update(it => inc) + +#let project(title: "", authors: (), spec: "", content) = { + // Set document metadata early + set document( + author: authors, + title: title, + ) + + // Inherit from gh-pages + show: _page-project + + if title != "" { + heading(title) + } + + context { + let inc = _resolve-inclusion-state.final() + external-book(spec: inc(spec)) + + let mt = book-meta-state.final() + let styles = (inc: inc, part: part-style, chapter: it => it) + + if mt != none { + mt.summary.map(it => visit-summary(it, styles)).sum() + } + } + + content +} diff --git a/spec/templates/page.typ b/spec/templates/page.typ new file mode 100644 index 000000000..1f7f88ea0 --- /dev/null +++ b/spec/templates/page.typ @@ -0,0 +1,159 @@ +// 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. +/// - plain-body (content): The plain body of the page. +#let project(title: "Typst Book", description: auto, authors: (), kind: "page", 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", + ) + + // 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 +} + +#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 + + + + + From 27da436930f28d8a6fad2a0a946cc665e4a0dbcb Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 24 Dec 2025 15:20:56 +0100 Subject: [PATCH 02/78] spec: Basic chip data format and layout See the original yetanotherco/lambda_vm_spec #1 for more details, if it still exists. * Introduce `config` and "variables" * chip column-to-table rendering * restructuring * some basic interactions idea * Sample lt chip design * Update formatting * Interpret variable indexing * BRANCH draft * Fix indexing + render template * Render labels for references to constraints * Rendering chip assumptions * Add an editorconfig for consistency in indentation and trailing newlines * The constraint range index found its way back home * Finish (?) LT chip * Improve lisp rendering * support constraint group rendering * Support "^" type setting * dvrm * add dvrm assumptions * Rendering virtual column definitions and polynomials for arith constraints * ignore ebook.pdf * Split LT and BRANCH into groups * Nicer mutual recursion in expression formatting * Use negation instead of mult by -1 in lt * Format expr.typ * Simplify subtraction expression * Remove parentheses using precedence rules * fmt * improve dvrm readability * fix lt parentheses * move `extended_n_sub_r` def from constraint to var * Set div chip word types to HL * divrem fixes * more dvrm tweaks * Specify grammar * add docs * Drop chip files * Improve `chip` readability * minor fixes --------- Co-authored-by: Erik Takke --- spec/.editorconfig | 10 +++ spec/.gitignore | 3 +- spec/book.typ | 2 +- spec/chip.typ | 163 +++++++++++++++++++++++++++++++++++++++++++ spec/expr.typ | 119 +++++++++++++++++++++++++++++++ spec/sample_page.typ | 7 -- spec/src.typ | 58 +++++++++++++++ spec/src/config.toml | 113 ++++++++++++++++++++++++++++++ spec/variables.typ | 20 ++++++ 9 files changed, 486 insertions(+), 9 deletions(-) create mode 100644 spec/.editorconfig create mode 100644 spec/chip.typ create mode 100644 spec/expr.typ delete mode 100644 spec/sample_page.typ create mode 100644 spec/src.typ create mode 100644 spec/src/config.toml create mode 100644 spec/variables.typ 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 index 4be6e160a..73218d5ba 100644 --- a/spec/.gitignore +++ b/spec/.gitignore @@ -1 +1,2 @@ -dist/* \ No newline at end of file +dist/* +ebook.pdf diff --git a/spec/book.typ b/spec/book.typ index b196e3fc6..f50d7ed29 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -6,7 +6,7 @@ #book-meta( title: "Lambda VM specification", summary: [ - #prefix-chapter("sample_page.typ")[Sample page] + #chapter("variables.typ")[Variables] ] ) diff --git a/spec/chip.typ b/spec/chip.typ new file mode 100644 index 000000000..c694db90a --- /dev/null +++ b/spec/chip.typ @@ -0,0 +1,163 @@ +#import "expr.typ": expr_to_code, expr_to_math + +/// 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 => config.variables.types.filter(type => type.label == var.type).at(0).subtypes.len()) + .sum() +} + +/// Generates a table listing `chip`'s columns. +#let render_chip_column_table(chip, config) = { + // Group variables by category + figure(table( + columns: (auto, auto, 1fr), + inset: 6pt, + align: left + top, + stroke: none, + table.header([*Label*], [*Type*], [*Description*]), + table.hline(stroke: stroke(thickness: 2pt)), + ..for (cat, vars) in chip.variables.pairs() { + ([#emph(cat)], [], [], table.hline(stroke: .6pt)) + for var in vars { + ([#raw(var.name)], [#raw(var.type)], [#eval(var.desc, mode: "markup")]) + for (i, poly) in var.at("polys", default: ()).enumerate() { + (if i == 0 { emph[def] }, [], expr_to_math(("=", ("idx", var.name, i), poly))) + } + if "poly" in var { + (emph[def], [], expr_to_math(var.poly)) + } + } + ([], [], []) + }, + ), caption: [Column overview of #chip.name chip.]) +} + +#let cref(constraint) = { + if "ref" in constraint { + label(constraint.ref) + } +} + +// Render a range if `obj` contains one. +#let interval(obj) = { + if "range" in obj { + [#raw(obj.range.at(0)) #sym.in` [`#obj.range.at(1)`,`#obj.range.at(2)`]`] + } else { return [] } +} + +#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 index = if "range" in assumption { "." + assumption.range.at(0) } else { "" } + let lbl = [#chip.name\-A] + show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) + [#figure(kind: "assumption", numbering: (i) => [#lbl#i#index], supplement: [], [])#cref(assumption)] + } + + 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)], [#interval(assumption)], [#eval(assumption.desc, mode: "markup")]) + }, + ), caption: [Assumption overview of #chip.name chip.]) +} + +/// 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") + + /// Render the contraint's tag. + let tag(constraint, group) = { + let index = if "range" in constraint { "." + constraint.range.at(0) } else { "" } + let prefix = if "prefix" in group { group.prefix } + let lbl = [#chip.name\-C#prefix] + show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) + [#figure(kind: "constraint", numbering: (i) => [#lbl#i#index], supplement: [], [])#cref(constraint)] + } + + /// 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)] + } else if kind == "template" { + raw(constraint.tag) + `<` + args_interaction_like(constraint.input, constraint.at("output", default: none)) + `>` + } else { + assert(false, message: "illegal constraint format: " + kind) + } + } + + // Whether constraints has polynomial constraints + let has_polynomial_constraints(constraint) = { + constraint.at("kind") == "arith" and ("poly" in constraint or "polys" 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 { + ([_polynomial constraint_], [], $#expr_to_math(poly) = 0$, []) + },) + } + + figure(table( + columns: (auto, auto, 1fr, auto), + inset: 6pt, + align: (top + left, top + left, top + left, top + center), + stroke: none, + table.header([*Tag*], [*Range*], [*Description*], [*Multiplicity*]), + table.hline(stroke: stroke(thickness: 2pt)), + ..for group in groups { + for constraint in chip.constraints.at(group) { + ( + [#tag(constraint, group)], + [#interval(constraint)], + [#repr_constraint(constraint)], + [#expr_to_math(constraint.at("multiplicity", default: ""))], + ) + if has_polynomial_constraints(constraint) { + render_polynomial_constraints(constraint) + } + } + }, + ), caption: [Constraint overview of #chip.name chip.]) +} diff --git a/spec/expr.typ b/spec/expr.typ new file mode 100644 index 000000000..df1ddb2e6 --- /dev/null +++ b/spec/expr.typ @@ -0,0 +1,119 @@ +// Grammar +// ::= () ; "" +// | var ; str(var) +// | int ; int +// | ["idx", expr1, expr2] ; expr1[expr2] +// | ["not", expr] ; !expr +// | ["+", expr1, expr2, ...] ; expr1 + expr2 + ... +// | ["*", expr1, expr2, ...] ; expr1 * expr2 * ... +// | ["/", expr1, expr2] ; expr1 / expr2 +// | ["^", expr1, expr2] ; expr1^expr2 +// | ["=", expr1, expr2] ; expr1 = expr2 +// | ["-", expr] ; -expr +// | ["-", expr1, expr2, ...] ; expr1 - expr2 - ... +// +// +// 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`. +// +// Precedence values: +// 0 : ^ +// 1 : neg (e.g., 5 => -5) +// 2 : * +// 3 : / +// 4 : not (e.g., 5 => 1-5) +// 5 : + +// 6 : - +// 7 : [] +// 8 : = +// 10: +#let MAX_PRECEDENCE = 10 + +// 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: (e) => { + assert(false, "Invalid expression: " + repr(e)) + }))(pp, res, expr) + } + } + res.with(MAX_PRECEDENCE) +} + +// 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( + ( + "idx": (pp, rec, e) => rec(0, e.at(1)) + `[` + rec(10, e.at(2)) + `]`, + "not": (pp, rec, e) => cwrap(`1 - ` + rec(4, e.at(1)), pp < 4), + "+": (pp, rec, e) => cwrap(e.slice(1).map(rec.with(5)).join(` + `), pp < 5), + "*": (pp, rec, e) => cwrap(e.slice(1).map(rec.with(2)).join(` ` + sym.dot + ` `), pp < 2), + "/": (pp, rec, e) => cwrap(rec(3, e.at(1)), pp < 3) + ` / ` + rec(3, e.at(2)), + "^": (pp, rec, e) => { + assert(type(e.at(1)) == int and type(e.at(2)) == int, message: "Can only exponentiate constants") + rec(0, e.at(1)) + `^` + rec(0, e.at(2)) + }, + "=": (pp, rec, e) => rec(8, e.at(1)) + ` = ` + rec(8, e.at(2)), + "-": (pp, rec, e) => { + if e.len() == 2 { + // Negation + cwrap(`-` + rec(1, e.at(1)), pp < 1) + } else { + // Subtraction + cwrap(e.slice(1).map(rec.with(6)).join(` - `), pp < 6) + } + }, + ), +) + +// Wrap math `expr` if `apply = true` +#let mwrap(expr, apply) = { + if apply { + $($ + expr + $)$ + } else { + expr + } +} + +// Typeset an expression as math +#let expr_to_math = make_expr_formatter( + ( + "idx": (pp, rec, e) => $#rec(7, e.at(1))_(#rec(7, e.at(2)))$, + "not": (pp, rec, e) => mwrap($1 - #rec(4, e.at(1))$, pp < 4), + "+": (pp, rec, e) => mwrap($#e.slice(1).map(rec.with(5)).join($+$)$, pp < 5), + "*": (pp, rec, e) => mwrap($#e.slice(1).map(rec.with(3)).join($dot$)$, pp < 3), + "/": (pp, rec, e) => $#rec(3, e.at(1)) / #rec(3, e.at(2))$, + "^": (pp, rec, e) => { + assert(type(e.at(1)) == int and type(e.at(2)) == int, message: "Can only exponentiate constants") + $#e.at(1)^#e.at(2)$ + }, + "=": (pp, rec, e) => $#rec(8, e.at(1)) = #rec(8, e.at(2))$, + "-": (pp, rec, e) => { + if e.len() == 2 { + // Negation + mwrap($-#rec(1, e.at(1))$, pp < 1) + } else { + // Subtraction + mwrap($#e.slice(1).map(rec.with(6)).join($-$)$, pp < 6) + } + }, + ), + var: v => if v.len() == 1 { $#v$ } else { $#raw(v)$ }, + num: n => math.equation[#n], +) diff --git a/spec/sample_page.typ b/spec/sample_page.typ deleted file mode 100644 index 6eb300c4e..000000000 --- a/spec/sample_page.typ +++ /dev/null @@ -1,7 +0,0 @@ -#import "/book.typ": book-page - -#show: book-page.with(title: "Hello, typst") - -= Hello, typst - -Sample page diff --git a/spec/src.typ b/spec/src.typ new file mode 100644 index 000000000..f791bc1ca --- /dev/null +++ b/spec/src.typ @@ -0,0 +1,58 @@ +/// Path to the config file. +#let CONFIG_PATH = "src/config.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 +} + +/// 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) + } + + for var in chip.variables.values().flatten() { + // Check that all variable types are valid + assert( + var.type in config.variables.types.map(type => type.label), + message: "found invalid var type:" + 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/config.toml b/spec/src/config.toml new file mode 100644 index 000000000..1977b9155 --- /dev/null +++ b/spec/src/config.toml @@ -0,0 +1,113 @@ +[metadata] +version = 1 + +[[variables.types]] +label = "BaseField" +subtypes = ["BaseField"] +desc = "Variable that can assume any value in the base field." + +[[variables.types]] +label = "Bit" +subtypes = ["BaseField"] +desc = "Variable that can only assume values in the set ${0,1}$." + +[[variables.types]] +label = "B4" +subtypes = ["BaseField"] +desc = "Variable that can only assume values in the range $[0, 2^4)$." + +[[variables.types]] +label = "Byte" +subtypes = ["BaseField"] +count = 1 +desc = "Variable that can only assume values in the range $[0, 2^8)$." + +[[variables.types]] +label = "Half" +subtypes = ["BaseField"] +desc = "Variable that can only assume values in the range $[0, 2^16)$." + +[[variables.types]] +label = "Word" +subtypes = ["BaseField"] +desc = "Variable that can only assume values in the range $[0, 2^32)$." + +[[variables.types]] +label = "WordHL" +subtypes = ["Half", "Half"] +desc = """\ + Variable that can only assume values in the range $[0, 2^32)$. \\ + Represented as an array of two `Half` variables.\ + """ + +[[variables.types]] +label = "WordBL" +subtypes = ["Byte", "Byte", "Byte", "Byte"] +desc = """\ + Variable that can only assume values in the range $[0, 2^32)$. \\ + Represented as an array of four `Byte` variables.\ + """ + +[[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. + """ + +# TODO: Having to define these manually will get tedious +[[variables.types]] +label = "Bit[3]" +subtypes = ["Bit", "Bit", "Bit"] +desc = "Three bits" + +[[variables.types]] +label = "Byte[2]" +subtypes = ["Byte", "Byte"] +desc = "Two bytes" + +[[variables.types]] +label = "Byte[8]" +subtypes = ["Byte", "Byte", "Byte", "Byte", "Byte", "Byte", "Byte", "Byte"] +desc = "Eight bytes" + +[[variables.types]] +label = "Half[3]" +subtypes = ["Half", "Half", "Half"] +desc = "Three halfwords" + +[[variables.types]] +label = "Half[8]" +subtypes = ["Half", "Half", "Half", "Half", "Half", "Half", "Half", "Half"] +desc = "Eight halfwords" + + +[variables.categories] +all = ["input", "output", "auxiliary", "virtual", "multiplicity"] +instantiated = ["input", "output", "auxiliary", "multiplicity"] diff --git a/spec/variables.typ b/spec/variables.typ new file mode 100644 index 000000000..42e7bc379 --- /dev/null +++ b/spec/variables.typ @@ -0,0 +1,20 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config + +#show: book-page.with(title: "Variables") + +#let config = load_config() + += Variables +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()]) + }, +) From efd78680c76b7ab5d31d0664244f1d14776ef3ce Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 30 Dec 2025 15:01:31 +0100 Subject: [PATCH 03/78] spec: Fix some chip rendering pain points (#83) This fixes the following pain points: - Assumptions and constraints requiring a `ref` for rendering to succeed - The `desc` field of `arith` constraints not being rendered - The `constraint` field of an `arith` constraint using eval in code mode - Long tables (columns and constraints) didn't break across pages - Template constraints did not have conditions rendered - Constraint groups didn't get the proper prefix if specified - The default branch of expression rendering has missing arguments It also introduces a nice visual todo macro --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/book.typ | 7 +++++++ spec/chip.typ | 43 +++++++++++++++++++++++++++++++++---------- spec/expr.typ | 4 ++-- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/spec/book.typ b/spec/book.typ index f50d7ed29..8b5cae160 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -13,3 +13,10 @@ // re-export page template #import "/templates/page.typ": project #let book-page = project + +#let todo(background: white, foreground: black, name: none, body) = block(fill: background, outset: 0.5em, 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") diff --git a/spec/chip.typ b/spec/chip.typ index c694db90a..cb8a8e4cf 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -20,6 +20,7 @@ /// Generates a table listing `chip`'s columns. #let render_chip_column_table(chip, config) = { // Group variables by category + show figure: set block(breakable: true) figure(table( columns: (auto, auto, 1fr), inset: 6pt, @@ -43,9 +44,11 @@ ), caption: [Column overview of #chip.name chip.]) } -#let cref(constraint) = { - if "ref" in constraint { - label(constraint.ref) +#let cref(obj, body) = { + if "ref" in obj { + [#body#label(obj.ref)] + } else { + body } } @@ -69,7 +72,7 @@ let index = if "range" in assumption { "." + assumption.range.at(0) } else { "" } let lbl = [#chip.name\-A] show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) - [#figure(kind: "assumption", numbering: (i) => [#lbl#i#index], supplement: [], [])#cref(assumption)] + cref(assumption)[#figure(kind: "assumption", numbering: (i) => [#lbl#i#index], supplement: [], [])] } figure(table( @@ -96,13 +99,16 @@ } assert(groups.all(group => group in all_groups), message: "unknown group") + // 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 index = if "range" in constraint { "." + constraint.range.at(0) } else { "" } let prefix = if "prefix" in group { group.prefix } let lbl = [#chip.name\-C#prefix] show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) - [#figure(kind: "constraint", numbering: (i) => [#lbl#i#index], supplement: [], [])#cref(constraint)] + cref(constraint)[#figure(kind: "constraint", numbering: (i) => [#lbl#i#index], supplement: [], [])] } /// Generates a representation of `constraint` @@ -112,17 +118,25 @@ if kind == "interaction" { raw(constraint.tag) + `[` + args_interaction_like(constraint.input, constraint.at("output", default: none)) + `]` } else if kind == "arith" { - [#eval(constraint.constraint)] + [#eval(constraint.constraint, mode: "markup")] } else if kind == "template" { - raw(constraint.tag) + `<` + args_interaction_like(constraint.input, constraint.at("output", default: none)) + `>` + 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 constraints has polynomial constraints + // Whether constraint has polynomial constraints let has_polynomial_constraints(constraint) = { - constraint.at("kind") == "arith" and ("poly" in constraint or "polys" in 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) = { + constraint.kind == "arith" and "desc" in constraint } // Rendering polynomial constraints @@ -139,6 +153,12 @@ },) } + // Rendering the additional "desc" field for arith constraints + let render_extra_description(constraint) = { + ([_description_], [], eval(constraint.desc, mode: "markup"), []) + } + + show figure: set block(breakable: true) figure(table( columns: (auto, auto, 1fr, auto), inset: 6pt, @@ -149,11 +169,14 @@ ..for group in groups { for constraint in chip.constraints.at(group) { ( - [#tag(constraint, group)], + [#tag(constraint, lookup_group(group))], [#interval(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/expr.typ b/spec/expr.typ index df1ddb2e6..1c08655fb 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -40,8 +40,8 @@ } else if type(expr) == int { num(expr) } else if type(expr) == array { - (dict.at(expr.at(0), default: (e) => { - assert(false, "Invalid expression: " + repr(e)) + (dict.at(expr.at(0), default: (pp, rec, e) => { + assert(false, message: "Invalid expression: " + repr(e)) }))(pp, res, expr) } } From 3084695bdfd2cc6272626c9bdf6539214183ce66 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:11:11 +0100 Subject: [PATCH 04/78] spec: support array-like types (#85) Support array-like variable types. Typed as: ```toml [[variables.auxiliary]] name = "var" type = ["Bit", 5] desc = "five bits" ``` --------- Co-authored-by: Robin Jadoul --- spec/chip.typ | 13 ++++++++++--- spec/expr.typ | 21 +++++++++++++++++++++ spec/src.typ | 12 ++++++++---- spec/src/config.toml | 27 --------------------------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index cb8a8e4cf..41bd44a29 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -1,4 +1,4 @@ -#import "expr.typ": expr_to_code, expr_to_math +#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) = { @@ -13,7 +13,14 @@ .filter(pair => pair.at(0) in config.variables.categories.instantiated) .map(pair => pair.at(1)) .flatten() - .map(var => config.variables.types.filter(type => type.label == var.type).at(0).subtypes.len()) + .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() } @@ -31,7 +38,7 @@ ..for (cat, vars) in chip.variables.pairs() { ([#emph(cat)], [], [], table.hline(stroke: .6pt)) for var in vars { - ([#raw(var.name)], [#raw(var.type)], [#eval(var.desc, mode: "markup")]) + ([#raw(var.name)], [#type_to_code(var.type)], [#eval(var.desc, mode: "markup")]) for (i, poly) in var.at("polys", default: ()).enumerate() { (if i == 0 { emph[def] }, [], expr_to_math(("=", ("idx", var.name, i), poly))) } diff --git a/spec/expr.typ b/spec/expr.typ index 1c08655fb..8b207fd4b 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -117,3 +117,24 @@ var: v => if v.len() == 1 { $#v$ } else { $#raw(v)$ }, num: n => math.equation[#n], ) + +// 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) == string { + return raw(typ) + } else { + assert(false, message: "Unknown format for type: " + repr(typ)) + } +} + +// Render a type to math +#let type_to_math(typ) = render_type_to_code(typ) // The code version looks reasonable enough in math too diff --git a/spec/src.typ b/spec/src.typ index f791bc1ca..75a81a5f9 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -38,12 +38,16 @@ assert(category in config.variables.categories.all) } + let all_labels = config.variables.types.map(type => type.label); for var in chip.variables.values().flatten() { + let type_label = if type(var.type) == array { + var.type.at(0) + } else { + var.type + } + // Check that all variable types are valid - assert( - var.type in config.variables.types.map(type => type.label), - message: "found invalid var type:" + var.type, - ) + assert(type_label in all_labels, message: "found invalid var type:" + repr(var.type)) } } diff --git a/spec/src/config.toml b/spec/src/config.toml index 1977b9155..29c0b9d83 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -81,33 +81,6 @@ desc = """\ The `Word` is the least significant digit. """ -# TODO: Having to define these manually will get tedious -[[variables.types]] -label = "Bit[3]" -subtypes = ["Bit", "Bit", "Bit"] -desc = "Three bits" - -[[variables.types]] -label = "Byte[2]" -subtypes = ["Byte", "Byte"] -desc = "Two bytes" - -[[variables.types]] -label = "Byte[8]" -subtypes = ["Byte", "Byte", "Byte", "Byte", "Byte", "Byte", "Byte", "Byte"] -desc = "Eight bytes" - -[[variables.types]] -label = "Half[3]" -subtypes = ["Half", "Half", "Half"] -desc = "Three halfwords" - -[[variables.types]] -label = "Half[8]" -subtypes = ["Half", "Half", "Half", "Half", "Half", "Half", "Half", "Half"] -desc = "Eight halfwords" - - [variables.categories] all = ["input", "output", "auxiliary", "virtual", "multiplicity"] instantiated = ["input", "output", "auxiliary", "multiplicity"] From 5e6a7d88cd884e2ea806d23cf9bdaa4c6b5f0821 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 30 Dec 2025 17:38:16 +0100 Subject: [PATCH 05/78] spec: Fixup wrong type sanity check for array types (#86) --- spec/expr.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/expr.typ b/spec/expr.typ index 8b207fd4b..d5a2e4d8d 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -129,7 +129,7 @@ if type(typ) == array { check_array_type(typ) return raw(typ.at(0) + "[" + str(typ.at(1)) + "]") - } else if type(typ) == string { + } else if type(typ) == str { return raw(typ) } else { assert(false, message: "Unknown format for type: " + repr(typ)) From 5157849a3709060054cb6fda74970b91023344d8 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 30 Dec 2025 17:33:57 +0100 Subject: [PATCH 06/78] Make precedence a lookup table instead of hardcoding it --- spec/expr.typ | 65 +++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/spec/expr.typ b/spec/expr.typ index d5a2e4d8d..c88168c27 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -16,19 +16,21 @@ // 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`. -// -// Precedence values: -// 0 : ^ -// 1 : neg (e.g., 5 => -5) -// 2 : * -// 3 : / -// 4 : not (e.g., 5 => 1-5) -// 5 : + -// 6 : - -// 7 : [] -// 8 : = -// 10: -#let MAX_PRECEDENCE = 10 + +#let PREC = ( + "MIN": -1, // + "pow": 0, // ^ + "neg": 1, // Unary - + "mul": 2, // * + "div": 3, // / + "not": 4, // not + "add": 5, // + + "sub": 6, // - + "idx": 7, // [] + "cast": 8, // cast + "eq": 9, // = + "MAX": 10, // +) // Mutual recursion through a trick from https://github.com/typst/typst/issues/744 #let make_expr_formatter(dict, empty: none, var: raw, num: str) = { @@ -45,7 +47,7 @@ }))(pp, res, expr) } } - res.with(MAX_PRECEDENCE) + res.with(PREC.MAX) } // Wrap code `expr` if `apply = true` @@ -60,23 +62,24 @@ // Typeset an expression as code #let expr_to_code = make_expr_formatter( ( - "idx": (pp, rec, e) => rec(0, e.at(1)) + `[` + rec(10, e.at(2)) + `]`, - "not": (pp, rec, e) => cwrap(`1 - ` + rec(4, e.at(1)), pp < 4), - "+": (pp, rec, e) => cwrap(e.slice(1).map(rec.with(5)).join(` + `), pp < 5), - "*": (pp, rec, e) => cwrap(e.slice(1).map(rec.with(2)).join(` ` + sym.dot + ` `), pp < 2), - "/": (pp, rec, e) => cwrap(rec(3, e.at(1)), pp < 3) + ` / ` + rec(3, e.at(2)), + "idx": (pp, rec, e) => rec(PREC.MIN, e.at(1)) + `[` + rec(PREC.MAX, e.at(2)) + `]`, + "not": (pp, rec, e) => cwrap(`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), + "*": (pp, rec, e) => 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") - rec(0, e.at(1)) + `^` + rec(0, e.at(2)) + // technically wrong associativity, but it's a constant + rec(PREC.pow, e.at(1)) + `^` + rec(PREC.pow, e.at(2)) }, - "=": (pp, rec, e) => rec(8, e.at(1)) + ` = ` + rec(8, 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(1, e.at(1)), pp < 1) + cwrap(`-` + rec(PREC.neg, e.at(1)), pp < PREC.neg) } else { // Subtraction - cwrap(e.slice(1).map(rec.with(6)).join(` - `), pp < 6) + cwrap(e.slice(1).map(rec.with(PREC.sub)).join(` - `), pp < PREC.sub) } }, ), @@ -94,23 +97,23 @@ // Typeset an expression as math #let expr_to_math = make_expr_formatter( ( - "idx": (pp, rec, e) => $#rec(7, e.at(1))_(#rec(7, e.at(2)))$, - "not": (pp, rec, e) => mwrap($1 - #rec(4, e.at(1))$, pp < 4), - "+": (pp, rec, e) => mwrap($#e.slice(1).map(rec.with(5)).join($+$)$, pp < 5), - "*": (pp, rec, e) => mwrap($#e.slice(1).map(rec.with(3)).join($dot$)$, pp < 3), - "/": (pp, rec, e) => $#rec(3, e.at(1)) / #rec(3, e.at(2))$, + "idx": (pp, rec, e) => $#rec(PREC.idx, e.at(1))_(#rec(PREC.idx, e.at(2)))$, + "not": (pp, rec, e) => mwrap($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), + "*": (pp, rec, e) => 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 and type(e.at(2)) == int, message: "Can only exponentiate constants") $#e.at(1)^#e.at(2)$ }, - "=": (pp, rec, e) => $#rec(8, e.at(1)) = #rec(8, 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(1, e.at(1))$, pp < 1) + mwrap($-#rec(PREC.neg, e.at(1))$, pp < PREC.neg) } else { // Subtraction - mwrap($#e.slice(1).map(rec.with(6)).join($-$)$, pp < 6) + mwrap($#e.slice(1).map(rec.with(PREC.sub)).join($-$)$, pp < PREC.sub) } }, ), From 9f9c2b9c77204bcda28339ecab67f2126c0a0df2 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 30 Dec 2025 17:49:54 +0100 Subject: [PATCH 07/78] Render type cast expressions --- spec/expr.typ | 58 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/spec/expr.typ b/spec/expr.typ index c88168c27..88d66d5d9 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -1,4 +1,30 @@ -// Grammar +// 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 @@ -11,6 +37,7 @@ // | ["=", 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, @@ -82,6 +109,10 @@ 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) + }, ), ) @@ -116,28 +147,11 @@ mwrap($#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)) 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], ) - -// 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) = render_type_to_code(typ) // The code version looks reasonable enough in math too From 9ad78c3190b0c88d0a1618caedb1fae81eadd6e1 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 30 Dec 2025 14:31:53 +0100 Subject: [PATCH 08/78] spec: Allow desc field on non-arith constraints as clarification --- spec/chip.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/chip.typ b/spec/chip.typ index 41bd44a29..d207cec8f 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -143,7 +143,7 @@ // Whether constraint has a "desc" field we need to render separately let has_extra_description(constraint) = { - constraint.kind == "arith" and "desc" in constraint + "desc" in constraint } // Rendering polynomial constraints From 03d30111040479df53223228ab77a1fe1e042eae Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 31 Dec 2025 10:51:52 +0100 Subject: [PATCH 09/78] spec: Modify cast operator precedence --- spec/expr.typ | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/expr.typ b/spec/expr.typ index 88d66d5d9..c005e5a35 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -48,13 +48,13 @@ "MIN": -1, // "pow": 0, // ^ "neg": 1, // Unary - - "mul": 2, // * - "div": 3, // / - "not": 4, // not - "add": 5, // + - "sub": 6, // - - "idx": 7, // [] - "cast": 8, // cast + "cast": 2, // cast + "mul": 3, // * + "div": 4, // / + "not": 5, // not + "add": 6, // + + "sub": 7, // - + "idx": 8, // [] "eq": 9, // = "MAX": 10, // ) From f3f21bcdf00a776f24956da3323b73a217436b8f Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:43:57 +0100 Subject: [PATCH 10/78] spec: improve definitions (#91) Updated definition rendering: * definitions are only allowed for virtual variables * definitions are now labelled with `def` rather than `poly` or `polys` * more flexible definitions possible for array-type virtuals. --- spec/chip.typ | 75 ++++++++++++++++++++++++++++++++++++++++++++------- spec/expr.typ | 4 ++- spec/src.typ | 12 ++++++++- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index d207cec8f..713f39b87 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -26,27 +26,82 @@ /// Generates a table listing `chip`'s columns. #let render_chip_column_table(chip, config) = { + + // Render a definition's range + let render_def_range(idx, range) = { + if type(range) == array { + if range.len() == 1 { + [#raw(idx) `=` #range.at(0)] + } else if range.len() == 2 { + [#raw(idx) #sym.in `[`#range.at(0)`,`#range.at(1)`]`] + } else { + assert(false, message: "invalid range: " + repr(range) + repr(range.len())) + } + } else { + [#raw(idx) `=` #range] + } + } + + // 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)) + + if "poly" in def { + ( + [], + table.cell(align: right, emph[definition]), + expr_to_math((":=", ("idx", var_name, def.idx), def.poly)), + render_def_range(def.idx, def.range) + ) + } else if "polys" in def { + ( + [], + table.cell(align: right, emph[definition]), + table.cell(colspan: 2, expr_to_math(("idx", var_name, def.idx))) + ) + for (i, poly) in def.polys.enumerate() { + ( + [], + [], + expr_to_math((":=", " ", poly.poly)), + render_def_range(def.idx, poly.range), + ) + } + } else { + assert(false, message: "invalid definition: " + repr(def)) + } + } + // Group variables by category show figure: set block(breakable: true) figure(table( - columns: (auto, auto, 1fr), + columns: (auto, auto, 1fr, auto), inset: 6pt, align: left + top, stroke: none, - table.header([*Label*], [*Type*], [*Description*]), + table.header([*Label*], [*Type*], table.cell(colspan: 2, [*Description*])), table.hline(stroke: stroke(thickness: 2pt)), ..for (cat, vars) in chip.variables.pairs() { - ([#emph(cat)], [], [], table.hline(stroke: .6pt)) + (table.cell(colspan: 4, emph(cat)), table.hline(stroke: .6pt)) for var in vars { - ([#raw(var.name)], [#type_to_code(var.type)], [#eval(var.desc, mode: "markup")]) - for (i, poly) in var.at("polys", default: ()).enumerate() { - (if i == 0 { emph[def] }, [], expr_to_math(("=", ("idx", var.name, i), poly))) - } - if "poly" in var { - (emph[def], [], expr_to_math(var.poly)) + ( + [#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, []), ) }, ), caption: [Column overview of #chip.name chip.]) } diff --git a/spec/expr.typ b/spec/expr.typ index c005e5a35..ae7bd0792 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -55,7 +55,7 @@ "add": 6, // + "sub": 7, // - "idx": 8, // [] - "eq": 9, // = + "eq": 9, // = and := "MAX": 10, // ) @@ -100,6 +100,7 @@ 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 @@ -138,6 +139,7 @@ $#e.at(1)^#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 diff --git a/spec/src.typ b/spec/src.typ index 75a81a5f9..7c9e68487 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -38,8 +38,18 @@ assert(category in config.variables.categories.all) } + // 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 chip.variables.values().flatten() { + for var in all_vars { let type_label = if type(var.type) == array { var.type.at(0) } else { From 442e32719acf52d91d31972d298096dbbd541c3b Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:26:29 +0100 Subject: [PATCH 11/78] spec: update table rendering (#93) * spec: update `description` printing * spec: update `polynomial constraint` printing --- spec/chip.typ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 713f39b87..30b93373c 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -211,13 +211,13 @@ } (..for poly in polys { - ([_polynomial constraint_], [], $#expr_to_math(poly) = 0$, []) + (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) = { - ([_description_], [], eval(constraint.desc, mode: "markup"), []) + (table.cell(align: right, colspan: 2, [_description_]), eval(constraint.desc, mode: "markup"), []) } show figure: set block(breakable: true) From c271c9309fd91b26f5d52e42dcd48d0ce93aeb54 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Sat, 3 Jan 2026 14:55:47 +0100 Subject: [PATCH 12/78] spec: introduce "condition" column type --- spec/src.typ | 5 ++++- spec/src/config.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/src.typ b/spec/src.typ index 7c9e68487..8200b47c1 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -35,7 +35,10 @@ #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) + assert( + category in config.variables.categories.all, + message: "invalid category: " + repr(category) + ) } // Check that `def` is only contained in `virtual` variables diff --git a/spec/src/config.toml b/spec/src/config.toml index 29c0b9d83..28abdcbfc 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -82,5 +82,5 @@ desc = """\ """ [variables.categories] -all = ["input", "output", "auxiliary", "virtual", "multiplicity"] +all = ["input", "output", "auxiliary", "virtual", "multiplicity", "condition"] instantiated = ["input", "output", "auxiliary", "multiplicity"] From a0801d9806e66e2b175a4d19d962d4c05cddf2d2 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Sat, 3 Jan 2026 16:24:33 +0100 Subject: [PATCH 13/78] spec: is_bit template --- spec/book.typ | 1 + spec/is_bit.typ | 47 ++++++++++++++++++++++++++++++++++++++++++++ spec/src/is_bit.toml | 20 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 spec/is_bit.typ create mode 100644 spec/src/is_bit.toml diff --git a/spec/book.typ b/spec/book.typ index 8b5cae160..dcf1d0c1b 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -7,6 +7,7 @@ title: "Lambda VM specification", summary: [ #chapter("variables.typ")[Variables] + #chapter("is_bit.typ")[IS_BIT template] ] ) diff --git a/spec/is_bit.typ b/spec/is_bit.typ new file mode 100644 index 000000000..c379080cd --- /dev/null +++ b/spec/is_bit.typ @@ -0,0 +1,47 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_column_table, render_constraint_table + +#show: book-page.with(title: "IS_BIT template") + +#let config = load_config() +#let chip = load_chip("src/is_bit.toml", config) + +#let is_bit = raw(chip.name) + +#let highlighted_code(code) = { + box( + inset: (left: 4pt, right: 4pt), + outset: (top: 4pt, bottom: 4pt), + radius: 2pt, + fill: luma(230), + raw(code)) +} + += #is_bit template +#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. + +== Interface +The #is_bit constraint template has the following interface: +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => IS_BIT")) +where `cond` is any value described by an expression _of degree at most $1$_. +Note that #highlighted_code("IS_BIT") can be used to denote the _unconditional_ application of the #is_bit template to `X`. + +== Variables +The #is_bit template operates on two variables: `cond` and `X`: +#render_chip_column_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. + +== Proof of correctness +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/src/is_bit.toml b/spec/src/is_bit.toml new file mode 100644 index 000000000..47e96a27e --- /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", ["not", "X"]] +ref = "isbit:c:isbit" From 37d4cc056b84db732cd8b05b29a518edd5271552 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Mon, 5 Jan 2026 11:42:10 +0100 Subject: [PATCH 14/78] spec: CPU chip for RV64IMC (#88) * spec: Initial CPU version to handle RV64IMC * Address review comments * Add word_instr as input to SHIFT --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/book.typ | 2 +- spec/cpu.typ | 84 +++++ spec/src/config.toml | 16 +- spec/src/cpu.toml | 727 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 827 insertions(+), 2 deletions(-) create mode 100644 spec/cpu.typ create mode 100644 spec/src/cpu.toml diff --git a/spec/book.typ b/spec/book.typ index dcf1d0c1b..c4c3a8c6d 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -1,4 +1,3 @@ - #import "@preview/shiroa:0.3.1": * #show: book @@ -8,6 +7,7 @@ summary: [ #chapter("variables.typ")[Variables] #chapter("is_bit.typ")[IS_BIT template] + #chapter("cpu.typ")[CPU chip] ] ) diff --git a/spec/cpu.typ b/spec/cpu.typ new file mode 100644 index 000000000..00a33f5a2 --- /dev/null +++ b/spec/cpu.typ @@ -0,0 +1,84 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, +) + +#let config = load_config() +#let chip = load_chip("src/cpu.toml", config) + +#show: book-page.with(title: "CPU chip") + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `CPU` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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") + +#rj[All casts for interactions will have to be reviewed once other chip interfaces stabilise] + +=== 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 + +#rj[proper ref to IsZero/IsEqual] +For @cpu:c:is_equal, refer to the logic of IsZero or IsEqual, in combination with the subtraction of @cpu:c:sub. + +#render_constraint_table(chip, config, groups: "misc") + +#rj[Document the choice to not have a multiplicity column here for padding] diff --git a/spec/src/config.toml b/spec/src/config.toml index 28abdcbfc..b66639e2a 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -78,9 +78,23 @@ 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. + 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 = "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, see there for more details about the magic number." + [variables.categories] all = ["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..562a657d0 --- /dev/null +++ b/spec/src/cpu.toml @@ -0,0 +1,727 @@ +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" + +[[variables.input]] +name = "rs1" +type = "Byte" +desc = "Source register 1 index" + +[[variables.input]] +name = "rs2" +type = "Byte" +desc = "Source register 2 index" + +[[variables.input]] +name = "rd" +type = "Byte" +desc = "Destination register index" + +[[variables.input]] +name = "write_register" +type = "Bit" +desc = "Whether to write back to the destination register" + +# 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 at least 2 bytes" + +[[variables.input]] +name = "memory_4bytes" +type = "Bit" +desc = "Whether the memory access (read or write) touches at least 4 bytes" + +[[variables.input]] +name = "memory_8bytes" +type = "Bit" +desc = "Whether the memory access (read or write) touches at least 8 bytes" + +# 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" + +# TODO: Should this just be a word? (CHECK: effect on computation/extension of arg2) +# TODO: make sure decode correctly extends this (may be zero for unsigned and word_instr?) +[[variables.input]] +name = "imm" +type = "DWordWL" +desc = "The fully extended 64-bit version of the immediate" + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Indicates whether we're dealing with a signed or unsigned instruction" + +[[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`""" + +[[variables.input]] +name = "muldiv_selector" +type = "Bit" +desc = "Selects which output of `MUL` (lo/hi) or `DIV` (quo/rem) is wanted" + +[[variables.input]] +name = "word_instr" +type = "Bit" +desc = "Whether the instruction is a \\*W instruction, requiring the inputs and outputs to be (sign) extended" + +[[variables.input]] +name = "ADD" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "SUB" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "SLT" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "AND" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "OR" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "XOR" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "SHIFT" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "JALR" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "BEQ" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "BLT" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "LOAD" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "STORE" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "MUL" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "DIVREM" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "ECALL" +type = "Bit" +desc = "One-hot ALU selector flag" + +[[variables.input]] +name = "EBREAK" +type = "Bit" +desc = "One-hot ALU selector flag" + + +# Output +[[variables.output]] +name = "next_pc" +type = "DWordWL" +desc = "The program counter for the next instruction" + +[[variables.output]] +name = "rvd" +type = "DWordWL" +desc = "The value to (maybe) be written back to rvd" + +# Auxiliary +[[variables.auxiliary]] +name = "rv1" +type = "DWordWHH" +desc = "The value of register `rs1`" + +[[variables.auxiliary]] +name = "rv2" +type = "DWordWHH" +desc = "The value of register `rs2`" + +[[variables.auxiliary]] +name = "rv1_sign_bit" +type = "Bit" +desc = "The sign bit of `rv1` if seen as a 32-bit word" + +[[variables.auxiliary]] +name = "arg1" +type = "DWordBL" +desc = "The extended version of `rv1`, depending on `c_type_instruction`" + +[[variables.auxiliary]] +name = "arg2_sign_bit" +type = "Bit" +desc = "The sign bit of `arg2` if seen as a 32-bit word" + +[[variables.auxiliary]] +name = "arg2" +type = "DWordBL" +desc = "A multiplexed version of `rv2` and `imm`, to be used as second argument to ALU calls" + +[[variables.auxiliary]] +name = "res_sign_bit" +type = "Bit" +desc = "The sign bit of `res`, if seen as a 32-bit word" + +[[variables.auxiliary]] +name = "res" +type = "DWordBL" +desc = "The ALU result" + +[[variables.auxiliary]] +name = "is_equal" +type = "Bit" +desc = "Whether `rv1` and `arg2` are equal" + +[[variables.auxiliary]] +name = "branch_cond" +type = "Bit" +desc = "Whether a branch is taken, i.e., the branch condition" + +# Virtual +[[variables.virtual]] +name = "packed_decode" +type = "BaseField" +desc = "A packed representation of all bit flags and register indices obtained from the decoding" +poly = ["+", + ["*", ["^", 2, 0], "write_register"], + ["*", ["^", 2, 1], "memory_2bytes"], + ["*", ["^", 2, 2], "memory_4bytes"], + ["*", ["^", 2, 3], "memory_8bytes"], + ["*", ["^", 2, 4], "c_type_instruction"], + ["*", ["^", 2, 5], "signed"], + ["*", ["^", 2, 6], "mp_selector"], + ["*", ["^", 2, 7], "muldiv_selector"], + ["*", ["^", 2, 8], "word_instr"], + ["*", ["^", 2, 9], "ADD"], + ["*", ["^", 2, 10], "SUB"], + ["*", ["^", 2, 11], "SLT"], + ["*", ["^", 2, 12], "AND"], + ["*", ["^", 2, 13], "OR"], + ["*", ["^", 2, 14], "XOR"], + ["*", ["^", 2, 15], "SHIFT"], + ["*", ["^", 2, 16], "JALR"], + ["*", ["^", 2, 17], "BEQ"], + ["*", ["^", 2, 18], "BLT"], + ["*", ["^", 2, 19], "LOAD"], + ["*", ["^", 2, 20], "STORE"], + ["*", ["^", 2, 21], "MUL"], + ["*", ["^", 2, 22], "DIVREM"], + ["*", ["^", 2, 23], "ECALL"], + ["*", ["^", 2, 24], "EBREAK"], + ["*", ["^", 2, 25], "rs1"], + ["*", ["^", 2, 33], "rs2"], + ["*", ["^", 2, 41], "rd"], +] + + +[[assumptions]] +desc = "The flags are a one-hot vector in the decoding" +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"] + + +[[constraint_groups]] +name = "range" +prefix = "R" + +[[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_kind_instruction"] +ref = "cpu:c:range_c_kind_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"] + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = ["rs2"] + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = ["rd"] + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "arg1", "i"]] +range = ["i", 0, 7] + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "arg2", "i"]] +range = ["i", 0, 7] + +[[constraints.range]] +kind = "interaction" +tag = "IS_BYTE" +input = [["idx", "res", "i"]] +range = ["i", 0, 7] + + +[[constraint_groups]] +name = "alu" +prefix = "A" + +[[constraints.alu]] +kind = "template" +tag = "ADD" +cond = ["+", "ADD", "LOAD", "STORE"] +input = [["cast", "arg1", "DWordWL"], ["cast", "arg2", "DWordWL"]] +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", "DWordHHW"], ["cast", "arg2", "DWordHHW"], "signed"] +output = ["idx", "res", 0] +multiplicity = ["+", "SLT", "BLT"] + +[[constraints.alu]] +kind = "arith" +constraint = "$#`SLT` + #`BLT` => #`res[i]` = 0$" +poly = ["*", ["+", "SLT", "BLT"], ["idx", "res", "i"]] +range = ["i", 1, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "AND_BYTE" +input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] +output = ["idx", "res", "i"] +multiplicity = "AND" +range = ["i", 0, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "OR_BYTE" +input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] +output = ["idx", "res", "i"] +multiplicity = "OR" +range = ["i", 0, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "XOR_BYTE" +input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] +output = ["idx", "res", "i"] +multiplicity = "XOR" +range = ["i", 0, 7] + +[[constraints.alu]] +kind = "interaction" +tag = "SHIFT" +input = [["cast", "arg1", "DWordHL"], ["idx", "arg2", 0], "mp_selector", "signed", "word_instr"] +output = ["cast", "res", "DWordHL"] +multiplicity = "SHIFT" + +[[constraints.alu]] +kind = "template" +tag = "ADD" +input = ["pc", ["cast", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], "DWordWL"]] +output = ["cast", "DWordWL", "res"] +cond = "JALR" + +# TODO: no types available, so no casting yet +[[constraints.alu]] +kind = "interaction" +tag = "MUL" +input = ["arg1", "signed", "arg2", "mp_selector", "muldiv_selector"] +output = "res" +multiplicity = "MUL" + +# TODO: no types available, so no casting yet +[[constraints.alu]] +kind = "interaction" +tag = "DVRM" +input = ["arg1", "arg2", "signed", "muldiv_selector"] +output = "res" +multiplicity = "DIVREM" + + +[[constraint_groups]] +name = "mem" +prefix = "M" + +# TODO: no types available, so no casting yet +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, "rs1"], "rv1", ["+", "timestamp", 0], 1, 0, 0] +output = "rv1" + +# TODO: no types available, so no casting yet +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, "rs2"], "rv2", ["+", "timestamp", 1], 1, 0, 0] +output = "rv2" + +# TODO: no types available, so no casting yet +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, "rd"], "rvd", ["+", "timestamp", 2], 1, 0, 0] + +# TODO: no types available, so no casting yet +[[constraints.mem]] +kind = "interaction" +tag = "LOAD" +input = [0, "res", ["+", "timestamp", 0], "memory_2bytes", "memory_4bytes", "memory_8bytes", "signed"] +output = "rvd" +multiplicity = "LOAD" + +# TODO: no types available, so no casting yet +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [0, "res", "rv2", ["+", "timestamp", 1], "memory_2bytes", "memory_4bytes", "memory_8bytes"] +multiplicity = "STORE" + +# TODO: no types available, so no casting yet +[[constraints.mem]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 255], "next_pc", ["+", "timestamp", 1], 1, 0, 0] +output = "pc" + + +[[constraint_groups]] +name = "sys" +prefix = "S" + +[[constraints.sys]] +kind = "arith" +constraint = "`!EBREAK`" +desc = "We treat `EBREAK` as an unprovable trap" +poly = ["not", "EBREAK"] + +# TODO: no types available, so no casting yet +[[constraints.sys]] +kind = "interaction" +tag = "ECALL" +input = ["rv1", "pc", "timestamp", "rv2"] +output = "rvd" +multiplicity = "ECALL" + + +[[constraint_groups]] +name = "ext" +prefix = "E" + +[[constraints.ext]] +kind = "arith" +constraint = "$(#`rv1_sign_bit` or #`arg2_sign_bit` or #`res_sign_bit`) => #`word_instr`$" +poly = ["*", ["+", "rv1_sign_bit", "arg2_sign_bit", "res_sign_bit"], ["not", "word_instr"]] + +[[constraints.ext]] +kind = "interaction" +tag = "MSB16" +input = [["idx", "rv1", 1]] +output = "rv1_sign_bit" +multiplicity = "word_instr" + +[[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_sign_bit` dot #`signed`$" +poly = ["-", ["idx", ["cast", "arg1", "DWordWL"], 1], ["*", ["not", "word_instr"], ["idx", "rv1", 2]], ["*", "signed", "rv1_sign_bit", ["-", ["^", 2, 32], 1]]] + +[[constraints.ext]] +kind = "interaction" +tag = "MSB16" +input = [["idx", "rv2", 1]] +output = "arg2_sign_bit" +multiplicity = "word_instr" + +[[constraints.ext]] +kind = "arith" +constraint = "$#`arg2[:4]` = (1 - #`STORE` - #`LOAD`) dot #`rv2[:2]` + (1 - #`BEQ` - #`BLT`) dot #`imm[0]`$" +poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 0], ["*", ["-", 1, "STORE", "LOAD"], ["idx", ["cast", "rv2", "DWordWL"], 0]], ["*", ["-", 1, "BEQ", "BLT"], ["idx", "imm", 0]]] + +[[constraints.ext]] +kind = "arith" +constraint = "$#`arg2[4:]` = (1 - #`STORE` - #`LOAD`) dot ((1 - #`word_instr`) dot #`rv2[2]` + #`signed` dot #`arg2_sign_bit` dot (2^(32) - 1)) + (1 - #`BEQ` - #`BLT`) dot #`imm[1]`$" +poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 1], ["*", ["-", 1, "STORE", "LOAD"], ["not", "word_instr"], ["idx", "rv2", 2]], ["*", ["-", 1, "STORE", "LOAD"], "signed", "arg2_sign_bit", ["-", ["^", 2, 32], 1]], ["*", ["-", 1, "BEQ", "BLT"], ["idx", "imm", 1]]] + +[[constraints.ext]] +kind = "interaction" +tag = "MSB8" +input = [["idx", "res", 3]] +output = "res_sign_bit" +multiplicity = "word_instr" + +[[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_sign_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_sign_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"], + ["*", ["not", ["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", ["idx", "imm", 0], ["cast", "arg1", "DWordWL"], "JALR"] +output = "next_pc" +multiplicity = "branch_cond" + +[[constraints.misc]] +kind = "template" +tag = "ADD" +input = ["pc", ["cast", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], "DWordWL"]] +output = "next_pc" +desc = "Increment `pc` to `next_pc` if we're not branching" From 4b8c801bbc646cd9eaef367c3cf687990e2f21c0 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:50:41 +0100 Subject: [PATCH 15/78] spec: improve multi-poly definition rendering (#98) --- spec/chip.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/chip.typ b/spec/chip.typ index 30b93373c..26d566e4f 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -71,7 +71,7 @@ ( [], [], - expr_to_math((":=", " ", poly.poly)), + table.cell(inset: (left: 1.5em), expr_to_math((":=", "", poly.poly))), render_def_range(def.idx, poly.range), ) } From 217d2d72244c46d51b3164298192e9e0e8618ce6 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Mon, 5 Jan 2026 15:50:52 +0100 Subject: [PATCH 16/78] spec: BRANCH chip (#92) * spec: init BRANCH chip * Small cleanup * Clean up variable naming and generally address review comments * outdated comment --------- Co-authored-by: Erik Takke --- spec/book.typ | 1 + spec/branch.typ | 38 ++++++++++++ spec/src/branch.toml | 140 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 spec/branch.typ create mode 100644 spec/src/branch.toml diff --git a/spec/book.typ b/spec/book.typ index c4c3a8c6d..af747c88c 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -8,6 +8,7 @@ #chapter("variables.typ")[Variables] #chapter("is_bit.typ")[IS_BIT template] #chapter("cpu.typ")[CPU chip] + #chapter("branch.typ")[BRANCH] ] ) diff --git a/spec/branch.typ b/spec/branch.typ new file mode 100644 index 000000000..d01e9fa03 --- /dev/null +++ b/spec/branch.typ @@ -0,0 +1,38 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, +) + +#let config = load_config() +#let chip = load_chip("src/branch.toml", config) + +#show: book-page.with(title: "BRANCH chip") + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `BRANCH` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_table(chip, config) + +== Assumptions + +#render_chip_assumptions(chip, config) + +== Constraints + +#rj[Check correspondence with CPU for passing in `offset` as word or dword] +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") + diff --git a/spec/src/branch.toml b/spec/src/branch.toml new file mode 100644 index 000000000..b93639602 --- /dev/null +++ b/spec/src/branch.toml @@ -0,0 +1,140 @@ +name = "BRANCH" + + +# Input + +[[variables.input]] +name = "pc" +type = "DWordWL" +desc = "The current pc, used as base address when `!JALR`" + +[[variables.input]] +name = "offset" +type = "Word" +desc = "The offset from the base address to jump to" + +[[variables.input]] +name = "register" +type = "DWordWL" +desc = "The base address to use when `JALR`" + +[[variables.input]] +name = "JALR" +type = "Bit" +desc = "Selects between `pc` and `register` as base address, needed for the `JALR` instruction" + + +# Output + +[[variables.output]] +name = "next_pc_high" +type = ["Half", 3] +desc = "The upper part of the next pc" + +[[variables.output]] +name = "next_pc_low" +type = ["Byte", 2] +desc = "The lower part of the next pc" + + +# 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." + + +# 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 = [ + {range = [0], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], ["idx", "unmasked_low_byte", 0]]}, + {range = [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 = [ + {range = [0], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], ["idx", "next_pc_low", 0]]}, + {range = [1], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 2]], ["idx", "next_pc_high", 1]]}, +]} + + +# Multiplicity + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" + + +[[assumptions]] +desc = "`pc` is range checked, `IS_WORD[pc[i]]`" +range = ["i", 0, 1] + +[[assumptions]] +desc = "`offset` is range checked, `IS_WORD[offset]`" + +[[assumptions]] +desc = "`register` is range checked, `IS_WORD[register[i]]`" +range = ["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 = [["idx", "unmasked_low_byte", 0], 254] +output = ["idx", "next_pc_low", 0] +multiplicity = "μ" + +[[constraints.all]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", "next_pc_high", "i"]] +range = ["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 = "-μ" From e53d2a8d473abb1d398bbdc1299c06d0304fe357 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:10:49 +0100 Subject: [PATCH 17/78] spec: conditionally render constraint table headers (#94) * spec: conditionally render constraint table headers * spec: simplify `selected_constraints` expression * spec: repurpose `selected_constraints` --- spec/chip.typ | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 26d566e4f..e5b40dadd 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -160,6 +160,7 @@ 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)) @@ -220,16 +221,29 @@ (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 => "range" in x) + + // 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*], [*Range*], [*Description*], [*Multiplicity*]), + table.header( + [*Tag*], + if do_display_range {[*Range*]} else {[]}, + [*Description*], + if do_display_multiplicity {[*Multiplicity*]} else {[]}, + ), table.hline(stroke: stroke(thickness: 2pt)), - ..for group in groups { - for constraint in chip.constraints.at(group) { + ..for (group, group_constraints) in selected_constraints.pairs() { + for constraint in group_constraints { ( [#tag(constraint, lookup_group(group))], [#interval(constraint)], @@ -243,6 +257,6 @@ render_polynomial_constraints(constraint) } } - }, + } ), caption: [Constraint overview of #chip.name chip.]) } From 1b838509024500d738cdf558ab26bbba3f61dd3e Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:17:11 +0100 Subject: [PATCH 18/78] spec: do not print index in assumption/constraint ref (#96) --- spec/chip.typ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index e5b40dadd..8be99ac2a 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -134,7 +134,7 @@ let index = if "range" in assumption { "." + assumption.range.at(0) } else { "" } let lbl = [#chip.name\-A] show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) - cref(assumption)[#figure(kind: "assumption", numbering: (i) => [#lbl#i#index], supplement: [], [])] + cref(assumption)[#figure(kind: "assumption", numbering: (i) => [#lbl#i], supplement: [], [])] } figure(table( @@ -171,7 +171,7 @@ let prefix = if "prefix" in group { group.prefix } let lbl = [#chip.name\-C#prefix] show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) - cref(constraint)[#figure(kind: "constraint", numbering: (i) => [#lbl#i#index], supplement: [], [])] + cref(constraint)[#figure(kind: "constraint", numbering: (i) => [#lbl#i], supplement: [], [])] } /// Generates a representation of `constraint` From 07000c76d22c08948b1a9249b73c066b5dc87267 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 7 Jan 2026 13:24:00 +0100 Subject: [PATCH 19/78] spec: Make constraint numbering restart when displaying multiple chips in one document (#108) --- spec/chip.typ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 8be99ac2a..e807e13ed 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -134,7 +134,7 @@ let index = if "range" in assumption { "." + assumption.range.at(0) } else { "" } let lbl = [#chip.name\-A] show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) - cref(assumption)[#figure(kind: "assumption", numbering: (i) => [#lbl#i], supplement: [], [])] + cref(assumption)[#figure(kind: chip.name + "assumption", numbering: (i) => [#lbl#i], supplement: [], [])] } figure(table( @@ -171,7 +171,7 @@ let prefix = if "prefix" in group { group.prefix } let lbl = [#chip.name\-C#prefix] show figure: (it) => align(left, block[#lbl#context it.counter.display()#index]) - cref(constraint)[#figure(kind: "constraint", numbering: (i) => [#lbl#i], supplement: [], [])] + cref(constraint)[#figure(kind: chip.name + "constraint", numbering: (i) => [#lbl#i], supplement: [], [])] } /// Generates a representation of `constraint` From 7e842e511fe9a5bb63d7c9fa468e81bf3338c18e Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Thu, 8 Jan 2026 11:17:27 +0100 Subject: [PATCH 20/78] spec: Introduce LT chip (#90) Co-authored-by: Erik Takke --- spec/book.typ | 1 + spec/lt.typ | 79 ++++++++++++++++++++++++++ spec/src/lt.toml | 143 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 spec/lt.typ create mode 100644 spec/src/lt.toml diff --git a/spec/book.typ b/spec/book.typ index af747c88c..7787acbce 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -9,6 +9,7 @@ #chapter("is_bit.typ")[IS_BIT template] #chapter("cpu.typ")[CPU chip] #chapter("branch.typ")[BRANCH] + #chapter("lt.typ")[LT], ] ) diff --git a/spec/lt.typ b/spec/lt.typ new file mode 100644 index 000000000..ff3b6dae3 --- /dev/null +++ b/spec/lt.typ @@ -0,0 +1,79 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, +) + +#let config = load_config() +#let chip = load_chip("src/lt.toml", config) + +#show: book-page.with(title: "LT chip") + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `LT` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_table(chip, config) + +== Assumptions +We assume the inputs `lhs`, `rhs` and `signed` are appropriately 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. + +#render_constraint_table(chip, config, groups: "sub") + +The chip contributes the following to the lookup argument. + +#render_constraint_table(chip, config, groups: "output") diff --git a/spec/src/lt.toml b/spec/src/lt.toml new file mode 100644 index 000000000..1a441c2b3 --- /dev/null +++ b/spec/src/lt.toml @@ -0,0 +1,143 @@ +name = "LT" + + +# Input + +[[variables.input]] +name = "lhs" +type = "DWordHHW" +desc = "The left operand" + +[[variables.input]] +name = "rhs" +type = "DWordHHW" +desc = "The right operand" + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "whether to interpret `lhs` and `rhs` as signed integers (1) or not (0)" + +# Output + +[[variables.output]] +name = "lt" +type = "Bit" +desc = "Whether $#`lhs` < #`rhs`$, taking `signed` into account" + + +# Auxiliary + +[[variables.auxiliary]] +name = "lhs_sub_rhs" +type = "DWordHL" +desc = "$#`lhs` - #`rhs`$" + +[[variables.auxiliary]] +name = "lhs_msb" +type = "Bit" +desc = "The most significant bit of `lhs`" + +[[variables.auxiliary]] +name = "rhs_msb" +type = "Bit" +desc = "The most significant bit of `rhs`" + +# Virtual + +[[variables.virtual]] +name = "carry" +type = ["Bit", 2] +desc = "The carry for adding `lhs_sub_rhs` back to `rhs`" +def = {idx = "i", polys = [ + {range = [0], poly = ["*", ["^", 2, -32], ["-", ["+", ["idx", "rhs", 0], ["idx", ["cast", "lhs_sub_rhs", "DWordWL"], 0]], ["idx", "lhs", 0]]]}, + {range = [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 = "" + + +[[assumptions]] +desc = "`IS_HALFWORD[lhs[i]]` and `IS_WORD[lhs[0]]`" +range = ["i", 1, 2] +ref = "lt:a:range_lhs" + +[[assumptions]] +desc = "`IS_HALFWORD[rhs[i]]` and `IS_WORD[rhs[0]]`" +range = ["i", 1, 2] +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"]] +range = ["i", 0, 1] + +[[constraints.sub]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", "lhs_sub_rhs", "i"]] +range = ["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 = ["lhs", "rhs", "signed"] +output = "lt" +multiplicity = "-μ" From 4af29ef1afdccb5ccaba7ff9be2bf70a473d650a Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Thu, 8 Jan 2026 13:31:10 +0100 Subject: [PATCH 21/78] spec: Fix constraint group lookup (#105) --- spec/chip.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/chip.typ b/spec/chip.typ index e807e13ed..6914aa1f6 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -160,7 +160,7 @@ 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() + 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)) From 9d07e5d0c0903f9e516ed27845983c1e247783ef Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:13:28 +0100 Subject: [PATCH 22/78] spec: `SHIFT` chip (#84) * spec: rough draft SHIFT chip * various minor fixes * implement right-limb shifting * Update rendering "polynomial constriant" in table * fix degree 4 issues * Further update to SHIFT chip * Clean up SHIFT * spec/shift: add assumption * spec/shift: Add lookup constraint * spec/shift: make extension virtual Kudos to Robin for uncovering this! * spec/shift: Simplify limb-situation Kudos to Robin for pointing this out! * spec/SHIFT: fix typo * Turn `limb_shift_x` into array * spec: support "sum" expression in math * Simplify limb-shifting constraint * spec: attempt at refactoring `shift` * spec: overhaul SHIFT * spec: SHIFT: rename `extensions` as `extension` Co-authored-by: Robin Jadoul * spec: SHIFT: make `shift` of type Byte * spec: SHIFT: replace variable '0x' with constant 0x * spec: SHIFT: remove "cheaper" remark * spec: SHIFT: fix `shifted` description * spec: SHIFT: make output a DWordWL * spec: SHIFT * spec: SHIFT: introduce explanation; update some constraint elaborations * Apply suggestions from the code review Co-authored-by: Robin Jadoul * spec: SHIFT: update `bits_shift` desc * spec: SHIFT: update `limb_shift` desc * spec: SHIFT: add missing IS_BIT constraint for limb_shift * spec: SHIFT: update description * spec: SHIFT: fix sum's expr-to-math * Minor language pass Co-authored-by: Robin Jadoul --------- Co-authored-by: Robin Jadoul --- spec/book.typ | 3 +- spec/expr.typ | 22 +++- spec/shift.typ | 175 +++++++++++++++++++++++++++ spec/src/shift.toml | 283 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 476 insertions(+), 7 deletions(-) create mode 100644 spec/shift.typ create mode 100644 spec/src/shift.toml diff --git a/spec/book.typ b/spec/book.typ index 7787acbce..6652ed646 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -8,8 +8,9 @@ #chapter("variables.typ")[Variables] #chapter("is_bit.typ")[IS_BIT template] #chapter("cpu.typ")[CPU chip] + #chapter("shift.typ")[SHIFT chip] #chapter("branch.typ")[BRANCH] - #chapter("lt.typ")[LT], + #chapter("lt.typ")[LT] ] ) diff --git a/spec/expr.typ b/spec/expr.typ index ae7bd0792..547f2cad2 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -31,6 +31,7 @@ // | ["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 @@ -51,12 +52,13 @@ "cast": 2, // cast "mul": 3, // * "div": 4, // / - "not": 5, // not - "add": 6, // + - "sub": 7, // - - "idx": 8, // [] - "eq": 9, // = and := - "MAX": 10, // + "sum": 5, // Σ + "not": 6, // not + "add": 7, // + + "sub": 8, // - + "idx": 9, // [] + "eq": 10, // = and := + "MAX": 11, // ) // Mutual recursion through a trick from https://github.com/typst/typst/issues/744 @@ -92,6 +94,7 @@ "idx": (pp, rec, e) => rec(PREC.MIN, e.at(1)) + `[` + rec(PREC.MAX, e.at(2)) + `]`, "not": (pp, rec, e) => cwrap(`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) => 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) => { @@ -132,6 +135,13 @@ "idx": (pp, rec, e) => $#rec(PREC.idx, e.at(1))_(#rec(PREC.idx, e.at(2)))$, "not": (pp, rec, e) => mwrap($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) => 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) => { diff --git a/spec/shift.typ b/spec/shift.typ new file mode 100644 index 000000000..3555d64e4 --- /dev/null +++ b/spec/shift.typ @@ -0,0 +1,175 @@ +#import "/book.typ": book-page, et +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_assumptions, +) + +#let config = load_config() +#let chip = load_chip("src/shift.toml", config) + +#let shift = raw(chip.name) + +#show: book-page.with(title: "SHIFT chip") + += #shift chip + +== Interface +The #shift chip has the following interface: +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(240), +``` +// param in: the value being shifted +// param shift: the number of bits to shift `in` by +// param direction: whether to shift left (0) or right (1) +// param signed: whether to interpret `in` as a signed (1) or unsigned (0) integer +// param word_instr: whether to execute the SLL/SR* (0) or SLLW/SR*W (1) instruction +// out shifted: the resulting value +SHIFT[shifted: DWord; in: DWord, shift: Byte, direction: Bit, signed: Bit, word_instr: Bit] +``` +) +In other words, 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. + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `SHIFT` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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 two lookup operations +- $#`HWSL[x: Half, y: B4]` := (#`x` #`<<` #`y`) mod 2^16$ (short for "HalfWord Shift Left"), and +- $#`HWSLC[x: Half, y: B4]` := #`x` #`>>` (16-#`y`)$ (short for "HalfWord Shift Left's Carry") +Note here that one can use these two lookups to compute `out: Half[4] := in << y` as: +$ + #`out[`i#`]` = cases( + #`HWSL[in[`0#`], y]` &"if" i = 0, + #`HWSL[in[`i#`], y] | HWSLC[in[`i-1#`], y]` &"if" i in [1, 3] + ) +$ +as long as $#`y` < 16$. +Observing that +$#`HWSL[x,` 16-#`y]` = (#`x` #`<<` (16-#`y`)) mod 2^16$, and +$#`HWSLC[x,` 16-#`y]` = #`x` #`>>` #`y`$ for $#`y` in [1, 15]$, +one can also use these lookups to compute `out := in >> y` as +$ + #`out[`i#`]` = cases( + #`HWSLC[in[`i#`],` 16-#`y] | HWSL[in[`i+1#`], y]` &"if" i in [0, 2], + #`HWSLC[in[`3#`],` 16-#`y]` &"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]`$ and $#`Y[`i#`] := HWSLC[in[`i#`], bit_shift]`$ 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") diff --git a/spec/src/shift.toml b/spec/src/shift.toml new file mode 100644 index 000000000..e2ddfa12b --- /dev/null +++ b/spec/src/shift.toml @@ -0,0 +1,283 @@ +name = "SHIFT" + +# Input + +[[variables.input]] +name = "in" +type = "DWordHL" +desc = "The value being shifted" + +[[variables.input]] +name = "shift" +type = "Byte" +desc = "Number of bits to shift `in` by." + +[[variables.input]] +name = "direction" +type = "Bit" +desc = "Whether to shift left (0) or right (1)." + +[[variables.input]] +name = "signed" +type = "Bit" +desc = "Whether to interpret `in` as a signed integer." + +[[variables.input]] +name = "word_instr" +type = "Bit" +desc = "Whether this is a Word-instruction (1) or not (0)." + + +# Output + +[[variables.output]] +name = "out" +type = "DWordWL" +desc = "$#`in <>/>>>` (#`shift` mod 32 dot (2 - #`word_instr`))$" + +# Auxiliary + +[[variables.auxiliary]] +name = "is_negative" +type = "Bit" +desc = "Whether `in` is negative" + +[[variables.auxiliary]] +name = "bit_shift" +type = "Byte" +desc = "Value by which to shift `in` to obtain `X` and `Y`" + +[[variables.auxiliary]] +name = "zbs" +type = "Bit" +desc = "Whether `bit_shift` is zero (1) or not (0)." + +[[variables.auxiliary]] +name = "X" +type = ["Half", 5] +desc = "scratch variable." + +[[variables.auxiliary]] +name = "Y" +type = ["Half", 4] +desc = "scratch variable." + +[[variables.auxiliary]] +name = "limb_shift" +type = ["Bit", 4] +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." + +# Virtual + +[[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=[ + {range=0, poly=["idx", "X", 0]}, + {range=[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", range=[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", range=[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", ["-", 3, "i"]], 3, ["idx", "limb_shift", "j"]]]]]]} + +# Multiplicities + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "" + + + +# Assumptions + +[[assumptions]] +desc = "`IS_HALFWORD[in[i]]`" +range = ["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], "shift"], 0x0F] +output = "bit_shift" +ref = "shift:c:bit_shift_if_right" +multiplicity = "right" + +[[constraints.bit_shift]] +kind = "template" +tag = "IsZero" +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 = ["idx", "X", "i"] +range = ["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"]]] +range = ["i", 0, 3] +ref = "shift:c:zbs_implies_X" + +[[constraints.intra_limb_shift]] +kind = "interaction" +tag = "HWSL" +input = ["extension", "bit_shift"] +output = ["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" + +[[constraints.intra_limb_shift]] +kind = "interaction" +tag = "HWSLC" +input = [["idx", "in", "i"], "bit_shift"] +output = ["idx", "Y", "i"] +range = ["i", 0, 3] +ref = "shift:c:hwslc_if_not_zero" +multiplicity = ["not", "zbs"] + +[[constraints.intra_limb_shift]] +kind = "arith" +constraint = "$#`zbs` => #`Y[i]` = #`in[i]` dot #`right`$" +poly = ["*", "zbs", ["-", ["idx", "Y", "i"], ["*", ["idx", "in", "i"], "right"]]] +range = ["i", 0, 3] +ref = "shift:c:zbs_implies_Y" + + +[[constraint_groups]] +name = "limb_shifting" + +[[constraints.limb_shifting]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "limb_shift", "i"]] +range = ["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"]] +range = ["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" From 795a7220aca598ba70fa085f4ef5b43e90f51ee1 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:17:05 +0100 Subject: [PATCH 23/78] spec: `ADD` template (#97) * spec: ADD draft * spec: ADD: fix `carry` size * spec: ADD: clarify sum is mod 2^64 * spec: introduce `SUB` template notation. * Fix assumption indices Co-authored-by: Robin Jadoul * Fix typos Co-authored-by: Robin Jadoul --------- Co-authored-by: Robin Jadoul --- spec/add.typ | 48 +++++++++++++++++++++++++++++++++++ spec/book.typ | 1 + spec/src/add.toml | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 spec/add.typ create mode 100644 spec/src/add.toml diff --git a/spec/add.typ b/spec/add.typ new file mode 100644 index 000000000..0dade7b01 --- /dev/null +++ b/spec/add.typ @@ -0,0 +1,48 @@ +#import "/book.typ": book-page, et +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table + +#show: book-page.with(title: "ADD/SUB") + +#let config = load_config() +#let chip = load_chip("src/add.toml", config) + +#let add = raw(chip.name) + +#let highlighted_code(code) = { + box( + inset: (left: 4pt, right: 4pt), + outset: (top: 4pt, bottom: 4pt), + radius: 2pt, + fill: luma(230), + raw(code)) +} + += #add template +#add is a constraint template that is used to assert that $#`sum` = #`lhs` + #`rhs` mod 2^64$, under the condition that `cond` is non-zero. + +== Notation +The #add constraint template has the following interface: +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => ADD")) +where `cond` is any value described by an expression _of degree at most $1$_. +#highlighted_code("ADD") can be used to denote the _unconditional_ application of the #add template to `lhs`, `rhs`, and `sum`. + +#let sub = raw("SUB") +=== #sub +For ease of notation, we moreover introduce the #sub constraint template. +Its interface +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => SUB")) +maps onto the #add template as +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => ADD")) +It constrains that $#`diff` = #`lhs` - #`rhs` mod 2^64$ when the expression `cond` is non-zero. +As with #add, #highlighted_code("SUB") can be used to denote the _unconditional_ application of the template. + +== Variables +#render_chip_column_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/book.typ b/spec/book.typ index 6652ed646..3e001e1e0 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -7,6 +7,7 @@ summary: [ #chapter("variables.typ")[Variables] #chapter("is_bit.typ")[IS_BIT template] + #chapter("add.typ")[ADD template] #chapter("cpu.typ")[CPU chip] #chapter("shift.typ")[SHIFT chip] #chapter("branch.typ")[BRANCH] diff --git a/spec/src/add.toml b/spec/src/add.toml new file mode 100644 index 000000000..a0ccf6942 --- /dev/null +++ b/spec/src/add.toml @@ -0,0 +1,64 @@ +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=[ + {range=0, poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "lhs", 0], ["idx", "rhs", 0]], ["idx", "sum", 0]]]}, + {range=1, poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "lhs", 1], ["idx", "rhs", 1], ["idx", "carry", 0]], ["idx", "sum", 1]]]}, +]} + + +# Assumptions + +[[assumptions]] +desc = "`IS_WORD[lhs[i]]`" +range = ["i", 0, 1] +ref = "add:a:lhs" + +[[assumptions]] +desc = "`IS_WORD[rhs[i]]`" +range = ["i", 0, 1] +ref = "add:a:rhs" + +[[assumptions]] +desc = "`IS_WORD[sum[i]]`" +range = ["i", 0, 1] +ref = "add:a:sum" + +# Constraints + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "template" +tag = "IS_BIT" +input = [["idx", "carry", "i"]] +range = ["i", 0, 1] +cond = "cond" +ref = "add:c:carry" + From 1ba94213b6bf11f0245438b06f4f282717fe380f Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:21:50 +0100 Subject: [PATCH 24/78] spec: have column table subheaders repeat on page wrap (#121) --- spec/chip.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/chip.typ b/spec/chip.typ index 6914aa1f6..84a575c92 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -90,7 +90,7 @@ table.header([*Label*], [*Type*], table.cell(colspan: 2, [*Description*])), table.hline(stroke: stroke(thickness: 2pt)), ..for (cat, vars) in chip.variables.pairs() { - (table.cell(colspan: 4, emph(cat)), table.hline(stroke: .6pt)) + (table.header(level:2, table.cell(colspan: 4, emph(cat))), table.hline(stroke: .6pt)) for var in vars { ( [#raw(var.name)], From 62fc94b5904e6fe73d6c8912c283523bb10cea77 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:24:20 +0100 Subject: [PATCH 25/78] spec: drop `dot` when multiplying constant with one-letter variable. (#120) --- spec/expr.typ | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spec/expr.typ b/spec/expr.typ index 547f2cad2..745d95d23 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -142,7 +142,15 @@ pp <= PREC.sub ) }, - "*": (pp, rec, e) => mwrap($#e.slice(1).map(rec.with(PREC.mul)).join($dot$)$, pp < PREC.mul), + "*": (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 and type(e.at(2)) == int, message: "Can only exponentiate constants") From bf9662fe8e1bed0e49a4713ab8a3b58b09195611 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:08:13 +0100 Subject: [PATCH 26/78] spec: `MUL` chip (#122) * spec: support "sum" expression * spec: introduce "QuadHL" type * spec: introduce MUL chip * spec: Introduce QuadWL * spec: introduce B20[4] * spec: simplify MUL to 26 columns * spec: Fix expr-sum bug * spec: simplify MUL to 22 columns * spec: improve MUL readability * spec: MUL: fix indexing * spec: MUL: refactor * spec: drop B20 * spec: MUL: fix raw_product relation * spec: MUL: fix IS_B19 check range Co-authored-by: Robin Jadoul * spec: MUL: add missing res range check assumption * spec: MUL: remove superfluous/invalid constraints * spec: MUL: leverage SIGN template * spec: MUL: fix index mistake * spec: MUL: update description * spec: permit non-constant exponents * spec: MUL: drop `limb_product` * spec: MUL: minor tweaks * spec: MUL: bump headers * spec: MUL: update description * spec: MUL: update to IS_B20 * spec: MUL: remove 'eloquent' * Apply suggestions from code review Thanks Robin! Co-authored-by: Robin Jadoul * spec: MUL: define padding --------- Co-authored-by: Robin Jadoul --- spec/book.typ | 1 + spec/expr.typ | 4 +- spec/mul.typ | 94 +++++++++++++++++++++++ spec/src/config.toml | 31 ++++++++ spec/src/mul.toml | 179 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 spec/mul.typ create mode 100644 spec/src/mul.toml diff --git a/spec/book.typ b/spec/book.typ index 3e001e1e0..01a362879 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -12,6 +12,7 @@ #chapter("shift.typ")[SHIFT chip] #chapter("branch.typ")[BRANCH] #chapter("lt.typ")[LT] + #chapter("mul.typ")[MUL chip] ] ) diff --git a/spec/expr.typ b/spec/expr.typ index 745d95d23..1044001e8 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -153,8 +153,8 @@ }, "/": (pp, rec, e) => $#rec(PREC.div, e.at(1)) / #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") - $#e.at(1)^#e.at(2)$ + 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))$, diff --git a/spec/mul.typ b/spec/mul.typ new file mode 100644 index 000000000..92fafe26b --- /dev/null +++ b/spec/mul.typ @@ -0,0 +1,94 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_assumptions, +) + +#let config = load_config() +#let chip = load_chip("src/mul.toml", config) + +#show: book-page.with(title: "MUL chip") + +#let mul = raw(chip.name) + += #mul chip + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `MUL` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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 @mul:a:res 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:a:res 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; `carry` is 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") \ No newline at end of file diff --git a/spec/src/config.toml b/spec/src/config.toml index b66639e2a..389e4b16a 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -27,6 +27,11 @@ label = "Half" subtypes = ["BaseField"] desc = "Variable that can only assume values in the range $[0, 2^16)$." +[[variables.types]] +label = "B20" +subtypes = ["BaseField"] +desc = "Variable that can only assume values in the range $[0, 2^20)$." + [[variables.types]] label = "Word" subtypes = ["BaseField"] @@ -48,6 +53,16 @@ desc = """\ Represented as an array of four `Byte` variables.\ """ +[[variables.types]] +label = "B35" +subtypes = ["BaseField"] +desc = "Variable that can only assume values in the range $[0, 2^35)$." + +[[variables.types]] +label = "B51" +subtypes = ["BaseField"] +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"] @@ -81,6 +96,22 @@ desc = """\ The `Word` is the *least* 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 = "DWordWHH" subtypes = ["Half", "Half", "Word"] diff --git a/spec/src/mul.toml b/spec/src/mul.toml new file mode 100644 index 000000000..bf9ffc276 --- /dev/null +++ b/spec/src/mul.toml @@ -0,0 +1,179 @@ +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 = "res" +type = "QuadWL" +desc = "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=[ + {range=[0, 3], poly=["idx", "lhs", "i"]}, + {range=[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=[ + {range=[0, 3], poly=["idx", "rhs", "i"]}, + {range=[4, 7], poly=["*", 0xFFFF, "rhs_is_negative"]}, +]} + +[[variables.virtual]] +name = "carry" +type = ["B20", 4] +desc = "carry values" +def = {idx="i", polys=[ + {range=0, poly=["*", ["^", 2, -32], ["-", ["idx", "raw_product", 0], ["idx", "res", 0]]]}, + {range=[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]]`" +range = ["i", 0, 3] + +[[assumptions]] +desc = "`IS_HALF[rhs[i]]`" +range = ["i", 0, 3] + +[[assumptions]] +desc = "`IS_WORD[res[i]]`" +range = ["i", 0, 3] +ref = "mul:a:res" + + +# 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_B20" +input = [["idx", "carry", "i"]] +range = ["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"]] +range = ["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 = ["idx", "res", "0:4"] +multiplicity = ["-", "μ_lo"] +ref = "mul:c:lookup_lo" + +[[constraints.lookup]] +kind = "interaction" +tag = "MUL" +input = ["lhs", "lhs_signed", "rhs", "rhs_signed", "1"] +output = ["idx", "res", "4:8"] +multiplicity = ["-", "μ_hi"] +ref = "mul:c:lookup_hi" \ No newline at end of file From 11a0c64e45a3f090ac22b76c1a1abfba8ed98154 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 13 Jan 2026 16:30:07 +0100 Subject: [PATCH 27/78] spec: Add support for specifying padding values of columns (#133) Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/branch.typ | 6 ++++++ spec/chip.typ | 26 ++++++++++++++++++++++++++ spec/lt.typ | 11 +++++++++-- spec/mul.typ | 9 ++++++++- spec/shift.typ | 7 +++++++ spec/src/branch.toml | 8 ++++++++ spec/src/config.toml | 1 + spec/src/lt.toml | 8 ++++++++ spec/src/shift.toml | 13 +++++++++++++ 9 files changed, 86 insertions(+), 3 deletions(-) diff --git a/spec/branch.typ b/spec/branch.typ index d01e9fa03..a18c252b7 100644 --- a/spec/branch.typ +++ b/spec/branch.typ @@ -6,6 +6,7 @@ total_nr_variables, total_nr_instantiated_columns, render_constraint_table, + render_chip_padding_table, ) #let config = load_config() @@ -36,3 +37,8 @@ The range checks on `unmasked_low_byte` and `next_pc_low[0]` are performed impli 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 index 84a575c92..ab709c404 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -24,6 +24,32 @@ .sum() } +#let render_chip_padding_table(chip, config) = { + // Whether `var` is a preprocessed variable. + let is_preprocessed(var) = { + config.variables.types + .filter(t => t.label == var.type) + .all(t => t.at("preprocessed", default: false)) + } + + let instantiated_vars = config.variables.categories.instantiated.map(c => chip.variables.at(c)).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)],) + } + }, + ), caption: [Overview of padding values for #chip.name chip.]) +} + /// Generates a table listing `chip`'s columns. #let render_chip_column_table(chip, config) = { diff --git a/spec/lt.typ b/spec/lt.typ index ff3b6dae3..3b57a62e3 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -3,9 +3,10 @@ #import "/chip.typ": ( render_chip_assumptions, render_chip_column_table, - total_nr_variables, - total_nr_instantiated_columns, + render_chip_padding_table, render_constraint_table, + total_nr_instantiated_columns, + total_nr_variables, ) #let config = load_config() @@ -77,3 +78,9 @@ And then we constrain the subtraction. 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/mul.typ b/spec/mul.typ index 92fafe26b..1892994f0 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -6,6 +6,7 @@ total_nr_instantiated_columns, render_constraint_table, render_chip_assumptions, + render_chip_padding_table, ) #let config = load_config() @@ -91,4 +92,10 @@ We constrain `lhs_is_negative` and `rhs_is_negative` according to their definiti === Lookup The #mul chip contributes the following to the lookup: -#render_constraint_table(chip, config, groups: "lookup") \ No newline at end of file +#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) diff --git a/spec/shift.typ b/spec/shift.typ index 3555d64e4..70aebc97c 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -6,6 +6,7 @@ total_nr_instantiated_columns, render_constraint_table, render_chip_assumptions, + render_chip_padding_table, ) #let config = load_config() @@ -173,3 +174,9 @@ 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/src/branch.toml b/spec/src/branch.toml index b93639602..d6620dcfd 100644 --- a/spec/src/branch.toml +++ b/spec/src/branch.toml @@ -7,21 +7,25 @@ name = "BRANCH" name = "pc" type = "DWordWL" desc = "The current pc, used as base address when `!JALR`" +pad = 0 [[variables.input]] name = "offset" type = "Word" 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 @@ -30,11 +34,13 @@ desc = "Selects between `pc` and `register` as base address, needed for the `JAL name = "next_pc_high" type = ["Half", 3] desc = "The upper part of the next pc" +pad = 0 # TODO(#128): improve handling for arrays [[variables.output]] name = "next_pc_low" type = ["Byte", 2] desc = "The lower part of the next pc" +pad = 0 # Auxiliary @@ -43,6 +49,7 @@ desc = "The lower part of the next pc" 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 @@ -72,6 +79,7 @@ def = {idx = "i", polys = [ name = "μ" type = "Bit" desc = "" +pad = 0 [[assumptions]] diff --git a/spec/src/config.toml b/spec/src/config.toml index 389e4b16a..68f1683de 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -125,6 +125,7 @@ desc = """\ 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, see there for more details about the magic number." +preprocessed = true [variables.categories] all = ["input", "output", "auxiliary", "virtual", "multiplicity", "condition"] diff --git a/spec/src/lt.toml b/spec/src/lt.toml index 1a441c2b3..0ee06abc9 100644 --- a/spec/src/lt.toml +++ b/spec/src/lt.toml @@ -7,16 +7,19 @@ name = "LT" 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 @@ -24,6 +27,7 @@ desc = "whether to interpret `lhs` and `rhs` as signed integers (1) or not (0)" name = "lt" type = "Bit" desc = "Whether $#`lhs` < #`rhs`$, taking `signed` into account" +pad = 0 # Auxiliary @@ -32,16 +36,19 @@ desc = "Whether $#`lhs` < #`rhs`$, taking `signed` into account" 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 @@ -67,6 +74,7 @@ def = ["idx", "carry", 1] name = "μ" type = "Bit" desc = "" +pad = 0 [[assumptions]] diff --git a/spec/src/shift.toml b/spec/src/shift.toml index e2ddfa12b..ad6172af8 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -6,26 +6,31 @@ name = "SHIFT" 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 @@ -34,6 +39,7 @@ desc = "Whether this is a Word-instruction (1) or not (0)." name = "out" type = "DWordWL" desc = "$#`in <>/>>>` (#`shift` mod 32 dot (2 - #`word_instr`))$" +pad = 0 # Auxiliary @@ -41,31 +47,37 @@ desc = "$#`in <>/>>>` (#`shift` mod 32 dot (2 - #`word_instr`))$" 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 = 0 # TODO: array [[variables.auxiliary]] name = "Y" type = ["Half", 4] desc = "scratch variable." +pad = 0 # TODO: array [[variables.auxiliary]] name = "limb_shift" type = ["Bit", 4] 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." +pad = 0 # TODO: array # Virtual @@ -114,6 +126,7 @@ def = {idx="i", range=[0, 3], poly=["+", ["*", "left", ["sum", ["=", "j", 0], "i name = "μ" type = "Bit" desc = "" +pad = 0 From eb3297a6d34e82bee9c8d4c862ffca23f10af1b4 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 13 Jan 2026 16:34:38 +0100 Subject: [PATCH 28/78] spec: update range specifications to iters concept (#130) Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/chip.typ | 88 ++++++++++++++++++++++++++++++-------------- spec/expr.typ | 14 ++++++- spec/src/add.toml | 14 +++---- spec/src/branch.toml | 14 +++---- spec/src/cpu.toml | 14 +++---- spec/src/lt.toml | 12 +++--- spec/src/shift.toml | 22 +++++------ 7 files changed, 111 insertions(+), 67 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index ab709c404..4e7d6a143 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -24,6 +24,31 @@ .sum() } +// 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) = { @@ -53,19 +78,17 @@ /// Generates a table listing `chip`'s columns. #let render_chip_column_table(chip, config) = { - // Render a definition's range - let render_def_range(idx, range) = { - if type(range) == array { - if range.len() == 1 { - [#raw(idx) `=` #range.at(0)] - } else if range.len() == 2 { - [#raw(idx) #sym.in `[`#range.at(0)`,`#range.at(1)`]`] + // 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 range: " + repr(range) + repr(range.len())) + assert(false, message: "Invalid def range: " + repr(name, ..args)) } - } else { - [#raw(idx) `=` #range] - } + }).join("\n") } // Render definition `def` @@ -80,25 +103,38 @@ 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((":=", ("idx", var_name, def.idx), def.poly)), - render_def_range(def.idx, def.range) + 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(("idx", var_name, def.idx))) + 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_range(def.idx, poly.range), + render_def_iters(iters_of(poly, name: idx)), ) } } else { @@ -140,11 +176,9 @@ } } -// Render a range if `obj` contains one. -#let interval(obj) = { - if "range" in obj { - [#raw(obj.range.at(0)) #sym.in` [`#obj.range.at(1)`,`#obj.range.at(2)`]`] - } else { return [] } +// 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) = { @@ -157,9 +191,9 @@ #let render_chip_assumptions(chip, config) = { let tag(assumption) = { - let index = if "range" in assumption { "." + assumption.range.at(0) } else { "" } + 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 it.counter.display()#index]) + 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: [], [])] } @@ -171,7 +205,7 @@ table.header([*Tag*], [*Range*], [*Description*]), table.hline(stroke: stroke(thickness: 2pt)), ..for assumption in chip.assumptions { - ([#tag(assumption)], [#interval(assumption)], [#eval(assumption.desc, mode: "markup")]) + ([#tag(assumption)], [#iters(assumption)], [#eval(assumption.desc, mode: "markup")]) }, ), caption: [Assumption overview of #chip.name chip.]) } @@ -193,10 +227,10 @@ /// Render the contraint's tag. let tag(constraint, group) = { - let index = if "range" in constraint { "." + constraint.range.at(0) } else { "" } + 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 it.counter.display()#index]) + 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: [], [])] } @@ -249,7 +283,7 @@ // 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 => "range" in x) + 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 @@ -272,7 +306,7 @@ for constraint in group_constraints { ( [#tag(constraint, lookup_group(group))], - [#interval(constraint)], + [#iters(constraint)], [#repr_constraint(constraint)], [#expr_to_math(constraint.at("multiplicity", default: ""))], ) diff --git a/spec/expr.typ b/spec/expr.typ index 1044001e8..bf705b462 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -129,10 +129,22 @@ } } +#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( ( - "idx": (pp, rec, e) => $#rec(PREC.idx, e.at(1))_(#rec(PREC.idx, e.at(2)))$, + "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($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) => { diff --git a/spec/src/add.toml b/spec/src/add.toml index a0ccf6942..c928a8b32 100644 --- a/spec/src/add.toml +++ b/spec/src/add.toml @@ -27,26 +27,25 @@ name = "carry" desc = "Carry values used to constrain the addition" type = ["Bit", 2] def = {idx="i", polys=[ - {range=0, poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "lhs", 0], ["idx", "rhs", 0]], ["idx", "sum", 0]]]}, - {range=1, poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "lhs", 1], ["idx", "rhs", 1], ["idx", "carry", 0]], ["idx", "sum", 1]]]}, + {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]]`" -range = ["i", 0, 1] +iter = ["i", 0, 1] ref = "add:a:lhs" [[assumptions]] desc = "`IS_WORD[rhs[i]]`" -range = ["i", 0, 1] +iter = ["i", 0, 1] ref = "add:a:rhs" [[assumptions]] desc = "`IS_WORD[sum[i]]`" -range = ["i", 0, 1] +iter = ["i", 0, 1] ref = "add:a:sum" # Constraints @@ -58,7 +57,6 @@ name = "all" kind = "template" tag = "IS_BIT" input = [["idx", "carry", "i"]] -range = ["i", 0, 1] +iter = ["i", 0, 1] cond = "cond" ref = "add:c:carry" - diff --git a/spec/src/branch.toml b/spec/src/branch.toml index d6620dcfd..e66974c8e 100644 --- a/spec/src/branch.toml +++ b/spec/src/branch.toml @@ -59,8 +59,8 @@ 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 = [ - {range = [0], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], ["idx", "unmasked_low_byte", 0]]}, - {range = [1], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 2]], ["idx", "next_pc_high", 1]]}, + {iter = 0, poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], ["idx", "unmasked_low_byte", 0]]}, + {iter = 1, poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 2]], ["idx", "next_pc_high", 1]]}, ]} [[variables.virtual]] @@ -68,8 +68,8 @@ name = "next_pc" type = "DWordWL" desc = "The computed next pc, after masking off the LSB as required by the ISA." def = {idx = "i", polys = [ - {range = [0], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 0]], ["*", ["^", 2, 8], ["idx", "next_pc_low", 1]], ["idx", "next_pc_low", 0]]}, - {range = [1], poly = ["+", ["*", ["^", 2, 16], ["idx", "next_pc_high", 2]], ["idx", "next_pc_high", 1]]}, + {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]]}, ]} @@ -84,14 +84,14 @@ pad = 0 [[assumptions]] desc = "`pc` is range checked, `IS_WORD[pc[i]]`" -range = ["i", 0, 1] +iter = ["i", 0, 1] [[assumptions]] desc = "`offset` is range checked, `IS_WORD[offset]`" [[assumptions]] desc = "`register` is range checked, `IS_WORD[register[i]]`" -range = ["i", 0, 1] +iter = ["i", 0, 1] [[assumptions]] desc = "`IS_BIT`" @@ -132,7 +132,7 @@ multiplicity = "μ" kind = "interaction" tag = "IS_HALFWORD" input = [["idx", "next_pc_high", "i"]] -range = ["i", 0, 2] +iter = ["i", 0, 2] multiplicity = "μ" diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 562a657d0..747497d44 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -456,19 +456,19 @@ input = ["rd"] kind = "interaction" tag = "IS_BYTE" input = [["idx", "arg1", "i"]] -range = ["i", 0, 7] +iter = ["i", 0, 7] [[constraints.range]] kind = "interaction" tag = "IS_BYTE" input = [["idx", "arg2", "i"]] -range = ["i", 0, 7] +iter = ["i", 0, 7] [[constraints.range]] kind = "interaction" tag = "IS_BYTE" input = [["idx", "res", "i"]] -range = ["i", 0, 7] +iter = ["i", 0, 7] [[constraint_groups]] @@ -501,7 +501,7 @@ multiplicity = ["+", "SLT", "BLT"] kind = "arith" constraint = "$#`SLT` + #`BLT` => #`res[i]` = 0$" poly = ["*", ["+", "SLT", "BLT"], ["idx", "res", "i"]] -range = ["i", 1, 7] +iter = ["i", 1, 7] [[constraints.alu]] kind = "interaction" @@ -509,7 +509,7 @@ tag = "AND_BYTE" input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] output = ["idx", "res", "i"] multiplicity = "AND" -range = ["i", 0, 7] +iter = ["i", 0, 7] [[constraints.alu]] kind = "interaction" @@ -517,7 +517,7 @@ tag = "OR_BYTE" input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] output = ["idx", "res", "i"] multiplicity = "OR" -range = ["i", 0, 7] +iter = ["i", 0, 7] [[constraints.alu]] kind = "interaction" @@ -525,7 +525,7 @@ tag = "XOR_BYTE" input = [["idx", "arg1", "i"], ["idx", "arg2", "i"]] output = ["idx", "res", "i"] multiplicity = "XOR" -range = ["i", 0, 7] +iter = ["i", 0, 7] [[constraints.alu]] kind = "interaction" diff --git a/spec/src/lt.toml b/spec/src/lt.toml index 0ee06abc9..3836cdd13 100644 --- a/spec/src/lt.toml +++ b/spec/src/lt.toml @@ -57,8 +57,8 @@ name = "carry" type = ["Bit", 2] desc = "The carry for adding `lhs_sub_rhs` back to `rhs`" def = {idx = "i", polys = [ - {range = [0], poly = ["*", ["^", 2, -32], ["-", ["+", ["idx", "rhs", 0], ["idx", ["cast", "lhs_sub_rhs", "DWordWL"], 0]], ["idx", "lhs", 0]]]}, - {range = [1], poly = ["*", ["^", 2, -32], ["-", ["+", ["idx", ["cast", "rhs", "DWordWL"], 1], ["idx", ["cast", "lhs_sub_rhs", "DWordWL"], 1], ["idx", "carry", 0]], ["idx", ["cast", "lhs", "DWordWL"], 1]]]}, + {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]] @@ -79,12 +79,12 @@ pad = 0 [[assumptions]] desc = "`IS_HALFWORD[lhs[i]]` and `IS_WORD[lhs[0]]`" -range = ["i", 1, 2] +iter = ["i", 1, 2] ref = "lt:a:range_lhs" [[assumptions]] desc = "`IS_HALFWORD[rhs[i]]` and `IS_WORD[rhs[0]]`" -range = ["i", 1, 2] +iter = ["i", 1, 2] ref = "lt:a:range_rhs" [[assumptions]] @@ -128,13 +128,13 @@ desc = "Constrain the subtraction" kind = "template" tag = "IS_BIT" input = [["idx", "carry", "i"]] -range = ["i", 0, 1] +iter = ["i", 0, 1] [[constraints.sub]] kind = "interaction" tag = "IS_HALFWORD" input = [["idx", "lhs_sub_rhs", "i"]] -range = ["i", 0, 3] +iter = ["i", 0, 3] multiplicity = "μ" ref = "lt:c:lhs_sub_rhs_range" diff --git a/spec/src/shift.toml b/spec/src/shift.toml index ad6172af8..4b7044e7d 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -104,21 +104,21 @@ name = "intra_limb_left" type = "DWordHL" desc = "`in << (shift % 16)` if `left`" def = {idx="i", polys=[ - {range=0, poly=["idx", "X", 0]}, - {range=[1, 3], poly=["+", ["idx", "X", "i"], ["idx", "Y", ["-", "i", 1]]]}, + {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", range=[0, 3], poly=["+", ["idx", "Y", "i"], ["idx", "X", ["+", "i", 1]]]} +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", range=[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", ["-", 3, "i"]], 3, ["idx", "limb_shift", "j"]]]]]]} +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", ["-", 3, "i"]], 3, ["idx", "limb_shift", "j"]]]]]]} # Multiplicities @@ -134,7 +134,7 @@ pad = 0 [[assumptions]] desc = "`IS_HALFWORD[in[i]]`" -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "shift:a:range_in" [[assumptions]] @@ -214,7 +214,7 @@ kind = "interaction" tag = "HWSL" input = [["idx", "in", "i"], "bit_shift"] output = ["idx", "X", "i"] -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "shift:c:hwsl_if_not_zero" multiplicity = ["not", "zbs"] @@ -222,7 +222,7 @@ multiplicity = ["not", "zbs"] kind = "arith" constraint = "$#`zbs` => #`X[i]` = #`in[i]` dot #`left`$" poly = ["*", "zbs", ["-", ["idx", "X", "i"], ["*", ["idx", "in", "i"], "left"]]] -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "shift:c:zbs_implies_X" [[constraints.intra_limb_shift]] @@ -244,7 +244,7 @@ kind = "interaction" tag = "HWSLC" input = [["idx", "in", "i"], "bit_shift"] output = ["idx", "Y", "i"] -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "shift:c:hwslc_if_not_zero" multiplicity = ["not", "zbs"] @@ -252,7 +252,7 @@ multiplicity = ["not", "zbs"] kind = "arith" constraint = "$#`zbs` => #`Y[i]` = #`in[i]` dot #`right`$" poly = ["*", "zbs", ["-", ["idx", "Y", "i"], ["*", ["idx", "in", "i"], "right"]]] -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "shift:c:zbs_implies_Y" @@ -263,7 +263,7 @@ name = "limb_shifting" kind = "template" tag = "IS_BIT" input = [["idx", "limb_shift", "i"]] -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "shift:c:limb_shift_is_bit" [[constraints.limb_shifting]] @@ -278,7 +278,7 @@ multiplicity = "μ" kind = "arith" constraint = "$#`out[:2]` = #`shifted[:4]`$" poly = ["-", ["idx", "out", "i"], ["idx", ["cast", "shifted", "DWordWL"], "i"]] -range = ["i", 0, 1] +iter = ["i", 0, 1] ref = "shift:c:out_eq_shifted" From 22fa781b300246c7c98117227e54ebe5e5c88e50 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:12:24 +0100 Subject: [PATCH 29/78] spec: `BITWISE` chip (#138) * spec: introduce BITWISE * spec: BITWISE: outline optimizations * spec: BITWISE: fix SLL naming mismatch * spec: BITWISE: fix length computation mistake * spec: drop `dot` in `expr_to_code` when multiplying constant with single-letter variable --- spec/bitwise.typ | 44 ++++++++++ spec/book.typ | 1 + spec/expr.typ | 10 ++- spec/src/bitwise.toml | 200 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 spec/bitwise.typ create mode 100644 spec/src/bitwise.toml diff --git a/spec/bitwise.typ b/spec/bitwise.typ new file mode 100644 index 000000000..34ec6dd10 --- /dev/null +++ b/spec/bitwise.typ @@ -0,0 +1,44 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_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.with(title: "BRANCH chip") + += #bitwise chip + +== Columns +#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_column_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) + +== Areas of Optimization +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`, `ZERO`, etc.) and 20-bit (`HWSL`, `HWSLC`, `IS_B20`) lookups in separate tables. ++ Combine `HWSL` and `HWSLC` into a single lookup (see also \#119). diff --git a/spec/book.typ b/spec/book.typ index 01a362879..1bf944862 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -13,6 +13,7 @@ #chapter("branch.typ")[BRANCH] #chapter("lt.typ")[LT] #chapter("mul.typ")[MUL chip] + #chapter("bitwise.typ")[BITWISE] ] ) diff --git a/spec/expr.typ b/spec/expr.typ index bf705b462..751493619 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -95,7 +95,15 @@ "not": (pp, rec, e) => cwrap(`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) => cwrap(e.slice(1).map(rec.with(PREC.mul)).join(` ` + sym.dot + ` `), pp < PREC.mul), + "*": (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") diff --git a/spec/src/bitwise.toml b/spec/src/bitwise.toml new file mode 100644 index 000000000..2eeec4059 --- /dev/null +++ b/spec/src/bitwise.toml @@ -0,0 +1,200 @@ +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 and #`Y` = 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 = "" + +[[variables.multiplicity]] +name = "μ_HWSLC" +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"]]] +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 = "SLL" +multiplicity = ["-", "μ_HWSL"] + +[[constraints.contributions]] +kind = "interaction" +tag = "HWSLC" +input = [["+", "X", ["*", 256, "Y"]], "Z"] +output = "SLLC" +multiplicity = ["-", "μ_HWSLC"] \ No newline at end of file From 52d152243b440ca7448224ef1ebbdaa0a7f04dea Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Thu, 15 Jan 2026 14:36:41 +0100 Subject: [PATCH 30/78] spec: Initial inefficient MEMW chip (#104) Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/book.typ | 1 + spec/chip.typ | 1 + spec/memw.typ | 59 ++++++++++ spec/src/cpu.toml | 6 +- spec/src/memw.toml | 288 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 spec/memw.typ create mode 100644 spec/src/memw.toml diff --git a/spec/book.typ b/spec/book.typ index 1bf944862..841ffb133 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -11,6 +11,7 @@ #chapter("cpu.typ")[CPU chip] #chapter("shift.typ")[SHIFT chip] #chapter("branch.typ")[BRANCH] + #chapter("memw.typ")[MEMW] #chapter("lt.typ")[LT] #chapter("mul.typ")[MUL chip] #chapter("bitwise.typ")[BITWISE] diff --git a/spec/chip.typ b/spec/chip.typ index 4e7d6a143..b24153098 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -113,6 +113,7 @@ } if "poly" in def { + // assert(false, message: repr(index_all(var_name, gather_indices(def)))) ( [], table.cell(align: right, emph[definition]), diff --git a/spec/memw.typ b/spec/memw.typ new file mode 100644 index 000000000..bcf6a64b0 --- /dev/null +++ b/spec/memw.typ @@ -0,0 +1,59 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, +) + +#let config = load_config() +#let chip = load_chip("src/memw.toml", config) + +#show: book-page.with(title: "MEMW chip") + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `MEMW` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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. +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 + +#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 appropriate range checking +(as long as no external entities provide negative multiplicities without range checking the timestamp). +This ensures the assumptions for `LT` are satisfied. + +We additionally check that the address does not overflow +for more significant bytes of the access. +#render_constraint_table(chip, config, groups: "overflow") + +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") + + +== Future optimization ideas + +- Fast path for aligned memory access where all bytes have the same old timestamp +- MEMB chip that deals does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) +- Compute `base_address[1] + 1` once and have high words of `address_add` as Words +- Improve overflow trapping somehow so we don't need `LT` (could tie into previous one by checking carry bit of the +1) +- Adding `μ_sum`/`w2`/`w4`/`write8` multiplicities to the `IS_HALFWORD` lookups may make some GKR things faster if there are known zeroes. diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 747497d44..97db6d6f0 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -38,17 +38,17 @@ desc = "Whether to write back to the destination register" [[variables.input]] name = "memory_2bytes" type = "Bit" -desc = "Whether the memory access (read or write) touches at least 2 bytes" +desc = "Whether the memory access (read or write) touches exactly 2 bytes" [[variables.input]] name = "memory_4bytes" type = "Bit" -desc = "Whether the memory access (read or write) touches at least 4 bytes" +desc = "Whether the memory access (read or write) touches exactly 4 bytes" [[variables.input]] name = "memory_8bytes" type = "Bit" -desc = "Whether the memory access (read or write) touches at least 8 bytes" +desc = "Whether the memory access (read or write) touches exactly 8 bytes" # TODO: Are there usecases where it's nicer to just have this as a length constant? [[variables.input]] diff --git a/spec/src/memw.toml b/spec/src/memw.toml new file mode 100644 index 000000000..9aa9cd592 --- /dev/null +++ b/spec/src/memw.toml @@ -0,0 +1,288 @@ +name = "MEMW" + +# Input + +[[variables.input]] +name = "is_register" +type = "Bit" +desc = "Whether the address represents a register index" + +[[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" + +[[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" + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "The timestamp at which this memory access is said to occur" + +[[variables.input]] +name = "write2" +type = "Bit" +desc = "Whether to write exactly 2 values" + +[[variables.input]] +name = "write4" +type = "Bit" +desc = "Whether to write exactly 4 values" + +[[variables.input]] +name = "write8" +type = "Bit" +desc = "Whether to write exactly 8 values" + +# 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""" + +# Auxiliary + +[[variables.auxiliary]] +name = "address_add" +type = ["DWordHL", 7] +desc = "`address_add[i] = base_address + i + 1`" + +[[variables.auxiliary]] +name = "old_timestamp" +type = ["DWordWL", 8] +desc = "The timestamp at which the address was last accessed" + +# 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`)" + +[[variables.multiplicity]] +name = "μ_write" +type = "Bit" +desc = "Whether we are performing a write (and hence not return `out`)" + + +[[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 = ["μ_sum"] + +[[constraints.consistency]] +kind = "arith" +constraint = "$#`w2` => #`μ_sum`$" +poly = ["*", "w2", ["not", "μ_sum"]] + +[[constraints.consistency]] +kind = "template" +tag = "ADD" +input = ["base_address", 1] +output = ["cast", ["idx", "address_add", 0], "DWordWL"] +multiplicity = "w2" + +[[constraints.consistency]] +kind = "template" +tag = "ADD" +input = ["base_address", ["+", "i", 1]] +output = ["cast", ["idx", "address_add", "i"], "DWordWL"] +iter = ["i", 1, 2] +multiplicity = "w4" + +[[constraints.consistency]] +kind = "template" +tag = "ADD" +input = ["base_address", ["+", "i", 1]] +output = ["cast", ["idx", "address_add", "i"], "DWordWL"] +iter = ["i", 3, 6] +multiplicity = "write8" + +[[constraints.consistency]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", ["idx", "address_add", "i"], "j"]] +iters = [ + ["i", 0, 6], + ["j", 0, 3], +] + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", 0], "timestamp"] +output = 1 +multiplicity = "μ_sum" + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", 1], "timestamp"] +output = 1 +multiplicity = "w2" + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", "i"], "timestamp"] +output = 1 +iter = ["i", 2, 3] +multiplicity = "w4" + +[[constraints.consistency]] +kind = "interaction" +tag = "LT" +input = [["idx", "old_timestamp", "i"], "timestamp"] +output = 1 +iter = ["i", 4, 7] +multiplicity = "write8" + + +[[constraint_groups]] +name = "overflow" +prefix = "R" + +[[constraints.overflow]] +kind = "interaction" +tag = "LT" +input = ["base_address", ["cast", ["idx", "address_add", 0], "DWordWL"]] +output = 1 +multiplicity = "write2" + +[[constraints.overflow]] +kind = "interaction" +tag = "LT" +input = ["base_address", ["cast", ["idx", "address_add", 2], "DWordWL"]] +output = 1 +multiplicity = "write4" + +[[constraints.overflow]] +kind = "interaction" +tag = "LT" +input = ["base_address", ["cast", ["idx", "address_add", 6], "DWordWL"]] +output = 1 +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" From 11cd790e747b344c58e6e3ff39513b0258b7916e Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Thu, 15 Jan 2026 15:38:39 +0100 Subject: [PATCH 31/78] spec: LOAD chip (#144) --- spec/book.typ | 1 + spec/load.typ | 42 ++++++++++++ spec/src/cpu.toml | 1 - spec/src/load.toml | 160 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 spec/load.typ create mode 100644 spec/src/load.toml diff --git a/spec/book.typ b/spec/book.typ index 841ffb133..3363c0c26 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -14,6 +14,7 @@ #chapter("memw.typ")[MEMW] #chapter("lt.typ")[LT] #chapter("mul.typ")[MUL chip] + #chapter("load.typ")[LOAD chip] #chapter("bitwise.typ")[BITWISE] ] ) diff --git a/spec/load.typ b/spec/load.typ new file mode 100644 index 000000000..931611108 --- /dev/null +++ b/spec/load.typ @@ -0,0 +1,42 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_table, + render_chip_padding_table, + render_constraint_table, + total_nr_instantiated_columns, + total_nr_variables, +) + +#let config = load_config() +#let chip = load_chip("src/load.toml", config) + +#show: book-page.with(title: "LOAD chip") + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `LOAD` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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/src/cpu.toml b/spec/src/cpu.toml index 97db6d6f0..52154751e 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -582,7 +582,6 @@ kind = "interaction" tag = "MEMW" input = [1, ["*", 2, "rd"], "rvd", ["+", "timestamp", 2], 1, 0, 0] -# TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "LOAD" diff --git a/spec/src/load.toml b/spec/src/load.toml new file mode 100644 index 000000000..fcbd2b87f --- /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"] +output = ["cast", "res", "DWordWL"] +multiplicity = ["-", "μ"] From 760f4461dfee0bd002c21f925976d3d212f243b4 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti <56092489+ColoCarletti@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:46:27 -0300 Subject: [PATCH 32/78] fix CPU-CA41 typo (#189) --- spec/src/cpu.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 52154751e..63ed2005f 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -538,7 +538,7 @@ multiplicity = "SHIFT" kind = "template" tag = "ADD" input = ["pc", ["cast", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], "DWordWL"]] -output = ["cast", "DWordWL", "res"] +output = ["cast", "res", "DWordWL"] cond = "JALR" # TODO: no types available, so no casting yet From a358eed8662b10bcd11c39bb6981b5ede3c948ce Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:51:43 +0100 Subject: [PATCH 33/78] spec: `DECODE` (#143) * spec: DECODE: decode basics * spec: DECODE: update table + add *W instructions * spec: fix padding table for chips that don't have all types of variables * spec: introduce B49 * spec: DECODE: split-off decode uncompressed * spec: DECODE: overhaul decode * Apply suggestions from code review Co-authored-by: Robin Jadoul * Apply suggestion from @RobinJadoul Co-authored-by: Robin Jadoul * Fix `ADDI` flag mistakes * spec: DECODE: make `packed_encode` a `BaseField`; remove superfluous `B49` * spec: DECODE: set `mem_xB` when reading/writing _exactly_ `x` bytes * spec: DECODE: update `mp_selector` description. * Apply suggestions from code review * spec: DECODE: merge uncompressed page into decode.typ --------- Co-authored-by: Robin Jadoul --- spec/book.typ | 1 + spec/chip.typ | 3 +- spec/decode.typ | 219 ++++++++++++++++++++++++++++++ spec/src/decode.toml | 58 ++++++++ spec/src/decode_uncompressed.toml | 157 +++++++++++++++++++++ 5 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 spec/decode.typ create mode 100644 spec/src/decode.toml create mode 100644 spec/src/decode_uncompressed.toml diff --git a/spec/book.typ b/spec/book.typ index 3363c0c26..15f90f276 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -8,6 +8,7 @@ #chapter("variables.typ")[Variables] #chapter("is_bit.typ")[IS_BIT template] #chapter("add.typ")[ADD template] + #chapter("decode.typ")[DECODE chip] #chapter("cpu.typ")[CPU chip] #chapter("shift.typ")[SHIFT chip] #chapter("branch.typ")[BRANCH] diff --git a/spec/chip.typ b/spec/chip.typ index b24153098..8e2c4ac33 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -57,7 +57,7 @@ .all(t => t.at("preprocessed", default: false)) } - let instantiated_vars = config.variables.categories.instantiated.map(c => chip.variables.at(c)).flatten() + let instantiated_vars = config.variables.categories.instantiated.map(c => chip.variables.at(c, default: ())).flatten() show figure: set block(breakable: true) figure(table( @@ -198,6 +198,7 @@ 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, diff --git a/spec/decode.typ b/spec/decode.typ new file mode 100644 index 000000000..24846d2c1 --- /dev/null +++ b/spec/decode.typ @@ -0,0 +1,219 @@ +#import "/book.typ": book-page, rj +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_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.with(title: "DECODE chip") + +#let decode = raw(chip.name) + += #decode table +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. + +== Columns +#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_column_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) + + +== 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_column_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_reg`*, *`w_instr`*, *`signed`*: whether to set the `write_register`, `word_instr` or `signed` flag, 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$. ++ 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 below. + +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, 40pt, 40pt, 40pt, 1fr, 15pt), + stroke: 0pt, + inset: (right: .5em), + align: (left, right, center, center, center, left, right), + fill: (_, y) => + if calc.odd(y) and y <= lines.len() { luma(245) } + else { white }, + table.header([*Operation*], [*op-flag*], [*`w_reg`*], [*`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_reg`*], [*`w_instr`*], [*`signed`*], [*other*]), + ), + caption: [Decoding table] + ) +} + +#let decoding = ( + // OP-IMM + ([`ADDI[W] rd, rs1, imm`], [`ADD`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), + ([`SLTI[U] rd, rs1, imm`], [`SLT`], [$#`rd` eq.not 0$], [], [#sym.not`[U]`], [], [#ref_note(, )]), + ([`ANDI rd, rs1, imm`], [`AND`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`ORI rd, rs1, imm`], [`OR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`XORI rd, rs1, imm`], [`XOR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`SLLI[W] rd, rs1, imm`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note()]), + ([`SRLI[W] rd, rs1, imm`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [`mp_selector`], [#ref_note(, )]), + ([`SRAI[W] rd, rs1, imm`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [1], [`mp_selector`], [#ref_note(, )]), + // OP + ([`ADD[W] rd, rs1, rs2`], [`ADD`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), + ([`SUB[W] rd, rs1, rs2`], [`SUB`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), + ([`SLT[U] rd, rs1, rs2`], [`SLT`], [$#`rd` eq.not 0$], [], [#sym.not`[U]`], [], [#ref_note(, )]), + ([`AND rd, rs1, rs2`], [`AND`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`OR rd, rs1, rs2`], [`OR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`XOR rd, rs1, rs2`], [`XOR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`SLL[W] rd, rs1, rs2`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), + ([`SRL[W] rd, rs1, rs2`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [`mp_selector`], [#ref_note(, )]), + ([`SRA[W] rd, rs1, rs2`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [1], [`mp_selector`], [#ref_note(, )]), + // OP - M + ([`MUL[W] rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [`[W]`], [1], [`mp_selector`], [#ref_note(, )]), + ([`MULH rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [], [1], [`mp_selector`, `muldiv_selector`], [#ref_note()]), + ([`MULHU rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [], [], [`muldiv_selector`], [#ref_note()]), + ([`MULHSU rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [], [1], [`muldiv_selector`], [#ref_note()]), + ([`DIV[U][W] rd, rs1, rs2`], [`DIVREM`], [$#`rd` eq.not 0$], [`[W]`], [#sym.not`[U]`], [], [#ref_note(, , )]), + ([`REM[U][W] rd, rs1, rs2`], [`DIVREM`], [$#`rd` eq.not 0$], [`[W]`], [#sym.not`[U]`], [`muldiv_selector`], [#ref_note(, , )]), + // LUI/AUIPC + ([`LUI rd, imm`], [`ADD`], [$#`rd` eq.not 0$], [], [], [], [#ref_note(, )]), + ([`AUIPC rd, imm`], [`ADD`], [$#`rd` eq.not 0$], [], [], [`rs1 := x255`], [#ref_note(, )]), + ([`JAL rd, imm`], [`JALR`], [$#`rd` eq.not 0$], [], [], [`rs1 := x255`], [#ref_note(, )]), + // Branching + ([`JALR rd, rs1, imm`], [`JALR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), + ([`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`], [1], [], [], [$#`rs1` := #`x17`$, $#`rs2` := #`x11`$, $#`rd` := #`x10`$], [#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_w_reg", + [`write_register`: $#`rd` eq.not 0$ indicates that $#`write_register` = 1$ when $#`rd` eq.not 0$ and $0$ otherwise.] + ) + ), + enum.item( + referenceable_note( + "note_word_instr", + [`word_instr`: `[W]` indicates that $#`word_instr` = 0$ 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:13]$ 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`. A system call can have up to 7 arguments and has 1 return value. The arguments are in registers A0-A6, in that order, and the return value is written into A0 before giving back control to the guest. A7 contains the system call number." #link("https://libriscv.no/docs/concepts/syscalls/#the-risc-v-system-call-abi")[[source]] + As such, + - syscall number in A7 (= register `x17`) + - first syscall argument in A1 (= register `x11`) + - syscall output in A0 (= register `x10`)] + ) + ), + enum.item( + referenceable_note( + "note-fence", + [`FENCE`: currently, the VM interprets this operation as `ADDI x0 x0 0`; a no-op.] + ) + ) +) diff --git a/spec/src/decode.toml b/spec/src/decode.toml new file mode 100644 index 000000000..6c01e4f6c --- /dev/null +++ b/spec/src/decode.toml @@ -0,0 +1,58 @@ +name = "DECODE" + +[[variables.output]] +name = "pc" +type = "DWordWL" +desc = "value of the program counter this instruction is associated with." +# TODO(#136): fix this when padding the CPU +pad = 1 + +[[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:7] `rs1`, \\ +[8:15] `rs2`, \\ +[16:23] `rd`, \\ +[24] `write_register`, \\ +[25] `memory_2bytes`, \\ +[26] `memory_4bytes`, \\ +[27] `memory_8bytes`, \\ +[28] `c_type`, \\ +[29] `signed`, \\ +[30] `mp_selector`, \\ +[31] `muldiv_selector`, \\ +[32] `word_instr`, \\ +[33] `ADD`, \\ +[34] `SUB`, \\ +[35] `SLT`, \\ +[36] `AND`, \\ +[37] `OR`, \\ +[38] `XOR`, \\ +[39] `SHIFT`, \\ +[40] `JALR`, \\ +[41] `BEQ`, \\ +[42] `BLT`, \\ +[43] `LOAD`, \\ +[44] `STORE`, \\ +[45] `MUL`, \\ +[46] `DIVREM`, \\ +[47] `ECALL`, \\ +[48] `EBREAK`; \\ +the remaining bits are set to zero. +""" +pad = 0 + +[[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..8457005f8 --- /dev/null +++ b/spec/src/decode_uncompressed.toml @@ -0,0 +1,157 @@ +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 = "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." From 1be9a481d9ef084a63d909f6c05343108041a24b Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 21 Jan 2026 11:37:15 +0100 Subject: [PATCH 34/78] =?UTF-8?q?=C2=A0spec:=20placeholder=20chapters=20fo?= =?UTF-8?q?r=20chips=20to=20come=20(#190)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/book.typ | 2 ++ spec/dvrm.typ | 17 +++++++++++++++++ spec/ecall.typ | 17 +++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 spec/dvrm.typ create mode 100644 spec/ecall.typ diff --git a/spec/book.typ b/spec/book.typ index 15f90f276..d12bafc09 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -15,7 +15,9 @@ #chapter("memw.typ")[MEMW] #chapter("lt.typ")[LT] #chapter("mul.typ")[MUL chip] + #chapter("dvrm.typ")[DVRM chip] #chapter("load.typ")[LOAD chip] + #chapter("ecall.typ")[ECALL chips] #chapter("bitwise.typ")[BITWISE] ] ) diff --git a/spec/dvrm.typ b/spec/dvrm.typ new file mode 100644 index 000000000..69e79cee2 --- /dev/null +++ b/spec/dvrm.typ @@ -0,0 +1,17 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_column_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_assumptions, + render_chip_padding_table, +) + +#let config = load_config() +// #let chip = load_chip("src/dvrm.toml", config) + +#show: book-page.with(title: "DVRM chip") + +*placeholder chapter: WIP* diff --git a/spec/ecall.typ b/spec/ecall.typ new file mode 100644 index 000000000..fee25768c --- /dev/null +++ b/spec/ecall.typ @@ -0,0 +1,17 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_column_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.with(title: "ECALL chips") + +*placeholder chapter: WIP* + From 2d39c55b890cde2cb7080327fcf1f22e485d4f16 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 21 Jan 2026 17:11:55 +0100 Subject: [PATCH 35/78] fix(spec): Use a better precedence value for "idx" (#197) --- spec/expr.typ | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/expr.typ b/spec/expr.typ index 751493619..a0530525b 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -47,16 +47,16 @@ #let PREC = ( "MIN": -1, // - "pow": 0, // ^ - "neg": 1, // Unary - - "cast": 2, // cast - "mul": 3, // * - "div": 4, // / - "sum": 5, // Σ - "not": 6, // not - "add": 7, // + - "sub": 8, // - - "idx": 9, // [] + "idx": 0, // [] + "pow": 1, // ^ + "neg": 2, // Unary - + "cast": 3, // cast + "mul": 4, // * + "div": 5, // / + "sum": 6, // Σ + "not": 7, // not + "add": 8, // + + "sub": 9, // - "eq": 10, // = and := "MAX": 11, // ) From 9cb3aff9270018202eae72b27318332c4f85321c Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 21 Jan 2026 17:12:42 +0100 Subject: [PATCH 36/78] fix(spec): Missing `write_register` multiplicity. (#196) --- spec/src/cpu.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 63ed2005f..a8345c820 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -581,6 +581,7 @@ output = "rv2" kind = "interaction" tag = "MEMW" input = [1, ["*", 2, "rd"], "rvd", ["+", "timestamp", 2], 1, 0, 0] +multiplicity = "write_register" [[constraints.mem]] kind = "interaction" From e68549f134a9f37ee51852a969981ca0e6d69f75 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 21 Jan 2026 18:03:59 +0100 Subject: [PATCH 37/78] spec: Initial version of memory argument (#164) Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/book.typ | 15 +++ spec/ebook.typ | 4 + spec/memory.typ | 233 +++++++++++++++++++++++++++++++++++++++++++ spec/src/config.toml | 6 ++ spec/src/page.toml | 57 +++++++++++ 5 files changed, 315 insertions(+) create mode 100644 spec/memory.typ create mode 100644 spec/src/page.toml diff --git a/spec/book.typ b/spec/book.typ index d12bafc09..29e61350c 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -5,6 +5,7 @@ #book-meta( title: "Lambda VM specification", summary: [ + #chapter("memory.typ")[Memory argument] #chapter("variables.typ")[Variables] #chapter("is_bit.typ")[IS_BIT template] #chapter("add.typ")[ADD template] @@ -32,3 +33,17 @@ ] #let rj = todo.with(background: teal, name: "Robin") #let et = todo.with(background: rgb("d4aa3a"), name: "Erik") + +#let style = state("style", ( + foreground: white, +)) + +#let aside(title, body) = context figure( + block(inset: (left: 1em, right: 1em, bottom: 1em), stroke: style.final().foreground, breakable: false)[ + #block(inset: (left: 1em, right: 1em, top: .75em, bottom: .75em), + width: 100% + 2em, + fill: rgb("55aaff"), + stroke: style.final().foreground, + align(center, strong(text(fill: black, title)))) + #align(left, body) +]) diff --git a/spec/ebook.typ b/spec/ebook.typ index abddf2701..410e926bb 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -1,8 +1,12 @@ #import "@preview/shiroa:0.3.1": * +#import "/book.typ": style #import "/templates/ebook.typ" #show: ebook.project.with(title: "typst-book", spec: "book.typ") +#style.update(( + foreground: black, +)) // set a resolver for inclusion #ebook.resolve-inclusion(it => include it) diff --git a/spec/memory.typ b/spec/memory.typ new file mode 100644 index 000000000..6687d733d --- /dev/null +++ b/spec/memory.typ @@ -0,0 +1,233 @@ +#import "/book.typ": book-page, rj, aside +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + render_chip_assumptions, + render_chip_column_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.with(title: "Memory argument") + += Memory argument + +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: +consuming a token corresponds to a "receive" and emitting a new token is a "send". +#rj[properly link/refer to the logup spec] + +== 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 functionality for comparisons. +#rj[Properly link/refer to the LT chip] + +#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 + ] +] + +#rj[reference to CPU chip/timestamp column and MEMW chip] + +== 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_column_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 + +#rj[Properly link/reference ECALL/HALT chip] +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. +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) diff --git a/spec/src/config.toml b/spec/src/config.toml index 68f1683de..d836f80e5 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -127,6 +127,12 @@ 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, 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 = ["input", "output", "auxiliary", "virtual", "multiplicity", "condition"] instantiated = ["input", "output", "auxiliary", "multiplicity"] diff --git a/spec/src/page.toml b/spec/src/page.toml new file mode 100644 index 000000000..8053d63df --- /dev/null +++ b/spec/src/page.toml @@ -0,0 +1,57 @@ +name = "PAGE" + +# Input + +[[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", ["cast", "offset", "DWordWL"]] + + +[[constraint_groups]] +name = "all" + +[[constraints.all]] +kind = "interaction" +tag = "IS_BYTE" +input = ["init"] + +[[constraints.all]] +kind = "interaction" +tag = "IS_BYTE" +input = ["fini"] + +[[constraints.all]] +kind = "interaction" +tag = "memory" +input = [0, "address", 0, "init"] +multiplicity = -1 + +[[constraints.all]] +kind = "interaction" +tag = "memory" +input = [0, "address", "timestamp", "fini"] +multiplicity = 1 From 4ca2a4e304b1aba3d03d7c3e4f9fa1fe16aa633f Mon Sep 17 00:00:00 2001 From: Cyprien de Saint Guilhem Date: Thu, 22 Jan 2026 08:48:10 -0800 Subject: [PATCH 38/78] fix(spec): Correct typo in spec README and align style (#210) --- spec/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/README.md b/spec/README.md index d841017cb..127e528c8 100644 --- a/spec/README.md +++ b/spec/README.md @@ -3,9 +3,9 @@ This repository contains specification for [`LambdaVM`](https://github.com/yetan 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 reposity +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. \ No newline at end of file +At this point, the wiki version is hosted locally and is actively updated as you modify the specification files. From 5e6fdc3eeb8630bde17699fb7fe806b2fe092c14 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Fri, 23 Jan 2026 12:05:47 +0100 Subject: [PATCH 39/78] spec: CPU padding (#195) * spec: CPU fast path for x0 reads * Do not write/read pc when in a padding row * specify padding for the CPU * Apply suggestions from code review Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> * spec: DECODE: update padding row * spec: DECODE: explain 'one more instruction' * spec: CPU: fix c_type_instruction typo * Apply suggestions from code review Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> * spec: align `packed_decode` in `DECODE` and `CPU` * spec: DECODE: add `read_registerX` to `packed_decode` * spec: DECODE: specify `read_register1` and `2` * spec: DECODE: update pc padding value * spec: DECODE: several small fixes * spec: DECODE: fix ECALL's rs2 value Co-authored-by: Robin Jadoul * spec: DECODE: minor rewording Co-authored-by: Robin Jadoul * spec: DECODE: minor fix --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> Co-authored-by: Erik Takke --- spec/cpu.typ | 10 ++ spec/decode.typ | 125 +++++++++++++----------- spec/src/cpu.toml | 156 +++++++++++++++++++++++------- spec/src/decode.toml | 63 ++++++------ spec/src/decode_uncompressed.toml | 10 ++ 5 files changed, 241 insertions(+), 123 deletions(-) diff --git a/spec/cpu.typ b/spec/cpu.typ index 00a33f5a2..784d750d2 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -6,6 +6,7 @@ total_nr_variables, total_nr_instantiated_columns, render_constraint_table, + render_chip_padding_table, ) #let config = load_config() @@ -82,3 +83,12 @@ For @cpu:c:is_equal, refer to the logic of IsZero or IsEqual, in combination wit #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 index 24846d2c1..d8332e033 100644 --- a/spec/decode.typ +++ b/spec/decode.typ @@ -32,8 +32,13 @@ The #decode table is comprised of #nr_variables variables that are expressed usi == 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. @@ -46,16 +51,17 @@ Note that the below table is _not_ used in practice: it is solely used for the p 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, +- *`operation`*: the assembly operation being encoded. - *`op-flag`*: which of the "`ALU` selector flags" operation flags to set. Each operation sets exactly one. -- *`w_reg`*, *`w_instr`*, *`signed`*: whether to set the `write_register`, `word_instr` or `signed` flag, respectively, +- *`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 below. + *The `c_type` flag is set independently of the below table*, as explained next. Further clarification is provided in the notes following the table. @@ -74,19 +80,19 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is show figure: set block(breakable: true) figure(table( - columns: (auto, auto, 40pt, 40pt, 40pt, 1fr, 15pt), + columns: (auto, auto, 40pt, 40pt, 1fr, 15pt), stroke: 0pt, inset: (right: .5em), - align: (left, right, center, center, center, left, right), + align: (left, right, center, center, left, right), fill: (_, y) => if calc.odd(y) and y <= lines.len() { luma(245) } else { white }, - table.header([*Operation*], [*op-flag*], [*`w_reg`*], [*`w_instr`*], [*`signed`*], [*other*], []), + 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_reg`*], [*`w_instr`*], [*`signed`*], [*other*]), + table.footer([*Operation*], [*op-flag*], [*`w_instr`*], [*`signed`*], [*other*]), ), caption: [Decoding table] ) @@ -94,56 +100,56 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is #let decoding = ( // OP-IMM - ([`ADDI[W] rd, rs1, imm`], [`ADD`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), - ([`SLTI[U] rd, rs1, imm`], [`SLT`], [$#`rd` eq.not 0$], [], [#sym.not`[U]`], [], [#ref_note(, )]), - ([`ANDI rd, rs1, imm`], [`AND`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`ORI rd, rs1, imm`], [`OR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`XORI rd, rs1, imm`], [`XOR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`SLLI[W] rd, rs1, imm`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note()]), - ([`SRLI[W] rd, rs1, imm`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [`mp_selector`], [#ref_note(, )]), - ([`SRAI[W] rd, rs1, imm`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [1], [`mp_selector`], [#ref_note(, )]), + ([`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`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), - ([`SUB[W] rd, rs1, rs2`], [`SUB`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), - ([`SLT[U] rd, rs1, rs2`], [`SLT`], [$#`rd` eq.not 0$], [], [#sym.not`[U]`], [], [#ref_note(, )]), - ([`AND rd, rs1, rs2`], [`AND`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`OR rd, rs1, rs2`], [`OR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`XOR rd, rs1, rs2`], [`XOR`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`SLL[W] rd, rs1, rs2`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [], [#ref_note(, )]), - ([`SRL[W] rd, rs1, rs2`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [], [`mp_selector`], [#ref_note(, )]), - ([`SRA[W] rd, rs1, rs2`], [`SHIFT`], [$#`rd` eq.not 0$], [`[W]`], [1], [`mp_selector`], [#ref_note(, )]), + ([`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`], [$#`rd` eq.not 0$], [`[W]`], [1], [`mp_selector`], [#ref_note(, )]), - ([`MULH rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [], [1], [`mp_selector`, `muldiv_selector`], [#ref_note()]), - ([`MULHU rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [], [], [`muldiv_selector`], [#ref_note()]), - ([`MULHSU rd, rs1, rs2`], [`MUL`], [$#`rd` eq.not 0$], [], [1], [`muldiv_selector`], [#ref_note()]), - ([`DIV[U][W] rd, rs1, rs2`], [`DIVREM`], [$#`rd` eq.not 0$], [`[W]`], [#sym.not`[U]`], [], [#ref_note(, , )]), - ([`REM[U][W] rd, rs1, rs2`], [`DIVREM`], [$#`rd` eq.not 0$], [`[W]`], [#sym.not`[U]`], [`muldiv_selector`], [#ref_note(, , )]), + ([`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`], [$#`rd` eq.not 0$], [], [], [], [#ref_note(, )]), - ([`AUIPC rd, imm`], [`ADD`], [$#`rd` eq.not 0$], [], [], [`rs1 := x255`], [#ref_note(, )]), - ([`JAL rd, imm`], [`JALR`], [$#`rd` eq.not 0$], [], [], [`rs1 := x255`], [#ref_note(, )]), + ([`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`], [$#`rd` eq.not 0$], [], [], [], [#ref_note()]), - ([`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()]), + ([`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()]), + ([`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`], [], [], [], [], []), + ([`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`], [1], [], [], [$#`rs1` := #`x17`$, $#`rs2` := #`x11`$, $#`rd` := #`x10`$], [#ref_note()]), - ([`EBREAK`], [`EBREAK`], [], [], [], [], []), + ([`ECALL`], [`ECALL`], [], [], [$#`rs1` := #`x17`$, $#`rs2` := #`x10`$, $#`rd` := #`x10`$], [#ref_note()]), + ([`EBREAK`], [`EBREAK`], [], [], [], []), // FENCE - ([`FENCE`], [`ADD`], [], [], [], [], [#ref_note()]), + ([`FENCE`], [`ADD`], [], [], [], [#ref_note()]), ) #decoding_table(decoding) @@ -157,16 +163,10 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is ==== Notes We note the following about the above decoding table: #enum(numbering: "[1]", - enum.item( - referenceable_note( - "note_w_reg", - [`write_register`: $#`rd` eq.not 0$ indicates that $#`write_register` = 1$ when $#`rd` eq.not 0$ and $0$ otherwise.] - ) - ), enum.item( referenceable_note( "note_word_instr", - [`word_instr`: `[W]` indicates that $#`word_instr` = 0$ for the `W`-variant of the operation, and $0$ for the non-`W`-variant.] + [`word_instr`: `[W]` indicates that $#`word_instr` = 1$ for the `W`-variant of the operation, and $0$ for the non-`W`-variant.] ) ), enum.item( @@ -194,9 +194,9 @@ We note the following about the above decoding table: 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`. + [`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:13]$ of `imm` and extending it to 64 bits; the least significant bit should always be 0.*] + 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( @@ -206,7 +206,7 @@ We note the following about the above decoding table: "On RISC-V a system call has its own instruction: `ECALL`. A system call can have up to 7 arguments and has 1 return value. The arguments are in registers A0-A6, in that order, and the return value is written into A0 before giving back control to the guest. A7 contains the system call number." #link("https://libriscv.no/docs/concepts/syscalls/#the-risc-v-system-call-abi")[[source]] As such, - syscall number in A7 (= register `x17`) - - first syscall argument in A1 (= register `x11`) + - first syscall argument in A0 (= register `x10`) - syscall output in A0 (= register `x10`)] ) ), @@ -217,3 +217,10 @@ We note the following about the above decoding table: ) ) ) + +== 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. \ No newline at end of file diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index a8345c820..49a78ee15 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -13,48 +13,69 @@ desc = "A preprocessed timestamp to coordinate the memory argument. Since we hav 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 # TODO: Should this just be a word? (CHECK: effect on computation/extension of arg2) # TODO: make sure decode correctly extends this (may be zero for unsigned and word_instr?) @@ -62,11 +83,13 @@ desc = "Whether the instruction is of C type, i.e., whether it is 2 bytes long i 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" @@ -75,96 +98,115 @@ desc = """Multi-purpose selector used by different ALU operations for different - 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 @@ -172,102 +214,122 @@ desc = "One-hot ALU selector flag" 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_sign_bit" type = "Bit" desc = "The sign bit of `rv1` if seen as a 32-bit word" +pad = 0 [[variables.auxiliary]] name = "arg1" type = "DWordBL" -desc = "The extended version of `rv1`, depending on `c_type_instruction`" +desc = "The extended version of `rv1`, depending on `word_instr`" +pad = 0 [[variables.auxiliary]] name = "arg2_sign_bit" type = "Bit" desc = "The sign bit of `arg2` if seen as a 32-bit word" +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_sign_bit" type = "Bit" desc = "The sign bit of `res`, if seen as a 32-bit word" +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" -poly = ["+", - ["*", ["^", 2, 0], "write_register"], - ["*", ["^", 2, 1], "memory_2bytes"], - ["*", ["^", 2, 2], "memory_4bytes"], - ["*", ["^", 2, 3], "memory_8bytes"], - ["*", ["^", 2, 4], "c_type_instruction"], - ["*", ["^", 2, 5], "signed"], - ["*", ["^", 2, 6], "mp_selector"], - ["*", ["^", 2, 7], "muldiv_selector"], - ["*", ["^", 2, 8], "word_instr"], - ["*", ["^", 2, 9], "ADD"], - ["*", ["^", 2, 10], "SUB"], - ["*", ["^", 2, 11], "SLT"], - ["*", ["^", 2, 12], "AND"], - ["*", ["^", 2, 13], "OR"], - ["*", ["^", 2, 14], "XOR"], - ["*", ["^", 2, 15], "SHIFT"], - ["*", ["^", 2, 16], "JALR"], - ["*", ["^", 2, 17], "BEQ"], - ["*", ["^", 2, 18], "BLT"], - ["*", ["^", 2, 19], "LOAD"], - ["*", ["^", 2, 20], "STORE"], - ["*", ["^", 2, 21], "MUL"], - ["*", ["^", 2, 22], "DIVREM"], - ["*", ["^", 2, 23], "ECALL"], - ["*", ["^", 2, 24], "EBREAK"], - ["*", ["^", 2, 25], "rs1"], - ["*", ["^", 2, 33], "rs2"], - ["*", ["^", 2, 41], "rd"], +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 = "The flags are a one-hot vector in the decoding" +desc = "At most one ALU selector flag is 1 by the decoding, and every other flag is 0." ref = "cpu:a:one-hot" [[assumptions]] @@ -287,6 +349,18 @@ input = ["pc", "imm", "packed_decode"] 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" @@ -314,8 +388,8 @@ ref = "cpu:c:range_memory_8bytes" [[constraints.range]] kind = "template" tag = "IS_BIT" -input = ["c_kind_instruction"] -ref = "cpu:c:range_c_kind_instruction" +input = ["c_type_instruction"] +ref = "cpu:c:range_c_type_instruction" [[constraints.range]] kind = "template" @@ -568,6 +642,13 @@ kind = "interaction" tag = "MEMW" input = [1, ["*", 2, "rs1"], "rv1", ["+", "timestamp", 0], 1, 0, 0] output = "rv1" +multiplicity = "read_register1" + +[[constraints.mem]] +kind = "arith" +constraint = "$#`!read_register1` => #`rv1[i]` = 0$" +poly = ["*", ["not", "read_register1"], ["idx", "rv1", "i"]] +iter = ["i", 0, 2] # TODO: no types available, so no casting yet [[constraints.mem]] @@ -575,6 +656,13 @@ kind = "interaction" tag = "MEMW" input = [1, ["*", 2, "rs2"], "rv2", ["+", "timestamp", 1], 1, 0, 0] output = "rv2" +multiplicity = "read_register2" + +[[constraints.mem]] +kind = "arith" +constraint = "$#`!read_register2` => #`rv2[i]` = 0$" +poly = ["*", ["not", "read_register2"], ["idx", "rv2", "i"]] +iter = ["i", 0, 2] # TODO: no types available, so no casting yet [[constraints.mem]] @@ -603,6 +691,7 @@ kind = "interaction" tag = "MEMW" input = [1, ["*", 2, 255], "next_pc", ["+", "timestamp", 1], 1, 0, 0] output = "pc" +multiplicity = ["not", "pad"] [[constraint_groups]] @@ -614,6 +703,7 @@ kind = "arith" constraint = "`!EBREAK`" desc = "We treat `EBREAK` as an unprovable trap" poly = ["not", "EBREAK"] +ref = "cpu:c:ebreak_traps" # TODO: no types available, so no casting yet [[constraints.sys]] diff --git a/spec/src/decode.toml b/spec/src/decode.toml index 6c01e4f6c..367db1568 100644 --- a/spec/src/decode.toml +++ b/spec/src/decode.toml @@ -4,8 +4,7 @@ name = "DECODE" name = "pc" type = "DWordWL" desc = "value of the program counter this instruction is associated with." -# TODO(#136): fix this when padding the CPU -pad = 1 +pad = 7 [[variables.output]] name = "packed_decode" @@ -13,37 +12,39 @@ 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:7] `rs1`, \\ -[8:15] `rs2`, \\ -[16:23] `rd`, \\ -[24] `write_register`, \\ -[25] `memory_2bytes`, \\ -[26] `memory_4bytes`, \\ -[27] `memory_8bytes`, \\ -[28] `c_type`, \\ -[29] `signed`, \\ -[30] `mp_selector`, \\ -[31] `muldiv_selector`, \\ -[32] `word_instr`, \\ -[33] `ADD`, \\ -[34] `SUB`, \\ -[35] `SLT`, \\ -[36] `AND`, \\ -[37] `OR`, \\ -[38] `XOR`, \\ -[39] `SHIFT`, \\ -[40] `JALR`, \\ -[41] `BEQ`, \\ -[42] `BLT`, \\ -[43] `LOAD`, \\ -[44] `STORE`, \\ -[45] `MUL`, \\ -[46] `DIVREM`, \\ -[47] `ECALL`, \\ -[48] `EBREAK`; \\ +[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 = 0 +pad = ["^", 2, 26] [[variables.output]] name = "imm" diff --git a/spec/src/decode_uncompressed.toml b/spec/src/decode_uncompressed.toml index 8457005f8..0f6c931c2 100644 --- a/spec/src/decode_uncompressed.toml +++ b/spec/src/decode_uncompressed.toml @@ -20,6 +20,16 @@ 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" From a184f95be3d22f8f166731576387a52ef7433251 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:51:09 +0100 Subject: [PATCH 40/78] spec: update `ECALL` signature (#244) * spec: update `ECALL` signature * spec: CPU/ECALL: cast rv1 to DWordWL --- spec/decode.typ | 9 +++------ spec/src/cpu.toml | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/spec/decode.typ b/spec/decode.typ index d8332e033..218cb84a3 100644 --- a/spec/decode.typ +++ b/spec/decode.typ @@ -146,7 +146,7 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is ([`SH rs1, rs2, imm`], [`STORE`], [], [], [`mem_2B`], []), ([`SB rs1, rs2, imm`], [`STORE`], [], [], [], []), // ECALL/EBREAK - ([`ECALL`], [`ECALL`], [], [], [$#`rs1` := #`x17`$, $#`rs2` := #`x10`$, $#`rd` := #`x10`$], [#ref_note()]), + ([`ECALL`], [`ECALL`], [], [], [$#`rs1` := #`x17`$], [#ref_note()]), ([`EBREAK`], [`EBREAK`], [], [], [], []), // FENCE ([`FENCE`], [`ADD`], [], [], [], [#ref_note()]), @@ -203,11 +203,8 @@ We note the following about the above decoding table: referenceable_note( "note-ecall", [`ECALL`: - "On RISC-V a system call has its own instruction: `ECALL`. A system call can have up to 7 arguments and has 1 return value. The arguments are in registers A0-A6, in that order, and the return value is written into A0 before giving back control to the guest. A7 contains the system call number." #link("https://libriscv.no/docs/concepts/syscalls/#the-risc-v-system-call-abi")[[source]] - As such, - - syscall number in A7 (= register `x17`) - - first syscall argument in A0 (= register `x10`) - - syscall output in A0 (= register `x10`)] + "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( diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 49a78ee15..d8609e935 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -705,12 +705,10 @@ desc = "We treat `EBREAK` as an unprovable trap" poly = ["not", "EBREAK"] ref = "cpu:c:ebreak_traps" -# TODO: no types available, so no casting yet [[constraints.sys]] kind = "interaction" tag = "ECALL" -input = ["rv1", "pc", "timestamp", "rv2"] -output = "rvd" +input = ["timestamp", ["cast", "rv1", "DWordWL"]] multiplicity = "ECALL" From 37a9a9fb5dc22ce48713fa6263d383c52d11a75c Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 27 Jan 2026 15:13:46 +0100 Subject: [PATCH 41/78] spec: Allow for cross referencing between different chapters, both in pdf and web mode (#225) * spec: Allow for cross referencing between different chapters, both in pdf and web mode * Improve PDF organization The PDF now no longer depends on shiroa trickery to compile, so errors are more clearly visible instead of being hidden behind layour iterations. Additionally, we can now have nice chapter headings and references to them. * Allow xref by specifying only the label * document strip-all * It does work, after all; with only ~7GB of RAM usage for the entire thing * small cleanup * Update spec/book.typ Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> * Address some review comments * less repetition for file names --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/add.typ | 5 +- spec/bitwise.typ | 4 +- spec/book.typ | 146 +++++++++++++++++++++++++++++++++------ spec/branch.typ | 2 +- spec/cpu.typ | 2 +- spec/decode.typ | 7 +- spec/dvrm.typ | 2 +- spec/ebook.typ | 19 +++-- spec/ecall.typ | 2 +- spec/is_bit.typ | 5 +- spec/load.typ | 2 +- spec/lt.typ | 2 +- spec/memory.typ | 8 +-- spec/memw.typ | 2 +- spec/mul.typ | 4 +- spec/shift.typ | 4 +- spec/templates/ebook.typ | 37 ---------- spec/templates/page.typ | 44 +++++++----- spec/variables.typ | 3 +- 19 files changed, 185 insertions(+), 115 deletions(-) delete mode 100644 spec/templates/ebook.typ diff --git a/spec/add.typ b/spec/add.typ index 0dade7b01..981a0ccb1 100644 --- a/spec/add.typ +++ b/spec/add.typ @@ -2,11 +2,11 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table -#show: book-page.with(title: "ADD/SUB") - #let config = load_config() #let chip = load_chip("src/add.toml", config) +#show: book-page(chip.name) + #let add = raw(chip.name) #let highlighted_code(code) = { @@ -18,7 +18,6 @@ raw(code)) } -= #add template #add is a constraint template that is used to assert that $#`sum` = #`lhs` + #`rhs` mod 2^64$, under the condition that `cond` is non-zero. == Notation diff --git a/spec/bitwise.typ b/spec/bitwise.typ index 34ec6dd10..9b5b4a638 100644 --- a/spec/bitwise.typ +++ b/spec/bitwise.typ @@ -13,9 +13,7 @@ #let bitwise = raw(chip.name) -#show: book-page.with(title: "BRANCH chip") - -= #bitwise chip +#show: book-page(chip.name) == Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/book.typ b/spec/book.typ index 29e61350c..b194b7f70 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -1,31 +1,35 @@ #import "@preview/shiroa:0.3.1": * +#import "/templates/page.typ": project #show: book -#book-meta( +#let meta = ( title: "Lambda VM specification", - summary: [ - #chapter("memory.typ")[Memory argument] - #chapter("variables.typ")[Variables] - #chapter("is_bit.typ")[IS_BIT template] - #chapter("add.typ")[ADD template] - #chapter("decode.typ")[DECODE chip] - #chapter("cpu.typ")[CPU chip] - #chapter("shift.typ")[SHIFT chip] - #chapter("branch.typ")[BRANCH] - #chapter("memw.typ")[MEMW] - #chapter("lt.typ")[LT] - #chapter("mul.typ")[MUL chip] - #chapter("dvrm.typ")[DVRM chip] - #chapter("load.typ")[LOAD chip] - #chapter("ecall.typ")[ECALL chips] - #chapter("bitwise.typ")[BITWISE] - ] + authors: ("3MI Labs", "Aligned"), + summary: ( + ("memory.typ", [Memory argument], ), + ("variables.typ", [Variables], ), + ("is_bit.typ", [IS_BIT template], ), + ("add.typ", [ADD/SUB template], ), + ("decode.typ", [DECODE table], ), + ("cpu.typ", [CPU chip], ), + ("shift.typ", [SHIFT chip], ), + ("branch.typ", [BRANCH chip], ), + ("memw.typ", [MEMW chip], ), + ("lt.typ", [LT chip], ), + ("mul.typ", [MUL chip], ), + ("dvrm.typ", [DVRM chip], ), + ("load.typ", [LOAD chip], ), + ("ecall.typ", [ECALL chips], ), + ("bitwise.typ", [BITWISE chips], ), + ) +) +#book-meta( + title: meta.title, + authors: meta.authors, + summary: meta.summary.map(((ch, title, _ref)) => chapter(ch, title)).join() ) -// re-export page template -#import "/templates/page.typ": project -#let book-page = project #let todo(background: white, foreground: black, name: none, body) = block(fill: background, outset: 0.5em, radius: 20%, stroke: black)[ #set text(fill: foreground) @@ -47,3 +51,103 @@ 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 if f not in _xref-included.get() { + hide(box(width: 0%, height: 0%, strip-all(include "/" + f))) + } + context _xref-included.update(x => x + ((f): true)) +} + +// 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.summary.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.summary.position(x => x == found) + 1)]) + } else { + // Because shiroa does weird url escaping + let shiroa-label = label(str(lbl).replace(":", "%3A")) + xref-include(ch) + // 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) = { + let file = if file.ends-with(".typ") { + file + } else { + lower(file) + ".typ" + } + assert(meta.summary.find(((f, _, _)) => f == file) != none, message: "Couldn't resolve typst source file " + file) + if is-shiroa { + (body) => { + context _xref-included.update(x => x + ((file): true)) + context _toplevel.update(s => { + if s == none { + file + } else { + s + } + }) + let cond() = _toplevel.final() == file + project.with(..args, title: context meta.summary.find(x => x.at(0) == _toplevel.final()).at(1), cond: cond)([ + #show ref: it => context if _toplevel.final() == file { + xref(it) + } + #body + ]) + } + } else { + (body) => body + } +} diff --git a/spec/branch.typ b/spec/branch.typ index a18c252b7..f448f2da4 100644 --- a/spec/branch.typ +++ b/spec/branch.typ @@ -12,7 +12,7 @@ #let config = load_config() #let chip = load_chip("src/branch.toml", config) -#show: book-page.with(title: "BRANCH chip") +#show: book-page(chip.name) == Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/cpu.typ b/spec/cpu.typ index 784d750d2..0afa75f62 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -12,7 +12,7 @@ #let config = load_config() #let chip = load_chip("src/cpu.toml", config) -#show: book-page.with(title: "CPU chip") +#show: book-page(chip.name) == Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/decode.typ b/spec/decode.typ index 218cb84a3..586625226 100644 --- a/spec/decode.typ +++ b/spec/decode.typ @@ -1,4 +1,4 @@ -#import "/book.typ": book-page, rj +#import "/book.typ": book-page, rj, xref #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, @@ -11,11 +11,10 @@ #let config = load_config() #let chip = load_chip("src/decode.toml", config) -#show: book-page.with(title: "DECODE chip") +#show: book-page(chip.name) #let decode = raw(chip.name) -= #decode table 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. @@ -220,4 +219,4 @@ In addition to decoding all instructions provided in the ELF and adding a corres 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. \ No newline at end of file +More details on this matter are provided in the `CPU` chip. diff --git a/spec/dvrm.typ b/spec/dvrm.typ index 69e79cee2..f1d9a3a4c 100644 --- a/spec/dvrm.typ +++ b/spec/dvrm.typ @@ -12,6 +12,6 @@ #let config = load_config() // #let chip = load_chip("src/dvrm.toml", config) -#show: book-page.with(title: "DVRM chip") +#show: book-page("dvrm.typ") *placeholder chapter: WIP* diff --git a/spec/ebook.typ b/spec/ebook.typ index 410e926bb..835751163 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -1,12 +1,19 @@ -#import "@preview/shiroa:0.3.1": * -#import "/book.typ": style +#import "/book.typ": style, meta -#import "/templates/ebook.typ" +#set document(author: meta.authors, title: meta.title) -#show: ebook.project.with(title: "typst-book", spec: "book.typ") #style.update(( foreground: black, )) -// set a resolver for inclusion -#ebook.resolve-inclusion(it => include it) +#align(center, title(meta.title)) +#pagebreak(weak: true) +#outline() + +#show heading: set heading(numbering: "1.1") + +#meta.summary.map(((ch, title, ref)) => [ + #pagebreak(weak: true) + #heading(supplement: [Chapter], level: 1, title)#ref + #include ch +]).join() diff --git a/spec/ecall.typ b/spec/ecall.typ index fee25768c..3bf2d3b69 100644 --- a/spec/ecall.typ +++ b/spec/ecall.typ @@ -11,7 +11,7 @@ #let config = load_config() -#show: book-page.with(title: "ECALL chips") +#show: book-page("ecall.typ") *placeholder chapter: WIP* diff --git a/spec/is_bit.typ b/spec/is_bit.typ index c379080cd..a12d62108 100644 --- a/spec/is_bit.typ +++ b/spec/is_bit.typ @@ -2,11 +2,11 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": render_chip_column_table, render_constraint_table -#show: book-page.with(title: "IS_BIT template") - #let config = load_config() #let chip = load_chip("src/is_bit.toml", config) +#show: book-page(chip.name) + #let is_bit = raw(chip.name) #let highlighted_code(code) = { @@ -18,7 +18,6 @@ raw(code)) } -= #is_bit template #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. diff --git a/spec/load.typ b/spec/load.typ index 931611108..71c274d1b 100644 --- a/spec/load.typ +++ b/spec/load.typ @@ -12,7 +12,7 @@ #let config = load_config() #let chip = load_chip("src/load.toml", config) -#show: book-page.with(title: "LOAD chip") +#show: book-page(chip.name) == Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/lt.typ b/spec/lt.typ index 3b57a62e3..cb4f9c141 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -12,7 +12,7 @@ #let config = load_config() #let chip = load_chip("src/lt.toml", config) -#show: book-page.with(title: "LT chip") +#show: book-page(chip.name) == Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/memory.typ b/spec/memory.typ index 6687d733d..ec8735e49 100644 --- a/spec/memory.typ +++ b/spec/memory.typ @@ -1,4 +1,4 @@ -#import "/book.typ": book-page, rj, aside +#import "/book.typ": book-page, rj, aside, xref #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, @@ -12,9 +12,7 @@ #let config = load_config() #let chip = load_chip("src/page.toml", config) -#show: book-page.with(title: "Memory argument") - -= Memory argument +#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. @@ -108,7 +106,7 @@ 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 functionality for comparisons. +We choose to represent timestamps as machine words, using the existing `LT` chip (@lt) functionality for comparisons. #rj[Properly link/refer to the LT chip] #aside[Note on options and trade-offs for timestamp representation][ diff --git a/spec/memw.typ b/spec/memw.typ index bcf6a64b0..77b786bf6 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -11,7 +11,7 @@ #let config = load_config() #let chip = load_chip("src/memw.toml", config) -#show: book-page.with(title: "MEMW chip") +#show: book-page(chip.name) == Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/mul.typ b/spec/mul.typ index 1892994f0..bc5898fa0 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -12,12 +12,10 @@ #let config = load_config() #let chip = load_chip("src/mul.toml", config) -#show: book-page.with(title: "MUL chip") +#show: book-page(chip.name) #let mul = raw(chip.name) -= #mul chip - == Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) diff --git a/spec/shift.typ b/spec/shift.typ index 70aebc97c..177ce6104 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -14,9 +14,7 @@ #let shift = raw(chip.name) -#show: book-page.with(title: "SHIFT chip") - -= #shift chip +#show: book-page(chip.name) == Interface The #shift chip has the following interface: diff --git a/spec/templates/ebook.typ b/spec/templates/ebook.typ deleted file mode 100644 index 44e0312d3..000000000 --- a/spec/templates/ebook.typ +++ /dev/null @@ -1,37 +0,0 @@ -#import "@preview/shiroa:0.3.1": * -#import "/templates/page.typ": part-style, project - -#let _page-project = project - -#let _resolve-inclusion-state = state("_resolve-inclusion", none) - -#let resolve-inclusion(inc) = _resolve-inclusion-state.update(it => inc) - -#let project(title: "", authors: (), spec: "", content) = { - // Set document metadata early - set document( - author: authors, - title: title, - ) - - // Inherit from gh-pages - show: _page-project - - if title != "" { - heading(title) - } - - context { - let inc = _resolve-inclusion-state.final() - external-book(spec: inc(spec)) - - let mt = book-meta-state.final() - let styles = (inc: inc, part: part-style, chapter: it => it) - - if mt != none { - mt.summary.map(it => visit-summary(it, styles)).sum() - } - } - - content -} diff --git a/spec/templates/page.typ b/spec/templates/page.typ index 1f7f88ea0..4ec7b27ac 100644 --- a/spec/templates/page.typ +++ b/spec/templates/page.typ @@ -85,8 +85,11 @@ /// - 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", plain-body) = { +#let project(title: "Typst Book", description: auto, authors: (), kind: "page", cond: none, plain-body) = { // set basic document metadata set document( author: authors, @@ -137,23 +140,28 @@ lang: "en", ) - // 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 + 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/variables.typ b/spec/variables.typ index 42e7bc379..d62fec7ac 100644 --- a/spec/variables.typ +++ b/spec/variables.typ @@ -1,11 +1,10 @@ #import "/book.typ": book-page #import "/src.typ": load_config -#show: book-page.with(title: "Variables") +#show: book-page("variables.typ") #let config = load_config() -= Variables 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. From 5084c80dcb5ba347092f8f5e001ce0f9ec1f5d30 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 27 Jan 2026 16:06:22 +0100 Subject: [PATCH 42/78] spec: Update LT interaction signature so that it can be used properly for timestamps (#246) * spec: Update LT interaction signature so that it can be used properly for timestamps * fix(spec): add missing signed argument to LT from MEMW * Update spec/src/lt.toml Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> * Update spec/src/lt.toml Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/lt.typ | 5 +++-- spec/src/cpu.toml | 2 +- spec/src/lt.toml | 22 +++++++++++++++++----- spec/src/memw.toml | 14 +++++++------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/spec/lt.typ b/spec/lt.typ index cb4f9c141..ea36eb4dc 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -22,7 +22,7 @@ The `LT` chip is comprised of #nr_variables variables that are expressed using # #render_chip_column_table(chip, config) == Assumptions -We assume the inputs `lhs`, `rhs` and `signed` are appropriately range checked. +We assume the inputs `lhs`, `rhs` and `signed` are partially range checked. #render_chip_assumptions(chip, config) == Constraints @@ -71,7 +71,8 @@ Therefore, we can use $Q$ to constrain `lt` when `signed = 1`. #render_constraint_table(chip, config, groups: "defs") -And then we constrain the subtraction. +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") diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index d8609e935..c151b6eff 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -567,7 +567,7 @@ ref = "cpu:c:sub" [[constraints.alu]] kind = "interaction" tag = "LT" -input = [["cast", "arg1", "DWordHHW"], ["cast", "arg2", "DWordHHW"], "signed"] +input = [["cast", "arg1", "DWordWL"], ["cast", "arg2", "DWordWL"], "signed"] output = ["idx", "res", 0] multiplicity = ["+", "SLT", "BLT"] diff --git a/spec/src/lt.toml b/spec/src/lt.toml index 3836cdd13..10497b637 100644 --- a/spec/src/lt.toml +++ b/spec/src/lt.toml @@ -78,13 +78,11 @@ pad = 0 [[assumptions]] -desc = "`IS_HALFWORD[lhs[i]]` and `IS_WORD[lhs[0]]`" -iter = ["i", 1, 2] +desc = "`IS_WORD[lhs[0]]`" ref = "lt:a:range_lhs" [[assumptions]] -desc = "`IS_HALFWORD[rhs[i]]` and `IS_WORD[rhs[0]]`" -iter = ["i", 1, 2] +desc = "`IS_WORD[rhs[0]]`" ref = "lt:a:range_rhs" [[assumptions]] @@ -130,6 +128,20 @@ tag = "IS_BIT" input = [["idx", "carry", "i"]] iter = ["i", 0, 1] +[[constraints.defs]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", "lhs", 1]] +multiplicity = "μ" +ref = "lt:c:range_lhs" + +[[constraints.defs]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", "rhs", 1]] +multiplicity = "μ" +ref = "lt:c:range_rhs" + [[constraints.sub]] kind = "interaction" tag = "IS_HALFWORD" @@ -146,6 +158,6 @@ desc = "Each row contributes the following to the LogUp sum" [[constraints.output]] kind = "interaction" tag = "LT" -input = ["lhs", "rhs", "signed"] +input = [["cast", "lhs", "DWordWL"], ["cast", "rhs", "DWordWL"], "signed"] output = "lt" multiplicity = "-μ" diff --git a/spec/src/memw.toml b/spec/src/memw.toml index 9aa9cd592..f7276a9cd 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -159,21 +159,21 @@ iters = [ [[constraints.consistency]] kind = "interaction" tag = "LT" -input = [["idx", "old_timestamp", 0], "timestamp"] +input = [["idx", "old_timestamp", 0], "timestamp", 0] output = 1 multiplicity = "μ_sum" [[constraints.consistency]] kind = "interaction" tag = "LT" -input = [["idx", "old_timestamp", 1], "timestamp"] +input = [["idx", "old_timestamp", 1], "timestamp", 0] output = 1 multiplicity = "w2" [[constraints.consistency]] kind = "interaction" tag = "LT" -input = [["idx", "old_timestamp", "i"], "timestamp"] +input = [["idx", "old_timestamp", "i"], "timestamp", 0] output = 1 iter = ["i", 2, 3] multiplicity = "w4" @@ -181,7 +181,7 @@ multiplicity = "w4" [[constraints.consistency]] kind = "interaction" tag = "LT" -input = [["idx", "old_timestamp", "i"], "timestamp"] +input = [["idx", "old_timestamp", "i"], "timestamp", 0] output = 1 iter = ["i", 4, 7] multiplicity = "write8" @@ -194,21 +194,21 @@ prefix = "R" [[constraints.overflow]] kind = "interaction" tag = "LT" -input = ["base_address", ["cast", ["idx", "address_add", 0], "DWordWL"]] +input = ["base_address", ["cast", ["idx", "address_add", 0], "DWordWL"], 0] output = 1 multiplicity = "write2" [[constraints.overflow]] kind = "interaction" tag = "LT" -input = ["base_address", ["cast", ["idx", "address_add", 2], "DWordWL"]] +input = ["base_address", ["cast", ["idx", "address_add", 2], "DWordWL"], 0] output = 1 multiplicity = "write4" [[constraints.overflow]] kind = "interaction" tag = "LT" -input = ["base_address", ["cast", ["idx", "address_add", 6], "DWordWL"]] +input = ["base_address", ["cast", ["idx", "address_add", 6], "DWordWL"], 0] output = 1 multiplicity = "write8" From c72eefef3c43faee9e0a463581a58397eb5b68a7 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:43:19 +0100 Subject: [PATCH 43/78] spec: `HALT` chip (#235) * spec: HALT: first draft * spec: HALT: add link to sys call number * spec: HALT: update ECALL signature * spec: HALT: minor update * spec: HALT: document cleanup verification alternative * adapt to new chapter format * spec: HALT: fix MEMW register indexing * spec: HALT: move halt.typ into ecall.typ --------- Co-authored-by: Robin Jadoul --- spec/ecall.typ | 44 ++++++++++++++++++++++++++++++++++-- spec/src/halt.toml | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 spec/src/halt.toml diff --git a/spec/ecall.typ b/spec/ecall.typ index 3bf2d3b69..9f2d96048 100644 --- a/spec/ecall.typ +++ b/spec/ecall.typ @@ -1,4 +1,4 @@ -#import "/book.typ": book-page +#import "/book.typ": book-page, aside #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_column_table, @@ -13,5 +13,45 @@ #show: book-page("ecall.typ") -*placeholder chapter: WIP* +#let config = load_config() +#let chip = load_chip("src/halt.toml", config) +#let halt = raw(chip.name) +== #halt chip + +=== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The #halt chip leverages #nr_variables variable, spanning #nr_columns columns: +#render_chip_column_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 +The HALT chip contributes the following interaction to the lookup-argument: +#render_constraint_table(chip, config, groups: "lookup") + +*Note*: #link("https://github.com/riscv-collab/riscv-gnu-toolchain/blob/master/linux-headers/include/asm-generic/unistd.h#L258")[$93$ is the system call number corresponding to `sys_exit`.] + +=== 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/src/halt.toml b/spec/src/halt.toml new file mode 100644 index 000000000..b0606e3e4 --- /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, ["*", 2, "i"], 0, ["-", ["^", 2, 64], 1], 1, 0, 0] +iter = ["i", 1, 9] +multiplicity = 1 +ref = "halt:c:zeroize_registers_lo" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 10], 0, ["-", ["^", 2, 64], 1], 1, 0, 0] +output = 0 +multiplicity = 1 +ref = "halt:c:read_zero_exit_code" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, "i"], 0, ["-", ["^", 2, 64], 1], 1, 0, 0] +iter = ["i", 11, 31] +multiplicity = 1 +ref = "halt:c:zeroize_registers_hi" + +[[constraints.all]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 255], 1, ["-", ["^", 2, 64], 1], 1, 0, 0] +multiplicity = 1 +ref = "halt:c:pc" + +[[constraint_groups]] +name = "lookup" + +[[constraints.lookup]] +kind = "interaction" +tag = "ECALL" +input = ["timestamp", 93] +multiplicity = ["-", 1] +ref = "halt:c:lookup" \ No newline at end of file From c902acdc83d7a61022b336f0196f8bca9cb7e7a7 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:45:19 +0100 Subject: [PATCH 44/78] spec: minor `MUL` fixes (#223) * spec: MUL: fix missing iters * spec: MUL: fix res slice in lookup contribution * spec: MU: split `res` into `lo` and `hi` * spec: MUL: replace `range` by `iter` * spec: MUL: update `lo` and `hi` types + introduce `res` as virtual * spec: MUL: add note on future optimization --- spec/mul.typ | 16 ++++++++--- spec/src/mul.toml | 68 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/spec/mul.typ b/spec/mul.typ index bc5898fa0..b2fb53d92 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -32,7 +32,6 @@ The following range checks are assumed to be performed/enforced outside of this #render_chip_assumptions(chip, config) == Constraints - === Overview When `lhs` and `rhs` are _unsigned_ integers, computing their product $mod 2^128$ comes down to evaluating $ @@ -65,14 +64,14 @@ We let `raw_product` capture the second summation in this last formula (see @mul 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 @mul:a:res and @mul:c:carry, combined with `carry`'s definition. +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:a:res forcing $#`res`_i < 2^32$, $#`res`_i$ can only assume one value: $#`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. @@ -81,7 +80,7 @@ In fact, in this situation it suffices to assert that $#`carry`_i < frac(p, 2^32 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; `carry` is appropriately range checked. +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 @@ -97,3 +96,12 @@ The #mul chip contributes the following to the lookup: The table can be padded to the next power of two with the following value assignments: #render_chip_padding_table(chip, config) + +== Notes +- `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. \ No newline at end of file diff --git a/spec/src/mul.toml b/spec/src/mul.toml index bf9ffc276..e987b0f75 100644 --- a/spec/src/mul.toml +++ b/spec/src/mul.toml @@ -31,9 +31,15 @@ pad = 0 # Output [[variables.output]] -name = "res" -type = "QuadWL" -desc = "the (extended) multiplication result" +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 @@ -63,8 +69,8 @@ name = "lhs_ext" type = ["Half", 8] desc = "sign-extended value of `lhs`" def = {idx="i", polys=[ - {range=[0, 3], poly=["idx", "lhs", "i"]}, - {range=[4, 7], poly=["*", 0xFFFF, "lhs_is_negative"]}, + {iter=[0, 3], poly=["idx", "lhs", "i"]}, + {iter=[4, 7], poly=["*", 0xFFFF, "lhs_is_negative"]}, ]} [[variables.virtual]] @@ -72,17 +78,27 @@ name = "rhs_ext" type = ["Half", 8] desc = "sign-extended value of `rhs`" def = {idx="i", polys=[ - {range=[0, 3], poly=["idx", "rhs", "i"]}, - {range=[4, 7], poly=["*", 0xFFFF, "rhs_is_negative"]}, + {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=[ - {range=0, poly=["*", ["^", 2, -32], ["-", ["idx", "raw_product", 0], ["idx", "res", 0]]]}, - {range=[1, 3], poly=["*", ["^", 2, -32], ["-", ["+", ["idx", "raw_product", "i"], ["idx", "carry", ["-", "i", 1]]], ["idx", "res", "i"]]]}, + {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]] @@ -109,17 +125,11 @@ pad = 0 [[assumptions]] desc = "`IS_HALF[lhs[i]]`" -range = ["i", 0, 3] +iter = ["i", 0, 3] [[assumptions]] desc = "`IS_HALF[rhs[i]]`" -range = ["i", 0, 3] - -[[assumptions]] -desc = "`IS_WORD[res[i]]`" -range = ["i", 0, 3] -ref = "mul:a:res" - +iter = ["i", 0, 3] # Constraints @@ -140,11 +150,27 @@ 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"]] -range = ["i", 0, 3] +iter = ["i", 0, 3] multiplicity = "μ_sum" ref = "mul:c:carry" @@ -156,7 +182,7 @@ name = "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"]] -range = ["i", 0, 3] +iter = ["i", 0, 3] ref = "mul:c:raw_product" [[constraint_groups]] @@ -166,7 +192,7 @@ name = "lookup" kind = "interaction" tag = "MUL" input = ["lhs", "lhs_signed", "rhs", "rhs_signed", "0"] -output = ["idx", "res", "0:4"] +output = ["cast", "lo", "DWordWL"] multiplicity = ["-", "μ_lo"] ref = "mul:c:lookup_lo" @@ -174,6 +200,6 @@ ref = "mul:c:lookup_lo" kind = "interaction" tag = "MUL" input = ["lhs", "lhs_signed", "rhs", "rhs_signed", "1"] -output = ["idx", "res", "4:8"] +output = ["cast", "hi", "DWordWL"] multiplicity = ["-", "μ_hi"] ref = "mul:c:lookup_hi" \ No newline at end of file From d5000f0d432c297dae38228c2ffa436687a20ac2 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:53:16 +0100 Subject: [PATCH 45/78] spec: `SIGN` (#279) * spec: introduce SIGN template * Update spec/src/sign.toml Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- spec/book.typ | 1 + spec/sign.typ | 42 ++++++++++++++++++++++++++++++++++++++++++ spec/src/sign.toml | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 spec/sign.typ create mode 100644 spec/src/sign.toml diff --git a/spec/book.typ b/spec/book.typ index b194b7f70..2fd6d8767 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -10,6 +10,7 @@ ("memory.typ", [Memory argument], ), ("variables.typ", [Variables], ), ("is_bit.typ", [IS_BIT template], ), + ("sign.typ", [SIGN template], ), ("add.typ", [ADD/SUB template], ), ("decode.typ", [DECODE table], ), ("cpu.typ", [CPU chip], ), diff --git a/spec/sign.typ b/spec/sign.typ new file mode 100644 index 000000000..6f8993f53 --- /dev/null +++ b/spec/sign.typ @@ -0,0 +1,42 @@ +#import "/book.typ": book-page +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table + + +#let config = load_config() +#let chip = load_chip("src/sign.toml", config) +#show: book-page(chip.name) + +#let sign = raw(chip.name) + +#let highlighted_code(code) = { + box( + inset: (left: 4pt, right: 4pt), + outset: (top: 4pt, bottom: 4pt), + radius: 2pt, + fill: luma(230), + raw(code)) +} + +#sign is a constraint template that is used to extract a `Half`word's sign. + +== Interface +The #sign constraint template has the following interface: +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("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 operates on three variables: +#render_chip_column_table(chip, config) + +== Assumptions +The #sign template operates on the following assumptions: +#render_chip_assumptions(chip, config) + +== 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/src/sign.toml b/spec/src/sign.toml new file mode 100644 index 000000000..ca799e0cc --- /dev/null +++ b/spec/src/sign.toml @@ -0,0 +1,42 @@ +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_HALF[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" + From 485b93ff81d752115387790688220195fcdcd907 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:57:16 +0100 Subject: [PATCH 46/78] spec: drop `IsZero` template (#278) --- spec/cpu.typ | 5 ++--- spec/src/shift.toml | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/cpu.typ b/spec/cpu.typ index 0afa75f62..9036593ac 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -76,9 +76,8 @@ including the appropriate sign/zero extension, depending on `word_instr`. #render_constraint_table(chip, config, groups: "ext") === Other constraints - -#rj[proper ref to IsZero/IsEqual] -For @cpu:c:is_equal, refer to the logic of IsZero or IsEqual, in combination with the subtraction of @cpu:c:sub. +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") diff --git a/spec/src/shift.toml b/spec/src/shift.toml index 4b7044e7d..591efb839 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -198,8 +198,8 @@ ref = "shift:c:bit_shift_if_right" multiplicity = "right" [[constraints.bit_shift]] -kind = "template" -tag = "IsZero" +kind = "interaction" +tag = "ZERO" input = ["bit_shift"] output = "zbs" ref = "shift:c:zbs" From 9bb2eed3c2d636db21dcad84dacf04bbe028928f Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:03:05 +0100 Subject: [PATCH 47/78] spec: fix header levels (#264) * spec: offset headers in PDF * spec: decrement header levels for chip descriptions * spec: move heading offset to ebook.typ --- spec/add.typ | 10 +++++----- spec/bitwise.typ | 6 +++--- spec/branch.typ | 8 ++++---- spec/cpu.typ | 20 ++++++++++---------- spec/decode.typ | 10 +++++----- spec/ebook.typ | 1 + spec/ecall.typ | 12 ++++++------ spec/is_bit.typ | 8 ++++---- spec/load.typ | 8 ++++---- spec/lt.typ | 8 ++++---- spec/memory.typ | 18 +++++++++--------- spec/memw.typ | 8 ++++---- spec/mul.typ | 18 +++++++++--------- spec/shift.typ | 24 ++++++++++++------------ spec/sign.typ | 8 ++++---- 15 files changed, 84 insertions(+), 83 deletions(-) diff --git a/spec/add.typ b/spec/add.typ index 981a0ccb1..241ea8621 100644 --- a/spec/add.typ +++ b/spec/add.typ @@ -20,14 +20,14 @@ #add is a constraint template that is used to assert that $#`sum` = #`lhs` + #`rhs` mod 2^64$, under the condition that `cond` is non-zero. -== Notation += Notation The #add constraint template has the following interface: #block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => ADD")) where `cond` is any value described by an expression _of degree at most $1$_. #highlighted_code("ADD") can be used to denote the _unconditional_ application of the #add template to `lhs`, `rhs`, and `sum`. #let sub = raw("SUB") -=== #sub +== #sub For ease of notation, we moreover introduce the #sub constraint template. Its interface #block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => SUB")) @@ -36,12 +36,12 @@ maps onto the #add template as It constrains that $#`diff` = #`lhs` - #`rhs` mod 2^64$ when the expression `cond` is non-zero. As with #add, #highlighted_code("SUB") can be used to denote the _unconditional_ application of the template. -== Variables += Variables #render_chip_column_table(chip, config) -== Assumptions += Assumptions #render_chip_assumptions(chip, config) -== Constraints += Constraints This template introduces the following constraints #render_constraint_table(chip, config) diff --git a/spec/bitwise.typ b/spec/bitwise.typ index 9b5b4a638..36fe3b6e0 100644 --- a/spec/bitwise.typ +++ b/spec/bitwise.typ @@ -15,7 +15,7 @@ #show: book-page(chip.name) -== Columns += Columns #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() @@ -27,11 +27,11 @@ Of these, the _input_ and _output_ variables (#nr_precomputed in total) are prec *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 += Lookup This chip adds the following interactions to the lookup: #render_constraint_table(chip, config) -== Areas of Optimization += Areas of Optimization 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]`. diff --git a/spec/branch.typ b/spec/branch.typ index f448f2da4..3e944ca63 100644 --- a/spec/branch.typ +++ b/spec/branch.typ @@ -14,18 +14,18 @@ #show: book-page(chip.name) -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The `BRANCH` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: #render_chip_column_table(chip, config) -== Assumptions += Assumptions #render_chip_assumptions(chip, config) -== Constraints += Constraints #rj[Check correspondence with CPU for passing in `offset` as word or dword] We constrain `next_pc` to be $#`base_address` + #`offset`$, @@ -37,7 +37,7 @@ The range checks on `unmasked_low_byte` and `next_pc_low[0]` are performed impli This chip contributes the following to the lookup argument. #render_constraint_table(chip, config, groups: "output") -== Padding += Padding The table can be padded to the next power of two with the following value assignments: diff --git a/spec/cpu.typ b/spec/cpu.typ index 9036593ac..ed6126388 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -14,24 +14,24 @@ #show: book-page(chip.name) -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The `CPU` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: #render_chip_column_table(chip, config) -== Assumptions += Assumptions #render_chip_assumptions(chip, config) -== Constraints += Constraints First, we perform a decoding lookup for the current PC. #render_constraint_table(chip, config, groups: "decode") #rj[All casts for interactions will have to be reviewed once other chip interfaces stabilise] -=== Range checks +== 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, @@ -46,13 +46,13 @@ The ranges of the other auxiliary columns are enforced through later constraints #render_constraint_table(chip, config, groups: "range") -=== ALU +== ALU The ALU functionality is then obtained through judicious dispatching to the corresponding chips. #render_constraint_table(chip, config, groups: "alu") -=== Memory +== 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. @@ -62,20 +62,20 @@ to ensure the access is disjoint with the `pc` read into `rv1` as part of the `A #render_constraint_table(chip, config, groups: "mem") -=== System +== System The interactions with the wider system. #render_constraint_table(chip, config, groups: "sys") -=== Input and output to the ALU +== 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 +== 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. @@ -83,7 +83,7 @@ Given that this difference is $0$ when both are equal, @cpu:c:is_equal ensures ` #rj[Document the choice to not have a multiplicity column here for padding] -== 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. diff --git a/spec/decode.typ b/spec/decode.typ index 586625226..87f6083f5 100644 --- a/spec/decode.typ +++ b/spec/decode.typ @@ -21,14 +21,14 @@ 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. -== Columns += Columns #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_column_table(chip, config) -== Padding += 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: @@ -39,7 +39,7 @@ Given that `CPU` asserts that `EBREAK = 0` (see @cpu:c:ebreak_traps), using this 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 += 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. @@ -64,7 +64,7 @@ For the purpose of brevity and readability, the table uses the following rules-o Further clarification is provided in the notes following the table. -=== C-type instructions +== 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. @@ -159,7 +159,7 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is [#figure(kind: "note", supplement: [], [#note]) #label(lbl)] } -==== Notes +== Notes We note the following about the above decoding table: #enum(numbering: "[1]", enum.item( diff --git a/spec/ebook.typ b/spec/ebook.typ index 835751163..f9ba76046 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -15,5 +15,6 @@ #meta.summary.map(((ch, title, ref)) => [ #pagebreak(weak: true) #heading(supplement: [Chapter], level: 1, title)#ref + #set heading(offset: 1) #include ch ]).join() diff --git a/spec/ecall.typ b/spec/ecall.typ index 9f2d96048..6908f768b 100644 --- a/spec/ecall.typ +++ b/spec/ecall.typ @@ -16,20 +16,20 @@ #let config = load_config() #let chip = load_chip("src/halt.toml", config) #let halt = raw(chip.name) -== #halt chip += #halt chip -=== Columns +== Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The #halt chip leverages #nr_variables variable, spanning #nr_columns columns: #render_chip_column_table(chip, config) -=== Assumptions +== Assumptions It is assumed the input is range checked: #render_chip_assumptions(chip, config) -=== Constraints +== 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 @@ -44,13 +44,13 @@ This prevents any other operation involving memory from being executed hereafter Alternatively, one could add 31 lookups to the "memory" table to remove the _known_ final tokens for the registers there. ]) -==== Lookup +=== Lookup The HALT chip contributes the following interaction to the lookup-argument: #render_constraint_table(chip, config, groups: "lookup") *Note*: #link("https://github.com/riscv-collab/riscv-gnu-toolchain/blob/master/linux-headers/include/asm-generic/unistd.h#L258")[$93$ is the system call number corresponding to `sys_exit`.] -=== Padding +== 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 index a12d62108..33477d377 100644 --- a/spec/is_bit.typ +++ b/spec/is_bit.typ @@ -21,17 +21,17 @@ #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. -== Interface += Interface The #is_bit constraint template has the following interface: #block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => IS_BIT")) where `cond` is any value described by an expression _of degree at most $1$_. Note that #highlighted_code("IS_BIT") can be used to denote the _unconditional_ application of the #is_bit template to `X`. -== Variables += Variables The #is_bit template operates on two variables: `cond` and `X`: #render_chip_column_table(chip, config) -== Constraints += 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*: @@ -39,7 +39,7 @@ It takes only one constraint to enforce that `X` must be either $0$ or $1$ whene - 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. -== Proof of correctness += Proof of correctness 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$. diff --git a/spec/load.typ b/spec/load.typ index 71c274d1b..bccb830f8 100644 --- a/spec/load.typ +++ b/spec/load.typ @@ -14,17 +14,17 @@ #show: book-page(chip.name) -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The `LOAD` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: #render_chip_column_table(chip, config) -== Assumptions += Assumptions #render_chip_assumptions(chip, config) -== Constraints += 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. @@ -35,7 +35,7 @@ The chip contributes the following to the lookup argument. #render_constraint_table(chip, config, groups: "output") -== Padding += Padding The table can be padded to the next power of two with the following value assignments: diff --git a/spec/lt.typ b/spec/lt.typ index ea36eb4dc..3447efd70 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -14,18 +14,18 @@ #show: book-page(chip.name) -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The `LT` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: #render_chip_column_table(chip, config) -== Assumptions += Assumptions We assume the inputs `lhs`, `rhs` and `signed` are partially range checked. #render_chip_assumptions(chip, config) -== Constraints += 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`. @@ -80,7 +80,7 @@ The chip contributes the following to the lookup argument. #render_constraint_table(chip, config, groups: "output") -== Padding += Padding The table can be padded to the next power of two with the following value assignments: diff --git a/spec/memory.typ b/spec/memory.typ index ec8735e49..1fcb7b54e 100644 --- a/spec/memory.typ +++ b/spec/memory.typ @@ -31,7 +31,7 @@ The initialization and finalization schemes together ensure both that (1) the ne 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 += 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 @@ -43,7 +43,7 @@ While there are some subsystems that can be modelled as read-only memory 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 += Memory operations Every memory operation has some conceptual attributes that are relevant to mention or discuss: @@ -75,7 +75,7 @@ For simplicity, we will always reserve a timestamp for every possible memory acc ] -== Permutation argument += 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"))$, @@ -99,7 +99,7 @@ this "balancing" act of tokens can be integrated (with sufficient domain separat consuming a token corresponds to a "receive" and emitting a new token is a "send". #rj[properly link/refer to the logup spec] -== Temporal integrity += 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. @@ -125,7 +125,7 @@ We choose to represent timestamps as machine words, using the existing `LT` chip #rj[reference to CPU chip/timestamp column and MEMW chip] -== Initialization and Finalization += 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 --- @@ -159,7 +159,7 @@ For each such table, the `page` variable is instantiated as the constant base ad 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 +== Page initialization #rj[check whether we need `fini` to be range-checked] We present here a set of constraints on the `PAGE` table that @@ -211,7 +211,7 @@ and hence doesn't need a column, nor a range check. Most programs and compilers should however favor a memory locality that makes paged initialization/finalization comparable. ] -=== Register initialization/finalization +== Register initialization/finalization #rj[Properly link/reference ECALL/HALT chip] The initial and final state of registers can be entirely known by @@ -221,11 +221,11 @@ by the HALT 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 += 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 += Future topics of interest - Optimize memory systems after determining factual bottlenecks (e.g. taking inspiration from Twist and Shout, or other recent research) diff --git a/spec/memw.typ b/spec/memw.typ index 77b786bf6..a3dfda42c 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -13,14 +13,14 @@ #show: book-page(chip.name) -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The `MEMW` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: #render_chip_column_table(chip, config) -== Assumptions += Assumptions #render_chip_assumptions(chip, config) @@ -29,7 +29,7 @@ as these are not necessary for the correctness of this chip in isolation. 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 += Constraints #render_constraint_table(chip, config, groups: "consistency") @@ -50,7 +50,7 @@ This chip contributes the following to the lookup argument. #render_constraint_table(chip, config, groups: "output") -== Future optimization ideas += Future optimization ideas - Fast path for aligned memory access where all bytes have the same old timestamp - MEMB chip that deals does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) diff --git a/spec/mul.typ b/spec/mul.typ index b2fb53d92..a2fb7d1fc 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -16,7 +16,7 @@ #let mul = raw(chip.name) -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) @@ -27,12 +27,12 @@ The `MUL` chip is comprised of #nr_variables variables that are expressed using $mat(delim: #none, top; bottom)$ } -== Assumptions += Assumptions The following range checks are assumed to be performed/enforced outside of this chip: #render_chip_assumptions(chip, config) -== Constraints -=== Overview += 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. @@ -79,25 +79,25 @@ However, there is some slack in how tight one has to constrain the `carry` value 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 +== 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 +== 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 +== Lookup The #mul chip contributes the following to the lookup: #render_constraint_table(chip, config, groups: "lookup") -== Padding += Padding The table can be padded to the next power of two with the following value assignments: #render_chip_padding_table(chip, config) -== Notes += Notes - `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. diff --git a/spec/shift.typ b/spec/shift.typ index 177ce6104..a2a3ec968 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -16,7 +16,7 @@ #show: book-page(chip.name) -== Interface += Interface The #shift chip has the following interface: #block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(240), ``` @@ -46,17 +46,17 @@ $ $ Here, `<<` and `>>` denote the _logical_ left and right shift operations, while `>>>` denotes the _arithmetic_ right shift operation. -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) The `SHIFT` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: #render_chip_column_table(chip, config) -== Assumptions += Assumptions #render_chip_assumptions(chip, config) -== Explanation += 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. @@ -70,7 +70,7 @@ The output variable `out` is equivalent to `shifted`, but expressed using `Word` 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 +== First phase We zoom in on the first step. Here, we make use of the two lookup operations - $#`HWSL[x: Half, y: B4]` := (#`x` #`<<` #`y`) mod 2^16$ (short for "HalfWord Shift Left"), and @@ -106,7 +106,7 @@ $ it only takes some rearranging and combining of the values $#`X[`i#`] := HWSL[in[`i#`], bit_shift]`$ and $#`Y[`i#`] := HWSLC[in[`i#`], bit_shift]`$ 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 +== 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$. @@ -114,13 +114,13 @@ 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 +== 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 += 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$. @@ -134,7 +134,7 @@ 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 +== 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]$, @@ -164,16 +164,16 @@ 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 +== 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 +== Lookups This chip adds the following interaction to the lookup. #render_constraint_table(chip, config, groups: "lookups") -== Padding += Padding The table can be padded to the next power of two with the following value assignments: diff --git a/spec/sign.typ b/spec/sign.typ index 6f8993f53..dcc941e47 100644 --- a/spec/sign.typ +++ b/spec/sign.typ @@ -20,20 +20,20 @@ #sign is a constraint template that is used to extract a `Half`word's sign. -== Interface += Interface The #sign constraint template has the following interface: #block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("SIGN")) It constrains that `sign` is set to `1` when both `X`'s most significant bit and `signed` are $1$, and $0$ otherwise. -== Variables += Variables The #sign template operates on three variables: #render_chip_column_table(chip, config) -== Assumptions += Assumptions The #sign template operates on the following assumptions: #render_chip_assumptions(chip, config) -== Constraints += 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. From ed3d8839aabfe5f5416eb3c069d3f7e7c9a9e510 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:25:03 +0100 Subject: [PATCH 48/78] spec: LOAD: fix LOAD-C9 signature (#284) --- spec/src/load.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/src/load.toml b/spec/src/load.toml index fcbd2b87f..f8a974c9a 100644 --- a/spec/src/load.toml +++ b/spec/src/load.toml @@ -155,6 +155,6 @@ name = "output" [[constraints.output]] kind = "interaction" tag = "LOAD" -input = ["base_address", "timestamp", "read2", "read4", "read8"] +input = ["base_address", "timestamp", "read2", "read4", "read8", "signed"] output = ["cast", "res", "DWordWL"] multiplicity = ["-", "μ"] From 76a008c681b23ca84a9ac1ac33ebd878cb4cc1a3 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:52:38 +0100 Subject: [PATCH 49/78] spec: `NEG` template (#270) * spec: tweak code-rendering "not" * spec: introduce NEG template * Update spec/book.typ Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * spec: update NEG * spec: NEG: refactor * spec: NEG: fix range-assumption on x * spec: NEG: update cond * spec: tweak math-rendering "not" Analogous to 801f5ee9 * spec: NEG: add non-zero x case distinction --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- spec/book.typ | 1 + spec/expr.typ | 4 +-- spec/neg.typ | 78 +++++++++++++++++++++++++++++++++++++++++++++++ spec/src/neg.toml | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 spec/neg.typ create mode 100644 spec/src/neg.toml diff --git a/spec/book.typ b/spec/book.typ index 2fd6d8767..64d78e15a 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -12,6 +12,7 @@ ("is_bit.typ", [IS_BIT template], ), ("sign.typ", [SIGN template], ), ("add.typ", [ADD/SUB template], ), + ("neg.typ", [NEG template], ), ("decode.typ", [DECODE table], ), ("cpu.typ", [CPU chip], ), ("shift.typ", [SHIFT chip], ), diff --git a/spec/expr.typ b/spec/expr.typ index a0530525b..1c6c7942e 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -92,7 +92,7 @@ #let expr_to_code = make_expr_formatter( ( "idx": (pp, rec, e) => rec(PREC.MIN, e.at(1)) + `[` + rec(PREC.MAX, e.at(2)) + `]`, - "not": (pp, rec, e) => cwrap(`1 - ` + rec(PREC.not, e.at(1)), pp < PREC.not), + "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) => { @@ -153,7 +153,7 @@ let (val, idxs) = flat_idxs(e) $#rec(PREC.idx, val)_(#idxs.map(idx => rec(PREC.idx, idx)).join($, $))$ }, - "not": (pp, rec, e) => mwrap($1 - #rec(PREC.not, e.at(1))$, pp < PREC.not), + "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)) diff --git a/spec/neg.typ b/spec/neg.typ new file mode 100644 index 000000000..ac8554689 --- /dev/null +++ b/spec/neg.typ @@ -0,0 +1,78 @@ +#import "/book.typ": book-page, et +#import "/src.typ": load_config, load_chip +#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table + +#let config = load_config() +#let chip = load_chip("src/neg.toml", config) +#show: book-page(chip.name) + +#let neg = raw(chip.name) + +#let highlighted_code(code) = { + box( + inset: (left: 4pt, right: 4pt), + outset: (top: 4pt, bottom: 4pt), + radius: 2pt, + fill: luma(230), + raw(code)) +} + +#neg is a constraint template that is used to assert that $#`neg` = -#`x`$, under the condition that `cond` is non-zero. + += Notation +The #neg constraint template has the following interface: +#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => NEG")) +where `cond` is a bit value (i.e., lies in ${0, 1}$) described by an expression _of degree at most $1$_. +#highlighted_code("NEG") can be used to denote the _unconditional_ application of the #neg template to `x` and `neg` (which is equivalent to $#`cond` = 1$). + += Variables +#render_chip_column_table(chip, config) + += Assumptions +#render_chip_assumptions(chip, config) + += Constraints +We constrain this equality using two constraints: +#render_constraint_table(chip, config) +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. + += Note +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/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" From f86e427129f06e7e51546795fffdea95d34062ed Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Wed, 24 Dec 2025 15:06:52 +0100 Subject: [PATCH 50/78] spec: Introduce DVRM chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec: DVRM: introduce `μ_sum` spec: DVRM: apply SIGN template spec: DVRM: fix `n_sub_r_is_negative` spec: DVRM: range check `n_sub_r` spec: DVRM: add missing LT constraint spec: DVRM: add missing abs_* range checks required by SUB calls. spec: DVRM: fix LT lookup spec: support variable labelling spec: DVRM: completely refactor DVRM chip spec: DVRM: make multiplicities binary spec: DVRM: spec padding spec: DVRM: remove superfluous TODOs spec: DVRM: drop msb lookup for `sign_r` spec: DVRM: replace `range=` by `iter=` spec: DVRM: replace range assumptions for q and r by constraints Apply suggestions from code review Co-authored-by: Robin Jadoul spec: DVRM: drop bit checks for multiplicities spec:DVRM: complete refactor spec: DVRM: update padding spec: DVRM: fix minor discrepancy spec: DVRM: drop superfluous `q_if_overflow` spec: DVRM: fix typos spec: DVRM: fix casting spec: ZERO: expand lookup to B20 spec: DVRM: abandon `IsZero` and `IsEqual` templates spec: DVRM: fix typo spec: expr: update constant rendering in expr_to_math Update spec/bitwise.typ Co-authored-by: Robin Jadoul spec: DVRM: replace [Half, x] by xHL spec: DVRM: use QuadHL-sub to constrain `extended_n_sub_r` spec: drop support variable labelling This reverts commit c8d68968d58ade883fc5fee1118fbd1957af5a3a (and removes a bit more). spec: DVRM: fix dvrm:c:div_by_zero Update spec/dvrm.typ Co-authored-by: Robin Jadoul --- spec/bitwise.typ | 2 +- spec/chip.typ | 2 +- spec/dvrm.typ | 135 +++++++++++++- spec/src/bitwise.toml | 4 +- spec/src/dvrm.toml | 404 ++++++++++++++++++++++++++++++++++++++++++ spec/src/mul.toml | 3 +- 6 files changed, 541 insertions(+), 9 deletions(-) create mode 100644 spec/src/dvrm.toml diff --git a/spec/bitwise.typ b/spec/bitwise.typ index 36fe3b6e0..ef1e3a671 100644 --- a/spec/bitwise.typ +++ b/spec/bitwise.typ @@ -38,5 +38,5 @@ The following ideas may prove to be optimizations for the #bitwise chip: + 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`, `ZERO`, etc.) and 20-bit (`HWSL`, `HWSLC`, `IS_B20`) lookups in separate tables. ++ Place the 16-bit (`AND`, `OR`, `XOR`, `MSB16`, etc.) and 20-bit (`HWSL`, `HWSLC`, `IS_B20`, `ZERO`) lookups in separate tables. + Combine `HWSL` and `HWSLC` into a single lookup (see also \#119). diff --git a/spec/chip.typ b/spec/chip.typ index 8e2c4ac33..10479943e 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -156,7 +156,7 @@ (table.header(level:2, table.cell(colspan: 4, emph(cat))), table.hline(stroke: .6pt)) for var in vars { ( - [#raw(var.name)], + [#raw(var.name)], [#type_to_code(var.type)], table.cell(colspan: 2, [#eval(var.desc, mode: "markup")]) ) diff --git a/spec/dvrm.typ b/spec/dvrm.typ index f1d9a3a4c..e68f4bee8 100644 --- a/spec/dvrm.typ +++ b/spec/dvrm.typ @@ -5,13 +5,140 @@ total_nr_variables, total_nr_instantiated_columns, render_constraint_table, - render_chip_assumptions, render_chip_padding_table, + render_chip_assumptions ) + #let config = load_config() -// #let chip = load_chip("src/dvrm.toml", config) +#let chip = load_chip("src/dvrm.toml", config) + +#show: book-page(chip.name) + +#let dvrm = raw(chip.name) + += Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The `DVRM` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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`). +@dvrm:c:abs_r_range_check and @dvrm:c:abs_d_range_check are required to uphold assumption @add:a:lhs required by the `SUB` chip. + +#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", )) -#show: book-page("dvrm.typ") +== Output +Lastly, this chip contributes the following to the lookup: +#render_constraint_table(chip, config, groups:("output", )) -*placeholder chapter: WIP* += 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/src/bitwise.toml b/spec/src/bitwise.toml index 2eeec4059..9b4a3f951 100644 --- a/spec/src/bitwise.toml +++ b/spec/src/bitwise.toml @@ -51,7 +51,7 @@ precomputed = "true" [[variables.output]] name = "ZERO" type = "Bit" -desc = "whether $#`X` = 0 and #`Y` = 0$" +desc = "whether $#`X` = 0$, $#`Y` = 0$ and $#`Z` = 0$." precomputed = "true" [[variables.output]] @@ -163,7 +163,7 @@ multiplicity = ["-", "μ_MSB16"] [[constraints.contributions]] kind = "interaction" tag = "ZERO" -input = [["+", "X", ["*", 256, "Y"]]] +input = [["+", "X", ["*", 256, "Y"], ["*", 65536, "Z"]]] output = "ZERO" multiplicity = ["-", "μ_ZERO"] diff --git a/spec/src/dvrm.toml b/spec/src/dvrm.toml new file mode 100644 index 000000000..ceeabf1e2 --- /dev/null +++ b/spec/src/dvrm.toml @@ -0,0 +1,404 @@ +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 = "DWordHL" +desc = "Absolute value of `r`." +pad = 0 + +[[variables.auxiliary]] +name = "abs_d" +type = "DWordHL" +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 = [["cast", "abs_r", "DWordWL"], ["cast", "abs_d", "DWordWL"], 0] +output = ["not", "div_by_zero"] +multiplicity = "μ_sum" +ref ="dvrm:c:abs_r_lt_abs_d" + +[[constraints.abs_diff]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "abs_r", "i"]] +iter = ["i", 0, 3] +multiplicity = "sign_r" +ref = "dvrm:c:abs_r_range_check" + +[[constraints.abs_diff]] +kind = "template" +tag = "SUB" +input = [0, ["cast", "r", "DWordWL"]] +output = ["cast", "abs_r", "DWordWL"] +cond = "sign_r" +ref = "dvrm:c:abs_r_if_negative" + +[[constraints.abs_diff]] +kind = "arith" +constraint = "$not#`sign_r` => #`abs_r[i]`=#`r[i]`$" +iter = ["i", 0, 3] +poly = ["*", ["-", 1, "sign_r"], ["-", ["idx", "abs_r", "i"], ["idx", "r", "i"]]] +ref = "dvrm:c:abs_r_if_nonnegative" + +[[constraints.abs_diff]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", "abs_d", "i"]] +iter = ["i", 0, 3] +multiplicity = "sign_d" +ref = "dvrm:c:abs_d_range_check" + +[[constraints.abs_diff]] +kind = "template" +tag = "SUB" +input = [0, ["cast", "d", "DWordWL"]] +output = ["cast", "abs_d", "DWordWL"] +cond = "sign_d" +ref = "dvrm:c:abs_d_if_negative" + +[[constraints.abs_diff]] +kind = "arith" +constraint = "$not#`sign_d` => #`abs_d[i]`=#`d[i]`$" +iter = ["i", 0, 3] +poly = ["*", ["-", 1, "sign_d"], ["-", ["idx", "abs_d", "i"], ["idx", "d", "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" \ No newline at end of file diff --git a/spec/src/mul.toml b/spec/src/mul.toml index e987b0f75..238bfe01f 100644 --- a/spec/src/mul.toml +++ b/spec/src/mul.toml @@ -130,6 +130,7 @@ iter = ["i", 0, 3] [[assumptions]] desc = "`IS_HALF[rhs[i]]`" iter = ["i", 0, 3] +ref = "mul:a:rhs" # Constraints @@ -202,4 +203,4 @@ tag = "MUL" input = ["lhs", "lhs_signed", "rhs", "rhs_signed", "1"] output = ["cast", "hi", "DWordWL"] multiplicity = ["-", "μ_hi"] -ref = "mul:c:lookup_hi" \ No newline at end of file +ref = "mul:c:lookup_hi" From 8f0e8d3c0a3389d982766862e35c4442c5dd599c Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:26:55 +0100 Subject: [PATCH 51/78] spec: signatures (#280) * spec: list all interaction signatures * Update spec/signatures.typ Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * spec: signatures: fix LOAD signature * spec: signatures: make IS_BIT's cond a BaseField * spec: signatures: make ECALL's syscallnr a DWordWL * spec: signatures: preemptively introduce NEG signature (see #270) * spec: signatures: fix DWordDL typo Co-authored-by: Robin Jadoul --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Robin Jadoul --- spec/book.typ | 1 + spec/signatures.typ | 90 ++++++++++++++++++++ spec/src.typ | 51 +++++++++++ spec/src/signatures.toml | 178 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 spec/signatures.typ create mode 100644 spec/src/signatures.toml diff --git a/spec/book.typ b/spec/book.typ index 64d78e15a..076d31cf3 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -9,6 +9,7 @@ summary: ( ("memory.typ", [Memory argument], ), ("variables.typ", [Variables], ), + ("signatures.typ", [Signatures], ), ("is_bit.typ", [IS_BIT template], ), ("sign.typ", [SIGN template], ), ("add.typ", [ADD/SUB template], ), diff --git a/spec/signatures.typ b/spec/signatures.typ new file mode 100644 index 000000000..5673cdcf6 --- /dev/null +++ b/spec/signatures.typ @@ -0,0 +1,90 @@ +#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)]) + }, + ), + caption: "Signature overview of interactions", +) + +#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)], ) + }, + ), + caption: "Signature overview of templates", +) diff --git a/spec/src.typ b/spec/src.typ index 8200b47c1..6328c4665 100644 --- a/spec/src.typ +++ b/spec/src.typ @@ -1,5 +1,7 @@ /// 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) = { @@ -31,6 +33,55 @@ 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 diff --git a/spec/src/signatures.toml b/spec/src/signatures.toml new file mode 100644 index 000000000..ba233f1e6 --- /dev/null +++ b/spec/src/signatures.toml @@ -0,0 +1,178 @@ +# 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", "Word", "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"] + +# 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" + +# HWSLC[res; X, shift] +[[signatures]] +tag = "HWSLC" +kind = "interaction" +input = ["Half", "B4"] +output = "Half" \ No newline at end of file From 1b2cfb99b35b0678b86ec42ccddd878279ae24ce Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:31:42 +0100 Subject: [PATCH 52/78] spec: Leverage `NEG` in `DVRM` (#287) * spec: DVRM: use NEG template for abs_r and abs_d This saves 4 columns. * Apply suggestions from @RobinJadoul Co-authored-by: Robin Jadoul --------- Co-authored-by: Robin Jadoul --- spec/dvrm.typ | 1 - spec/src/dvrm.toml | 46 +++++++++++++++------------------------------- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/spec/dvrm.typ b/spec/dvrm.typ index e68f4bee8..54e71d771 100644 --- a/spec/dvrm.typ +++ b/spec/dvrm.typ @@ -67,7 +67,6 @@ Focusing on the first statement, we observe that this trivially holds when $#`si 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`). -@dvrm:c:abs_r_range_check and @dvrm:c:abs_d_range_check are required to uphold assumption @add:a:lhs required by the `SUB` chip. #render_constraint_table(chip, config, groups:("abs_diff", )) diff --git a/spec/src/dvrm.toml b/spec/src/dvrm.toml index ceeabf1e2..d93449228 100644 --- a/spec/src/dvrm.toml +++ b/spec/src/dvrm.toml @@ -51,13 +51,13 @@ pad = 0 [[variables.auxiliary]] name = "abs_r" -type = "DWordHL" +type = "DWordWL" desc = "Absolute value of `r`." pad = 0 [[variables.auxiliary]] name = "abs_d" -type = "DWordHL" +type = "DWordWL" desc = "Absolute value of `d`." pad = 0 @@ -215,55 +215,39 @@ name = "abs_diff" [[constraints.abs_diff]] kind = "interaction" tag = "LT" -input = [["cast", "abs_r", "DWordWL"], ["cast", "abs_d", "DWordWL"], 0] +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 = "interaction" -tag = "IS_HALF" -input = [["idx", "abs_r", "i"]] -iter = ["i", 0, 3] -multiplicity = "sign_r" -ref = "dvrm:c:abs_r_range_check" - [[constraints.abs_diff]] kind = "template" -tag = "SUB" -input = [0, ["cast", "r", "DWordWL"]] -output = ["cast", "abs_r", "DWordWL"] +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[i]`=#`r[i]`$" -iter = ["i", 0, 3] -poly = ["*", ["-", 1, "sign_r"], ["-", ["idx", "abs_r", "i"], ["idx", "r", "i"]]] +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 = "interaction" -tag = "IS_HALF" -input = [["idx", "abs_d", "i"]] -iter = ["i", 0, 3] -multiplicity = "sign_d" -ref = "dvrm:c:abs_d_range_check" - [[constraints.abs_diff]] kind = "template" -tag = "SUB" -input = [0, ["cast", "d", "DWordWL"]] -output = ["cast", "abs_d", "DWordWL"] +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[i]`=#`d[i]`$" -iter = ["i", 0, 3] -poly = ["*", ["-", 1, "sign_d"], ["-", ["idx", "abs_d", "i"], ["idx", "d", "i"]]] +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]] From 742a5bdb4ee109fbdb74ebdb73ab7b934478907a Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 10 Feb 2026 15:58:39 +0100 Subject: [PATCH 53/78] spec: Add initial tooling to check data formats, prepare for more elaborate type checking (#271) * spec: Add initial tooling to check data formats, prepare for more elaborate type checking * Initial type checking * ruff format * Update some more typing mismatches * Move to range-based type checks * Avoid casting to more limbs by leveraging scalar-array mult and literal casts * toml fixes to pass type checks * Type check virtual definitions properly now * ruff format * Make typst compile by turning big range values to string * Switch some isinstance checks around to make both mypy and ty work * Fix issues after rebasing on spec/main * Address review comments * Review comments * lit -> const --- spec/chip.typ | 1 - spec/memory.typ | 1 + spec/src/bitwise.toml | 24 +- spec/src/branch.toml | 8 +- spec/src/config.toml | 48 +- spec/src/cpu.toml | 25 +- spec/src/dvrm.toml | 8 +- spec/src/is_bit.toml | 2 +- spec/src/lt.toml | 2 +- spec/src/memw.toml | 29 +- spec/src/mul.toml | 6 +- spec/src/page.toml | 10 +- spec/src/shift.toml | 2 +- spec/tooling/chip.py | 988 ++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1082 insertions(+), 72 deletions(-) create mode 100644 spec/tooling/chip.py diff --git a/spec/chip.typ b/spec/chip.typ index 10479943e..4749b886e 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -113,7 +113,6 @@ } if "poly" in def { - // assert(false, message: repr(index_all(var_name, gather_indices(def)))) ( [], table.cell(align: right, emph[definition]), diff --git a/spec/memory.typ b/spec/memory.typ index 1fcb7b54e..62059de37 100644 --- a/spec/memory.typ +++ b/spec/memory.typ @@ -229,3 +229,4 @@ add the required balancing terms to the LogUp sum. = 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/src/bitwise.toml b/spec/src/bitwise.toml index 9b4a3f951..75e8faee4 100644 --- a/spec/src/bitwise.toml +++ b/spec/src/bitwise.toml @@ -4,67 +4,67 @@ name = "BITWISE" name = "X" type = "Byte" desc = "" -precomputed = "true" +precomputed = true [[variables.input]] name = "Y" type = "Byte" desc = "" -precomputed = "true" +precomputed = true [[variables.input]] name = "Z" type = "B4" desc = "" -precomputed = "true" +precomputed = true [[variables.output]] name = "AND" type = "Byte" desc = "the binary AND of `X` and `Y`" -precomputed = "true" +precomputed = true [[variables.output]] name = "OR" type = "Byte" desc = "the binary OR of `X` and `Y`" -precomputed = "true" +precomputed = true [[variables.output]] name = "XOR" type = "Byte" desc = "the binary XOR of `X` and `Y`" -precomputed = "true" +precomputed = true [[variables.output]] name = "MSB8" type = "Bit" desc = "the most significant bit of `X`" -precomputed = "true" +precomputed = true [[variables.output]] name = "MSB16" type = "Bit" desc = "the most significant bit of `Y`" -precomputed = "true" +precomputed = true [[variables.output]] name = "ZERO" type = "Bit" desc = "whether $#`X` = 0$, $#`Y` = 0$ and $#`Z` = 0$." -precomputed = "true" +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" +precomputed = true [[variables.output]] name = "SLLC" type = "Half" desc = "`X||Y` logically right-shifted by `Z`: $(#`X` + 256#`Y`) #`>>` (16 - #`Z`)$" -precomputed = "true" +precomputed = true [[variables.multiplicity]] name = "μ_AND" @@ -197,4 +197,4 @@ kind = "interaction" tag = "HWSLC" input = [["+", "X", ["*", 256, "Y"]], "Z"] output = "SLLC" -multiplicity = ["-", "μ_HWSLC"] \ No newline at end of file +multiplicity = ["-", "μ_HWSLC"] diff --git a/spec/src/branch.toml b/spec/src/branch.toml index e66974c8e..beb3c1922 100644 --- a/spec/src/branch.toml +++ b/spec/src/branch.toml @@ -11,7 +11,7 @@ pad = 0 [[variables.input]] name = "offset" -type = "Word" +type = "DWordWL" desc = "The offset from the base address to jump to" pad = 0 @@ -59,7 +59,7 @@ 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]], ["idx", "unmasked_low_byte", 0]]}, + {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]]}, ]} @@ -124,7 +124,7 @@ multiplicity = "μ" [[constraints.all]] kind = "interaction" tag = "AND_BYTE" -input = [["idx", "unmasked_low_byte", 0], 254] +input = ["unmasked_low_byte", 254] output = ["idx", "next_pc_low", 0] multiplicity = "μ" @@ -145,4 +145,4 @@ kind = "interaction" tag = "BRANCH" input = ["pc", "offset", "register", "JALR"] output = "next_pc" -multiplicity = "-μ" +multiplicity = ["-", "μ"] diff --git a/spec/src/config.toml b/spec/src/config.toml index d836f80e5..0f6ef11d6 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -4,63 +4,49 @@ 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"] -count = 1 +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 = "WordHL" -subtypes = ["Half", "Half"] -desc = """\ - Variable that can only assume values in the range $[0, 2^32)$. \\ - Represented as an array of two `Half` variables.\ - """ - -[[variables.types]] -label = "WordBL" -subtypes = ["Byte", "Byte", "Byte", "Byte"] -desc = """\ - Variable that can only assume values in the range $[0, 2^32)$. \\ - Represented as an array of four `Byte` variables.\ - """ - -[[variables.types]] -label = "B35" -subtypes = ["BaseField"] -desc = "Variable that can only assume values in the range $[0, 2^35)$." - [[variables.types]] label = "B51" subtypes = ["BaseField"] +range = [0, 2251799813685247] desc = "Variable that can only assume values in the range $[0, 2^51)$." [[variables.types]] @@ -96,6 +82,15 @@ desc = """\ 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"] @@ -112,15 +107,6 @@ desc = """\ Represented as an array of four `Word` variables.\ """ -[[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 = "Timestamp" subtypes = ["DWordWL"] diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index c151b6eff..994fda508 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -343,6 +343,7 @@ name = "decode" kind = "interaction" tag = "DECODE" input = ["pc", "imm", "packed_decode"] +multiplicity = 1 [[constraint_groups]] @@ -515,34 +516,40 @@ ref = "cpu:c:range_EBREAK" 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]] @@ -611,7 +618,7 @@ multiplicity = "SHIFT" [[constraints.alu]] kind = "template" tag = "ADD" -input = ["pc", ["cast", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], "DWordWL"]] +input = ["pc", ["*", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], ["cast", 1, "DWordWL"]]] output = ["cast", "res", "DWordWL"] cond = "JALR" @@ -640,7 +647,7 @@ prefix = "M" [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "rs1"], "rv1", ["+", "timestamp", 0], 1, 0, 0] +input = [1, ["*", 2, "rs1"], "rv1", ["+", "timestamp", ["cast", 0, "DWordWL"]], 1, 0, 0] output = "rv1" multiplicity = "read_register1" @@ -654,7 +661,7 @@ iter = ["i", 0, 2] [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "rs2"], "rv2", ["+", "timestamp", 1], 1, 0, 0] +input = [1, ["*", 2, "rs2"], "rv2", ["+", "timestamp", ["cast", 1, "DWordWL"]], 1, 0, 0] output = "rv2" multiplicity = "read_register2" @@ -668,13 +675,13 @@ iter = ["i", 0, 2] [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "rd"], "rvd", ["+", "timestamp", 2], 1, 0, 0] +input = [1, ["*", 2, "rd"], "rvd", ["+", "timestamp", ["cast", 2, "DWordWL"]], 1, 0, 0] multiplicity = "write_register" [[constraints.mem]] kind = "interaction" tag = "LOAD" -input = [0, "res", ["+", "timestamp", 0], "memory_2bytes", "memory_4bytes", "memory_8bytes", "signed"] +input = [0, "res", ["+", "timestamp", ["cast", 0, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes", "signed"] output = "rvd" multiplicity = "LOAD" @@ -682,14 +689,14 @@ multiplicity = "LOAD" [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [0, "res", "rv2", ["+", "timestamp", 1], "memory_2bytes", "memory_4bytes", "memory_8bytes"] +input = [0, "res", "rv2", ["+", "timestamp", ["cast", 1, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes"] multiplicity = "STORE" # TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, 255], "next_pc", ["+", "timestamp", 1], 1, 0, 0] +input = [1, ["*", 2, 255], "next_pc", ["+", "timestamp", ["cast", 1, "DWordWL"]], 1, 0, 0] output = "pc" multiplicity = ["not", "pad"] @@ -795,7 +802,7 @@ poly = ["+", ["-", "branch_cond"], "JALR", ["*", ["idx", "res", 0], ["not", "mp_selector"], "BLT"], - ["*", ["not", ["idx", "res", 0]], "mp_selector", "BLT"], + ["*", ["-", 1, ["idx", "res", 0]], "mp_selector", "BLT"], ["*", "is_equal", ["not", "mp_selector"], "BEQ"], ["*", ["not", "is_equal"], "mp_selector", "BEQ"] ] @@ -810,6 +817,6 @@ multiplicity = "branch_cond" [[constraints.misc]] kind = "template" tag = "ADD" -input = ["pc", ["cast", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_type_instruction"]]], "DWordWL"]] +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/dvrm.toml b/spec/src/dvrm.toml index d93449228..52583907c 100644 --- a/spec/src/dvrm.toml +++ b/spec/src/dvrm.toml @@ -376,13 +376,13 @@ desc = "Each row contributes the following to the LogUp sum" [[constraints.output]] kind = "interaction" tag = "DVRM" -input = ["n", "d", "signed", "0"] +input = ["n", "d", "signed", 0] output = ["cast", "q", "DWordWL"] -multiplicity = "-μ_q" +multiplicity = ["-", "μ_q"] [[constraints.output]] kind = "interaction" tag = "DVRM" -input = ["n", "d", "signed", "1"] +input = ["n", "d", "signed", 1] output = ["cast", "r", "DWordWL"] -multiplicity = "-μ_r" \ No newline at end of file +multiplicity = ["-", "μ_r"] diff --git a/spec/src/is_bit.toml b/spec/src/is_bit.toml index 47e96a27e..a72b5f648 100644 --- a/spec/src/is_bit.toml +++ b/spec/src/is_bit.toml @@ -16,5 +16,5 @@ name = "all" [[constraints.all]] kind = "arith" constraint = "$#`cond` => #`X` (1-#`X`) = 0$" -poly = ["*", "cond", "X", ["not", "X"]] +poly = ["*", "cond", "X", ["-", 1, "X"]] ref = "isbit:c:isbit" diff --git a/spec/src/lt.toml b/spec/src/lt.toml index 10497b637..1941dbb7a 100644 --- a/spec/src/lt.toml +++ b/spec/src/lt.toml @@ -160,4 +160,4 @@ kind = "interaction" tag = "LT" input = [["cast", "lhs", "DWordWL"], ["cast", "rhs", "DWordWL"], "signed"] output = "lt" -multiplicity = "-μ" +multiplicity = ["-", "μ"] diff --git a/spec/src/memw.toml b/spec/src/memw.toml index f7276a9cd..af005c2b4 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -129,7 +129,7 @@ kind = "template" tag = "ADD" input = ["base_address", 1] output = ["cast", ["idx", "address_add", 0], "DWordWL"] -multiplicity = "w2" +cond = "w2" [[constraints.consistency]] kind = "template" @@ -137,7 +137,7 @@ tag = "ADD" input = ["base_address", ["+", "i", 1]] output = ["cast", ["idx", "address_add", "i"], "DWordWL"] iter = ["i", 1, 2] -multiplicity = "w4" +cond = "w4" [[constraints.consistency]] kind = "template" @@ -145,16 +145,37 @@ tag = "ADD" input = ["base_address", ["+", "i", 1]] output = ["cast", ["idx", "address_add", "i"], "DWordWL"] iter = ["i", 3, 6] -multiplicity = "write8" +cond = "write8" + +[[constraints.consistency]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", ["idx", "address_add", "i"], "j"]] +iters = [ + ["i", 0, 0], + ["j", 0, 3], +] +multiplicity = "w2" [[constraints.consistency]] kind = "interaction" tag = "IS_HALFWORD" input = [["idx", ["idx", "address_add", "i"], "j"]] iters = [ - ["i", 0, 6], + ["i", 1, 2], ["j", 0, 3], ] +multiplicity = "w4" + +[[constraints.consistency]] +kind = "interaction" +tag = "IS_HALFWORD" +input = [["idx", ["idx", "address_add", "i"], "j"]] +iters = [ + ["i", 3, 6], + ["j", 0, 3], +] +multiplicity = "write8" [[constraints.consistency]] kind = "interaction" diff --git a/spec/src/mul.toml b/spec/src/mul.toml index 238bfe01f..a798c682d 100644 --- a/spec/src/mul.toml +++ b/spec/src/mul.toml @@ -182,7 +182,7 @@ 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"]] +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" @@ -192,7 +192,7 @@ name = "lookup" [[constraints.lookup]] kind = "interaction" tag = "MUL" -input = ["lhs", "lhs_signed", "rhs", "rhs_signed", "0"] +input = ["lhs", "lhs_signed", "rhs", "rhs_signed", 0] output = ["cast", "lo", "DWordWL"] multiplicity = ["-", "μ_lo"] ref = "mul:c:lookup_lo" @@ -200,7 +200,7 @@ ref = "mul:c:lookup_lo" [[constraints.lookup]] kind = "interaction" tag = "MUL" -input = ["lhs", "lhs_signed", "rhs", "rhs_signed", "1"] +input = ["lhs", "lhs_signed", "rhs", "rhs_signed", 1] output = ["cast", "hi", "DWordWL"] multiplicity = ["-", "μ_hi"] ref = "mul:c:lookup_hi" diff --git a/spec/src/page.toml b/spec/src/page.toml index 8053d63df..21ec76757 100644 --- a/spec/src/page.toml +++ b/spec/src/page.toml @@ -2,6 +2,12 @@ name = "PAGE" # Input +# TODO: add `page` as "constant" column or smth +[[variables.input]] +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" @@ -28,7 +34,7 @@ desc = "The timestamp at which this address was last accessed" 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", ["cast", "offset", "DWordWL"]] +def = ["+", "page", ["*", "offset", ["cast", 1, "DWordWL"]]] [[constraint_groups]] @@ -38,11 +44,13 @@ name = "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" diff --git a/spec/src/shift.toml b/spec/src/shift.toml index 591efb839..bd6c471a6 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -292,5 +292,5 @@ kind = "interaction" tag = "SHIFT" input = ["in", "shift", "direction", "signed", "word_instr"] output = "out" -multiplicity = "-μ" +multiplicity = ["-", "μ"] ref = "shift:c:lookup" diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py new file mode 100644 index 000000000..8a15ae338 --- /dev/null +++ b/spec/tooling/chip.py @@ -0,0 +1,988 @@ +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) + +type Expr = ( + LitExpr + | VarExpr + | 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}", + ) + 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 ["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, 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)) + return cls(tomllib.load(open(filename, "rb"))) + + @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, name=idx) + ) + ] + elif "polys" in data: + idx = data.get("idx", None) + self.defs = [ + PolyWithIters( + build_expr(config, poly["poly"]), iters_of(poly, 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"}) + reporter.asserts("def" in data, f"Missing def for virtual column: {data!r}") + 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 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 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) + + +@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) + + 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] + + +@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) + + 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)) + return cls(config, tomllib.load(open(filename, "rb"))) + + @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) + + +if __name__ == "__main__": + config = Config.from_file(sys.argv[1]) + signatures = sys.argv[2] # Later + 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 not reported: + for chip in chips: + reporter.update_location(f"Chip {chip.name}") + # TODO: do something with the signatures + # Use list for the sideeffect of forcing the generator until we use the content + list(chip.typecheck()) From 26ae8331d7cd6095539744b47e47cd990ce0050a Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 10 Feb 2026 16:43:15 +0100 Subject: [PATCH 54/78] spec: Introduce array expressions (#295) Closes #135 --- spec/chip.typ | 4 ++-- spec/expr.typ | 3 +++ spec/src/branch.toml | 2 +- spec/src/shift.toml | 6 +++--- spec/tooling/chip.py | 3 +++ 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spec/chip.typ b/spec/chip.typ index 4749b886e..f3ac28a74 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -52,9 +52,9 @@ #let render_chip_padding_table(chip, config) = { // Whether `var` is a preprocessed variable. let is_preprocessed(var) = { - config.variables.types + let type = config.variables.types .filter(t => t.label == var.type) - .all(t => t.at("preprocessed", default: false)) + 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() diff --git a/spec/expr.typ b/spec/expr.typ index 1c6c7942e..c4f84eb59 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -28,6 +28,7 @@ // ::= () ; "" // | var ; str(var) // | int ; int +// | ["arr", expr, ...] ; [expr, ...] // | ["idx", expr1, expr2] ; expr1[expr2] // | ["not", expr] ; !expr // | ["+", expr1, expr2, ...] ; expr1 + expr2 + ... @@ -91,6 +92,7 @@ // 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), @@ -149,6 +151,7 @@ // 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($, $))$ diff --git a/spec/src/branch.toml b/spec/src/branch.toml index beb3c1922..34bcdf8cb 100644 --- a/spec/src/branch.toml +++ b/spec/src/branch.toml @@ -34,7 +34,7 @@ pad = 0 name = "next_pc_high" type = ["Half", 3] desc = "The upper part of the next pc" -pad = 0 # TODO(#128): improve handling for arrays +pad = ["arr", 0, 0, 0] [[variables.output]] name = "next_pc_low" diff --git a/spec/src/shift.toml b/spec/src/shift.toml index bd6c471a6..5faed54c7 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -65,19 +65,19 @@ pad = 1 name = "X" type = ["Half", 5] desc = "scratch variable." -pad = 0 # TODO: array +pad = ["arr", 0, 0, 0, 0, 0] [[variables.auxiliary]] name = "Y" type = ["Half", 4] desc = "scratch variable." -pad = 0 # TODO: array +pad = ["arr", 0, 0, 0, 0] [[variables.auxiliary]] name = "limb_shift" type = ["Bit", 4] 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." -pad = 0 # TODO: array +pad = ["arr", 0, 0, 0, 0] # Virtual diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index 8a15ae338..6a78dc091 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -62,6 +62,7 @@ def get_const(self) -> int: type Expr = ( LitExpr | VarExpr + | ArrExpr | IdxExpr | CastExpr | MulExpr @@ -303,6 +304,8 @@ def build_expr(config: Optional["Config"], data: object) -> Expr: 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]: From 9fd3bf813273b7c10053ffeb70e9728c237cf341 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 10 Feb 2026 16:46:19 +0100 Subject: [PATCH 55/78] spec: separate ALU path for STORE to enable byte representation of rv2 to exist in arg2 (#308) * spec: separate ALU path for STORE to enable byte representation of rv2 to exist in arg2 * Apply review suggestion Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> * Update spec/src/cpu.toml Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/src/cpu.toml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 994fda508..283597246 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -559,10 +559,17 @@ prefix = "A" [[constraints.alu]] kind = "template" tag = "ADD" -cond = ["+", "ADD", "LOAD", "STORE"] +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" @@ -689,7 +696,7 @@ multiplicity = "LOAD" [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [0, "res", "rv2", ["+", "timestamp", ["cast", 1, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes"] +input = [0, "res", ["cast", "arg2", ["Byte", 8]], ["+", "timestamp", ["cast", 1, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes"] multiplicity = "STORE" # TODO: no types available, so no casting yet @@ -754,13 +761,13 @@ multiplicity = "word_instr" [[constraints.ext]] kind = "arith" -constraint = "$#`arg2[:4]` = (1 - #`STORE` - #`LOAD`) dot #`rv2[:2]` + (1 - #`BEQ` - #`BLT`) dot #`imm[0]`$" -poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 0], ["*", ["-", 1, "STORE", "LOAD"], ["idx", ["cast", "rv2", "DWordWL"], 0]], ["*", ["-", 1, "BEQ", "BLT"], ["idx", "imm", 0]]] +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 - #`STORE` - #`LOAD`) dot ((1 - #`word_instr`) dot #`rv2[2]` + #`signed` dot #`arg2_sign_bit` dot (2^(32) - 1)) + (1 - #`BEQ` - #`BLT`) dot #`imm[1]`$" -poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 1], ["*", ["-", 1, "STORE", "LOAD"], ["not", "word_instr"], ["idx", "rv2", 2]], ["*", ["-", 1, "STORE", "LOAD"], "signed", "arg2_sign_bit", ["-", ["^", 2, 32], 1]], ["*", ["-", 1, "BEQ", "BLT"], ["idx", "imm", 1]]] +constraint = "$#`arg2[4:]` = (1 - #`LOAD`) dot ((1 - #`word_instr`) dot #`rv2[2]` + #`signed` dot #`arg2_sign_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", "arg2_sign_bit", ["-", ["^", 2, 32], 1]], ["*", ["-", 1, "BEQ", "BLT", "STORE"], ["idx", "imm", 1]]] [[constraints.ext]] kind = "interaction" From 172cf3e12ce5f7a4de43fc29c09f6a9d3a14e1f1 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:39:11 +0100 Subject: [PATCH 56/78] spec: `COMMIT` chip (#283) * spec: update footnote numbering * spec: COMMIT: specify commit chip * spec: COMMIT: fix typos * Move footnote numbering to a more general spot and allow easy future updates * Update common-formatting location * spec: COMMIT: update citation links * spec: COMMIT: deal with committing 0 bytes * spec: COMMIT: list future improvement * Fix typos Co-authored-by: Robin Jadoul * spec: COMMIT: rearrange CNB multiplicity * spec: COMMIT: update padding strategy permitting ADD and SUB constraints of lower degree * spec: COMMIT: list two possible optimizations --------- Co-authored-by: Robin Jadoul --- spec/book.typ | 8 +- spec/ebook.typ | 3 +- spec/ecall.typ | 115 +++++++++++++++++++++- spec/src/commit.toml | 221 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 spec/src/commit.toml diff --git a/spec/book.typ b/spec/book.typ index 076d31cf3..7a55323cd 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -33,6 +33,11 @@ summary: meta.summary.map(((ch, title, _ref)) => chapter(ch, title)).join() ) +#let common-formatting(body) = { + set footnote(numbering: "[1]") + body +} + #let todo(background: white, foreground: black, name: none, body) = block(fill: background, outset: 0.5em, radius: 20%, stroke: black)[ #set text(fill: foreground) @@ -134,6 +139,7 @@ assert(meta.summary.find(((f, _, _)) => f == file) != none, message: "Couldn't resolve typst source file " + file) if is-shiroa { (body) => { + show: common-formatting context _xref-included.update(x => x + ((file): true)) context _toplevel.update(s => { if s == none { @@ -151,6 +157,6 @@ ]) } } else { - (body) => body + body => body } } diff --git a/spec/ebook.typ b/spec/ebook.typ index f9ba76046..e1b15253e 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -1,4 +1,4 @@ -#import "/book.typ": style, meta +#import "/book.typ": style, meta, common-formatting #set document(author: meta.authors, title: meta.title) @@ -10,6 +10,7 @@ #pagebreak(weak: true) #outline() +#show: common-formatting #show heading: set heading(numbering: "1.1") #meta.summary.map(((ch, title, ref)) => [ diff --git a/spec/ecall.typ b/spec/ecall.typ index 6908f768b..dd3544ef3 100644 --- a/spec/ecall.typ +++ b/spec/ecall.typ @@ -12,6 +12,15 @@ #let config = load_config() #show: book-page("ecall.typ") += About `ECALL` +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]]]). + #let config = load_config() #let chip = load_chip("src/halt.toml", config) @@ -45,13 +54,113 @@ This prevents any other operation involving memory from being executed hereafter ]) === Lookup -The HALT chip contributes the following interaction to the lookup-argument: +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") -*Note*: #link("https://github.com/riscv-collab/riscv-gnu-toolchain/blob/master/linux-headers/include/asm-generic/unistd.h#L258")[$93$ is the system call number corresponding to `sys_exit`.] - == 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. + +#let config = load_config() +#let chip = load_chip("src/commit.toml", config) +#let commit = raw(chip.name) += #commit chip + +== Columns +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) + +The #commit chip leverages #nr_variables variables, spanning #nr_columns columns: +#render_chip_column_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]]]) +#[ +#show raw.where(block: true): it => block(it, fill: luma(230), inset: 1em, width: 100%, radius: 5pt) +```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/src/commit.toml b/spec/src/commit.toml new file mode 100644 index 000000000..5d8325363 --- /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",64] +multiplicity = ["-", "first"] +ref = "commit:c:receive_ecall" + +[[constraint_groups]] +name = "read_input" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 11], "address", "timestamp", 1, 0, 0] +output = "address" +multiplicity = "first" +ref = "commit:c:read_address" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 12], "count", "timestamp", 1, 0, 0] +output = "count" +multiplicity = "first" +ref = "commit:c:read_count" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 10], "count", "timestamp", 1, 0, 0] +output = 1 +multiplicity = "first" +ref = "commit:c:read_fd_write_count" + +[[constraints.read_input]] +kind = "interaction" +tag = "MEMW" +input = [1, ["*", 2, 254], ["+", "index", ["cast", "count", "BaseField"]], "timestamp", 0, 0, 0] +output = "index" +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 = "MEWM" +input = [0, "address", "value", "timestamp", 0, 0, 0] +output = "value" +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"], "count_decr"] +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" From 8af2cb7daf54eb3c08bfab2a85c42bdd095eb9c6 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Thu, 12 Feb 2026 14:46:27 +0100 Subject: [PATCH 57/78] spec: Typecheck signatures and make all chips pass (#312) * spec: Typecheck signatures and make all chips pass * Apply suggestion from @RobinJadoul * Apply suggestion from @RobinJadoul * Apply suggestion from @RobinJadoul * s/IS_HALFWORD/IS_HALF * Ensure constants being casted fit into the first limb --- spec/memw.typ | 2 +- spec/src/branch.toml | 2 +- spec/src/cpu.toml | 39 +++++++---------- spec/src/halt.toml | 14 +++--- spec/src/lt.toml | 6 +-- spec/src/memw.toml | 24 +++++------ spec/src/page.toml | 2 +- spec/src/shift.toml | 2 +- spec/src/signatures.toml | 10 ++++- spec/tooling/chip.py | 93 ++++++++++++++++++++++++++++++++++------ 10 files changed, 128 insertions(+), 66 deletions(-) diff --git a/spec/memw.typ b/spec/memw.typ index a3dfda42c..6c12f00a7 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -56,4 +56,4 @@ This chip contributes the following to the lookup argument. - MEMB chip that deals does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) - Compute `base_address[1] + 1` once and have high words of `address_add` as Words - Improve overflow trapping somehow so we don't need `LT` (could tie into previous one by checking carry bit of the +1) -- Adding `μ_sum`/`w2`/`w4`/`write8` multiplicities to the `IS_HALFWORD` lookups may make some GKR things faster if there are known zeroes. +- Adding `μ_sum`/`w2`/`w4`/`write8` multiplicities to the `IS_HALF` lookups may make some GKR things faster if there are known zeroes. diff --git a/spec/src/branch.toml b/spec/src/branch.toml index 34bcdf8cb..a98974678 100644 --- a/spec/src/branch.toml +++ b/spec/src/branch.toml @@ -130,7 +130,7 @@ multiplicity = "μ" [[constraints.all]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", "next_pc_high", "i"]] iter = ["i", 0, 2] multiplicity = "μ" diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 283597246..634d00bf9 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -77,8 +77,6 @@ type = "Bit" desc = "Whether the instruction is of C type, i.e., whether it is 2 bytes long instead of 4" pad = 0 -# TODO: Should this just be a word? (CHECK: effect on computation/extension of arg2) -# TODO: make sure decode correctly extends this (may be zero for unsigned and word_instr?) [[variables.input]] name = "imm" type = "DWordWL" @@ -619,7 +617,7 @@ iter = ["i", 0, 7] kind = "interaction" tag = "SHIFT" input = [["cast", "arg1", "DWordHL"], ["idx", "arg2", 0], "mp_selector", "signed", "word_instr"] -output = ["cast", "res", "DWordHL"] +output = ["cast", "res", "DWordWL"] multiplicity = "SHIFT" [[constraints.alu]] @@ -629,20 +627,18 @@ input = ["pc", ["*", ["+", ["*", 2, "c_type_instruction"], ["*", 4, ["not", "c_t output = ["cast", "res", "DWordWL"] cond = "JALR" -# TODO: no types available, so no casting yet [[constraints.alu]] kind = "interaction" tag = "MUL" -input = ["arg1", "signed", "arg2", "mp_selector", "muldiv_selector"] -output = "res" +input = [["cast", "arg1", "DWordHL"], "signed", ["cast", "arg2", "DWordHL"], "mp_selector", "muldiv_selector"] +output = ["cast", "res", "DWordWL"] multiplicity = "MUL" -# TODO: no types available, so no casting yet [[constraints.alu]] kind = "interaction" tag = "DVRM" -input = ["arg1", "arg2", "signed", "muldiv_selector"] -output = "res" +input = [["cast", "arg1", "DWordHL"], ["cast", "arg2", "DWordHL"], "signed", "muldiv_selector"] +output = ["cast", "res", "DWordWL"] multiplicity = "DIVREM" @@ -650,12 +646,11 @@ multiplicity = "DIVREM" name = "mem" prefix = "M" -# TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "rs1"], "rv1", ["+", "timestamp", ["cast", 0, "DWordWL"]], 1, 0, 0] -output = "rv1" +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]] @@ -664,12 +659,11 @@ constraint = "$#`!read_register1` => #`rv1[i]` = 0$" poly = ["*", ["not", "read_register1"], ["idx", "rv1", "i"]] iter = ["i", 0, 2] -# TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "rs2"], "rv2", ["+", "timestamp", ["cast", 1, "DWordWL"]], 1, 0, 0] -output = "rv2" +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]] @@ -678,33 +672,30 @@ constraint = "$#`!read_register2` => #`rv2[i]` = 0$" poly = ["*", ["not", "read_register2"], ["idx", "rv2", "i"]] iter = ["i", 0, 2] -# TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "rd"], "rvd", ["+", "timestamp", ["cast", 2, "DWordWL"]], 1, 0, 0] +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 = [0, "res", ["+", "timestamp", ["cast", 0, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes", "signed"] +input = [["cast", "res", "DWordWL"], ["+", "timestamp", ["cast", 0, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes", "signed"] output = "rvd" multiplicity = "LOAD" -# TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [0, "res", ["cast", "arg2", ["Byte", 8]], ["+", "timestamp", ["cast", 1, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes"] +input = [0, ["cast", "res", "DWordWL"], ["cast", "arg2", ["Byte", 8]], ["+", "timestamp", ["cast", 1, "DWordWL"]], "memory_2bytes", "memory_4bytes", "memory_8bytes"] multiplicity = "STORE" -# TODO: no types available, so no casting yet [[constraints.mem]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, 255], "next_pc", ["+", "timestamp", ["cast", 1, "DWordWL"]], 1, 0, 0] -output = "pc" +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"] @@ -817,7 +808,7 @@ poly = ["+", [[constraints.misc]] kind = "interaction" tag = "BRANCH" -input = ["pc", ["idx", "imm", 0], ["cast", "arg1", "DWordWL"], "JALR"] +input = ["pc", "imm", ["cast", "arg1", "DWordWL"], "JALR"] output = "next_pc" multiplicity = "branch_cond" diff --git a/spec/src/halt.toml b/spec/src/halt.toml index b0606e3e4..9fee04877 100644 --- a/spec/src/halt.toml +++ b/spec/src/halt.toml @@ -17,7 +17,7 @@ name = "all" [[constraints.all]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, "i"], 0, ["-", ["^", 2, 64], 1], 1, 0, 0] +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" @@ -25,15 +25,15 @@ ref = "halt:c:zeroize_registers_lo" [[constraints.all]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, 10], 0, ["-", ["^", 2, 64], 1], 1, 0, 0] -output = 0 +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, ["*", 2, "i"], 0, ["-", ["^", 2, 64], 1], 1, 0, 0] +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" @@ -41,7 +41,7 @@ ref = "halt:c:zeroize_registers_hi" [[constraints.all]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, 255], 1, ["-", ["^", 2, 64], 1], 1, 0, 0] +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" @@ -51,6 +51,6 @@ name = "lookup" [[constraints.lookup]] kind = "interaction" tag = "ECALL" -input = ["timestamp", 93] +input = ["timestamp", ["cast", 93, "DWordWL"]] multiplicity = ["-", 1] -ref = "halt:c:lookup" \ No newline at end of file +ref = "halt:c:lookup" diff --git a/spec/src/lt.toml b/spec/src/lt.toml index 1941dbb7a..70d25c919 100644 --- a/spec/src/lt.toml +++ b/spec/src/lt.toml @@ -130,21 +130,21 @@ iter = ["i", 0, 1] [[constraints.defs]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", "lhs", 1]] multiplicity = "μ" ref = "lt:c:range_lhs" [[constraints.defs]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", "rhs", 1]] multiplicity = "μ" ref = "lt:c:range_rhs" [[constraints.sub]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", "lhs_sub_rhs", "i"]] iter = ["i", 0, 3] multiplicity = "μ" diff --git a/spec/src/memw.toml b/spec/src/memw.toml index af005c2b4..0ae3b9410 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -127,14 +127,14 @@ poly = ["*", "w2", ["not", "μ_sum"]] [[constraints.consistency]] kind = "template" tag = "ADD" -input = ["base_address", 1] +input = ["base_address", ["cast", 1, "DWordWL"]] output = ["cast", ["idx", "address_add", 0], "DWordWL"] cond = "w2" [[constraints.consistency]] kind = "template" tag = "ADD" -input = ["base_address", ["+", "i", 1]] +input = ["base_address", ["cast", ["+", "i", 1], "DWordWL"]] output = ["cast", ["idx", "address_add", "i"], "DWordWL"] iter = ["i", 1, 2] cond = "w4" @@ -142,14 +142,14 @@ cond = "w4" [[constraints.consistency]] kind = "template" tag = "ADD" -input = ["base_address", ["+", "i", 1]] +input = ["base_address", ["cast", ["+", "i", 1], "DWordWL"]] output = ["cast", ["idx", "address_add", "i"], "DWordWL"] iter = ["i", 3, 6] cond = "write8" [[constraints.consistency]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", ["idx", "address_add", "i"], "j"]] iters = [ ["i", 0, 0], @@ -159,7 +159,7 @@ multiplicity = "w2" [[constraints.consistency]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", ["idx", "address_add", "i"], "j"]] iters = [ ["i", 1, 2], @@ -169,7 +169,7 @@ multiplicity = "w4" [[constraints.consistency]] kind = "interaction" -tag = "IS_HALFWORD" +tag = "IS_HALF" input = [["idx", ["idx", "address_add", "i"], "j"]] iters = [ ["i", 3, 6], @@ -253,40 +253,40 @@ multiplicity = ["-", "μ_sum"] [[constraints.memory]] kind = "interaction" tag = "memory" -input = ["is_register", ["idx", "address_add", 0], ["idx", "old_timestamp", 1], ["idx", "old", 1]] +input = ["is_register", ["cast", ["idx", "address_add", 0], "DWordWL"], ["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]] +input = ["is_register", ["cast", ["idx", "address_add", 0], "DWordWL"], "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"]] +input = ["is_register", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], ["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"]] +input = ["is_register", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], "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"]] +input = ["is_register", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], ["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"]] +input = ["is_register", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], "timestamp", ["idx", "value", "i"]] multiplicity = ["-", "write8"] iter = ["i", 4, 7] diff --git a/spec/src/page.toml b/spec/src/page.toml index 21ec76757..e937cfaa4 100644 --- a/spec/src/page.toml +++ b/spec/src/page.toml @@ -55,7 +55,7 @@ multiplicity = 1 [[constraints.all]] kind = "interaction" tag = "memory" -input = [0, "address", 0, "init"] +input = [0, "address", ["cast", 0, "DWordWL"], "init"] multiplicity = -1 [[constraints.all]] diff --git a/spec/src/shift.toml b/spec/src/shift.toml index 5faed54c7..45c00b064 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -133,7 +133,7 @@ pad = 0 # Assumptions [[assumptions]] -desc = "`IS_HALFWORD[in[i]]`" +desc = "`IS_HALF[in[i]]`" iter = ["i", 0, 3] ref = "shift:a:range_in" diff --git a/spec/src/signatures.toml b/spec/src/signatures.toml index ba233f1e6..66b5bd6cd 100644 --- a/spec/src/signatures.toml +++ b/spec/src/signatures.toml @@ -53,7 +53,7 @@ output = "DWordWL" [[signatures]] tag = "BRANCH" kind = "interaction" -input = ["DWordWL", "Word", "DWordWL", "Bit"] +input = ["DWordWL", "DWordWL", "DWordWL", "Bit"] output = "DWordWL" # MEMW[old; is_register, base_address, value, timestamp, write2, write4, write8] @@ -175,4 +175,10 @@ output = "Half" tag = "HWSLC" kind = "interaction" input = ["Half", "B4"] -output = "Half" \ No newline at end of file +output = "Half" + +# The actual memory tokens, see MEMW and PAGE +[[signatures]] +tag = "memory" +kind = "interaction" +input = ["Bit", "DWordWL", "DWordWL", "BaseField"] diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index 6a78dc091..58deb4b3c 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -59,6 +59,23 @@ def get_const(self) -> int: 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 @@ -150,6 +167,11 @@ def typecheck(self, env: Environment) -> Type: 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 @@ -612,16 +634,6 @@ def __init__(self, config: Config, category: str, data: dict): self.def_ = VirtualDef(config, self.name, self.type, def_) def typecheck(self, env: Environment) -> Type: - 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 handle_iters( env: Environment, iters: list[Iter], @@ -794,6 +806,21 @@ class Signature: 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: @@ -971,9 +998,49 @@ def typecheck(self) -> Iterable[Signature]: 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 + 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]: + data = tomllib.load(open(filename, "rb")) + 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 = sys.argv[2] # Later + signatures = read_signatures(config, sys.argv[2]) if reporter.reported: sys.exit(1) reported = False @@ -986,6 +1053,4 @@ def typecheck(self) -> Iterable[Signature]: if not reported: for chip in chips: reporter.update_location(f"Chip {chip.name}") - # TODO: do something with the signatures - # Use list for the sideeffect of forcing the generator until we use the content - list(chip.typecheck()) + check_signatures(chip.typecheck(), signatures) From 5b81913b865538e311a1ea8d2f6ed5bc009e7531 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Thu, 12 Feb 2026 15:57:32 +0100 Subject: [PATCH 58/78] spec: Variable category for constants (#327) Closes #303 --- spec/src/config.toml | 2 +- spec/src/page.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/src/config.toml b/spec/src/config.toml index 0f6ef11d6..fd0885d40 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -120,5 +120,5 @@ desc = "A preprocessed column holding the row index (zero-indexed)." preprocessed = true [variables.categories] -all = ["input", "output", "auxiliary", "virtual", "multiplicity", "condition"] +all = ["constant", "input", "output", "auxiliary", "virtual", "multiplicity", "condition"] instantiated = ["input", "output", "auxiliary", "multiplicity"] diff --git a/spec/src/page.toml b/spec/src/page.toml index e937cfaa4..dff939558 100644 --- a/spec/src/page.toml +++ b/spec/src/page.toml @@ -2,8 +2,7 @@ name = "PAGE" # Input -# TODO: add `page` as "constant" column or smth -[[variables.input]] +[[variables.constant]] name = "page" type = "DWordWL" desc = "Constant column containing the page base address; should be integrated into the constraints directly" From 68457ea6a12a48ac26cd170561a87a6c76f936ea Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Fri, 13 Feb 2026 14:27:36 +0100 Subject: [PATCH 59/78] spec: Fix interaction signatures for COMMIT (#328) --- spec/src/commit.toml | 26 +++++++++++++------------- spec/src/signatures.toml | 12 ++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/spec/src/commit.toml b/spec/src/commit.toml index 5d8325363..89fa133c6 100644 --- a/spec/src/commit.toml +++ b/spec/src/commit.toml @@ -73,7 +73,7 @@ name = "incoming" [[constraints.incoming]] kind = "interaction" tag = "ECALL" -input = ["timestamp",64] +input = ["timestamp", ["cast", 64, "DWordWL"]] multiplicity = ["-", "first"] ref = "commit:c:receive_ecall" @@ -83,32 +83,32 @@ name = "read_input" [[constraints.read_input]] kind = "interaction" tag = "MEMW" -input = [1, ["*", 2, 11], "address", "timestamp", 1, 0, 0] -output = "address" +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, ["*", 2, 12], "count", "timestamp", 1, 0, 0] -output = "count" +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, ["*", 2, 10], "count", "timestamp", 1, 0, 0] -output = 1 +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, ["*", 2, 254], ["+", "index", ["cast", "count", "BaseField"]], "timestamp", 0, 0, 0] -output = "index" +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" @@ -152,9 +152,9 @@ name = "commit" [[constraints.commit]] kind = "interaction" -tag = "MEWM" -input = [0, "address", "value", "timestamp", 0, 0, 0] -output = "value" +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" @@ -209,7 +209,7 @@ name = "lookups" [[constraints.lookups]] kind = "interaction" tag = "CNB" -input = ["timestamp", ["+", "index", 1], ["cast", "address_incr", "DWordWL"], "count_decr"] +input = ["timestamp", ["+", "index", 1], ["cast", "address_incr", "DWordWL"], ["cast", "count_decr", "DWordWL"]] multiplicity = ["-", "μ", "end"] ref = "commit:c:send_commit_next_byte" diff --git a/spec/src/signatures.toml b/spec/src/signatures.toml index 66b5bd6cd..69a839d2e 100644 --- a/spec/src/signatures.toml +++ b/spec/src/signatures.toml @@ -103,6 +103,18 @@ 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" From b0bdd6017399b67760cbcd5835359c5dce9f25cc Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Tue, 17 Feb 2026 15:10:32 +0100 Subject: [PATCH 60/78] spec: Cleanup, uniformize chapters, make colors work better on web. (#336) * spec: Cleanup, uniformize chapters, make colors work better on web. * Fix double scroll bar * Improve decode table * Remove `style` state and make aside box grey. Having multiple web themes makes the style approach almost always wrong, since we cannot rely on the scheme being dark or light, in contrast to a regular PDF. * Apply suggestions from code review Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> * Update spec/cpu.typ Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --------- Co-authored-by: Erik <159244975+erik-3milabs@users.noreply.github.com> --- spec/add.typ | 34 ++++++++-------------------------- spec/bitwise.typ | 4 ++++ spec/book.typ | 20 +++++++++----------- spec/branch.typ | 4 +++- spec/cpu.typ | 6 ++++-- spec/decode.typ | 7 ++++--- spec/dvrm.typ | 2 ++ spec/ebook.typ | 7 ++----- spec/ecall.typ | 9 +++++---- spec/is_bit.typ | 15 --------------- spec/load.typ | 4 ++++ spec/lt.typ | 3 +++ spec/memory.typ | 8 +++----- spec/memw.typ | 7 +++++++ spec/mul.typ | 5 ++++- spec/neg.typ | 16 +--------------- spec/shift.typ | 15 +-------------- spec/sign.typ | 13 ------------- spec/src/config.toml | 2 +- 19 files changed, 65 insertions(+), 116 deletions(-) diff --git a/spec/add.typ b/spec/add.typ index 241ea8621..d2afb1788 100644 --- a/spec/add.typ +++ b/spec/add.typ @@ -8,33 +8,15 @@ #show: book-page(chip.name) #let add = raw(chip.name) - -#let highlighted_code(code) = { - box( - inset: (left: 4pt, right: 4pt), - outset: (top: 4pt, bottom: 4pt), - radius: 2pt, - fill: luma(230), - raw(code)) -} - -#add is a constraint template that is used to assert that $#`sum` = #`lhs` + #`rhs` mod 2^64$, under the condition that `cond` is non-zero. - -= Notation -The #add constraint template has the following interface: -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => ADD")) -where `cond` is any value described by an expression _of degree at most $1$_. -#highlighted_code("ADD") can be used to denote the _unconditional_ application of the #add template to `lhs`, `rhs`, and `sum`. - #let sub = raw("SUB") -== #sub -For ease of notation, we moreover introduce the #sub constraint template. -Its interface -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => SUB")) -maps onto the #add template as -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => ADD")) -It constrains that $#`diff` = #`lhs` - #`rhs` mod 2^64$ when the expression `cond` is non-zero. -As with #add, #highlighted_code("SUB") can be used to denote the _unconditional_ application of the template. + +#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 #render_chip_column_table(chip, config) diff --git a/spec/bitwise.typ b/spec/bitwise.typ index ef1e3a671..d0b3d89e2 100644 --- a/spec/bitwise.typ +++ b/spec/bitwise.typ @@ -14,6 +14,10 @@ #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. = Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/book.typ b/spec/book.typ index 7a55323cd..338b2679b 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -35,6 +35,7 @@ #let common-formatting(body) = { set footnote(numbering: "[1]") + show raw.where(block: true): it => block(it, inset: 1em, width: 100%, radius: 5pt) body } @@ -46,16 +47,12 @@ #let rj = todo.with(background: teal, name: "Robin") #let et = todo.with(background: rgb("d4aa3a"), name: "Erik") -#let style = state("style", ( - foreground: white, -)) - #let aside(title, body) = context figure( - block(inset: (left: 1em, right: 1em, bottom: 1em), stroke: style.final().foreground, breakable: false)[ + 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: style.final().foreground, + stroke: luma(50%), align(center, strong(text(fill: black, title)))) #align(left, body) ]) @@ -83,10 +80,9 @@ // Invisibly include another chapter, so that its labels can be resolved #let xref-include(f) = { - context if f not in _xref-included.get() { - hide(box(width: 0%, height: 0%, strip-all(include "/" + f))) + context { + place(hide(box(width: auto, height: 0%, strip-all(include "/" + f)))) } - context _xref-included.update(x => x + ((f): true)) } // Generate a cross-link for references to other chapters. @@ -102,7 +98,7 @@ } else { // Because shiroa does weird url escaping let shiroa-label = label(str(lbl).replace(":", "%3A")) - xref-include(ch) + 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 @@ -140,7 +136,6 @@ if is-shiroa { (body) => { show: common-formatting - context _xref-included.update(x => x + ((file): true)) context _toplevel.update(s => { if s == none { file @@ -153,6 +148,9 @@ #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 ]) } diff --git a/spec/branch.typ b/spec/branch.typ index 3e944ca63..90503e862 100644 --- a/spec/branch.typ +++ b/spec/branch.typ @@ -13,6 +13,9 @@ #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. = Columns #let nr_variables = total_nr_variables(chip) @@ -27,7 +30,6 @@ The `BRANCH` chip is comprised of #nr_variables variables that are expressed usi = Constraints -#rj[Check correspondence with CPU for passing in `offset` as word or dword] We constrain `next_pc` to be $#`base_address` + #`offset`$, where `base_address` equals `pc` when $#`JALR` = 0$ and `register` otherwise. diff --git a/spec/cpu.typ b/spec/cpu.typ index ed6126388..08fe1533d 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -13,6 +13,10 @@ #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). = Columns #let nr_variables = total_nr_variables(chip) @@ -29,8 +33,6 @@ First, we perform a decoding lookup for the current PC. #render_constraint_table(chip, config, groups: "decode") -#rj[All casts for interactions will have to be reviewed once other chip interfaces stabilise] - == Range checks We constrain all columns to have the appropriate ranges. diff --git a/spec/decode.typ b/spec/decode.typ index 87f6083f5..f57d7c76f 100644 --- a/spec/decode.typ +++ b/spec/decode.typ @@ -79,13 +79,14 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is show figure: set block(breakable: true) figure(table( - columns: (auto, auto, 40pt, 40pt, 1fr, 15pt), + columns: (auto, auto, auto, auto, 1fr, auto), stroke: 0pt, inset: (right: .5em), align: (left, right, center, center, left, right), fill: (_, y) => - if calc.odd(y) and y <= lines.len() { luma(245) } - else { white }, + // 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), diff --git a/spec/dvrm.typ b/spec/dvrm.typ index 54e71d771..920aec075 100644 --- a/spec/dvrm.typ +++ b/spec/dvrm.typ @@ -17,6 +17,8 @@ #let dvrm = raw(chip.name) +The #dvrm chip provides division and remainder functionality, both signed and unsigned. + = Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) diff --git a/spec/ebook.typ b/spec/ebook.typ index e1b15253e..0e08536fd 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -1,17 +1,14 @@ -#import "/book.typ": style, meta, common-formatting +#import "/book.typ": meta, common-formatting #set document(author: meta.authors, title: meta.title) -#style.update(( - foreground: black, -)) - #align(center, title(meta.title)) #pagebreak(weak: true) #outline() #show: common-formatting #show heading: set heading(numbering: "1.1") +#show raw.where(block: true): set block(fill: luma(230)) #meta.summary.map(((ch, title, ref)) => [ #pagebreak(weak: true) diff --git a/spec/ecall.typ b/spec/ecall.typ index dd3544ef3..3b82019db 100644 --- a/spec/ecall.typ +++ b/spec/ecall.typ @@ -12,7 +12,9 @@ #let config = load_config() #show: book-page("ecall.typ") -= About `ECALL` + +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]]]), @@ -90,12 +92,11 @@ This is why @commit:c:receive_ecall has multiplicity $-#`first`$. 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]]]) -#[ -#show raw.where(block: true): it => block(it, fill: luma(230), inset: 1em, width: 100%, radius: 5pt) + ```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, diff --git a/spec/is_bit.typ b/spec/is_bit.typ index 33477d377..b09242fe4 100644 --- a/spec/is_bit.typ +++ b/spec/is_bit.typ @@ -9,24 +9,9 @@ #let is_bit = raw(chip.name) -#let highlighted_code(code) = { - box( - inset: (left: 4pt, right: 4pt), - outset: (top: 4pt, bottom: 4pt), - radius: 2pt, - fill: luma(230), - raw(code)) -} - #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. -= Interface -The #is_bit constraint template has the following interface: -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => IS_BIT")) -where `cond` is any value described by an expression _of degree at most $1$_. -Note that #highlighted_code("IS_BIT") can be used to denote the _unconditional_ application of the #is_bit template to `X`. - = Variables The #is_bit template operates on two variables: `cond` and `X`: #render_chip_column_table(chip, config) diff --git a/spec/load.typ b/spec/load.typ index bccb830f8..b12e1c04d 100644 --- a/spec/load.typ +++ b/spec/load.typ @@ -13,6 +13,10 @@ #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). = Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/lt.typ b/spec/lt.typ index 3447efd70..8e55b390b 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -13,6 +13,9 @@ #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. = Columns #let nr_variables = total_nr_variables(chip) diff --git a/spec/memory.typ b/spec/memory.typ index 62059de37..778183dab 100644 --- a/spec/memory.typ +++ b/spec/memory.typ @@ -107,7 +107,8 @@ This raises the question of how to represent timestamps and cleanly perform this 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. -#rj[Properly link/refer to the LT chip] +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])][ @@ -123,8 +124,6 @@ We choose to represent timestamps as machine words, using the existing `LT` chip ] ] -#rj[reference to CPU chip/timestamp column and MEMW chip] - = Initialization and Finalization Because the LogUp argument handling token consumption and emission needs to be fully balanced @@ -213,11 +212,10 @@ and hence doesn't need a column, nor a range check. == Register initialization/finalization -#rj[Properly link/reference ECALL/HALT chip] 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. +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. diff --git a/spec/memw.typ b/spec/memw.typ index 6c12f00a7..4b644218a 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -13,6 +13,13 @@ #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). + = Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) diff --git a/spec/mul.typ b/spec/mul.typ index a2fb7d1fc..68ab4a2b8 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -16,6 +16,9 @@ #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. + = Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) @@ -104,4 +107,4 @@ The table can be padded to the next power of two with the following value assign 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. \ No newline at end of file + 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 index ac8554689..d700892cb 100644 --- a/spec/neg.typ +++ b/spec/neg.typ @@ -8,22 +8,8 @@ #let neg = raw(chip.name) -#let highlighted_code(code) = { - box( - inset: (left: 4pt, right: 4pt), - outset: (top: 4pt, bottom: 4pt), - radius: 2pt, - fill: luma(230), - raw(code)) -} - #neg is a constraint template that is used to assert that $#`neg` = -#`x`$, under the condition that `cond` is non-zero. - -= Notation -The #neg constraint template has the following interface: -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("cond => NEG")) -where `cond` is a bit value (i.e., lies in ${0, 1}$) described by an expression _of degree at most $1$_. -#highlighted_code("NEG") can be used to denote the _unconditional_ application of the #neg template to `x` and `neg` (which is equivalent to $#`cond` = 1$). +It requires `cond` to be a bit. = Variables #render_chip_column_table(chip, config) diff --git a/spec/shift.typ b/spec/shift.typ index a2a3ec968..b705adb32 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -16,20 +16,7 @@ #show: book-page(chip.name) -= Interface -The #shift chip has the following interface: -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(240), -``` -// param in: the value being shifted -// param shift: the number of bits to shift `in` by -// param direction: whether to shift left (0) or right (1) -// param signed: whether to interpret `in` as a signed (1) or unsigned (0) integer -// param word_instr: whether to execute the SLL/SR* (0) or SLLW/SR*W (1) instruction -// out shifted: the resulting value -SHIFT[shifted: DWord; in: DWord, shift: Byte, direction: Bit, signed: Bit, word_instr: Bit] -``` -) -In other words, the #shift chip is designed to constrain that +The #shift chip is designed to constrain that $ #`shifted` := cases( #`in` #`<<` #`s` " if" #`direction` = 0, diff --git a/spec/sign.typ b/spec/sign.typ index dcc941e47..fc1b8d0a5 100644 --- a/spec/sign.typ +++ b/spec/sign.typ @@ -9,20 +9,7 @@ #let sign = raw(chip.name) -#let highlighted_code(code) = { - box( - inset: (left: 4pt, right: 4pt), - outset: (top: 4pt, bottom: 4pt), - radius: 2pt, - fill: luma(230), - raw(code)) -} - #sign is a constraint template that is used to extract a `Half`word's sign. - -= Interface -The #sign constraint template has the following interface: -#block(radius: 5pt, width: 100%, inset: 1.5em, fill: luma(230), raw("SIGN")) It constrains that `sign` is set to `1` when both `X`'s most significant bit and `signed` are $1$, and $0$ otherwise. = Variables diff --git a/spec/src/config.toml b/spec/src/config.toml index fd0885d40..7e6389e3b 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -110,7 +110,7 @@ desc = """\ [[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, see there for more details about the magic number." +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]] From 2a259a510db7f63a334bd68bff5ad302d4c157e8 Mon Sep 17 00:00:00 2001 From: Cyprien de Saint Guilhem Date: Mon, 23 Feb 2026 08:06:35 -0800 Subject: [PATCH 61/78] spec: LogUp: Vanilla protocol description (#243) --------- Co-authored-by: Robin Jadoul --- spec/book.typ | 4 +- spec/logup.typ | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ spec/memory.typ | 3 +- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 spec/logup.typ diff --git a/spec/book.typ b/spec/book.typ index 338b2679b..190e63f17 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -7,6 +7,7 @@ title: "Lambda VM specification", authors: ("3MI Labs", "Aligned"), summary: ( + ("logup.typ", [LogUp argument], ), ("memory.typ", [Memory argument], ), ("variables.typ", [Variables], ), ("signatures.typ", [Signatures], ), @@ -40,12 +41,13 @@ } -#let todo(background: white, foreground: black, name: none, body) = block(fill: background, outset: 0.5em, radius: 20%, stroke: black)[ +#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)[ 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/memory.typ b/spec/memory.typ index 778183dab..183bb95fa 100644 --- a/spec/memory.typ +++ b/spec/memory.typ @@ -95,9 +95,8 @@ 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: +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". -#rj[properly link/refer to the logup spec] = Temporal integrity From fa26492f99849c9bb3f41c529d22d44cf46b62de Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Fri, 27 Feb 2026 11:27:54 +0100 Subject: [PATCH 62/78] spec: Add a version and title/front pages (#367) --- spec/book.typ | 3 ++- spec/ebook.typ | 2 ++ spec/front.typ | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 spec/front.typ diff --git a/spec/book.typ b/spec/book.typ index 190e63f17..bf8b044ec 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -6,6 +6,7 @@ #let meta = ( title: "Lambda VM specification", authors: ("3MI Labs", "Aligned"), + version: "0.2", summary: ( ("logup.typ", [LogUp argument], ), ("memory.typ", [Memory argument], ), @@ -31,7 +32,7 @@ #book-meta( title: meta.title, authors: meta.authors, - summary: meta.summary.map(((ch, title, _ref)) => chapter(ch, title)).join() + summary: prefix-chapter("front.typ", meta.title) + meta.summary.map(((ch, title, _ref)) => chapter(ch, title)).join() ) #let common-formatting(body) = { diff --git a/spec/ebook.typ b/spec/ebook.typ index 0e08536fd..c176dcec3 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -3,6 +3,8 @@ #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() 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]. + From 376a726900b36b01e821d1b5bab6c0389daaf98e Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Fri, 13 Mar 2026 12:50:09 +0100 Subject: [PATCH 63/78] spec: Losing some MEMW weight (#398) --- spec/memw.typ | 42 +++++-- spec/src/memw.toml | 107 ++++------------- spec/src/memw_aligned.toml | 228 +++++++++++++++++++++++++++++++++++++ spec/tooling/chip.py | 2 +- 4 files changed, 285 insertions(+), 94 deletions(-) create mode 100644 spec/src/memw_aligned.toml diff --git a/spec/memw.typ b/spec/memw.typ index 4b644218a..57907e26c 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -38,6 +38,13 @@ we document it here, keeping the type information as a reading help. = Constraints +We can compute the addresses for the later bytes based on a single bit each, +indicating whether adding `i` to `base_address` overflows the lower limb. +We can safely assume that additions for which this bit is not correctly set +will have either an overflow on the upper or lower word, and hence 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` @@ -45,9 +52,9 @@ in the memory argument automatically ensures appropriate range checking (as long as no external entities provide negative multiplicities without range checking the timestamp). This ensures the assumptions for `LT` are satisfied. -We additionally check that the address does not overflow -for more significant bytes of the access. -#render_constraint_table(chip, config, groups: "overflow") +There is no need to check that the address does 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. @@ -56,11 +63,32 @@ to effectuate that part of the memory argument. This chip contributes the following to the lookup argument. #render_constraint_table(chip, config, groups: "output") += Read-size aligned fast path + +#let alignedchip = load_chip("src/memw_aligned.toml", config) +#let aligned = raw(alignedchip.name) + +When a memory access happens at an address with proper alignment +(that is, enough trailing zeros) for its access size, 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. +#render_chip_column_table(alignedchip, config) +#render_chip_assumptions(alignedchip, config) +#render_constraint_table(alignedchip, config) + = Future optimization ideas -- Fast path for aligned memory access where all bytes have the same old timestamp -- MEMB chip that deals does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) -- Compute `base_address[1] + 1` once and have high words of `address_add` as Words -- Improve overflow trapping somehow so we don't need `LT` (could tie into previous one by checking carry bit of the +1) +- `MEMB` chip that does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) +- Additional fast path for registers? (Always guaranteed same timestamp, alignment could be an assumption, always only two values) - Adding `μ_sum`/`w2`/`w4`/`write8` multiplicities to the `IS_HALF` lookups may make some GKR things faster if there are known zeroes. diff --git a/spec/src/memw.toml b/spec/src/memw.toml index 0ae3b9410..c9519e115 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -48,9 +48,9 @@ Only the elements corresponding to the `writeN` bits are guaranteed""" # Auxiliary [[variables.auxiliary]] -name = "address_add" -type = ["DWordHL", 7] -desc = "`address_add[i] = base_address + i + 1`" +name = "add_limb_overflow" +type = ["Bit", 7] +desc = "Whether adding `i` to `base_address[0]` as a field element exceeds $2^32$" [[variables.auxiliary]] name = "old_timestamp" @@ -71,6 +71,15 @@ 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", "add_limb_overflow", "i"]]], + ["+", ["idx", "base_address", 1], ["idx", "add_limb_overflow", "i"]]] + [[variables.virtual]] name = "μ_sum" type = "Bit" @@ -126,56 +135,9 @@ poly = ["*", "w2", ["not", "μ_sum"]] [[constraints.consistency]] kind = "template" -tag = "ADD" -input = ["base_address", ["cast", 1, "DWordWL"]] -output = ["cast", ["idx", "address_add", 0], "DWordWL"] -cond = "w2" - -[[constraints.consistency]] -kind = "template" -tag = "ADD" -input = ["base_address", ["cast", ["+", "i", 1], "DWordWL"]] -output = ["cast", ["idx", "address_add", "i"], "DWordWL"] -iter = ["i", 1, 2] -cond = "w4" - -[[constraints.consistency]] -kind = "template" -tag = "ADD" -input = ["base_address", ["cast", ["+", "i", 1], "DWordWL"]] -output = ["cast", ["idx", "address_add", "i"], "DWordWL"] -iter = ["i", 3, 6] -cond = "write8" - -[[constraints.consistency]] -kind = "interaction" -tag = "IS_HALF" -input = [["idx", ["idx", "address_add", "i"], "j"]] -iters = [ - ["i", 0, 0], - ["j", 0, 3], -] -multiplicity = "w2" - -[[constraints.consistency]] -kind = "interaction" -tag = "IS_HALF" -input = [["idx", ["idx", "address_add", "i"], "j"]] -iters = [ - ["i", 1, 2], - ["j", 0, 3], -] -multiplicity = "w4" - -[[constraints.consistency]] -kind = "interaction" -tag = "IS_HALF" -input = [["idx", ["idx", "address_add", "i"], "j"]] -iters = [ - ["i", 3, 6], - ["j", 0, 3], -] -multiplicity = "write8" +tag = "IS_BIT" +input = [["idx", "add_limb_overflow", "i"]] +iter = ["i", 0, 6] [[constraints.consistency]] kind = "interaction" @@ -207,33 +169,6 @@ output = 1 iter = ["i", 4, 7] multiplicity = "write8" - -[[constraint_groups]] -name = "overflow" -prefix = "R" - -[[constraints.overflow]] -kind = "interaction" -tag = "LT" -input = ["base_address", ["cast", ["idx", "address_add", 0], "DWordWL"], 0] -output = 1 -multiplicity = "write2" - -[[constraints.overflow]] -kind = "interaction" -tag = "LT" -input = ["base_address", ["cast", ["idx", "address_add", 2], "DWordWL"], 0] -output = 1 -multiplicity = "write4" - -[[constraints.overflow]] -kind = "interaction" -tag = "LT" -input = ["base_address", ["cast", ["idx", "address_add", 6], "DWordWL"], 0] -output = 1 -multiplicity = "write8" - - [[constraint_groups]] name = "memory" prefix = "M" @@ -253,40 +188,40 @@ multiplicity = ["-", "μ_sum"] [[constraints.memory]] kind = "interaction" tag = "memory" -input = ["is_register", ["cast", ["idx", "address_add", 0], "DWordWL"], ["idx", "old_timestamp", 1], ["idx", "old", 1]] +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", ["cast", ["idx", "address_add", 0], "DWordWL"], "timestamp", ["idx", "value", 1]] +input = ["is_register", ["idx", "address_add", 0], "timestamp", ["idx", "value", 1]] multiplicity = ["-", "w2"] [[constraints.memory]] kind = "interaction" tag = "memory" -input = ["is_register", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], ["idx", "old_timestamp", "i"], ["idx", "old", "i"]] +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", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], "timestamp", ["idx", "value", "i"]] +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", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], ["idx", "old_timestamp", "i"], ["idx", "old", "i"]] +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", ["cast", ["idx", "address_add", ["-", "i", 1]], "DWordWL"], "timestamp", ["idx", "value", "i"]] +input = ["is_register", ["idx", "address_add", ["-", "i", 1]], "timestamp", ["idx", "value", "i"]] multiplicity = ["-", "write8"] iter = ["i", 4, 7] diff --git a/spec/src/memw_aligned.toml b/spec/src/memw_aligned.toml new file mode 100644 index 000000000..715f57c85 --- /dev/null +++ b/spec/src/memw_aligned.toml @@ -0,0 +1,228 @@ +name = "MEMW-A" + +# Input + +[[variables.input]] +name = "is_register" +type = "Bit" +desc = "Whether the address represents a register index" + +[[variables.input]] +name = "base_address_high" +type = "Word" +desc = "The high word of the base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" + +[[variables.input]] +name = "base_address_mid" +type = "Half" +desc = "The middle halfword of the base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" + +[[variables.input]] +name = "base_address_low" +type = ["Byte", 2] +desc = "The low bytes of the base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" + +[[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" + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "The timestamp at which this memory access is said to occur" + +[[variables.input]] +name = "write2" +type = "Bit" +desc = "Whether to write exactly 2 values" + +[[variables.input]] +name = "write4" +type = "Bit" +desc = "Whether to write exactly 4 values" + +[[variables.input]] +name = "write8" +type = "Bit" +desc = "Whether to write exactly 8 values" + +# 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""" + +# Auxiliary + +[[variables.auxiliary]] +name = "old_timestamp" +type = "DWordWL" +desc = "The timestamp at which the address was last accessed" + +# Virtual + +[[variables.virtual]] +name = "base_address" +type = "DWordWL" +desc = "Recomposing the base address from its parts" +defs = {idx = "i", polys = [["+", ["*", ["^", 2, 16], "base_address_mid"], ["*", ["^", 2, 8], ["idx", "base_address_low", 1]], ["idx", "base_address_low", 0]], "base_address_high"]} + +[[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`)" + +[[variables.multiplicity]] +name = "μ_write" +type = "Bit" +desc = "Whether we are performing a write (and hence not return `out`)" + +[[assumptions]] +desc = "`IS_WORD[base_address_high]`" + +[[assumptions]] +desc = "`IS_HALF[base_address_mid]`" + +[[assumptions]] +desc = "`IS_BYTE[base_address_low[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 = "AND_BYTE" +input = [["idx", "base_address_low", 0], ["+", ["*", "write2", 1], ["*", "write4", 3], ["*", "write8", 7]]] +output = 0 + +[[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", "base_address", "old_timestamp", ["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", ["+", "base_address", ["cast", 1, "DWordWL"]], "old_timestamp", ["idx", "old", 1]] +multiplicity = "w2" + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", "base_address", ["cast", 1, "DWordWL"]], "timestamp", ["idx", "value", 1]] +multiplicity = ["-", "w2"] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", "base_address", ["cast", "i", "DWordWL"]], "old_timestamp", ["idx", "old", "i"]] +multiplicity = "w4" +iter = ["i", 2, 3] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", "base_address", ["cast", "i", "DWordWL"]], "timestamp", ["idx", "value", "i"]] +multiplicity = ["-", "w4"] +iter = ["i", 2, 3] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", "base_address", ["cast", "i", "DWordWL"]], "old_timestamp", ["idx", "old", "i"]] +multiplicity = "write8" +iter = ["i", 4, 7] + +[[constraints.memory]] +kind = "interaction" +tag = "memory" +input = ["is_register", ["+", "base_address", ["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", "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/tooling/chip.py b/spec/tooling/chip.py index 58deb4b3c..688743754 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -627,7 +627,7 @@ class VirtualVariable(Variable): def_: VirtualDef def __init__(self, config: Config, category: str, data: dict): - assert_no_unexpected(data, set(Variable.__annotations__.keys()) | {"def"}) + assert_no_unexpected(data, (set(Variable.__annotations__.keys()) | {"def"}) - {"pad"}) reporter.asserts("def" in data, f"Missing def for virtual column: {data!r}") def_ = data.pop("def", {}) super().__init__(config, category, data) From 42b4c9f58d299dcad942a0da3bef9fda8a42402f Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Fri, 13 Mar 2026 12:50:31 +0100 Subject: [PATCH 64/78] spec: Some fixes and improvements for SHIFT (#400) * spec: Some fixes for SHIFT Closes: #389 * spec: Merge HWSL with HWSLC, to simplify SHIFT Closes: #119 * typo Co-authored-by: Cyprien de Saint Guilhem --------- Co-authored-by: Cyprien de Saint Guilhem --- spec/bitwise.typ | 3 +-- spec/shift.typ | 23 +++++++++---------- spec/src/bitwise.toml | 14 +----------- spec/src/shift.toml | 48 ++++++++++++++++++++-------------------- spec/src/signatures.toml | 9 +------- 5 files changed, 38 insertions(+), 59 deletions(-) diff --git a/spec/bitwise.typ b/spec/bitwise.typ index d0b3d89e2..06a2ce822 100644 --- a/spec/bitwise.typ +++ b/spec/bitwise.typ @@ -42,5 +42,4 @@ The following ideas may prove to be optimizations for the #bitwise chip: + 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`, `HWSLC`, `IS_B20`, `ZERO`) lookups in separate tables. -+ Combine `HWSL` and `HWSLC` into a single lookup (see also \#119). ++ Place the 16-bit (`AND`, `OR`, `XOR`, `MSB16`, etc.) and 20-bit (`HWSL`, `IS_B20`, `ZERO`) lookups in separate tables. diff --git a/spec/shift.typ b/spec/shift.typ index b705adb32..289ade1d7 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -59,25 +59,24 @@ Here, we start with discussing the _logical_ left/right shift operations only; t == First phase We zoom in on the first step. -Here, we make use of the two lookup operations -- $#`HWSL[x: Half, y: B4]` := (#`x` #`<<` #`y`) mod 2^16$ (short for "HalfWord Shift Left"), and -- $#`HWSLC[x: Half, y: B4]` := #`x` #`>>` (16-#`y`)$ (short for "HalfWord Shift Left's Carry") -Note here that one can use these two lookups to compute `out: Half[4] := in << y` as: +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]` &"if" i = 0, - #`HWSL[in[`i#`], y] | HWSLC[in[`i-1#`], y]` &"if" i in [1, 3] + #`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]` = (#`x` #`<<` (16-#`y`)) mod 2^16$, and -$#`HWSLC[x,` 16-#`y]` = #`x` #`>>` #`y`$ for $#`y` in [1, 15]$, -one can also use these lookups to compute `out := in >> y` as +$#`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( - #`HWSLC[in[`i#`],` 16-#`y] | HWSL[in[`i+1#`], y]` &"if" i in [0, 2], - #`HWSLC[in[`3#`],` 16-#`y]` &"if" i = 3 + #`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$. @@ -90,7 +89,7 @@ $ (16-#`shift`) mod 16 & "when shifting right" ), $ -it only takes some rearranging and combining of the values $#`X[`i#`] := HWSL[in[`i#`], bit_shift]`$ and $#`Y[`i#`] := HWSLC[in[`i#`], bit_shift]`$ to form the limbs of $#`in <> shift` mod 16$. +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 diff --git a/spec/src/bitwise.toml b/spec/src/bitwise.toml index 75e8faee4..67d73facd 100644 --- a/spec/src/bitwise.toml +++ b/spec/src/bitwise.toml @@ -116,11 +116,6 @@ name = "μ_HWSL" type = "BaseField" desc = "" -[[variables.multiplicity]] -name = "μ_HWSLC" -type = "BaseField" -desc = "" - [[constraint_groups]] name = "contributions" @@ -189,12 +184,5 @@ multiplicity = ["-", "μ_IS_B20"] kind = "interaction" tag = "HWSL" input = [["+", "X", ["*", 256, "Y"]], "Z"] -output = "SLL" +output = ["arr", "SLL", "SLLC"] multiplicity = ["-", "μ_HWSL"] - -[[constraints.contributions]] -kind = "interaction" -tag = "HWSLC" -input = [["+", "X", ["*", 256, "Y"]], "Z"] -output = "SLLC" -multiplicity = ["-", "μ_HWSLC"] diff --git a/spec/src/shift.toml b/spec/src/shift.toml index 45c00b064..bbe22a5d9 100644 --- a/spec/src/shift.toml +++ b/spec/src/shift.toml @@ -74,13 +74,22 @@ desc = "scratch variable." pad = ["arr", 0, 0, 0, 0] [[variables.auxiliary]] -name = "limb_shift" -type = ["Bit", 4] -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." -pad = ["arr", 0, 0, 0, 0] +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" @@ -118,7 +127,7 @@ def = {idx="i", iter=[0, 3], poly=["+", ["idx", "Y", "i"], ["idx", "X", ["+", "i 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", ["-", 3, "i"]], 3, ["idx", "limb_shift", "j"]]]]]]} +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 @@ -192,7 +201,7 @@ multiplicity = "left" [[constraints.bit_shift]] kind = "interaction" tag = "AND_BYTE" -input = [["-", ["^", 2, 8], "shift"], 0x0F] +input = [["-", ["^", 2, 8], ["*", 16, "zbs"], "shift"], 0x0F] output = "bit_shift" ref = "shift:c:bit_shift_if_right" multiplicity = "right" @@ -213,7 +222,7 @@ name = "intra_limb_shift" kind = "interaction" tag = "HWSL" input = [["idx", "in", "i"], "bit_shift"] -output = ["idx", "X", "i"] +output = ["arr", ["idx", "X", "i"], ["idx", "Y", "i"]] iter = ["i", 0, 3] ref = "shift:c:hwsl_if_not_zero" multiplicity = ["not", "zbs"] @@ -225,11 +234,18 @@ 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 = ["idx", "X", 4] +output = ["arr", ["idx", "X", 4], ["-", "extension", ["idx", "X", 4]]] ref = "shift:c:hwsl_x4_if_not_zero" multiplicity = ["not", "zbs"] @@ -239,22 +255,6 @@ constraint = "$#`zbs` => #`X[4]` = 0$" poly = ["*", "zbs", ["idx", "X", 4]] ref = "shift:c:zbs_implies_X_4" -[[constraints.intra_limb_shift]] -kind = "interaction" -tag = "HWSLC" -input = [["idx", "in", "i"], "bit_shift"] -output = ["idx", "Y", "i"] -iter = ["i", 0, 3] -ref = "shift:c:hwslc_if_not_zero" -multiplicity = ["not", "zbs"] - -[[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" - [[constraint_groups]] name = "limb_shifting" diff --git a/spec/src/signatures.toml b/spec/src/signatures.toml index 69a839d2e..17ecd3933 100644 --- a/spec/src/signatures.toml +++ b/spec/src/signatures.toml @@ -180,14 +180,7 @@ input = ["B20"] tag = "HWSL" kind = "interaction" input = ["Half", "B4"] -output = "Half" - -# HWSLC[res; X, shift] -[[signatures]] -tag = "HWSLC" -kind = "interaction" -input = ["Half", "B4"] -output = "Half" +output = ["Half", 2] # The actual memory tokens, see MEMW and PAGE [[signatures]] From 5cf536911966cf9ffe4d9ab8b46a7fe49e97e1e9 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Mon, 16 Mar 2026 10:25:19 +0100 Subject: [PATCH 65/78] Fix type checking for MEMW_A (#423) --- spec/src/memw_aligned.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/src/memw_aligned.toml b/spec/src/memw_aligned.toml index 715f57c85..8df912111 100644 --- a/spec/src/memw_aligned.toml +++ b/spec/src/memw_aligned.toml @@ -1,4 +1,4 @@ -name = "MEMW-A" +name = "MEMW_A" # Input @@ -68,7 +68,9 @@ desc = "The timestamp at which the address was last accessed" name = "base_address" type = "DWordWL" desc = "Recomposing the base address from its parts" -defs = {idx = "i", polys = [["+", ["*", ["^", 2, 16], "base_address_mid"], ["*", ["^", 2, 8], ["idx", "base_address_low", 1]], ["idx", "base_address_low", 0]], "base_address_high"]} +def = {idx = "i", polys = [ + { iter = 0, poly = ["+", ["*", ["^", 2, 16], "base_address_mid"], ["*", ["^", 2, 8], ["idx", "base_address_low", 1]], ["idx", "base_address_low", 0]] }, + { iter = 1, poly = "base_address_high" }]} [[variables.virtual]] name = "w2" @@ -131,10 +133,11 @@ iter = ["i", 0, 1] name = "consistency" [[constraints.consistency]] -kind = "template" +kind = "interaction" tag = "AND_BYTE" input = [["idx", "base_address_low", 0], ["+", ["*", "write2", 1], ["*", "write4", 3], ["*", "write8", 7]]] output = 0 +multiplicity = "μ_sum" [[constraints.consistency]] kind = "template" From 8f7ddeae14a57f85be1d7610d71c75ffed6091f9 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:23:22 +0100 Subject: [PATCH 66/78] Spec/memw update (#434) * spec/memw: read/write from/to -> read from/write to * spec/memw: rename add_limb_overflow as carry * spec/memw: minor var desc updates * spec/memw: remove superfluous minus symbol * spec/memw: update description * spec/memw_a: minor optimization * Apply suggestions from code review Co-authored-by: Robin Jadoul * spec/MEMW: fix interaction typing * spec/MEMW: drop superfluous notes * spec/MEMW: update alignment requirement * spec/MEMW: intentionally separate carry's prose and .toml descriptions --------- Co-authored-by: Robin Jadoul --- spec/memw.typ | 29 +++++++++--------- spec/src/memw.toml | 18 ++++++------ spec/src/memw_aligned.toml | 60 ++++++++++++-------------------------- 3 files changed, 43 insertions(+), 64 deletions(-) diff --git a/spec/memw.typ b/spec/memw.typ index 57907e26c..f70496aa8 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -33,26 +33,27 @@ The `MEMW` chip is comprised of #nr_variables variables that are expressed using 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. -These properties are necessary for the consistency of the system as a whole, and therefore +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 -We can compute the addresses for the later bytes based on a single bit each, -indicating whether adding `i` to `base_address` overflows the lower limb. -We can safely assume that additions for which this bit is not correctly set -will have either an overflow on the upper or lower word, and hence not match -any existing memory tokens, which are only initialized for correctly formatted -and range-checked doublewords (see @memory). +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 appropriate range checking -(as long as no external entities provide negative multiplicities without range checking the 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 address does not overflow, +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. @@ -60,7 +61,7 @@ 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. +This chip contributes the following to the lookup argument: #render_constraint_table(chip, config, groups: "output") = Read-size aligned fast path @@ -68,9 +69,9 @@ This chip contributes the following to the lookup argument. #let alignedchip = load_chip("src/memw_aligned.toml", config) #let aligned = raw(alignedchip.name) -When a memory access happens at an address with proper alignment -(that is, enough trailing zeros) for its access size, and all accessed -elements were last accessed at the same timestamp, we can +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. diff --git a/spec/src/memw.toml b/spec/src/memw.toml index c9519e115..4905fc5aa 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -10,17 +10,17 @@ desc = "Whether the address represents a register index" [[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" +desc = "The base address to read from/write to. Gets offset by $[0, 7]$ depending on the size of the access" [[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" +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" [[variables.input]] name = "timestamp" type = "DWordWL" -desc = "The timestamp at which this memory access is said to occur" +desc = "The timestamp at which this memory access occurs" [[variables.input]] name = "write2" @@ -48,14 +48,14 @@ Only the elements corresponding to the `writeN` bits are guaranteed""" # Auxiliary [[variables.auxiliary]] -name = "add_limb_overflow" +name = "carry" type = ["Bit", 7] -desc = "Whether adding `i` to `base_address[0]` as a field element exceeds $2^32$" +desc = "Whether `base_address[0] + i + 1` $>= 2^32$" [[variables.auxiliary]] name = "old_timestamp" type = ["DWordWL", 8] -desc = "The timestamp at which the address was last accessed" +desc = "The timestamp at which address `base_address + i` was last accessed" # Virtual @@ -77,8 +77,8 @@ 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", "add_limb_overflow", "i"]]], - ["+", ["idx", "base_address", 1], ["idx", "add_limb_overflow", "i"]]] + ["-", ["+", ["idx", "base_address", 0], "i", 1], ["*", ["^", 2, 32], ["idx", "carry", "i"]]], + ["+", ["idx", "base_address", 1], ["idx", "carry", "i"]]] [[variables.virtual]] name = "μ_sum" @@ -136,7 +136,7 @@ poly = ["*", "w2", ["not", "μ_sum"]] [[constraints.consistency]] kind = "template" tag = "IS_BIT" -input = [["idx", "add_limb_overflow", "i"]] +input = [["idx", "carry", "i"]] iter = ["i", 0, 6] [[constraints.consistency]] diff --git a/spec/src/memw_aligned.toml b/spec/src/memw_aligned.toml index 8df912111..be6bb1603 100644 --- a/spec/src/memw_aligned.toml +++ b/spec/src/memw_aligned.toml @@ -8,19 +8,9 @@ type = "Bit" desc = "Whether the address represents a register index" [[variables.input]] -name = "base_address_high" -type = "Word" -desc = "The high word of the base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" - -[[variables.input]] -name = "base_address_mid" -type = "Half" -desc = "The middle halfword of the base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" - -[[variables.input]] -name = "base_address_low" -type = ["Byte", 2] -desc = "The low bytes of the base address to read/write from/to, gets offset by $[0, 7]$, depending on how big the access is" +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" [[variables.input]] name = "value" @@ -52,7 +42,7 @@ desc = "Whether to write exactly 8 values" [[variables.output]] name = "old" type = ["BaseField", 8] -desc = """The old value written at `base_address`. See `value` for information about representation. +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""" # Auxiliary @@ -64,14 +54,6 @@ desc = "The timestamp at which the address was last accessed" # Virtual -[[variables.virtual]] -name = "base_address" -type = "DWordWL" -desc = "Recomposing the base address from its parts" -def = {idx = "i", polys = [ - { iter = 0, poly = ["+", ["*", ["^", 2, 16], "base_address_mid"], ["*", ["^", 2, 8], ["idx", "base_address_low", 1]], ["idx", "base_address_low", 0]] }, - { iter = 1, poly = "base_address_high" }]} - [[variables.virtual]] name = "w2" type = "Bit" @@ -103,14 +85,11 @@ type = "Bit" desc = "Whether we are performing a write (and hence not return `out`)" [[assumptions]] -desc = "`IS_WORD[base_address_high]`" - -[[assumptions]] -desc = "`IS_HALF[base_address_mid]`" +desc = "`IS_HALF[base_address[i]]`" +iter = ["i", 0, 1] [[assumptions]] -desc = "`IS_BYTE[base_address_low[i]]`" -iter = ["i", 0, 1] +desc = "`IS_WORD[base_address[2]]`" [[assumptions]] desc = "`IS_BIT`" @@ -134,9 +113,8 @@ name = "consistency" [[constraints.consistency]] kind = "interaction" -tag = "AND_BYTE" -input = [["idx", "base_address_low", 0], ["+", ["*", "write2", 1], ["*", "write4", 3], ["*", "write8", 7]]] -output = 0 +tag = "IS_HALF" +input = [["+", ["idx", "base_address", 0], "write2", ["*", 3, "write4"], ["*", 7, "write8"]]] multiplicity = "μ_sum" [[constraints.consistency]] @@ -163,52 +141,52 @@ prefix = "M" [[constraints.memory]] kind = "interaction" tag = "memory" -input = ["is_register", "base_address", "old_timestamp", ["idx", "old", 0]] +input = ["is_register", ["cast", "base_address", "DWordWL"], "old_timestamp", ["idx", "old", 0]] multiplicity = "μ_sum" [[constraints.memory]] kind = "interaction" tag = "memory" -input = ["is_register", "base_address", "timestamp", ["idx", "value", 0]] +input = ["is_register", ["cast", "base_address", "DWordWL"], "timestamp", ["idx", "value", 0]] multiplicity = ["-", "μ_sum"] [[constraints.memory]] kind = "interaction" tag = "memory" -input = ["is_register", ["+", "base_address", ["cast", 1, "DWordWL"]], "old_timestamp", ["idx", "old", 1]] +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", ["+", "base_address", ["cast", 1, "DWordWL"]], "timestamp", ["idx", "value", 1]] +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", ["+", "base_address", ["cast", "i", "DWordWL"]], "old_timestamp", ["idx", "old", "i"]] +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", ["+", "base_address", ["cast", "i", "DWordWL"]], "timestamp", ["idx", "value", "i"]] +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", ["+", "base_address", ["cast", "i", "DWordWL"]], "old_timestamp", ["idx", "old", "i"]] +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", ["+", "base_address", ["cast", "i", "DWordWL"]], "timestamp", ["idx", "value", "i"]] +input = ["is_register", ["+", ["cast", "base_address", "DWordWL"], ["cast", "i", "DWordWL"]], "timestamp", ["idx", "value", "i"]] multiplicity = ["-", "write8"] iter = ["i", 4, 7] @@ -220,12 +198,12 @@ prefix = "O" [[constraints.output]] kind = "interaction" tag = "MEMW" -input = ["is_register", "base_address", "value", "timestamp", "write2", "write4", "write8"] +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", "base_address", "value", "timestamp", "write2", "write4", "write8"] +input = ["is_register", ["cast", "base_address", "DWordWL"], "value", "timestamp", "write2", "write4", "write8"] multiplicity = "μ_write" From a9c073a94628b016280722e86491728368deff37 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:31:35 +0100 Subject: [PATCH 67/78] spec/MEMW(_A): minor update (#459) * spec/memw: read/write from/to -> read from/write to * spec/memw: rename add_limb_overflow as carry * spec/memw: minor var desc updates * spec/memw: remove superfluous minus symbol * spec/memw: update description * spec/memw_a: minor optimization * Apply suggestions from code review Co-authored-by: Robin Jadoul * spec/MEMW: fix interaction typing * spec/MEMW: drop superfluous notes * spec/MEMW: update alignment requirement * spec/MEMW: intentionally separate carry's prose and .toml descriptions * spec/MEMW: fix multiplicities * spec/MEMW_A: padding * spec/MEMW: padding * spec/MEMW: bit check multiplicities * spec/MEMW: simplify padding --------- Co-authored-by: Robin Jadoul --- spec/memw.typ | 8 ++++++++ spec/src/memw.toml | 27 ++++++++++++++++++++++++--- spec/src/memw_aligned.toml | 25 +++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/spec/memw.typ b/spec/memw.typ index f70496aa8..b1f95a491 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -6,6 +6,7 @@ total_nr_variables, total_nr_instantiated_columns, render_constraint_table, + render_chip_padding_table, ) #let config = load_config() @@ -64,6 +65,10 @@ to effectuate that part of the memory argument. 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) @@ -87,6 +92,9 @@ The #aligned chip only needs #nr_variables variables, expressed through #nr_colu #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) = Future optimization ideas diff --git a/spec/src/memw.toml b/spec/src/memw.toml index 4905fc5aa..1cc0dd3c2 100644 --- a/spec/src/memw.toml +++ b/spec/src/memw.toml @@ -6,36 +6,43 @@ name = "MEMW" 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 @@ -44,6 +51,7 @@ 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 @@ -51,11 +59,13 @@ Only the elements corresponding to the `writeN` bits are guaranteed""" 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 @@ -92,12 +102,13 @@ def = ["+", "μ_read", "μ_write"] 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]]`" @@ -123,6 +134,16 @@ 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" @@ -235,10 +256,10 @@ kind = "interaction" tag = "MEMW" input = ["is_register", "base_address", "value", "timestamp", "write2", "write4", "write8"] output = "old" -multiplicity = "μ_read" +multiplicity = ["-", "μ_read"] [[constraints.output]] kind = "interaction" tag = "MEMW" input = ["is_register", "base_address", "value", "timestamp", "write2", "write4", "write8"] -multiplicity = "μ_write" +multiplicity = ["-", "μ_write"] diff --git a/spec/src/memw_aligned.toml b/spec/src/memw_aligned.toml index be6bb1603..93a636aba 100644 --- a/spec/src/memw_aligned.toml +++ b/spec/src/memw_aligned.toml @@ -6,36 +6,43 @@ name = "MEMW_A" 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 @@ -44,6 +51,7 @@ 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 @@ -51,6 +59,7 @@ Only the elements corresponding to the `writeN` bits are guaranteed""" name = "old_timestamp" type = "DWordWL" desc = "The timestamp at which the address was last accessed" +pad = 0 # Virtual @@ -78,11 +87,13 @@ def = ["+", "μ_read", "μ_write"] 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]]`" @@ -117,6 +128,16 @@ 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" @@ -200,10 +221,10 @@ kind = "interaction" tag = "MEMW" input = ["is_register", ["cast", "base_address", "DWordWL"], "value", "timestamp", "write2", "write4", "write8"] output = "old" -multiplicity = "μ_read" +multiplicity = ["-", "μ_read"] [[constraints.output]] kind = "interaction" tag = "MEMW" input = ["is_register", ["cast", "base_address", "DWordWL"], "value", "timestamp", "write2", "write4", "write8"] -multiplicity = "μ_write" +multiplicity = ["-", "μ_write"] From 7d4518f82542a2e11d4dc44d14813812b0135b22 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:25:04 +0100 Subject: [PATCH 68/78] spec/MEMW_R: register access fast path (#457) --------- Co-authored-by: Robin Jadoul --- spec/memw.typ | 60 ++++++++++++++- spec/src/memw_register.toml | 141 ++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 spec/src/memw_register.toml diff --git a/spec/memw.typ b/spec/memw.typ index b1f95a491..1c7a1e6e5 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -6,7 +6,7 @@ total_nr_variables, total_nr_instantiated_columns, render_constraint_table, - render_chip_padding_table, + render_chip_padding_table ) #let config = load_config() @@ -96,8 +96,62 @@ The #aligned chip only needs #nr_variables variables, expressed through #nr_colu The table can be padded to the next power of two with the following value assignments: #render_chip_padding_table(alignedchip, config) -= Future optimization ideas += 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. + +== Columns +#let nr_variables = total_nr_variables(register_chip) +#let nr_columns = total_nr_instantiated_columns(register_chip, config) + +The #reg chip is comprised of #nr_variables variables that are expressed using #nr_columns columns: +#render_chip_column_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) + += Future optimization ideas - `MEMB` chip that does a one-byte write to remove old_timestamp from here (uncertain tradeoffs) -- Additional fast path for registers? (Always guaranteed same timestamp, alignment could be an assumption, always only two values) - 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/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"] From 12474262a3b498c5da6dd61175aff90356b37419 Mon Sep 17 00:00:00 2001 From: Robin Jadoul Date: Wed, 25 Mar 2026 10:51:09 +0100 Subject: [PATCH 69/78] spec: Fix CPU sign bit constraints for `word_instr` (#435) --- spec/sign.typ | 2 ++ spec/src/cpu.toml | 57 +++++++++++++++++++--------------------------- spec/src/sign.toml | 3 --- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/spec/sign.typ b/spec/sign.typ index fc1b8d0a5..7135ba9c6 100644 --- a/spec/sign.typ +++ b/spec/sign.typ @@ -20,6 +20,8 @@ The #sign template operates on three variables: 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. diff --git a/spec/src/cpu.toml b/spec/src/cpu.toml index 634d00bf9..a455b854f 100644 --- a/spec/src/cpu.toml +++ b/spec/src/cpu.toml @@ -234,9 +234,9 @@ desc = "The value of register `rs2`" pad = 0 [[variables.auxiliary]] -name = "rv1_sign_bit" +name = "rv1_ext_bit" type = "Bit" -desc = "The sign bit of `rv1` if seen as a 32-bit word" +desc = "The sign bit of `rv1` if seen as a 32-bit word, used for sign extension with `word_instr`" pad = 0 [[variables.auxiliary]] @@ -246,9 +246,9 @@ desc = "The extended version of `rv1`, depending on `word_instr`" pad = 0 [[variables.auxiliary]] -name = "arg2_sign_bit" +name = "rv2_ext_bit" type = "Bit" -desc = "The sign bit of `arg2` if seen as a 32-bit word" +desc = "The sign bit of `rv2` if seen as a 32-bit word, used for sign extension with `word_instr`" pad = 0 [[variables.auxiliary]] @@ -258,9 +258,9 @@ desc = "A multiplexed version of `rv2` and `imm`, to be used as second argument pad = 0 [[variables.auxiliary]] -name = "res_sign_bit" +name = "res_ext_bit" type = "Bit" -desc = "The sign bit of `res`, if seen as a 32-bit word" +desc = "The sign bit of `res`, if seen as a 32-bit word, used for sign extension with `word_instr`" pad = 0 [[variables.auxiliary]] @@ -722,16 +722,10 @@ name = "ext" prefix = "E" [[constraints.ext]] -kind = "arith" -constraint = "$(#`rv1_sign_bit` or #`arg2_sign_bit` or #`res_sign_bit`) => #`word_instr`$" -poly = ["*", ["+", "rv1_sign_bit", "arg2_sign_bit", "res_sign_bit"], ["not", "word_instr"]] - -[[constraints.ext]] -kind = "interaction" -tag = "MSB16" -input = [["idx", "rv1", 1]] -output = "rv1_sign_bit" -multiplicity = "word_instr" +kind = "template" +tag = "SIGN" +input = [["idx", "rv1", 1], "word_instr"] +output = "rv1_ext_bit" [[constraints.ext]] kind = "arith" @@ -740,15 +734,14 @@ poly = ["-", ["idx", ["cast", "arg1", "DWordWL"], 0], ["idx", ["cast", "rv1", "D [[constraints.ext]] kind = "arith" -constraint = "$#`arg1[4:]` = #`rv1[2]` dot (1 - #`word_instr`) + (2^(32) - 1) dot #`rv1_sign_bit` dot #`signed`$" -poly = ["-", ["idx", ["cast", "arg1", "DWordWL"], 1], ["*", ["not", "word_instr"], ["idx", "rv1", 2]], ["*", "signed", "rv1_sign_bit", ["-", ["^", 2, 32], 1]]] +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 = "interaction" -tag = "MSB16" -input = [["idx", "rv2", 1]] -output = "arg2_sign_bit" -multiplicity = "word_instr" +kind = "template" +tag = "SIGN" +input = [["idx", "rv2", 1], "word_instr"] +output = "rv2_ext_bit" [[constraints.ext]] kind = "arith" @@ -757,15 +750,14 @@ poly = ["-", ["idx", ["cast", "arg2", "DWordWL"], 0], ["*", ["not", "LOAD"], ["i [[constraints.ext]] kind = "arith" -constraint = "$#`arg2[4:]` = (1 - #`LOAD`) dot ((1 - #`word_instr`) dot #`rv2[2]` + #`signed` dot #`arg2_sign_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", "arg2_sign_bit", ["-", ["^", 2, 32], 1]], ["*", ["-", 1, "BEQ", "BLT", "STORE"], ["idx", "imm", 1]]] +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 = "interaction" -tag = "MSB8" -input = [["idx", "res", 3]] -output = "res_sign_bit" -multiplicity = "word_instr" +kind = "template" +tag = "SIGN" +input = [["idx", ["cast", "res", "DWordHL"], 1], "word_instr"] +output = "res_ext_bit" [[constraints.ext]] kind = "arith" @@ -774,10 +766,9 @@ poly = ["*", ["not", "LOAD"], ["-", ["idx", "rvd", 0], ["idx", ["cast", "res", " [[constraints.ext]] kind = "arith" -constraint = "$#`!LOAD` => #`rvd[1]` = (1 - #`word_instr`) dot #`res[4:]` + #`res_sign_bit` dot (2^(32) - 1)$" +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_sign_bit", ["-", ["^", 2, 32], 1]]]] - +poly = ["*", ["not", "LOAD"], ["-", ["idx", "rvd", 1], ["*", ["not", "word_instr"], ["idx", ["cast", "res", "DWordWL"], 1]], ["*", "res_ext_bit", ["-", ["^", 2, 32], 1]]]] [[constraint_groups]] diff --git a/spec/src/sign.toml b/spec/src/sign.toml index ca799e0cc..24e99bd0e 100644 --- a/spec/src/sign.toml +++ b/spec/src/sign.toml @@ -16,9 +16,6 @@ type = "Bit" desc = "Sign of `X`" -[[assumptions]] -desc = "`IS_HALF[X]`" - [[assumptions]] desc = "`IS_BIT`" From 950c5255a306713a0999faa04d418f37f0591106 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:20:29 +0200 Subject: [PATCH 70/78] spec: interaction counter (#469) * spec: recursively compute chip interaction count * spec: print interaction count per chip * spec: cleanup * spec/interaction-counter: add multi-dimensional iter support * spec/interaction-counter: count SUB interactions * spec/interaction-counter: drop silent lookup fails * spec/interaction-counter: remove superfluous code * spec/interaction_count: merge getter and setter * spec/interaction_counter: clean up --- spec/add.typ | 6 ++++- spec/branch.typ | 4 ++- spec/chip.typ | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ spec/cpu.typ | 4 ++- spec/dvrm.typ | 5 ++-- spec/ecall.typ | 7 +++-- spec/is_bit.typ | 4 ++- spec/load.typ | 4 ++- spec/lt.typ | 4 ++- spec/memw.typ | 10 ++++--- spec/mul.typ | 4 ++- spec/neg.typ | 5 +++- spec/shift.typ | 4 ++- spec/sign.typ | 8 +++--- 14 files changed, 120 insertions(+), 19 deletions(-) diff --git a/spec/add.typ b/spec/add.typ index d2afb1788..7fb5cd43a 100644 --- a/spec/add.typ +++ b/spec/add.typ @@ -1,12 +1,15 @@ #import "/book.typ": book-page, et #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table +#import "/chip.typ": render_chip_column_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") @@ -19,6 +22,7 @@ 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_column_table(chip, config) = Assumptions diff --git a/spec/branch.typ b/spec/branch.typ index 90503e862..ee406f2e0 100644 --- a/spec/branch.typ +++ b/spec/branch.typ @@ -3,6 +3,7 @@ #import "/chip.typ": ( render_chip_assumptions, render_chip_column_table, + compute_nr_interactions, total_nr_variables, total_nr_instantiated_columns, render_constraint_table, @@ -20,8 +21,9 @@ The #branch chip computes the target address of a branching instruction. = Columns #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: +The #branch chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions diff --git a/spec/chip.typ b/spec/chip.typ index f3ac28a74..8304717ff 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -24,6 +24,76 @@ .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) = { diff --git a/spec/cpu.typ b/spec/cpu.typ index 08fe1533d..c5d693782 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -5,6 +5,7 @@ render_chip_column_table, total_nr_variables, total_nr_instantiated_columns, + compute_nr_interactions, render_constraint_table, render_chip_padding_table, ) @@ -21,8 +22,9 @@ It bases its decisions on the entry of the `DECODE` table (@decode) correspondin = Columns #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: +The #cpu chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions diff --git a/spec/dvrm.typ b/spec/dvrm.typ index 920aec075..9afc3ed7f 100644 --- a/spec/dvrm.typ +++ b/spec/dvrm.typ @@ -4,12 +4,12 @@ render_chip_column_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) @@ -22,8 +22,9 @@ The #dvrm chip provides division and remainder functionality, both signed and un = Columns #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: +The #dvrm chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions diff --git a/spec/ecall.typ b/spec/ecall.typ index 3b82019db..2a7759876 100644 --- a/spec/ecall.typ +++ b/spec/ecall.typ @@ -4,6 +4,7 @@ render_chip_column_table, total_nr_variables, total_nr_instantiated_columns, + compute_nr_interactions, render_constraint_table, render_chip_assumptions, render_chip_padding_table, @@ -32,8 +33,9 @@ where `A0`-`A7` are symbolic names for the registers `x10`-`x17` == Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_halt_interactions = compute_nr_interactions(chip) -The #halt chip leverages #nr_variables variable, spanning #nr_columns columns: +The #halt chip leverages #nr_variables variable, spanning #nr_columns columns and leverages #nr_halt_interactions interaction(s): #render_chip_column_table(chip, config) == Assumptions @@ -76,8 +78,9 @@ As such, no padding is defined. == Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_commit_interactions = compute_nr_interactions(chip) -The #commit chip leverages #nr_variables variables, spanning #nr_columns columns: +The #commit chip leverages #nr_variables variables, spanning #nr_columns columns and leverages #nr_commit_interactions interactions: #render_chip_column_table(chip, config) == Constraints diff --git a/spec/is_bit.typ b/spec/is_bit.typ index b09242fe4..53a466501 100644 --- a/spec/is_bit.typ +++ b/spec/is_bit.typ @@ -1,12 +1,14 @@ #import "/book.typ": book-page #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_constraint_table +#import "/chip.typ": render_chip_column_table, render_constraint_table, set_nr_interactions #let config = load_config() #let chip = load_chip("src/is_bit.toml", config) #show: book-page(chip.name) +#set_nr_interactions(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. diff --git a/spec/load.typ b/spec/load.typ index b12e1c04d..e7d6d3da5 100644 --- a/spec/load.typ +++ b/spec/load.typ @@ -5,6 +5,7 @@ render_chip_column_table, render_chip_padding_table, render_constraint_table, + compute_nr_interactions, total_nr_instantiated_columns, total_nr_variables, ) @@ -21,8 +22,9 @@ It delegates low-level memory handling to the `MEMW` chip (@memw). = Columns #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: +The #load chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions diff --git a/spec/lt.typ b/spec/lt.typ index 8e55b390b..c9e700310 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -7,6 +7,7 @@ render_constraint_table, total_nr_instantiated_columns, total_nr_variables, + compute_nr_interactions, ) #let config = load_config() @@ -20,8 +21,9 @@ The #lt chip constrains an indicator bit for the less-than relation, signed or u = Columns #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: +The #lt chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions diff --git a/spec/memw.typ b/spec/memw.typ index 1c7a1e6e5..fb5c93e90 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -5,6 +5,7 @@ render_chip_column_table, total_nr_variables, total_nr_instantiated_columns, + compute_nr_interactions, render_constraint_table, render_chip_padding_table ) @@ -24,8 +25,9 @@ in order to satisfy the design of the memory argument (@memory). = Columns #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: +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_column_table(chip, config) = Assumptions @@ -73,6 +75,7 @@ The table can be padded to the next power of two with the following value assign #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), @@ -87,7 +90,7 @@ Further logic remains essentially the same, so we briefly present the relevant t #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. +The #aligned chip only needs #nr_variables variables, expressed through #nr_columns columns; it leverages #nr_aligned_interactions interactions. #render_chip_column_table(alignedchip, config) #render_chip_assumptions(alignedchip, config) #render_constraint_table(alignedchip, config) @@ -119,8 +122,9 @@ Note moreover that this chip does not guard against misaligned register access f == Columns #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: +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_column_table(register_chip, config) == Assumptions diff --git a/spec/mul.typ b/spec/mul.typ index 68ab4a2b8..479c2f32d 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -4,6 +4,7 @@ render_chip_column_table, total_nr_variables, total_nr_instantiated_columns, + compute_nr_interactions, render_constraint_table, render_chip_assumptions, render_chip_padding_table, @@ -22,8 +23,9 @@ as well as providing access to the low and high halfs of the multiplication resu = Columns #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: +The #mul chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) #let stackrel(top, bottom) = { diff --git a/spec/neg.typ b/spec/neg.typ index d700892cb..b2f75117a 100644 --- a/spec/neg.typ +++ b/spec/neg.typ @@ -1,17 +1,20 @@ #import "/book.typ": book-page, et #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table +#import "/chip.typ": render_chip_column_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_column_table(chip, config) = Assumptions diff --git a/spec/shift.typ b/spec/shift.typ index 289ade1d7..f356122a3 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -4,6 +4,7 @@ render_chip_column_table, total_nr_variables, total_nr_instantiated_columns, + compute_nr_interactions, render_constraint_table, render_chip_assumptions, render_chip_padding_table, @@ -36,8 +37,9 @@ Here, `<<` and `>>` denote the _logical_ left and right shift operations, while = Columns #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: +The `SHIFT` chip is comprised of #nr_variables variables that are expressed using #nr_columns columns and leverages #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions diff --git a/spec/sign.typ b/spec/sign.typ index 7135ba9c6..9e727e0ac 100644 --- a/spec/sign.typ +++ b/spec/sign.typ @@ -1,19 +1,21 @@ #import "/book.typ": book-page #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table - +#import "/chip.typ": render_chip_column_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 operates on three variables: +The #sign template operates on three variables and introduces #nr_interactions interaction(s): #render_chip_column_table(chip, config) = Assumptions From 6eecb987b1a274aa5840f9ea620b1a78be5f41ed Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:11:46 +0200 Subject: [PATCH 71/78] spec: run spec-tooling in CI (#440) * spec: have tooling exit(1) on error * spec: run spec tooling in CI * spec/tooling: verbosely state when no issues are found * spec/CI: ignore benchmarks for spec stuff --- .github/workflows/hyperfine.yaml | 1 + .github/workflows/pr_spec.yaml | 25 +++++++++++++++++++++++++ spec/tooling/chip.py | 17 +++++++++++++---- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pr_spec.yaml diff --git a/.github/workflows/hyperfine.yaml b/.github/workflows/hyperfine.yaml index f12655d71..e7ef9307e 100644 --- a/.github/workflows/hyperfine.yaml +++ b/.github/workflows/hyperfine.yaml @@ -3,6 +3,7 @@ name: Hyperfine Benchmark on: pull_request: branches: ["**"] + paths-ignore: ["spec/**"] concurrency: group: ${{ github.workflow }}-${{ github.ref }} 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/tooling/chip.py b/spec/tooling/chip.py index 688743754..15339c10c 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -1043,6 +1043,7 @@ def check_signatures(found: Iterable[Signature], expected: list[Signature]): signatures = read_signatures(config, sys.argv[2]) if reporter.reported: sys.exit(1) + reported = False chips: list[Chip] = [] for file in sys.argv[3:]: @@ -1050,7 +1051,15 @@ def check_signatures(found: Iterable[Signature], expected: list[Signature]): continue chips.append(Chip.from_file(config, file)) reported |= reporter.reported - if not reported: - for chip in chips: - reporter.update_location(f"Chip {chip.name}") - check_signatures(chip.typecheck(), signatures) + 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) From 34afbba89792da3105fe1bf629d4455af08822e8 Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:22:00 +0200 Subject: [PATCH 72/78] spec: update TOC (#478) * spec: add chapters * spec: clean up toc * spec: separate ecalls * Fixes for shiroa * Reformat ebook.typ a bit --------- Co-authored-by: Robin Jadoul --- spec/about_ecalls.typ | 24 +++++++++++ spec/book.typ | 78 ++++++++++++++++++++++------------ spec/{ecall.typ => commit.typ} | 73 +++---------------------------- spec/ebook.typ | 40 +++++++++++++---- spec/halt.typ | 56 ++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 102 deletions(-) create mode 100644 spec/about_ecalls.typ rename spec/{ecall.typ => commit.typ} (70%) create mode 100644 spec/halt.typ diff --git a/spec/about_ecalls.typ b/spec/about_ecalls.typ new file mode 100644 index 000000000..d2f55f55a --- /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_column_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/book.typ b/spec/book.typ index bf8b044ec..38fec945c 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -8,31 +8,53 @@ authors: ("3MI Labs", "Aligned"), version: "0.2", summary: ( - ("logup.typ", [LogUp argument], ), - ("memory.typ", [Memory argument], ), - ("variables.typ", [Variables], ), - ("signatures.typ", [Signatures], ), - ("is_bit.typ", [IS_BIT template], ), - ("sign.typ", [SIGN template], ), - ("add.typ", [ADD/SUB template], ), - ("neg.typ", [NEG template], ), - ("decode.typ", [DECODE table], ), - ("cpu.typ", [CPU chip], ), - ("shift.typ", [SHIFT chip], ), - ("branch.typ", [BRANCH chip], ), - ("memw.typ", [MEMW chip], ), - ("lt.typ", [LT chip], ), - ("mul.typ", [MUL chip], ), - ("dvrm.typ", [DVRM chip], ), - ("load.typ", [LOAD chip], ), - ("ecall.typ", [ECALL chips], ), - ("bitwise.typ", [BITWISE chips], ), + ("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(((ch, title, _ref)) => chapter(ch, title)).join() + 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) = { @@ -93,11 +115,11 @@ #let xref(rf) = { assert(is-shiroa, message: "xref should only be used when compiling for shiroa") let lbl = rf.target - let found = meta.summary.find(((_, _, tag)) => str(lbl).starts-with(str(tag))) + 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.summary.position(x => x == found) + 1)]) + 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")) @@ -130,12 +152,12 @@ } #let book-page(file, ..args) = { - let file = if file.ends-with(".typ") { - file - } else { - lower(file) + ".typ" + if not file.ends-with(".typ") { + file = lower(file) + ".typ" } - assert(meta.summary.find(((f, _, _)) => f == file) != none, message: "Couldn't resolve typst source file " + file) + + 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 @@ -147,7 +169,7 @@ } }) let cond() = _toplevel.final() == file - project.with(..args, title: context meta.summary.find(x => x.at(0) == _toplevel.final()).at(1), cond: cond)([ + 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) } diff --git a/spec/ecall.typ b/spec/commit.typ similarity index 70% rename from spec/ecall.typ rename to spec/commit.typ index 2a7759876..a5974e763 100644 --- a/spec/ecall.typ +++ b/spec/commit.typ @@ -10,80 +10,21 @@ render_chip_padding_table, ) -#let config = load_config() - -#show: book-page("ecall.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]]]). - - -#let config = load_config() -#let chip = load_chip("src/halt.toml", config) -#let halt = raw(chip.name) -= #halt chip - -== Columns -#let nr_variables = total_nr_variables(chip) -#let nr_columns = total_nr_instantiated_columns(chip, config) -#let nr_halt_interactions = compute_nr_interactions(chip) - -The #halt chip leverages #nr_variables variable, spanning #nr_columns columns and leverages #nr_halt_interactions interaction(s): -#render_chip_column_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. - +#show: book-page("commit.typ") #let config = load_config() #let chip = load_chip("src/commit.toml", config) #let commit = raw(chip.name) -= #commit chip -== Columns += Columns #let nr_variables = total_nr_variables(chip) #let nr_columns = total_nr_instantiated_columns(chip, config) -#let nr_commit_interactions = compute_nr_interactions(chip) +#let nr_interactions = compute_nr_interactions(chip) -The #commit chip leverages #nr_variables variables, spanning #nr_columns columns and leverages #nr_commit_interactions interactions: +The #commit chip leverages #nr_variables variables, spanning #nr_columns columns and leverages #nr_interactions interactions: #render_chip_column_table(chip, config) -== Constraints += 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]]]) @@ -152,11 +93,11 @@ Lastly, we must make sure `first`, `end` and `μ` are bits (@commit:c:range_firs These are required to ensure the multiplicities $-(#`μ` - #`first`)$ and $#`μ` - #`end`$ are binary. #render_constraint_table(chip, config, groups: "bits") -== Padding += Padding To pad this chip, use the below data. #render_chip_padding_table(chip, config) -== Notes/optimizations += 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`. diff --git a/spec/ebook.typ b/spec/ebook.typ index c176dcec3..1bd691b9f 100644 --- a/spec/ebook.typ +++ b/spec/ebook.typ @@ -6,15 +6,39 @@ #align(center, text(style: "italic", fill: luma(40%))[Version #meta.version]) #align(center, meta.authors.join(", ")) #pagebreak(weak: true) -#outline() + +// 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: "1.1") +#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)) -#meta.summary.map(((ch, title, ref)) => [ - #pagebreak(weak: true) - #heading(supplement: [Chapter], level: 1, title)#ref - #set heading(offset: 1) - #include ch -]).join() +#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/halt.typ b/spec/halt.typ new file mode 100644 index 000000000..803c06e60 --- /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_column_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) + += Columns +#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_column_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. From 46b40241888c2eeacb273c93c2a857994614bccb Mon Sep 17 00:00:00 2001 From: Erik <159244975+erik-3milabs@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:47:00 +0200 Subject: [PATCH 73/78] spec: cleanup before v0.2 (#479) * spec: add backticks to section titles * spec: replace "columns" headers with "variables" * spec: auto count nr_variables * spec: standardize "optimizations"-section headers * spec/config: add missing spaces * spec/chip: rename *_column_table as *_variable_table * spec: drop table captions * spec/v0.2: turn note into aside * spec: place correctness arguments in separate section --- spec/about_ecalls.typ | 2 +- spec/add.typ | 4 ++-- spec/bitwise.typ | 8 ++++---- spec/book.typ | 36 ++++++++++++++++++------------------ spec/branch.typ | 6 +++--- spec/chip.typ | 12 ++++++------ spec/commit.typ | 6 +++--- spec/cpu.typ | 6 +++--- spec/decode.typ | 12 +++++------- spec/dvrm.typ | 6 +++--- spec/halt.typ | 8 ++++---- spec/is_bit.typ | 9 +++++---- spec/load.typ | 6 +++--- spec/lt.typ | 6 +++--- spec/memory.typ | 4 ++-- spec/memw.typ | 15 ++++++++------- spec/mul.typ | 11 +++++------ spec/neg.typ | 17 ++++++++++------- spec/shift.typ | 8 ++++---- spec/sign.typ | 6 +++--- spec/signatures.typ | 14 ++++---------- spec/src/config.toml | 4 ++-- 22 files changed, 101 insertions(+), 105 deletions(-) diff --git a/spec/about_ecalls.typ b/spec/about_ecalls.typ index d2f55f55a..ef4203610 100644 --- a/spec/about_ecalls.typ +++ b/spec/about_ecalls.typ @@ -1,7 +1,7 @@ #import "/book.typ": book-page, aside #import "/src.typ": load_config, load_chip #import "/chip.typ": ( - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, render_constraint_table, diff --git a/spec/add.typ b/spec/add.typ index 7fb5cd43a..0772aa30b 100644 --- a/spec/add.typ +++ b/spec/add.typ @@ -1,6 +1,6 @@ #import "/book.typ": book-page, et #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table, set_nr_interactions, compute_nr_interactions, +#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) @@ -23,7 +23,7 @@ It constrains that $#`diff` equiv #`lhs` - #`rhs` (mod 2^64)$ when the expressio = Variables This template introduces #nr_interactions interaction(s). -#render_chip_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions #render_chip_assumptions(chip, config) diff --git a/spec/bitwise.typ b/spec/bitwise.typ index 06a2ce822..82f9e36f9 100644 --- a/spec/bitwise.typ +++ b/spec/bitwise.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, render_constraint_table, @@ -19,14 +19,14 @@ The #bitwise chips deal with precomputed lookup tables for bitwise boolean operations and convenience functionalities over small domains. -= Columns += 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_column_table(chip, config) +#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)$. @@ -35,7 +35,7 @@ As such, it has length $2^8 dot 2^8 dot 2^4 = 2^(20)$. This chip adds the following interactions to the lookup: #render_constraint_table(chip, config) -= Areas of Optimization += 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]`. diff --git a/spec/book.typ b/spec/book.typ index 38fec945c..bcc5fec19 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -9,7 +9,7 @@ version: "0.2", summary: ( ("PROOF SYSTEM", ( - ("logup.typ", [LogUp argument], ), + ("logup.typ", [`LogUp` argument], ), ("memory.typ", [Memory argument], ), )), ("OVERVIEW", ( @@ -17,31 +17,31 @@ ("signatures.typ", [Signatures], ), )), ("TEMPLATES", ( - ("is_bit.typ", [IS_BIT template], ), - ("sign.typ", [SIGN template], ), - ("add.typ", [ADD/SUB template], ), - ("neg.typ", [NEG template], ), + ("is_bit.typ", [`IS_BIT` template], ), + ("sign.typ", [`SIGN` template], ), + ("add.typ", [`ADD`/`SUB` template], ), + ("neg.typ", [`NEG` template], ), )), ("MEMORY", ( - ("memw.typ", [MEMW chip], ), + ("memw.typ", [`MEMW` chip], ), )), ("CPU", ( - ("decode.typ", [DECODE table], ), - ("cpu.typ", [CPU chip], ), + ("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], ), + ("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], ), + ("about_ecalls.typ", [About `ECALL`], ), + ("halt.typ", [`HALT` chip], ), + ("commit.typ", [`COMMIT` chip], ), )) ) ) diff --git a/spec/branch.typ b/spec/branch.typ index ee406f2e0..0743ae2f9 100644 --- a/spec/branch.typ +++ b/spec/branch.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, compute_nr_interactions, total_nr_variables, total_nr_instantiated_columns, @@ -18,13 +18,13 @@ The #branch chip computes the target address of a branching instruction. -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions diff --git a/spec/chip.typ b/spec/chip.typ index 8304717ff..f3e0892f7 100644 --- a/spec/chip.typ +++ b/spec/chip.typ @@ -142,11 +142,11 @@ ([#raw(var.name)], [$:=$], [#expr_to_math(var.pad)],) } }, - ), caption: [Overview of padding values for #chip.name chip.]) + )) } -/// Generates a table listing `chip`'s columns. -#let render_chip_column_table(chip, config) = { +/// Generates a table listing `chip`'s variables. +#let render_chip_variable_table(chip, config) = { // Render a definition's iterators let render_def_iters(iters) = { @@ -235,7 +235,7 @@ } (table.cell(colspan: 4, []), ) }, - ), caption: [Column overview of #chip.name chip.]) + )) } #let cref(obj, body) = { @@ -278,7 +278,7 @@ ..for assumption in chip.assumptions { ([#tag(assumption)], [#iters(assumption)], [#eval(assumption.desc, mode: "markup")]) }, - ), caption: [Assumption overview of #chip.name chip.]) + )) } /// Generates a table listing all interactions initiated by `chip`'s. @@ -389,5 +389,5 @@ } } } - ), caption: [Constraint overview of #chip.name chip.]) + )) } diff --git a/spec/commit.typ b/spec/commit.typ index a5974e763..a6fea1b6c 100644 --- a/spec/commit.typ +++ b/spec/commit.typ @@ -1,7 +1,7 @@ #import "/book.typ": book-page, aside #import "/src.typ": load_config, load_chip #import "/chip.typ": ( - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -16,13 +16,13 @@ #let chip = load_chip("src/commit.toml", config) #let commit = raw(chip.name) -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Constraints In this VM, committing is considered equivalent to writing a value to `stdout`. diff --git a/spec/cpu.typ b/spec/cpu.typ index c5d693782..2fbd60d59 100644 --- a/spec/cpu.typ +++ b/spec/cpu.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -19,13 +19,13 @@ 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). -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions #render_chip_assumptions(chip, config) diff --git a/spec/decode.typ b/spec/decode.typ index f57d7c76f..bb5d0d5a1 100644 --- a/spec/decode.typ +++ b/spec/decode.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, render_constraint_table, @@ -21,12 +21,12 @@ 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. -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Padding The #decode table must be padded to a length that is a power of two. @@ -46,7 +46,7 @@ Note that the below table is _not_ used in practice: it is solely used for the p #let config = load_config() #let uncompressed_chip = load_chip("src/decode_uncompressed.toml", config) -#render_chip_column_table(uncompressed_chip, 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: @@ -93,9 +93,7 @@ To indicate an instruction is provided in compressed form, the `c_type` flag is ..lines.flatten(), table.hline(stroke: 1.5pt), table.footer([*Operation*], [*op-flag*], [*`w_instr`*], [*`signed`*], [*other*]), - ), - caption: [Decoding table] - ) + )) } #let decoding = ( diff --git a/spec/dvrm.typ b/spec/dvrm.typ index 9afc3ed7f..1118aa10a 100644 --- a/spec/dvrm.typ +++ b/spec/dvrm.typ @@ -1,7 +1,7 @@ #import "/book.typ": book-page #import "/src.typ": load_config, load_chip #import "/chip.typ": ( - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -19,13 +19,13 @@ The #dvrm chip provides division and remainder functionality, both signed and unsigned. -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions #render_chip_assumptions(chip, config) diff --git a/spec/halt.typ b/spec/halt.typ index 803c06e60..691154f61 100644 --- a/spec/halt.typ +++ b/spec/halt.typ @@ -1,7 +1,7 @@ #import "/book.typ": book-page, aside #import "/src.typ": load_config, load_chip #import "/chip.typ": ( - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -16,13 +16,13 @@ #let chip = load_chip("src/halt.toml", config) #let halt = raw(chip.name) -= Columns += 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_column_table(chip, config) +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: diff --git a/spec/is_bit.typ b/spec/is_bit.typ index 53a466501..5246a623a 100644 --- a/spec/is_bit.typ +++ b/spec/is_bit.typ @@ -1,6 +1,6 @@ #import "/book.typ": book-page #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_constraint_table, set_nr_interactions +#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) @@ -8,6 +8,7 @@ #show: book-page(chip.name) #set_nr_interactions(chip) +#let nr_variables = total_nr_variables(chip) #let is_bit = raw(chip.name) @@ -15,8 +16,8 @@ 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 two variables: `cond` and `X`: -#render_chip_column_table(chip, config) +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$: @@ -26,7 +27,7 @@ It takes only one constraint to enforce that `X` must be either $0$ or $1$ whene - 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. -= Proof of correctness +== 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$. diff --git a/spec/load.typ b/spec/load.typ index e7d6d3da5..ac469ec79 100644 --- a/spec/load.typ +++ b/spec/load.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, render_chip_padding_table, render_constraint_table, compute_nr_interactions, @@ -19,13 +19,13 @@ 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). -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions #render_chip_assumptions(chip, config) diff --git a/spec/lt.typ b/spec/lt.typ index c9e700310..2e0ac8be4 100644 --- a/spec/lt.typ +++ b/spec/lt.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, render_chip_padding_table, render_constraint_table, total_nr_instantiated_columns, @@ -18,13 +18,13 @@ The #lt chip constrains an indicator bit for the less-than relation, signed or unsigned. -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions We assume the inputs `lhs`, `rhs` and `signed` are partially range checked. diff --git a/spec/memory.typ b/spec/memory.typ index 183bb95fa..876884c85 100644 --- a/spec/memory.typ +++ b/spec/memory.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, render_chip_padding_table, render_constraint_table, total_nr_instantiated_columns, @@ -168,7 +168,7 @@ We present here a set of constraints on the `PAGE` table that For zero-initialized pages, `init` can be a constant `0`, and hence doesn't need a column, nor a range check. -#render_chip_column_table(chip, config) +#render_chip_variable_table(chip, config) #render_constraint_table(chip, config) diff --git a/spec/memw.typ b/spec/memw.typ index fb5c93e90..425508597 100644 --- a/spec/memw.typ +++ b/spec/memw.typ @@ -2,7 +2,7 @@ #import "/src.typ": load_config, load_chip #import "/chip.typ": ( render_chip_assumptions, - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -22,13 +22,13 @@ 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). -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions @@ -91,7 +91,7 @@ Further logic remains essentially the same, so we briefly present the relevant t #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_column_table(alignedchip, config) +#render_chip_variable_table(alignedchip, config) #render_chip_assumptions(alignedchip, config) #render_constraint_table(alignedchip, config) @@ -119,13 +119,13 @@ If either of these rules does not apply to your access, you should fall back to 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. -== Columns +== 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_column_table(register_chip, config) +#render_chip_variable_table(register_chip, config) == Assumptions The following range checks are assumed to be performed/enforced outside of this chip: @@ -155,7 +155,8 @@ Lastly, this chip contributes the following interactions to the logup: The table can be padded to the next power of two with the following value assignments: #render_chip_padding_table(register_chip, config) -= Future optimization ideas += 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 index 479c2f32d..6e6de12e0 100644 --- a/spec/mul.typ +++ b/spec/mul.typ @@ -1,7 +1,7 @@ #import "/book.typ": book-page #import "/src.typ": load_config, load_chip #import "/chip.typ": ( - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -20,13 +20,13 @@ The #mul chip constrains multiplication, both signed and unsigned, as well as providing access to the low and high halfs of the multiplication result. -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) #let stackrel(top, bottom) = { $mat(delim: #none, top; bottom)$ @@ -102,11 +102,10 @@ The table can be padded to the next power of two with the following value assign #render_chip_padding_table(chip, config) -= Notes += 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`, +- 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 index b2f75117a..336152892 100644 --- a/spec/neg.typ +++ b/spec/neg.typ @@ -1,6 +1,6 @@ -#import "/book.typ": book-page, et +#import "/book.typ": book-page, aside, et #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, render_chip_assumptions, render_constraint_table, compute_nr_interactions, +#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) @@ -15,7 +15,7 @@ It requires `cond` to be a bit. = Variables This template introduces #nr_interactions interaction(s). -#render_chip_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions #render_chip_assumptions(chip, config) @@ -23,6 +23,8 @@ This template introduces #nr_interactions interaction(s). = 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 $ @@ -61,7 +63,8 @@ $ when `cond` is set. When `cond` is not set, the two lookups are not executed, allowing `neg` to take any value in either case. -= Note -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. +#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 index f356122a3..c464d5d55 100644 --- a/spec/shift.typ +++ b/spec/shift.typ @@ -1,7 +1,7 @@ #import "/book.typ": book-page, et #import "/src.typ": load_config, load_chip #import "/chip.typ": ( - render_chip_column_table, + render_chip_variable_table, total_nr_variables, total_nr_instantiated_columns, compute_nr_interactions, @@ -26,7 +26,7 @@ $ ) $ where -$ +$ #`s` := cases( #`shift` mod 32 "if" #`word_instr` = 1, #`shift` mod 64 "if" #`word_instr` = 0, @@ -34,13 +34,13 @@ $ $ Here, `<<` and `>>` denote the _logical_ left and right shift operations, while `>>>` denotes the _arithmetic_ right shift operation. -= Columns += 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_column_table(chip, config) +#render_chip_variable_table(chip, config) = Assumptions #render_chip_assumptions(chip, config) diff --git a/spec/sign.typ b/spec/sign.typ index 9e727e0ac..14a6e4000 100644 --- a/spec/sign.typ +++ b/spec/sign.typ @@ -1,6 +1,6 @@ #import "/book.typ": book-page #import "/src.typ": load_config, load_chip -#import "/chip.typ": render_chip_column_table, total_nr_variables, render_chip_assumptions, render_constraint_table, compute_nr_interactions, +#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) @@ -15,8 +15,8 @@ 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 operates on three variables and introduces #nr_interactions interaction(s): -#render_chip_column_table(chip, config) +The #sign template introduces #nr_interactions interaction(s): +#render_chip_variable_table(chip, config) = Assumptions The #sign template operates on the following assumptions: diff --git a/spec/signatures.typ b/spec/signatures.typ index 5673cdcf6..12d84f757 100644 --- a/spec/signatures.typ +++ b/spec/signatures.typ @@ -56,8 +56,7 @@ #let interactions = signatures.signatures.filter(s => s.kind == "interaction") The following lists signatures of the #interactions.len() interactions in this VM. -#figure( - table( +#figure(table( columns: (1fr, auto), inset: 7pt, align: (top+left, center), @@ -68,14 +67,11 @@ The following lists signatures of the #interactions.len() interactions in this V ..for sig in interactions { ([#render_signature(sig)], [#interaction_bus_size(sig)]) }, - ), - caption: "Signature overview of interactions", -) +)) #let templates = signatures.signatures.filter(s => s.kind == "template") Below, we list the signatures of the #templates.len() templates in this VM. -#figure( - table( +#figure(table( columns: 1fr, inset: 7pt, align: (top+left, center), @@ -85,6 +81,4 @@ Below, we list the signatures of the #templates.len() templates in this VM. ..for sig in templates { ([#render_signature(sig)], ) }, - ), - caption: "Signature overview of templates", -) +)) diff --git a/spec/src/config.toml b/spec/src/config.toml index 7e6389e3b..d9dcaec37 100644 --- a/spec/src/config.toml +++ b/spec/src/config.toml @@ -78,7 +78,7 @@ 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.\ + Represented as a `Word` and two `Half` variables. \ The `Word` is the *least* significant digit. """ @@ -87,7 +87,7 @@ 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.\ + Represented as a `Word` and two `Half` variables. \ The `Word` is the *most* significant digit. """ From 7ff3235cfc725ed8e8b2e49cb95c2fac77aba196 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 10 Apr 2026 14:03:05 +0200 Subject: [PATCH 74/78] spec/tooling: add default case in `build_signature` --- spec/tooling/chip.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index 15339c10c..7e4807762 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -1015,6 +1015,9 @@ def build_signature(config: Config, data: dict) -> Signature: "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"]] From 33cf8855db8f49d6d7f288b8aaa898ed0c513a76 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 10 Apr 2026 14:06:12 +0200 Subject: [PATCH 75/78] spec/tooling: fix hidden global variable --- spec/tooling/chip.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index 7e4807762..e9fde322d 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -390,7 +390,7 @@ def typecheck[T]( yield from callback(env.with_val(self.name, Range.const(i))) -def iters_of(obj: dict, name=None) -> list[Iter]: +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.""" @@ -607,14 +607,14 @@ def __init__(self, config: Config, name: str, tp: Type, data: dict): idx = data.get("idx", None) self.defs = [ PolyWithIters( - build_expr(config, data["poly"]), iters_of(data, name=idx) + 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, name=idx) + build_expr(config, poly["poly"]), iters_of(poly, config, name=idx) ) for poly in data["polys"] ] @@ -753,7 +753,7 @@ def __init__(self, config: Config, data: dict): data, set(self.__annotations__.keys()) | {"iter", "iters", "ref"} ) self.desc = data["desc"] - self.iters = iters_of(data) + self.iters = iters_of(data, config) @dataclass @@ -778,7 +778,7 @@ def __init__(self, config: Config, data: dict): 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) + 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? @@ -873,7 +873,7 @@ def __init__(self, config: Config, data: dict): f"Missing {self.conditional_name}: {data!r}", ) self.conditional = None - self.iters = iters_of(data) + self.iters = iters_of(data, config) def typecheck(self, env: Environment) -> Iterable[Signature]: def callback(e: Environment) -> Iterable[Signature]: From 9a0f9320dbba076679cde4466bdaecf8c13bebc1 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 10 Apr 2026 14:16:18 +0200 Subject: [PATCH 76/78] spec/tooling: fix silent side effect --- spec/tooling/chip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index e9fde322d..d0544d4da 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -1,3 +1,4 @@ +import copy import sys import tomllib from collections.abc import Callable, Iterable @@ -629,6 +630,7 @@ class VirtualVariable(Variable): 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_) From c895bac8ecc99285a4072a2144efb3c2497bec56 Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 10 Apr 2026 14:18:11 +0200 Subject: [PATCH 77/78] spec/tooling: context manage file reads --- spec/tooling/chip.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/tooling/chip.py b/spec/tooling/chip.py index d0544d4da..d597d2274 100644 --- a/spec/tooling/chip.py +++ b/spec/tooling/chip.py @@ -539,7 +539,8 @@ def __init__(self, data: dict): @classmethod def from_file(cls, filename: str | Path) -> Self: reporter.update_location(str(filename)) - return cls(tomllib.load(open(filename, "rb"))) + with open(filename, "rb") as fp: + return cls(tomllib.load(fp)) @classmethod def from_string(cls, s: str) -> Self: @@ -976,7 +977,8 @@ def __init__(self, config: Config, data: dict): @classmethod def from_file(cls, config: Config, filename: str | Path) -> Self: reporter.update_location(str(filename)) - return cls(config, tomllib.load(open(filename, "rb"))) + with open(filename, "rb") as fp: + return cls(config, tomllib.load(fp)) @classmethod def from_string(cls, config: Config, s: str) -> Self: @@ -1031,7 +1033,8 @@ def build_signature(config: Config, data: dict) -> Signature: def read_signatures(config, filename) -> list[Signature]: - data = tomllib.load(open(filename, "rb")) + 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"]] From 23179c395239f8b819de12845f03f5b5491eb6aa Mon Sep 17 00:00:00 2001 From: Erik Takke Date: Fri, 10 Apr 2026 14:44:08 +0200 Subject: [PATCH 78/78] spec: fix precedence Swap precedence of ADD and SUB and treat the first subexpression of a SUB differently --- spec/expr.typ | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/expr.typ b/spec/expr.typ index c4f84eb59..2de0d6ba3 100644 --- a/spec/expr.typ +++ b/spec/expr.typ @@ -56,8 +56,8 @@ "div": 5, // / "sum": 6, // Σ "not": 7, // not - "add": 8, // + - "sub": 9, // - + "sub": 8, // - + "add": 9, // + "eq": 10, // = and := "MAX": 11, // ) @@ -187,7 +187,10 @@ mwrap($-#rec(PREC.neg, e.at(1))$, pp < PREC.neg) } else { // Subtraction - mwrap($#e.slice(1).map(rec.with(PREC.sub)).join($-$)$, pp < PREC.sub) + mwrap( + $#rec(PREC.add, e.at(1))-#e.slice(2).map(rec.with(PREC.sub)).join($-$)$, + pp <= PREC.sub + ) } }, "cast": (pp, rec, e) => {