From b31922cbec08ba96c71c1f84d6b909b019d8f762 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 08:55:04 +0200 Subject: [PATCH 01/14] feat(tree-sitter-compact): add declaration-level Compact grammar Adds a Cargo workspace and a tree-sitter grammar that resolves every Compact declaration to a named node while leaving bodies, parameter lists and expressions opaque. The grammar exists to feed a doc-comment linter, so precision stops at the declaration boundary and the full expression grammar is out of scope. * grammar: fourteen declaration kinds with `name` fields, recursive module bodies, three comment kinds as extras, structured type expressions so nested generic arguments resolve. * binding: native Rust binding, generated parser committed, CI fails when regeneration changes it. * tests: tree-sitter corpus plus a Rust corpus test that rejects any ERROR or MISSING node over the upstream formatter fixtures, the in-repo Compact sources, and an optional external tree. --- .github/workflows/rust.yml | 46 + Cargo.lock | 216 + Cargo.toml | 17 + biome.json | 1 + crates/tree-sitter-compact/Cargo.toml | 34 + crates/tree-sitter-compact/README.md | 88 + .../bindings/rust/build.rs | 19 + .../tree-sitter-compact/bindings/rust/lib.rs | 34 + crates/tree-sitter-compact/grammar.js | 256 + crates/tree-sitter-compact/package.json | 16 + .../queries/highlights.scm | 42 + crates/tree-sitter-compact/src/grammar.json | 1380 ++++ .../tree-sitter-compact/src/node-types.json | 913 +++ crates/tree-sitter-compact/src/parser.c | 7042 +++++++++++++++++ .../src/tree_sitter/alloc.h | 54 + .../src/tree_sitter/array.h | 291 + .../src/tree_sitter/parser.h | 286 + .../test/corpus/circuits-and-constructors.txt | 152 + .../test/corpus/comments.txt | 134 + .../test/corpus/imports.txt | 141 + .../test/corpus/modules-and-exports.txt | 116 + .../test/corpus/pragma-and-include.txt | 37 + .../test/corpus/structs-enums-contracts.txt | 105 + .../test/corpus/types-ledger-witness.txt | 149 + .../test/fixtures/upstream/NOTICE | 17 + .../upstream/already_formatted.compact | 26 + .../test/fixtures/upstream/example_1.compact | 34 + .../test/fixtures/upstream/example_10.compact | 62 + .../test/fixtures/upstream/example_11.compact | 32 + .../test/fixtures/upstream/example_12.compact | 36 + .../test/fixtures/upstream/example_13.compact | 185 + .../test/fixtures/upstream/example_2.compact | 40 + .../test/fixtures/upstream/example_3.compact | 41 + .../test/fixtures/upstream/example_4.compact | 32 + .../test/fixtures/upstream/example_5.compact | 34 + .../test/fixtures/upstream/example_6.compact | 31 + .../test/fixtures/upstream/example_7.compact | 32 + .../test/fixtures/upstream/example_8.compact | 43 + .../test/fixtures/upstream/example_9.compact | 34 + crates/tree-sitter-compact/tests/corpus.rs | 214 + crates/tree-sitter-compact/tree-sitter.json | 37 + rust-toolchain.toml | 3 + 42 files changed, 12502 insertions(+) create mode 100644 .github/workflows/rust.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/tree-sitter-compact/Cargo.toml create mode 100644 crates/tree-sitter-compact/README.md create mode 100644 crates/tree-sitter-compact/bindings/rust/build.rs create mode 100644 crates/tree-sitter-compact/bindings/rust/lib.rs create mode 100644 crates/tree-sitter-compact/grammar.js create mode 100644 crates/tree-sitter-compact/package.json create mode 100644 crates/tree-sitter-compact/queries/highlights.scm create mode 100644 crates/tree-sitter-compact/src/grammar.json create mode 100644 crates/tree-sitter-compact/src/node-types.json create mode 100644 crates/tree-sitter-compact/src/parser.c create mode 100644 crates/tree-sitter-compact/src/tree_sitter/alloc.h create mode 100644 crates/tree-sitter-compact/src/tree_sitter/array.h create mode 100644 crates/tree-sitter-compact/src/tree_sitter/parser.h create mode 100644 crates/tree-sitter-compact/test/corpus/circuits-and-constructors.txt create mode 100644 crates/tree-sitter-compact/test/corpus/comments.txt create mode 100644 crates/tree-sitter-compact/test/corpus/imports.txt create mode 100644 crates/tree-sitter-compact/test/corpus/modules-and-exports.txt create mode 100644 crates/tree-sitter-compact/test/corpus/pragma-and-include.txt create mode 100644 crates/tree-sitter-compact/test/corpus/structs-enums-contracts.txt create mode 100644 crates/tree-sitter-compact/test/corpus/types-ledger-witness.txt create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/NOTICE create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/already_formatted.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_1.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_10.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_11.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_12.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_13.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_2.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_3.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_4.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_5.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_6.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_7.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_8.compact create mode 100644 crates/tree-sitter-compact/test/fixtures/upstream/example_9.compact create mode 100644 crates/tree-sitter-compact/tests/corpus.rs create mode 100644 crates/tree-sitter-compact/tree-sitter.json create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..2bc8283 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,46 @@ +name: Rust + +on: + pull_request: + push: + branches: + - main + - beta + +jobs: + rust: + name: Run Rust Checks + runs-on: ubuntu-24.04 + + permissions: + contents: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: audit + + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install the pinned toolchain + run: | + rustup toolchain install 1.96 --profile minimal --component rustfmt --component clippy + rustup show active-toolchain + + - name: Check formatting + run: cargo fmt --all --check + + - name: Lint + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Test + run: cargo test --workspace + + - name: Check the generated parser is up to date + working-directory: crates/tree-sitter-compact + run: npx -y tree-sitter-cli@0.25.8 generate + + - name: Fail if regeneration changed the parser + run: git diff --exit-code -- crates/tree-sitter-compact/src diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e78a9e5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,216 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-compact" +version = "0.1.0" +dependencies = [ + "cc", + "tree-sitter", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..af07435 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[workspace] +resolver = "2" +members = ["crates/*"] + +[workspace.package] +edition = "2024" +license = "MIT" +repository = "https://github.com/OpenZeppelin/compact-tools" +rust-version = "1.96" + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } + +[workspace.dependencies] +cc = "1" +tree-sitter = "0.25" +tree-sitter-language = "0.1" diff --git a/biome.json b/biome.json index 79dd4db..88b7ec7 100644 --- a/biome.json +++ b/biome.json @@ -13,6 +13,7 @@ "!**/tsconfig.base.json", "!**/tsconfig*.json", "!**/*.compact", + "!crates/tree-sitter-compact/src/**/*", "!**/artifacts/**/*", "!**/coverage/**/*", "!**/dist/**/*", diff --git a/crates/tree-sitter-compact/Cargo.toml b/crates/tree-sitter-compact/Cargo.toml new file mode 100644 index 0000000..3626181 --- /dev/null +++ b/crates/tree-sitter-compact/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tree-sitter-compact" +version = "0.1.0" +description = "Declaration-level Compact grammar for tree-sitter" +build = "bindings/rust/build.rs" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +include = [ + "bindings/rust/*", + "grammar.js", + "queries/*", + "src/*", + "tree-sitter.json", + "LICENSE*", + "README.md", +] + +[lints] +workspace = true + +[lib] +path = "bindings/rust/lib.rs" + +[dependencies] +tree-sitter-language.workspace = true + +[build-dependencies] +cc.workspace = true + +[dev-dependencies] +tree-sitter.workspace = true diff --git a/crates/tree-sitter-compact/README.md b/crates/tree-sitter-compact/README.md new file mode 100644 index 0000000..287c198 --- /dev/null +++ b/crates/tree-sitter-compact/README.md @@ -0,0 +1,88 @@ +# tree-sitter-compact + +Declaration-level [tree-sitter](https://tree-sitter.github.io) grammar for the +Compact smart-contract language (Midnight), tracking language version 0.26. + +It exists to feed a doc-comment linter, so it resolves declarations precisely and +leaves everything else opaque. + +## What it covers + +- Every top-level and module-level declaration is a named node with a `name` + field: `pragma_declaration`, `include_declaration`, `import_declaration`, + `export_declaration`, `module_declaration`, `struct_declaration`, + `enum_declaration`, `contract_declaration`, `implements_declaration`, + `type_declaration`, `ledger_declaration`, `witness_declaration`, + `constructor_declaration`, `circuit_declaration`. +- `module_declaration` recurses: a `module_body` holds the same declarations. +- Comments are named nodes in `extras`: `line_comment`, `block_comment`, + `doc_comment`. +- Types are structured enough to walk: `type_expression`, `generic_arguments`, + `generic_parameters`, `generic_parameter`. +- All import forms: bare module, quoted path, generic arguments, `prefix`, and + the `{ a, b as c } from` selection. + +## What is opaque + +- Circuit and constructor bodies: `block` is a brace-balanced token run. Strings + and comments are lexed first, so braces inside them do not count. +- Struct, enum and external-contract bodies reuse the same `block`. +- Parameter lists: `parameter_list` is a paren-balanced run, so destructuring + patterns parse without a pattern grammar. +- Statements and expressions. There is no expression grammar and none is planned. +- Version expressions: `version_constraint` is one token that runs to the `;`. + +Angle brackets are balanced only inside `generic_arguments`, where a `>` always +closes a type-argument list. They are never balanced in opaque runs. + +## Modifiers + +`export`, `sealed`, `pure` and `new` stay anonymous keyword tokens directly under +the declaration node. A consumer tests for them by walking children and comparing +`node.kind()`: + +```rust +let exported = { + let mut cursor = declaration.walk(); + declaration + .children(&mut cursor) + .any(|child| child.kind() == "export") +}; +``` + +They are not fields, because a field would need a distinct name per modifier and +the linter only ever asks whether one is present. + +## Comment disambiguation + +`doc_comment` matches `/** … */` and outranks `block_comment` by lexical +precedence when both match the same span. `/**/` matches only `block_comment`, +since the doc pattern needs at least one `*` after the opener. + +## Regenerating the parser + +`src/parser.c`, `src/grammar.json`, `src/node-types.json` and `src/tree_sitter/*` +are generated and committed. After editing `grammar.js`: + +```sh +npx -y tree-sitter-cli@0.25.8 generate +``` + +CI regenerates and fails on any diff under `crates/tree-sitter-compact/src`. + +## Tests + +```sh +# Grammar corpus, run from this directory. +npx -y tree-sitter-cli@0.25.8 test + +# Rust bindings plus the fixture corpora. +cargo test --workspace + +# Add an external tree of .compact files. +COMPACT_CORPUS_DIR=/path/to/contracts cargo test --workspace -- --nocapture corpus +``` + +`tests/corpus.rs` asserts that no fixture produces an `ERROR` or `MISSING` node. +`test/fixtures/upstream/` holds the Compact formatter's own examples; see the +`NOTICE` beside them for provenance. diff --git a/crates/tree-sitter-compact/bindings/rust/build.rs b/crates/tree-sitter-compact/bindings/rust/build.rs new file mode 100644 index 0000000..c65afd8 --- /dev/null +++ b/crates/tree-sitter-compact/bindings/rust/build.rs @@ -0,0 +1,19 @@ +fn main() { + let src_dir = std::path::Path::new("src"); + + let mut config = cc::Build::new(); + config.std("c11").include(src_dir); + config.file(src_dir.join("parser.c")); + + // Generated code trips these on some compiler versions; they are not actionable here. + config.flag_if_supported("-Wno-unused-but-set-variable"); + config.flag_if_supported("-Wno-unused-parameter"); + config.flag_if_supported("-Wno-trigraphs"); + + config.compile("tree-sitter-compact"); + + println!("cargo:rerun-if-changed=src/parser.c"); + println!("cargo:rerun-if-changed=src/grammar.json"); + println!("cargo:rerun-if-changed=src/node-types.json"); + println!("cargo:rerun-if-changed=src/tree_sitter"); +} diff --git a/crates/tree-sitter-compact/bindings/rust/lib.rs b/crates/tree-sitter-compact/bindings/rust/lib.rs new file mode 100644 index 0000000..601c974 --- /dev/null +++ b/crates/tree-sitter-compact/bindings/rust/lib.rs @@ -0,0 +1,34 @@ +//! Declaration-level tree-sitter grammar for Compact (Midnight). +//! +//! Declarations are named nodes with a `name` field; circuit and constructor +//! bodies, parameter lists and statements stay opaque brace-balanced runs. + +#![deny(unsafe_op_in_unsafe_fn)] + +use tree_sitter_language::LanguageFn; + +unsafe extern "C" { + fn tree_sitter_compact() -> *const (); +} + +/// The tree-sitter language for Compact. +// SAFETY: `tree_sitter_compact` is generated by tree-sitter-cli into `src/parser.c`, +// which `build.rs` links into this crate, and it returns a `'static` language pointer. +pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_compact) }; + +/// The node-type metadata that tree-sitter-cli generated alongside the parser. +pub const NODE_TYPES: &str = include_str!("../../src/node-types.json"); + +/// The highlight query shipped with the grammar. +pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm"); + +#[cfg(test)] +mod tests { + #[test] + fn language_loads_into_a_parser() { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&super::LANGUAGE.into()) + .expect("the generated parser matches the tree-sitter ABI this crate builds against"); + } +} diff --git a/crates/tree-sitter-compact/grammar.js b/crates/tree-sitter-compact/grammar.js new file mode 100644 index 0000000..55c2d25 --- /dev/null +++ b/crates/tree-sitter-compact/grammar.js @@ -0,0 +1,256 @@ +/// +// @ts-check + +// Declaration-level grammar for Compact (Midnight), language version 0.26. +// Every top-level and module-level declaration is a named node carrying a `name` +// field; bodies, parameter lists and statements stay opaque brace-balanced runs. + +/** + * Comma-separated list with an optional trailing comma. + * @param {RuleOrLiteral} rule + */ +function commaSep(rule) { + return optional(seq(rule, repeat(seq(',', rule)), optional(','))); +} + +module.exports = grammar({ + name: 'compact', + + word: ($) => $.identifier, + + extras: ($) => [/\s/, $.doc_comment, $.block_comment, $.line_comment], + + rules: { + source_file: ($) => repeat($._declaration), + + _declaration: ($) => + choice( + $.pragma_declaration, + $.include_declaration, + $.import_declaration, + $.export_declaration, + $.module_declaration, + $.struct_declaration, + $.enum_declaration, + $.implements_declaration, + $.contract_declaration, + $.type_declaration, + $.ledger_declaration, + $.witness_declaration, + $.constructor_declaration, + $.circuit_declaration, + ), + + // pragma id version-expr ; + pragma_declaration: ($) => + seq('pragma', field('name', $.identifier), $.version_constraint, ';'), + + // Version expressions are not modelled; the run stops at the terminating `;`. + version_constraint: (_$) => token(/[^;{}\r\n]+/), + + // include "file" ; + include_declaration: ($) => seq('include', field('path', $.string), ';'), + + // import {sel} from? name gargs? prefix? ; + import_declaration: ($) => + seq( + 'import', + optional($.import_selection), + field('name', choice($.identifier, $.string)), + optional($.generic_arguments), + optional($.import_prefix), + ';', + ), + + import_selection: ($) => seq('{', commaSep($.import_element), '}', 'from'), + + import_element: ($) => + seq( + field('name', $.identifier), + optional(seq('as', field('alias', $.identifier))), + ), + + import_prefix: ($) => seq('prefix', field('prefix', $.identifier)), + + // export { id, ... } ;? + export_declaration: ($) => + seq('export', '{', commaSep($.identifier), '}', optional(';')), + + // export? module Name gparams? { declaration... } + module_declaration: ($) => + seq( + optional('export'), + 'module', + field('name', $.identifier), + optional($.generic_parameters), + field('body', $.module_body), + ), + + module_body: ($) => seq('{', repeat($._declaration), '}'), + + // export? struct Name gparams? { typed-id... } ;? + struct_declaration: ($) => + seq( + optional('export'), + 'struct', + field('name', $.identifier), + optional($.generic_parameters), + field('body', $.block), + optional(';'), + ), + + // export? enum Name { id, ... } ;? + enum_declaration: ($) => + seq( + optional('export'), + 'enum', + field('name', $.identifier), + field('body', $.block), + optional(';'), + ), + + // contract implements type ; + implements_declaration: ($) => + seq('contract', 'implements', field('type', $.type_expression), ';'), + + // export? contract Name { circuit-declaration... } ;? + contract_declaration: ($) => + seq( + optional('export'), + 'contract', + field('name', $.identifier), + field('body', $.block), + optional(';'), + ), + + // export? new? type Name gparams? = type ; + type_declaration: ($) => + seq( + optional('export'), + optional('new'), + 'type', + field('name', $.identifier), + optional($.generic_parameters), + '=', + field('value', $.type_expression), + ';', + ), + + // export? sealed? ledger id : type ; + ledger_declaration: ($) => + seq( + optional('export'), + optional('sealed'), + 'ledger', + field('name', $.identifier), + ':', + field('type', $.type_expression), + ';', + ), + + // export? witness id gparams? (params) : type ; + witness_declaration: ($) => + seq( + optional('export'), + 'witness', + field('name', $.identifier), + optional($.generic_parameters), + field('parameters', $.parameter_list), + ':', + field('type', $.type_expression), + ';', + ), + + // constructor (params) block + constructor_declaration: ($) => + seq( + 'constructor', + field('parameters', $.parameter_list), + field('body', $.block), + ), + + // export? pure? circuit name gparams? (params) : type block + circuit_declaration: ($) => + seq( + optional('export'), + optional('pure'), + 'circuit', + field('name', $.identifier), + optional($.generic_parameters), + field('parameters', $.parameter_list), + ':', + field('type', $.type_expression), + field('body', $.block), + ), + + // <#T, U,> + generic_parameters: ($) => seq('<', commaSep($.generic_parameter), '>'), + + generic_parameter: ($) => seq(optional('#'), field('name', $.identifier)), + + // Angle brackets nest only here, where a `>` always closes a type argument list. + generic_arguments: ($) => seq('<', repeat($._type_item), '>'), + + type_expression: ($) => repeat1($._type_item), + + _type_item: ($) => + choice( + $.identifier, + $.number, + $.string, + $.generic_arguments, + $._tuple_type, + ',', + ':', + '..', + ), + + _tuple_type: ($) => seq('[', repeat($._type_item), ']'), + + // Brace-balanced opaque run: circuit and constructor bodies, struct, enum and + // external-contract bodies, and destructuring patterns inside parameter lists. + block: ($) => seq('{', repeat($._opaque), '}'), + + parameter_list: ($) => seq('(', repeat($._opaque), ')'), + + _paren_group: ($) => seq('(', repeat($._opaque), ')'), + + _bracket_group: ($) => seq('[', repeat($._opaque), ']'), + + _opaque: ($) => + choice( + $.block, + $._paren_group, + $._bracket_group, + $.string, + $._opaque_token, + '/', + ), + + // Anything that is not a delimiter, a quote or a comment opener. + // biome-ignore lint/complexity/noUselessEscapeInRegex: tree-sitter's regex engine rejects an unescaped `[` in a class. + _opaque_token: (_$) => token(/[^\s(){}\[\]"'\/]+/), + + identifier: (_$) => /[A-Za-z_$][A-Za-z0-9_$]*/, + + number: (_$) => /[0-9]+/, + + // Double-quoted literals span lines; the Compact formatter emits them that way. + string: (_$) => + token( + choice( + seq('"', repeat(choice(/[^"\\]/, /\\(.|\r?\n)/)), '"'), + seq("'", repeat(choice(/[^'\\\r\n]/, /\\./)), "'"), + ), + ), + + // `/** ... */` outranks `/* ... */` at equal length; `/**/` matches only the latter. + doc_comment: (_$) => + token(prec(2, seq('/**', /[^*]*\*+([^/*][^*]*\*+)*/, '/'))), + + block_comment: (_$) => + token(prec(1, seq('/*', /[^*]*\*+([^/*][^*]*\*+)*/, '/'))), + + line_comment: (_$) => token(seq('//', /[^\r\n]*/)), + }, +}); diff --git a/crates/tree-sitter-compact/package.json b/crates/tree-sitter-compact/package.json new file mode 100644 index 0000000..7f85f18 --- /dev/null +++ b/crates/tree-sitter-compact/package.json @@ -0,0 +1,16 @@ +{ + "name": "tree-sitter-compact", + "version": "0.1.0", + "private": true, + "description": "Declaration-level Compact grammar for tree-sitter", + "license": "MIT", + "main": "grammar.js", + "tree-sitter": [ + { + "scope": "source.compact", + "file-types": [ + "compact" + ] + } + ] +} diff --git a/crates/tree-sitter-compact/queries/highlights.scm b/crates/tree-sitter-compact/queries/highlights.scm new file mode 100644 index 0000000..da7e44b --- /dev/null +++ b/crates/tree-sitter-compact/queries/highlights.scm @@ -0,0 +1,42 @@ +; Declaration names. +(pragma_declaration name: (identifier) @variable.builtin) +(module_declaration name: (identifier) @type) +(struct_declaration name: (identifier) @type) +(enum_declaration name: (identifier) @type) +(contract_declaration name: (identifier) @type) +(type_declaration name: (identifier) @type) +(circuit_declaration name: (identifier) @function) +(witness_declaration name: (identifier) @function) +(ledger_declaration name: (identifier) @property) +(generic_parameter name: (identifier) @type.parameter) + +; Keywords and modifiers. +[ + "pragma" + "include" + "import" + "export" + "from" + "as" + "prefix" + "module" + "struct" + "enum" + "contract" + "implements" + "type" + "new" + "sealed" + "ledger" + "witness" + "constructor" + "circuit" + "pure" +] @keyword + +(string) @string +(number) @number + +(line_comment) @comment +(block_comment) @comment +(doc_comment) @comment.documentation diff --git a/crates/tree-sitter-compact/src/grammar.json b/crates/tree-sitter-compact/src/grammar.json new file mode 100644 index 0000000..7bc3a42 --- /dev/null +++ b/crates/tree-sitter-compact/src/grammar.json @@ -0,0 +1,1380 @@ +{ + "$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/grammar.schema.json", + "name": "compact", + "word": "identifier", + "rules": { + "source_file": { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_declaration" + } + }, + "_declaration": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "pragma_declaration" + }, + { + "type": "SYMBOL", + "name": "include_declaration" + }, + { + "type": "SYMBOL", + "name": "import_declaration" + }, + { + "type": "SYMBOL", + "name": "export_declaration" + }, + { + "type": "SYMBOL", + "name": "module_declaration" + }, + { + "type": "SYMBOL", + "name": "struct_declaration" + }, + { + "type": "SYMBOL", + "name": "enum_declaration" + }, + { + "type": "SYMBOL", + "name": "implements_declaration" + }, + { + "type": "SYMBOL", + "name": "contract_declaration" + }, + { + "type": "SYMBOL", + "name": "type_declaration" + }, + { + "type": "SYMBOL", + "name": "ledger_declaration" + }, + { + "type": "SYMBOL", + "name": "witness_declaration" + }, + { + "type": "SYMBOL", + "name": "constructor_declaration" + }, + { + "type": "SYMBOL", + "name": "circuit_declaration" + } + ] + }, + "pragma_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "pragma" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "SYMBOL", + "name": "version_constraint" + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "version_constraint": { + "type": "TOKEN", + "content": { + "type": "PATTERN", + "value": "[^;{}\\r\\n]+" + } + }, + "include_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "include" + }, + { + "type": "FIELD", + "name": "path", + "content": { + "type": "SYMBOL", + "name": "string" + } + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "import_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "import" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "import_selection" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "SYMBOL", + "name": "string" + } + ] + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "generic_arguments" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "import_prefix" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "import_selection": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "{" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "import_element" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "import_element" + } + ] + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "BLANK" + } + ] + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "}" + }, + { + "type": "STRING", + "value": "from" + } + ] + }, + "import_element": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "as" + }, + { + "type": "FIELD", + "name": "alias", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + } + ] + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "import_prefix": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "prefix" + }, + { + "type": "FIELD", + "name": "prefix", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + } + ] + }, + "export_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "identifier" + } + ] + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "BLANK" + } + ] + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "}" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": ";" + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "module_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "module" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "generic_parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "module_body" + } + } + ] + }, + "module_body": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "{" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_declaration" + } + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "struct_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "struct" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "generic_parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "block" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": ";" + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "enum_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "enum" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "block" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": ";" + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "implements_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "contract" + }, + { + "type": "STRING", + "value": "implements" + }, + { + "type": "FIELD", + "name": "type", + "content": { + "type": "SYMBOL", + "name": "type_expression" + } + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "contract_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "contract" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "block" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": ";" + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "type_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "new" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "type" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "generic_parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "=" + }, + { + "type": "FIELD", + "name": "value", + "content": { + "type": "SYMBOL", + "name": "type_expression" + } + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "ledger_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "sealed" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "ledger" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "type", + "content": { + "type": "SYMBOL", + "name": "type_expression" + } + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "witness_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "witness" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "generic_parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "FIELD", + "name": "parameters", + "content": { + "type": "SYMBOL", + "name": "parameter_list" + } + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "type", + "content": { + "type": "SYMBOL", + "name": "type_expression" + } + }, + { + "type": "STRING", + "value": ";" + } + ] + }, + "constructor_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "constructor" + }, + { + "type": "FIELD", + "name": "parameters", + "content": { + "type": "SYMBOL", + "name": "parameter_list" + } + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "block" + } + } + ] + }, + "circuit_declaration": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "export" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "pure" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "circuit" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "generic_parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "FIELD", + "name": "parameters", + "content": { + "type": "SYMBOL", + "name": "parameter_list" + } + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "type", + "content": { + "type": "SYMBOL", + "name": "type_expression" + } + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "block" + } + } + ] + }, + "generic_parameters": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "<" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "generic_parameter" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "generic_parameter" + } + ] + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "BLANK" + } + ] + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": ">" + } + ] + }, + "generic_parameter": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "#" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + } + ] + }, + "generic_arguments": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "<" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_type_item" + } + }, + { + "type": "STRING", + "value": ">" + } + ] + }, + "type_expression": { + "type": "REPEAT1", + "content": { + "type": "SYMBOL", + "name": "_type_item" + } + }, + "_type_item": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "SYMBOL", + "name": "number" + }, + { + "type": "SYMBOL", + "name": "string" + }, + { + "type": "SYMBOL", + "name": "generic_arguments" + }, + { + "type": "SYMBOL", + "name": "_tuple_type" + }, + { + "type": "STRING", + "value": "," + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "STRING", + "value": ".." + } + ] + }, + "_tuple_type": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "[" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_type_item" + } + }, + { + "type": "STRING", + "value": "]" + } + ] + }, + "block": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "{" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_opaque" + } + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "parameter_list": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "(" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_opaque" + } + }, + { + "type": "STRING", + "value": ")" + } + ] + }, + "_paren_group": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "(" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_opaque" + } + }, + { + "type": "STRING", + "value": ")" + } + ] + }, + "_bracket_group": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "[" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "_opaque" + } + }, + { + "type": "STRING", + "value": "]" + } + ] + }, + "_opaque": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "block" + }, + { + "type": "SYMBOL", + "name": "_paren_group" + }, + { + "type": "SYMBOL", + "name": "_bracket_group" + }, + { + "type": "SYMBOL", + "name": "string" + }, + { + "type": "SYMBOL", + "name": "_opaque_token" + }, + { + "type": "STRING", + "value": "/" + } + ] + }, + "_opaque_token": { + "type": "TOKEN", + "content": { + "type": "PATTERN", + "value": "[^\\s(){}\\[\\]\"'\\/]+" + } + }, + "identifier": { + "type": "PATTERN", + "value": "[A-Za-z_$][A-Za-z0-9_$]*" + }, + "number": { + "type": "PATTERN", + "value": "[0-9]+" + }, + "string": { + "type": "TOKEN", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "\"" + }, + { + "type": "REPEAT", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "PATTERN", + "value": "[^\"\\\\]" + }, + { + "type": "PATTERN", + "value": "\\\\(.|\\r?\\n)" + } + ] + } + }, + { + "type": "STRING", + "value": "\"" + } + ] + }, + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "'" + }, + { + "type": "REPEAT", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "PATTERN", + "value": "[^'\\\\\\r\\n]" + }, + { + "type": "PATTERN", + "value": "\\\\." + } + ] + } + }, + { + "type": "STRING", + "value": "'" + } + ] + } + ] + } + }, + "doc_comment": { + "type": "TOKEN", + "content": { + "type": "PREC", + "value": 2, + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "/**" + }, + { + "type": "PATTERN", + "value": "[^*]*\\*+([^/*][^*]*\\*+)*" + }, + { + "type": "STRING", + "value": "/" + } + ] + } + } + }, + "block_comment": { + "type": "TOKEN", + "content": { + "type": "PREC", + "value": 1, + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "/*" + }, + { + "type": "PATTERN", + "value": "[^*]*\\*+([^/*][^*]*\\*+)*" + }, + { + "type": "STRING", + "value": "/" + } + ] + } + } + }, + "line_comment": { + "type": "TOKEN", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "//" + }, + { + "type": "PATTERN", + "value": "[^\\r\\n]*" + } + ] + } + } + }, + "extras": [ + { + "type": "PATTERN", + "value": "\\s" + }, + { + "type": "SYMBOL", + "name": "doc_comment" + }, + { + "type": "SYMBOL", + "name": "block_comment" + }, + { + "type": "SYMBOL", + "name": "line_comment" + } + ], + "conflicts": [], + "precedences": [], + "externals": [], + "inline": [], + "supertypes": [], + "reserved": {} +} \ No newline at end of file diff --git a/crates/tree-sitter-compact/src/node-types.json b/crates/tree-sitter-compact/src/node-types.json new file mode 100644 index 0000000..9c16e7d --- /dev/null +++ b/crates/tree-sitter-compact/src/node-types.json @@ -0,0 +1,913 @@ +[ + { + "type": "block", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "block", + "named": true + }, + { + "type": "string", + "named": true + } + ] + } + }, + { + "type": "circuit_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "block", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "parameters": { + "multiple": false, + "required": true, + "types": [ + { + "type": "parameter_list", + "named": true + } + ] + }, + "type": { + "multiple": false, + "required": true, + "types": [ + { + "type": "type_expression", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "generic_parameters", + "named": true + } + ] + } + }, + { + "type": "constructor_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "block", + "named": true + } + ] + }, + "parameters": { + "multiple": false, + "required": true, + "types": [ + { + "type": "parameter_list", + "named": true + } + ] + } + } + }, + { + "type": "contract_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "block", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "enum_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "block", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "export_declaration", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + { + "type": "generic_arguments", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "generic_arguments", + "named": true + }, + { + "type": "identifier", + "named": true + }, + { + "type": "number", + "named": true + }, + { + "type": "string", + "named": true + } + ] + } + }, + { + "type": "generic_parameter", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "generic_parameters", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "generic_parameter", + "named": true + } + ] + } + }, + { + "type": "implements_declaration", + "named": true, + "fields": { + "type": { + "multiple": false, + "required": true, + "types": [ + { + "type": "type_expression", + "named": true + } + ] + } + } + }, + { + "type": "import_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + }, + { + "type": "string", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "generic_arguments", + "named": true + }, + { + "type": "import_prefix", + "named": true + }, + { + "type": "import_selection", + "named": true + } + ] + } + }, + { + "type": "import_element", + "named": true, + "fields": { + "alias": { + "multiple": false, + "required": false, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "import_prefix", + "named": true, + "fields": { + "prefix": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "import_selection", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "import_element", + "named": true + } + ] + } + }, + { + "type": "include_declaration", + "named": true, + "fields": { + "path": { + "multiple": false, + "required": true, + "types": [ + { + "type": "string", + "named": true + } + ] + } + } + }, + { + "type": "ledger_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "type": { + "multiple": false, + "required": true, + "types": [ + { + "type": "type_expression", + "named": true + } + ] + } + } + }, + { + "type": "module_body", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "circuit_declaration", + "named": true + }, + { + "type": "constructor_declaration", + "named": true + }, + { + "type": "contract_declaration", + "named": true + }, + { + "type": "enum_declaration", + "named": true + }, + { + "type": "export_declaration", + "named": true + }, + { + "type": "implements_declaration", + "named": true + }, + { + "type": "import_declaration", + "named": true + }, + { + "type": "include_declaration", + "named": true + }, + { + "type": "ledger_declaration", + "named": true + }, + { + "type": "module_declaration", + "named": true + }, + { + "type": "pragma_declaration", + "named": true + }, + { + "type": "struct_declaration", + "named": true + }, + { + "type": "type_declaration", + "named": true + }, + { + "type": "witness_declaration", + "named": true + } + ] + } + }, + { + "type": "module_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "module_body", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "generic_parameters", + "named": true + } + ] + } + }, + { + "type": "parameter_list", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "block", + "named": true + }, + { + "type": "string", + "named": true + } + ] + } + }, + { + "type": "pragma_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "version_constraint", + "named": true + } + ] + } + }, + { + "type": "source_file", + "named": true, + "root": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "circuit_declaration", + "named": true + }, + { + "type": "constructor_declaration", + "named": true + }, + { + "type": "contract_declaration", + "named": true + }, + { + "type": "enum_declaration", + "named": true + }, + { + "type": "export_declaration", + "named": true + }, + { + "type": "implements_declaration", + "named": true + }, + { + "type": "import_declaration", + "named": true + }, + { + "type": "include_declaration", + "named": true + }, + { + "type": "ledger_declaration", + "named": true + }, + { + "type": "module_declaration", + "named": true + }, + { + "type": "pragma_declaration", + "named": true + }, + { + "type": "struct_declaration", + "named": true + }, + { + "type": "type_declaration", + "named": true + }, + { + "type": "witness_declaration", + "named": true + } + ] + } + }, + { + "type": "struct_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "block", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "generic_parameters", + "named": true + } + ] + } + }, + { + "type": "type_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "value": { + "multiple": false, + "required": true, + "types": [ + { + "type": "type_expression", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "generic_parameters", + "named": true + } + ] + } + }, + { + "type": "type_expression", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "generic_arguments", + "named": true + }, + { + "type": "identifier", + "named": true + }, + { + "type": "number", + "named": true + }, + { + "type": "string", + "named": true + } + ] + } + }, + { + "type": "witness_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "parameters": { + "multiple": false, + "required": true, + "types": [ + { + "type": "parameter_list", + "named": true + } + ] + }, + "type": { + "multiple": false, + "required": true, + "types": [ + { + "type": "type_expression", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "generic_parameters", + "named": true + } + ] + } + }, + { + "type": "#", + "named": false + }, + { + "type": "(", + "named": false + }, + { + "type": ")", + "named": false + }, + { + "type": ",", + "named": false + }, + { + "type": "..", + "named": false + }, + { + "type": "/", + "named": false + }, + { + "type": ":", + "named": false + }, + { + "type": ";", + "named": false + }, + { + "type": "<", + "named": false + }, + { + "type": "=", + "named": false + }, + { + "type": ">", + "named": false + }, + { + "type": "[", + "named": false + }, + { + "type": "]", + "named": false + }, + { + "type": "as", + "named": false + }, + { + "type": "block_comment", + "named": true, + "extra": true + }, + { + "type": "circuit", + "named": false + }, + { + "type": "constructor", + "named": false + }, + { + "type": "contract", + "named": false + }, + { + "type": "doc_comment", + "named": true, + "extra": true + }, + { + "type": "enum", + "named": false + }, + { + "type": "export", + "named": false + }, + { + "type": "from", + "named": false + }, + { + "type": "identifier", + "named": true + }, + { + "type": "implements", + "named": false + }, + { + "type": "import", + "named": false + }, + { + "type": "include", + "named": false + }, + { + "type": "ledger", + "named": false + }, + { + "type": "line_comment", + "named": true, + "extra": true + }, + { + "type": "module", + "named": false + }, + { + "type": "new", + "named": false + }, + { + "type": "number", + "named": true + }, + { + "type": "pragma", + "named": false + }, + { + "type": "prefix", + "named": false + }, + { + "type": "pure", + "named": false + }, + { + "type": "sealed", + "named": false + }, + { + "type": "string", + "named": true + }, + { + "type": "struct", + "named": false + }, + { + "type": "type", + "named": false + }, + { + "type": "version_constraint", + "named": true + }, + { + "type": "witness", + "named": false + }, + { + "type": "{", + "named": false + }, + { + "type": "}", + "named": false + } +] \ No newline at end of file diff --git a/crates/tree-sitter-compact/src/parser.c b/crates/tree-sitter-compact/src/parser.c new file mode 100644 index 0000000..3166d21 --- /dev/null +++ b/crates/tree-sitter-compact/src/parser.c @@ -0,0 +1,7042 @@ +/* Automatically @generated by tree-sitter v0.25.8 */ + +#include "tree_sitter/parser.h" + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + +#define LANGUAGE_VERSION 15 +#define STATE_COUNT 265 +#define LARGE_STATE_COUNT 2 +#define SYMBOL_COUNT 81 +#define ALIAS_COUNT 0 +#define TOKEN_COUNT 44 +#define EXTERNAL_TOKEN_COUNT 0 +#define FIELD_COUNT 8 +#define MAX_ALIAS_SEQUENCE_LENGTH 9 +#define MAX_RESERVED_WORD_SET_SIZE 0 +#define PRODUCTION_ID_COUNT 32 +#define SUPERTYPE_COUNT 0 + +enum ts_symbol_identifiers { + sym_identifier = 1, + anon_sym_pragma = 2, + anon_sym_SEMI = 3, + sym_version_constraint = 4, + anon_sym_include = 5, + anon_sym_import = 6, + anon_sym_LBRACE = 7, + anon_sym_COMMA = 8, + anon_sym_RBRACE = 9, + anon_sym_from = 10, + anon_sym_as = 11, + anon_sym_prefix = 12, + anon_sym_export = 13, + anon_sym_module = 14, + anon_sym_struct = 15, + anon_sym_enum = 16, + anon_sym_contract = 17, + anon_sym_implements = 18, + anon_sym_new = 19, + anon_sym_type = 20, + anon_sym_EQ = 21, + anon_sym_sealed = 22, + anon_sym_ledger = 23, + anon_sym_COLON = 24, + anon_sym_witness = 25, + anon_sym_constructor = 26, + anon_sym_pure = 27, + anon_sym_circuit = 28, + anon_sym_LT = 29, + anon_sym_GT = 30, + anon_sym_POUND = 31, + anon_sym_DOT_DOT = 32, + anon_sym_LBRACK = 33, + anon_sym_RBRACK = 34, + anon_sym_LPAREN = 35, + anon_sym_RPAREN = 36, + anon_sym_SLASH = 37, + sym__opaque_token = 38, + sym_number = 39, + sym_string = 40, + sym_doc_comment = 41, + sym_block_comment = 42, + sym_line_comment = 43, + sym_source_file = 44, + sym__declaration = 45, + sym_pragma_declaration = 46, + sym_include_declaration = 47, + sym_import_declaration = 48, + sym_import_selection = 49, + sym_import_element = 50, + sym_import_prefix = 51, + sym_export_declaration = 52, + sym_module_declaration = 53, + sym_module_body = 54, + sym_struct_declaration = 55, + sym_enum_declaration = 56, + sym_implements_declaration = 57, + sym_contract_declaration = 58, + sym_type_declaration = 59, + sym_ledger_declaration = 60, + sym_witness_declaration = 61, + sym_constructor_declaration = 62, + sym_circuit_declaration = 63, + sym_generic_parameters = 64, + sym_generic_parameter = 65, + sym_generic_arguments = 66, + sym_type_expression = 67, + sym__type_item = 68, + sym__tuple_type = 69, + sym_block = 70, + sym_parameter_list = 71, + sym__paren_group = 72, + sym__bracket_group = 73, + sym__opaque = 74, + aux_sym_source_file_repeat1 = 75, + aux_sym_import_selection_repeat1 = 76, + aux_sym_export_declaration_repeat1 = 77, + aux_sym_generic_parameters_repeat1 = 78, + aux_sym_generic_arguments_repeat1 = 79, + aux_sym_block_repeat1 = 80, +}; + +static const char * const ts_symbol_names[] = { + [ts_builtin_sym_end] = "end", + [sym_identifier] = "identifier", + [anon_sym_pragma] = "pragma", + [anon_sym_SEMI] = ";", + [sym_version_constraint] = "version_constraint", + [anon_sym_include] = "include", + [anon_sym_import] = "import", + [anon_sym_LBRACE] = "{", + [anon_sym_COMMA] = ",", + [anon_sym_RBRACE] = "}", + [anon_sym_from] = "from", + [anon_sym_as] = "as", + [anon_sym_prefix] = "prefix", + [anon_sym_export] = "export", + [anon_sym_module] = "module", + [anon_sym_struct] = "struct", + [anon_sym_enum] = "enum", + [anon_sym_contract] = "contract", + [anon_sym_implements] = "implements", + [anon_sym_new] = "new", + [anon_sym_type] = "type", + [anon_sym_EQ] = "=", + [anon_sym_sealed] = "sealed", + [anon_sym_ledger] = "ledger", + [anon_sym_COLON] = ":", + [anon_sym_witness] = "witness", + [anon_sym_constructor] = "constructor", + [anon_sym_pure] = "pure", + [anon_sym_circuit] = "circuit", + [anon_sym_LT] = "<", + [anon_sym_GT] = ">", + [anon_sym_POUND] = "#", + [anon_sym_DOT_DOT] = "..", + [anon_sym_LBRACK] = "[", + [anon_sym_RBRACK] = "]", + [anon_sym_LPAREN] = "(", + [anon_sym_RPAREN] = ")", + [anon_sym_SLASH] = "/", + [sym__opaque_token] = "_opaque_token", + [sym_number] = "number", + [sym_string] = "string", + [sym_doc_comment] = "doc_comment", + [sym_block_comment] = "block_comment", + [sym_line_comment] = "line_comment", + [sym_source_file] = "source_file", + [sym__declaration] = "_declaration", + [sym_pragma_declaration] = "pragma_declaration", + [sym_include_declaration] = "include_declaration", + [sym_import_declaration] = "import_declaration", + [sym_import_selection] = "import_selection", + [sym_import_element] = "import_element", + [sym_import_prefix] = "import_prefix", + [sym_export_declaration] = "export_declaration", + [sym_module_declaration] = "module_declaration", + [sym_module_body] = "module_body", + [sym_struct_declaration] = "struct_declaration", + [sym_enum_declaration] = "enum_declaration", + [sym_implements_declaration] = "implements_declaration", + [sym_contract_declaration] = "contract_declaration", + [sym_type_declaration] = "type_declaration", + [sym_ledger_declaration] = "ledger_declaration", + [sym_witness_declaration] = "witness_declaration", + [sym_constructor_declaration] = "constructor_declaration", + [sym_circuit_declaration] = "circuit_declaration", + [sym_generic_parameters] = "generic_parameters", + [sym_generic_parameter] = "generic_parameter", + [sym_generic_arguments] = "generic_arguments", + [sym_type_expression] = "type_expression", + [sym__type_item] = "_type_item", + [sym__tuple_type] = "_tuple_type", + [sym_block] = "block", + [sym_parameter_list] = "parameter_list", + [sym__paren_group] = "_paren_group", + [sym__bracket_group] = "_bracket_group", + [sym__opaque] = "_opaque", + [aux_sym_source_file_repeat1] = "source_file_repeat1", + [aux_sym_import_selection_repeat1] = "import_selection_repeat1", + [aux_sym_export_declaration_repeat1] = "export_declaration_repeat1", + [aux_sym_generic_parameters_repeat1] = "generic_parameters_repeat1", + [aux_sym_generic_arguments_repeat1] = "generic_arguments_repeat1", + [aux_sym_block_repeat1] = "block_repeat1", +}; + +static const TSSymbol ts_symbol_map[] = { + [ts_builtin_sym_end] = ts_builtin_sym_end, + [sym_identifier] = sym_identifier, + [anon_sym_pragma] = anon_sym_pragma, + [anon_sym_SEMI] = anon_sym_SEMI, + [sym_version_constraint] = sym_version_constraint, + [anon_sym_include] = anon_sym_include, + [anon_sym_import] = anon_sym_import, + [anon_sym_LBRACE] = anon_sym_LBRACE, + [anon_sym_COMMA] = anon_sym_COMMA, + [anon_sym_RBRACE] = anon_sym_RBRACE, + [anon_sym_from] = anon_sym_from, + [anon_sym_as] = anon_sym_as, + [anon_sym_prefix] = anon_sym_prefix, + [anon_sym_export] = anon_sym_export, + [anon_sym_module] = anon_sym_module, + [anon_sym_struct] = anon_sym_struct, + [anon_sym_enum] = anon_sym_enum, + [anon_sym_contract] = anon_sym_contract, + [anon_sym_implements] = anon_sym_implements, + [anon_sym_new] = anon_sym_new, + [anon_sym_type] = anon_sym_type, + [anon_sym_EQ] = anon_sym_EQ, + [anon_sym_sealed] = anon_sym_sealed, + [anon_sym_ledger] = anon_sym_ledger, + [anon_sym_COLON] = anon_sym_COLON, + [anon_sym_witness] = anon_sym_witness, + [anon_sym_constructor] = anon_sym_constructor, + [anon_sym_pure] = anon_sym_pure, + [anon_sym_circuit] = anon_sym_circuit, + [anon_sym_LT] = anon_sym_LT, + [anon_sym_GT] = anon_sym_GT, + [anon_sym_POUND] = anon_sym_POUND, + [anon_sym_DOT_DOT] = anon_sym_DOT_DOT, + [anon_sym_LBRACK] = anon_sym_LBRACK, + [anon_sym_RBRACK] = anon_sym_RBRACK, + [anon_sym_LPAREN] = anon_sym_LPAREN, + [anon_sym_RPAREN] = anon_sym_RPAREN, + [anon_sym_SLASH] = anon_sym_SLASH, + [sym__opaque_token] = sym__opaque_token, + [sym_number] = sym_number, + [sym_string] = sym_string, + [sym_doc_comment] = sym_doc_comment, + [sym_block_comment] = sym_block_comment, + [sym_line_comment] = sym_line_comment, + [sym_source_file] = sym_source_file, + [sym__declaration] = sym__declaration, + [sym_pragma_declaration] = sym_pragma_declaration, + [sym_include_declaration] = sym_include_declaration, + [sym_import_declaration] = sym_import_declaration, + [sym_import_selection] = sym_import_selection, + [sym_import_element] = sym_import_element, + [sym_import_prefix] = sym_import_prefix, + [sym_export_declaration] = sym_export_declaration, + [sym_module_declaration] = sym_module_declaration, + [sym_module_body] = sym_module_body, + [sym_struct_declaration] = sym_struct_declaration, + [sym_enum_declaration] = sym_enum_declaration, + [sym_implements_declaration] = sym_implements_declaration, + [sym_contract_declaration] = sym_contract_declaration, + [sym_type_declaration] = sym_type_declaration, + [sym_ledger_declaration] = sym_ledger_declaration, + [sym_witness_declaration] = sym_witness_declaration, + [sym_constructor_declaration] = sym_constructor_declaration, + [sym_circuit_declaration] = sym_circuit_declaration, + [sym_generic_parameters] = sym_generic_parameters, + [sym_generic_parameter] = sym_generic_parameter, + [sym_generic_arguments] = sym_generic_arguments, + [sym_type_expression] = sym_type_expression, + [sym__type_item] = sym__type_item, + [sym__tuple_type] = sym__tuple_type, + [sym_block] = sym_block, + [sym_parameter_list] = sym_parameter_list, + [sym__paren_group] = sym__paren_group, + [sym__bracket_group] = sym__bracket_group, + [sym__opaque] = sym__opaque, + [aux_sym_source_file_repeat1] = aux_sym_source_file_repeat1, + [aux_sym_import_selection_repeat1] = aux_sym_import_selection_repeat1, + [aux_sym_export_declaration_repeat1] = aux_sym_export_declaration_repeat1, + [aux_sym_generic_parameters_repeat1] = aux_sym_generic_parameters_repeat1, + [aux_sym_generic_arguments_repeat1] = aux_sym_generic_arguments_repeat1, + [aux_sym_block_repeat1] = aux_sym_block_repeat1, +}; + +static const TSSymbolMetadata ts_symbol_metadata[] = { + [ts_builtin_sym_end] = { + .visible = false, + .named = true, + }, + [sym_identifier] = { + .visible = true, + .named = true, + }, + [anon_sym_pragma] = { + .visible = true, + .named = false, + }, + [anon_sym_SEMI] = { + .visible = true, + .named = false, + }, + [sym_version_constraint] = { + .visible = true, + .named = true, + }, + [anon_sym_include] = { + .visible = true, + .named = false, + }, + [anon_sym_import] = { + .visible = true, + .named = false, + }, + [anon_sym_LBRACE] = { + .visible = true, + .named = false, + }, + [anon_sym_COMMA] = { + .visible = true, + .named = false, + }, + [anon_sym_RBRACE] = { + .visible = true, + .named = false, + }, + [anon_sym_from] = { + .visible = true, + .named = false, + }, + [anon_sym_as] = { + .visible = true, + .named = false, + }, + [anon_sym_prefix] = { + .visible = true, + .named = false, + }, + [anon_sym_export] = { + .visible = true, + .named = false, + }, + [anon_sym_module] = { + .visible = true, + .named = false, + }, + [anon_sym_struct] = { + .visible = true, + .named = false, + }, + [anon_sym_enum] = { + .visible = true, + .named = false, + }, + [anon_sym_contract] = { + .visible = true, + .named = false, + }, + [anon_sym_implements] = { + .visible = true, + .named = false, + }, + [anon_sym_new] = { + .visible = true, + .named = false, + }, + [anon_sym_type] = { + .visible = true, + .named = false, + }, + [anon_sym_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_sealed] = { + .visible = true, + .named = false, + }, + [anon_sym_ledger] = { + .visible = true, + .named = false, + }, + [anon_sym_COLON] = { + .visible = true, + .named = false, + }, + [anon_sym_witness] = { + .visible = true, + .named = false, + }, + [anon_sym_constructor] = { + .visible = true, + .named = false, + }, + [anon_sym_pure] = { + .visible = true, + .named = false, + }, + [anon_sym_circuit] = { + .visible = true, + .named = false, + }, + [anon_sym_LT] = { + .visible = true, + .named = false, + }, + [anon_sym_GT] = { + .visible = true, + .named = false, + }, + [anon_sym_POUND] = { + .visible = true, + .named = false, + }, + [anon_sym_DOT_DOT] = { + .visible = true, + .named = false, + }, + [anon_sym_LBRACK] = { + .visible = true, + .named = false, + }, + [anon_sym_RBRACK] = { + .visible = true, + .named = false, + }, + [anon_sym_LPAREN] = { + .visible = true, + .named = false, + }, + [anon_sym_RPAREN] = { + .visible = true, + .named = false, + }, + [anon_sym_SLASH] = { + .visible = true, + .named = false, + }, + [sym__opaque_token] = { + .visible = false, + .named = true, + }, + [sym_number] = { + .visible = true, + .named = true, + }, + [sym_string] = { + .visible = true, + .named = true, + }, + [sym_doc_comment] = { + .visible = true, + .named = true, + }, + [sym_block_comment] = { + .visible = true, + .named = true, + }, + [sym_line_comment] = { + .visible = true, + .named = true, + }, + [sym_source_file] = { + .visible = true, + .named = true, + }, + [sym__declaration] = { + .visible = false, + .named = true, + }, + [sym_pragma_declaration] = { + .visible = true, + .named = true, + }, + [sym_include_declaration] = { + .visible = true, + .named = true, + }, + [sym_import_declaration] = { + .visible = true, + .named = true, + }, + [sym_import_selection] = { + .visible = true, + .named = true, + }, + [sym_import_element] = { + .visible = true, + .named = true, + }, + [sym_import_prefix] = { + .visible = true, + .named = true, + }, + [sym_export_declaration] = { + .visible = true, + .named = true, + }, + [sym_module_declaration] = { + .visible = true, + .named = true, + }, + [sym_module_body] = { + .visible = true, + .named = true, + }, + [sym_struct_declaration] = { + .visible = true, + .named = true, + }, + [sym_enum_declaration] = { + .visible = true, + .named = true, + }, + [sym_implements_declaration] = { + .visible = true, + .named = true, + }, + [sym_contract_declaration] = { + .visible = true, + .named = true, + }, + [sym_type_declaration] = { + .visible = true, + .named = true, + }, + [sym_ledger_declaration] = { + .visible = true, + .named = true, + }, + [sym_witness_declaration] = { + .visible = true, + .named = true, + }, + [sym_constructor_declaration] = { + .visible = true, + .named = true, + }, + [sym_circuit_declaration] = { + .visible = true, + .named = true, + }, + [sym_generic_parameters] = { + .visible = true, + .named = true, + }, + [sym_generic_parameter] = { + .visible = true, + .named = true, + }, + [sym_generic_arguments] = { + .visible = true, + .named = true, + }, + [sym_type_expression] = { + .visible = true, + .named = true, + }, + [sym__type_item] = { + .visible = false, + .named = true, + }, + [sym__tuple_type] = { + .visible = false, + .named = true, + }, + [sym_block] = { + .visible = true, + .named = true, + }, + [sym_parameter_list] = { + .visible = true, + .named = true, + }, + [sym__paren_group] = { + .visible = false, + .named = true, + }, + [sym__bracket_group] = { + .visible = false, + .named = true, + }, + [sym__opaque] = { + .visible = false, + .named = true, + }, + [aux_sym_source_file_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_import_selection_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_export_declaration_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_generic_parameters_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_generic_arguments_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_block_repeat1] = { + .visible = false, + .named = false, + }, +}; + +enum ts_field_identifiers { + field_alias = 1, + field_body = 2, + field_name = 3, + field_parameters = 4, + field_path = 5, + field_prefix = 6, + field_type = 7, + field_value = 8, +}; + +static const char * const ts_field_names[] = { + [0] = NULL, + [field_alias] = "alias", + [field_body] = "body", + [field_name] = "name", + [field_parameters] = "parameters", + [field_path] = "path", + [field_prefix] = "prefix", + [field_type] = "type", + [field_value] = "value", +}; + +static const TSMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = { + [1] = {.index = 0, .length = 1}, + [2] = {.index = 1, .length = 1}, + [3] = {.index = 2, .length = 1}, + [4] = {.index = 3, .length = 2}, + [5] = {.index = 5, .length = 2}, + [6] = {.index = 7, .length = 1}, + [7] = {.index = 8, .length = 1}, + [8] = {.index = 9, .length = 2}, + [9] = {.index = 11, .length = 2}, + [10] = {.index = 13, .length = 1}, + [11] = {.index = 14, .length = 2}, + [12] = {.index = 16, .length = 2}, + [13] = {.index = 18, .length = 2}, + [14] = {.index = 20, .length = 2}, + [15] = {.index = 22, .length = 2}, + [16] = {.index = 24, .length = 2}, + [17] = {.index = 26, .length = 2}, + [18] = {.index = 28, .length = 3}, + [19] = {.index = 31, .length = 4}, + [20] = {.index = 35, .length = 2}, + [21] = {.index = 37, .length = 2}, + [22] = {.index = 39, .length = 2}, + [23] = {.index = 41, .length = 3}, + [24] = {.index = 44, .length = 4}, + [25] = {.index = 48, .length = 3}, + [26] = {.index = 51, .length = 4}, + [27] = {.index = 55, .length = 2}, + [28] = {.index = 57, .length = 3}, + [29] = {.index = 60, .length = 4}, + [30] = {.index = 64, .length = 4}, + [31] = {.index = 68, .length = 4}, +}; + +static const TSFieldMapEntry ts_field_map_entries[] = { + [0] = + {field_path, 1}, + [1] = + {field_name, 0}, + [2] = + {field_name, 1}, + [3] = + {field_body, 2}, + {field_name, 1}, + [5] = + {field_body, 2}, + {field_parameters, 1}, + [7] = + {field_prefix, 1}, + [8] = + {field_name, 2}, + [9] = + {field_body, 3}, + {field_name, 2}, + [11] = + {field_body, 3}, + {field_name, 1}, + [13] = + {field_type, 2}, + [14] = + {field_alias, 2}, + {field_name, 0}, + [16] = + {field_body, 4}, + {field_name, 2}, + [18] = + {field_name, 1}, + {field_value, 3}, + [20] = + {field_name, 1}, + {field_type, 3}, + [22] = + {field_name, 2}, + {field_value, 4}, + [24] = + {field_name, 2}, + {field_type, 4}, + [26] = + {field_name, 1}, + {field_value, 4}, + [28] = + {field_name, 1}, + {field_parameters, 2}, + {field_type, 4}, + [31] = + {field_body, 5}, + {field_name, 1}, + {field_parameters, 2}, + {field_type, 4}, + [35] = + {field_name, 3}, + {field_value, 5}, + [37] = + {field_name, 2}, + {field_value, 5}, + [39] = + {field_name, 3}, + {field_type, 5}, + [41] = + {field_name, 2}, + {field_parameters, 3}, + {field_type, 5}, + [44] = + {field_body, 6}, + {field_name, 2}, + {field_parameters, 3}, + {field_type, 5}, + [48] = + {field_name, 1}, + {field_parameters, 3}, + {field_type, 5}, + [51] = + {field_body, 6}, + {field_name, 1}, + {field_parameters, 3}, + {field_type, 5}, + [55] = + {field_name, 3}, + {field_value, 6}, + [57] = + {field_name, 2}, + {field_parameters, 4}, + {field_type, 6}, + [60] = + {field_body, 7}, + {field_name, 3}, + {field_parameters, 4}, + {field_type, 6}, + [64] = + {field_body, 7}, + {field_name, 2}, + {field_parameters, 4}, + {field_type, 6}, + [68] = + {field_body, 8}, + {field_name, 3}, + {field_parameters, 5}, + {field_type, 7}, +}; + +static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { + [0] = {0}, +}; + +static const uint16_t ts_non_terminal_alias_map[] = { + 0, +}; + +static const TSStateId ts_primary_state_ids[STATE_COUNT] = { + [0] = 0, + [1] = 1, + [2] = 2, + [3] = 3, + [4] = 4, + [5] = 5, + [6] = 6, + [7] = 7, + [8] = 8, + [9] = 9, + [10] = 10, + [11] = 11, + [12] = 12, + [13] = 13, + [14] = 14, + [15] = 15, + [16] = 16, + [17] = 17, + [18] = 18, + [19] = 19, + [20] = 20, + [21] = 21, + [22] = 22, + [23] = 23, + [24] = 24, + [25] = 25, + [26] = 26, + [27] = 27, + [28] = 28, + [29] = 29, + [30] = 30, + [31] = 31, + [32] = 32, + [33] = 33, + [34] = 34, + [35] = 35, + [36] = 36, + [37] = 37, + [38] = 38, + [39] = 39, + [40] = 40, + [41] = 41, + [42] = 42, + [43] = 43, + [44] = 44, + [45] = 45, + [46] = 46, + [47] = 47, + [48] = 48, + [49] = 49, + [50] = 50, + [51] = 51, + [52] = 52, + [53] = 53, + [54] = 54, + [55] = 55, + [56] = 56, + [57] = 57, + [58] = 58, + [59] = 59, + [60] = 60, + [61] = 61, + [62] = 62, + [63] = 63, + [64] = 64, + [65] = 65, + [66] = 66, + [67] = 67, + [68] = 68, + [69] = 69, + [70] = 70, + [71] = 71, + [72] = 72, + [73] = 73, + [74] = 74, + [75] = 75, + [76] = 76, + [77] = 77, + [78] = 78, + [79] = 79, + [80] = 80, + [81] = 81, + [82] = 82, + [83] = 83, + [84] = 84, + [85] = 85, + [86] = 86, + [87] = 87, + [88] = 88, + [89] = 89, + [90] = 90, + [91] = 91, + [92] = 92, + [93] = 93, + [94] = 94, + [95] = 95, + [96] = 96, + [97] = 97, + [98] = 96, + [99] = 99, + [100] = 100, + [101] = 101, + [102] = 102, + [103] = 103, + [104] = 104, + [105] = 105, + [106] = 106, + [107] = 107, + [108] = 100, + [109] = 109, + [110] = 110, + [111] = 111, + [112] = 112, + [113] = 15, + [114] = 18, + [115] = 115, + [116] = 116, + [117] = 117, + [118] = 118, + [119] = 119, + [120] = 120, + [121] = 121, + [122] = 122, + [123] = 123, + [124] = 124, + [125] = 125, + [126] = 126, + [127] = 127, + [128] = 128, + [129] = 129, + [130] = 130, + [131] = 131, + [132] = 132, + [133] = 133, + [134] = 134, + [135] = 135, + [136] = 136, + [137] = 137, + [138] = 138, + [139] = 139, + [140] = 140, + [141] = 141, + [142] = 142, + [143] = 143, + [144] = 144, + [145] = 145, + [146] = 146, + [147] = 147, + [148] = 148, + [149] = 149, + [150] = 150, + [151] = 151, + [152] = 152, + [153] = 153, + [154] = 154, + [155] = 155, + [156] = 156, + [157] = 157, + [158] = 158, + [159] = 159, + [160] = 160, + [161] = 161, + [162] = 162, + [163] = 163, + [164] = 164, + [165] = 165, + [166] = 166, + [167] = 167, + [168] = 168, + [169] = 169, + [170] = 170, + [171] = 171, + [172] = 172, + [173] = 173, + [174] = 174, + [175] = 175, + [176] = 176, + [177] = 177, + [178] = 178, + [179] = 179, + [180] = 180, + [181] = 181, + [182] = 182, + [183] = 183, + [184] = 184, + [185] = 185, + [186] = 186, + [187] = 187, + [188] = 188, + [189] = 189, + [190] = 190, + [191] = 191, + [192] = 192, + [193] = 193, + [194] = 194, + [195] = 195, + [196] = 196, + [197] = 197, + [198] = 198, + [199] = 199, + [200] = 200, + [201] = 201, + [202] = 202, + [203] = 203, + [204] = 204, + [205] = 205, + [206] = 206, + [207] = 207, + [208] = 208, + [209] = 209, + [210] = 210, + [211] = 211, + [212] = 212, + [213] = 213, + [214] = 214, + [215] = 215, + [216] = 216, + [217] = 217, + [218] = 218, + [219] = 219, + [220] = 220, + [221] = 221, + [222] = 222, + [223] = 223, + [224] = 224, + [225] = 225, + [226] = 226, + [227] = 227, + [228] = 228, + [229] = 229, + [230] = 230, + [231] = 231, + [232] = 232, + [233] = 233, + [234] = 234, + [235] = 235, + [236] = 236, + [237] = 237, + [238] = 238, + [239] = 239, + [240] = 240, + [241] = 241, + [242] = 242, + [243] = 243, + [244] = 244, + [245] = 245, + [246] = 246, + [247] = 247, + [248] = 248, + [249] = 249, + [250] = 250, + [251] = 251, + [252] = 252, + [253] = 253, + [254] = 254, + [255] = 255, + [256] = 256, + [257] = 257, + [258] = 258, + [259] = 259, + [260] = 260, + [261] = 261, + [262] = 262, + [263] = 263, + [264] = 264, +}; + +static const TSCharacterRange sym__opaque_token_character_set_1[] = { + {0, 0x08}, {0x0e, 0x1f}, {'!', '!'}, {'#', '&'}, {'*', '.'}, {'0', 'Z'}, {'\\', '\\'}, {'^', 'z'}, + {'|', '|'}, {'~', 0x10ffff}, +}; + +static bool ts_lex(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + if (eof) ADVANCE(14); + ADVANCE_MAP( + '"', 3, + '#', 33, + '\'', 4, + '(', 37, + ')', 38, + ',', 27, + '.', 11, + '/', 39, + ':', 30, + ';', 15, + '<', 31, + '=', 29, + '>', 32, + '[', 35, + ']', 36, + '{', 26, + '}', 28, + ); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(0); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(42); + if (lookahead == '$' || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(41); + END_STATE(); + case 1: + if (lookahead == '\r') ADVANCE(3); + if (lookahead != 0) ADVANCE(3); + END_STATE(); + case 2: + ADVANCE_MAP( + '"', 3, + '\'', 4, + '(', 37, + ')', 38, + '/', 39, + '[', 35, + ']', 36, + '{', 26, + '}', 28, + ); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(2); + if (lookahead != 0) ADVANCE(40); + END_STATE(); + case 3: + if (lookahead == '"') ADVANCE(43); + if (lookahead == '\\') ADVANCE(1); + if (lookahead != 0) ADVANCE(3); + END_STATE(); + case 4: + if (lookahead == '\'') ADVANCE(43); + if (lookahead == '\\') ADVANCE(13); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r') ADVANCE(4); + END_STATE(); + case 5: + if (lookahead == '*') ADVANCE(6); + if (lookahead != 0) ADVANCE(10); + END_STATE(); + case 6: + if (lookahead == '*') ADVANCE(7); + if (lookahead == '/') ADVANCE(46); + if (lookahead != 0) ADVANCE(8); + END_STATE(); + case 7: + if (lookahead == '*') ADVANCE(7); + if (lookahead == '/') ADVANCE(44); + if (lookahead != 0) ADVANCE(8); + END_STATE(); + case 8: + if (lookahead == '*') ADVANCE(7); + if (lookahead != 0) ADVANCE(8); + END_STATE(); + case 9: + if (lookahead == '*') ADVANCE(9); + if (lookahead == '/') ADVANCE(45); + if (lookahead != 0) ADVANCE(10); + END_STATE(); + case 10: + if (lookahead == '*') ADVANCE(9); + if (lookahead != 0) ADVANCE(10); + END_STATE(); + case 11: + if (lookahead == '.') ADVANCE(34); + END_STATE(); + case 12: + if (lookahead == '/') ADVANCE(16); + if (lookahead == '\n' || + lookahead == '\r') SKIP(12); + if (('\t' <= lookahead && lookahead <= '\f') || + lookahead == ' ') ADVANCE(23); + if (lookahead != 0 && + lookahead != ';' && + lookahead != '{' && + lookahead != '}') ADVANCE(25); + END_STATE(); + case 13: + if (lookahead != 0 && + lookahead != '\n') ADVANCE(4); + END_STATE(); + case 14: + ACCEPT_TOKEN(ts_builtin_sym_end); + END_STATE(); + case 15: + ACCEPT_TOKEN(anon_sym_SEMI); + END_STATE(); + case 16: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(17); + if (lookahead == '/') ADVANCE(24); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r' && + lookahead != ';' && + lookahead != '{' && + lookahead != '}') ADVANCE(25); + END_STATE(); + case 17: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(19); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(10); + if (lookahead != 0) ADVANCE(22); + END_STATE(); + case 18: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(18); + if (lookahead == '/') ADVANCE(44); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(8); + if (lookahead != 0) ADVANCE(20); + END_STATE(); + case 19: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(18); + if (lookahead == '/') ADVANCE(47); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(8); + if (lookahead != 0) ADVANCE(20); + END_STATE(); + case 20: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(18); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(8); + if (lookahead != 0) ADVANCE(20); + END_STATE(); + case 21: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(21); + if (lookahead == '/') ADVANCE(45); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(10); + if (lookahead != 0) ADVANCE(22); + END_STATE(); + case 22: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '*') ADVANCE(21); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(10); + if (lookahead != 0) ADVANCE(22); + END_STATE(); + case 23: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == '/') ADVANCE(16); + if (lookahead == '\t' || + lookahead == 0x0b || + lookahead == '\f' || + lookahead == ' ') ADVANCE(23); + if (lookahead != 0 && + (lookahead < '\t' || '\r' < lookahead) && + lookahead != ';' && + lookahead != '{' && + lookahead != '}') ADVANCE(25); + END_STATE(); + case 24: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(48); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r') ADVANCE(24); + END_STATE(); + case 25: + ACCEPT_TOKEN(sym_version_constraint); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r' && + lookahead != ';' && + lookahead != '{' && + lookahead != '}') ADVANCE(25); + END_STATE(); + case 26: + ACCEPT_TOKEN(anon_sym_LBRACE); + END_STATE(); + case 27: + ACCEPT_TOKEN(anon_sym_COMMA); + END_STATE(); + case 28: + ACCEPT_TOKEN(anon_sym_RBRACE); + END_STATE(); + case 29: + ACCEPT_TOKEN(anon_sym_EQ); + END_STATE(); + case 30: + ACCEPT_TOKEN(anon_sym_COLON); + END_STATE(); + case 31: + ACCEPT_TOKEN(anon_sym_LT); + END_STATE(); + case 32: + ACCEPT_TOKEN(anon_sym_GT); + END_STATE(); + case 33: + ACCEPT_TOKEN(anon_sym_POUND); + END_STATE(); + case 34: + ACCEPT_TOKEN(anon_sym_DOT_DOT); + END_STATE(); + case 35: + ACCEPT_TOKEN(anon_sym_LBRACK); + END_STATE(); + case 36: + ACCEPT_TOKEN(anon_sym_RBRACK); + END_STATE(); + case 37: + ACCEPT_TOKEN(anon_sym_LPAREN); + END_STATE(); + case 38: + ACCEPT_TOKEN(anon_sym_RPAREN); + END_STATE(); + case 39: + ACCEPT_TOKEN(anon_sym_SLASH); + if (lookahead == '*') ADVANCE(5); + if (lookahead == '/') ADVANCE(48); + END_STATE(); + case 40: + ACCEPT_TOKEN(sym__opaque_token); + if ((!eof && set_contains(sym__opaque_token_character_set_1, 10, lookahead))) ADVANCE(40); + END_STATE(); + case 41: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == '$' || + ('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(41); + END_STATE(); + case 42: + ACCEPT_TOKEN(sym_number); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(42); + END_STATE(); + case 43: + ACCEPT_TOKEN(sym_string); + END_STATE(); + case 44: + ACCEPT_TOKEN(sym_doc_comment); + END_STATE(); + case 45: + ACCEPT_TOKEN(sym_block_comment); + END_STATE(); + case 46: + ACCEPT_TOKEN(sym_block_comment); + if (lookahead == '*') ADVANCE(7); + if (lookahead != 0) ADVANCE(8); + END_STATE(); + case 47: + ACCEPT_TOKEN(sym_block_comment); + if (lookahead == '*') ADVANCE(18); + if (lookahead == '\n' || + lookahead == '\r' || + lookahead == ';' || + lookahead == '{' || + lookahead == '}') ADVANCE(8); + if (lookahead != 0) ADVANCE(20); + END_STATE(); + case 48: + ACCEPT_TOKEN(sym_line_comment); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r') ADVANCE(48); + END_STATE(); + default: + return false; + } +} + +static bool ts_lex_keywords(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + ADVANCE_MAP( + 'a', 1, + 'c', 2, + 'e', 3, + 'f', 4, + 'i', 5, + 'l', 6, + 'm', 7, + 'n', 8, + 'p', 9, + 's', 10, + 't', 11, + 'w', 12, + ); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(0); + END_STATE(); + case 1: + if (lookahead == 's') ADVANCE(13); + END_STATE(); + case 2: + if (lookahead == 'i') ADVANCE(14); + if (lookahead == 'o') ADVANCE(15); + END_STATE(); + case 3: + if (lookahead == 'n') ADVANCE(16); + if (lookahead == 'x') ADVANCE(17); + END_STATE(); + case 4: + if (lookahead == 'r') ADVANCE(18); + END_STATE(); + case 5: + if (lookahead == 'm') ADVANCE(19); + if (lookahead == 'n') ADVANCE(20); + END_STATE(); + case 6: + if (lookahead == 'e') ADVANCE(21); + END_STATE(); + case 7: + if (lookahead == 'o') ADVANCE(22); + END_STATE(); + case 8: + if (lookahead == 'e') ADVANCE(23); + END_STATE(); + case 9: + if (lookahead == 'r') ADVANCE(24); + if (lookahead == 'u') ADVANCE(25); + END_STATE(); + case 10: + if (lookahead == 'e') ADVANCE(26); + if (lookahead == 't') ADVANCE(27); + END_STATE(); + case 11: + if (lookahead == 'y') ADVANCE(28); + END_STATE(); + case 12: + if (lookahead == 'i') ADVANCE(29); + END_STATE(); + case 13: + ACCEPT_TOKEN(anon_sym_as); + END_STATE(); + case 14: + if (lookahead == 'r') ADVANCE(30); + END_STATE(); + case 15: + if (lookahead == 'n') ADVANCE(31); + END_STATE(); + case 16: + if (lookahead == 'u') ADVANCE(32); + END_STATE(); + case 17: + if (lookahead == 'p') ADVANCE(33); + END_STATE(); + case 18: + if (lookahead == 'o') ADVANCE(34); + END_STATE(); + case 19: + if (lookahead == 'p') ADVANCE(35); + END_STATE(); + case 20: + if (lookahead == 'c') ADVANCE(36); + END_STATE(); + case 21: + if (lookahead == 'd') ADVANCE(37); + END_STATE(); + case 22: + if (lookahead == 'd') ADVANCE(38); + END_STATE(); + case 23: + if (lookahead == 'w') ADVANCE(39); + END_STATE(); + case 24: + if (lookahead == 'a') ADVANCE(40); + if (lookahead == 'e') ADVANCE(41); + END_STATE(); + case 25: + if (lookahead == 'r') ADVANCE(42); + END_STATE(); + case 26: + if (lookahead == 'a') ADVANCE(43); + END_STATE(); + case 27: + if (lookahead == 'r') ADVANCE(44); + END_STATE(); + case 28: + if (lookahead == 'p') ADVANCE(45); + END_STATE(); + case 29: + if (lookahead == 't') ADVANCE(46); + END_STATE(); + case 30: + if (lookahead == 'c') ADVANCE(47); + END_STATE(); + case 31: + if (lookahead == 's') ADVANCE(48); + if (lookahead == 't') ADVANCE(49); + END_STATE(); + case 32: + if (lookahead == 'm') ADVANCE(50); + END_STATE(); + case 33: + if (lookahead == 'o') ADVANCE(51); + END_STATE(); + case 34: + if (lookahead == 'm') ADVANCE(52); + END_STATE(); + case 35: + if (lookahead == 'l') ADVANCE(53); + if (lookahead == 'o') ADVANCE(54); + END_STATE(); + case 36: + if (lookahead == 'l') ADVANCE(55); + END_STATE(); + case 37: + if (lookahead == 'g') ADVANCE(56); + END_STATE(); + case 38: + if (lookahead == 'u') ADVANCE(57); + END_STATE(); + case 39: + ACCEPT_TOKEN(anon_sym_new); + END_STATE(); + case 40: + if (lookahead == 'g') ADVANCE(58); + END_STATE(); + case 41: + if (lookahead == 'f') ADVANCE(59); + END_STATE(); + case 42: + if (lookahead == 'e') ADVANCE(60); + END_STATE(); + case 43: + if (lookahead == 'l') ADVANCE(61); + END_STATE(); + case 44: + if (lookahead == 'u') ADVANCE(62); + END_STATE(); + case 45: + if (lookahead == 'e') ADVANCE(63); + END_STATE(); + case 46: + if (lookahead == 'n') ADVANCE(64); + END_STATE(); + case 47: + if (lookahead == 'u') ADVANCE(65); + END_STATE(); + case 48: + if (lookahead == 't') ADVANCE(66); + END_STATE(); + case 49: + if (lookahead == 'r') ADVANCE(67); + END_STATE(); + case 50: + ACCEPT_TOKEN(anon_sym_enum); + END_STATE(); + case 51: + if (lookahead == 'r') ADVANCE(68); + END_STATE(); + case 52: + ACCEPT_TOKEN(anon_sym_from); + END_STATE(); + case 53: + if (lookahead == 'e') ADVANCE(69); + END_STATE(); + case 54: + if (lookahead == 'r') ADVANCE(70); + END_STATE(); + case 55: + if (lookahead == 'u') ADVANCE(71); + END_STATE(); + case 56: + if (lookahead == 'e') ADVANCE(72); + END_STATE(); + case 57: + if (lookahead == 'l') ADVANCE(73); + END_STATE(); + case 58: + if (lookahead == 'm') ADVANCE(74); + END_STATE(); + case 59: + if (lookahead == 'i') ADVANCE(75); + END_STATE(); + case 60: + ACCEPT_TOKEN(anon_sym_pure); + END_STATE(); + case 61: + if (lookahead == 'e') ADVANCE(76); + END_STATE(); + case 62: + if (lookahead == 'c') ADVANCE(77); + END_STATE(); + case 63: + ACCEPT_TOKEN(anon_sym_type); + END_STATE(); + case 64: + if (lookahead == 'e') ADVANCE(78); + END_STATE(); + case 65: + if (lookahead == 'i') ADVANCE(79); + END_STATE(); + case 66: + if (lookahead == 'r') ADVANCE(80); + END_STATE(); + case 67: + if (lookahead == 'a') ADVANCE(81); + END_STATE(); + case 68: + if (lookahead == 't') ADVANCE(82); + END_STATE(); + case 69: + if (lookahead == 'm') ADVANCE(83); + END_STATE(); + case 70: + if (lookahead == 't') ADVANCE(84); + END_STATE(); + case 71: + if (lookahead == 'd') ADVANCE(85); + END_STATE(); + case 72: + if (lookahead == 'r') ADVANCE(86); + END_STATE(); + case 73: + if (lookahead == 'e') ADVANCE(87); + END_STATE(); + case 74: + if (lookahead == 'a') ADVANCE(88); + END_STATE(); + case 75: + if (lookahead == 'x') ADVANCE(89); + END_STATE(); + case 76: + if (lookahead == 'd') ADVANCE(90); + END_STATE(); + case 77: + if (lookahead == 't') ADVANCE(91); + END_STATE(); + case 78: + if (lookahead == 's') ADVANCE(92); + END_STATE(); + case 79: + if (lookahead == 't') ADVANCE(93); + END_STATE(); + case 80: + if (lookahead == 'u') ADVANCE(94); + END_STATE(); + case 81: + if (lookahead == 'c') ADVANCE(95); + END_STATE(); + case 82: + ACCEPT_TOKEN(anon_sym_export); + END_STATE(); + case 83: + if (lookahead == 'e') ADVANCE(96); + END_STATE(); + case 84: + ACCEPT_TOKEN(anon_sym_import); + END_STATE(); + case 85: + if (lookahead == 'e') ADVANCE(97); + END_STATE(); + case 86: + ACCEPT_TOKEN(anon_sym_ledger); + END_STATE(); + case 87: + ACCEPT_TOKEN(anon_sym_module); + END_STATE(); + case 88: + ACCEPT_TOKEN(anon_sym_pragma); + END_STATE(); + case 89: + ACCEPT_TOKEN(anon_sym_prefix); + END_STATE(); + case 90: + ACCEPT_TOKEN(anon_sym_sealed); + END_STATE(); + case 91: + ACCEPT_TOKEN(anon_sym_struct); + END_STATE(); + case 92: + if (lookahead == 's') ADVANCE(98); + END_STATE(); + case 93: + ACCEPT_TOKEN(anon_sym_circuit); + END_STATE(); + case 94: + if (lookahead == 'c') ADVANCE(99); + END_STATE(); + case 95: + if (lookahead == 't') ADVANCE(100); + END_STATE(); + case 96: + if (lookahead == 'n') ADVANCE(101); + END_STATE(); + case 97: + ACCEPT_TOKEN(anon_sym_include); + END_STATE(); + case 98: + ACCEPT_TOKEN(anon_sym_witness); + END_STATE(); + case 99: + if (lookahead == 't') ADVANCE(102); + END_STATE(); + case 100: + ACCEPT_TOKEN(anon_sym_contract); + END_STATE(); + case 101: + if (lookahead == 't') ADVANCE(103); + END_STATE(); + case 102: + if (lookahead == 'o') ADVANCE(104); + END_STATE(); + case 103: + if (lookahead == 's') ADVANCE(105); + END_STATE(); + case 104: + if (lookahead == 'r') ADVANCE(106); + END_STATE(); + case 105: + ACCEPT_TOKEN(anon_sym_implements); + END_STATE(); + case 106: + ACCEPT_TOKEN(anon_sym_constructor); + END_STATE(); + default: + return false; + } +} + +static const TSLexerMode ts_lex_modes[STATE_COUNT] = { + [0] = {.lex_state = 0}, + [1] = {.lex_state = 0}, + [2] = {.lex_state = 0}, + [3] = {.lex_state = 0}, + [4] = {.lex_state = 0}, + [5] = {.lex_state = 0}, + [6] = {.lex_state = 0}, + [7] = {.lex_state = 0}, + [8] = {.lex_state = 0}, + [9] = {.lex_state = 0}, + [10] = {.lex_state = 0}, + [11] = {.lex_state = 0}, + [12] = {.lex_state = 0}, + [13] = {.lex_state = 0}, + [14] = {.lex_state = 0}, + [15] = {.lex_state = 0}, + [16] = {.lex_state = 0}, + [17] = {.lex_state = 0}, + [18] = {.lex_state = 0}, + [19] = {.lex_state = 0}, + [20] = {.lex_state = 0}, + [21] = {.lex_state = 0}, + [22] = {.lex_state = 0}, + [23] = {.lex_state = 0}, + [24] = {.lex_state = 0}, + [25] = {.lex_state = 0}, + [26] = {.lex_state = 0}, + [27] = {.lex_state = 0}, + [28] = {.lex_state = 0}, + [29] = {.lex_state = 0}, + [30] = {.lex_state = 0}, + [31] = {.lex_state = 0}, + [32] = {.lex_state = 0}, + [33] = {.lex_state = 0}, + [34] = {.lex_state = 0}, + [35] = {.lex_state = 0}, + [36] = {.lex_state = 0}, + [37] = {.lex_state = 0}, + [38] = {.lex_state = 0}, + [39] = {.lex_state = 0}, + [40] = {.lex_state = 0}, + [41] = {.lex_state = 0}, + [42] = {.lex_state = 0}, + [43] = {.lex_state = 0}, + [44] = {.lex_state = 0}, + [45] = {.lex_state = 0}, + [46] = {.lex_state = 0}, + [47] = {.lex_state = 0}, + [48] = {.lex_state = 0}, + [49] = {.lex_state = 0}, + [50] = {.lex_state = 0}, + [51] = {.lex_state = 0}, + [52] = {.lex_state = 0}, + [53] = {.lex_state = 0}, + [54] = {.lex_state = 0}, + [55] = {.lex_state = 0}, + [56] = {.lex_state = 0}, + [57] = {.lex_state = 0}, + [58] = {.lex_state = 0}, + [59] = {.lex_state = 0}, + [60] = {.lex_state = 0}, + [61] = {.lex_state = 0}, + [62] = {.lex_state = 0}, + [63] = {.lex_state = 0}, + [64] = {.lex_state = 0}, + [65] = {.lex_state = 0}, + [66] = {.lex_state = 0}, + [67] = {.lex_state = 0}, + [68] = {.lex_state = 2}, + [69] = {.lex_state = 0}, + [70] = {.lex_state = 0}, + [71] = {.lex_state = 0}, + [72] = {.lex_state = 0}, + [73] = {.lex_state = 0}, + [74] = {.lex_state = 0}, + [75] = {.lex_state = 0}, + [76] = {.lex_state = 0}, + [77] = {.lex_state = 0}, + [78] = {.lex_state = 0}, + [79] = {.lex_state = 0}, + [80] = {.lex_state = 0}, + [81] = {.lex_state = 0}, + [82] = {.lex_state = 0}, + [83] = {.lex_state = 0}, + [84] = {.lex_state = 0}, + [85] = {.lex_state = 0}, + [86] = {.lex_state = 0}, + [87] = {.lex_state = 0}, + [88] = {.lex_state = 0}, + [89] = {.lex_state = 0}, + [90] = {.lex_state = 0}, + [91] = {.lex_state = 0}, + [92] = {.lex_state = 0}, + [93] = {.lex_state = 0}, + [94] = {.lex_state = 0}, + [95] = {.lex_state = 0}, + [96] = {.lex_state = 2}, + [97] = {.lex_state = 2}, + [98] = {.lex_state = 2}, + [99] = {.lex_state = 0}, + [100] = {.lex_state = 2}, + [101] = {.lex_state = 2}, + [102] = {.lex_state = 0}, + [103] = {.lex_state = 2}, + [104] = {.lex_state = 2}, + [105] = {.lex_state = 0}, + [106] = {.lex_state = 2}, + [107] = {.lex_state = 2}, + [108] = {.lex_state = 2}, + [109] = {.lex_state = 2}, + [110] = {.lex_state = 2}, + [111] = {.lex_state = 2}, + [112] = {.lex_state = 2}, + [113] = {.lex_state = 2}, + [114] = {.lex_state = 2}, + [115] = {.lex_state = 0}, + [116] = {.lex_state = 0}, + [117] = {.lex_state = 0}, + [118] = {.lex_state = 0}, + [119] = {.lex_state = 0}, + [120] = {.lex_state = 0}, + [121] = {.lex_state = 0}, + [122] = {.lex_state = 0}, + [123] = {.lex_state = 0}, + [124] = {.lex_state = 0}, + [125] = {.lex_state = 0}, + [126] = {.lex_state = 0}, + [127] = {.lex_state = 0}, + [128] = {.lex_state = 0}, + [129] = {.lex_state = 0}, + [130] = {.lex_state = 0}, + [131] = {.lex_state = 0}, + [132] = {.lex_state = 0}, + [133] = {.lex_state = 0}, + [134] = {.lex_state = 0}, + [135] = {.lex_state = 0}, + [136] = {.lex_state = 0}, + [137] = {.lex_state = 0}, + [138] = {.lex_state = 0}, + [139] = {.lex_state = 0}, + [140] = {.lex_state = 0}, + [141] = {.lex_state = 0}, + [142] = {.lex_state = 0}, + [143] = {.lex_state = 0}, + [144] = {.lex_state = 0}, + [145] = {.lex_state = 0}, + [146] = {.lex_state = 0}, + [147] = {.lex_state = 0}, + [148] = {.lex_state = 0}, + [149] = {.lex_state = 0}, + [150] = {.lex_state = 0}, + [151] = {.lex_state = 0}, + [152] = {.lex_state = 0}, + [153] = {.lex_state = 0}, + [154] = {.lex_state = 0}, + [155] = {.lex_state = 0}, + [156] = {.lex_state = 0}, + [157] = {.lex_state = 0}, + [158] = {.lex_state = 0}, + [159] = {.lex_state = 0}, + [160] = {.lex_state = 0}, + [161] = {.lex_state = 0}, + [162] = {.lex_state = 0}, + [163] = {.lex_state = 0}, + [164] = {.lex_state = 0}, + [165] = {.lex_state = 0}, + [166] = {.lex_state = 0}, + [167] = {.lex_state = 0}, + [168] = {.lex_state = 0}, + [169] = {.lex_state = 0}, + [170] = {.lex_state = 0}, + [171] = {.lex_state = 0}, + [172] = {.lex_state = 0}, + [173] = {.lex_state = 0}, + [174] = {.lex_state = 0}, + [175] = {.lex_state = 0}, + [176] = {.lex_state = 0}, + [177] = {.lex_state = 0}, + [178] = {.lex_state = 0}, + [179] = {.lex_state = 0}, + [180] = {.lex_state = 0}, + [181] = {.lex_state = 0}, + [182] = {.lex_state = 0}, + [183] = {.lex_state = 0}, + [184] = {.lex_state = 0}, + [185] = {.lex_state = 0}, + [186] = {.lex_state = 0}, + [187] = {.lex_state = 0}, + [188] = {.lex_state = 0}, + [189] = {.lex_state = 0}, + [190] = {.lex_state = 0}, + [191] = {.lex_state = 0}, + [192] = {.lex_state = 0}, + [193] = {.lex_state = 0}, + [194] = {.lex_state = 0}, + [195] = {.lex_state = 0}, + [196] = {.lex_state = 0}, + [197] = {.lex_state = 0}, + [198] = {.lex_state = 0}, + [199] = {.lex_state = 0}, + [200] = {.lex_state = 0}, + [201] = {.lex_state = 0}, + [202] = {.lex_state = 0}, + [203] = {.lex_state = 0}, + [204] = {.lex_state = 0}, + [205] = {.lex_state = 0}, + [206] = {.lex_state = 0}, + [207] = {.lex_state = 0}, + [208] = {.lex_state = 0}, + [209] = {.lex_state = 0}, + [210] = {.lex_state = 0}, + [211] = {.lex_state = 0}, + [212] = {.lex_state = 0}, + [213] = {.lex_state = 0}, + [214] = {.lex_state = 0}, + [215] = {.lex_state = 0}, + [216] = {.lex_state = 0}, + [217] = {.lex_state = 0}, + [218] = {.lex_state = 0}, + [219] = {.lex_state = 0}, + [220] = {.lex_state = 0}, + [221] = {.lex_state = 0}, + [222] = {.lex_state = 0}, + [223] = {.lex_state = 0}, + [224] = {.lex_state = 0}, + [225] = {.lex_state = 0}, + [226] = {.lex_state = 12}, + [227] = {.lex_state = 0}, + [228] = {.lex_state = 0}, + [229] = {.lex_state = 0}, + [230] = {.lex_state = 0}, + [231] = {.lex_state = 0}, + [232] = {.lex_state = 0}, + [233] = {.lex_state = 0}, + [234] = {.lex_state = 0}, + [235] = {.lex_state = 0}, + [236] = {.lex_state = 0}, + [237] = {.lex_state = 0}, + [238] = {.lex_state = 0}, + [239] = {.lex_state = 0}, + [240] = {.lex_state = 0}, + [241] = {.lex_state = 0}, + [242] = {.lex_state = 0}, + [243] = {.lex_state = 0}, + [244] = {.lex_state = 0}, + [245] = {.lex_state = 0}, + [246] = {.lex_state = 0}, + [247] = {.lex_state = 0}, + [248] = {.lex_state = 0}, + [249] = {.lex_state = 0}, + [250] = {.lex_state = 0}, + [251] = {.lex_state = 0}, + [252] = {.lex_state = 0}, + [253] = {.lex_state = 0}, + [254] = {.lex_state = 0}, + [255] = {.lex_state = 0}, + [256] = {.lex_state = 0}, + [257] = {.lex_state = 0}, + [258] = {.lex_state = 0}, + [259] = {.lex_state = 0}, + [260] = {.lex_state = 0}, + [261] = {.lex_state = 0}, + [262] = {.lex_state = 0}, + [263] = {.lex_state = 0}, + [264] = {.lex_state = 0}, +}; + +static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { + [STATE(0)] = { + [ts_builtin_sym_end] = ACTIONS(1), + [sym_identifier] = ACTIONS(1), + [anon_sym_pragma] = ACTIONS(1), + [anon_sym_SEMI] = ACTIONS(1), + [anon_sym_include] = ACTIONS(1), + [anon_sym_import] = ACTIONS(1), + [anon_sym_LBRACE] = ACTIONS(1), + [anon_sym_COMMA] = ACTIONS(1), + [anon_sym_RBRACE] = ACTIONS(1), + [anon_sym_from] = ACTIONS(1), + [anon_sym_as] = ACTIONS(1), + [anon_sym_prefix] = ACTIONS(1), + [anon_sym_export] = ACTIONS(1), + [anon_sym_module] = ACTIONS(1), + [anon_sym_struct] = ACTIONS(1), + [anon_sym_enum] = ACTIONS(1), + [anon_sym_contract] = ACTIONS(1), + [anon_sym_implements] = ACTIONS(1), + [anon_sym_new] = ACTIONS(1), + [anon_sym_type] = ACTIONS(1), + [anon_sym_EQ] = ACTIONS(1), + [anon_sym_sealed] = ACTIONS(1), + [anon_sym_ledger] = ACTIONS(1), + [anon_sym_COLON] = ACTIONS(1), + [anon_sym_witness] = ACTIONS(1), + [anon_sym_constructor] = ACTIONS(1), + [anon_sym_pure] = ACTIONS(1), + [anon_sym_circuit] = ACTIONS(1), + [anon_sym_LT] = ACTIONS(1), + [anon_sym_GT] = ACTIONS(1), + [anon_sym_POUND] = ACTIONS(1), + [anon_sym_DOT_DOT] = ACTIONS(1), + [anon_sym_LBRACK] = ACTIONS(1), + [anon_sym_RBRACK] = ACTIONS(1), + [anon_sym_LPAREN] = ACTIONS(1), + [anon_sym_RPAREN] = ACTIONS(1), + [anon_sym_SLASH] = ACTIONS(1), + [sym_number] = ACTIONS(1), + [sym_string] = ACTIONS(1), + [sym_doc_comment] = ACTIONS(3), + [sym_block_comment] = ACTIONS(5), + [sym_line_comment] = ACTIONS(3), + }, + [STATE(1)] = { + [sym_source_file] = STATE(207), + [sym__declaration] = STATE(3), + [sym_pragma_declaration] = STATE(3), + [sym_include_declaration] = STATE(3), + [sym_import_declaration] = STATE(3), + [sym_export_declaration] = STATE(3), + [sym_module_declaration] = STATE(3), + [sym_struct_declaration] = STATE(3), + [sym_enum_declaration] = STATE(3), + [sym_implements_declaration] = STATE(3), + [sym_contract_declaration] = STATE(3), + [sym_type_declaration] = STATE(3), + [sym_ledger_declaration] = STATE(3), + [sym_witness_declaration] = STATE(3), + [sym_constructor_declaration] = STATE(3), + [sym_circuit_declaration] = STATE(3), + [aux_sym_source_file_repeat1] = STATE(3), + [ts_builtin_sym_end] = ACTIONS(7), + [anon_sym_pragma] = ACTIONS(9), + [anon_sym_include] = ACTIONS(11), + [anon_sym_import] = ACTIONS(13), + [anon_sym_export] = ACTIONS(15), + [anon_sym_module] = ACTIONS(17), + [anon_sym_struct] = ACTIONS(19), + [anon_sym_enum] = ACTIONS(21), + [anon_sym_contract] = ACTIONS(23), + [anon_sym_new] = ACTIONS(25), + [anon_sym_type] = ACTIONS(27), + [anon_sym_sealed] = ACTIONS(29), + [anon_sym_ledger] = ACTIONS(31), + [anon_sym_witness] = ACTIONS(33), + [anon_sym_constructor] = ACTIONS(35), + [anon_sym_pure] = ACTIONS(37), + [anon_sym_circuit] = ACTIONS(39), + [sym_doc_comment] = ACTIONS(3), + [sym_block_comment] = ACTIONS(5), + [sym_line_comment] = ACTIONS(3), + }, +}; + +static const uint16_t ts_small_parse_table[] = { + [0] = 20, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(43), 1, + anon_sym_pragma, + ACTIONS(46), 1, + anon_sym_include, + ACTIONS(49), 1, + anon_sym_import, + ACTIONS(52), 1, + anon_sym_export, + ACTIONS(55), 1, + anon_sym_module, + ACTIONS(58), 1, + anon_sym_struct, + ACTIONS(61), 1, + anon_sym_enum, + ACTIONS(64), 1, + anon_sym_contract, + ACTIONS(67), 1, + anon_sym_new, + ACTIONS(70), 1, + anon_sym_type, + ACTIONS(73), 1, + anon_sym_sealed, + ACTIONS(76), 1, + anon_sym_ledger, + ACTIONS(79), 1, + anon_sym_witness, + ACTIONS(82), 1, + anon_sym_constructor, + ACTIONS(85), 1, + anon_sym_pure, + ACTIONS(88), 1, + anon_sym_circuit, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(41), 2, + ts_builtin_sym_end, + anon_sym_RBRACE, + STATE(2), 16, + sym__declaration, + sym_pragma_declaration, + sym_include_declaration, + sym_import_declaration, + sym_export_declaration, + sym_module_declaration, + sym_struct_declaration, + sym_enum_declaration, + sym_implements_declaration, + sym_contract_declaration, + sym_type_declaration, + sym_ledger_declaration, + sym_witness_declaration, + sym_constructor_declaration, + sym_circuit_declaration, + aux_sym_source_file_repeat1, + [78] = 20, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(9), 1, + anon_sym_pragma, + ACTIONS(11), 1, + anon_sym_include, + ACTIONS(13), 1, + anon_sym_import, + ACTIONS(15), 1, + anon_sym_export, + ACTIONS(17), 1, + anon_sym_module, + ACTIONS(19), 1, + anon_sym_struct, + ACTIONS(21), 1, + anon_sym_enum, + ACTIONS(23), 1, + anon_sym_contract, + ACTIONS(25), 1, + anon_sym_new, + ACTIONS(27), 1, + anon_sym_type, + ACTIONS(29), 1, + anon_sym_sealed, + ACTIONS(31), 1, + anon_sym_ledger, + ACTIONS(33), 1, + anon_sym_witness, + ACTIONS(35), 1, + anon_sym_constructor, + ACTIONS(37), 1, + anon_sym_pure, + ACTIONS(39), 1, + anon_sym_circuit, + ACTIONS(91), 1, + ts_builtin_sym_end, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(2), 16, + sym__declaration, + sym_pragma_declaration, + sym_include_declaration, + sym_import_declaration, + sym_export_declaration, + sym_module_declaration, + sym_struct_declaration, + sym_enum_declaration, + sym_implements_declaration, + sym_contract_declaration, + sym_type_declaration, + sym_ledger_declaration, + sym_witness_declaration, + sym_constructor_declaration, + sym_circuit_declaration, + aux_sym_source_file_repeat1, + [155] = 20, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(9), 1, + anon_sym_pragma, + ACTIONS(11), 1, + anon_sym_include, + ACTIONS(13), 1, + anon_sym_import, + ACTIONS(15), 1, + anon_sym_export, + ACTIONS(17), 1, + anon_sym_module, + ACTIONS(19), 1, + anon_sym_struct, + ACTIONS(21), 1, + anon_sym_enum, + ACTIONS(23), 1, + anon_sym_contract, + ACTIONS(25), 1, + anon_sym_new, + ACTIONS(27), 1, + anon_sym_type, + ACTIONS(29), 1, + anon_sym_sealed, + ACTIONS(31), 1, + anon_sym_ledger, + ACTIONS(33), 1, + anon_sym_witness, + ACTIONS(35), 1, + anon_sym_constructor, + ACTIONS(37), 1, + anon_sym_pure, + ACTIONS(39), 1, + anon_sym_circuit, + ACTIONS(93), 1, + anon_sym_RBRACE, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(5), 16, + sym__declaration, + sym_pragma_declaration, + sym_include_declaration, + sym_import_declaration, + sym_export_declaration, + sym_module_declaration, + sym_struct_declaration, + sym_enum_declaration, + sym_implements_declaration, + sym_contract_declaration, + sym_type_declaration, + sym_ledger_declaration, + sym_witness_declaration, + sym_constructor_declaration, + sym_circuit_declaration, + aux_sym_source_file_repeat1, + [232] = 20, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(9), 1, + anon_sym_pragma, + ACTIONS(11), 1, + anon_sym_include, + ACTIONS(13), 1, + anon_sym_import, + ACTIONS(15), 1, + anon_sym_export, + ACTIONS(17), 1, + anon_sym_module, + ACTIONS(19), 1, + anon_sym_struct, + ACTIONS(21), 1, + anon_sym_enum, + ACTIONS(23), 1, + anon_sym_contract, + ACTIONS(25), 1, + anon_sym_new, + ACTIONS(27), 1, + anon_sym_type, + ACTIONS(29), 1, + anon_sym_sealed, + ACTIONS(31), 1, + anon_sym_ledger, + ACTIONS(33), 1, + anon_sym_witness, + ACTIONS(35), 1, + anon_sym_constructor, + ACTIONS(37), 1, + anon_sym_pure, + ACTIONS(39), 1, + anon_sym_circuit, + ACTIONS(95), 1, + anon_sym_RBRACE, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(2), 16, + sym__declaration, + sym_pragma_declaration, + sym_include_declaration, + sym_import_declaration, + sym_export_declaration, + sym_module_declaration, + sym_struct_declaration, + sym_enum_declaration, + sym_implements_declaration, + sym_contract_declaration, + sym_type_declaration, + sym_ledger_declaration, + sym_witness_declaration, + sym_constructor_declaration, + sym_circuit_declaration, + aux_sym_source_file_repeat1, + [309] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(99), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(97), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [340] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(103), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(101), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [371] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(107), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(105), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [402] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(111), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(109), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [433] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(115), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(113), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [464] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(119), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(117), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [495] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(123), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(121), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [526] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(127), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(125), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [557] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(131), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(129), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [588] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(133), 19, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_SEMI, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [617] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(137), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(135), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [648] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(141), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(139), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [679] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(143), 19, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_SEMI, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [708] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(147), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(145), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [739] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(149), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [767] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(151), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [795] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(153), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [823] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(155), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [851] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(157), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [879] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(159), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [907] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(161), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [935] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(163), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [963] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(165), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [991] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(167), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1019] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(169), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1047] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(171), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1075] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(173), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1103] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(113), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1131] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(135), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1159] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(175), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1187] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(177), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1215] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(179), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1243] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(181), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1271] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(183), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1299] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(185), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1327] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(187), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1355] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(189), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1383] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(191), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1411] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(193), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1439] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(195), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1467] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(145), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1495] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(197), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1523] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(199), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1551] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(201), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1579] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(203), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1607] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(205), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1635] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(207), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1663] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(209), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1691] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(211), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1719] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(213), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1747] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(215), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1775] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(217), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1803] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(219), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1831] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(221), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1859] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(223), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1887] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(225), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1915] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(227), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1943] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(229), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1971] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(231), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [1999] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(233), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [2027] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(235), 18, + ts_builtin_sym_end, + anon_sym_pragma, + anon_sym_include, + anon_sym_import, + anon_sym_RBRACE, + anon_sym_export, + anon_sym_module, + anon_sym_struct, + anon_sym_enum, + anon_sym_contract, + anon_sym_new, + anon_sym_type, + anon_sym_sealed, + anon_sym_ledger, + anon_sym_witness, + anon_sym_constructor, + anon_sym_pure, + anon_sym_circuit, + [2055] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(242), 1, + anon_sym_LT, + ACTIONS(245), 1, + anon_sym_LBRACK, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(240), 4, + anon_sym_SEMI, + anon_sym_LBRACE, + anon_sym_GT, + anon_sym_RBRACK, + STATE(67), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(237), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2089] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(248), 1, + anon_sym_LBRACE, + ACTIONS(253), 1, + anon_sym_LBRACK, + ACTIONS(256), 1, + anon_sym_LPAREN, + ACTIONS(259), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(262), 2, + sym__opaque_token, + sym_string, + ACTIONS(251), 3, + anon_sym_RBRACE, + anon_sym_RBRACK, + anon_sym_RPAREN, + STATE(68), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [2125] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(267), 2, + anon_sym_SEMI, + anon_sym_LBRACE, + STATE(67), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(265), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2157] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(163), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2188] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(187), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2219] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + ACTIONS(277), 1, + anon_sym_RBRACK, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(79), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(275), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2250] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(223), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2281] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(229), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2312] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(279), 2, + anon_sym_prefix, + sym_identifier, + ACTIONS(281), 11, + anon_sym_SEMI, + anon_sym_LBRACE, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_LT, + anon_sym_GT, + anon_sym_DOT_DOT, + anon_sym_LBRACK, + anon_sym_RBRACK, + sym_number, + sym_string, + [2337] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + ACTIONS(283), 1, + anon_sym_GT, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(67), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(265), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2368] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(247), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2399] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(252), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2430] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + ACTIONS(285), 1, + anon_sym_RBRACK, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(67), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(265), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2461] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(201), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2492] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(204), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2523] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(287), 2, + anon_sym_prefix, + sym_identifier, + ACTIONS(289), 11, + anon_sym_SEMI, + anon_sym_LBRACE, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_LT, + anon_sym_GT, + anon_sym_DOT_DOT, + anon_sym_LBRACK, + anon_sym_RBRACK, + sym_number, + sym_string, + [2548] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(232), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2579] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(220), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2610] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(221), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2641] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(224), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2672] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(171), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2703] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(234), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2734] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(177), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2765] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(241), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2796] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(244), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2827] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(181), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2858] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(182), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2889] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + ACTIONS(293), 1, + anon_sym_GT, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(76), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(291), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2920] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(271), 1, + anon_sym_LBRACK, + STATE(217), 1, + sym_type_expression, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + STATE(69), 4, + sym_generic_arguments, + sym__type_item, + sym__tuple_type, + aux_sym_generic_arguments_repeat1, + ACTIONS(273), 6, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_DOT_DOT, + sym_identifier, + sym_number, + sym_string, + [2951] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(297), 1, + anon_sym_RBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(303), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(305), 2, + sym__opaque_token, + sym_string, + STATE(68), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [2985] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(307), 1, + anon_sym_RBRACK, + ACTIONS(309), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(311), 2, + sym__opaque_token, + sym_string, + STATE(104), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3019] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(303), 1, + anon_sym_SLASH, + ACTIONS(313), 1, + anon_sym_RBRACE, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(305), 2, + sym__opaque_token, + sym_string, + STATE(68), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3053] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(315), 12, + anon_sym_SEMI, + anon_sym_LBRACE, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_LT, + anon_sym_GT, + anon_sym_DOT_DOT, + anon_sym_LBRACK, + anon_sym_RBRACK, + sym_identifier, + sym_number, + sym_string, + [3075] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(317), 1, + anon_sym_RBRACE, + ACTIONS(319), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(321), 2, + sym__opaque_token, + sym_string, + STATE(98), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3109] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(323), 1, + anon_sym_RPAREN, + ACTIONS(325), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(327), 2, + sym__opaque_token, + sym_string, + STATE(106), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3143] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(329), 12, + anon_sym_SEMI, + anon_sym_LBRACE, + anon_sym_COMMA, + anon_sym_COLON, + anon_sym_LT, + anon_sym_GT, + anon_sym_DOT_DOT, + anon_sym_LBRACK, + anon_sym_RBRACK, + sym_identifier, + sym_number, + sym_string, + [3165] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(331), 1, + anon_sym_RPAREN, + ACTIONS(333), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(335), 2, + sym__opaque_token, + sym_string, + STATE(107), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3199] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(303), 1, + anon_sym_SLASH, + ACTIONS(337), 1, + anon_sym_RBRACK, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(305), 2, + sym__opaque_token, + sym_string, + STATE(68), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3233] = 14, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(339), 1, + anon_sym_LBRACE, + ACTIONS(341), 1, + anon_sym_module, + ACTIONS(343), 1, + anon_sym_struct, + ACTIONS(345), 1, + anon_sym_enum, + ACTIONS(347), 1, + anon_sym_contract, + ACTIONS(349), 1, + anon_sym_new, + ACTIONS(351), 1, + anon_sym_type, + ACTIONS(353), 1, + anon_sym_sealed, + ACTIONS(355), 1, + anon_sym_ledger, + ACTIONS(357), 1, + anon_sym_witness, + ACTIONS(359), 1, + anon_sym_pure, + ACTIONS(361), 1, + anon_sym_circuit, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3277] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(303), 1, + anon_sym_SLASH, + ACTIONS(363), 1, + anon_sym_RPAREN, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(305), 2, + sym__opaque_token, + sym_string, + STATE(68), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3311] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(303), 1, + anon_sym_SLASH, + ACTIONS(365), 1, + anon_sym_RPAREN, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(305), 2, + sym__opaque_token, + sym_string, + STATE(68), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3345] = 9, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(295), 1, + anon_sym_LBRACE, + ACTIONS(299), 1, + anon_sym_LBRACK, + ACTIONS(301), 1, + anon_sym_LPAREN, + ACTIONS(367), 1, + anon_sym_RBRACE, + ACTIONS(369), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(371), 2, + sym__opaque_token, + sym_string, + STATE(96), 5, + sym_block, + sym__paren_group, + sym__bracket_group, + sym__opaque, + aux_sym_block_repeat1, + [3379] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(375), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(373), 8, + anon_sym_LBRACE, + anon_sym_RBRACE, + anon_sym_LBRACK, + anon_sym_RBRACK, + anon_sym_LPAREN, + anon_sym_RPAREN, + sym__opaque_token, + sym_string, + [3400] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(379), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(377), 8, + anon_sym_LBRACE, + anon_sym_RBRACE, + anon_sym_LBRACK, + anon_sym_RBRACK, + anon_sym_LPAREN, + anon_sym_RPAREN, + sym__opaque_token, + sym_string, + [3421] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(383), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(381), 8, + anon_sym_LBRACE, + anon_sym_RBRACE, + anon_sym_LBRACK, + anon_sym_RBRACK, + anon_sym_LPAREN, + anon_sym_RPAREN, + sym__opaque_token, + sym_string, + [3442] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(387), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(385), 8, + anon_sym_LBRACE, + anon_sym_RBRACE, + anon_sym_LBRACK, + anon_sym_RBRACK, + anon_sym_LPAREN, + anon_sym_RPAREN, + sym__opaque_token, + sym_string, + [3463] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(389), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(133), 8, + anon_sym_LBRACE, + anon_sym_RBRACE, + anon_sym_LBRACK, + anon_sym_RBRACK, + anon_sym_LPAREN, + anon_sym_RPAREN, + sym__opaque_token, + sym_string, + [3484] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(391), 1, + anon_sym_SLASH, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(143), 8, + anon_sym_LBRACE, + anon_sym_RBRACE, + anon_sym_LBRACK, + anon_sym_RBRACK, + anon_sym_LPAREN, + anon_sym_RPAREN, + sym__opaque_token, + sym_string, + [3505] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(393), 1, + anon_sym_SEMI, + ACTIONS(395), 1, + anon_sym_prefix, + STATE(148), 1, + sym_generic_arguments, + STATE(237), 1, + sym_import_prefix, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3528] = 7, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(269), 1, + anon_sym_LT, + ACTIONS(395), 1, + anon_sym_prefix, + ACTIONS(397), 1, + anon_sym_SEMI, + STATE(140), 1, + sym_generic_arguments, + STATE(200), 1, + sym_import_prefix, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3551] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + ACTIONS(401), 1, + anon_sym_LT, + STATE(11), 1, + sym_block, + STATE(170), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3571] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + ACTIONS(401), 1, + anon_sym_LT, + STATE(7), 1, + sym_block, + STATE(168), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3591] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(403), 1, + sym_identifier, + ACTIONS(405), 1, + anon_sym_GT, + ACTIONS(407), 1, + anon_sym_POUND, + STATE(173), 1, + sym_generic_parameter, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3611] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(411), 1, + anon_sym_LBRACE, + STATE(158), 1, + sym_import_selection, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(409), 2, + sym_identifier, + sym_string, + [3629] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(159), 1, + sym_generic_parameters, + STATE(211), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3649] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(415), 1, + anon_sym_LBRACE, + STATE(41), 1, + sym_module_body, + STATE(191), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3669] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(415), 1, + anon_sym_LBRACE, + STATE(42), 1, + sym_module_body, + STATE(189), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3689] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(403), 1, + sym_identifier, + ACTIONS(407), 1, + anon_sym_POUND, + ACTIONS(417), 1, + anon_sym_GT, + STATE(132), 1, + sym_generic_parameter, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3709] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(188), 1, + sym_generic_parameters, + STATE(260), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3729] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(180), 1, + sym_generic_parameters, + STATE(203), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3749] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(403), 1, + sym_identifier, + ACTIONS(407), 1, + anon_sym_POUND, + ACTIONS(419), 1, + anon_sym_GT, + STATE(173), 1, + sym_generic_parameter, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3769] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(155), 1, + sym_generic_parameters, + STATE(259), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3789] = 6, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(190), 1, + sym_generic_parameters, + STATE(263), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3809] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(421), 3, + anon_sym_LBRACE, + anon_sym_EQ, + anon_sym_LPAREN, + [3822] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(423), 3, + anon_sym_LBRACE, + anon_sym_EQ, + anon_sym_LPAREN, + [3835] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(425), 1, + anon_sym_COMMA, + ACTIONS(427), 1, + anon_sym_GT, + STATE(139), 1, + aux_sym_generic_parameters_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3852] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(429), 1, + sym_identifier, + ACTIONS(431), 1, + anon_sym_RBRACE, + STATE(167), 1, + sym_import_element, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3869] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(433), 1, + anon_sym_COMMA, + ACTIONS(436), 1, + anon_sym_RBRACE, + STATE(134), 1, + aux_sym_import_selection_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3886] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(438), 1, + anon_sym_COMMA, + ACTIONS(441), 1, + anon_sym_RBRACE, + STATE(135), 1, + aux_sym_export_declaration_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3903] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(443), 1, + anon_sym_EQ, + STATE(250), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3920] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(429), 1, + sym_identifier, + ACTIONS(445), 1, + anon_sym_RBRACE, + STATE(167), 1, + sym_import_element, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3937] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(445), 1, + anon_sym_RBRACE, + ACTIONS(447), 1, + anon_sym_COMMA, + STATE(134), 1, + aux_sym_import_selection_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3954] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(419), 1, + anon_sym_GT, + ACTIONS(449), 1, + anon_sym_COMMA, + STATE(146), 1, + aux_sym_generic_parameters_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3971] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(395), 1, + anon_sym_prefix, + ACTIONS(451), 1, + anon_sym_SEMI, + STATE(219), 1, + sym_import_prefix, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [3988] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(453), 1, + anon_sym_COMMA, + ACTIONS(455), 1, + anon_sym_RBRACE, + STATE(135), 1, + aux_sym_export_declaration_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4005] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(459), 1, + anon_sym_as, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(457), 2, + anon_sym_COMMA, + anon_sym_RBRACE, + [4020] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(461), 1, + anon_sym_COMMA, + ACTIONS(463), 1, + anon_sym_RBRACE, + STATE(138), 1, + aux_sym_import_selection_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4037] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(465), 3, + anon_sym_LBRACE, + anon_sym_EQ, + anon_sym_LPAREN, + [4050] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(429), 1, + sym_identifier, + ACTIONS(467), 1, + anon_sym_RBRACE, + STATE(143), 1, + sym_import_element, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4067] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(469), 1, + anon_sym_COMMA, + ACTIONS(472), 1, + anon_sym_GT, + STATE(146), 1, + aux_sym_generic_parameters_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4084] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(474), 1, + anon_sym_EQ, + STATE(243), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4101] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(395), 1, + anon_sym_prefix, + ACTIONS(476), 1, + anon_sym_SEMI, + STATE(197), 1, + sym_import_prefix, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4118] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(478), 1, + anon_sym_COMMA, + ACTIONS(480), 1, + anon_sym_RBRACE, + STATE(141), 1, + aux_sym_export_declaration_repeat1, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4135] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(403), 1, + sym_identifier, + ACTIONS(407), 1, + anon_sym_POUND, + STATE(173), 1, + sym_generic_parameter, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4152] = 5, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(401), 1, + anon_sym_LT, + ACTIONS(482), 1, + anon_sym_EQ, + STATE(245), 1, + sym_generic_parameters, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4169] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(484), 3, + anon_sym_LBRACE, + anon_sym_EQ, + anon_sym_LPAREN, + [4182] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(486), 2, + sym_identifier, + sym_string, + [4194] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(488), 2, + anon_sym_LBRACE, + anon_sym_COLON, + [4206] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(225), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4220] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(490), 2, + anon_sym_COMMA, + anon_sym_GT, + [4232] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(160), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4246] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(492), 2, + sym_identifier, + sym_string, + [4258] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(194), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4272] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(22), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4286] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(494), 2, + anon_sym_LBRACE, + anon_sym_COLON, + [4298] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(455), 1, + anon_sym_RBRACE, + ACTIONS(496), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4312] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(53), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4326] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(498), 2, + sym_identifier, + sym_string, + [4338] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(429), 1, + sym_identifier, + STATE(167), 1, + sym_import_element, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4352] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(500), 2, + anon_sym_COMMA, + anon_sym_RBRACE, + [4364] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(436), 2, + anon_sym_COMMA, + anon_sym_RBRACE, + [4376] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(14), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4390] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(502), 1, + sym_identifier, + ACTIONS(504), 1, + anon_sym_implements, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4404] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(17), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4418] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(58), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4432] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(506), 2, + anon_sym_COMMA, + anon_sym_GT, + [4444] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(472), 2, + anon_sym_COMMA, + anon_sym_GT, + [4456] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(508), 1, + sym_identifier, + ACTIONS(510), 1, + anon_sym_RBRACE, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4470] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(441), 2, + anon_sym_COMMA, + anon_sym_RBRACE, + [4482] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(512), 2, + sym_identifier, + sym_string, + [4494] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(60), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4508] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + ACTIONS(514), 2, + sym_identifier, + sym_string, + [4520] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(496), 1, + sym_identifier, + ACTIONS(516), 1, + anon_sym_RBRACE, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4534] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(230), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4548] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(63), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4562] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(64), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4576] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(8), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4590] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(12), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4604] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(13), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4618] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(9), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4632] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(399), 1, + anon_sym_LBRACE, + STATE(65), 1, + sym_block, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4646] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(253), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4660] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(415), 1, + anon_sym_LBRACE, + STATE(23), 1, + sym_module_body, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4674] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(413), 1, + anon_sym_LPAREN, + STATE(261), 1, + sym_parameter_list, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4688] = 4, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(415), 1, + anon_sym_LBRACE, + STATE(35), 1, + sym_module_body, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4702] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(518), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4713] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(520), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4724] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(522), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4735] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(524), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4746] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(526), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4757] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(528), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4768] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(530), 1, + anon_sym_from, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4779] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(361), 1, + anon_sym_circuit, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4790] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(451), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4801] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(532), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4812] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(534), 1, + sym_string, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4823] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(536), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4834] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(538), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4845] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(540), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4856] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(542), 1, + anon_sym_type, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4867] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(544), 1, + ts_builtin_sym_end, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4878] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(546), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4889] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(548), 1, + anon_sym_ledger, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4900] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(550), 1, + anon_sym_from, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4911] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(552), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4922] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(554), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4933] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(556), 1, + anon_sym_circuit, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4944] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(558), 1, + anon_sym_from, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4955] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(496), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4966] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(560), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4977] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(562), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4988] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(564), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [4999] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(566), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5010] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(568), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5021] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(570), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5032] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(572), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5043] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(574), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5054] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(576), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5065] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(578), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5076] = 2, + ACTIONS(580), 1, + sym_version_constraint, + ACTIONS(5), 3, + sym_doc_comment, + sym_block_comment, + sym_line_comment, + [5085] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(582), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5096] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(584), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5107] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(586), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5118] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(588), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5129] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(590), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5140] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(592), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5151] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(594), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5162] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(596), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5173] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(598), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5184] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(351), 1, + anon_sym_type, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5195] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(476), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5206] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(600), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5217] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(602), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5228] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(604), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5239] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(606), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5250] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(608), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5261] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(610), 1, + anon_sym_EQ, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5272] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(612), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5283] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(614), 1, + anon_sym_EQ, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5294] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(616), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5305] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(618), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5316] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(620), 1, + anon_sym_from, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5327] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(355), 1, + anon_sym_ledger, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5338] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(622), 1, + anon_sym_EQ, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5349] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(624), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5360] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(626), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5371] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(628), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5382] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(630), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5393] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(632), 1, + anon_sym_SEMI, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5404] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(634), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5415] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(636), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5426] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(638), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5437] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(640), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5448] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(642), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5459] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(644), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5470] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(646), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5481] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(648), 1, + anon_sym_COLON, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, + [5492] = 3, + ACTIONS(5), 1, + sym_block_comment, + ACTIONS(650), 1, + sym_identifier, + ACTIONS(3), 2, + sym_doc_comment, + sym_line_comment, +}; + +static const uint32_t ts_small_parse_table_map[] = { + [SMALL_STATE(2)] = 0, + [SMALL_STATE(3)] = 78, + [SMALL_STATE(4)] = 155, + [SMALL_STATE(5)] = 232, + [SMALL_STATE(6)] = 309, + [SMALL_STATE(7)] = 340, + [SMALL_STATE(8)] = 371, + [SMALL_STATE(9)] = 402, + [SMALL_STATE(10)] = 433, + [SMALL_STATE(11)] = 464, + [SMALL_STATE(12)] = 495, + [SMALL_STATE(13)] = 526, + [SMALL_STATE(14)] = 557, + [SMALL_STATE(15)] = 588, + [SMALL_STATE(16)] = 617, + [SMALL_STATE(17)] = 648, + [SMALL_STATE(18)] = 679, + [SMALL_STATE(19)] = 708, + [SMALL_STATE(20)] = 739, + [SMALL_STATE(21)] = 767, + [SMALL_STATE(22)] = 795, + [SMALL_STATE(23)] = 823, + [SMALL_STATE(24)] = 851, + [SMALL_STATE(25)] = 879, + [SMALL_STATE(26)] = 907, + [SMALL_STATE(27)] = 935, + [SMALL_STATE(28)] = 963, + [SMALL_STATE(29)] = 991, + [SMALL_STATE(30)] = 1019, + [SMALL_STATE(31)] = 1047, + [SMALL_STATE(32)] = 1075, + [SMALL_STATE(33)] = 1103, + [SMALL_STATE(34)] = 1131, + [SMALL_STATE(35)] = 1159, + [SMALL_STATE(36)] = 1187, + [SMALL_STATE(37)] = 1215, + [SMALL_STATE(38)] = 1243, + [SMALL_STATE(39)] = 1271, + [SMALL_STATE(40)] = 1299, + [SMALL_STATE(41)] = 1327, + [SMALL_STATE(42)] = 1355, + [SMALL_STATE(43)] = 1383, + [SMALL_STATE(44)] = 1411, + [SMALL_STATE(45)] = 1439, + [SMALL_STATE(46)] = 1467, + [SMALL_STATE(47)] = 1495, + [SMALL_STATE(48)] = 1523, + [SMALL_STATE(49)] = 1551, + [SMALL_STATE(50)] = 1579, + [SMALL_STATE(51)] = 1607, + [SMALL_STATE(52)] = 1635, + [SMALL_STATE(53)] = 1663, + [SMALL_STATE(54)] = 1691, + [SMALL_STATE(55)] = 1719, + [SMALL_STATE(56)] = 1747, + [SMALL_STATE(57)] = 1775, + [SMALL_STATE(58)] = 1803, + [SMALL_STATE(59)] = 1831, + [SMALL_STATE(60)] = 1859, + [SMALL_STATE(61)] = 1887, + [SMALL_STATE(62)] = 1915, + [SMALL_STATE(63)] = 1943, + [SMALL_STATE(64)] = 1971, + [SMALL_STATE(65)] = 1999, + [SMALL_STATE(66)] = 2027, + [SMALL_STATE(67)] = 2055, + [SMALL_STATE(68)] = 2089, + [SMALL_STATE(69)] = 2125, + [SMALL_STATE(70)] = 2157, + [SMALL_STATE(71)] = 2188, + [SMALL_STATE(72)] = 2219, + [SMALL_STATE(73)] = 2250, + [SMALL_STATE(74)] = 2281, + [SMALL_STATE(75)] = 2312, + [SMALL_STATE(76)] = 2337, + [SMALL_STATE(77)] = 2368, + [SMALL_STATE(78)] = 2399, + [SMALL_STATE(79)] = 2430, + [SMALL_STATE(80)] = 2461, + [SMALL_STATE(81)] = 2492, + [SMALL_STATE(82)] = 2523, + [SMALL_STATE(83)] = 2548, + [SMALL_STATE(84)] = 2579, + [SMALL_STATE(85)] = 2610, + [SMALL_STATE(86)] = 2641, + [SMALL_STATE(87)] = 2672, + [SMALL_STATE(88)] = 2703, + [SMALL_STATE(89)] = 2734, + [SMALL_STATE(90)] = 2765, + [SMALL_STATE(91)] = 2796, + [SMALL_STATE(92)] = 2827, + [SMALL_STATE(93)] = 2858, + [SMALL_STATE(94)] = 2889, + [SMALL_STATE(95)] = 2920, + [SMALL_STATE(96)] = 2951, + [SMALL_STATE(97)] = 2985, + [SMALL_STATE(98)] = 3019, + [SMALL_STATE(99)] = 3053, + [SMALL_STATE(100)] = 3075, + [SMALL_STATE(101)] = 3109, + [SMALL_STATE(102)] = 3143, + [SMALL_STATE(103)] = 3165, + [SMALL_STATE(104)] = 3199, + [SMALL_STATE(105)] = 3233, + [SMALL_STATE(106)] = 3277, + [SMALL_STATE(107)] = 3311, + [SMALL_STATE(108)] = 3345, + [SMALL_STATE(109)] = 3379, + [SMALL_STATE(110)] = 3400, + [SMALL_STATE(111)] = 3421, + [SMALL_STATE(112)] = 3442, + [SMALL_STATE(113)] = 3463, + [SMALL_STATE(114)] = 3484, + [SMALL_STATE(115)] = 3505, + [SMALL_STATE(116)] = 3528, + [SMALL_STATE(117)] = 3551, + [SMALL_STATE(118)] = 3571, + [SMALL_STATE(119)] = 3591, + [SMALL_STATE(120)] = 3611, + [SMALL_STATE(121)] = 3629, + [SMALL_STATE(122)] = 3649, + [SMALL_STATE(123)] = 3669, + [SMALL_STATE(124)] = 3689, + [SMALL_STATE(125)] = 3709, + [SMALL_STATE(126)] = 3729, + [SMALL_STATE(127)] = 3749, + [SMALL_STATE(128)] = 3769, + [SMALL_STATE(129)] = 3789, + [SMALL_STATE(130)] = 3809, + [SMALL_STATE(131)] = 3822, + [SMALL_STATE(132)] = 3835, + [SMALL_STATE(133)] = 3852, + [SMALL_STATE(134)] = 3869, + [SMALL_STATE(135)] = 3886, + [SMALL_STATE(136)] = 3903, + [SMALL_STATE(137)] = 3920, + [SMALL_STATE(138)] = 3937, + [SMALL_STATE(139)] = 3954, + [SMALL_STATE(140)] = 3971, + [SMALL_STATE(141)] = 3988, + [SMALL_STATE(142)] = 4005, + [SMALL_STATE(143)] = 4020, + [SMALL_STATE(144)] = 4037, + [SMALL_STATE(145)] = 4050, + [SMALL_STATE(146)] = 4067, + [SMALL_STATE(147)] = 4084, + [SMALL_STATE(148)] = 4101, + [SMALL_STATE(149)] = 4118, + [SMALL_STATE(150)] = 4135, + [SMALL_STATE(151)] = 4152, + [SMALL_STATE(152)] = 4169, + [SMALL_STATE(153)] = 4182, + [SMALL_STATE(154)] = 4194, + [SMALL_STATE(155)] = 4206, + [SMALL_STATE(156)] = 4220, + [SMALL_STATE(157)] = 4232, + [SMALL_STATE(158)] = 4246, + [SMALL_STATE(159)] = 4258, + [SMALL_STATE(160)] = 4272, + [SMALL_STATE(161)] = 4286, + [SMALL_STATE(162)] = 4298, + [SMALL_STATE(163)] = 4312, + [SMALL_STATE(164)] = 4326, + [SMALL_STATE(165)] = 4338, + [SMALL_STATE(166)] = 4352, + [SMALL_STATE(167)] = 4364, + [SMALL_STATE(168)] = 4376, + [SMALL_STATE(169)] = 4390, + [SMALL_STATE(170)] = 4404, + [SMALL_STATE(171)] = 4418, + [SMALL_STATE(172)] = 4432, + [SMALL_STATE(173)] = 4444, + [SMALL_STATE(174)] = 4456, + [SMALL_STATE(175)] = 4470, + [SMALL_STATE(176)] = 4482, + [SMALL_STATE(177)] = 4494, + [SMALL_STATE(178)] = 4508, + [SMALL_STATE(179)] = 4520, + [SMALL_STATE(180)] = 4534, + [SMALL_STATE(181)] = 4548, + [SMALL_STATE(182)] = 4562, + [SMALL_STATE(183)] = 4576, + [SMALL_STATE(184)] = 4590, + [SMALL_STATE(185)] = 4604, + [SMALL_STATE(186)] = 4618, + [SMALL_STATE(187)] = 4632, + [SMALL_STATE(188)] = 4646, + [SMALL_STATE(189)] = 4660, + [SMALL_STATE(190)] = 4674, + [SMALL_STATE(191)] = 4688, + [SMALL_STATE(192)] = 4702, + [SMALL_STATE(193)] = 4713, + [SMALL_STATE(194)] = 4724, + [SMALL_STATE(195)] = 4735, + [SMALL_STATE(196)] = 4746, + [SMALL_STATE(197)] = 4757, + [SMALL_STATE(198)] = 4768, + [SMALL_STATE(199)] = 4779, + [SMALL_STATE(200)] = 4790, + [SMALL_STATE(201)] = 4801, + [SMALL_STATE(202)] = 4812, + [SMALL_STATE(203)] = 4823, + [SMALL_STATE(204)] = 4834, + [SMALL_STATE(205)] = 4845, + [SMALL_STATE(206)] = 4856, + [SMALL_STATE(207)] = 4867, + [SMALL_STATE(208)] = 4878, + [SMALL_STATE(209)] = 4889, + [SMALL_STATE(210)] = 4900, + [SMALL_STATE(211)] = 4911, + [SMALL_STATE(212)] = 4922, + [SMALL_STATE(213)] = 4933, + [SMALL_STATE(214)] = 4944, + [SMALL_STATE(215)] = 4955, + [SMALL_STATE(216)] = 4966, + [SMALL_STATE(217)] = 4977, + [SMALL_STATE(218)] = 4988, + [SMALL_STATE(219)] = 4999, + [SMALL_STATE(220)] = 5010, + [SMALL_STATE(221)] = 5021, + [SMALL_STATE(222)] = 5032, + [SMALL_STATE(223)] = 5043, + [SMALL_STATE(224)] = 5054, + [SMALL_STATE(225)] = 5065, + [SMALL_STATE(226)] = 5076, + [SMALL_STATE(227)] = 5085, + [SMALL_STATE(228)] = 5096, + [SMALL_STATE(229)] = 5107, + [SMALL_STATE(230)] = 5118, + [SMALL_STATE(231)] = 5129, + [SMALL_STATE(232)] = 5140, + [SMALL_STATE(233)] = 5151, + [SMALL_STATE(234)] = 5162, + [SMALL_STATE(235)] = 5173, + [SMALL_STATE(236)] = 5184, + [SMALL_STATE(237)] = 5195, + [SMALL_STATE(238)] = 5206, + [SMALL_STATE(239)] = 5217, + [SMALL_STATE(240)] = 5228, + [SMALL_STATE(241)] = 5239, + [SMALL_STATE(242)] = 5250, + [SMALL_STATE(243)] = 5261, + [SMALL_STATE(244)] = 5272, + [SMALL_STATE(245)] = 5283, + [SMALL_STATE(246)] = 5294, + [SMALL_STATE(247)] = 5305, + [SMALL_STATE(248)] = 5316, + [SMALL_STATE(249)] = 5327, + [SMALL_STATE(250)] = 5338, + [SMALL_STATE(251)] = 5349, + [SMALL_STATE(252)] = 5360, + [SMALL_STATE(253)] = 5371, + [SMALL_STATE(254)] = 5382, + [SMALL_STATE(255)] = 5393, + [SMALL_STATE(256)] = 5404, + [SMALL_STATE(257)] = 5415, + [SMALL_STATE(258)] = 5426, + [SMALL_STATE(259)] = 5437, + [SMALL_STATE(260)] = 5448, + [SMALL_STATE(261)] = 5459, + [SMALL_STATE(262)] = 5470, + [SMALL_STATE(263)] = 5481, + [SMALL_STATE(264)] = 5492, +}; + +static const TSParseActionEntry ts_parse_actions[] = { + [0] = {.entry = {.count = 0, .reusable = false}}, + [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), + [3] = {.entry = {.count = 1, .reusable = true}}, SHIFT_EXTRA(), + [5] = {.entry = {.count = 1, .reusable = false}}, SHIFT_EXTRA(), + [7] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 0, 0, 0), + [9] = {.entry = {.count = 1, .reusable = true}}, SHIFT(218), + [11] = {.entry = {.count = 1, .reusable = true}}, SHIFT(202), + [13] = {.entry = {.count = 1, .reusable = true}}, SHIFT(120), + [15] = {.entry = {.count = 1, .reusable = true}}, SHIFT(105), + [17] = {.entry = {.count = 1, .reusable = true}}, SHIFT(242), + [19] = {.entry = {.count = 1, .reusable = true}}, SHIFT(231), + [21] = {.entry = {.count = 1, .reusable = true}}, SHIFT(262), + [23] = {.entry = {.count = 1, .reusable = true}}, SHIFT(169), + [25] = {.entry = {.count = 1, .reusable = true}}, SHIFT(236), + [27] = {.entry = {.count = 1, .reusable = true}}, SHIFT(240), + [29] = {.entry = {.count = 1, .reusable = true}}, SHIFT(249), + [31] = {.entry = {.count = 1, .reusable = true}}, SHIFT(239), + [33] = {.entry = {.count = 1, .reusable = true}}, SHIFT(222), + [35] = {.entry = {.count = 1, .reusable = true}}, SHIFT(157), + [37] = {.entry = {.count = 1, .reusable = true}}, SHIFT(199), + [39] = {.entry = {.count = 1, .reusable = true}}, SHIFT(205), + [41] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), + [43] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(218), + [46] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(202), + [49] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(120), + [52] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(105), + [55] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(242), + [58] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(231), + [61] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(262), + [64] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(169), + [67] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(236), + [70] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(240), + [73] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(249), + [76] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(239), + [79] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(222), + [82] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(157), + [85] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(199), + [88] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(205), + [91] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 1, 0, 0), + [93] = {.entry = {.count = 1, .reusable = true}}, SHIFT(21), + [95] = {.entry = {.count = 1, .reusable = true}}, SHIFT(40), + [97] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_export_declaration, 3, 0, 0), + [99] = {.entry = {.count = 1, .reusable = true}}, SHIFT(33), + [101] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 3, 0, 4), + [103] = {.entry = {.count = 1, .reusable = true}}, SHIFT(26), + [105] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 3, 0, 4), + [107] = {.entry = {.count = 1, .reusable = true}}, SHIFT(27), + [109] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_contract_declaration, 3, 0, 4), + [111] = {.entry = {.count = 1, .reusable = true}}, SHIFT(29), + [113] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_export_declaration, 4, 0, 0), + [115] = {.entry = {.count = 1, .reusable = true}}, SHIFT(34), + [117] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 4, 0, 8), + [119] = {.entry = {.count = 1, .reusable = true}}, SHIFT(37), + [121] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 4, 0, 8), + [123] = {.entry = {.count = 1, .reusable = true}}, SHIFT(38), + [125] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_contract_declaration, 4, 0, 8), + [127] = {.entry = {.count = 1, .reusable = true}}, SHIFT(39), + [129] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 4, 0, 9), + [131] = {.entry = {.count = 1, .reusable = true}}, SHIFT(66), + [133] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_block, 2, 0, 0), + [135] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_export_declaration, 5, 0, 0), + [137] = {.entry = {.count = 1, .reusable = true}}, SHIFT(46), + [139] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 5, 0, 12), + [141] = {.entry = {.count = 1, .reusable = true}}, SHIFT(48), + [143] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_block, 3, 0, 0), + [145] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_export_declaration, 6, 0, 0), + [147] = {.entry = {.count = 1, .reusable = true}}, SHIFT(54), + [149] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_ledger_declaration, 7, 0, 22), + [151] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_module_body, 2, 0, 0), + [153] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_constructor_declaration, 3, 0, 5), + [155] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_module_declaration, 4, 0, 9), + [157] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_pragma_declaration, 4, 0, 3), + [159] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_declaration, 4, 0, 3), + [161] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 4, 0, 4), + [163] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 4, 0, 4), + [165] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_implements_declaration, 4, 0, 10), + [167] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_contract_declaration, 4, 0, 4), + [169] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_declaration, 4, 0, 7), + [171] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_declaration, 5, 0, 3), + [173] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_declaration, 5, 0, 7), + [175] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_module_declaration, 5, 0, 12), + [177] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_include_declaration, 3, 0, 1), + [179] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 5, 0, 8), + [181] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 5, 0, 8), + [183] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_contract_declaration, 5, 0, 8), + [185] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_module_body, 3, 0, 0), + [187] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_module_declaration, 4, 0, 8), + [189] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_module_declaration, 3, 0, 4), + [191] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_declaration, 5, 0, 13), + [193] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_ledger_declaration, 5, 0, 14), + [195] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_declaration, 6, 0, 7), + [197] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_declaration, 3, 0, 3), + [199] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 6, 0, 12), + [201] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_declaration, 6, 0, 15), + [203] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_ledger_declaration, 6, 0, 16), + [205] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_declaration, 6, 0, 17), + [207] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_witness_declaration, 6, 0, 18), + [209] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_circuit_declaration, 6, 0, 19), + [211] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_export_declaration, 7, 0, 0), + [213] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_declaration, 7, 0, 20), + [215] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_declaration, 7, 0, 21), + [217] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_witness_declaration, 7, 0, 23), + [219] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_circuit_declaration, 7, 0, 24), + [221] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_witness_declaration, 7, 0, 25), + [223] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_circuit_declaration, 7, 0, 26), + [225] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_declaration, 8, 0, 27), + [227] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_witness_declaration, 8, 0, 28), + [229] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_circuit_declaration, 8, 0, 29), + [231] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_circuit_declaration, 8, 0, 30), + [233] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_circuit_declaration, 9, 0, 31), + [235] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_struct_declaration, 5, 0, 9), + [237] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_generic_arguments_repeat1, 2, 0, 0), SHIFT_REPEAT(67), + [240] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_generic_arguments_repeat1, 2, 0, 0), + [242] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_generic_arguments_repeat1, 2, 0, 0), SHIFT_REPEAT(94), + [245] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_generic_arguments_repeat1, 2, 0, 0), SHIFT_REPEAT(72), + [248] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_block_repeat1, 2, 0, 0), SHIFT_REPEAT(108), + [251] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_block_repeat1, 2, 0, 0), + [253] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_block_repeat1, 2, 0, 0), SHIFT_REPEAT(97), + [256] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_block_repeat1, 2, 0, 0), SHIFT_REPEAT(103), + [259] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_block_repeat1, 2, 0, 0), SHIFT_REPEAT(68), + [262] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_block_repeat1, 2, 0, 0), SHIFT_REPEAT(68), + [265] = {.entry = {.count = 1, .reusable = true}}, SHIFT(67), + [267] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_type_expression, 1, 0, 0), + [269] = {.entry = {.count = 1, .reusable = true}}, SHIFT(94), + [271] = {.entry = {.count = 1, .reusable = true}}, SHIFT(72), + [273] = {.entry = {.count = 1, .reusable = true}}, SHIFT(69), + [275] = {.entry = {.count = 1, .reusable = true}}, SHIFT(79), + [277] = {.entry = {.count = 1, .reusable = true}}, SHIFT(99), + [279] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_generic_arguments, 2, 0, 0), + [281] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_arguments, 2, 0, 0), + [283] = {.entry = {.count = 1, .reusable = true}}, SHIFT(82), + [285] = {.entry = {.count = 1, .reusable = true}}, SHIFT(102), + [287] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_generic_arguments, 3, 0, 0), + [289] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_arguments, 3, 0, 0), + [291] = {.entry = {.count = 1, .reusable = true}}, SHIFT(76), + [293] = {.entry = {.count = 1, .reusable = true}}, SHIFT(75), + [295] = {.entry = {.count = 1, .reusable = true}}, SHIFT(108), + [297] = {.entry = {.count = 1, .reusable = true}}, SHIFT(114), + [299] = {.entry = {.count = 1, .reusable = true}}, SHIFT(97), + [301] = {.entry = {.count = 1, .reusable = true}}, SHIFT(103), + [303] = {.entry = {.count = 1, .reusable = false}}, SHIFT(68), + [305] = {.entry = {.count = 1, .reusable = true}}, SHIFT(68), + [307] = {.entry = {.count = 1, .reusable = true}}, SHIFT(112), + [309] = {.entry = {.count = 1, .reusable = false}}, SHIFT(104), + [311] = {.entry = {.count = 1, .reusable = true}}, SHIFT(104), + [313] = {.entry = {.count = 1, .reusable = true}}, SHIFT(18), + [315] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__tuple_type, 2, 0, 0), + [317] = {.entry = {.count = 1, .reusable = true}}, SHIFT(15), + [319] = {.entry = {.count = 1, .reusable = false}}, SHIFT(98), + [321] = {.entry = {.count = 1, .reusable = true}}, SHIFT(98), + [323] = {.entry = {.count = 1, .reusable = true}}, SHIFT(154), + [325] = {.entry = {.count = 1, .reusable = false}}, SHIFT(106), + [327] = {.entry = {.count = 1, .reusable = true}}, SHIFT(106), + [329] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__tuple_type, 3, 0, 0), + [331] = {.entry = {.count = 1, .reusable = true}}, SHIFT(111), + [333] = {.entry = {.count = 1, .reusable = false}}, SHIFT(107), + [335] = {.entry = {.count = 1, .reusable = true}}, SHIFT(107), + [337] = {.entry = {.count = 1, .reusable = true}}, SHIFT(109), + [339] = {.entry = {.count = 1, .reusable = true}}, SHIFT(174), + [341] = {.entry = {.count = 1, .reusable = true}}, SHIFT(228), + [343] = {.entry = {.count = 1, .reusable = true}}, SHIFT(235), + [345] = {.entry = {.count = 1, .reusable = true}}, SHIFT(251), + [347] = {.entry = {.count = 1, .reusable = true}}, SHIFT(256), + [349] = {.entry = {.count = 1, .reusable = true}}, SHIFT(206), + [351] = {.entry = {.count = 1, .reusable = true}}, SHIFT(208), + [353] = {.entry = {.count = 1, .reusable = true}}, SHIFT(209), + [355] = {.entry = {.count = 1, .reusable = true}}, SHIFT(193), + [357] = {.entry = {.count = 1, .reusable = true}}, SHIFT(195), + [359] = {.entry = {.count = 1, .reusable = true}}, SHIFT(213), + [361] = {.entry = {.count = 1, .reusable = true}}, SHIFT(216), + [363] = {.entry = {.count = 1, .reusable = true}}, SHIFT(161), + [365] = {.entry = {.count = 1, .reusable = true}}, SHIFT(110), + [367] = {.entry = {.count = 1, .reusable = true}}, SHIFT(113), + [369] = {.entry = {.count = 1, .reusable = false}}, SHIFT(96), + [371] = {.entry = {.count = 1, .reusable = true}}, SHIFT(96), + [373] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__bracket_group, 3, 0, 0), + [375] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym__bracket_group, 3, 0, 0), + [377] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__paren_group, 3, 0, 0), + [379] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym__paren_group, 3, 0, 0), + [381] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__paren_group, 2, 0, 0), + [383] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym__paren_group, 2, 0, 0), + [385] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__bracket_group, 2, 0, 0), + [387] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym__bracket_group, 2, 0, 0), + [389] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_block, 2, 0, 0), + [391] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_block, 3, 0, 0), + [393] = {.entry = {.count = 1, .reusable = true}}, SHIFT(47), + [395] = {.entry = {.count = 1, .reusable = true}}, SHIFT(233), + [397] = {.entry = {.count = 1, .reusable = true}}, SHIFT(30), + [399] = {.entry = {.count = 1, .reusable = true}}, SHIFT(100), + [401] = {.entry = {.count = 1, .reusable = true}}, SHIFT(124), + [403] = {.entry = {.count = 1, .reusable = true}}, SHIFT(172), + [405] = {.entry = {.count = 1, .reusable = true}}, SHIFT(130), + [407] = {.entry = {.count = 1, .reusable = true}}, SHIFT(196), + [409] = {.entry = {.count = 1, .reusable = true}}, SHIFT(115), + [411] = {.entry = {.count = 1, .reusable = true}}, SHIFT(145), + [413] = {.entry = {.count = 1, .reusable = true}}, SHIFT(101), + [415] = {.entry = {.count = 1, .reusable = true}}, SHIFT(4), + [417] = {.entry = {.count = 1, .reusable = true}}, SHIFT(131), + [419] = {.entry = {.count = 1, .reusable = true}}, SHIFT(144), + [421] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_parameters, 5, 0, 0), + [423] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_parameters, 2, 0, 0), + [425] = {.entry = {.count = 1, .reusable = true}}, SHIFT(127), + [427] = {.entry = {.count = 1, .reusable = true}}, SHIFT(152), + [429] = {.entry = {.count = 1, .reusable = true}}, SHIFT(142), + [431] = {.entry = {.count = 1, .reusable = true}}, SHIFT(210), + [433] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_import_selection_repeat1, 2, 0, 0), SHIFT_REPEAT(165), + [436] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_import_selection_repeat1, 2, 0, 0), + [438] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_export_declaration_repeat1, 2, 0, 0), SHIFT_REPEAT(215), + [441] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_export_declaration_repeat1, 2, 0, 0), + [443] = {.entry = {.count = 1, .reusable = true}}, SHIFT(73), + [445] = {.entry = {.count = 1, .reusable = true}}, SHIFT(198), + [447] = {.entry = {.count = 1, .reusable = true}}, SHIFT(133), + [449] = {.entry = {.count = 1, .reusable = true}}, SHIFT(119), + [451] = {.entry = {.count = 1, .reusable = true}}, SHIFT(32), + [453] = {.entry = {.count = 1, .reusable = true}}, SHIFT(179), + [455] = {.entry = {.count = 1, .reusable = true}}, SHIFT(16), + [457] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_element, 1, 0, 2), + [459] = {.entry = {.count = 1, .reusable = true}}, SHIFT(264), + [461] = {.entry = {.count = 1, .reusable = true}}, SHIFT(137), + [463] = {.entry = {.count = 1, .reusable = true}}, SHIFT(248), + [465] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_parameters, 4, 0, 0), + [467] = {.entry = {.count = 1, .reusable = true}}, SHIFT(214), + [469] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_generic_parameters_repeat1, 2, 0, 0), SHIFT_REPEAT(150), + [472] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_generic_parameters_repeat1, 2, 0, 0), + [474] = {.entry = {.count = 1, .reusable = true}}, SHIFT(95), + [476] = {.entry = {.count = 1, .reusable = true}}, SHIFT(25), + [478] = {.entry = {.count = 1, .reusable = true}}, SHIFT(162), + [480] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), + [482] = {.entry = {.count = 1, .reusable = true}}, SHIFT(77), + [484] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_parameters, 3, 0, 0), + [486] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_selection, 4, 0, 0), + [488] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameter_list, 2, 0, 0), + [490] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_parameter, 2, 0, 3), + [492] = {.entry = {.count = 1, .reusable = true}}, SHIFT(116), + [494] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameter_list, 3, 0, 0), + [496] = {.entry = {.count = 1, .reusable = true}}, SHIFT(175), + [498] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_selection, 5, 0, 0), + [500] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_element, 3, 0, 11), + [502] = {.entry = {.count = 1, .reusable = false}}, SHIFT(186), + [504] = {.entry = {.count = 1, .reusable = false}}, SHIFT(83), + [506] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_generic_parameter, 1, 0, 2), + [508] = {.entry = {.count = 1, .reusable = true}}, SHIFT(149), + [510] = {.entry = {.count = 1, .reusable = true}}, SHIFT(6), + [512] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_selection, 3, 0, 0), + [514] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_selection, 6, 0, 0), + [516] = {.entry = {.count = 1, .reusable = true}}, SHIFT(19), + [518] = {.entry = {.count = 1, .reusable = true}}, SHIFT(128), + [520] = {.entry = {.count = 1, .reusable = true}}, SHIFT(258), + [522] = {.entry = {.count = 1, .reusable = true}}, SHIFT(89), + [524] = {.entry = {.count = 1, .reusable = true}}, SHIFT(125), + [526] = {.entry = {.count = 1, .reusable = true}}, SHIFT(156), + [528] = {.entry = {.count = 1, .reusable = true}}, SHIFT(31), + [530] = {.entry = {.count = 1, .reusable = true}}, SHIFT(164), + [532] = {.entry = {.count = 1, .reusable = true}}, SHIFT(51), + [534] = {.entry = {.count = 1, .reusable = true}}, SHIFT(227), + [536] = {.entry = {.count = 1, .reusable = true}}, SHIFT(81), + [538] = {.entry = {.count = 1, .reusable = true}}, SHIFT(52), + [540] = {.entry = {.count = 1, .reusable = true}}, SHIFT(121), + [542] = {.entry = {.count = 1, .reusable = true}}, SHIFT(254), + [544] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), + [546] = {.entry = {.count = 1, .reusable = true}}, SHIFT(151), + [548] = {.entry = {.count = 1, .reusable = true}}, SHIFT(257), + [550] = {.entry = {.count = 1, .reusable = true}}, SHIFT(178), + [552] = {.entry = {.count = 1, .reusable = true}}, SHIFT(70), + [554] = {.entry = {.count = 1, .reusable = true}}, SHIFT(24), + [556] = {.entry = {.count = 1, .reusable = true}}, SHIFT(192), + [558] = {.entry = {.count = 1, .reusable = true}}, SHIFT(176), + [560] = {.entry = {.count = 1, .reusable = true}}, SHIFT(129), + [562] = {.entry = {.count = 1, .reusable = true}}, SHIFT(55), + [564] = {.entry = {.count = 1, .reusable = true}}, SHIFT(226), + [566] = {.entry = {.count = 1, .reusable = true}}, SHIFT(45), + [568] = {.entry = {.count = 1, .reusable = true}}, SHIFT(56), + [570] = {.entry = {.count = 1, .reusable = true}}, SHIFT(20), + [572] = {.entry = {.count = 1, .reusable = true}}, SHIFT(126), + [574] = {.entry = {.count = 1, .reusable = true}}, SHIFT(43), + [576] = {.entry = {.count = 1, .reusable = true}}, SHIFT(57), + [578] = {.entry = {.count = 1, .reusable = true}}, SHIFT(71), + [580] = {.entry = {.count = 1, .reusable = false}}, SHIFT(212), + [582] = {.entry = {.count = 1, .reusable = true}}, SHIFT(36), + [584] = {.entry = {.count = 1, .reusable = true}}, SHIFT(122), + [586] = {.entry = {.count = 1, .reusable = true}}, SHIFT(44), + [588] = {.entry = {.count = 1, .reusable = true}}, SHIFT(88), + [590] = {.entry = {.count = 1, .reusable = true}}, SHIFT(118), + [592] = {.entry = {.count = 1, .reusable = true}}, SHIFT(28), + [594] = {.entry = {.count = 1, .reusable = true}}, SHIFT(255), + [596] = {.entry = {.count = 1, .reusable = true}}, SHIFT(59), + [598] = {.entry = {.count = 1, .reusable = true}}, SHIFT(117), + [600] = {.entry = {.count = 1, .reusable = true}}, SHIFT(74), + [602] = {.entry = {.count = 1, .reusable = true}}, SHIFT(238), + [604] = {.entry = {.count = 1, .reusable = true}}, SHIFT(136), + [606] = {.entry = {.count = 1, .reusable = true}}, SHIFT(61), + [608] = {.entry = {.count = 1, .reusable = true}}, SHIFT(123), + [610] = {.entry = {.count = 1, .reusable = true}}, SHIFT(90), + [612] = {.entry = {.count = 1, .reusable = true}}, SHIFT(62), + [614] = {.entry = {.count = 1, .reusable = true}}, SHIFT(84), + [616] = {.entry = {.count = 1, .reusable = true}}, SHIFT(85), + [618] = {.entry = {.count = 1, .reusable = true}}, SHIFT(49), + [620] = {.entry = {.count = 1, .reusable = true}}, SHIFT(153), + [622] = {.entry = {.count = 1, .reusable = true}}, SHIFT(80), + [624] = {.entry = {.count = 1, .reusable = true}}, SHIFT(184), + [626] = {.entry = {.count = 1, .reusable = true}}, SHIFT(50), + [628] = {.entry = {.count = 1, .reusable = true}}, SHIFT(91), + [630] = {.entry = {.count = 1, .reusable = true}}, SHIFT(147), + [632] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_import_prefix, 2, 0, 6), + [634] = {.entry = {.count = 1, .reusable = true}}, SHIFT(185), + [636] = {.entry = {.count = 1, .reusable = true}}, SHIFT(246), + [638] = {.entry = {.count = 1, .reusable = true}}, SHIFT(78), + [640] = {.entry = {.count = 1, .reusable = true}}, SHIFT(92), + [642] = {.entry = {.count = 1, .reusable = true}}, SHIFT(86), + [644] = {.entry = {.count = 1, .reusable = true}}, SHIFT(93), + [646] = {.entry = {.count = 1, .reusable = true}}, SHIFT(183), + [648] = {.entry = {.count = 1, .reusable = true}}, SHIFT(87), + [650] = {.entry = {.count = 1, .reusable = true}}, SHIFT(166), +}; + +#ifdef __cplusplus +extern "C" { +#endif +#ifdef TREE_SITTER_HIDE_SYMBOLS +#define TS_PUBLIC +#elif defined(_WIN32) +#define TS_PUBLIC __declspec(dllexport) +#else +#define TS_PUBLIC __attribute__((visibility("default"))) +#endif + +TS_PUBLIC const TSLanguage *tree_sitter_compact(void) { + static const TSLanguage language = { + .abi_version = LANGUAGE_VERSION, + .symbol_count = SYMBOL_COUNT, + .alias_count = ALIAS_COUNT, + .token_count = TOKEN_COUNT, + .external_token_count = EXTERNAL_TOKEN_COUNT, + .state_count = STATE_COUNT, + .large_state_count = LARGE_STATE_COUNT, + .production_id_count = PRODUCTION_ID_COUNT, + .supertype_count = SUPERTYPE_COUNT, + .field_count = FIELD_COUNT, + .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, + .parse_table = &ts_parse_table[0][0], + .small_parse_table = ts_small_parse_table, + .small_parse_table_map = ts_small_parse_table_map, + .parse_actions = ts_parse_actions, + .symbol_names = ts_symbol_names, + .field_names = ts_field_names, + .field_map_slices = ts_field_map_slices, + .field_map_entries = ts_field_map_entries, + .symbol_metadata = ts_symbol_metadata, + .public_symbol_map = ts_symbol_map, + .alias_map = ts_non_terminal_alias_map, + .alias_sequences = &ts_alias_sequences[0][0], + .lex_modes = (const void*)ts_lex_modes, + .lex_fn = ts_lex, + .keyword_lex_fn = ts_lex_keywords, + .keyword_capture_token = sym_identifier, + .primary_state_ids = ts_primary_state_ids, + .name = "compact", + .max_reserved_word_set_size = 0, + .metadata = { + .major_version = 0, + .minor_version = 1, + .patch_version = 0, + }, + }; + return &language; +} +#ifdef __cplusplus +} +#endif diff --git a/crates/tree-sitter-compact/src/tree_sitter/alloc.h b/crates/tree-sitter-compact/src/tree_sitter/alloc.h new file mode 100644 index 0000000..1abdd12 --- /dev/null +++ b/crates/tree-sitter-compact/src/tree_sitter/alloc.h @@ -0,0 +1,54 @@ +#ifndef TREE_SITTER_ALLOC_H_ +#define TREE_SITTER_ALLOC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +// Allow clients to override allocation functions +#ifdef TREE_SITTER_REUSE_ALLOCATOR + +extern void *(*ts_current_malloc)(size_t size); +extern void *(*ts_current_calloc)(size_t count, size_t size); +extern void *(*ts_current_realloc)(void *ptr, size_t size); +extern void (*ts_current_free)(void *ptr); + +#ifndef ts_malloc +#define ts_malloc ts_current_malloc +#endif +#ifndef ts_calloc +#define ts_calloc ts_current_calloc +#endif +#ifndef ts_realloc +#define ts_realloc ts_current_realloc +#endif +#ifndef ts_free +#define ts_free ts_current_free +#endif + +#else + +#ifndef ts_malloc +#define ts_malloc malloc +#endif +#ifndef ts_calloc +#define ts_calloc calloc +#endif +#ifndef ts_realloc +#define ts_realloc realloc +#endif +#ifndef ts_free +#define ts_free free +#endif + +#endif + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_ALLOC_H_ diff --git a/crates/tree-sitter-compact/src/tree_sitter/array.h b/crates/tree-sitter-compact/src/tree_sitter/array.h new file mode 100644 index 0000000..a17a574 --- /dev/null +++ b/crates/tree-sitter-compact/src/tree_sitter/array.h @@ -0,0 +1,291 @@ +#ifndef TREE_SITTER_ARRAY_H_ +#define TREE_SITTER_ARRAY_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "./alloc.h" + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4101) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +#define Array(T) \ + struct { \ + T *contents; \ + uint32_t size; \ + uint32_t capacity; \ + } + +/// Initialize an array. +#define array_init(self) \ + ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL) + +/// Create an empty array. +#define array_new() \ + { NULL, 0, 0 } + +/// Get a pointer to the element at a given `index` in the array. +#define array_get(self, _index) \ + (assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index]) + +/// Get a pointer to the first element in the array. +#define array_front(self) array_get(self, 0) + +/// Get a pointer to the last element in the array. +#define array_back(self) array_get(self, (self)->size - 1) + +/// Clear the array, setting its size to zero. Note that this does not free any +/// memory allocated for the array's contents. +#define array_clear(self) ((self)->size = 0) + +/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is +/// less than the array's current capacity, this function has no effect. +#define array_reserve(self, new_capacity) \ + _array__reserve((Array *)(self), array_elem_size(self), new_capacity) + +/// Free any memory allocated for this array. Note that this does not free any +/// memory allocated for the array's contents. +#define array_delete(self) _array__delete((Array *)(self)) + +/// Push a new `element` onto the end of the array. +#define array_push(self, element) \ + (_array__grow((Array *)(self), 1, array_elem_size(self)), \ + (self)->contents[(self)->size++] = (element)) + +/// Increase the array's size by `count` elements. +/// New elements are zero-initialized. +#define array_grow_by(self, count) \ + do { \ + if ((count) == 0) break; \ + _array__grow((Array *)(self), count, array_elem_size(self)); \ + memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \ + (self)->size += (count); \ + } while (0) + +/// Append all elements from one array to the end of another. +#define array_push_all(self, other) \ + array_extend((self), (other)->size, (other)->contents) + +/// Append `count` elements to the end of the array, reading their values from the +/// `contents` pointer. +#define array_extend(self, count, contents) \ + _array__splice( \ + (Array *)(self), array_elem_size(self), (self)->size, \ + 0, count, contents \ + ) + +/// Remove `old_count` elements from the array starting at the given `index`. At +/// the same index, insert `new_count` new elements, reading their values from the +/// `new_contents` pointer. +#define array_splice(self, _index, old_count, new_count, new_contents) \ + _array__splice( \ + (Array *)(self), array_elem_size(self), _index, \ + old_count, new_count, new_contents \ + ) + +/// Insert one `element` into the array at the given `index`. +#define array_insert(self, _index, element) \ + _array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element)) + +/// Remove one element from the array at the given `index`. +#define array_erase(self, _index) \ + _array__erase((Array *)(self), array_elem_size(self), _index) + +/// Pop the last element off the array, returning the element by value. +#define array_pop(self) ((self)->contents[--(self)->size]) + +/// Assign the contents of one array to another, reallocating if necessary. +#define array_assign(self, other) \ + _array__assign((Array *)(self), (const Array *)(other), array_elem_size(self)) + +/// Swap one array with another +#define array_swap(self, other) \ + _array__swap((Array *)(self), (Array *)(other)) + +/// Get the size of the array contents +#define array_elem_size(self) (sizeof *(self)->contents) + +/// Search a sorted array for a given `needle` value, using the given `compare` +/// callback to determine the order. +/// +/// If an existing element is found to be equal to `needle`, then the `index` +/// out-parameter is set to the existing value's index, and the `exists` +/// out-parameter is set to true. Otherwise, `index` is set to an index where +/// `needle` should be inserted in order to preserve the sorting, and `exists` +/// is set to false. +#define array_search_sorted_with(self, compare, needle, _index, _exists) \ + _array__search_sorted(self, 0, compare, , needle, _index, _exists) + +/// Search a sorted array for a given `needle` value, using integer comparisons +/// of a given struct field (specified with a leading dot) to determine the order. +/// +/// See also `array_search_sorted_with`. +#define array_search_sorted_by(self, field, needle, _index, _exists) \ + _array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists) + +/// Insert a given `value` into a sorted array, using the given `compare` +/// callback to determine the order. +#define array_insert_sorted_with(self, compare, value) \ + do { \ + unsigned _index, _exists; \ + array_search_sorted_with(self, compare, &(value), &_index, &_exists); \ + if (!_exists) array_insert(self, _index, value); \ + } while (0) + +/// Insert a given `value` into a sorted array, using integer comparisons of +/// a given struct field (specified with a leading dot) to determine the order. +/// +/// See also `array_search_sorted_by`. +#define array_insert_sorted_by(self, field, value) \ + do { \ + unsigned _index, _exists; \ + array_search_sorted_by(self, field, (value) field, &_index, &_exists); \ + if (!_exists) array_insert(self, _index, value); \ + } while (0) + +// Private + +typedef Array(void) Array; + +/// This is not what you're looking for, see `array_delete`. +static inline void _array__delete(Array *self) { + if (self->contents) { + ts_free(self->contents); + self->contents = NULL; + self->size = 0; + self->capacity = 0; + } +} + +/// This is not what you're looking for, see `array_erase`. +static inline void _array__erase(Array *self, size_t element_size, + uint32_t index) { + assert(index < self->size); + char *contents = (char *)self->contents; + memmove(contents + index * element_size, contents + (index + 1) * element_size, + (self->size - index - 1) * element_size); + self->size--; +} + +/// This is not what you're looking for, see `array_reserve`. +static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) { + if (new_capacity > self->capacity) { + if (self->contents) { + self->contents = ts_realloc(self->contents, new_capacity * element_size); + } else { + self->contents = ts_malloc(new_capacity * element_size); + } + self->capacity = new_capacity; + } +} + +/// This is not what you're looking for, see `array_assign`. +static inline void _array__assign(Array *self, const Array *other, size_t element_size) { + _array__reserve(self, element_size, other->size); + self->size = other->size; + memcpy(self->contents, other->contents, self->size * element_size); +} + +/// This is not what you're looking for, see `array_swap`. +static inline void _array__swap(Array *self, Array *other) { + Array swap = *other; + *other = *self; + *self = swap; +} + +/// This is not what you're looking for, see `array_push` or `array_grow_by`. +static inline void _array__grow(Array *self, uint32_t count, size_t element_size) { + uint32_t new_size = self->size + count; + if (new_size > self->capacity) { + uint32_t new_capacity = self->capacity * 2; + if (new_capacity < 8) new_capacity = 8; + if (new_capacity < new_size) new_capacity = new_size; + _array__reserve(self, element_size, new_capacity); + } +} + +/// This is not what you're looking for, see `array_splice`. +static inline void _array__splice(Array *self, size_t element_size, + uint32_t index, uint32_t old_count, + uint32_t new_count, const void *elements) { + uint32_t new_size = self->size + new_count - old_count; + uint32_t old_end = index + old_count; + uint32_t new_end = index + new_count; + assert(old_end <= self->size); + + _array__reserve(self, element_size, new_size); + + char *contents = (char *)self->contents; + if (self->size > old_end) { + memmove( + contents + new_end * element_size, + contents + old_end * element_size, + (self->size - old_end) * element_size + ); + } + if (new_count > 0) { + if (elements) { + memcpy( + (contents + index * element_size), + elements, + new_count * element_size + ); + } else { + memset( + (contents + index * element_size), + 0, + new_count * element_size + ); + } + } + self->size += new_count - old_count; +} + +/// A binary search routine, based on Rust's `std::slice::binary_search_by`. +/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`. +#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \ + do { \ + *(_index) = start; \ + *(_exists) = false; \ + uint32_t size = (self)->size - *(_index); \ + if (size == 0) break; \ + int comparison; \ + while (size > 1) { \ + uint32_t half_size = size / 2; \ + uint32_t mid_index = *(_index) + half_size; \ + comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \ + if (comparison <= 0) *(_index) = mid_index; \ + size -= half_size; \ + } \ + comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \ + if (comparison == 0) *(_exists) = true; \ + else if (comparison < 0) *(_index) += 1; \ + } while (0) + +/// Helper macro for the `_sorted_by` routines below. This takes the left (existing) +/// parameter by reference in order to work with the generic sorting function above. +#define _compare_int(a, b) ((int)*(a) - (int)(b)) + +#ifdef _MSC_VER +#pragma warning(pop) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_ARRAY_H_ diff --git a/crates/tree-sitter-compact/src/tree_sitter/parser.h b/crates/tree-sitter-compact/src/tree_sitter/parser.h new file mode 100644 index 0000000..858107d --- /dev/null +++ b/crates/tree-sitter-compact/src/tree_sitter/parser.h @@ -0,0 +1,286 @@ +#ifndef TREE_SITTER_PARSER_H_ +#define TREE_SITTER_PARSER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define ts_builtin_sym_error ((TSSymbol)-1) +#define ts_builtin_sym_end 0 +#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 + +#ifndef TREE_SITTER_API_H_ +typedef uint16_t TSStateId; +typedef uint16_t TSSymbol; +typedef uint16_t TSFieldId; +typedef struct TSLanguage TSLanguage; +typedef struct TSLanguageMetadata { + uint8_t major_version; + uint8_t minor_version; + uint8_t patch_version; +} TSLanguageMetadata; +#endif + +typedef struct { + TSFieldId field_id; + uint8_t child_index; + bool inherited; +} TSFieldMapEntry; + +// Used to index the field and supertype maps. +typedef struct { + uint16_t index; + uint16_t length; +} TSMapSlice; + +typedef struct { + bool visible; + bool named; + bool supertype; +} TSSymbolMetadata; + +typedef struct TSLexer TSLexer; + +struct TSLexer { + int32_t lookahead; + TSSymbol result_symbol; + void (*advance)(TSLexer *, bool); + void (*mark_end)(TSLexer *); + uint32_t (*get_column)(TSLexer *); + bool (*is_at_included_range_start)(const TSLexer *); + bool (*eof)(const TSLexer *); + void (*log)(const TSLexer *, const char *, ...); +}; + +typedef enum { + TSParseActionTypeShift, + TSParseActionTypeReduce, + TSParseActionTypeAccept, + TSParseActionTypeRecover, +} TSParseActionType; + +typedef union { + struct { + uint8_t type; + TSStateId state; + bool extra; + bool repetition; + } shift; + struct { + uint8_t type; + uint8_t child_count; + TSSymbol symbol; + int16_t dynamic_precedence; + uint16_t production_id; + } reduce; + uint8_t type; +} TSParseAction; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; +} TSLexMode; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; + uint16_t reserved_word_set_id; +} TSLexerMode; + +typedef union { + TSParseAction action; + struct { + uint8_t count; + bool reusable; + } entry; +} TSParseActionEntry; + +typedef struct { + int32_t start; + int32_t end; +} TSCharacterRange; + +struct TSLanguage { + uint32_t abi_version; + uint32_t symbol_count; + uint32_t alias_count; + uint32_t token_count; + uint32_t external_token_count; + uint32_t state_count; + uint32_t large_state_count; + uint32_t production_id_count; + uint32_t field_count; + uint16_t max_alias_sequence_length; + const uint16_t *parse_table; + const uint16_t *small_parse_table; + const uint32_t *small_parse_table_map; + const TSParseActionEntry *parse_actions; + const char * const *symbol_names; + const char * const *field_names; + const TSMapSlice *field_map_slices; + const TSFieldMapEntry *field_map_entries; + const TSSymbolMetadata *symbol_metadata; + const TSSymbol *public_symbol_map; + const uint16_t *alias_map; + const TSSymbol *alias_sequences; + const TSLexerMode *lex_modes; + bool (*lex_fn)(TSLexer *, TSStateId); + bool (*keyword_lex_fn)(TSLexer *, TSStateId); + TSSymbol keyword_capture_token; + struct { + const bool *states; + const TSSymbol *symbol_map; + void *(*create)(void); + void (*destroy)(void *); + bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); + unsigned (*serialize)(void *, char *); + void (*deserialize)(void *, const char *, unsigned); + } external_scanner; + const TSStateId *primary_state_ids; + const char *name; + const TSSymbol *reserved_words; + uint16_t max_reserved_word_set_size; + uint32_t supertype_count; + const TSSymbol *supertype_symbols; + const TSMapSlice *supertype_map_slices; + const TSSymbol *supertype_map_entries; + TSLanguageMetadata metadata; +}; + +static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { + uint32_t index = 0; + uint32_t size = len - index; + while (size > 1) { + uint32_t half_size = size / 2; + uint32_t mid_index = index + half_size; + const TSCharacterRange *range = &ranges[mid_index]; + if (lookahead >= range->start && lookahead <= range->end) { + return true; + } else if (lookahead > range->end) { + index = mid_index; + } + size -= half_size; + } + const TSCharacterRange *range = &ranges[index]; + return (lookahead >= range->start && lookahead <= range->end); +} + +/* + * Lexer Macros + */ + +#ifdef _MSC_VER +#define UNUSED __pragma(warning(suppress : 4101)) +#else +#define UNUSED __attribute__((unused)) +#endif + +#define START_LEXER() \ + bool result = false; \ + bool skip = false; \ + UNUSED \ + bool eof = false; \ + int32_t lookahead; \ + goto start; \ + next_state: \ + lexer->advance(lexer, skip); \ + start: \ + skip = false; \ + lookahead = lexer->lookahead; + +#define ADVANCE(state_value) \ + { \ + state = state_value; \ + goto next_state; \ + } + +#define ADVANCE_MAP(...) \ + { \ + static const uint16_t map[] = { __VA_ARGS__ }; \ + for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \ + if (map[i] == lookahead) { \ + state = map[i + 1]; \ + goto next_state; \ + } \ + } \ + } + +#define SKIP(state_value) \ + { \ + skip = true; \ + state = state_value; \ + goto next_state; \ + } + +#define ACCEPT_TOKEN(symbol_value) \ + result = true; \ + lexer->result_symbol = symbol_value; \ + lexer->mark_end(lexer); + +#define END_STATE() return result; + +/* + * Parse Table Macros + */ + +#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT) + +#define STATE(id) id + +#define ACTIONS(id) id + +#define SHIFT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = (state_value) \ + } \ + }} + +#define SHIFT_REPEAT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = (state_value), \ + .repetition = true \ + } \ + }} + +#define SHIFT_EXTRA() \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .extra = true \ + } \ + }} + +#define REDUCE(symbol_name, children, precedence, prod_id) \ + {{ \ + .reduce = { \ + .type = TSParseActionTypeReduce, \ + .symbol = symbol_name, \ + .child_count = children, \ + .dynamic_precedence = precedence, \ + .production_id = prod_id \ + }, \ + }} + +#define RECOVER() \ + {{ \ + .type = TSParseActionTypeRecover \ + }} + +#define ACCEPT_INPUT() \ + {{ \ + .type = TSParseActionTypeAccept \ + }} + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_PARSER_H_ diff --git a/crates/tree-sitter-compact/test/corpus/circuits-and-constructors.txt b/crates/tree-sitter-compact/test/corpus/circuits-and-constructors.txt new file mode 100644 index 0000000..0290bca --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/circuits-and-constructors.txt @@ -0,0 +1,152 @@ +================================================================================ +plain circuit +================================================================================ + +circuit assertInitialized(): [] { + assert(_isInitialized, "not initialized"); +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression) + body: (block + (string)))) + +================================================================================ +exported circuit +================================================================================ + +export circuit owner(): Either, ContractAddress> { + return _owner; +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier) + (generic_arguments + (identifier) + (generic_arguments + (number)) + (identifier))) + body: (block))) + +================================================================================ +exported pure circuit +================================================================================ + +export pure circuit add(a: Uint<32>, b: Uint<32>): Uint<64> { + return a + b; +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier) + (generic_arguments + (number))) + body: (block))) + +================================================================================ +pure circuit +================================================================================ + +pure circuit half(x: Field): Field { + return x; +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier)) + body: (block))) + +================================================================================ +generic circuit +================================================================================ + +export circuit pick<#T, U>(a: T, b: U): T { + return a; +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + (generic_parameters + (generic_parameter + name: (identifier)) + (generic_parameter + name: (identifier))) + parameters: (parameter_list) + type: (type_expression + (identifier)) + body: (block))) + +================================================================================ +circuit with a destructuring parameter +================================================================================ + +circuit unpack({ left, right }: Pair): [] { + return []; +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list + (block)) + type: (type_expression) + body: (block))) + +================================================================================ +circuit body with braces inside a string +================================================================================ + +circuit shout(): [] { + assert(false, "} not a brace {"); +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression) + body: (block + (string)))) + +================================================================================ +constructor +================================================================================ + +constructor(initialOwner: Either, ContractAddress>) { + Ownable_initialize(initialOwner); +} + +-------------------------------------------------------------------------------- + +(source_file + (constructor_declaration + (parameter_list) + (block))) diff --git a/crates/tree-sitter-compact/test/corpus/comments.txt b/crates/tree-sitter-compact/test/corpus/comments.txt new file mode 100644 index 0000000..edeb558 --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/comments.txt @@ -0,0 +1,134 @@ +================================================================================ +line comment +================================================================================ + +// a note +ledger _x: Field; + +-------------------------------------------------------------------------------- + +(source_file + (line_comment) + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))) + +================================================================================ +block comment +================================================================================ + +/* a note */ +ledger _x: Field; + +-------------------------------------------------------------------------------- + +(source_file + (block_comment) + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))) + +================================================================================ +empty block comment +================================================================================ + +/**/ +ledger _x: Field; + +-------------------------------------------------------------------------------- + +(source_file + (block_comment) + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))) + +================================================================================ +doc comment +================================================================================ + +/** @description A flag. */ +ledger _x: Boolean; + +-------------------------------------------------------------------------------- + +(source_file + (doc_comment) + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))) + +================================================================================ +doc comment before an exported circuit +================================================================================ + +/** + * @description Returns the owner. + * @circuitInfo k=7, rows=76 + */ +export circuit owner(): Bytes<32> { + return _owner; +} + +-------------------------------------------------------------------------------- + +(source_file + (doc_comment) + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier) + (generic_arguments + (number))) + body: (block))) + +================================================================================ +doc comment before an exported module +================================================================================ + +/** @module Ownable */ +export module Ownable { + /** @description A flag. */ + export ledger _isInitialized: Boolean; +} + +-------------------------------------------------------------------------------- + +(source_file + (doc_comment) + (module_declaration + name: (identifier) + body: (module_body + (doc_comment) + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))))) + +================================================================================ +comments inside a circuit body +================================================================================ + +circuit noop(): [] { + // a line + /* a block */ + /** a doc */ + return []; +} + +-------------------------------------------------------------------------------- + +(source_file + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression) + body: (block + (line_comment) + (block_comment) + (doc_comment)))) diff --git a/crates/tree-sitter-compact/test/corpus/imports.txt b/crates/tree-sitter-compact/test/corpus/imports.txt new file mode 100644 index 0000000..b8bef8a --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/imports.txt @@ -0,0 +1,141 @@ +================================================================================ +bare module import +================================================================================ + +import CompactStandardLibrary; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + name: (identifier))) + +================================================================================ +path import +================================================================================ + +import "./Foo"; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + name: (string))) + +================================================================================ +module import with prefix +================================================================================ + +import Foo prefix Foo_; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + name: (identifier) + (import_prefix + prefix: (identifier)))) + +================================================================================ +path import with prefix +================================================================================ + +import "./Foo" prefix F_; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + name: (string) + (import_prefix + prefix: (identifier)))) + +================================================================================ +import with generic arguments +================================================================================ + +import Foo prefix X_; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + name: (identifier) + (generic_arguments + (identifier) + (identifier)) + (import_prefix + prefix: (identifier)))) + +================================================================================ +path import with nested generic arguments +================================================================================ + +import "../../Signer"> prefix Signer_; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + name: (string) + (generic_arguments + (identifier) + (generic_arguments + (identifier) + (identifier))) + (import_prefix + prefix: (identifier)))) + +================================================================================ +import selection +================================================================================ + +import { _name, _symbol } from "./Core"; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + (import_selection + (import_element + (identifier)) + (import_element + (identifier))) + (string))) + +================================================================================ +import selection with alias and prefix +================================================================================ + +import { selfAsRecipient, UINT128_MAX as MAX } from "../utils/Utils" prefix Utils_; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + (import_selection + (import_element + (identifier)) + (import_element + (identifier) + (identifier))) + (string) + (import_prefix + (identifier)))) + +================================================================================ +import selection from a bare module +================================================================================ + +import { ZswapCoinPublicKey, Either } from CompactStandardLibrary; + +-------------------------------------------------------------------------------- + +(source_file + (import_declaration + (import_selection + (import_element + (identifier)) + (import_element + (identifier))) + (identifier))) diff --git a/crates/tree-sitter-compact/test/corpus/modules-and-exports.txt b/crates/tree-sitter-compact/test/corpus/modules-and-exports.txt new file mode 100644 index 0000000..29eebea --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/modules-and-exports.txt @@ -0,0 +1,116 @@ +================================================================================ +module with declarations in its body +================================================================================ + +module Ownable { + import CompactStandardLibrary; + export ledger _owner: Bytes<32>; +} + +-------------------------------------------------------------------------------- + +(source_file + (module_declaration + name: (identifier) + body: (module_body + (import_declaration + name: (identifier)) + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier) + (generic_arguments + (number))))))) + +================================================================================ +exported generic module +================================================================================ + +export module Signer { + export circuit check(x: T): Boolean { return true; } +} + +-------------------------------------------------------------------------------- + +(source_file + (module_declaration + name: (identifier) + (generic_parameters + (generic_parameter + name: (identifier))) + body: (module_body + (circuit_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier)) + body: (block))))) + +================================================================================ +nested modules +================================================================================ + +module Outer { + module Inner { + export ledger _flag: Boolean; + } +} + +-------------------------------------------------------------------------------- + +(source_file + (module_declaration + name: (identifier) + body: (module_body + (module_declaration + name: (identifier) + body: (module_body + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))))))) + +================================================================================ +export list +================================================================================ + +export { A, B }; + +-------------------------------------------------------------------------------- + +(source_file + (export_declaration + (identifier) + (identifier))) + +================================================================================ +export list without a trailing semicolon +================================================================================ + +export { A, B, } + +-------------------------------------------------------------------------------- + +(source_file + (export_declaration + (identifier) + (identifier))) + +================================================================================ +export list inside a module +================================================================================ + +module M { + export { _name, _symbol, _decimals }; +} + +-------------------------------------------------------------------------------- + +(source_file + (module_declaration + name: (identifier) + body: (module_body + (export_declaration + (identifier) + (identifier) + (identifier))))) diff --git a/crates/tree-sitter-compact/test/corpus/pragma-and-include.txt b/crates/tree-sitter-compact/test/corpus/pragma-and-include.txt new file mode 100644 index 0000000..47ec355 --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/pragma-and-include.txt @@ -0,0 +1,37 @@ +================================================================================ +pragma with a lower bound +================================================================================ + +pragma language_version >= 0.23.0; + +-------------------------------------------------------------------------------- + +(source_file + (pragma_declaration + name: (identifier) + (version_constraint))) + +================================================================================ +pragma with alternative versions +================================================================================ + +pragma language_version 0.16 || 0.17; + +-------------------------------------------------------------------------------- + +(source_file + (pragma_declaration + name: (identifier) + (version_constraint))) + +================================================================================ +include +================================================================================ + +include "./shared"; + +-------------------------------------------------------------------------------- + +(source_file + (include_declaration + path: (string))) diff --git a/crates/tree-sitter-compact/test/corpus/structs-enums-contracts.txt b/crates/tree-sitter-compact/test/corpus/structs-enums-contracts.txt new file mode 100644 index 0000000..50a7918 --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/structs-enums-contracts.txt @@ -0,0 +1,105 @@ +================================================================================ +struct +================================================================================ + +struct Val { + a: Field; + b: Boolean; +} + +-------------------------------------------------------------------------------- + +(source_file + (struct_declaration + name: (identifier) + body: (block))) + +================================================================================ +exported struct with a trailing semicolon +================================================================================ + +export struct Ciphertext { + c1: CurvePoint, + c2: CurvePoint, +}; + +-------------------------------------------------------------------------------- + +(source_file + (struct_declaration + name: (identifier) + body: (block))) + +================================================================================ +generic struct +================================================================================ + +export struct Box { + value: T; +} + +-------------------------------------------------------------------------------- + +(source_file + (struct_declaration + name: (identifier) + (generic_parameters + (generic_parameter + name: (identifier))) + body: (block))) + +================================================================================ +enum +================================================================================ + +enum Color { red, white, blue } + +-------------------------------------------------------------------------------- + +(source_file + (enum_declaration + name: (identifier) + body: (block))) + +================================================================================ +exported enum with a trailing semicolon +================================================================================ + +export enum ProposalStatus { Pending, Approved, Rejected, }; + +-------------------------------------------------------------------------------- + +(source_file + (enum_declaration + name: (identifier) + body: (block))) + +================================================================================ +external contract declaration +================================================================================ + +export contract Token { + circuit balanceOf(owner: Bytes<32>): Uint<128>; + pure circuit name(): Opaque<"string">; +}; + +-------------------------------------------------------------------------------- + +(source_file + (contract_declaration + name: (identifier) + body: (block + (string)))) + +================================================================================ +contract implements +================================================================================ + +contract implements Token; + +-------------------------------------------------------------------------------- + +(source_file + (implements_declaration + (type_expression + (identifier)))) diff --git a/crates/tree-sitter-compact/test/corpus/types-ledger-witness.txt b/crates/tree-sitter-compact/test/corpus/types-ledger-witness.txt new file mode 100644 index 0000000..67c6b33 --- /dev/null +++ b/crates/tree-sitter-compact/test/corpus/types-ledger-witness.txt @@ -0,0 +1,149 @@ +================================================================================ +ledger field +================================================================================ + +ledger _counter: Counter; + +-------------------------------------------------------------------------------- + +(source_file + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier)))) + +================================================================================ +exported ledger field +================================================================================ + +export ledger _owner: Either, ContractAddress>; + +-------------------------------------------------------------------------------- + +(source_file + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier) + (generic_arguments + (identifier) + (generic_arguments + (number)) + (identifier))))) + +================================================================================ +exported sealed ledger field +================================================================================ + +export sealed ledger _domain: Bytes<32>; + +-------------------------------------------------------------------------------- + +(source_file + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier) + (generic_arguments + (number))))) + +================================================================================ +ledger field with a tuple type +================================================================================ + +ledger _pair: [Field, Boolean]; + +-------------------------------------------------------------------------------- + +(source_file + (ledger_declaration + name: (identifier) + type: (type_expression + (identifier) + (identifier)))) + +================================================================================ +witness +================================================================================ + +witness wit_CallerSK(): Bytes<32>; + +-------------------------------------------------------------------------------- + +(source_file + (witness_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier) + (generic_arguments + (number))))) + +================================================================================ +exported witness with parameters +================================================================================ + +export witness wit_balance(ct: ElGamal_Ciphertext): Uint<128>; + +-------------------------------------------------------------------------------- + +(source_file + (witness_declaration + name: (identifier) + parameters: (parameter_list) + type: (type_expression + (identifier) + (generic_arguments + (number))))) + +================================================================================ +type alias +================================================================================ + +type Hash = Bytes<32>; + +-------------------------------------------------------------------------------- + +(source_file + (type_declaration + name: (identifier) + value: (type_expression + (identifier) + (generic_arguments + (number))))) + +================================================================================ +exported new type alias +================================================================================ + +export new type AccountIdentifier = Bytes<32>; + +-------------------------------------------------------------------------------- + +(source_file + (type_declaration + name: (identifier) + value: (type_expression + (identifier) + (generic_arguments + (number))))) + +================================================================================ +generic type alias +================================================================================ + +export type Pair = [A, B]; + +-------------------------------------------------------------------------------- + +(source_file + (type_declaration + name: (identifier) + (generic_parameters + (generic_parameter + name: (identifier)) + (generic_parameter + name: (identifier))) + value: (type_expression + (identifier) + (identifier)))) diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/NOTICE b/crates/tree-sitter-compact/test/fixtures/upstream/NOTICE new file mode 100644 index 0000000..9352fb8 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/NOTICE @@ -0,0 +1,17 @@ +The `.compact` files in this directory are copied verbatim from the Compact +compiler repository and are used here only as parser fixtures. + +Source: https://github.com/LFDT-Minokawa/compact +Path: examples/formatter/output/ +Commit: c47230cc8c3e743634166d9b84698ac2b238c418 +License: Apache-2.0 +Copyright: Copyright (C) 2025 Midnight Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/already_formatted.compact b/crates/tree-sitter-compact/test/fixtures/upstream/already_formatted.compact new file mode 100644 index 0000000..800fb64 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/already_formatted.compact @@ -0,0 +1,26 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export ledger round: Counter; + +export circuit increment(): [] { + round.increment(1); +} + +export circuit decrement(): [] { + round.decrement(1); +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_1.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_1.compact new file mode 100644 index 0000000..bae9c5b --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_1.compact @@ -0,0 +1,34 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export circuit method(): [] { + const p = 1; + const z = 2; + const x = 3; +} + +constructor() { + const p: Field = 1; + + const z: Boolean = true; + + const y = default; + + const f = default> as Field; +} + +witness method_2(): Boolean; diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_10.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_10.compact new file mode 100644 index 0000000..e6274da --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_10.compact @@ -0,0 +1,62 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export ledger c: Counter; + +export circuit decrement(amount: Uint<16>): [] { + return c.decrement(amount); +} + +export circuit increment(amount: Uint<16>): [] { + for (const f of 1..10) { + c.increment(1); + } + return c.increment(amount); +} + +export circuit reset_to_default(): [] { + assert(1 < 2, "hoho"); + return c.reset_to_default(); +} + +export circuit read(): Uint<64> { + if (1 == 2) + + { + return c.read(); + } + return c.read(); +} + +struct Val { + x: Field, + y: Boolean, + f: Field, + + x: Opaque<"string">, + + d: Vector<1, Boolean>, +} + +enum french { + a, + bm, + c, + d, +} + +export { Maybe }; diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_11.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_11.compact new file mode 100644 index 0000000..062f535 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_11.compact @@ -0,0 +1,32 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +struct mariusz { + x: Field, + y: Field, + z: Field, +} + +export circuit bob(a: Field, x: Field,): [Uint<32>, Uint<32>,] { + return [1, 2,]; +} + +constructor() { + + const p = [1, 1, 1, 1, 1, 1, 1, 1, 1,]; + const f = mariusz { 1, 2, 3, }; + const g = [bob(1, 2,), bob(1, 2,), bob(1, 2,),]; + const phil = [1, 2, 3, [1, 2, 3,],]; +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_12.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_12.compact new file mode 100644 index 0000000..7d1f0d3 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_12.compact @@ -0,0 +1,36 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +struct var9a { + a: Field, + b: Field +} + +struct var9b { + a: var9a, + b: var9a +} + +struct var9c { + a: var9b, + b: var9b +} + +export circuit test9(): [] { + const p = var9c { a: var9b { a: var9a { 1, 2 }, b: var9a { 1, 2 } }, + b: var9b { a: var9a { 1, 2 }, b: var9a { 1, 2 }, } }, + x = 1, + y = p.a.a.a + p.b.b.b; +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_13.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_13.compact new file mode 100644 index 0000000..34734d9 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_13.compact @@ -0,0 +1,185 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export ledger total_operations: Counter; + +export ledger last_calculated_result: Field; + +export ledger operation_category_code: Uint<8>; + +export ledger result_value: Field; + +export ledger multiplier: Field; + +export ledger op_multiplier: Field; + +witness get_user_calculation_parameters_from_external_validation_source( + user_identifier_hash: Field, + initial_amount_value: Uint<64>, + scaling_factor_multiplier: Uint<64>, + offset_adjustment_value: Uint<64> + ): Uint<64>; + +witness retrieve_transaction_validation_context_and_authorization_flags( + transaction_reference_id: Field, + sender_account_identifier: Field, + receiver_account_identifier: Field, + operation_type_code: Uint<8> + ): [Boolean, Uint<64>]; + +circuit perform_complex_arithmetic_calculation_with_multiple_inputs( + base_calculation_value: Uint<64>, + primary_scaling_factor: Uint<64>, + secondary_offset_amount: Uint<64>, + final_adjustment_modifier: Uint<64> + ): Uint<64> { + return ((base_calculation_value * primary_scaling_factor) + secondary_offset_amount) + + final_adjustment_modifier; +} + +export circuit execute_tiered_bonus_calculation_based_on_user_metrics( + user_account_balance: Uint<64>, + transaction_total_amount: Uint<64>, + accumulated_loyalty_points: Uint<64>, + account_age_in_months: Uint<64> + ): Uint<64> { + const base = transaction_total_amount * 10 as Uint<64>; + const loyalty = accumulated_loyalty_points * 2 as Uint<64>; + const age = account_age_in_months * 3 as Uint<64>; + return (base + loyalty) + age; +} + +export circuit test_multiline_call_expressions(input_value: Uint<64>): Uint<64> { + const result_from_arithmetic = + perform_complex_arithmetic_calculation_with_multiple_inputs( + input_value, + 10 as Uint<64>, + 5 as Uint<64>, + 2 as Uint<64> + ); + const bonus_calculation_result = + execute_tiered_bonus_calculation_based_on_user_metrics( + input_value * 2 as Uint<64>, + input_value + 100 as Uint<64>, + 50 as Uint<64>, + 12 as Uint<64> + ); + return result_from_arithmetic + bonus_calculation_result; +} + +export circuit test_anonymous_circuit_in_fold(values: Vector<4, Uint<64>>): Uint<64> { + const sum_result = + fold((accumulator_running_total: Uint<64>, current_element_value: Uint<64>): Uint<64> => { + const intermediate_calculation = accumulator_running_total + current_element_value; + return intermediate_calculation; + }, + 0 as Uint<64>, + values); + return sum_result; +} + +export circuit test_complex_anonymous_circuit(input_vector: Vector<4, Uint<64>>): Uint<64> { + const transformed = + fold((running_accumulator_total: Uint<64>, vector_element_value: Uint<64>): Uint<64> => { + const doubled_value = vector_element_value * 2 as Uint<64>; + const with_bonus_added = doubled_value + 10 as Uint<64>; + return running_accumulator_total + with_bonus_added; + }, + 0 as Uint<64>, + input_vector); + return transformed; +} + +export circuit test_map_anonymous(input: Vector<4, Uint<64>>): Vector<4, Uint<64>> { + const result = map((element_value: Uint<64>): Uint<64> => { + const step_one = element_value * 3 as Uint<64>; + const step_two = step_one + 7 as Uint<64>; + return step_two; + }, + input); + return result; +} + +export circuit test_if_else_chain(operation_code: Uint<8>): Uint<64> { + result_value = 0 as Field; + if (operation_code == (0 as Uint<8>)) { + result_value = 100 as Field; + } else if (operation_code == (1 as Uint<8>)) { + result_value = 200 as Field; + } else if (operation_code == (2 as Uint<8>)) { + result_value = 300 as Field; + } else if (operation_code == (3 as Uint<8>)) { + result_value = 400 as Field; + } else if (operation_code == (4 as Uint<8>)) { + result_value = 500 as Field; + } else { + result_value = 0 as Field; + } + return result_value as Uint<64>; +} + +export circuit test_complex_if_else(category: Uint<8>, amount: Uint<64>): Uint<64> { + multiplier = 100 as Field; + if (category == (0 as Uint<8>)) { + multiplier = 100 as Field; + } else if (category == (1 as Uint<8>)) { + multiplier = 110 as Field; + } else if (category == (2 as Uint<8>)) { + multiplier = 125 as Field; + } else if (category == (3 as Uint<8>)) { + multiplier = 150 as Field; + } else if (category == (4 as Uint<8>)) { + multiplier = 200 as Field; + } else { + multiplier = 100 as Field; + } + return (amount * (multiplier as Uint<64>)); +} + +export circuit test_all_scenarios_combined( + op_type: Uint<8>, + base_amount: Uint<64>, + test_values: Vector<4, Uint<64>> + ): Uint<64> { + total_operations.increment(1 as Uint<16>); + const arithmetic_result = + perform_complex_arithmetic_calculation_with_multiple_inputs( + base_amount, + 2 as Uint<64>, + 10 as Uint<64>, + 5 as Uint<64> + ); + const vector_sum = + fold((acc: Uint<64>, val: Uint<64>): Uint<64> => { + return acc + val; + }, + 0 as Uint<64>, + test_values); + op_multiplier = 1 as Field; + if (op_type == (0 as Uint<8>)) { + op_multiplier = 1 as Field; + } else if (op_type == (1 as Uint<8>)) { + op_multiplier = 2 as Field; + } else if (op_type == (2 as Uint<8>)) { + op_multiplier = 3 as Field; + } else { + op_multiplier = 1 as Field; + } + last_calculated_result = ((arithmetic_result + vector_sum) * (op_multiplier as Uint<64>)) as Field; + operation_category_code = op_type; + return last_calculated_result as Uint<64>; +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_2.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_2.compact new file mode 100644 index 0000000..364c3f2 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_2.compact @@ -0,0 +1,40 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export circuit method_3(): [] { + const p = 1; + const z = 2; + + if (p > z) { + assert(1 > 2, "troll"); + if (p > z) { + assert(1 > 2, "troll"); + if (p > z) { + assert(1 > 2, "troll"); + if (p > z) { + assert(1 > 2, "troll"); + if (p > z) { + assert(1 > 2, "troll"); + if (p > z) { + assert(1 > 2, "troll"); + } + } + } + } + } + } +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_3.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_3.compact new file mode 100644 index 0000000..7f806e4 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_3.compact @@ -0,0 +1,41 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +pragma language_version >= 0.15; + +export circuit method_4(): [] { + for (const i of 1..10) { + + assert(1 > 2, "troll"); + for (const i of 1..10) { + + assert(1 > 2, "troll"); + for (const i of 1..10) { + + assert(1 > 2, "troll"); + for (const i of 1..10) { + + assert(1 > 2, "troll"); + + } + + } + + } + + } +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_4.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_4.compact new file mode 100644 index 0000000..716061b --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_4.compact @@ -0,0 +1,32 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export ledger test: Map; + +export ledger test2: Map; + +export circuit ronan(): [] { + return []; +} + +witness caroline(): Field; + +export circuit Grendel(x: Field, y: Field, z: Field, d: Uint<32>): Field + + { + return x + y + z; +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_5.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_5.compact new file mode 100644 index 0000000..881f9b2 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_5.compact @@ -0,0 +1,34 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +constructor(x: Field, y: Field, z: Field) { + const p = x + y + z; +} + +export circuit test(): Field { + assert(1 > 2, "hello world"); +} + +export circuit test2(): [] { + + const g = default>; +} + +export circuit test3(): Boolean { + const p = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,]; + return []; +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_6.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_6.compact new file mode 100644 index 0000000..67d4ac4 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_6.compact @@ -0,0 +1,31 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +constructor() { + const p = default; + const z = 1; +} + +export circuit test(): [] { + return []; +} + +witness barbara(): Boolean; + +witness arnold(x: Float): Boolean; + +export ledger counter: Counter; diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_7.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_7.compact new file mode 100644 index 0000000..ea7282d --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_7.compact @@ -0,0 +1,32 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +constructor() { + const f = 4; + const z = 2; + const p = f + z; +} + +export circuit randolph(x: Field, z: Field): Field { + return x + z; +} + +witness adam(x: Field, y: Uint<32>): Uint<16>; + +export ledger round: Counter; + +export ledger ariadna: Map; diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_8.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_8.compact new file mode 100644 index 0000000..8280eb9 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_8.compact @@ -0,0 +1,43 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pragma language_version > 0.14; + +export ledger counter: Counter; + +export circuit reynolds(): Field { + return default; +} + +constructor(x: Field, y: Field, z: Field) { + for (const x of 1..10) { + assert(i == i, "hello world"); + } + for (const y of 0..10) { + if (y == 1) { + assert(y == y, + " + gorgonzola" + ); + } + } + return []; +} + +export circuit burt(): [] { + + const y = 1; + +} diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_9.compact b/crates/tree-sitter-compact/test/fixtures/upstream/example_9.compact new file mode 100644 index 0000000..a78e319 --- /dev/null +++ b/crates/tree-sitter-compact/test/fixtures/upstream/example_9.compact @@ -0,0 +1,34 @@ +// This file is part of Compact. +// Copyright (C) 2025 Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CompactStandardLibrary; + +export ledger c: Counter; + +export circuit decrement(amount: Uint<16>): [] { + return c.decrement(amount); +} + +export circuit increment(amount: Uint<16>): [] { + return c.increment(amount); +} + +export circuit reset_to_default(): [] { + return c.reset_to_default(); +} + +export circuit read(): Uint<64> { + return c.read(); +} diff --git a/crates/tree-sitter-compact/tests/corpus.rs b/crates/tree-sitter-compact/tests/corpus.rs new file mode 100644 index 0000000..ea03373 --- /dev/null +++ b/crates/tree-sitter-compact/tests/corpus.rs @@ -0,0 +1,214 @@ +//! Real Compact sources must parse with no `ERROR` and no `MISSING` node. + +use std::path::{Path, PathBuf}; + +use tree_sitter::{Node, Parser, Tree}; + +/// Directories that never hold first-party Compact sources. +const SKIPPED_DIRS: [&str; 5] = ["node_modules", "target", "dist", "build", ".git"]; + +fn parser() -> Parser { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_compact::LANGUAGE.into()) + .expect("the generated parser matches the tree-sitter ABI this crate builds against"); + parser +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .canonicalize() + .expect("the crate sits two levels below the workspace root") +} + +fn compact_files(dirs: &[PathBuf]) -> Vec { + let mut files = Vec::new(); + for dir in dirs { + collect_compact_files(dir, &mut files); + } + files.sort(); + files +} + +fn collect_compact_files(dir: &Path, out: &mut Vec) { + let entries = std::fs::read_dir(dir) + .unwrap_or_else(|error| panic!("corpus directory {} is readable: {error}", dir.display())); + + for entry in entries { + let entry = entry.expect("a directory entry read without error"); + let path = entry.path(); + let name = entry.file_name(); + + if path.is_dir() { + if !SKIPPED_DIRS.iter().any(|skipped| name == *skipped) { + collect_compact_files(&path, out); + } + } else if path + .extension() + .is_some_and(|extension| extension == "compact") + { + out.push(path); + } + } +} + +fn child_kinds(node: Node) -> Vec<&'static str> { + let mut cursor = node.walk(); + let mut kinds = Vec::new(); + for child in node.children(&mut cursor) { + kinds.push(child.kind()); + } + kinds +} + +struct Defect { + kind: String, + start: usize, + end: usize, + excerpt: String, +} + +fn excerpt(source: &str, start: usize) -> String { + let tail = &source[start..]; + let mut end = 0; + for (index, character) in tail.char_indices().take(120) { + end = index + character.len_utf8(); + } + tail[..end].replace('\n', "\\n") +} + +fn find_defect(node: Node, source: &str) -> Option { + if node.is_error() || node.is_missing() { + let start = node.start_byte(); + return Some(Defect { + kind: if node.is_missing() { + format!("MISSING {}", node.kind()) + } else { + node.kind().to_owned() + }, + start, + end: node.end_byte(), + excerpt: excerpt(source, start), + }); + } + + if !node.has_error() { + return None; + } + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + if let Some(defect) = find_defect(child, source) { + return Some(defect); + } + } + None +} + +fn assert_parses_clean(parser: &mut Parser, files: &[PathBuf]) { + for path in files { + let source = std::fs::read_to_string(path) + .unwrap_or_else(|error| panic!("{} is readable UTF-8: {error}", path.display())); + let tree: Tree = parser + .parse(&source, None) + .unwrap_or_else(|| panic!("{} produced a parse tree", path.display())); + + if let Some(defect) = find_defect(tree.root_node(), &source) { + let Defect { + kind, + start, + end, + excerpt, + } = defect; + panic!( + "{}: {kind} at bytes {start}..{end}\n {excerpt}", + path.display() + ); + } + } +} + +#[test] +fn upstream_corpus_parses_without_defects() { + let files = + compact_files(&[Path::new(env!("CARGO_MANIFEST_DIR")).join("test/fixtures/upstream")]); + + assert!( + !files.is_empty(), + "the upstream fixture directory is populated" + ); + println!("upstream corpus: {} files", files.len()); + assert_parses_clean(&mut parser(), &files); +} + +#[test] +fn in_repo_corpus_parses_without_defects() { + let root = workspace_root(); + let files = compact_files(&[ + root.join("tests/integrations/fixtures"), + root.join("examples"), + root.join("packages/simulator/test/fixtures/sample-contracts"), + ]); + + assert!(!files.is_empty(), "the repository ships Compact sources"); + println!("in-repo corpus: {} files", files.len()); + assert_parses_clean(&mut parser(), &files); +} + +#[test] +fn external_corpus_parses_without_defects() { + let Ok(dir) = std::env::var("COMPACT_CORPUS_DIR") else { + println!("external corpus: COMPACT_CORPUS_DIR unset, nothing checked"); + return; + }; + + let files = compact_files(&[PathBuf::from(&dir)]); + + assert!( + !files.is_empty(), + "COMPACT_CORPUS_DIR holds Compact sources" + ); + println!("external corpus: {} files under {dir}", files.len()); + assert_parses_clean(&mut parser(), &files); +} + +#[test] +fn doc_commented_export_pure_circuit_exposes_name_and_modifiers() { + let source = "/** @description Adds two numbers. */\n\ + export pure circuit add(a: Uint<32>, b: Uint<32>): Uint<64> {\n\ + \x20 return a + b;\n\ + }\n"; + + let tree = parser() + .parse(source, None) + .expect("the snippet produced a parse tree"); + let root = tree.root_node(); + assert!(!root.has_error(), "the snippet parsed without defects"); + assert_eq!(child_kinds(root), ["doc_comment", "circuit_declaration"]); + + let circuit = root.child(1).expect("the circuit follows its doc comment"); + assert_eq!( + child_kinds(circuit), + [ + "export", + "pure", + "circuit", + "identifier", + "parameter_list", + ":", + "type_expression", + "block", + ] + ); + + let name = circuit + .child_by_field_name("name") + .expect("a circuit declaration carries a name field"); + assert_eq!( + name.utf8_text(source.as_bytes()) + .expect("the source is valid UTF-8"), + "add" + ); +} diff --git a/crates/tree-sitter-compact/tree-sitter.json b/crates/tree-sitter-compact/tree-sitter.json new file mode 100644 index 0000000..f3f86e5 --- /dev/null +++ b/crates/tree-sitter-compact/tree-sitter.json @@ -0,0 +1,37 @@ +{ + "grammars": [ + { + "name": "compact", + "camelcase": "Compact", + "scope": "source.compact", + "path": ".", + "file-types": ["compact"], + "highlights": ["queries/highlights.scm"], + "injection-regex": "^compact$" + } + ], + "metadata": { + "version": "0.1.0", + "license": "MIT", + "description": "Declaration-level Compact grammar for tree-sitter", + "authors": [ + { + "name": "OpenZeppelin" + } + ], + "links": { + "repository": "https://github.com/OpenZeppelin/compact-tools" + } + }, + "bindings": { + "c": true, + "go": false, + "java": false, + "kotlin": false, + "node": false, + "python": false, + "rust": true, + "swift": false, + "zig": false + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..38faa68 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.96" +components = ["rustfmt", "clippy"] From 0d4991ba96c40c26d540bc2bb855d5f89ab7fe7a Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 09:41:51 +0200 Subject: [PATCH 02/14] refactor(compact-tree-sitter): rename the grammar crate Every crate in this workspace is named compact-*, so the grammar crate follows suit instead of the tree-sitter- convention. The grammar name in tree-sitter.json and the generated C symbol stay `compact`; only the Cargo package and its directory move. --- .github/workflows/rust.yml | 4 ++-- Cargo.lock | 18 +++++++++--------- biome.json | 2 +- .../Cargo.toml | 2 +- .../README.md | 4 ++-- .../bindings/rust/build.rs | 2 +- .../bindings/rust/lib.rs | 0 .../grammar.js | 0 .../package.json | 2 +- .../queries/highlights.scm | 0 .../src/grammar.json | 0 .../src/node-types.json | 0 .../src/parser.c | 0 .../src/tree_sitter/alloc.h | 0 .../src/tree_sitter/array.h | 0 .../src/tree_sitter/parser.h | 0 .../test/corpus/circuits-and-constructors.txt | 0 .../test/corpus/comments.txt | 0 .../test/corpus/imports.txt | 0 .../test/corpus/modules-and-exports.txt | 0 .../test/corpus/pragma-and-include.txt | 0 .../test/corpus/structs-enums-contracts.txt | 0 .../test/corpus/types-ledger-witness.txt | 0 .../test/fixtures/upstream/NOTICE | 0 .../upstream/already_formatted.compact | 0 .../test/fixtures/upstream/example_1.compact | 0 .../test/fixtures/upstream/example_10.compact | 0 .../test/fixtures/upstream/example_11.compact | 0 .../test/fixtures/upstream/example_12.compact | 0 .../test/fixtures/upstream/example_13.compact | 0 .../test/fixtures/upstream/example_2.compact | 0 .../test/fixtures/upstream/example_3.compact | 0 .../test/fixtures/upstream/example_4.compact | 0 .../test/fixtures/upstream/example_5.compact | 0 .../test/fixtures/upstream/example_6.compact | 0 .../test/fixtures/upstream/example_7.compact | 0 .../test/fixtures/upstream/example_8.compact | 0 .../test/fixtures/upstream/example_9.compact | 0 .../tests/corpus.rs | 2 +- .../tree-sitter.json | 0 40 files changed, 18 insertions(+), 18 deletions(-) rename crates/{tree-sitter-compact => compact-tree-sitter}/Cargo.toml (95%) rename crates/{tree-sitter-compact => compact-tree-sitter}/README.md (97%) rename crates/{tree-sitter-compact => compact-tree-sitter}/bindings/rust/build.rs (93%) rename crates/{tree-sitter-compact => compact-tree-sitter}/bindings/rust/lib.rs (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/grammar.js (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/package.json (89%) rename crates/{tree-sitter-compact => compact-tree-sitter}/queries/highlights.scm (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/src/grammar.json (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/src/node-types.json (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/src/parser.c (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/src/tree_sitter/alloc.h (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/src/tree_sitter/array.h (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/src/tree_sitter/parser.h (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/circuits-and-constructors.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/comments.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/imports.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/modules-and-exports.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/pragma-and-include.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/structs-enums-contracts.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/corpus/types-ledger-witness.txt (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/NOTICE (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/already_formatted.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_1.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_10.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_11.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_12.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_13.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_2.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_3.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_4.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_5.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_6.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_7.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_8.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/test/fixtures/upstream/example_9.compact (100%) rename crates/{tree-sitter-compact => compact-tree-sitter}/tests/corpus.rs (99%) rename crates/{tree-sitter-compact => compact-tree-sitter}/tree-sitter.json (100%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2bc8283..a3f227a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,8 +39,8 @@ jobs: run: cargo test --workspace - name: Check the generated parser is up to date - working-directory: crates/tree-sitter-compact + working-directory: crates/compact-tree-sitter run: npx -y tree-sitter-cli@0.25.8 generate - name: Fail if regeneration changed the parser - run: git diff --exit-code -- crates/tree-sitter-compact/src + run: git diff --exit-code -- crates/compact-tree-sitter/src diff --git a/Cargo.lock b/Cargo.lock index e78a9e5..f1a0406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "compact-tree-sitter" +version = "0.1.0" +dependencies = [ + "cc", + "tree-sitter", + "tree-sitter-language", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -188,15 +197,6 @@ dependencies = [ "tree-sitter-language", ] -[[package]] -name = "tree-sitter-compact" -version = "0.1.0" -dependencies = [ - "cc", - "tree-sitter", - "tree-sitter-language", -] - [[package]] name = "tree-sitter-language" version = "0.1.8" diff --git a/biome.json b/biome.json index 88b7ec7..045f84c 100644 --- a/biome.json +++ b/biome.json @@ -13,7 +13,7 @@ "!**/tsconfig.base.json", "!**/tsconfig*.json", "!**/*.compact", - "!crates/tree-sitter-compact/src/**/*", + "!crates/compact-tree-sitter/src/**/*", "!**/artifacts/**/*", "!**/coverage/**/*", "!**/dist/**/*", diff --git a/crates/tree-sitter-compact/Cargo.toml b/crates/compact-tree-sitter/Cargo.toml similarity index 95% rename from crates/tree-sitter-compact/Cargo.toml rename to crates/compact-tree-sitter/Cargo.toml index 3626181..8ae71a8 100644 --- a/crates/tree-sitter-compact/Cargo.toml +++ b/crates/compact-tree-sitter/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tree-sitter-compact" +name = "compact-tree-sitter" version = "0.1.0" description = "Declaration-level Compact grammar for tree-sitter" build = "bindings/rust/build.rs" diff --git a/crates/tree-sitter-compact/README.md b/crates/compact-tree-sitter/README.md similarity index 97% rename from crates/tree-sitter-compact/README.md rename to crates/compact-tree-sitter/README.md index 287c198..50f0d70 100644 --- a/crates/tree-sitter-compact/README.md +++ b/crates/compact-tree-sitter/README.md @@ -1,4 +1,4 @@ -# tree-sitter-compact +# compact-tree-sitter Declaration-level [tree-sitter](https://tree-sitter.github.io) grammar for the Compact smart-contract language (Midnight), tracking language version 0.26. @@ -68,7 +68,7 @@ are generated and committed. After editing `grammar.js`: npx -y tree-sitter-cli@0.25.8 generate ``` -CI regenerates and fails on any diff under `crates/tree-sitter-compact/src`. +CI regenerates and fails on any diff under `crates/compact-tree-sitter/src`. ## Tests diff --git a/crates/tree-sitter-compact/bindings/rust/build.rs b/crates/compact-tree-sitter/bindings/rust/build.rs similarity index 93% rename from crates/tree-sitter-compact/bindings/rust/build.rs rename to crates/compact-tree-sitter/bindings/rust/build.rs index c65afd8..603d5b7 100644 --- a/crates/tree-sitter-compact/bindings/rust/build.rs +++ b/crates/compact-tree-sitter/bindings/rust/build.rs @@ -10,7 +10,7 @@ fn main() { config.flag_if_supported("-Wno-unused-parameter"); config.flag_if_supported("-Wno-trigraphs"); - config.compile("tree-sitter-compact"); + config.compile("compact-tree-sitter"); println!("cargo:rerun-if-changed=src/parser.c"); println!("cargo:rerun-if-changed=src/grammar.json"); diff --git a/crates/tree-sitter-compact/bindings/rust/lib.rs b/crates/compact-tree-sitter/bindings/rust/lib.rs similarity index 100% rename from crates/tree-sitter-compact/bindings/rust/lib.rs rename to crates/compact-tree-sitter/bindings/rust/lib.rs diff --git a/crates/tree-sitter-compact/grammar.js b/crates/compact-tree-sitter/grammar.js similarity index 100% rename from crates/tree-sitter-compact/grammar.js rename to crates/compact-tree-sitter/grammar.js diff --git a/crates/tree-sitter-compact/package.json b/crates/compact-tree-sitter/package.json similarity index 89% rename from crates/tree-sitter-compact/package.json rename to crates/compact-tree-sitter/package.json index 7f85f18..2760c2c 100644 --- a/crates/tree-sitter-compact/package.json +++ b/crates/compact-tree-sitter/package.json @@ -1,5 +1,5 @@ { - "name": "tree-sitter-compact", + "name": "compact-tree-sitter", "version": "0.1.0", "private": true, "description": "Declaration-level Compact grammar for tree-sitter", diff --git a/crates/tree-sitter-compact/queries/highlights.scm b/crates/compact-tree-sitter/queries/highlights.scm similarity index 100% rename from crates/tree-sitter-compact/queries/highlights.scm rename to crates/compact-tree-sitter/queries/highlights.scm diff --git a/crates/tree-sitter-compact/src/grammar.json b/crates/compact-tree-sitter/src/grammar.json similarity index 100% rename from crates/tree-sitter-compact/src/grammar.json rename to crates/compact-tree-sitter/src/grammar.json diff --git a/crates/tree-sitter-compact/src/node-types.json b/crates/compact-tree-sitter/src/node-types.json similarity index 100% rename from crates/tree-sitter-compact/src/node-types.json rename to crates/compact-tree-sitter/src/node-types.json diff --git a/crates/tree-sitter-compact/src/parser.c b/crates/compact-tree-sitter/src/parser.c similarity index 100% rename from crates/tree-sitter-compact/src/parser.c rename to crates/compact-tree-sitter/src/parser.c diff --git a/crates/tree-sitter-compact/src/tree_sitter/alloc.h b/crates/compact-tree-sitter/src/tree_sitter/alloc.h similarity index 100% rename from crates/tree-sitter-compact/src/tree_sitter/alloc.h rename to crates/compact-tree-sitter/src/tree_sitter/alloc.h diff --git a/crates/tree-sitter-compact/src/tree_sitter/array.h b/crates/compact-tree-sitter/src/tree_sitter/array.h similarity index 100% rename from crates/tree-sitter-compact/src/tree_sitter/array.h rename to crates/compact-tree-sitter/src/tree_sitter/array.h diff --git a/crates/tree-sitter-compact/src/tree_sitter/parser.h b/crates/compact-tree-sitter/src/tree_sitter/parser.h similarity index 100% rename from crates/tree-sitter-compact/src/tree_sitter/parser.h rename to crates/compact-tree-sitter/src/tree_sitter/parser.h diff --git a/crates/tree-sitter-compact/test/corpus/circuits-and-constructors.txt b/crates/compact-tree-sitter/test/corpus/circuits-and-constructors.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/circuits-and-constructors.txt rename to crates/compact-tree-sitter/test/corpus/circuits-and-constructors.txt diff --git a/crates/tree-sitter-compact/test/corpus/comments.txt b/crates/compact-tree-sitter/test/corpus/comments.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/comments.txt rename to crates/compact-tree-sitter/test/corpus/comments.txt diff --git a/crates/tree-sitter-compact/test/corpus/imports.txt b/crates/compact-tree-sitter/test/corpus/imports.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/imports.txt rename to crates/compact-tree-sitter/test/corpus/imports.txt diff --git a/crates/tree-sitter-compact/test/corpus/modules-and-exports.txt b/crates/compact-tree-sitter/test/corpus/modules-and-exports.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/modules-and-exports.txt rename to crates/compact-tree-sitter/test/corpus/modules-and-exports.txt diff --git a/crates/tree-sitter-compact/test/corpus/pragma-and-include.txt b/crates/compact-tree-sitter/test/corpus/pragma-and-include.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/pragma-and-include.txt rename to crates/compact-tree-sitter/test/corpus/pragma-and-include.txt diff --git a/crates/tree-sitter-compact/test/corpus/structs-enums-contracts.txt b/crates/compact-tree-sitter/test/corpus/structs-enums-contracts.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/structs-enums-contracts.txt rename to crates/compact-tree-sitter/test/corpus/structs-enums-contracts.txt diff --git a/crates/tree-sitter-compact/test/corpus/types-ledger-witness.txt b/crates/compact-tree-sitter/test/corpus/types-ledger-witness.txt similarity index 100% rename from crates/tree-sitter-compact/test/corpus/types-ledger-witness.txt rename to crates/compact-tree-sitter/test/corpus/types-ledger-witness.txt diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/NOTICE b/crates/compact-tree-sitter/test/fixtures/upstream/NOTICE similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/NOTICE rename to crates/compact-tree-sitter/test/fixtures/upstream/NOTICE diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/already_formatted.compact b/crates/compact-tree-sitter/test/fixtures/upstream/already_formatted.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/already_formatted.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/already_formatted.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_1.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_1.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_1.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_1.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_10.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_10.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_10.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_10.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_11.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_11.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_11.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_11.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_12.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_12.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_12.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_12.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_13.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_13.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_13.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_13.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_2.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_2.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_2.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_2.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_3.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_3.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_3.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_3.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_4.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_4.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_4.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_4.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_5.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_5.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_5.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_5.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_6.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_6.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_6.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_6.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_7.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_7.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_7.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_7.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_8.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_8.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_8.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_8.compact diff --git a/crates/tree-sitter-compact/test/fixtures/upstream/example_9.compact b/crates/compact-tree-sitter/test/fixtures/upstream/example_9.compact similarity index 100% rename from crates/tree-sitter-compact/test/fixtures/upstream/example_9.compact rename to crates/compact-tree-sitter/test/fixtures/upstream/example_9.compact diff --git a/crates/tree-sitter-compact/tests/corpus.rs b/crates/compact-tree-sitter/tests/corpus.rs similarity index 99% rename from crates/tree-sitter-compact/tests/corpus.rs rename to crates/compact-tree-sitter/tests/corpus.rs index ea03373..7ff16ed 100644 --- a/crates/tree-sitter-compact/tests/corpus.rs +++ b/crates/compact-tree-sitter/tests/corpus.rs @@ -10,7 +10,7 @@ const SKIPPED_DIRS: [&str; 5] = ["node_modules", "target", "dist", "build", ".gi fn parser() -> Parser { let mut parser = Parser::new(); parser - .set_language(&tree_sitter_compact::LANGUAGE.into()) + .set_language(&compact_tree_sitter::LANGUAGE.into()) .expect("the generated parser matches the tree-sitter ABI this crate builds against"); parser } diff --git a/crates/tree-sitter-compact/tree-sitter.json b/crates/compact-tree-sitter/tree-sitter.json similarity index 100% rename from crates/tree-sitter-compact/tree-sitter.json rename to crates/compact-tree-sitter/tree-sitter.json From ec5cb8a0e5d21cfe11d7a78870ce45b8e994d4ff Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 09:43:29 +0200 Subject: [PATCH 03/14] feat(compact-lint): add the check command Adds the compact-lint CLI with one subcommand, check, that enforces per-declaration-kind doc-comment templates from compact-lint.toml and optionally runs compact format --check. Findings print one per line as path:line:col: rule: message on stdout; exit 1 on findings, 2 on any error. fix and fill-constraints come later and only add tables to the same toml. * doc: tags parsed into an ordered list with line and column, so a finding points at the @ of the offending tag. * rules: missing-doc, missing-tag, forbidden-tag, module-name, missing-constraints, constraints-format, constraints-placeholder (--strict), parse, format. Constraints apply to exported non-pure circuits only. * config: every field optional, unknown keys and tags without their @ rejected; an explicit --config is a preset anchored at the cwd. * tests: one fixture case per rule through the binary, plus unit tests per module. --- Cargo.lock | 365 +++++++++++++++ Cargo.toml | 10 + crates/compact-lint/Cargo.toml | 33 ++ crates/compact-lint/README.md | 128 ++++++ .../examples/compact-contracts.toml | 40 ++ crates/compact-lint/src/check.rs | 117 +++++ crates/compact-lint/src/config.rs | 305 +++++++++++++ crates/compact-lint/src/discover.rs | 114 +++++ crates/compact-lint/src/doc.rs | 211 +++++++++ crates/compact-lint/src/format.rs | 156 +++++++ crates/compact-lint/src/lib.rs | 16 + crates/compact-lint/src/main.rs | 114 +++++ crates/compact-lint/src/model.rs | 177 ++++++++ crates/compact-lint/src/report.rs | 142 ++++++ crates/compact-lint/src/rules.rs | 415 ++++++++++++++++++ crates/compact-lint/tests/check.rs | 172 ++++++++ .../tests/fixtures/clean/Clean.compact | 56 +++ .../tests/fixtures/clean/compact-lint.toml | 27 ++ .../tests/fixtures/clean/expected.txt | 0 .../constraints-format/Malformed.compact | 17 + .../constraints-format/compact-lint.toml | 27 ++ .../fixtures/constraints-format/expected.txt | 2 + .../constraints-placeholder/Unfilled.compact | 11 + .../constraints-placeholder/compact-lint.toml | 27 ++ .../constraints-placeholder/expected.txt | 1 + .../fixtures/forbidden-tag/Returns.compact | 18 + .../fixtures/forbidden-tag/compact-lint.toml | 27 ++ .../tests/fixtures/forbidden-tag/expected.txt | 2 + .../missing-constraints/Unmeasured.compact | 17 + .../missing-constraints/compact-lint.toml | 27 ++ .../fixtures/missing-constraints/expected.txt | 1 + .../tests/fixtures/missing-doc/Gaps.compact | 20 + .../fixtures/missing-doc/compact-lint.toml | 27 ++ .../tests/fixtures/missing-doc/expected.txt | 3 + .../fixtures/missing-tag/Untagged.compact | 10 + .../fixtures/missing-tag/compact-lint.toml | 27 ++ .../tests/fixtures/missing-tag/expected.txt | 2 + .../fixtures/module-name/Renamed.compact | 6 + .../fixtures/module-name/compact-lint.toml | 27 ++ .../tests/fixtures/module-name/expected.txt | 1 + .../tests/fixtures/parse/Broken.compact | 3 + .../tests/fixtures/parse/compact-lint.toml | 2 + .../tests/fixtures/parse/expected.txt | 1 + 43 files changed, 2904 insertions(+) create mode 100644 crates/compact-lint/Cargo.toml create mode 100644 crates/compact-lint/README.md create mode 100644 crates/compact-lint/examples/compact-contracts.toml create mode 100644 crates/compact-lint/src/check.rs create mode 100644 crates/compact-lint/src/config.rs create mode 100644 crates/compact-lint/src/discover.rs create mode 100644 crates/compact-lint/src/doc.rs create mode 100644 crates/compact-lint/src/format.rs create mode 100644 crates/compact-lint/src/lib.rs create mode 100644 crates/compact-lint/src/main.rs create mode 100644 crates/compact-lint/src/model.rs create mode 100644 crates/compact-lint/src/report.rs create mode 100644 crates/compact-lint/src/rules.rs create mode 100644 crates/compact-lint/tests/check.rs create mode 100644 crates/compact-lint/tests/fixtures/clean/Clean.compact create mode 100644 crates/compact-lint/tests/fixtures/clean/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/clean/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/constraints-format/Malformed.compact create mode 100644 crates/compact-lint/tests/fixtures/constraints-format/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/constraints-format/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/constraints-placeholder/Unfilled.compact create mode 100644 crates/compact-lint/tests/fixtures/constraints-placeholder/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/constraints-placeholder/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/forbidden-tag/Returns.compact create mode 100644 crates/compact-lint/tests/fixtures/forbidden-tag/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/forbidden-tag/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/missing-constraints/Unmeasured.compact create mode 100644 crates/compact-lint/tests/fixtures/missing-constraints/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/missing-constraints/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/missing-doc/Gaps.compact create mode 100644 crates/compact-lint/tests/fixtures/missing-doc/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/missing-doc/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/missing-tag/Untagged.compact create mode 100644 crates/compact-lint/tests/fixtures/missing-tag/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/missing-tag/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/module-name/Renamed.compact create mode 100644 crates/compact-lint/tests/fixtures/module-name/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/module-name/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/parse/Broken.compact create mode 100644 crates/compact-lint/tests/fixtures/parse/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/parse/expected.txt diff --git a/Cargo.lock b/Cargo.lock index f1a0406..80f353a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,88 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "cc" version = "1.4.6" @@ -21,6 +103,68 @@ dependencies = [ "shlex", ] +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact-lint" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "clap", + "compact-tree-sitter", + "globset", + "serde", + "thiserror", + "toml", + "tree-sitter", + "walkdir", +] + [[package]] name = "compact-tree-sitter" version = "0.1.0" @@ -30,6 +174,12 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "equivalent" version = "1.0.2" @@ -42,12 +192,31 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indexmap" version = "2.14.2" @@ -58,18 +227,69 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -117,6 +337,15 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.229" @@ -124,6 +353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -160,6 +390,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "shlex" version = "2.0.1" @@ -172,6 +411,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "3.0.5" @@ -183,6 +428,71 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tree-sitter" version = "0.25.10" @@ -209,6 +519,61 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index af07435..5e096be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,16 @@ rust-version = "1.96" pedantic = { level = "warn", priority = -1 } [workspace.dependencies] +anyhow = "1.0" cc = "1" +clap = { version = "4.6", features = ["derive", "env"] } +globset = "0.4" +serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0" +toml = "1.1" tree-sitter = "0.25" +compact-tree-sitter = { version = "0.1.0", path = "crates/compact-tree-sitter" } tree-sitter-language = "0.1" +walkdir = "2.5" + +assert_cmd = "2.2" diff --git a/crates/compact-lint/Cargo.toml b/crates/compact-lint/Cargo.toml new file mode 100644 index 0000000..0983475 --- /dev/null +++ b/crates/compact-lint/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "compact-lint" +version = "0.1.0" +description = "Doc-comment linter for Compact sources" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[lib] +name = "compact_lint" +path = "src/lib.rs" + +[[bin]] +name = "compact-lint" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +globset.workspace = true +serde.workspace = true +thiserror.workspace = true +toml.workspace = true +tree-sitter.workspace = true +compact-tree-sitter.workspace = true +walkdir.workspace = true + +[dev-dependencies] +assert_cmd.workspace = true diff --git a/crates/compact-lint/README.md b/crates/compact-lint/README.md new file mode 100644 index 0000000..28bd628 --- /dev/null +++ b/crates/compact-lint/README.md @@ -0,0 +1,128 @@ +# compact-lint + +Doc-comment linter for Compact sources. It parses `.compact` files with +[`compact-tree-sitter`](../compact-tree-sitter), matches every declaration against the +per-kind template in `compact-lint.toml`, and prints one finding per line. + +## Usage + +```sh +compact-lint check [PATHS]... [--config ] [--strict] [--no-format] [--compact-bin ] +``` + +- `PATHS` — files or directories, walked recursively for `.compact`. Skipped while + walking: `node_modules`, `target`, `dist`, `build`, `.git`. +- No `PATHS` — the config's `include` globs are used instead. +- `--config ` — use this config instead of searching upward for + `compact-lint.toml`. +- `--strict` — turn `k=?` / `rows=?` placeholders into findings. Use it on release + branches. +- `--no-format` — skip the `compact format --check` pass. Required where the `compact` + binary is unavailable, CI included. +- `--compact-bin ` — path to the `compact` binary. Also settable with + `COMPACT_LINT_COMPACT_BIN`; the flag wins. + +Findings go to stdout, one per line, in `path:line:col: rule: message` form (the +rustc / eslint shape, so editors and `grep` both read it). The `N findings in M files` +summary goes to stderr, where `M` is the number of files checked. + +## Exit codes + +- `0` — no findings. +- `1` — findings. +- `2` — usage, config, IO or parser error. + +## Rules + +| Rule | Meaning | +| --- | --- | +| `missing-doc` | The declaration needs docs per `kinds..docs` and has no doc comment attached. | +| `missing-tag` | A tag listed in `kinds..tags` is absent from the doc comment. | +| `forbidden-tag` | A tag listed in `tags.forbid` is present. | +| `module-name` | `@module ` does not name the module it documents. | +| `missing-constraints` | An exported non-pure circuit has no constraints tag. | +| `constraints-format` | The constraints value is not `k=, rows=`. | +| `constraints-placeholder` | `--strict` only: the constraints value still holds a `?`. | +| `parse` | The file holds a syntax defect; the first one is reported and the doc rules are skipped for that file. | +| `format` | `compact format --check` reported the file. | + +Rule detail: + +- **Attachment.** A doc comment counts as attached only when it is the declaration's + immediately preceding *named* sibling. A `//` or `/* */` comment between the two + breaks the attachment and the declaration reads as undocumented. +- **Export.** "Exported" means the declaration carries the `export` token itself. A + non-exported circuit inside an exported module is not exported. +- **Constraints.** The rule applies to exported non-pure circuits only, and that is + fixed, not configurable. A missing doc comment reports as `missing-doc`, not + `missing-constraints`. +- **`module-name`.** The documented name is the first word of the `@module` value, + without its generic arguments or trailing punctuation, so `@module Signer` matches + `module Signer` and `@module Utils.` matches `module Utils`. +- **`forbidden-tag`.** It fires on any doc comment attached to a checked declaration, + including declarations whose docs are optional. + +## Config + +`compact-lint.toml`, searched for upward from the current directory. Every field is +optional; unknown fields are rejected. A config found by walking upward anchors the +`include` globs at its own directory; a config named with `--config` is a shareable +preset, so the globs stay anchored at the current directory. + +```toml +version = 1 +include = ["contracts/src/**/*.compact"] +exclude = ["**/test/mocks/**", "**/archive/**"] + +[constraints] +tag = "@constraints" + +[tags] +forbid = ["@return"] + +[kinds.module] +docs = "exported" +tags = ["@module", "@description"] +``` + +Defaults when no config file is found: + +| Key | Default | +| --- | --- | +| `version` | `1`, the only version this build accepts | +| `include` | `["**/*.compact"]` | +| `exclude` | `[]` | +| `constraints.tag` | `"@constraints"` | +| `tags.forbid` | `[]` | +| `kinds..docs` | `"exported"` | +| `kinds..tags` | `[]` | + +- `kinds` takes one table per kind: `module`, `circuit`, `ledger`, `witness`, + `constructor`, `struct`, `enum`, `contract`, `type`. `pragma`, `import`, `include`, + `export { … }` and `contract implements` are never checked. +- `docs` is `"all"`, `"exported"` or `"none"`. +- `constructor` carries no `export` token, so `"exported"` means "never" for it. Use + `"all"` or `"none"` there. +- `exclude` filters files found by walking a directory. A file named on the command + line is always checked. + +`examples/compact-contracts.toml` is the config for OpenZeppelin/compact-contracts. + +## Doc-comment model + +- `/**`, `*/` and the leading ` * ` gutter are stripped. +- A tag line's first non-blank token is `@` followed by one or more ASCII letters. +- A tag's value is the rest of its line plus every following line until the next tag or + a blank line. +- Repeated tags are kept in source order; `@param` normally repeats. +- `@param {Type} name - text` is not parsed further. + +## Tests + +```sh +cargo test --workspace +``` + +`tests/fixtures//` holds one case per rule plus a clean case: a +`compact-lint.toml`, the `.compact` sources, and `expected.txt`, the exact stdout with +paths relative to the case directory. diff --git a/crates/compact-lint/examples/compact-contracts.toml b/crates/compact-lint/examples/compact-contracts.toml new file mode 100644 index 0000000..402186d --- /dev/null +++ b/crates/compact-lint/examples/compact-contracts.toml @@ -0,0 +1,40 @@ +# compact-lint config for OpenZeppelin/compact-contracts. +# Copy it to the repository root as compact-lint.toml. + +version = 1 +include = ["contracts/src/**/*.compact"] +exclude = ["**/test/mocks/**", "**/archive/**"] + +# `@constraints` is the default; set `tag` to point the rule at another annotation. +[constraints] +tag = "@constraints" + +[tags] +forbid = ["@return"] + +[kinds.module] +docs = "exported" +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" + +[kinds.contract] + +[kinds.type] diff --git a/crates/compact-lint/src/check.rs b/crates/compact-lint/src/check.rs new file mode 100644 index 0000000..2f2d2a8 --- /dev/null +++ b/crates/compact-lint/src/check.rs @@ -0,0 +1,117 @@ +//! The `check` subcommand: resolve config, collect files, run the rules. + +use std::ffi::OsString; +use std::path::{Path, PathBuf}; + +use thiserror::Error; + +use crate::config::{Config, ConfigError, discover as discover_config}; +use crate::discover::{self, DiscoverError}; +use crate::format::{self, FormatError}; +use crate::report::Finding; +use crate::rules::{LintError, Linter}; + +#[derive(Debug, Error)] +pub enum CheckError { + #[error(transparent)] + Config(#[from] ConfigError), + #[error(transparent)] + Discover(#[from] DiscoverError), + #[error(transparent)] + Lint(#[from] LintError), + #[error(transparent)] + Format(#[from] FormatError), + #[error("cannot read {path}")] + Read { + path: PathBuf, + #[source] + source: std::io::Error, + }, +} + +/// Everything the `check` subcommand needs, already resolved from flags and env. +pub struct Options { + pub paths: Vec, + pub config_path: Option, + pub strict: bool, + pub no_format: bool, + pub compact_bin: OsString, +} + +/// The findings, plus how many files produced them. +pub struct Outcome { + pub findings: Vec, + pub files_checked: usize, +} + +impl Outcome { + #[must_use] + pub fn summary(&self) -> String { + format!( + "{} findings in {} files", + self.findings.len(), + self.files_checked + ) + } +} + +/// Runs the subcommand with `cwd` as the working directory. +/// # Errors +/// Returns an error when the config, the file walk, the parser or the formatter fails. +pub fn run(options: &Options, cwd: &Path) -> Result { + let (config, config_path, base) = resolve_config(options, cwd)?; + let source = config_path.as_deref().unwrap_or(Path::new("")); + + let exclude = config.exclude_set(source)?; + let files = if options.paths.is_empty() { + let include = config.include_set(source)?; + discover::from_globs(&base, &include, &exclude)? + } else { + discover::from_paths(&options.paths, &base, &exclude)? + }; + let files: Vec = files.iter().map(|path| display_path(path, cwd)).collect(); + + let mut linter = Linter::new()?; + let mut findings = Vec::new(); + for path in &files { + let text = std::fs::read_to_string(path).map_err(|source| CheckError::Read { + path: path.clone(), + source, + })?; + findings.extend(linter.check(path, &text, &config, options.strict)?); + } + + if !options.no_format { + findings.extend(format::check(&options.compact_bin, &files)?); + } + + findings.sort_by(|left, right| left.sort_key().cmp(&right.sort_key())); + Ok(Outcome { + findings, + files_checked: files.len(), + }) +} + +/// An explicit `--config` is a shareable preset, so glob discovery stays anchored at +/// `cwd`; a config found by walking upward anchors discovery at its own directory. +fn resolve_config( + options: &Options, + cwd: &Path, +) -> Result<(Config, Option, PathBuf), ConfigError> { + if let Some(path) = options.config_path.as_deref() { + return Ok((Config::load(path)?, Some(path.to_owned()), cwd.to_owned())); + } + + match discover_config(cwd) { + Some(path) => { + let base = path.parent().unwrap_or(cwd).to_owned(); + Ok((Config::load(&path)?, Some(path), base)) + } + None => Ok((Config::default(), None, cwd.to_owned())), + } +} + +/// Paths under the working directory print relative to it; anything else prints as is. +fn display_path(path: &Path, cwd: &Path) -> PathBuf { + path.strip_prefix(cwd).unwrap_or(path).to_owned() +} diff --git a/crates/compact-lint/src/config.rs b/crates/compact-lint/src/config.rs new file mode 100644 index 0000000..0d7ae2b --- /dev/null +++ b/crates/compact-lint/src/config.rs @@ -0,0 +1,305 @@ +//! `compact-lint.toml`: discovery, parsing, and the built-in defaults. + +use std::path::{Path, PathBuf}; + +use globset::{Glob, GlobSet, GlobSetBuilder}; +use serde::Deserialize; +use thiserror::Error; + +use crate::doc::Tag; +use crate::model::DeclKind; + +/// The config file name searched for upward from the current directory. +pub const CONFIG_FILE_NAME: &str = "compact-lint.toml"; + +/// The only schema version this build understands. +pub const SUPPORTED_VERSION: u32 = 1; + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("cannot read config {path}")] + Read { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("cannot parse config {path}")] + Parse { + path: PathBuf, + #[source] + source: toml::de::Error, + }, + #[error( + "config {path} is version {found}, but this build understands version {SUPPORTED_VERSION}" + )] + Version { path: PathBuf, found: u32 }, + #[error( + "config {path} lists tag {tag:?}; tags are spelled with their `@`, like `@description`" + )] + Tag { path: PathBuf, tag: String }, + #[error("config {path} has an invalid glob {pattern:?}")] + Glob { + path: PathBuf, + pattern: String, + #[source] + source: globset::Error, + }, +} + +/// How much of a kind needs documenting. +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum DocsPolicy { + /// Every declaration of the kind. + All, + /// Only declarations carrying the `export` token. + #[default] + Exported, + /// The kind is not checked for docs. + None, +} + +/// Per-kind requirements. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields, default)] +pub struct KindConfig { + pub docs: DocsPolicy, + pub tags: Vec, +} + +/// One table per checked declaration kind. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields, default)] +pub struct KindsConfig { + pub module: KindConfig, + pub circuit: KindConfig, + pub ledger: KindConfig, + pub witness: KindConfig, + pub constructor: KindConfig, + #[serde(rename = "struct")] + pub struct_: KindConfig, + #[serde(rename = "enum")] + pub enum_: KindConfig, + pub contract: KindConfig, + #[serde(rename = "type")] + pub type_: KindConfig, +} + +impl KindsConfig { + #[must_use] + pub fn get(&self, kind: DeclKind) -> &KindConfig { + match kind { + DeclKind::Module => &self.module, + DeclKind::Circuit => &self.circuit, + DeclKind::Ledger => &self.ledger, + DeclKind::Witness => &self.witness, + DeclKind::Constructor => &self.constructor, + DeclKind::Struct => &self.struct_, + DeclKind::Enum => &self.enum_, + DeclKind::Contract => &self.contract, + DeclKind::Type => &self.type_, + } + } +} + +/// The circuit constraints annotation. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields, default)] +pub struct ConstraintsConfig { + pub tag: Tag, +} + +impl Default for ConstraintsConfig { + fn default() -> Self { + Self { + tag: Tag::new("@constraints"), + } + } +} + +/// Tags no doc comment may carry. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields, default)] +pub struct TagsConfig { + pub forbid: Vec, +} + +/// A parsed `compact-lint.toml`, or the built-in defaults when no file was found. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + pub version: u32, + pub include: Vec, + pub exclude: Vec, + pub constraints: ConstraintsConfig, + pub tags: TagsConfig, + pub kinds: KindsConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + version: SUPPORTED_VERSION, + include: vec!["**/*.compact".to_owned()], + exclude: Vec::new(), + constraints: ConstraintsConfig::default(), + tags: TagsConfig::default(), + kinds: KindsConfig::default(), + } + } +} + +impl Config { + /// Parses a config file and rejects a version this build does not implement. + /// # Errors + /// Returns an error when the file is unreadable, malformed, or a future version. + pub fn load(path: &Path) -> Result { + let text = std::fs::read_to_string(path).map_err(|source| ConfigError::Read { + path: path.to_owned(), + source, + })?; + + let config: Self = toml::from_str(&text).map_err(|source| ConfigError::Parse { + path: path.to_owned(), + source, + })?; + + if config.version != SUPPORTED_VERSION { + return Err(ConfigError::Version { + path: path.to_owned(), + found: config.version, + }); + } + + if let Some(tag) = config.tags().find(|tag| !tag.is_well_formed()) { + return Err(ConfigError::Tag { + path: path.to_owned(), + tag: tag.to_string(), + }); + } + + Ok(config) + } + + /// Every tag the config mentions: required per kind, forbidden, and the constraints tag. + fn tags(&self) -> impl Iterator { + let per_kind = [ + &self.kinds.module, + &self.kinds.circuit, + &self.kinds.ledger, + &self.kinds.witness, + &self.kinds.constructor, + &self.kinds.struct_, + &self.kinds.enum_, + &self.kinds.contract, + &self.kinds.type_, + ] + .into_iter() + .flat_map(|kind| kind.tags.iter()); + + per_kind + .chain(self.tags.forbid.iter()) + .chain(std::iter::once(&self.constraints.tag)) + } + + /// Compiles the `include` globs. + /// # Errors + /// Returns an error when a pattern is not a valid glob. + pub fn include_set(&self, path: &Path) -> Result { + glob_set(&self.include, path) + } + + /// Compiles the `exclude` globs; an empty set matches nothing. + /// # Errors + /// Returns an error when a pattern is not a valid glob. + pub fn exclude_set(&self, path: &Path) -> Result { + glob_set(&self.exclude, path) + } +} + +fn glob_set(patterns: &[String], path: &Path) -> Result { + let mut builder = GlobSetBuilder::new(); + for pattern in patterns { + let glob = Glob::new(pattern).map_err(|source| ConfigError::Glob { + path: path.to_owned(), + pattern: pattern.clone(), + source, + })?; + builder.add(glob); + } + builder.build().map_err(|source| ConfigError::Glob { + path: path.to_owned(), + pattern: String::new(), + source, + }) +} + +/// Walks up from `start` looking for `compact-lint.toml`. +#[must_use] +pub fn discover(start: &Path) -> Option { + for directory in start.ancestors() { + let candidate = directory.join(CONFIG_FILE_NAME); + if candidate.is_file() { + return Some(candidate); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::{Config, DocsPolicy, SUPPORTED_VERSION}; + use crate::doc::Tag; + use crate::model::DeclKind; + + #[test] + fn defaults_require_docs_on_exported_declarations_only() { + let config = Config::default(); + + assert_eq!(config.version, SUPPORTED_VERSION); + assert_eq!(config.constraints.tag, Tag::new("@constraints")); + for kind in [DeclKind::Module, DeclKind::Circuit, DeclKind::Type] { + assert_eq!(config.kinds.get(kind).docs, DocsPolicy::Exported); + assert!(config.kinds.get(kind).tags.is_empty()); + } + } + + #[test] + fn omitted_tables_fall_back_to_defaults() { + let config: Config = toml::from_str("version = 1\n[kinds.witness]\ndocs = \"all\"\n") + .expect("the snippet is valid config"); + + assert_eq!(config.kinds.witness.docs, DocsPolicy::All); + assert_eq!(config.kinds.circuit.docs, DocsPolicy::Exported); + assert_eq!(config.include, ["**/*.compact"]); + } + + #[test] + fn an_unknown_key_is_rejected() { + let error = toml::from_str::("verison = 1\n").expect_err("the key is misspelled"); + + assert!(error.to_string().contains("verison"), "{error}"); + } + + #[test] + fn a_tag_without_its_at_sign_is_rejected() { + let dir = std::env::temp_dir().join(format!("compact-lint-config-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("the temp dir is writable"); + let path = dir.join("compact-lint.toml"); + std::fs::write(&path, "[kinds.circuit]\ntags = [\"description\"]\n") + .expect("the config is writable"); + + let error = Config::load(&path).expect_err("the tag lacks its @"); + + assert!(error.to_string().contains("\"description\""), "{error}"); + std::fs::remove_dir_all(&dir).expect("the temp dir is removable"); + } + + #[test] + fn an_unknown_kind_is_rejected() { + let error = + toml::from_str::("[kinds.circiut]\n").expect_err("the kind is misspelled"); + + assert!(error.to_string().contains("circiut"), "{error}"); + } +} diff --git a/crates/compact-lint/src/discover.rs b/crates/compact-lint/src/discover.rs new file mode 100644 index 0000000..f647c9f --- /dev/null +++ b/crates/compact-lint/src/discover.rs @@ -0,0 +1,114 @@ +//! Turning CLI paths and config globs into the list of files to check. + +use std::path::{Path, PathBuf}; + +use globset::GlobSet; +use thiserror::Error; +use walkdir::WalkDir; + +/// Directories that never hold first-party Compact sources. +const SKIPPED_DIRS: [&str; 5] = ["node_modules", "target", "dist", "build", ".git"]; + +const COMPACT_EXTENSION: &str = "compact"; + +#[derive(Debug, Error)] +pub enum DiscoverError { + #[error("cannot walk {path}")] + Walk { + path: PathBuf, + #[source] + source: walkdir::Error, + }, + #[error("path {0} does not exist")] + Missing(PathBuf), +} + +/// Collects the `.compact` files under the given paths. +/// +/// A path named on the command line is always checked, even when `exclude` matches +/// it; files found by walking a directory are filtered by `exclude`. +/// # Errors +/// Returns an error when a path does not exist or a directory cannot be walked. +pub fn from_paths( + paths: &[PathBuf], + base: &Path, + exclude: &GlobSet, +) -> Result, DiscoverError> { + let mut files = Vec::new(); + + for path in paths { + if path.is_file() { + files.push(path.clone()); + } else if path.is_dir() { + walk(path, base, exclude, None, &mut files)?; + } else { + return Err(DiscoverError::Missing(path.clone())); + } + } + + Ok(sorted(files)) +} + +/// Collects the `.compact` files under `base` that `include` matches and `exclude` does not. +/// # Errors +/// Returns an error when `base` cannot be walked. +pub fn from_globs( + base: &Path, + include: &GlobSet, + exclude: &GlobSet, +) -> Result, DiscoverError> { + let mut files = Vec::new(); + walk(base, base, exclude, Some(include), &mut files)?; + Ok(sorted(files)) +} + +fn walk( + root: &Path, + base: &Path, + exclude: &GlobSet, + include: Option<&GlobSet>, + out: &mut Vec, +) -> Result<(), DiscoverError> { + let walker = WalkDir::new(root) + .follow_links(false) + .into_iter() + .filter_entry(|entry| entry.depth() == 0 || !is_skipped_dir(entry.file_name())); + + for entry in walker { + let entry = entry.map_err(|source| DiscoverError::Walk { + path: root.to_owned(), + source, + })?; + + if !entry.file_type().is_file() { + continue; + } + + let path = entry.path(); + if path + .extension() + .is_none_or(|extension| extension != COMPACT_EXTENSION) + { + continue; + } + + let relative = path.strip_prefix(base).unwrap_or(path); + if include.is_some_and(|set| !set.is_match(relative)) || exclude.is_match(relative) { + continue; + } + + out.push(path.to_owned()); + } + + Ok(()) +} + +fn is_skipped_dir(name: &std::ffi::OsStr) -> bool { + SKIPPED_DIRS.iter().any(|skipped| name == *skipped) +} + +fn sorted(mut files: Vec) -> Vec { + files.sort(); + files.dedup(); + files +} diff --git a/crates/compact-lint/src/doc.rs b/crates/compact-lint/src/doc.rs new file mode 100644 index 0000000..685007f --- /dev/null +++ b/crates/compact-lint/src/doc.rs @@ -0,0 +1,211 @@ +//! Parsing of `/** … */` doc comments into an ordered tag list. + +use std::fmt; + +use serde::Deserialize; + +/// A doc-comment tag name, stored with its leading `@` (`@description`). +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[serde(transparent)] +pub struct Tag(String); + +impl Tag { + pub fn new(name: impl Into) -> Self { + Self(name.into()) + } + + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } + + /// `@` followed by at least one ASCII letter, the same shape the parser recognises. + #[must_use] + pub fn is_well_formed(&self) -> bool { + self.0.strip_prefix('@').is_some_and(|name| { + !name.is_empty() && name.bytes().all(|byte| byte.is_ascii_alphabetic()) + }) + } +} + +impl fmt::Display for Tag { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +/// One tag occurrence: its name, its value, and where it sits inside the comment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TagOccurrence { + pub tag: Tag, + pub value: String, + /// 0-based line index inside the doc comment, counted from the `/**` line. + pub line_offset: usize, + /// 0-based byte offset of the `@` in its line; on the first line, counted after `/**`. + pub column_offset: usize, +} + +/// A parsed doc comment. Repeated tags (`@param`) are kept in source order. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DocComment { + tags: Vec, +} + +impl DocComment { + /// Parses the raw `/** … */` text, stripping the delimiters and the ` * ` gutter. + #[must_use] + pub fn parse(raw: &str) -> Self { + let opened = raw.strip_prefix("/**").unwrap_or(raw); + let inner = opened.strip_suffix("*/").unwrap_or(opened); + + let mut tags: Vec = Vec::new(); + let mut open: Option = None; + + for (line_offset, line) in inner.lines().enumerate() { + let content = strip_gutter(line).trim(); + + if content.is_empty() { + open = None; + continue; + } + + if let Some((tag, value)) = split_tag(content) { + tags.push(TagOccurrence { + tag, + value: value.to_owned(), + line_offset, + column_offset: line.find('@').unwrap_or(0), + }); + open = Some(tags.len() - 1); + continue; + } + + // A non-blank, non-tag line continues the tag above it. + if let Some(index) = open + && let Some(occurrence) = tags.get_mut(index) + { + if !occurrence.value.is_empty() { + occurrence.value.push('\n'); + } + occurrence.value.push_str(content); + } + } + + Self { tags } + } + + /// Every tag occurrence, in source order. + #[must_use] + pub fn tags(&self) -> &[TagOccurrence] { + &self.tags + } + + #[must_use] + pub fn has(&self, tag: &Tag) -> bool { + self.tags.iter().any(|occurrence| &occurrence.tag == tag) + } + + /// The first occurrence of `tag`, which is the one every rule reads. + #[must_use] + pub fn first(&self, tag: &Tag) -> Option<&TagOccurrence> { + self.tags.iter().find(|occurrence| &occurrence.tag == tag) + } + + /// The value of the first occurrence of `tag`. + #[must_use] + pub fn value(&self, tag: &Tag) -> Option<&str> { + self.first(tag).map(|occurrence| occurrence.value.as_str()) + } +} + +/// Removes the leading whitespace, one `*`, and one following space from a comment line. +fn strip_gutter(line: &str) -> &str { + let trimmed = line.trim_start(); + match trimmed.strip_prefix('*') { + Some(rest) => rest.strip_prefix(' ').unwrap_or(rest), + None => trimmed, + } +} + +/// A tag line starts with `@` followed by at least one ASCII letter. +fn split_tag(content: &str) -> Option<(Tag, &str)> { + let rest = content.strip_prefix('@')?; + let name_len = rest + .find(|character: char| !character.is_ascii_alphabetic()) + .unwrap_or(rest.len()); + if name_len == 0 { + return None; + } + + let (name, value) = rest.split_at(name_len); + Some((Tag::new(format!("@{name}")), value.trim())) +} + +#[cfg(test)] +mod tests { + use super::{DocComment, Tag}; + + #[test] + fn gutter_and_delimiters_are_stripped() { + let doc = DocComment::parse("/**\n * @description Adds two numbers.\n */"); + + assert_eq!(doc.tags().len(), 1); + assert_eq!(doc.tags()[0].tag, Tag::new("@description")); + assert_eq!(doc.tags()[0].value, "Adds two numbers."); + assert_eq!(doc.tags()[0].line_offset, 1); + assert_eq!(doc.tags()[0].column_offset, 3); + } + + #[test] + fn a_blank_line_ends_a_value() { + let doc = DocComment::parse("/**\n * @notice first\n *\n * loose prose\n */"); + + assert_eq!(doc.value(&Tag::new("@notice")), Some("first")); + } + + #[test] + fn a_non_tag_line_continues_the_tag_above_it() { + let doc = DocComment::parse("/**\n * @notice first\n * second\n */"); + + assert_eq!(doc.value(&Tag::new("@notice")), Some("first\nsecond")); + } + + #[test] + fn repeated_tags_keep_source_order() { + let doc = DocComment::parse( + "/**\n * @param {Uint<8>} a - left\n * @param {Uint<8>} b - right\n */", + ); + + let params: Vec<&str> = doc + .tags() + .iter() + .filter(|occurrence| occurrence.tag == Tag::new("@param")) + .map(|occurrence| occurrence.value.as_str()) + .collect(); + assert_eq!(params, ["{Uint<8>} a - left", "{Uint<8>} b - right"]); + } + + #[test] + fn return_and_returns_are_distinct_tags() { + let doc = DocComment::parse("/**\n * @returns The sum.\n */"); + + assert!(doc.has(&Tag::new("@returns"))); + assert!(!doc.has(&Tag::new("@return"))); + } + + #[test] + fn an_at_sign_without_letters_is_not_a_tag() { + let doc = DocComment::parse("/**\n * @1234 not a tag\n * mail@example.com\n */"); + + assert!(doc.tags().is_empty()); + } + + #[test] + fn a_single_line_comment_parses() { + let doc = DocComment::parse("/** @description One liner. */"); + + assert_eq!(doc.value(&Tag::new("@description")), Some("One liner.")); + assert_eq!(doc.tags()[0].line_offset, 0); + assert_eq!(doc.tags()[0].column_offset, 1); + } +} diff --git a/crates/compact-lint/src/format.rs b/crates/compact-lint/src/format.rs new file mode 100644 index 0000000..d3a0c46 --- /dev/null +++ b/crates/compact-lint/src/format.rs @@ -0,0 +1,156 @@ +//! The optional `compact format --check` pass. + +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use thiserror::Error; + +use crate::report::{Finding, Position, RuleId}; + +/// Environment override for the `compact` binary; the `--compact-bin` flag wins over it. +pub const COMPACT_BIN_ENV: &str = "COMPACT_LINT_COMPACT_BIN"; + +/// The default binary name, resolved through `PATH`. +pub const DEFAULT_COMPACT_BIN: &str = "compact"; + +pub const MESSAGE: &str = "not formatted; run `compact format`"; + +#[derive(Debug, Error)] +pub enum FormatError { + #[error("cannot run `{binary}`; pass --no-format to skip the format check")] + BinaryMissing { binary: String }, + #[error("cannot run `{binary}`")] + Spawn { + binary: String, + #[source] + source: std::io::Error, + }, +} + +/// Runs `compact format --check` once over every file and maps its report to findings. +/// +/// The formatter writes one `:` line per unformatted file to stderr, followed by +/// an indented diff. An output that holds no such line yields one finding on the first +/// checked file carrying the formatter's own first line. +/// # Errors +/// Returns an error when the `compact` binary is missing or cannot be spawned. +pub fn check(binary: &OsStr, files: &[PathBuf]) -> Result, FormatError> { + if files.is_empty() { + return Ok(Vec::new()); + } + + let output = Command::new(binary) + .arg("format") + .arg("--check") + .args(files) + .output() + .map_err(|source| { + let binary = binary.to_string_lossy().into_owned(); + if source.kind() == std::io::ErrorKind::NotFound { + FormatError::BinaryMissing { binary } + } else { + FormatError::Spawn { binary, source } + } + })?; + + if output.status.success() { + return Ok(Vec::new()); + } + + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let reported: Vec<&str> = reported_paths(&stderr) + .chain(reported_paths(&stdout)) + .collect(); + + if reported.is_empty() { + let raw = first_line(&stderr) + .or_else(|| first_line(&stdout)) + .unwrap_or("compact format --check failed"); + return Ok(vec![Finding::new( + files[0].clone(), + Position::file_start(), + RuleId::FORMAT, + raw.to_owned(), + )]); + } + + Ok(reported + .into_iter() + .map(|path| { + Finding::new( + resolve(path, files), + Position::file_start(), + RuleId::FORMAT, + MESSAGE.to_owned(), + ) + }) + .collect()) +} + +/// Unformatted files are announced as a flush-left `:` line. +fn reported_paths(output: &str) -> impl Iterator { + output.lines().filter_map(|line| { + let path = line.strip_suffix(':')?; + (!path.is_empty() && !path.starts_with(char::is_whitespace) && path.ends_with(".compact")) + .then_some(path) + }) +} + +/// Prefers the path spelling the caller passed in, so output stays consistent. +/// Matching is by whole path components, so `Token.compact` never claims `NativeToken.compact`. +fn resolve(reported: &str, files: &[PathBuf]) -> PathBuf { + let reported = Path::new(reported); + files + .iter() + .find(|file| reported.ends_with(file) || file.ends_with(reported)) + .cloned() + .unwrap_or_else(|| reported.to_owned()) +} + +fn first_line(output: &str) -> Option<&str> { + output.lines().map(str::trim).find(|line| !line.is_empty()) +} + +#[cfg(test)] +mod tests { + use super::{reported_paths, resolve}; + use std::path::PathBuf; + + const SAMPLE: &str = "/tmp/bad.compact:\n 18 | export ledger round: Counter;\n- 21 | - round.increment(1);\n+ 21 | + round.increment(1);\n\nError: formatting failed\n"; + + #[test] + fn flush_left_path_lines_become_findings() { + let paths: Vec<&str> = reported_paths(SAMPLE).collect(); + + assert_eq!(paths, ["/tmp/bad.compact"]); + } + + #[test] + fn indented_diff_lines_are_not_paths() { + let paths: Vec<&str> = reported_paths(" a.compact:\n\ttrailing:\n").collect(); + + assert!(paths.is_empty()); + } + + #[test] + fn an_absolute_report_maps_back_to_the_relative_path_passed_in() { + let files = [PathBuf::from("contracts/src/Token.compact")]; + + assert_eq!( + resolve("/work/contracts/src/Token.compact", &files), + PathBuf::from("contracts/src/Token.compact") + ); + } + + #[test] + fn a_shorter_file_name_with_the_same_suffix_is_not_a_match() { + let files = [PathBuf::from("Token.compact")]; + + assert_eq!( + resolve("/work/NativeToken.compact", &files), + PathBuf::from("/work/NativeToken.compact") + ); + } +} diff --git a/crates/compact-lint/src/lib.rs b/crates/compact-lint/src/lib.rs new file mode 100644 index 0000000..fdaf6e9 --- /dev/null +++ b/crates/compact-lint/src/lib.rs @@ -0,0 +1,16 @@ +//! Doc-comment linter for Compact sources. +//! +//! The crate parses `.compact` files with the bundled tree-sitter grammar, matches each +//! declaration against the per-kind doc-comment template in `compact-lint.toml`, and +//! reports one [`report::Finding`] per violation. + +#![forbid(unsafe_code)] + +pub mod check; +pub mod config; +pub mod discover; +pub mod doc; +pub mod format; +pub mod model; +pub mod report; +pub mod rules; diff --git a/crates/compact-lint/src/main.rs b/crates/compact-lint/src/main.rs new file mode 100644 index 0000000..dc4d364 --- /dev/null +++ b/crates/compact-lint/src/main.rs @@ -0,0 +1,114 @@ +//! `compact-lint` command line entry point. + +#![forbid(unsafe_code)] + +use std::ffi::OsString; +use std::io::Write; +use std::path::PathBuf; +use std::process::ExitCode; + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; + +use compact_lint::check::{self, Options, Outcome}; +use compact_lint::format::{COMPACT_BIN_ENV, DEFAULT_COMPACT_BIN}; + +/// Exit code for a run that produced findings. +const EXIT_FINDINGS: u8 = 1; + +/// Exit code for a usage, config, IO or parser error. +const EXIT_ERROR: u8 = 2; + +#[derive(Debug, Parser)] +#[command( + name = "compact-lint", + version, + about = "Doc-comment linter for Compact sources" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Check doc comments against the per-kind templates in compact-lint.toml. + Check(CheckArgs), +} + +#[derive(Debug, clap::Args)] +struct CheckArgs { + /// Files or directories to check; defaults to the config's include globs. + paths: Vec, + + /// Config file to use instead of searching upward for compact-lint.toml. + #[arg(long, value_name = "FILE")] + config: Option, + + /// Report `k=?` / `rows=?` placeholders in the constraints tag. + #[arg(long)] + strict: bool, + + /// Skip the `compact format --check` pass. + #[arg(long)] + no_format: bool, + + /// Path to the `compact` binary. + #[arg(long, value_name = "PATH", env = COMPACT_BIN_ENV)] + compact_bin: Option, +} + +fn main() -> ExitCode { + match run() { + Ok(code) => code, + Err(error) => { + report_error(&error); + ExitCode::from(EXIT_ERROR) + } + } +} + +fn run() -> Result { + let cli = Cli::parse(); + let Command::Check(args) = cli.command; + + let cwd = std::env::current_dir().context("reading the current directory")?; + let options = Options { + paths: args.paths, + config_path: args.config, + strict: args.strict, + no_format: args.no_format, + compact_bin: args + .compact_bin + .unwrap_or_else(|| OsString::from(DEFAULT_COMPACT_BIN)), + }; + + let outcome = check::run(&options, &cwd).context("running the check")?; + emit(&outcome).context("writing the report")?; + + Ok(if outcome.findings.is_empty() { + ExitCode::SUCCESS + } else { + ExitCode::from(EXIT_FINDINGS) + }) +} + +fn emit(outcome: &Outcome) -> std::io::Result<()> { + let stdout = std::io::stdout(); + let mut out = stdout.lock(); + for finding in &outcome.findings { + writeln!(out, "{finding}")?; + } + out.flush()?; + + eprintln!("{}", outcome.summary()); + Ok(()) +} + +/// Prints the error and every source under it, one cause per line. +fn report_error(error: &anyhow::Error) { + eprintln!("compact-lint: {error}"); + for cause in error.chain().skip(1) { + eprintln!(" caused by: {cause}"); + } +} diff --git a/crates/compact-lint/src/model.rs b/crates/compact-lint/src/model.rs new file mode 100644 index 0000000..968eee8 --- /dev/null +++ b/crates/compact-lint/src/model.rs @@ -0,0 +1,177 @@ +//! Declarations the linter checks, lifted out of the tree-sitter tree. + +use std::fmt; + +use tree_sitter::Node; + +use crate::doc::{DocComment, TagOccurrence}; +use crate::report::Position; + +/// The declaration kinds the linter has rules for. +/// +/// `pragma`, `include`, `import`, `export { … }` and `contract implements` are +/// never checked, so they have no variant here. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum DeclKind { + Module, + Circuit, + Ledger, + Witness, + Constructor, + Struct, + Enum, + Contract, + Type, +} + +impl DeclKind { + /// Maps a tree-sitter node kind, returning `None` for unchecked declarations. + #[must_use] + pub fn from_node_kind(node_kind: &str) -> Option { + match node_kind { + "module_declaration" => Some(Self::Module), + "circuit_declaration" => Some(Self::Circuit), + "ledger_declaration" => Some(Self::Ledger), + "witness_declaration" => Some(Self::Witness), + "constructor_declaration" => Some(Self::Constructor), + "struct_declaration" => Some(Self::Struct), + "enum_declaration" => Some(Self::Enum), + "contract_declaration" => Some(Self::Contract), + "type_declaration" => Some(Self::Type), + _ => None, + } + } + + /// The name used in config keys and messages. + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Module => "module", + Self::Circuit => "circuit", + Self::Ledger => "ledger", + Self::Witness => "witness", + Self::Constructor => "constructor", + Self::Struct => "struct", + Self::Enum => "enum", + Self::Contract => "contract", + Self::Type => "type", + } + } +} + +impl fmt::Display for DeclKind { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// One declaration with the facts the rules need. +#[derive(Clone, Debug)] +pub struct Declaration { + pub kind: DeclKind, + /// The `name` field; `constructor` has none. + pub name: Option, + /// The declaration carries the `export` token itself; nesting never grants it. + pub exported: bool, + pub pure: bool, + pub position: Position, + /// The doc comment immediately preceding the declaration, if one attaches. + pub doc: Option, +} + +/// A doc comment bound to a declaration, with the source line it starts on. +#[derive(Clone, Debug)] +pub struct AttachedDoc { + pub comment: DocComment, + /// 0-based row of the `/**` line, for turning a tag's offset into a position. + pub start_row: usize, + pub start_column: usize, +} + +impl AttachedDoc { + /// The position of a tag's `@`; a first-line offset is counted after the `/**`. + #[must_use] + pub const fn tag_position(&self, occurrence: &TagOccurrence) -> Position { + let column = if occurrence.line_offset == 0 { + self.start_column + "/**".len() + occurrence.column_offset + } else { + occurrence.column_offset + }; + Position::from_zero_based(self.start_row + occurrence.line_offset, column) + } +} + +/// Collects every checked declaration, descending into module bodies. +#[must_use] +pub fn declarations(root: Node<'_>, source: &str) -> Vec { + let mut found = Vec::new(); + collect(root, source, &mut found); + found +} + +fn collect(parent: Node<'_>, source: &str, out: &mut Vec) { + let mut cursor = parent.walk(); + for node in parent.named_children(&mut cursor) { + let Some(kind) = DeclKind::from_node_kind(node.kind()) else { + continue; + }; + + out.push(declaration(kind, node, source)); + + // Only modules nest; every other body is an opaque token run. + if kind == DeclKind::Module + && let Some(body) = node.child_by_field_name("body") + { + collect(body, source, out); + } + } +} + +fn declaration(kind: DeclKind, node: Node<'_>, source: &str) -> Declaration { + let mut cursor = node.walk(); + let mut exported = false; + let mut pure = false; + for child in node.children(&mut cursor) { + match child.kind() { + "export" => exported = true, + "pure" => pure = true, + _ => {} + } + } + + let name = node + .child_by_field_name("name") + .and_then(|name| text(name, source)) + .map(str::to_owned); + + let start = node.start_position(); + + Declaration { + kind, + name, + exported, + pure, + position: Position::from_zero_based(start.row, start.column), + doc: attached_doc(node, source), + } +} + +/// A doc comment attaches only as the immediately preceding named sibling; a line +/// or block comment in between breaks the attachment. +fn attached_doc(node: Node<'_>, source: &str) -> Option { + let previous = node.prev_named_sibling()?; + if previous.kind() != "doc_comment" { + return None; + } + + let start = previous.start_position(); + Some(AttachedDoc { + comment: DocComment::parse(text(previous, source)?), + start_row: start.row, + start_column: start.column, + }) +} + +fn text<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> { + source.get(node.start_byte()..node.end_byte()) +} diff --git a/crates/compact-lint/src/report.rs b/crates/compact-lint/src/report.rs new file mode 100644 index 0000000..b2d9f17 --- /dev/null +++ b/crates/compact-lint/src/report.rs @@ -0,0 +1,142 @@ +//! Findings and the one-line `path:line:col: rule: message` rendering. + +use std::fmt; +use std::path::{Path, PathBuf}; + +/// A lint rule's stable identifier, as printed in the output and matched by CI greps. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct RuleId(&'static str); + +impl RuleId { + pub const MISSING_DOC: Self = Self("missing-doc"); + pub const MISSING_TAG: Self = Self("missing-tag"); + pub const FORBIDDEN_TAG: Self = Self("forbidden-tag"); + pub const MODULE_NAME: Self = Self("module-name"); + pub const MISSING_CONSTRAINTS: Self = Self("missing-constraints"); + pub const CONSTRAINTS_FORMAT: Self = Self("constraints-format"); + pub const CONSTRAINTS_PLACEHOLDER: Self = Self("constraints-placeholder"); + pub const PARSE: Self = Self("parse"); + pub const FORMAT: Self = Self("format"); + + /// Every rule this build can emit, in the order the README lists them. + pub const ALL: [Self; 9] = [ + Self::MISSING_DOC, + Self::MISSING_TAG, + Self::FORBIDDEN_TAG, + Self::MODULE_NAME, + Self::MISSING_CONSTRAINTS, + Self::CONSTRAINTS_FORMAT, + Self::CONSTRAINTS_PLACEHOLDER, + Self::PARSE, + Self::FORMAT, + ]; + + #[must_use] + pub const fn as_str(self) -> &'static str { + self.0 + } +} + +impl fmt::Display for RuleId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.0) + } +} + +/// A 1-based source position; column 1 is the first byte of a line. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position { + pub line: usize, + pub column: usize, +} + +impl Position { + /// Converts a tree-sitter 0-based row/column pair. + #[must_use] + pub const fn from_zero_based(row: usize, column: usize) -> Self { + Self { + line: row + 1, + column: column + 1, + } + } + + /// The start of a file, used by whole-file findings. + #[must_use] + pub const fn file_start() -> Self { + Self { line: 1, column: 1 } + } +} + +/// One lint violation at one source position. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Finding { + pub path: PathBuf, + pub position: Position, + pub rule: RuleId, + pub message: String, +} + +impl Finding { + pub fn new( + path: impl Into, + position: Position, + rule: RuleId, + message: String, + ) -> Self { + Self { + path: path.into(), + position, + rule, + message, + } + } + + /// Sorts findings by file, then position, then rule, so output is stable across runs. + #[must_use] + pub fn sort_key(&self) -> (&Path, Position, RuleId) { + (self.path.as_path(), self.position, self.rule) + } +} + +impl fmt::Display for Finding { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{}:{}:{}: {}: {}", + self.path.display(), + self.position.line, + self.position.column, + self.rule, + self.message + ) + } +} + +#[cfg(test)] +mod tests { + use super::{Finding, Position, RuleId}; + + #[test] + fn finding_renders_as_path_line_col_rule_message() { + let finding = Finding::new( + "contracts/src/access/Ownable.compact", + Position::from_zero_based(41, 0), + RuleId::MISSING_DOC, + "circuit `assertOnlyOwner` has no doc comment".to_owned(), + ); + + assert_eq!( + finding.to_string(), + "contracts/src/access/Ownable.compact:42:1: missing-doc: circuit `assertOnlyOwner` has no doc comment" + ); + } + + #[test] + fn rule_ids_are_unique() { + let mut ids: Vec<&str> = RuleId::ALL.iter().map(|rule| rule.as_str()).collect(); + ids.sort_unstable(); + let count = ids.len(); + ids.dedup(); + assert_eq!(ids.len(), count); + } +} diff --git a/crates/compact-lint/src/rules.rs b/crates/compact-lint/src/rules.rs new file mode 100644 index 0000000..9bc0bf7 --- /dev/null +++ b/crates/compact-lint/src/rules.rs @@ -0,0 +1,415 @@ +//! The doc-comment rules, run over one parsed file at a time. + +use std::path::{Path, PathBuf}; + +use thiserror::Error; +use tree_sitter::{Node, Parser}; + +use crate::config::{Config, DocsPolicy}; +use crate::doc::Tag; +use crate::model::{DeclKind, Declaration, declarations}; +use crate::report::{Finding, Position, RuleId}; + +#[derive(Debug, Error)] +pub enum LintError { + #[error("the bundled Compact grammar does not match the linked tree-sitter ABI")] + Language(#[source] tree_sitter::LanguageError), + #[error("tree-sitter returned no parse tree for {0}")] + NoTree(PathBuf), +} + +/// A reusable parser plus the rules that run over its trees. +pub struct Linter { + parser: Parser, +} + +impl Linter { + /// # Errors + /// Returns an error when the bundled grammar does not match the linked ABI. + pub fn new() -> Result { + let mut parser = Parser::new(); + parser + .set_language(&compact_tree_sitter::LANGUAGE.into()) + .map_err(LintError::Language)?; + Ok(Self { parser }) + } + + /// Checks one file. `display` is the path printed in findings. + /// + /// A tree holding an `ERROR` or `MISSING` node yields a single `parse` finding and + /// no doc findings, because declarations around the defect are not trustworthy. + /// # Errors + /// Returns an error when tree-sitter produces no tree for `source`. + pub fn check( + &mut self, + display: &Path, + source: &str, + config: &Config, + strict: bool, + ) -> Result, LintError> { + let tree = self + .parser + .parse(source, None) + .ok_or_else(|| LintError::NoTree(display.to_owned()))?; + let root = tree.root_node(); + + if let Some(defect) = first_defect(root) { + return Ok(vec![defect.finding(display)]); + } + + let mut findings = Vec::new(); + for declaration in declarations(root, source) { + check_declaration(&declaration, display, config, strict, &mut findings); + } + findings.sort_by(|left, right| left.sort_key().cmp(&right.sort_key())); + Ok(findings) + } +} + +struct Defect { + position: Position, + kind: String, + missing: bool, +} + +impl Defect { + fn finding(&self, display: &Path) -> Finding { + let message = if self.missing { + format!("parse error: missing {}", self.kind) + } else { + format!("parse error: unexpected {}", self.kind) + }; + Finding::new(display, self.position, RuleId::PARSE, message) + } +} + +/// The first `ERROR` or `MISSING` node in document order. +fn first_defect(node: Node<'_>) -> Option { + if node.is_error() || node.is_missing() { + let start = node.start_position(); + return Some(Defect { + position: Position::from_zero_based(start.row, start.column), + kind: node.kind().to_owned(), + missing: node.is_missing(), + }); + } + + if !node.has_error() { + return None; + } + + let mut cursor = node.walk(); + node.children(&mut cursor).find_map(first_defect) +} + +fn check_declaration( + declaration: &Declaration, + display: &Path, + config: &Config, + strict: bool, + findings: &mut Vec, +) { + let kind_config = config.kinds.get(declaration.kind); + let requires_docs = match kind_config.docs { + DocsPolicy::All => true, + DocsPolicy::Exported => declaration.exported, + DocsPolicy::None => false, + }; + + let Some(attached) = declaration.doc.as_ref() else { + if requires_docs { + findings.push(Finding::new( + display, + declaration.position, + RuleId::MISSING_DOC, + format!("{} has no doc comment", subject(declaration)), + )); + } + return; + }; + + // A forbidden tag is a finding wherever it appears, documented or not. + for forbidden in &config.tags.forbid { + for occurrence in attached.comment.tags() { + if &occurrence.tag == forbidden { + findings.push(Finding::new( + display, + attached.tag_position(occurrence), + RuleId::FORBIDDEN_TAG, + format!("forbidden tag {forbidden}"), + )); + } + } + } + + if declaration.kind == DeclKind::Module { + check_module_name(declaration, display, findings); + } + + if !requires_docs { + return; + } + + for required in &kind_config.tags { + if !attached.comment.has(required) { + findings.push(Finding::new( + display, + declaration.position, + RuleId::MISSING_TAG, + format!("{} doc comment has no {required}", subject(declaration)), + )); + } + } + + check_constraints(declaration, display, config, strict, findings); +} + +/// `@module ` must name the module it documents. +fn check_module_name(declaration: &Declaration, display: &Path, findings: &mut Vec) { + let tag = Tag::new("@module"); + let (Some(attached), Some(name)) = (declaration.doc.as_ref(), declaration.name.as_deref()) + else { + return; + }; + let Some(occurrence) = attached.comment.first(&tag) else { + return; + }; + + let documented = module_name(&occurrence.value); + if documented != name { + findings.push(Finding::new( + display, + attached.tag_position(occurrence), + RuleId::MODULE_NAME, + format!("@module names `{documented}`, but the module is `{name}`"), + )); + } +} + +/// The documented name is the first word, without generic arguments or trailing punctuation. +fn module_name(value: &str) -> &str { + let first = value.split_whitespace().next().unwrap_or_default(); + let without_generics = first.split('<').next().unwrap_or(first); + without_generics.trim_end_matches(['.', ',', ':', ';']) +} + +/// Exported non-pure circuits carry the constraints annotation. +fn check_constraints( + declaration: &Declaration, + display: &Path, + config: &Config, + strict: bool, + findings: &mut Vec, +) { + if declaration.kind != DeclKind::Circuit || !declaration.exported || declaration.pure { + return; + } + let Some(attached) = declaration.doc.as_ref() else { + return; + }; + let tag = &config.constraints.tag; + + let Some(occurrence) = attached.comment.first(tag) else { + findings.push(Finding::new( + display, + declaration.position, + RuleId::MISSING_CONSTRAINTS, + format!("{} doc comment has no {tag}", subject(declaration)), + )); + return; + }; + + // The annotation is one line; anything below it is prose, not part of the value. + let value = occurrence.value.lines().next().unwrap_or_default().trim(); + let position = attached.tag_position(occurrence); + + let Some(placeholders) = parse_constraints(value) else { + findings.push(Finding::new( + display, + position, + RuleId::CONSTRAINTS_FORMAT, + format!("{tag} value `{value}` is not `k=, rows=`"), + )); + return; + }; + + if strict && placeholders { + findings.push(Finding::new( + display, + position, + RuleId::CONSTRAINTS_PLACEHOLDER, + format!("{tag} value `{value}` still holds a placeholder"), + )); + } +} + +/// Matches `k=,*rows=`; returns whether either field is a `?` placeholder. +fn parse_constraints(value: &str) -> Option { + let rest = value.strip_prefix("k=")?; + let (k, rest) = rest.split_once(',')?; + let rows = rest.trim_start().strip_prefix("rows=")?; + + let k_placeholder = is_placeholder(k)?; + let rows_placeholder = is_placeholder(rows)?; + Some(k_placeholder || rows_placeholder) +} + +/// A constraints field is either a decimal count or a `?` placeholder. +fn is_placeholder(field: &str) -> Option { + match field { + "?" => Some(true), + digits if !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) => { + Some(false) + } + _ => None, + } +} + +/// The kind plus the backquoted name, or a bare kind when the declaration has no name. +fn subject(declaration: &Declaration) -> String { + match declaration.name.as_deref() { + Some(name) => format!("{} `{name}`", declaration.kind), + None => declaration.kind.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::{Linter, module_name, parse_constraints}; + use crate::config::{Config, DocsPolicy}; + use crate::doc::Tag; + use std::path::Path; + + fn findings(source: &str, config: &Config, strict: bool) -> Vec { + Linter::new() + .expect("the bundled grammar loads") + .check(Path::new("a.compact"), source, config, strict) + .expect("the source produced a tree") + .iter() + .map(ToString::to_string) + .collect() + } + + fn circuit_config() -> Config { + let mut config = Config::default(); + config.kinds.circuit.tags = vec![Tag::new("@description")]; + config + } + + #[test] + fn a_non_exported_circuit_inside_an_exported_module_needs_no_docs() { + let source = "/** @description M. */\nexport module M {\n circuit helper(): [] { }\n}\n"; + + assert!(findings(source, &Config::default(), false).is_empty()); + } + + #[test] + fn an_exported_module_without_a_doc_comment_is_reported() { + let source = "export module M {\n}\n"; + + assert_eq!( + findings(source, &Config::default(), false), + ["a.compact:1:1: missing-doc: module `M` has no doc comment"] + ); + } + + #[test] + fn a_line_comment_between_the_doc_and_the_declaration_breaks_attachment() { + let source = "/** @description M. */\n// a note\nexport module M {\n}\n"; + + assert_eq!( + findings(source, &Config::default(), false), + ["a.compact:3:1: missing-doc: module `M` has no doc comment"] + ); + } + + #[test] + fn a_pure_exported_circuit_needs_no_constraints() { + let source = "/** @description Adds. */\nexport pure circuit add(a: Uint<8>): Uint<8> { return a; }\n"; + + assert!(findings(source, &circuit_config(), false).is_empty()); + } + + #[test] + fn an_exported_circuit_without_constraints_is_reported() { + let source = + "/** @description Bumps. */\nexport circuit bump(): [] { count.increment(1); }\n"; + + assert_eq!( + findings(source, &circuit_config(), false), + ["a.compact:2:1: missing-constraints: circuit `bump` doc comment has no @constraints"] + ); + } + + #[test] + fn a_placeholder_is_a_finding_only_under_strict() { + let source = "/**\n * @description Bumps.\n * @constraints k=?, rows=?\n */\nexport circuit bump(): [] { }\n"; + let config = circuit_config(); + + assert!(findings(source, &config, false).is_empty()); + assert_eq!( + findings(source, &config, true), + [ + "a.compact:3:4: constraints-placeholder: @constraints value `k=?, rows=?` still holds a placeholder" + ] + ); + } + + #[test] + fn a_forbidden_tag_is_reported_even_where_docs_are_optional() { + let source = "/**\n * @return nothing\n */\ncircuit hidden(): [] { }\n"; + let mut config = Config::default(); + config.tags.forbid = vec![Tag::new("@return")]; + + assert_eq!( + findings(source, &config, false), + ["a.compact:2:4: forbidden-tag: forbidden tag @return"] + ); + } + + #[test] + fn a_witness_under_docs_all_is_checked_without_export() { + let source = "witness wit_secret(): Bytes<32>;\n"; + let mut config = Config::default(); + config.kinds.witness.docs = DocsPolicy::All; + + assert_eq!( + findings(source, &config, false), + ["a.compact:1:1: missing-doc: witness `wit_secret` has no doc comment"] + ); + } + + #[test] + fn a_parse_defect_suppresses_the_doc_rules() { + let source = "export module M {\nexport circuit\n"; + + let reported = findings(source, &Config::default(), false); + assert_eq!(reported.len(), 1, "{reported:?}"); + assert!(reported[0].contains("parse error"), "{reported:?}"); + } + + #[test] + fn the_constraints_tag_is_configurable() { + let source = "/**\n * @description Bumps.\n * @circuitInfo k=6, rows=28\n */\nexport circuit bump(): [] { }\n"; + let mut config = circuit_config(); + config.constraints.tag = Tag::new("@circuitInfo"); + + assert!(findings(source, &config, false).is_empty()); + } + + #[test] + fn constraints_accept_flexible_spacing_after_the_comma() { + assert_eq!(parse_constraints("k=10, rows=626"), Some(false)); + assert_eq!(parse_constraints("k=10,rows=626"), Some(false)); + assert_eq!(parse_constraints("k=?, rows=626"), Some(true)); + assert_eq!(parse_constraints("k=10, rows=626 (approx)"), None); + assert_eq!(parse_constraints("rows=626, k=10"), None); + } + + #[test] + fn a_documented_module_name_drops_generics_and_trailing_punctuation() { + assert_eq!(module_name("Signer"), "Signer"); + assert_eq!(module_name("Utils."), "Utils"); + assert_eq!(module_name("ShieldedToken (archived)"), "ShieldedToken"); + } +} diff --git a/crates/compact-lint/tests/check.rs b/crates/compact-lint/tests/check.rs new file mode 100644 index 0000000..452c09d --- /dev/null +++ b/crates/compact-lint/tests/check.rs @@ -0,0 +1,172 @@ +//! `compact-lint check` end to end, one fixture case per rule. +//! +//! Every case runs with `--no-format`, because CI has no `compact` binary. + +use std::path::PathBuf; +use std::process::Command; + +use assert_cmd::prelude::*; + +/// Exit code for a run that produced findings. +const EXIT_FINDINGS: i32 = 1; + +/// Exit code for a usage, config, IO or parser error. +const EXIT_ERROR: i32 = 2; + +fn case(name: &str) -> PathBuf { + // The child reports its working directory resolved, so the expectation must be too. + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(name) + .canonicalize() + .expect("the fixture case exists") +} + +fn binary() -> Command { + Command::cargo_bin("compact-lint").expect("the binary is built alongside this test") +} + +fn expected(name: &str) -> String { + std::fs::read_to_string(case(name).join("expected.txt")).expect("the case has an expectation") +} + +/// Runs `check` inside a case directory and returns its exit code and stdout. +fn run(name: &str, extra: &[&str]) -> (i32, String) { + let output = binary() + .current_dir(case(name)) + .args(["check", "--no-format"]) + .args(extra) + .output() + .expect("the binary ran"); + + ( + output.status.code().expect("the binary was not signalled"), + String::from_utf8(output.stdout).expect("the report is UTF-8"), + ) +} + +fn assert_case(name: &str, extra: &[&str], code: i32) { + let (actual_code, stdout) = run(name, extra); + + assert_eq!(stdout, expected(name), "stdout for case {name}"); + assert_eq!(actual_code, code, "exit code for case {name}"); +} + +#[test] +fn a_fully_documented_module_reports_nothing() { + assert_case("clean", &[], 0); +} + +#[test] +fn undocumented_declarations_and_detached_docs_report_missing_doc() { + assert_case("missing-doc", &[], EXIT_FINDINGS); +} + +#[test] +fn a_doc_comment_without_a_required_tag_reports_missing_tag() { + assert_case("missing-tag", &[], EXIT_FINDINGS); +} + +#[test] +fn a_forbidden_tag_reports_wherever_it_appears() { + assert_case("forbidden-tag", &[], EXIT_FINDINGS); +} + +#[test] +fn a_module_tag_naming_another_module_reports_module_name() { + assert_case("module-name", &[], EXIT_FINDINGS); +} + +#[test] +fn an_unannotated_exported_circuit_reports_missing_constraints() { + assert_case("missing-constraints", &[], EXIT_FINDINGS); +} + +#[test] +fn a_malformed_constraints_value_reports_constraints_format() { + assert_case("constraints-format", &[], EXIT_FINDINGS); +} + +#[test] +fn a_placeholder_reports_only_under_strict() { + let (code, stdout) = run("constraints-placeholder", &[]); + assert_eq!(stdout, ""); + assert_eq!(code, 0); + + assert_case("constraints-placeholder", &["--strict"], EXIT_FINDINGS); +} + +#[test] +fn a_broken_file_reports_parse_instead_of_crashing() { + assert_case("parse", &[], EXIT_FINDINGS); +} + +#[test] +fn an_explicit_path_replaces_the_include_globs() { + let output = binary() + .current_dir(case("missing-doc")) + .args(["check", "--no-format", "Gaps.compact"]) + .output() + .expect("the binary ran"); + + assert_eq!( + String::from_utf8(output.stdout).expect("the report is UTF-8"), + expected("missing-doc") + ); +} + +#[test] +fn an_explicit_config_anchors_discovery_at_the_working_directory() { + let config = case("clean").join("compact-lint.toml"); + let output = binary() + .current_dir(case("missing-doc")) + .args(["check", "--no-format", "--config"]) + .arg(&config) + .output() + .expect("the binary ran"); + + assert_eq!( + String::from_utf8(output.stdout).expect("the report is UTF-8"), + expected("missing-doc") + ); +} + +#[test] +fn a_missing_compact_binary_exits_two_and_names_the_escape_hatch() { + let output = binary() + .current_dir(case("clean")) + .args(["check", "--compact-bin", "compact-lint-no-such-binary"]) + .output() + .expect("the binary ran"); + + let stderr = String::from_utf8(output.stderr).expect("the error is UTF-8"); + assert_eq!(output.status.code(), Some(EXIT_ERROR), "{stderr}"); + assert!(stderr.contains("--no-format"), "{stderr}"); +} + +#[test] +fn an_unreadable_config_exits_two() { + let output = binary() + .current_dir(case("clean")) + .args(["check", "--no-format", "--config", "compact-lint.toml.nope"]) + .output() + .expect("the binary ran"); + + assert_eq!(output.status.code(), Some(EXIT_ERROR)); +} + +#[test] +fn the_summary_goes_to_stderr_and_the_findings_to_stdout() { + let output = binary() + .current_dir(case("module-name")) + .args(["check", "--no-format"]) + .output() + .expect("the binary ran"); + + let stderr = String::from_utf8(output.stderr).expect("the summary is UTF-8"); + assert_eq!(stderr.trim_end(), "1 findings in 1 files"); + assert!( + String::from_utf8_lossy(&output.stdout).contains("module-name"), + "the rule id belongs on stdout" + ); +} diff --git a/crates/compact-lint/tests/fixtures/clean/Clean.compact b/crates/compact-lint/tests/fixtures/clean/Clean.compact new file mode 100644 index 0000000..030f785 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/clean/Clean.compact @@ -0,0 +1,56 @@ +pragma language_version >= 0.23.0; + +/** + * @module Clean + * @description A module that satisfies every rule. + */ +export module Clean { + import CompactStandardLibrary; + + /** + * @description Whether the module is initialized. + */ + export ledger _isInitialized: Boolean; + + /** + * @description The caller's secret key. + */ + witness wit_Secret(): Bytes<32>; + + /** + * @description A single counter reading. + */ + export struct Reading { + value: Uint<64> + } + + /** + * @description The directions a counter moves in. + */ + export enum Direction { + Up, + Down + } + + constructor() { + _isInitialized = false; + } + + /** + * @description Doubles its argument. + * @param {Uint<8>} value - The value to double. + * @returns {Uint<16>} The doubled value. + */ + export pure circuit double(value: Uint<8>): Uint<16> { + return value as Uint<16> * 2; + } + + /** + * @description Marks the module initialized. + * @constraints k=10, rows=626 + * @returns {[]} Empty tuple. + */ + export circuit initialize(): [] { + _isInitialized = true; + } +} diff --git a/crates/compact-lint/tests/fixtures/clean/compact-lint.toml b/crates/compact-lint/tests/fixtures/clean/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/clean/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/clean/expected.txt b/crates/compact-lint/tests/fixtures/clean/expected.txt new file mode 100644 index 0000000..e69de29 diff --git a/crates/compact-lint/tests/fixtures/constraints-format/Malformed.compact b/crates/compact-lint/tests/fixtures/constraints-format/Malformed.compact new file mode 100644 index 0000000..9e2365a --- /dev/null +++ b/crates/compact-lint/tests/fixtures/constraints-format/Malformed.compact @@ -0,0 +1,17 @@ +/** + * @module Malformed + * @description A module whose annotations do not parse. + */ +export module Malformed { + /** + * @description The comma is missing. + * @constraints k=10 rows=626 + */ + export circuit first(): [] { } + + /** + * @description The fields are the wrong way round. + * @constraints rows=626, k=10 + */ + export circuit second(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/constraints-format/compact-lint.toml b/crates/compact-lint/tests/fixtures/constraints-format/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/constraints-format/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/constraints-format/expected.txt b/crates/compact-lint/tests/fixtures/constraints-format/expected.txt new file mode 100644 index 0000000..a1b1ac8 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/constraints-format/expected.txt @@ -0,0 +1,2 @@ +Malformed.compact:8:6: constraints-format: @constraints value `k=10 rows=626` is not `k=, rows=` +Malformed.compact:14:6: constraints-format: @constraints value `rows=626, k=10` is not `k=, rows=` diff --git a/crates/compact-lint/tests/fixtures/constraints-placeholder/Unfilled.compact b/crates/compact-lint/tests/fixtures/constraints-placeholder/Unfilled.compact new file mode 100644 index 0000000..4b12883 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/constraints-placeholder/Unfilled.compact @@ -0,0 +1,11 @@ +/** + * @module Unfilled + * @description A module whose annotation is still a placeholder. + */ +export module Unfilled { + /** + * @description Not measured yet. + * @constraints k=?, rows=? + */ + export circuit run(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/constraints-placeholder/compact-lint.toml b/crates/compact-lint/tests/fixtures/constraints-placeholder/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/constraints-placeholder/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/constraints-placeholder/expected.txt b/crates/compact-lint/tests/fixtures/constraints-placeholder/expected.txt new file mode 100644 index 0000000..ba3214e --- /dev/null +++ b/crates/compact-lint/tests/fixtures/constraints-placeholder/expected.txt @@ -0,0 +1 @@ +Unfilled.compact:8:6: constraints-placeholder: @constraints value `k=?, rows=?` still holds a placeholder diff --git a/crates/compact-lint/tests/fixtures/forbidden-tag/Returns.compact b/crates/compact-lint/tests/fixtures/forbidden-tag/Returns.compact new file mode 100644 index 0000000..61836af --- /dev/null +++ b/crates/compact-lint/tests/fixtures/forbidden-tag/Returns.compact @@ -0,0 +1,18 @@ +/** + * @module Returns + * @description A module that still uses the old return tag. + */ +export module Returns { + /** + * @description Does nothing. + * @constraints k=6, rows=28 + * @return {[]} - Empty tuple. + */ + export circuit run(): [] { } + + /** + * @description Not exported, so its docs are optional, but the tag is not. + * @return {[]} - Empty tuple. + */ + circuit helper(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/forbidden-tag/compact-lint.toml b/crates/compact-lint/tests/fixtures/forbidden-tag/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/forbidden-tag/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/forbidden-tag/expected.txt b/crates/compact-lint/tests/fixtures/forbidden-tag/expected.txt new file mode 100644 index 0000000..0edaa79 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/forbidden-tag/expected.txt @@ -0,0 +1,2 @@ +Returns.compact:9:6: forbidden-tag: forbidden tag @return +Returns.compact:15:6: forbidden-tag: forbidden tag @return diff --git a/crates/compact-lint/tests/fixtures/missing-constraints/Unmeasured.compact b/crates/compact-lint/tests/fixtures/missing-constraints/Unmeasured.compact new file mode 100644 index 0000000..f8e0a62 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-constraints/Unmeasured.compact @@ -0,0 +1,17 @@ +/** + * @module Unmeasured + * @description A module whose exported circuit is unmeasured. + */ +export module Unmeasured { + /** + * @description Writes to the ledger without an annotation. + */ + export circuit run(): [] { } + + /** + * @description Pure circuits are never annotated. + */ + export pure circuit double(value: Uint<8>): Uint<16> { + return value as Uint<16> * 2; + } +} diff --git a/crates/compact-lint/tests/fixtures/missing-constraints/compact-lint.toml b/crates/compact-lint/tests/fixtures/missing-constraints/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-constraints/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/missing-constraints/expected.txt b/crates/compact-lint/tests/fixtures/missing-constraints/expected.txt new file mode 100644 index 0000000..18ec688 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-constraints/expected.txt @@ -0,0 +1 @@ +Unmeasured.compact:9:3: missing-constraints: circuit `run` doc comment has no @constraints diff --git a/crates/compact-lint/tests/fixtures/missing-doc/Gaps.compact b/crates/compact-lint/tests/fixtures/missing-doc/Gaps.compact new file mode 100644 index 0000000..13a4d3e --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-doc/Gaps.compact @@ -0,0 +1,20 @@ +/** + * @module Gaps + * @description A module whose members are undocumented. + */ +export module Gaps { + export ledger _owner: Bytes<32>; + + export enum Kind { + One + } + + /** + * @description Detached by the line comment below it. + * @constraints k=6, rows=28 + */ + // The comment that breaks the attachment. + export circuit run(): [] { + _owner = default>; + } +} diff --git a/crates/compact-lint/tests/fixtures/missing-doc/compact-lint.toml b/crates/compact-lint/tests/fixtures/missing-doc/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-doc/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/missing-doc/expected.txt b/crates/compact-lint/tests/fixtures/missing-doc/expected.txt new file mode 100644 index 0000000..b3649c7 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-doc/expected.txt @@ -0,0 +1,3 @@ +Gaps.compact:6:3: missing-doc: ledger `_owner` has no doc comment +Gaps.compact:8:3: missing-doc: enum `Kind` has no doc comment +Gaps.compact:17:3: missing-doc: circuit `run` has no doc comment diff --git a/crates/compact-lint/tests/fixtures/missing-tag/Untagged.compact b/crates/compact-lint/tests/fixtures/missing-tag/Untagged.compact new file mode 100644 index 0000000..98d8116 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-tag/Untagged.compact @@ -0,0 +1,10 @@ +/** + * @module Untagged + */ +export module Untagged { + /** + * The module has no @description, and neither has this circuit. + * @constraints k=6, rows=28 + */ + export circuit run(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/missing-tag/compact-lint.toml b/crates/compact-lint/tests/fixtures/missing-tag/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-tag/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/missing-tag/expected.txt b/crates/compact-lint/tests/fixtures/missing-tag/expected.txt new file mode 100644 index 0000000..9fb4a3b --- /dev/null +++ b/crates/compact-lint/tests/fixtures/missing-tag/expected.txt @@ -0,0 +1,2 @@ +Untagged.compact:4:1: missing-tag: module `Untagged` doc comment has no @description +Untagged.compact:9:3: missing-tag: circuit `run` doc comment has no @description diff --git a/crates/compact-lint/tests/fixtures/module-name/Renamed.compact b/crates/compact-lint/tests/fixtures/module-name/Renamed.compact new file mode 100644 index 0000000..8ea00b6 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/module-name/Renamed.compact @@ -0,0 +1,6 @@ +/** + * @module Stale + * @description The tag still names the module's old name. + */ +export module Renamed { +} diff --git a/crates/compact-lint/tests/fixtures/module-name/compact-lint.toml b/crates/compact-lint/tests/fixtures/module-name/compact-lint.toml new file mode 100644 index 0000000..0813e24 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/module-name/compact-lint.toml @@ -0,0 +1,27 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/module-name/expected.txt b/crates/compact-lint/tests/fixtures/module-name/expected.txt new file mode 100644 index 0000000..6d31a1c --- /dev/null +++ b/crates/compact-lint/tests/fixtures/module-name/expected.txt @@ -0,0 +1 @@ +Renamed.compact:2:4: module-name: @module names `Stale`, but the module is `Renamed` diff --git a/crates/compact-lint/tests/fixtures/parse/Broken.compact b/crates/compact-lint/tests/fixtures/parse/Broken.compact new file mode 100644 index 0000000..c21593b --- /dev/null +++ b/crates/compact-lint/tests/fixtures/parse/Broken.compact @@ -0,0 +1,3 @@ +export module Broken { + export circuit +} diff --git a/crates/compact-lint/tests/fixtures/parse/compact-lint.toml b/crates/compact-lint/tests/fixtures/parse/compact-lint.toml new file mode 100644 index 0000000..56ee774 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/parse/compact-lint.toml @@ -0,0 +1,2 @@ +version = 1 +include = ["**/*.compact"] diff --git a/crates/compact-lint/tests/fixtures/parse/expected.txt b/crates/compact-lint/tests/fixtures/parse/expected.txt new file mode 100644 index 0000000..b50d9d7 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/parse/expected.txt @@ -0,0 +1 @@ +Broken.compact:2:3: parse: parse error: unexpected ERROR From a8ac6560535ced886a40b2acb80c13ea2dd5e0e8 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 10:15:01 +0200 Subject: [PATCH 04/14] feat(compact-lint): add the fix command Adds fix, which rewrites doc comments so a following check is clean for every rule it covers and a second fix is a no-op. Detection now yields a structured Issue that check renders as a Finding and fix renders as an Edit, so the two subcommands cannot disagree. * fix: doc skeletons above undocumented declarations, missing tags in config order (a missing @description adopts prose that opens the body), the constraints placeholder after the description block, tag renames from the new [tags] rename map, and @module name repairs. Edits on one comment merge into a single replacement; files are rewritten atomically with their line ending and permissions kept. * target: config and path resolution shared by check and fix. * config: [fix] placeholder and [tags] rename, both validated. * tests: one before/after fixture per rule through the binary, run on a temp copy and followed by check and a second fix. --- Cargo.lock | 84 +++ Cargo.toml | 1 + crates/compact-lint/Cargo.toml | 1 + crates/compact-lint/README.md | 66 +- .../examples/compact-contracts.toml | 1 + crates/compact-lint/src/check.rs | 53 +- crates/compact-lint/src/config.rs | 67 +- crates/compact-lint/src/edit.rs | 570 ++++++++++++++++++ crates/compact-lint/src/fix.rs | 377 ++++++++++++ crates/compact-lint/src/lib.rs | 6 +- crates/compact-lint/src/main.rs | 74 ++- crates/compact-lint/src/model.rs | 7 + crates/compact-lint/src/rules.rs | 306 +++++++--- crates/compact-lint/src/target.rs | 65 ++ crates/compact-lint/tests/fix.rs | 223 +++++++ .../fix-constraints/after/Unmeasured.compact | 37 ++ .../fix-constraints/before/Unmeasured.compact | 31 + .../fix-constraints/compact-lint.toml | 13 + .../fixtures/fix-constraints/expected.txt | 3 + .../fixtures/fix-crlf/after/Windows.compact | 19 + .../fixtures/fix-crlf/before/Windows.compact | 13 + .../tests/fixtures/fix-crlf/compact-lint.toml | 18 + .../tests/fixtures/fix-crlf/expected.txt | 3 + .../fix-missing-tag/after/Untagged.compact | 25 + .../fix-missing-tag/before/Untagged.compact | 18 + .../fix-missing-tag/compact-lint.toml | 14 + .../fixtures/fix-missing-tag/expected.txt | 5 + .../fix-module-name/after/Renamed.compact | 12 + .../fix-module-name/before/Renamed.compact | 12 + .../fix-module-name/compact-lint.toml | 8 + .../fixtures/fix-module-name/expected.txt | 2 + .../fixtures/fix-rename/after/Returns.compact | 19 + .../fix-rename/before/Returns.compact | 19 + .../fixtures/fix-rename/compact-lint.toml | 15 + .../tests/fixtures/fix-rename/expected.txt | 2 + .../fixtures/fix-skeleton/after/Bare.compact | 59 ++ .../fixtures/fix-skeleton/before/Bare.compact | 32 + .../fixtures/fix-skeleton/compact-lint.toml | 31 + .../tests/fixtures/fix-skeleton/expected.txt | 8 + .../fix-unfixable/after/Malformed.compact | 12 + .../fix-unfixable/before/Malformed.compact | 12 + .../fixtures/fix-unfixable/compact-lint.toml | 15 + .../tests/fixtures/fix-unfixable/expected.txt | 0 43 files changed, 2224 insertions(+), 134 deletions(-) create mode 100644 crates/compact-lint/src/edit.rs create mode 100644 crates/compact-lint/src/fix.rs create mode 100644 crates/compact-lint/src/target.rs create mode 100644 crates/compact-lint/tests/fix.rs create mode 100644 crates/compact-lint/tests/fixtures/fix-constraints/after/Unmeasured.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-constraints/before/Unmeasured.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-constraints/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-constraints/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/fix-crlf/after/Windows.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-crlf/before/Windows.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-crlf/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-crlf/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/fix-missing-tag/after/Untagged.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-missing-tag/before/Untagged.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-missing-tag/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-missing-tag/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/fix-module-name/after/Renamed.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-module-name/before/Renamed.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-module-name/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-module-name/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/fix-rename/after/Returns.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-rename/before/Returns.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-rename/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-rename/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/fix-skeleton/after/Bare.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-skeleton/before/Bare.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-skeleton/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-skeleton/expected.txt create mode 100644 crates/compact-lint/tests/fixtures/fix-unfixable/after/Malformed.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-unfixable/before/Malformed.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-unfixable/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-unfixable/expected.txt diff --git a/Cargo.lock b/Cargo.lock index 80f353a..7246e94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + [[package]] name = "bstr" version = "1.13.1" @@ -103,6 +109,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.6.7" @@ -159,6 +171,7 @@ dependencies = [ "compact-tree-sitter", "globset", "serde", + "tempfile", "thiserror", "toml", "tree-sitter", @@ -186,12 +199,39 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "find-msvc-tools" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "globset" version = "0.4.20" @@ -245,6 +285,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.34" @@ -257,6 +303,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -308,6 +360,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "regex" version = "1.13.1" @@ -337,6 +395,19 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "same-file" version = "1.0.6" @@ -428,6 +499,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "termtree" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 5e096be..753653f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ tree-sitter-language = "0.1" walkdir = "2.5" assert_cmd = "2.2" +tempfile = "3.27" diff --git a/crates/compact-lint/Cargo.toml b/crates/compact-lint/Cargo.toml index 0983475..384ce97 100644 --- a/crates/compact-lint/Cargo.toml +++ b/crates/compact-lint/Cargo.toml @@ -31,3 +31,4 @@ walkdir.workspace = true [dev-dependencies] assert_cmd.workspace = true +tempfile.workspace = true diff --git a/crates/compact-lint/README.md b/crates/compact-lint/README.md index 28bd628..0c4f3b2 100644 --- a/crates/compact-lint/README.md +++ b/crates/compact-lint/README.md @@ -2,12 +2,14 @@ Doc-comment linter for Compact sources. It parses `.compact` files with [`compact-tree-sitter`](../compact-tree-sitter), matches every declaration against the -per-kind template in `compact-lint.toml`, and prints one finding per line. +per-kind template in `compact-lint.toml`, and prints one finding per line. `fix` +rewrites the comments the repairable rules report. ## Usage ```sh compact-lint check [PATHS]... [--config ] [--strict] [--no-format] [--compact-bin ] +compact-lint fix [PATHS]... [--config ] [--dry-run] ``` - `PATHS` — files or directories, walked recursively for `.compact`. Skipped while @@ -32,6 +34,52 @@ summary goes to stderr, where `M` is the number of files checked. - `1` — findings. - `2` — usage, config, IO or parser error. +## Fix + +`fix` rewrites `.compact` files so a following `check` is clean for every rule it +covers. It runs the same detection pass as `check`, so the two never disagree. + +```sh +compact-lint fix [PATHS]... [--config ] [--dry-run] +``` + +- `PATHS` and `--config` resolve exactly as they do for `check`. +- `--dry-run` — report the edits and write nothing. Exit `1` when there are edits, so + CI gates on it the way it gates on `cargo fmt --check`. + +Edits go to stdout, one per line, in `path:line:col: fix: ` form. The +`N edits in M files` summary goes to stderr, where `M` is the number of files changed. + +Exit codes: + +- `0` — the files were written, including when there was nothing to do. +- `1` — `--dry-run` only: edits would be made. +- `2` — usage, config, IO or parser error. + +What each rule's fix does: + +| Rule | Fix | +| --- | --- | +| `missing-doc` | Inserts a doc skeleton above the declaration, indented to it. Body lines come from `kinds..tags` in config order; `@module` takes the declaration's name, every other tag takes `fix.placeholder`. Exported non-pure circuits also get the constraints line. A kind with neither gets a single placeholder line. | +| `missing-tag` | Inserts each missing tag after the block of the last tag that precedes it in `kinds..tags`, else at the top of the body. A missing `@description` adopts untagged prose that opens the body instead of writing a placeholder. A `/** … */` one-liner expands to the multi-line form first. | +| `missing-constraints` | Inserts ` k=?, rows=?` after the `@description` block, with a blank ` *` line before it and, where prose follows, one after. Without a `@description` it opens the body. | +| `forbidden-tag` | Renames the tag in place, and only where `tags.rename` maps it. | +| `module-name` | Replaces the first word of the `@module` value with the module's name, keeping what follows. | + +Not fixed: + +- `constraints-format` — a human wrote a measurement the rule cannot parse; guessing at + it would lose the value. +- `constraints-placeholder` — filling `k=?` needs a real measurement. +- `parse` — the file does not parse, so no rewrite is trustworthy. +- `format` — that is `compact format`'s job. +- A forbidden tag with no `tags.rename` entry stays a `check` finding. + +Existing lines are never reordered or reflowed. Edits are computed against the original +text and applied from the highest offset down; several edits on one comment merge into a +single replacement of it. Files are rewritten through a sibling `.tmp` file and renamed +into place, keeping the file's line ending and its trailing newline, or lack of one. + ## Rules | Rule | Meaning | @@ -79,6 +127,10 @@ tag = "@constraints" [tags] forbid = ["@return"] +rename = { "@return" = "@returns" } + +[fix] +placeholder = "TODO" [kinds.module] docs = "exported" @@ -94,6 +146,8 @@ Defaults when no config file is found: | `exclude` | `[]` | | `constraints.tag` | `"@constraints"` | | `tags.forbid` | `[]` | +| `tags.rename` | `{}` | +| `fix.placeholder` | `"TODO"` | | `kinds..docs` | `"exported"` | | `kinds..tags` | `[]` | @@ -105,6 +159,10 @@ Defaults when no config file is found: `"all"` or `"none"` there. - `exclude` filters files found by walking a directory. A file named on the command line is always checked. +- `tags.rename` maps a forbidden tag to the tag `fix` writes in its place. Both spellings + are validated like every other tag. An unmapped forbidden tag is reported, never + rewritten. +- `fix.placeholder` is the text `fix` writes where it has no value of its own. `examples/compact-contracts.toml` is the config for OpenZeppelin/compact-contracts. @@ -123,6 +181,10 @@ Defaults when no config file is found: cargo test --workspace ``` -`tests/fixtures//` holds one case per rule plus a clean case: a +`tests/fixtures//` holds one `check` case per rule plus a clean case: a `compact-lint.toml`, the `.compact` sources, and `expected.txt`, the exact stdout with paths relative to the case directory. + +`tests/fixtures/fix-/` holds one `fix` case per rule: a `compact-lint.toml`, +`before/`, `after/` and `expected.txt`. The run copies `before/` into a temporary +directory, so the fixtures are never rewritten in place. diff --git a/crates/compact-lint/examples/compact-contracts.toml b/crates/compact-lint/examples/compact-contracts.toml index 402186d..d20c3a9 100644 --- a/crates/compact-lint/examples/compact-contracts.toml +++ b/crates/compact-lint/examples/compact-contracts.toml @@ -11,6 +11,7 @@ tag = "@constraints" [tags] forbid = ["@return"] +rename = { "@return" = "@returns" } [kinds.module] docs = "exported" diff --git a/crates/compact-lint/src/check.rs b/crates/compact-lint/src/check.rs index 2f2d2a8..7fbc195 100644 --- a/crates/compact-lint/src/check.rs +++ b/crates/compact-lint/src/check.rs @@ -1,22 +1,19 @@ -//! The `check` subcommand: resolve config, collect files, run the rules. +//! The `check` subcommand: resolve the target, run the rules, add the format pass. use std::ffi::OsString; use std::path::{Path, PathBuf}; use thiserror::Error; -use crate::config::{Config, ConfigError, discover as discover_config}; -use crate::discover::{self, DiscoverError}; use crate::format::{self, FormatError}; use crate::report::Finding; use crate::rules::{LintError, Linter}; +use crate::target::{self, TargetError}; #[derive(Debug, Error)] pub enum CheckError { #[error(transparent)] - Config(#[from] ConfigError), - #[error(transparent)] - Discover(#[from] DiscoverError), + Target(#[from] TargetError), #[error(transparent)] Lint(#[from] LintError), #[error(transparent)] @@ -59,59 +56,25 @@ impl Outcome { /// # Errors /// Returns an error when the config, the file walk, the parser or the formatter fails. pub fn run(options: &Options, cwd: &Path) -> Result { - let (config, config_path, base) = resolve_config(options, cwd)?; - let source = config_path.as_deref().unwrap_or(Path::new("")); - - let exclude = config.exclude_set(source)?; - let files = if options.paths.is_empty() { - let include = config.include_set(source)?; - discover::from_globs(&base, &include, &exclude)? - } else { - discover::from_paths(&options.paths, &base, &exclude)? - }; - let files: Vec = files.iter().map(|path| display_path(path, cwd)).collect(); + let target = target::resolve(&options.paths, options.config_path.as_deref(), cwd)?; let mut linter = Linter::new()?; let mut findings = Vec::new(); - for path in &files { + for path in &target.files { let text = std::fs::read_to_string(path).map_err(|source| CheckError::Read { path: path.clone(), source, })?; - findings.extend(linter.check(path, &text, &config, options.strict)?); + findings.extend(linter.check(path, &text, &target.config, options.strict)?); } if !options.no_format { - findings.extend(format::check(&options.compact_bin, &files)?); + findings.extend(format::check(&options.compact_bin, &target.files)?); } findings.sort_by(|left, right| left.sort_key().cmp(&right.sort_key())); Ok(Outcome { findings, - files_checked: files.len(), + files_checked: target.files.len(), }) } - -/// An explicit `--config` is a shareable preset, so glob discovery stays anchored at -/// `cwd`; a config found by walking upward anchors discovery at its own directory. -fn resolve_config( - options: &Options, - cwd: &Path, -) -> Result<(Config, Option, PathBuf), ConfigError> { - if let Some(path) = options.config_path.as_deref() { - return Ok((Config::load(path)?, Some(path.to_owned()), cwd.to_owned())); - } - - match discover_config(cwd) { - Some(path) => { - let base = path.parent().unwrap_or(cwd).to_owned(); - Ok((Config::load(&path)?, Some(path), base)) - } - None => Ok((Config::default(), None, cwd.to_owned())), - } -} - -/// Paths under the working directory print relative to it; anything else prints as is. -fn display_path(path: &Path, cwd: &Path) -> PathBuf { - path.strip_prefix(cwd).unwrap_or(path).to_owned() -} diff --git a/crates/compact-lint/src/config.rs b/crates/compact-lint/src/config.rs index 0d7ae2b..002c6d1 100644 --- a/crates/compact-lint/src/config.rs +++ b/crates/compact-lint/src/config.rs @@ -1,5 +1,6 @@ //! `compact-lint.toml`: discovery, parsing, and the built-in defaults. +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use globset::{Glob, GlobSet, GlobSetBuilder}; @@ -117,11 +118,28 @@ impl Default for ConstraintsConfig { } } -/// Tags no doc comment may carry. +/// Tags no doc comment may carry, and the replacements `fix` writes for them. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields, default)] pub struct TagsConfig { pub forbid: Vec, + /// Forbidden tag to its replacement; an unmapped forbidden tag stays a finding. + pub rename: BTreeMap, +} + +/// What `fix` writes where it has no value to write. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields, default)] +pub struct FixConfig { + pub placeholder: String, +} + +impl Default for FixConfig { + fn default() -> Self { + Self { + placeholder: "TODO".to_owned(), + } + } } /// A parsed `compact-lint.toml`, or the built-in defaults when no file was found. @@ -133,6 +151,7 @@ pub struct Config { pub exclude: Vec, pub constraints: ConstraintsConfig, pub tags: TagsConfig, + pub fix: FixConfig, pub kinds: KindsConfig, } @@ -144,6 +163,7 @@ impl Default for Config { exclude: Vec::new(), constraints: ConstraintsConfig::default(), tags: TagsConfig::default(), + fix: FixConfig::default(), kinds: KindsConfig::default(), } } @@ -199,9 +219,17 @@ impl Config { per_kind .chain(self.tags.forbid.iter()) + .chain(self.tags.rename.keys()) + .chain(self.tags.rename.values()) .chain(std::iter::once(&self.constraints.tag)) } + /// The replacement `fix` writes for a forbidden tag, when the config names one. + #[must_use] + pub fn rename_of(&self, tag: &Tag) -> Option<&Tag> { + self.tags.rename.get(tag) + } + /// Compiles the `include` globs. /// # Errors /// Returns an error when a pattern is not a valid glob. @@ -295,6 +323,43 @@ mod tests { std::fs::remove_dir_all(&dir).expect("the temp dir is removable"); } + #[test] + fn a_rename_maps_a_forbidden_tag_to_its_replacement() { + let config: Config = toml::from_str( + "[tags]\nforbid = [\"@return\"]\nrename = { \"@return\" = \"@returns\" }\n", + ) + .expect("the snippet is valid config"); + + assert_eq!( + config.rename_of(&Tag::new("@return")), + Some(&Tag::new("@returns")) + ); + assert_eq!(config.rename_of(&Tag::new("@notice")), None); + } + + #[test] + fn a_rename_target_without_its_at_sign_is_rejected() { + let dir = std::env::temp_dir().join(format!("compact-lint-rename-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("the temp dir is writable"); + let path = dir.join("compact-lint.toml"); + std::fs::write(&path, "[tags]\nrename = { \"@return\" = \"returns\" }\n") + .expect("the config is writable"); + + let error = Config::load(&path).expect_err("the replacement lacks its @"); + + assert!(error.to_string().contains("\"returns\""), "{error}"); + std::fs::remove_dir_all(&dir).expect("the temp dir is removable"); + } + + #[test] + fn the_fix_placeholder_defaults_to_todo() { + assert_eq!(Config::default().fix.placeholder, "TODO"); + + let config: Config = toml::from_str("[fix]\nplaceholder = \"FIXME\"\n") + .expect("the snippet is valid config"); + assert_eq!(config.fix.placeholder, "FIXME"); + } + #[test] fn an_unknown_kind_is_rejected() { let error = diff --git a/crates/compact-lint/src/edit.rs b/crates/compact-lint/src/edit.rs new file mode 100644 index 0000000..281d859 --- /dev/null +++ b/crates/compact-lint/src/edit.rs @@ -0,0 +1,570 @@ +//! Byte-range edits and the doc-comment rewriting `fix` applies. +//! +//! Every edit is computed against the original text. They are applied from the highest +//! offset down, so positions never shift, and edits that target the same doc comment are +//! merged into one replacement of that comment. + +use std::ops::Range; + +use crate::report::Position; + +/// One repair, carrying the line `fix` prints for it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Edit { + pub position: Position, + pub message: String, + pub kind: EditKind, +} + +/// What an edit rewrites. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditKind { + /// Text inserted at a byte offset, used for a whole doc-comment skeleton. + Insert { offset: usize, text: String }, + /// One rewrite of the doc comment spanning `range`. + Doc { + range: Range, + /// Column the `/**` starts on, the indentation of every line the op writes. + indent: usize, + op: DocOp, + }, +} + +/// One change to the lines of a doc comment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DocOp { + /// A `@tag value` line added after the last of `after` present, else at the top of the body. + InsertTag { + line: String, + /// Tags that precede this one in config order. + after: Vec, + /// Untagged prose opening the body becomes this tag's value instead of the placeholder. + adopts_prose: bool, + }, + /// A `@tag k=?, rows=?` line added after the `@description` block. + InsertConstraints(String), + /// The tag name on one line replaced in place. + RenameTag { + /// 0-based line index inside the original comment. + line: usize, + from: String, + to: String, + }, + /// The first word of `@module`'s value replaced with the module's name. + SetModuleName { + /// 0-based line index inside the original comment. + line: usize, + name: String, + }, +} + +/// The line ending the file already uses, taken from its first line. +#[must_use] +pub fn newline_of(source: &str) -> &'static str { + match source.find('\n') { + Some(index) if index > 0 && source.as_bytes()[index - 1] == b'\r' => "\r\n", + _ => "\n", + } +} + +/// Rewrites `source` with every edit, merging the edits that share a doc comment. +#[must_use] +pub fn apply(source: &str, edits: &[Edit], newline: &str) -> String { + let mut replacements: Vec<(Range, String)> = Vec::new(); + let mut docs: Vec<(Range, usize, Vec<&DocOp>)> = Vec::new(); + + for edit in edits { + match &edit.kind { + EditKind::Insert { offset, text } => { + replacements.push((*offset..*offset, text.clone())); + } + EditKind::Doc { range, indent, op } => { + match docs.iter_mut().find(|(known, _, _)| known == range) { + Some((_, _, ops)) => ops.push(op), + None => docs.push((range.clone(), *indent, vec![op])), + } + } + } + } + + for (range, indent, ops) in docs { + let original = source.get(range.clone()).unwrap_or_default(); + replacements.push((range, render_doc(original, indent, newline, &ops))); + } + + replacements.sort_by_key(|(range, _)| std::cmp::Reverse(range.start)); + + let mut out = source.to_owned(); + for (range, text) in replacements { + if range.end <= out.len() { + out.replace_range(range, &text); + } + } + out +} + +/// Applies every op to one doc comment and renders it back with `newline`. +#[must_use] +fn render_doc(doc: &str, indent: usize, newline: &str, ops: &[&DocOp]) -> String { + let mut lines: Vec = doc + .split('\n') + .map(|line| line.strip_suffix('\r').unwrap_or(line).to_owned()) + .collect(); + + let margin = " ".repeat(indent); + let gutter = format!("{margin} * "); + + // An insertion needs a body, so a `/** … */` one-liner expands first. + let expands = lines.len() == 1 + && ops + .iter() + .any(|op| matches!(op, DocOp::InsertTag { .. } | DocOp::InsertConstraints(_))); + let shift = usize::from(expands); + if expands { + lines = expand(&lines[0], &margin, &gutter); + } + + // In-place ops address the original lines, so they run before any insertion moves + // them; tags precede the constraints line, which anchors itself on `@description`. + for op in ops { + match op { + DocOp::RenameTag { line, from, to } => { + if let Some(target) = lines.get_mut(line + shift) + && let Some(renamed) = rename_tag(target, from, to) + { + *target = renamed; + } + } + DocOp::SetModuleName { line, name } => { + if let Some(target) = lines.get_mut(line + shift) + && let Some(renamed) = set_module_name(target, name) + { + *target = renamed; + } + } + _ => {} + } + } + + for op in ops { + if let DocOp::InsertTag { + line, + after, + adopts_prose, + } = op + { + insert_tag(&mut lines, &gutter, line, after, *adopts_prose); + } + } + + for op in ops { + if let DocOp::InsertConstraints(body) = op { + insert_constraints(&mut lines, &margin, &gutter, body); + } + } + + lines.join(newline) +} + +/// Splits `/** text */` into the three-line form, keeping the comment's indentation. +fn expand(single: &str, margin: &str, gutter: &str) -> Vec { + let inner = single + .strip_prefix("/**") + .and_then(|rest| rest.strip_suffix("*/")) + .unwrap_or(single) + .trim(); + + vec![ + "/**".to_owned(), + format!("{gutter}{inner}"), + format!("{margin} */"), + ] +} + +/// A tag line lands after the block of the last tag in `after` that is present, else at +/// the top of the body. With `adopts_prose`, untagged prose opening the body takes the +/// tag instead of a new line going in. +fn insert_tag( + lines: &mut Vec, + gutter: &str, + line: &str, + after: &[String], + adopts_prose: bool, +) { + let closer = lines.len().saturating_sub(1); + + if adopts_prose + && let Some(first) = (1..closer).find(|index| !is_blank(&lines[*index])) + && !starts_tag(content(&lines[first])) + && let Some((tag, _)) = line.split_once(' ') + { + lines[first] = format!("{gutter}{tag} {}", content(&lines[first])); + return; + } + + let at = tag_block_end(lines, closer, after).map_or(1.min(closer), |end| end + 1); + lines.insert(at, format!("{gutter}{line}")); +} + +/// The constraints line follows the `@description` block, or opens the body without one. +fn insert_constraints(lines: &mut Vec, margin: &str, gutter: &str, body: &str) { + let closer = lines.len().saturating_sub(1); + let blank = format!("{margin} *"); + + let at = match description_end(lines, closer) { + Some(end) => { + lines.insert(end + 1, blank.clone()); + end + 2 + } + None => 1.min(closer), + }; + + lines.insert(at, format!("{gutter}{body}")); + + let follows = at + 1; + if follows < lines.len() && follows != lines.len() - 1 && !is_blank(&lines[follows]) { + lines.insert(follows, blank); + } +} + +/// The last line of the `@description` block: its tag line plus every continuation. +fn description_end(lines: &[String], closer: usize) -> Option { + tag_block_end(lines, closer, &["@description".to_owned()]) +} + +/// The last line of the block opened by the last occurrence of any tag in `tags`. +fn tag_block_end(lines: &[String], closer: usize, tags: &[String]) -> Option { + let start = (1..closer).rev().find(|index| { + let text = content(&lines[*index]); + tags.iter().any(|tag| { + text.strip_prefix(tag.as_str()) + .is_some_and(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace)) + }) + })?; + + let mut end = start; + while end + 1 < closer { + let next = content(&lines[end + 1]); + if next.is_empty() || starts_tag(next) { + break; + } + end += 1; + } + Some(end) +} + +/// A comment line without its indentation, its `*` and the space after it. +fn content(line: &str) -> &str { + let trimmed = line.trim_start(); + match trimmed.strip_prefix('*') { + Some(rest) => rest.trim(), + None => trimmed.trim(), + } +} + +fn is_blank(line: &str) -> bool { + content(line).is_empty() +} + +fn starts_tag(content: &str) -> bool { + content + .strip_prefix('@') + .is_some_and(|rest| rest.starts_with(|character: char| character.is_ascii_alphabetic())) +} + +/// Replaces the first whole-word occurrence of `from`, so `@return` never claims `@returns`. +fn rename_tag(line: &str, from: &str, to: &str) -> Option { + let bytes = line.as_bytes(); + let mut search = 0; + while let Some(offset) = line.get(search..)?.find(from) { + let start = search + offset; + let end = start + from.len(); + let before_is_word = start > 0 && bytes[start - 1].is_ascii_alphanumeric(); + let after_is_word = bytes.get(end).is_some_and(u8::is_ascii_alphabetic); + if !before_is_word && !after_is_word { + return Some(format!("{}{to}{}", &line[..start], &line[end..])); + } + search = end; + } + None +} + +/// Replaces the first word after `@module`, keeping whatever follows it. +fn set_module_name(line: &str, name: &str) -> Option { + let tag = "@module"; + let start = line.find(tag)?; + let after = start + tag.len(); + let rest = line.get(after..)?; + + let spaces = rest.len() - rest.trim_start().len(); + let value = rest.trim_start(); + let word = value + .find(char::is_whitespace) + .map_or(value.len(), |index| index); + + if value.is_empty() { + return Some(format!("{line} {name}")); + } + Some(format!( + "{}{}{name}{}", + &line[..after], + &rest[..spaces], + &value[word..] + )) +} + +#[cfg(test)] +mod tests { + use super::{ + DocOp, Edit, EditKind, apply, newline_of, rename_tag, render_doc, set_module_name, + }; + use crate::report::Position; + + fn doc_edit(range: std::ops::Range, indent: usize, op: DocOp) -> Edit { + Edit { + position: Position::file_start(), + message: String::new(), + kind: EditKind::Doc { range, indent, op }, + } + } + + fn tag(line: &str, after: &[&str]) -> DocOp { + DocOp::InsertTag { + line: line.to_owned(), + after: after.iter().map(ToString::to_string).collect(), + adopts_prose: line.starts_with("@description"), + } + } + + fn rendered(doc: &str, indent: usize, ops: &[DocOp]) -> String { + let borrowed: Vec<&DocOp> = ops.iter().collect(); + render_doc(doc, indent, "\n", &borrowed) + } + + #[test] + fn a_tag_goes_in_at_the_top_of_the_body() { + let doc = "/**\n * @constraints k=6, rows=28\n */"; + + assert_eq!( + rendered(doc, 2, &[tag("@description TODO", &[])]), + "/**\n * @description TODO\n * @constraints k=6, rows=28\n */" + ); + } + + #[test] + fn several_tags_keep_the_order_they_were_produced_in() { + let doc = "/**\n * @notice loose\n */"; + let ops = [ + tag("@module Ownable", &[]), + tag("@description TODO", &["@module"]), + ]; + + assert_eq!( + rendered(doc, 0, &ops), + "/**\n * @module Ownable\n * @description TODO\n * @notice loose\n */" + ); + } + + #[test] + fn a_tag_lands_after_the_tags_that_precede_it_in_config_order() { + let doc = "/**\n * @module Ownable\n * spanning two lines\n *\n * @notice loose\n */"; + + assert_eq!( + rendered(doc, 0, &[tag("@description TODO", &["@module"])]), + "/**\n * @module Ownable\n * spanning two lines\n * @description TODO\n *\n * @notice loose\n */" + ); + } + + #[test] + fn a_description_adopts_the_prose_that_opens_the_body() { + let doc = "/**\n * Approved accounts.\n * @param {T} a - A.\n */"; + + assert_eq!( + rendered(doc, 0, &[tag("@description TODO", &[])]), + "/**\n * @description Approved accounts.\n * @param {T} a - A.\n */" + ); + assert_eq!( + rendered( + "/** Approved accounts. */", + 2, + &[tag("@description TODO", &[])] + ), + "/**\n * @description Approved accounts.\n */" + ); + } + + #[test] + fn a_single_line_doc_expands_before_a_tag_goes_in() { + let doc = "/** @description Approved accounts. */"; + + assert_eq!( + rendered(doc, 2, &[tag("@module M", &[])]), + "/**\n * @module M\n * @description Approved accounts.\n */" + ); + } + + #[test] + fn constraints_follow_the_description_and_its_continuation_lines() { + let doc = "/**\n * @description Sends a coin\n * to the recipient.\n * @param {Coin} coin - The coin.\n */"; + + assert_eq!( + rendered( + doc, + 0, + &[DocOp::InsertConstraints( + "@constraints k=?, rows=?".to_owned() + )] + ), + "/**\n * @description Sends a coin\n * to the recipient.\n *\n * @constraints k=?, rows=?\n *\n * @param {Coin} coin - The coin.\n */" + ); + } + + #[test] + fn constraints_need_no_trailing_blank_before_the_closer() { + let doc = "/**\n * @description Bumps.\n */"; + + assert_eq!( + rendered( + doc, + 0, + &[DocOp::InsertConstraints( + "@constraints k=?, rows=?".to_owned() + )] + ), + "/**\n * @description Bumps.\n *\n * @constraints k=?, rows=?\n */" + ); + } + + #[test] + fn constraints_open_the_body_when_there_is_no_description() { + let doc = "/**\n * @notice loose prose.\n */"; + + assert_eq!( + rendered( + doc, + 0, + &[DocOp::InsertConstraints( + "@constraints k=?, rows=?".to_owned() + )] + ), + "/**\n * @constraints k=?, rows=?\n *\n * @notice loose prose.\n */" + ); + } + + #[test] + fn a_missing_description_and_missing_constraints_compose_on_one_comment() { + let doc = "/**\n * @param {Uint<8>} value - The value.\n */"; + let composed = "/**\n * @description TODO\n *\n * @constraints k=?, rows=?\n *\n * @param {Uint<8>} value - The value.\n */"; + let tag = tag("@description TODO", &[]); + let constraints = DocOp::InsertConstraints("@constraints k=?, rows=?".to_owned()); + + assert_eq!( + rendered(doc, 0, &[tag.clone(), constraints.clone()]), + composed + ); + // The rule that reports first must not decide where the annotation lands. + assert_eq!(rendered(doc, 0, &[constraints, tag]), composed); + } + + #[test] + fn two_edits_on_one_comment_merge_into_a_single_replacement() { + let source = "/**\n * @return nothing\n */\nexport circuit run(): [] { }\n"; + let edits = [ + doc_edit( + 0..26, + 0, + DocOp::RenameTag { + line: 1, + from: "@return".to_owned(), + to: "@returns".to_owned(), + }, + ), + doc_edit(0..26, 0, tag("@description TODO", &[])), + ]; + + assert_eq!( + apply(source, &edits, "\n"), + "/**\n * @description TODO\n * @returns nothing\n */\nexport circuit run(): [] { }\n" + ); + } + + #[test] + fn an_insertion_and_a_later_comment_edit_both_land() { + let source = + "export ledger a: Uint<8>;\n/**\n * @return x\n */\nexport circuit run(): [] { }\n"; + let edits = [ + Edit { + position: Position::file_start(), + message: String::new(), + kind: EditKind::Insert { + offset: 0, + text: "/**\n * TODO\n */\n".to_owned(), + }, + }, + doc_edit( + 26..45, + 0, + DocOp::RenameTag { + line: 1, + from: "@return".to_owned(), + to: "@returns".to_owned(), + }, + ), + ]; + + assert_eq!( + apply(source, &edits, "\n"), + "/**\n * TODO\n */\nexport ledger a: Uint<8>;\n/**\n * @returns x\n */\nexport circuit run(): [] { }\n" + ); + } + + #[test] + fn a_rename_leaves_the_rest_of_the_line_alone() { + assert_eq!( + rename_tag(" * @return {[]} - Empty tuple.", "@return", "@returns"), + Some(" * @returns {[]} - Empty tuple.".to_owned()) + ); + } + + #[test] + fn a_rename_does_not_match_a_longer_tag() { + assert_eq!(rename_tag(" * @returns x", "@return", "@returns"), None); + } + + #[test] + fn a_module_rewrite_keeps_what_follows_the_name() { + assert_eq!( + set_module_name(" * @module Stale (archived)", "Renamed"), + Some(" * @module Renamed (archived)".to_owned()) + ); + assert_eq!( + set_module_name(" * @module Stale", "Renamed"), + Some(" * @module Renamed".to_owned()) + ); + } + + #[test] + fn crlf_sources_keep_their_line_ending() { + let source = "/**\r\n * @description Bumps.\r\n */\r\n"; + let edits = [doc_edit( + 0..32, + 0, + DocOp::InsertConstraints("@constraints k=?, rows=?".to_owned()), + )]; + + assert_eq!(newline_of(source), "\r\n"); + assert_eq!( + apply(source, &edits, newline_of(source)), + "/**\r\n * @description Bumps.\r\n *\r\n * @constraints k=?, rows=?\r\n */\r\n" + ); + } + + #[test] + fn a_file_without_a_trailing_newline_keeps_it_missing() { + let source = "/**\n * @description Bumps.\n */"; + let edits = [doc_edit(0..30, 0, tag("@module M", &[]))]; + + let out = apply(source, &edits, "\n"); + assert!(!out.ends_with('\n'), "{out:?}"); + } +} diff --git a/crates/compact-lint/src/fix.rs b/crates/compact-lint/src/fix.rs new file mode 100644 index 0000000..88e42bd --- /dev/null +++ b/crates/compact-lint/src/fix.rs @@ -0,0 +1,377 @@ +//! The `fix` subcommand: turn the fixable issues into edits and rewrite the files. + +use std::path::{Path, PathBuf}; + +use thiserror::Error; + +use crate::config::Config; +use crate::doc::Tag; +use crate::edit::{DocOp, Edit, EditKind, apply, newline_of}; +use crate::model::DeclKind; +use crate::report::Position; +use crate::rules::{Issue, LintError, Linter}; +use crate::target::{self, TargetError}; + +/// The value written into an inserted constraints annotation. +const UNMEASURED: &str = "k=?, rows=?"; + +/// The tag whose value is the declaration's own name. +const MODULE_TAG: &str = "@module"; + +/// The tag that adopts untagged prose opening a doc comment. +const DESCRIPTION_TAG: &str = "@description"; + +#[derive(Debug, Error)] +pub enum FixError { + #[error(transparent)] + Target(#[from] TargetError), + #[error(transparent)] + Lint(#[from] LintError), + #[error("cannot read {path}")] + Read { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("cannot write {path}")] + Write { + path: PathBuf, + #[source] + source: std::io::Error, + }, +} + +/// Everything the `fix` subcommand needs, already resolved from flags. +pub struct Options { + pub paths: Vec, + pub config_path: Option, + pub dry_run: bool, +} + +/// The edits one file took. +pub struct FileEdits { + pub path: PathBuf, + pub edits: Vec, +} + +/// Every file the run changed, in the order they were visited. +pub struct Outcome { + pub files: Vec, +} + +impl Outcome { + #[must_use] + pub fn edits(&self) -> usize { + self.files.iter().map(|file| file.edits.len()).sum() + } + + #[must_use] + pub fn summary(&self) -> String { + format!("{} edits in {} files", self.edits(), self.files.len()) + } +} + +/// Runs the subcommand with `cwd` as the working directory. +/// # Errors +/// Returns an error when the config, the file walk, the parser or a write fails. +pub fn run(options: &Options, cwd: &Path) -> Result { + let target = target::resolve(&options.paths, options.config_path.as_deref(), cwd)?; + + let mut linter = Linter::new()?; + let mut files = Vec::new(); + + for path in &target.files { + let source = std::fs::read_to_string(path).map_err(|source| FixError::Read { + path: path.clone(), + source, + })?; + + let newline = newline_of(&source); + let edits: Vec = linter + .issues(path, &source, &target.config, false)? + .iter() + .filter_map(|issue| edit_for(issue, &target.config, newline)) + .collect(); + + if edits.is_empty() { + continue; + } + + if !options.dry_run { + write_atomically(path, &apply(&source, &edits, newline))?; + } + files.push(FileEdits { + path: path.clone(), + edits, + }); + } + + Ok(Outcome { files }) +} + +/// The repair for one issue, or `None` where the rule has no safe fix. +/// +/// `constraints-format` and `constraints-placeholder` need a measurement a rewrite +/// cannot invent, and `parse` and `format` are not doc-comment issues at all. +#[must_use] +pub fn edit_for(issue: &Issue, config: &Config, newline: &str) -> Option { + match issue { + Issue::MissingDoc { + position, + subject, + kind, + name, + offset, + indent, + constraints, + } => Some(Edit { + position: *position, + message: format!("inserted doc skeleton for {subject}"), + kind: EditKind::Insert { + offset: *offset, + text: skeleton( + *kind, + name.as_deref(), + *constraints, + config, + *indent, + newline, + ), + }, + }), + + Issue::MissingTag { + position, + kind, + tag, + name, + doc, + .. + } => Some(Edit { + position: *position, + message: format!("inserted {tag}"), + kind: EditKind::Doc { + range: doc.range.clone(), + indent: doc.indent, + op: missing_tag_op(*kind, tag, name.as_deref(), config), + }, + }), + + Issue::MissingConstraints { + position, tag, doc, .. + } => Some(Edit { + position: *position, + message: format!("inserted {tag} {UNMEASURED}"), + kind: EditKind::Doc { + range: doc.range.clone(), + indent: doc.indent, + op: DocOp::InsertConstraints(format!("{tag} {UNMEASURED}")), + }, + }), + + Issue::ForbiddenTag { + position, + tag, + line_offset, + doc, + } => { + let replacement = config.rename_of(tag)?; + Some(Edit { + position: *position, + message: format!("renamed {tag} to {replacement}"), + kind: EditKind::Doc { + range: doc.range.clone(), + indent: doc.indent, + op: DocOp::RenameTag { + line: *line_offset, + from: tag.to_string(), + to: replacement.to_string(), + }, + }, + }) + } + + Issue::ModuleName { + position, + name, + line_offset, + doc, + .. + } => Some(Edit { + position: *position, + message: format!("set {MODULE_TAG} to `{name}`"), + kind: EditKind::Doc { + range: doc.range.clone(), + indent: doc.indent, + op: DocOp::SetModuleName { + line: *line_offset, + name: name.clone(), + }, + }, + }), + + Issue::Parse { .. } + | Issue::ConstraintsFormat { .. } + | Issue::ConstraintsPlaceholder { .. } => None, + } +} + +/// The tag line lands after the tags that precede it in config order; `@description` +/// adopts prose that opens the body. +fn missing_tag_op(kind: DeclKind, tag: &Tag, name: Option<&str>, config: &Config) -> DocOp { + DocOp::InsertTag { + line: tag_line(tag, name, config), + after: config + .kinds + .get(kind) + .tags + .iter() + .take_while(|required| *required != tag) + .map(ToString::to_string) + .collect(), + adopts_prose: tag.as_str() == DESCRIPTION_TAG, + } +} + +/// `@module` takes the declaration's own name; every other tag takes the placeholder. +fn tag_line(tag: &Tag, name: Option<&str>, config: &Config) -> String { + match name { + Some(name) if tag.as_str() == MODULE_TAG => format!("{tag} {name}"), + _ => format!("{tag} {}", config.fix.placeholder), + } +} + +/// The doc comment `fix` writes above an undocumented declaration. +/// +/// Body lines come from `kinds..tags` in config order, then the constraints +/// annotation where the declaration takes one. +fn skeleton( + kind: DeclKind, + name: Option<&str>, + constraints: bool, + config: &Config, + indent: usize, + newline: &str, +) -> String { + let mut body: Vec = config + .kinds + .get(kind) + .tags + .iter() + .map(|tag| tag_line(tag, name, config)) + .collect(); + + if constraints { + if !body.is_empty() { + body.push(String::new()); + } + body.push(format!("{} {UNMEASURED}", config.constraints.tag)); + } + if body.is_empty() { + body.push(config.fix.placeholder.clone()); + } + + let margin = " ".repeat(indent); + let mut lines = vec!["/**".to_owned()]; + for line in body { + if line.is_empty() { + lines.push(format!("{margin} *")); + } else { + lines.push(format!("{margin} * {line}")); + } + } + lines.push(format!("{margin} */")); + + // The declaration follows on its own line, back at its original indentation. + format!("{}{newline}{margin}", lines.join(newline)) +} + +/// Writes through a sibling temporary file, so a failed write never truncates the source. +fn write_atomically(path: &Path, text: &str) -> Result<(), FixError> { + let temporary = path.with_extension("compact.tmp"); + + std::fs::write(&temporary, text).map_err(|source| FixError::Write { + path: temporary.clone(), + source, + })?; + let permissions = std::fs::metadata(path) + .map_err(|source| FixError::Read { + path: path.to_owned(), + source, + })? + .permissions(); + std::fs::set_permissions(&temporary, permissions).map_err(|source| FixError::Write { + path: temporary.clone(), + source, + })?; + std::fs::rename(&temporary, path).map_err(|source| FixError::Write { + path: path.to_owned(), + source, + }) +} + +/// The line `fix` prints for one edit. +#[must_use] +pub fn line(path: &Path, position: Position, message: &str) -> String { + format!( + "{}:{}:{}: fix: {message}", + path.display(), + position.line, + position.column + ) +} + +#[cfg(test)] +mod tests { + use super::skeleton; + use crate::config::Config; + use crate::doc::Tag; + use crate::model::DeclKind; + + fn config() -> Config { + let mut config = Config::default(); + config.kinds.module.tags = vec![Tag::new("@module"), Tag::new("@description")]; + config.kinds.circuit.tags = vec![Tag::new("@description")]; + config + } + + #[test] + fn a_module_skeleton_names_the_module() { + assert_eq!( + skeleton(DeclKind::Module, Some("Ownable"), false, &config(), 0, "\n"), + "/**\n * @module Ownable\n * @description TODO\n */\n" + ); + } + + #[test] + fn an_exported_circuit_skeleton_carries_the_constraints_line() { + assert_eq!( + skeleton(DeclKind::Circuit, Some("run"), true, &config(), 2, "\n"), + "/**\n * @description TODO\n *\n * @constraints k=?, rows=?\n */\n " + ); + } + + #[test] + fn a_pure_circuit_skeleton_stops_at_the_tag_lines() { + assert_eq!( + skeleton(DeclKind::Circuit, Some("double"), false, &config(), 2, "\n"), + "/**\n * @description TODO\n */\n " + ); + } + + #[test] + fn a_kind_with_no_required_tags_still_gets_a_placeholder_body() { + assert_eq!( + skeleton(DeclKind::Type, Some("RoleId"), false, &config(), 2, "\n"), + "/**\n * TODO\n */\n " + ); + } + + #[test] + fn a_crlf_skeleton_uses_the_file_line_ending() { + assert_eq!( + skeleton(DeclKind::Type, None, false, &config(), 0, "\r\n"), + "/**\r\n * TODO\r\n */\r\n" + ); + } +} diff --git a/crates/compact-lint/src/lib.rs b/crates/compact-lint/src/lib.rs index fdaf6e9..2eb94bb 100644 --- a/crates/compact-lint/src/lib.rs +++ b/crates/compact-lint/src/lib.rs @@ -2,7 +2,8 @@ //! //! The crate parses `.compact` files with the bundled tree-sitter grammar, matches each //! declaration against the per-kind doc-comment template in `compact-lint.toml`, and -//! reports one [`report::Finding`] per violation. +//! yields one [`rules::Issue`] per violation. `check` renders an issue as a +//! [`report::Finding`]; `fix` renders the repairable ones as an [`edit::Edit`]. #![forbid(unsafe_code)] @@ -10,7 +11,10 @@ pub mod check; pub mod config; pub mod discover; pub mod doc; +pub mod edit; +pub mod fix; pub mod format; pub mod model; pub mod report; pub mod rules; +pub mod target; diff --git a/crates/compact-lint/src/main.rs b/crates/compact-lint/src/main.rs index dc4d364..e47c785 100644 --- a/crates/compact-lint/src/main.rs +++ b/crates/compact-lint/src/main.rs @@ -10,10 +10,11 @@ use std::process::ExitCode; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use compact_lint::check::{self, Options, Outcome}; +use compact_lint::check::{self, Outcome}; +use compact_lint::fix; use compact_lint::format::{COMPACT_BIN_ENV, DEFAULT_COMPACT_BIN}; -/// Exit code for a run that produced findings. +/// Exit code for a run that produced findings, or a `--dry-run` that would edit. const EXIT_FINDINGS: u8 = 1; /// Exit code for a usage, config, IO or parser error. @@ -34,6 +35,8 @@ struct Cli { enum Command { /// Check doc comments against the per-kind templates in compact-lint.toml. Check(CheckArgs), + /// Rewrite doc comments so the rules `fix` covers stop reporting. + Fix(FixArgs), } #[derive(Debug, clap::Args)] @@ -58,6 +61,20 @@ struct CheckArgs { compact_bin: Option, } +#[derive(Debug, clap::Args)] +struct FixArgs { + /// Files or directories to fix; defaults to the config's include globs. + paths: Vec, + + /// Config file to use instead of searching upward for compact-lint.toml. + #[arg(long, value_name = "FILE")] + config: Option, + + /// Report the edits without writing them; exits 1 when there are any. + #[arg(long)] + dry_run: bool, +} + fn main() -> ExitCode { match run() { Ok(code) => code, @@ -70,10 +87,16 @@ fn main() -> ExitCode { fn run() -> Result { let cli = Cli::parse(); - let Command::Check(args) = cli.command; - let cwd = std::env::current_dir().context("reading the current directory")?; - let options = Options { + + match cli.command { + Command::Check(args) => run_check(args, &cwd), + Command::Fix(args) => run_fix(args, &cwd), + } +} + +fn run_check(args: CheckArgs, cwd: &std::path::Path) -> Result { + let options = check::Options { paths: args.paths, config_path: args.config, strict: args.strict, @@ -83,8 +106,8 @@ fn run() -> Result { .unwrap_or_else(|| OsString::from(DEFAULT_COMPACT_BIN)), }; - let outcome = check::run(&options, &cwd).context("running the check")?; - emit(&outcome).context("writing the report")?; + let outcome = check::run(&options, cwd).context("running the check")?; + emit_findings(&outcome).context("writing the report")?; Ok(if outcome.findings.is_empty() { ExitCode::SUCCESS @@ -93,7 +116,24 @@ fn run() -> Result { }) } -fn emit(outcome: &Outcome) -> std::io::Result<()> { +fn run_fix(args: FixArgs, cwd: &std::path::Path) -> Result { + let options = fix::Options { + paths: args.paths, + config_path: args.config, + dry_run: args.dry_run, + }; + + let outcome = fix::run(&options, cwd).context("running the fix")?; + emit_edits(&outcome).context("writing the report")?; + + Ok(if options.dry_run && outcome.edits() > 0 { + ExitCode::from(EXIT_FINDINGS) + } else { + ExitCode::SUCCESS + }) +} + +fn emit_findings(outcome: &Outcome) -> std::io::Result<()> { let stdout = std::io::stdout(); let mut out = stdout.lock(); for finding in &outcome.findings { @@ -105,6 +145,24 @@ fn emit(outcome: &Outcome) -> std::io::Result<()> { Ok(()) } +fn emit_edits(outcome: &fix::Outcome) -> std::io::Result<()> { + let stdout = std::io::stdout(); + let mut out = stdout.lock(); + for file in &outcome.files { + for edit in &file.edits { + writeln!( + out, + "{}", + fix::line(&file.path, edit.position, &edit.message) + )?; + } + } + out.flush()?; + + eprintln!("{}", outcome.summary()); + Ok(()) +} + /// Prints the error and every source under it, one cause per line. fn report_error(error: &anyhow::Error) { eprintln!("compact-lint: {error}"); diff --git a/crates/compact-lint/src/model.rs b/crates/compact-lint/src/model.rs index 968eee8..5634f9c 100644 --- a/crates/compact-lint/src/model.rs +++ b/crates/compact-lint/src/model.rs @@ -1,6 +1,7 @@ //! Declarations the linter checks, lifted out of the tree-sitter tree. use std::fmt; +use std::ops::Range; use tree_sitter::Node; @@ -75,6 +76,8 @@ pub struct Declaration { pub exported: bool, pub pure: bool, pub position: Position, + /// Byte offset of the declaration's first token, where `fix` inserts a skeleton. + pub offset: usize, /// The doc comment immediately preceding the declaration, if one attaches. pub doc: Option, } @@ -86,6 +89,8 @@ pub struct AttachedDoc { /// 0-based row of the `/**` line, for turning a tag's offset into a position. pub start_row: usize, pub start_column: usize, + /// Byte range of the `/** … */` node, the span `fix` rewrites. + pub range: Range, } impl AttachedDoc { @@ -152,6 +157,7 @@ fn declaration(kind: DeclKind, node: Node<'_>, source: &str) -> Declaration { exported, pure, position: Position::from_zero_based(start.row, start.column), + offset: node.start_byte(), doc: attached_doc(node, source), } } @@ -169,6 +175,7 @@ fn attached_doc(node: Node<'_>, source: &str) -> Option { comment: DocComment::parse(text(previous, source)?), start_row: start.row, start_column: start.column, + range: previous.start_byte()..previous.end_byte(), }) } diff --git a/crates/compact-lint/src/rules.rs b/crates/compact-lint/src/rules.rs index 9bc0bf7..eb723dd 100644 --- a/crates/compact-lint/src/rules.rs +++ b/crates/compact-lint/src/rules.rs @@ -1,5 +1,9 @@ //! The doc-comment rules, run over one parsed file at a time. +//! +//! Detection yields [`Issue`]s. `check` renders them as [`Finding`]s and `fix` renders +//! the fixable ones as edits, so both subcommands read the same detection pass. +use std::ops::Range; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -18,6 +22,142 @@ pub enum LintError { NoTree(PathBuf), } +/// Where a doc comment sits in the file, for the rules that rewrite one. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DocRef { + /// Byte range of the `/** … */` node. + pub range: Range, + /// Column the `/**` starts on, reused as the rewritten comment's indentation. + pub indent: usize, +} + +/// One rule violation, carrying what `check` prints and what `fix` needs to repair it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Issue { + Parse { + position: Position, + node_kind: String, + missing: bool, + }, + MissingDoc { + position: Position, + subject: String, + kind: DeclKind, + name: Option, + /// Byte offset of the declaration's first token. + offset: usize, + /// The declaration's start column, reused as the skeleton's indentation. + indent: usize, + /// The declaration also carries the constraints annotation. + constraints: bool, + }, + MissingTag { + position: Position, + subject: String, + kind: DeclKind, + tag: Tag, + /// The declaration's name, which `@module` takes as its value. + name: Option, + doc: DocRef, + }, + ForbiddenTag { + position: Position, + tag: Tag, + /// 0-based line index of the tag inside the doc comment. + line_offset: usize, + doc: DocRef, + }, + ModuleName { + position: Position, + documented: String, + name: String, + /// 0-based line index of `@module` inside the doc comment. + line_offset: usize, + doc: DocRef, + }, + MissingConstraints { + position: Position, + subject: String, + tag: Tag, + doc: DocRef, + }, + ConstraintsFormat { + position: Position, + tag: Tag, + value: String, + }, + ConstraintsPlaceholder { + position: Position, + tag: Tag, + value: String, + }, +} + +impl Issue { + #[must_use] + pub const fn position(&self) -> Position { + match self { + Self::Parse { position, .. } + | Self::MissingDoc { position, .. } + | Self::MissingTag { position, .. } + | Self::ForbiddenTag { position, .. } + | Self::ModuleName { position, .. } + | Self::MissingConstraints { position, .. } + | Self::ConstraintsFormat { position, .. } + | Self::ConstraintsPlaceholder { position, .. } => *position, + } + } + + #[must_use] + pub const fn rule(&self) -> RuleId { + match self { + Self::Parse { .. } => RuleId::PARSE, + Self::MissingDoc { .. } => RuleId::MISSING_DOC, + Self::MissingTag { .. } => RuleId::MISSING_TAG, + Self::ForbiddenTag { .. } => RuleId::FORBIDDEN_TAG, + Self::ModuleName { .. } => RuleId::MODULE_NAME, + Self::MissingConstraints { .. } => RuleId::MISSING_CONSTRAINTS, + Self::ConstraintsFormat { .. } => RuleId::CONSTRAINTS_FORMAT, + Self::ConstraintsPlaceholder { .. } => RuleId::CONSTRAINTS_PLACEHOLDER, + } + } + + #[must_use] + pub fn message(&self) -> String { + match self { + Self::Parse { + node_kind, missing, .. + } => { + if *missing { + format!("parse error: missing {node_kind}") + } else { + format!("parse error: unexpected {node_kind}") + } + } + Self::MissingDoc { subject, .. } => format!("{subject} has no doc comment"), + Self::MissingTag { subject, tag, .. } + | Self::MissingConstraints { subject, tag, .. } => { + format!("{subject} doc comment has no {tag}") + } + Self::ForbiddenTag { tag, .. } => format!("forbidden tag {tag}"), + Self::ModuleName { + documented, name, .. + } => format!("@module names `{documented}`, but the module is `{name}`"), + Self::ConstraintsFormat { tag, value, .. } => { + format!("{tag} value `{value}` is not `k=, rows=`") + } + Self::ConstraintsPlaceholder { tag, value, .. } => { + format!("{tag} value `{value}` still holds a placeholder") + } + } + } + + #[must_use] + pub fn finding(&self, display: &Path) -> Finding { + Finding::new(display, self.position(), self.rule(), self.message()) + } +} + /// A reusable parser plus the rules that run over its trees. pub struct Linter { parser: Parser, @@ -34,19 +174,19 @@ impl Linter { Ok(Self { parser }) } - /// Checks one file. `display` is the path printed in findings. + /// Runs every rule over one file, in source order. /// - /// A tree holding an `ERROR` or `MISSING` node yields a single `parse` finding and - /// no doc findings, because declarations around the defect are not trustworthy. + /// A tree holding an `ERROR` or `MISSING` node yields a single `parse` issue and no + /// doc issues, because declarations around the defect are not trustworthy. /// # Errors /// Returns an error when tree-sitter produces no tree for `source`. - pub fn check( + pub fn issues( &mut self, display: &Path, source: &str, config: &Config, strict: bool, - ) -> Result, LintError> { + ) -> Result, LintError> { let tree = self .parser .parse(source, None) @@ -54,42 +194,42 @@ impl Linter { let root = tree.root_node(); if let Some(defect) = first_defect(root) { - return Ok(vec![defect.finding(display)]); + return Ok(vec![defect]); } - let mut findings = Vec::new(); + let mut issues = Vec::new(); for declaration in declarations(root, source) { - check_declaration(&declaration, display, config, strict, &mut findings); + check_declaration(&declaration, config, strict, &mut issues); } - findings.sort_by(|left, right| left.sort_key().cmp(&right.sort_key())); - Ok(findings) + issues.sort_by_key(|issue| (issue.position(), issue.rule())); + Ok(issues) } -} - -struct Defect { - position: Position, - kind: String, - missing: bool, -} -impl Defect { - fn finding(&self, display: &Path) -> Finding { - let message = if self.missing { - format!("parse error: missing {}", self.kind) - } else { - format!("parse error: unexpected {}", self.kind) - }; - Finding::new(display, self.position, RuleId::PARSE, message) + /// Checks one file. `display` is the path printed in findings. + /// # Errors + /// Returns an error when tree-sitter produces no tree for `source`. + pub fn check( + &mut self, + display: &Path, + source: &str, + config: &Config, + strict: bool, + ) -> Result, LintError> { + Ok(self + .issues(display, source, config, strict)? + .iter() + .map(|issue| issue.finding(display)) + .collect()) } } /// The first `ERROR` or `MISSING` node in document order. -fn first_defect(node: Node<'_>) -> Option { +fn first_defect(node: Node<'_>) -> Option { if node.is_error() || node.is_missing() { let start = node.start_position(); - return Some(Defect { + return Some(Issue::Parse { position: Position::from_zero_based(start.row, start.column), - kind: node.kind().to_owned(), + node_kind: node.kind().to_owned(), missing: node.is_missing(), }); } @@ -104,10 +244,9 @@ fn first_defect(node: Node<'_>) -> Option { fn check_declaration( declaration: &Declaration, - display: &Path, config: &Config, strict: bool, - findings: &mut Vec, + issues: &mut Vec, ) { let kind_config = config.kinds.get(declaration.kind); let requires_docs = match kind_config.docs { @@ -118,32 +257,40 @@ fn check_declaration( let Some(attached) = declaration.doc.as_ref() else { if requires_docs { - findings.push(Finding::new( - display, - declaration.position, - RuleId::MISSING_DOC, - format!("{} has no doc comment", subject(declaration)), - )); + issues.push(Issue::MissingDoc { + position: declaration.position, + subject: subject(declaration), + kind: declaration.kind, + name: declaration.name.clone(), + offset: declaration.offset, + indent: declaration.position.column - 1, + constraints: takes_constraints(declaration), + }); } return; }; - // A forbidden tag is a finding wherever it appears, documented or not. + let doc = DocRef { + range: attached.range.clone(), + indent: attached.start_column, + }; + + // A forbidden tag is an issue wherever it appears, documented or not. for forbidden in &config.tags.forbid { for occurrence in attached.comment.tags() { if &occurrence.tag == forbidden { - findings.push(Finding::new( - display, - attached.tag_position(occurrence), - RuleId::FORBIDDEN_TAG, - format!("forbidden tag {forbidden}"), - )); + issues.push(Issue::ForbiddenTag { + position: attached.tag_position(occurrence), + tag: forbidden.clone(), + line_offset: occurrence.line_offset, + doc: doc.clone(), + }); } } } if declaration.kind == DeclKind::Module { - check_module_name(declaration, display, findings); + check_module_name(declaration, &doc, issues); } if !requires_docs { @@ -152,20 +299,22 @@ fn check_declaration( for required in &kind_config.tags { if !attached.comment.has(required) { - findings.push(Finding::new( - display, - declaration.position, - RuleId::MISSING_TAG, - format!("{} doc comment has no {required}", subject(declaration)), - )); + issues.push(Issue::MissingTag { + position: declaration.position, + subject: subject(declaration), + kind: declaration.kind, + tag: required.clone(), + name: declaration.name.clone(), + doc: doc.clone(), + }); } } - check_constraints(declaration, display, config, strict, findings); + check_constraints(declaration, &doc, config, strict, issues); } /// `@module ` must name the module it documents. -fn check_module_name(declaration: &Declaration, display: &Path, findings: &mut Vec) { +fn check_module_name(declaration: &Declaration, doc: &DocRef, issues: &mut Vec) { let tag = Tag::new("@module"); let (Some(attached), Some(name)) = (declaration.doc.as_ref(), declaration.name.as_deref()) else { @@ -177,12 +326,13 @@ fn check_module_name(declaration: &Declaration, display: &Path, findings: &mut V let documented = module_name(&occurrence.value); if documented != name { - findings.push(Finding::new( - display, - attached.tag_position(occurrence), - RuleId::MODULE_NAME, - format!("@module names `{documented}`, but the module is `{name}`"), - )); + issues.push(Issue::ModuleName { + position: attached.tag_position(occurrence), + documented: documented.to_owned(), + name: name.to_owned(), + line_offset: occurrence.line_offset, + doc: doc.clone(), + }); } } @@ -194,14 +344,18 @@ fn module_name(value: &str) -> &str { } /// Exported non-pure circuits carry the constraints annotation. +const fn takes_constraints(declaration: &Declaration) -> bool { + matches!(declaration.kind, DeclKind::Circuit) && declaration.exported && !declaration.pure +} + fn check_constraints( declaration: &Declaration, - display: &Path, + doc: &DocRef, config: &Config, strict: bool, - findings: &mut Vec, + issues: &mut Vec, ) { - if declaration.kind != DeclKind::Circuit || !declaration.exported || declaration.pure { + if !takes_constraints(declaration) { return; } let Some(attached) = declaration.doc.as_ref() else { @@ -210,12 +364,12 @@ fn check_constraints( let tag = &config.constraints.tag; let Some(occurrence) = attached.comment.first(tag) else { - findings.push(Finding::new( - display, - declaration.position, - RuleId::MISSING_CONSTRAINTS, - format!("{} doc comment has no {tag}", subject(declaration)), - )); + issues.push(Issue::MissingConstraints { + position: declaration.position, + subject: subject(declaration), + tag: tag.clone(), + doc: doc.clone(), + }); return; }; @@ -224,22 +378,20 @@ fn check_constraints( let position = attached.tag_position(occurrence); let Some(placeholders) = parse_constraints(value) else { - findings.push(Finding::new( - display, + issues.push(Issue::ConstraintsFormat { position, - RuleId::CONSTRAINTS_FORMAT, - format!("{tag} value `{value}` is not `k=, rows=`"), - )); + tag: tag.clone(), + value: value.to_owned(), + }); return; }; if strict && placeholders { - findings.push(Finding::new( - display, + issues.push(Issue::ConstraintsPlaceholder { position, - RuleId::CONSTRAINTS_PLACEHOLDER, - format!("{tag} value `{value}` still holds a placeholder"), - )); + tag: tag.clone(), + value: value.to_owned(), + }); } } diff --git a/crates/compact-lint/src/target.rs b/crates/compact-lint/src/target.rs new file mode 100644 index 0000000..aaa5e16 --- /dev/null +++ b/crates/compact-lint/src/target.rs @@ -0,0 +1,65 @@ +//! What a subcommand runs over: the resolved config plus the file list. + +use std::path::{Path, PathBuf}; + +use thiserror::Error; + +use crate::config::{Config, ConfigError, discover as discover_config}; +use crate::discover::{self, DiscoverError}; + +#[derive(Debug, Error)] +pub enum TargetError { + #[error(transparent)] + Config(#[from] ConfigError), + #[error(transparent)] + Discover(#[from] DiscoverError), +} + +/// The config and the files it selects, shared by `check` and `fix`. +pub struct Target { + pub config: Config, + pub files: Vec, +} + +/// Resolves the config and the file list for one run. +/// +/// An explicit `config_path` is a shareable preset, so glob discovery stays anchored at +/// `cwd`; a config found by walking upward anchors discovery at its own directory. +/// # Errors +/// Returns an error when the config is unreadable or a path cannot be walked. +pub fn resolve( + paths: &[PathBuf], + config_path: Option<&Path>, + cwd: &Path, +) -> Result { + let (config, source, base) = match config_path { + Some(path) => (Config::load(path)?, path.to_owned(), cwd.to_owned()), + None => match discover_config(cwd) { + Some(path) => { + let base = path.parent().unwrap_or(cwd).to_owned(); + (Config::load(&path)?, path, base) + } + None => ( + Config::default(), + PathBuf::from(""), + cwd.to_owned(), + ), + }, + }; + + let exclude = config.exclude_set(&source)?; + let files = if paths.is_empty() { + let include = config.include_set(&source)?; + discover::from_globs(&base, &include, &exclude)? + } else { + discover::from_paths(paths, &base, &exclude)? + }; + + let files = files.iter().map(|path| display_path(path, cwd)).collect(); + Ok(Target { config, files }) +} + +/// Paths under the working directory print relative to it; anything else prints as is. +fn display_path(path: &Path, cwd: &Path) -> PathBuf { + path.strip_prefix(cwd).unwrap_or(path).to_owned() +} diff --git a/crates/compact-lint/tests/fix.rs b/crates/compact-lint/tests/fix.rs new file mode 100644 index 0000000..fae2b52 --- /dev/null +++ b/crates/compact-lint/tests/fix.rs @@ -0,0 +1,223 @@ +//! `compact-lint fix` end to end, one fixture case per rule. +//! +//! A case holds `compact-lint.toml`, `before/`, `after/` and `expected.txt`. The run +//! works on a copy of `before/`, so the fixtures themselves are never rewritten. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use assert_cmd::prelude::*; +use tempfile::TempDir; + +/// Exit code for a `--dry-run` that would edit. +const EXIT_EDITS: i32 = 1; + +fn case(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(format!("fix-{name}")) + .canonicalize() + .expect("the fixture case exists") +} + +fn binary() -> Command { + Command::cargo_bin("compact-lint").expect("the binary is built alongside this test") +} + +/// Copies the case's config and `before/` tree into a fresh temporary directory. +fn work(name: &str) -> TempDir { + let directory = TempDir::new().expect("a temporary directory is available"); + let root = case(name); + + std::fs::copy( + root.join("compact-lint.toml"), + directory.path().join("compact-lint.toml"), + ) + .expect("the case has a config"); + copy_tree(&root.join("before"), directory.path()); + + directory +} + +fn copy_tree(from: &Path, to: &Path) { + for entry in std::fs::read_dir(from).expect("the tree is readable") { + let entry = entry.expect("the entry is readable"); + let target = to.join(entry.file_name()); + if entry.path().is_dir() { + std::fs::create_dir_all(&target).expect("the target is writable"); + copy_tree(&entry.path(), &target); + } else { + std::fs::copy(entry.path(), &target).expect("the file is copyable"); + } + } +} + +/// Every `.compact` file under `root`, keyed by its path relative to it. +fn sources(root: &Path) -> BTreeMap> { + let mut found = BTreeMap::new(); + collect(root, root, &mut found); + found +} + +fn collect(root: &Path, directory: &Path, out: &mut BTreeMap>) { + for entry in std::fs::read_dir(directory).expect("the tree is readable") { + let entry = entry.expect("the entry is readable"); + let path = entry.path(); + if path.is_dir() { + collect(root, &path, out); + } else if path + .extension() + .is_some_and(|extension| extension == "compact") + { + let relative = path.strip_prefix(root).expect("the path is under the root"); + out.insert( + relative.to_owned(), + std::fs::read(&path).expect("the source is readable"), + ); + } + } +} + +fn expected(name: &str) -> String { + std::fs::read_to_string(case(name).join("expected.txt")).expect("the case has an expectation") +} + +/// Runs a subcommand inside `directory` and returns its exit code and stdout. +fn run(directory: &Path, args: &[&str]) -> (i32, String) { + let output = binary() + .current_dir(directory) + .args(args) + .output() + .expect("the binary ran"); + + ( + output.status.code().expect("the binary was not signalled"), + String::from_utf8(output.stdout).expect("the report is UTF-8"), + ) +} + +/// The whole contract of a fixable case: it fixes, it lands, it settles, it previews. +fn assert_case(name: &str) { + let directory = work(name); + let root = directory.path(); + + let (code, stdout) = run(root, &["fix"]); + assert_eq!(stdout, expected(name), "stdout for case {name}"); + assert_eq!(code, 0, "exit code for case {name}"); + assert_eq!( + sources(root), + sources(&case(name).join("after")), + "rewritten sources for case {name}" + ); + + let (code, stdout) = run(root, &["check", "--no-format"]); + assert_eq!(stdout, "", "check is clean after fixing case {name}"); + assert_eq!(code, 0, "check exit code after fixing case {name}"); + + let (code, stdout) = run(root, &["fix"]); + assert_eq!(stdout, "", "a second fix on case {name} edits nothing"); + assert_eq!(code, 0, "second fix exit code for case {name}"); + + let preview = work(name); + let before = sources(preview.path()); + let (code, stdout) = run(preview.path(), &["fix", "--dry-run"]); + assert_eq!(stdout, expected(name), "dry-run stdout for case {name}"); + assert_eq!(code, EXIT_EDITS, "dry-run exit code for case {name}"); + assert_eq!( + sources(preview.path()), + before, + "dry-run wrote to case {name}" + ); +} + +#[test] +fn undocumented_declarations_of_every_kind_take_a_skeleton() { + assert_case("skeleton"); +} + +#[test] +fn a_required_tag_goes_into_the_comment_that_lacks_it() { + assert_case("missing-tag"); +} + +#[test] +fn the_constraints_annotation_follows_the_description_block() { + assert_case("constraints"); +} + +#[test] +fn a_forbidden_tag_with_a_replacement_is_renamed_in_place() { + assert_case("rename"); +} + +#[test] +fn a_stale_module_tag_takes_the_module_name() { + assert_case("module-name"); +} + +#[test] +fn a_crlf_source_keeps_its_line_endings() { + assert_case("crlf"); +} + +#[test] +fn nothing_fixable_writes_nothing_and_exits_zero() { + let directory = work("unfixable"); + let root = directory.path(); + let before = sources(root); + + let (code, stdout) = run(root, &["fix"]); + assert_eq!(stdout, ""); + assert_eq!(code, 0); + assert_eq!(sources(root), before); + + let (code, stdout) = run(root, &["fix", "--dry-run"]); + assert_eq!(stdout, ""); + assert_eq!(code, 0); +} + +#[test] +fn the_summary_goes_to_stderr_and_the_edits_to_stdout() { + let directory = work("rename"); + let output = binary() + .current_dir(directory.path()) + .arg("fix") + .output() + .expect("the binary ran"); + + let stderr = String::from_utf8(output.stderr).expect("the summary is UTF-8"); + assert_eq!(stderr.trim_end(), "2 edits in 1 files"); + assert!( + String::from_utf8_lossy(&output.stdout).contains(": fix: "), + "the edits belong on stdout" + ); +} + +#[test] +fn an_explicit_config_replaces_the_one_beside_the_sources() { + let directory = work("rename"); + std::fs::remove_file(directory.path().join("compact-lint.toml")) + .expect("the copied config is removable"); + + let config = case("rename").join("compact-lint.toml"); + let output = binary() + .current_dir(directory.path()) + .args(["fix", "--config"]) + .arg(&config) + .output() + .expect("the binary ran"); + + assert_eq!( + String::from_utf8(output.stdout).expect("the report is UTF-8"), + expected("rename") + ); +} + +#[test] +fn a_missing_path_exits_two() { + let directory = work("rename"); + let (code, _) = run(directory.path(), &["fix", "Nope.compact"]); + + assert_eq!(code, 2); +} diff --git a/crates/compact-lint/tests/fixtures/fix-constraints/after/Unmeasured.compact b/crates/compact-lint/tests/fixtures/fix-constraints/after/Unmeasured.compact new file mode 100644 index 0000000..6c55524 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-constraints/after/Unmeasured.compact @@ -0,0 +1,37 @@ +/** + * @module Unmeasured + * @description A module whose exported circuits are unmeasured. + */ +export module Unmeasured { + /** + * @description Bumps the counter. + * + * @constraints k=?, rows=? + */ + export circuit bump(): [] { } + + /** + * @description Sends a coin + * to the recipient named below. + * + * @constraints k=?, rows=? + * + * @param {Bytes<32>} to - The recipient. + * @returns {[]} Empty tuple. + */ + export circuit send(to: Bytes<32>): [] { } + + /** + * @constraints k=?, rows=? + * + * @notice The annotation goes to the top, since there is no description. + */ + export circuit run(): [] { } + + /** + * @description Pure circuits are never annotated. + */ + export pure circuit double(value: Uint<8>): Uint<16> { + return value as Uint<16> * 2; + } +} diff --git a/crates/compact-lint/tests/fixtures/fix-constraints/before/Unmeasured.compact b/crates/compact-lint/tests/fixtures/fix-constraints/before/Unmeasured.compact new file mode 100644 index 0000000..ea9ffde --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-constraints/before/Unmeasured.compact @@ -0,0 +1,31 @@ +/** + * @module Unmeasured + * @description A module whose exported circuits are unmeasured. + */ +export module Unmeasured { + /** + * @description Bumps the counter. + */ + export circuit bump(): [] { } + + /** + * @description Sends a coin + * to the recipient named below. + * + * @param {Bytes<32>} to - The recipient. + * @returns {[]} Empty tuple. + */ + export circuit send(to: Bytes<32>): [] { } + + /** + * @notice The annotation goes to the top, since there is no description. + */ + export circuit run(): [] { } + + /** + * @description Pure circuits are never annotated. + */ + export pure circuit double(value: Uint<8>): Uint<16> { + return value as Uint<16> * 2; + } +} diff --git a/crates/compact-lint/tests/fixtures/fix-constraints/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-constraints/compact-lint.toml new file mode 100644 index 0000000..832f4ee --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-constraints/compact-lint.toml @@ -0,0 +1,13 @@ +version = 1 +include = ["**/*.compact"] + +[constraints] +tag = "@constraints" + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-constraints/expected.txt b/crates/compact-lint/tests/fixtures/fix-constraints/expected.txt new file mode 100644 index 0000000..9b49823 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-constraints/expected.txt @@ -0,0 +1,3 @@ +Unmeasured.compact:9:3: fix: inserted @constraints k=?, rows=? +Unmeasured.compact:18:3: fix: inserted @constraints k=?, rows=? +Unmeasured.compact:23:3: fix: inserted @constraints k=?, rows=? diff --git a/crates/compact-lint/tests/fixtures/fix-crlf/after/Windows.compact b/crates/compact-lint/tests/fixtures/fix-crlf/after/Windows.compact new file mode 100644 index 0000000..d1f3118 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-crlf/after/Windows.compact @@ -0,0 +1,19 @@ +/** + * @module Windows + * @description A module written with Windows line endings. + */ +export module Windows { + /** + * @description TODO + */ + export ledger _count: Counter; + + /** + * @description Bumps the counter. + * + * @constraints k=?, rows=? + * + * @returns {[]} - Empty tuple. + */ + export circuit bump(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-crlf/before/Windows.compact b/crates/compact-lint/tests/fixtures/fix-crlf/before/Windows.compact new file mode 100644 index 0000000..2ba9860 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-crlf/before/Windows.compact @@ -0,0 +1,13 @@ +/** + * @module Windows + * @description A module written with Windows line endings. + */ +export module Windows { + export ledger _count: Counter; + + /** + * @description Bumps the counter. + * @return {[]} - Empty tuple. + */ + export circuit bump(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-crlf/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-crlf/compact-lint.toml new file mode 100644 index 0000000..5e3068c --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-crlf/compact-lint.toml @@ -0,0 +1,18 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] +rename = { "@return" = "@returns" } + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-crlf/expected.txt b/crates/compact-lint/tests/fixtures/fix-crlf/expected.txt new file mode 100644 index 0000000..994f2b4 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-crlf/expected.txt @@ -0,0 +1,3 @@ +Windows.compact:6:3: fix: inserted doc skeleton for ledger `_count` +Windows.compact:10:6: fix: renamed @return to @returns +Windows.compact:12:3: fix: inserted @constraints k=?, rows=? diff --git a/crates/compact-lint/tests/fixtures/fix-missing-tag/after/Untagged.compact b/crates/compact-lint/tests/fixtures/fix-missing-tag/after/Untagged.compact new file mode 100644 index 0000000..5f112de --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-missing-tag/after/Untagged.compact @@ -0,0 +1,25 @@ +/** + * @module Untagged + * @description TODO + */ +export module Untagged { + /** + * @description Approved accounts. Absent means not allowed. + */ + export ledger _allowed: Set>; + + /** + * @description The circuit has no description either. + * @constraints k=6, rows=28 + */ + export circuit run(): [] { } + + /** + * @description TODO + * + * @constraints k=?, rows=? + * + * @param {Uint<8>} value - The value to store. + */ + export circuit store(value: Uint<8>): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-missing-tag/before/Untagged.compact b/crates/compact-lint/tests/fixtures/fix-missing-tag/before/Untagged.compact new file mode 100644 index 0000000..49ef079 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-missing-tag/before/Untagged.compact @@ -0,0 +1,18 @@ +/** + * @module Untagged + */ +export module Untagged { + /** Approved accounts. Absent means not allowed. */ + export ledger _allowed: Set>; + + /** + * The circuit has no description either. + * @constraints k=6, rows=28 + */ + export circuit run(): [] { } + + /** + * @param {Uint<8>} value - The value to store. + */ + export circuit store(value: Uint<8>): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-missing-tag/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-missing-tag/compact-lint.toml new file mode 100644 index 0000000..3af7b21 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-missing-tag/compact-lint.toml @@ -0,0 +1,14 @@ +version = 1 +include = ["**/*.compact"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-missing-tag/expected.txt b/crates/compact-lint/tests/fixtures/fix-missing-tag/expected.txt new file mode 100644 index 0000000..a33b4e2 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-missing-tag/expected.txt @@ -0,0 +1,5 @@ +Untagged.compact:4:1: fix: inserted @description +Untagged.compact:6:3: fix: inserted @description +Untagged.compact:12:3: fix: inserted @description +Untagged.compact:17:3: fix: inserted @constraints k=?, rows=? +Untagged.compact:17:3: fix: inserted @description diff --git a/crates/compact-lint/tests/fixtures/fix-module-name/after/Renamed.compact b/crates/compact-lint/tests/fixtures/fix-module-name/after/Renamed.compact new file mode 100644 index 0000000..4e467b1 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-module-name/after/Renamed.compact @@ -0,0 +1,12 @@ +/** + * @module Renamed (archived) + * @description The tag still names the module's old name. + */ +export module Renamed { + /** + * @module Nested + * @description The inner tag names nothing that exists. + */ + export module Nested { + } +} diff --git a/crates/compact-lint/tests/fixtures/fix-module-name/before/Renamed.compact b/crates/compact-lint/tests/fixtures/fix-module-name/before/Renamed.compact new file mode 100644 index 0000000..2ac3362 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-module-name/before/Renamed.compact @@ -0,0 +1,12 @@ +/** + * @module Stale (archived) + * @description The tag still names the module's old name. + */ +export module Renamed { + /** + * @module Inner. + * @description The inner tag names nothing that exists. + */ + export module Nested { + } +} diff --git a/crates/compact-lint/tests/fixtures/fix-module-name/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-module-name/compact-lint.toml new file mode 100644 index 0000000..d404f25 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-module-name/compact-lint.toml @@ -0,0 +1,8 @@ +version = 1 +include = ["**/*.compact"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-module-name/expected.txt b/crates/compact-lint/tests/fixtures/fix-module-name/expected.txt new file mode 100644 index 0000000..5e9811a --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-module-name/expected.txt @@ -0,0 +1,2 @@ +Renamed.compact:2:4: fix: set @module to `Renamed` +Renamed.compact:7:6: fix: set @module to `Nested` diff --git a/crates/compact-lint/tests/fixtures/fix-rename/after/Returns.compact b/crates/compact-lint/tests/fixtures/fix-rename/after/Returns.compact new file mode 100644 index 0000000..21cbe5d --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename/after/Returns.compact @@ -0,0 +1,19 @@ +/** + * @module Returns + * @description A module that still uses the old return tag. + */ +export module Returns { + /** + * @description Does nothing. + * @constraints k=6, rows=28 + * @returns {[]} - Empty tuple. + */ + export circuit run(): [] { } + + /** + * @description Not exported, so its docs are optional, but the tag is not. + * @returns {[]} - Empty tuple. + * @returns {[]} - The new spelling is left alone. + */ + circuit helper(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-rename/before/Returns.compact b/crates/compact-lint/tests/fixtures/fix-rename/before/Returns.compact new file mode 100644 index 0000000..8c8fc7a --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename/before/Returns.compact @@ -0,0 +1,19 @@ +/** + * @module Returns + * @description A module that still uses the old return tag. + */ +export module Returns { + /** + * @description Does nothing. + * @constraints k=6, rows=28 + * @return {[]} - Empty tuple. + */ + export circuit run(): [] { } + + /** + * @description Not exported, so its docs are optional, but the tag is not. + * @return {[]} - Empty tuple. + * @returns {[]} - The new spelling is left alone. + */ + circuit helper(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-rename/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-rename/compact-lint.toml new file mode 100644 index 0000000..ad6ecc5 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename/compact-lint.toml @@ -0,0 +1,15 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] +rename = { "@return" = "@returns" } + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-rename/expected.txt b/crates/compact-lint/tests/fixtures/fix-rename/expected.txt new file mode 100644 index 0000000..50c5c2a --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename/expected.txt @@ -0,0 +1,2 @@ +Returns.compact:9:6: fix: renamed @return to @returns +Returns.compact:15:6: fix: renamed @return to @returns diff --git a/crates/compact-lint/tests/fixtures/fix-skeleton/after/Bare.compact b/crates/compact-lint/tests/fixtures/fix-skeleton/after/Bare.compact new file mode 100644 index 0000000..77abb21 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-skeleton/after/Bare.compact @@ -0,0 +1,59 @@ +pragma language_version >= 0.23.0; + +/** + * @module Bare + * @description TODO + */ +export module Bare { + import CompactStandardLibrary; + + /** + * @description TODO + */ + export ledger _count: Counter; + + /** + * @description TODO + */ + witness wit_Secret(): Bytes<32>; + + /** + * @description TODO + */ + export struct Reading { + value: Uint<64> + } + + /** + * @description TODO + */ + export enum Direction { + Up, + Down + } + + /** + * TODO + */ + export type Alias = Bytes<32>; + + constructor() { + _count.resetToDefault(); + } + + /** + * @description TODO + */ + export pure circuit double(value: Uint<8>): Uint<16> { + return value as Uint<16> * 2; + } + + /** + * @description TODO + * + * @constraints k=?, rows=? + */ + export circuit bump(): [] { + _count.increment(1); + } +} diff --git a/crates/compact-lint/tests/fixtures/fix-skeleton/before/Bare.compact b/crates/compact-lint/tests/fixtures/fix-skeleton/before/Bare.compact new file mode 100644 index 0000000..9a88c83 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-skeleton/before/Bare.compact @@ -0,0 +1,32 @@ +pragma language_version >= 0.23.0; + +export module Bare { + import CompactStandardLibrary; + + export ledger _count: Counter; + + witness wit_Secret(): Bytes<32>; + + export struct Reading { + value: Uint<64> + } + + export enum Direction { + Up, + Down + } + + export type Alias = Bytes<32>; + + constructor() { + _count.resetToDefault(); + } + + export pure circuit double(value: Uint<8>): Uint<16> { + return value as Uint<16> * 2; + } + + export circuit bump(): [] { + _count.increment(1); + } +} diff --git a/crates/compact-lint/tests/fixtures/fix-skeleton/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-skeleton/compact-lint.toml new file mode 100644 index 0000000..5fd3408 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-skeleton/compact-lint.toml @@ -0,0 +1,31 @@ +version = 1 +include = ["**/*.compact"] + +[fix] +placeholder = "TODO" + +[tags] +forbid = ["@return"] +rename = { "@return" = "@returns" } + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.ledger] +tags = ["@description"] + +[kinds.witness] +docs = "all" +tags = ["@description"] + +[kinds.struct] +tags = ["@description"] + +[kinds.enum] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-skeleton/expected.txt b/crates/compact-lint/tests/fixtures/fix-skeleton/expected.txt new file mode 100644 index 0000000..8dadb68 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-skeleton/expected.txt @@ -0,0 +1,8 @@ +Bare.compact:3:1: fix: inserted doc skeleton for module `Bare` +Bare.compact:6:3: fix: inserted doc skeleton for ledger `_count` +Bare.compact:8:3: fix: inserted doc skeleton for witness `wit_Secret` +Bare.compact:10:3: fix: inserted doc skeleton for struct `Reading` +Bare.compact:14:3: fix: inserted doc skeleton for enum `Direction` +Bare.compact:19:3: fix: inserted doc skeleton for type `Alias` +Bare.compact:25:3: fix: inserted doc skeleton for circuit `double` +Bare.compact:29:3: fix: inserted doc skeleton for circuit `bump` diff --git a/crates/compact-lint/tests/fixtures/fix-unfixable/after/Malformed.compact b/crates/compact-lint/tests/fixtures/fix-unfixable/after/Malformed.compact new file mode 100644 index 0000000..d7039c8 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-unfixable/after/Malformed.compact @@ -0,0 +1,12 @@ +/** + * @module Malformed + * @description A module whose annotations a rewrite cannot repair. + */ +export module Malformed { + /** + * @description The comma is missing, and nothing can guess the measurement. + * @constraints k=10 rows=626 + * @throws {Error} The forbidden tag has no replacement. + */ + export circuit first(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-unfixable/before/Malformed.compact b/crates/compact-lint/tests/fixtures/fix-unfixable/before/Malformed.compact new file mode 100644 index 0000000..d7039c8 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-unfixable/before/Malformed.compact @@ -0,0 +1,12 @@ +/** + * @module Malformed + * @description A module whose annotations a rewrite cannot repair. + */ +export module Malformed { + /** + * @description The comma is missing, and nothing can guess the measurement. + * @constraints k=10 rows=626 + * @throws {Error} The forbidden tag has no replacement. + */ + export circuit first(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-unfixable/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-unfixable/compact-lint.toml new file mode 100644 index 0000000..9bb3ab8 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-unfixable/compact-lint.toml @@ -0,0 +1,15 @@ +version = 1 +include = ["**/*.compact"] + +# `@throws` is forbidden but has no replacement, so `fix` leaves it where it is. +[tags] +forbid = ["@throws"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description"] + +[kinds.constructor] +docs = "none" diff --git a/crates/compact-lint/tests/fixtures/fix-unfixable/expected.txt b/crates/compact-lint/tests/fixtures/fix-unfixable/expected.txt new file mode 100644 index 0000000..e69de29 From 46ef29a12ffb52bdbee32ffd7eb60270a790898a Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:36:45 +0200 Subject: [PATCH 05/14] ci(rust): stop persisting checkout credentials The default checkout leaves the job's GITHUB_TOKEN in .git/config, so any later step or build script in the job can read and reuse it. Nothing in this workflow talks to the remote after checkout, so opt out. --- .github/workflows/rust.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a3f227a..4775a02 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,6 +23,8 @@ jobs: - name: Check out code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install the pinned toolchain run: | From f06882b5a19ac08c8aa7d0921be47a4ab7279da3 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:37:25 +0200 Subject: [PATCH 06/14] docs(compact-tree-sitter): correct the name field claim Both the grammar header and the README said every declaration node carries a `name` field. Four do not, and a consumer that reads `name` unconditionally gets nothing back for them. --- crates/compact-tree-sitter/README.md | 15 +++++++++------ crates/compact-tree-sitter/grammar.js | 7 +++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/compact-tree-sitter/README.md b/crates/compact-tree-sitter/README.md index 50f0d70..f07c988 100644 --- a/crates/compact-tree-sitter/README.md +++ b/crates/compact-tree-sitter/README.md @@ -8,12 +8,15 @@ leaves everything else opaque. ## What it covers -- Every top-level and module-level declaration is a named node with a `name` - field: `pragma_declaration`, `include_declaration`, `import_declaration`, - `export_declaration`, `module_declaration`, `struct_declaration`, - `enum_declaration`, `contract_declaration`, `implements_declaration`, - `type_declaration`, `ledger_declaration`, `witness_declaration`, - `constructor_declaration`, `circuit_declaration`. +- Every top-level and module-level declaration is a named node, but the fields + differ per declaration. These carry a `name` field: `pragma_declaration`, + `import_declaration`, `module_declaration`, `struct_declaration`, + `enum_declaration`, `contract_declaration`, `type_declaration`, + `ledger_declaration`, `witness_declaration`, `circuit_declaration`. +- The exceptions: `include_declaration` carries `path`, + `implements_declaration` carries `type`, `constructor_declaration` has no + identifier, and `export_declaration` holds its exported names as bare + `identifier` children with no field. - `module_declaration` recurses: a `module_body` holds the same declarations. - Comments are named nodes in `extras`: `line_comment`, `block_comment`, `doc_comment`. diff --git a/crates/compact-tree-sitter/grammar.js b/crates/compact-tree-sitter/grammar.js index 55c2d25..2ffb901 100644 --- a/crates/compact-tree-sitter/grammar.js +++ b/crates/compact-tree-sitter/grammar.js @@ -2,8 +2,11 @@ // @ts-check // Declaration-level grammar for Compact (Midnight), language version 0.26. -// Every top-level and module-level declaration is a named node carrying a `name` -// field; bodies, parameter lists and statements stay opaque brace-balanced runs. +// Every top-level and module-level declaration is a named node, but the fields +// vary: most carry `name`, while `include_declaration` carries `path`, +// `implements_declaration` carries `type`, `constructor_declaration` has no +// identifier at all, and `export_declaration` holds bare `identifier` children. +// Bodies, parameter lists and statements stay opaque brace-balanced runs. /** * Comma-separated list with an optional trailing comma. From e2b329d69f8e31dac0772edc871d91b67f5e37bb Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:38:16 +0200 Subject: [PATCH 07/14] test(compact-tree-sitter): skip symlinks in the corpus walk A symlinked directory inside the corpus makes the recursion loop, and a symlinked file is walked twice. Skip both. --- crates/compact-tree-sitter/tests/corpus.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/compact-tree-sitter/tests/corpus.rs b/crates/compact-tree-sitter/tests/corpus.rs index 7ff16ed..f2ab924 100644 --- a/crates/compact-tree-sitter/tests/corpus.rs +++ b/crates/compact-tree-sitter/tests/corpus.rs @@ -41,6 +41,11 @@ fn collect_compact_files(dir: &Path, out: &mut Vec) { let path = entry.path(); let name = entry.file_name(); + // A symlinked directory can point back up the tree and loop forever. + if path.is_symlink() { + continue; + } + if path.is_dir() { if !SKIPPED_DIRS.iter().any(|skipped| name == *skipped) { collect_compact_files(&path, out); From 334b4a98a8440e290f2dad8ee1b60efe5722939a Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:40:49 +0200 Subject: [PATCH 08/14] fix(compact-lint): resolve explicit paths against cwd Discovery matches exclude globs against paths relative to the config's directory, but an explicit CLI path stayed relative to the process cwd. Running `check archive` from a subdirectory produced `archive/X.compact`, which never matched an exclude written as `contracts/archive/**`, so excluded files were silently linted. Anchor relative CLI paths at cwd first; the display step still strips cwd, so the report is unchanged. --- crates/compact-lint/src/check.rs | 38 ++++++++++++++++++- crates/compact-lint/tests/check.rs | 15 ++++++++ .../exclude-explicit-dir/compact-lint.toml | 9 +++++ .../contracts/archive/Old.compact | 7 ++++ .../contracts/live/Live.compact | 7 ++++ .../exclude-explicit-dir/expected.txt | 1 + 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 crates/compact-lint/tests/fixtures/exclude-explicit-dir/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/archive/Old.compact create mode 100644 crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/live/Live.compact create mode 100644 crates/compact-lint/tests/fixtures/exclude-explicit-dir/expected.txt diff --git a/crates/compact-lint/src/check.rs b/crates/compact-lint/src/check.rs index 2f2d2a8..76ee393 100644 --- a/crates/compact-lint/src/check.rs +++ b/crates/compact-lint/src/check.rs @@ -67,7 +67,8 @@ pub fn run(options: &Options, cwd: &Path) -> Result { let include = config.include_set(source)?; discover::from_globs(&base, &include, &exclude)? } else { - discover::from_paths(&options.paths, &base, &exclude)? + let paths = absolute_paths(&options.paths, cwd); + discover::from_paths(&paths, &base, &exclude)? }; let files: Vec = files.iter().map(|path| display_path(path, cwd)).collect(); @@ -111,7 +112,42 @@ fn resolve_config( } } +/// Discovery matches the exclude globs against paths relative to the config's +/// directory, so a CLI path has to be anchored at `cwd` first: a bare `archive` +/// given from a subdirectory would otherwise never match `contracts/archive/**`. +fn absolute_paths(paths: &[PathBuf], cwd: &Path) -> Vec { + paths + .iter() + .map(|path| { + if path.is_absolute() { + path.clone() + } else { + cwd.join(path) + } + }) + .collect() +} + /// Paths under the working directory print relative to it; anything else prints as is. fn display_path(path: &Path, cwd: &Path) -> PathBuf { path.strip_prefix(cwd).unwrap_or(path).to_owned() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_relative_path_is_anchored_at_the_working_directory() { + let resolved = absolute_paths(&[PathBuf::from("archive")], Path::new("/repo/contracts")); + + assert_eq!(resolved, vec![PathBuf::from("/repo/contracts/archive")]); + } + + #[test] + fn an_absolute_path_is_left_alone() { + let resolved = absolute_paths(&[PathBuf::from("/elsewhere/X.compact")], Path::new("/repo")); + + assert_eq!(resolved, vec![PathBuf::from("/elsewhere/X.compact")]); + } +} diff --git a/crates/compact-lint/tests/check.rs b/crates/compact-lint/tests/check.rs index 452c09d..c4fc99f 100644 --- a/crates/compact-lint/tests/check.rs +++ b/crates/compact-lint/tests/check.rs @@ -115,6 +115,21 @@ fn an_explicit_path_replaces_the_include_globs() { ); } +#[test] +fn an_exclude_glob_applies_to_an_explicit_path_given_from_a_subdirectory() { + let output = binary() + .current_dir(case("exclude-explicit-dir").join("contracts")) + .args(["check", "--no-format", "live", "archive"]) + .output() + .expect("the binary ran"); + + assert_eq!( + String::from_utf8(output.stdout).expect("the report is UTF-8"), + expected("exclude-explicit-dir") + ); + assert_eq!(output.status.code(), Some(EXIT_FINDINGS)); +} + #[test] fn an_explicit_config_anchors_discovery_at_the_working_directory() { let config = case("clean").join("compact-lint.toml"); diff --git a/crates/compact-lint/tests/fixtures/exclude-explicit-dir/compact-lint.toml b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/compact-lint.toml new file mode 100644 index 0000000..02f0d72 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/compact-lint.toml @@ -0,0 +1,9 @@ +version = 1 +include = ["contracts/**/*.compact"] +exclude = ["contracts/archive/**"] + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.ledger] +tags = ["@description"] diff --git a/crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/archive/Old.compact b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/archive/Old.compact new file mode 100644 index 0000000..57a9386 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/archive/Old.compact @@ -0,0 +1,7 @@ +/** + * @module Old + * @description A retired module the config excludes. + */ +export module Old { + export ledger _owner: Bytes<32>; +} diff --git a/crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/live/Live.compact b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/live/Live.compact new file mode 100644 index 0000000..10781e5 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/contracts/live/Live.compact @@ -0,0 +1,7 @@ +/** + * @module Live + * @description A maintained module. + */ +export module Live { + export ledger _owner: Bytes<32>; +} diff --git a/crates/compact-lint/tests/fixtures/exclude-explicit-dir/expected.txt b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/expected.txt new file mode 100644 index 0000000..31799ef --- /dev/null +++ b/crates/compact-lint/tests/fixtures/exclude-explicit-dir/expected.txt @@ -0,0 +1 @@ +live/Live.compact:6:3: missing-doc: ledger `_owner` has no doc comment From 93448137ec298676f483cf6fd806dede58b0e5ea Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:41:17 +0200 Subject: [PATCH 09/14] refactor(compact-lint): mark Tag::new must_use A constructor with no side effects; matches the rest of Tag. --- crates/compact-lint/src/doc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/compact-lint/src/doc.rs b/crates/compact-lint/src/doc.rs index 685007f..4da3200 100644 --- a/crates/compact-lint/src/doc.rs +++ b/crates/compact-lint/src/doc.rs @@ -10,6 +10,7 @@ use serde::Deserialize; pub struct Tag(String); impl Tag { + #[must_use] pub fn new(name: impl Into) -> Self { Self(name.into()) } From 3ade154d446b26eaf9d9fb40e832404ef881bce7 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:42:15 +0200 Subject: [PATCH 10/14] fix(compact-lint): check constraints under docs = none The constraints rule ran after the per-kind docs gate, so kinds.circuit.docs = "none" silently disabled it. The rule is fixed rather than configurable, so it now runs before the gate; only the required-tag loop stays behind it. --- crates/compact-lint/src/rules.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/compact-lint/src/rules.rs b/crates/compact-lint/src/rules.rs index 9bc0bf7..4e8cefc 100644 --- a/crates/compact-lint/src/rules.rs +++ b/crates/compact-lint/src/rules.rs @@ -146,6 +146,9 @@ fn check_declaration( check_module_name(declaration, display, findings); } + // The constraints rule is fixed, so it stays outside the per-kind docs policy. + check_constraints(declaration, display, config, strict, findings); + if !requires_docs { return; } @@ -160,8 +163,6 @@ fn check_declaration( )); } } - - check_constraints(declaration, display, config, strict, findings); } /// `@module ` must name the module it documents. @@ -341,6 +342,19 @@ mod tests { ); } + #[test] + fn the_constraints_rule_survives_docs_none() { + let source = + "/** @description Bumps. */\nexport circuit bump(): [] { count.increment(1); }\n"; + let mut config = circuit_config(); + config.kinds.circuit.docs = DocsPolicy::None; + + assert_eq!( + findings(source, &config, false), + ["a.compact:2:1: missing-constraints: circuit `bump` doc comment has no @constraints"] + ); + } + #[test] fn a_placeholder_is_a_finding_only_under_strict() { let source = "/**\n * @description Bumps.\n * @constraints k=?, rows=?\n */\nexport circuit bump(): [] { }\n"; From 0acfa84bb211bece6ff194329ba1569811aeae57 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:45:39 +0200 Subject: [PATCH 11/14] fix(compact-lint): skip inserting a tag a rename supplies A config that forbids @return, renames it to @returns and requires @returns produced both a rename edit and an insert edit on the same doc comment, so fix left the comment with @returns twice. Drop the insert when a rename in that comment already yields the missing tag. --- crates/compact-lint/src/fix.rs | 26 +++++++++++++++++-- crates/compact-lint/tests/fix.rs | 12 +++++++++ .../after/Supplied.compact | 12 +++++++++ .../before/Supplied.compact | 12 +++++++++ .../fix-rename-supplies-tag/compact-lint.toml | 12 +++++++++ .../fix-rename-supplies-tag/expected.txt | 1 + 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/after/Supplied.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/before/Supplied.compact create mode 100644 crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/compact-lint.toml create mode 100644 crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/expected.txt diff --git a/crates/compact-lint/src/fix.rs b/crates/compact-lint/src/fix.rs index 88e42bd..a2d21c1 100644 --- a/crates/compact-lint/src/fix.rs +++ b/crates/compact-lint/src/fix.rs @@ -87,9 +87,10 @@ pub fn run(options: &Options, cwd: &Path) -> Result { })?; let newline = newline_of(&source); - let edits: Vec = linter - .issues(path, &source, &target.config, false)? + let issues = linter.issues(path, &source, &target.config, false)?; + let edits: Vec = issues .iter() + .filter(|issue| !supplied_by_a_rename(issue, &issues, &target.config)) .filter_map(|issue| edit_for(issue, &target.config, newline)) .collect(); @@ -109,6 +110,27 @@ pub fn run(options: &Options, cwd: &Path) -> Result { Ok(Outcome { files }) } +/// Whether a forbidden tag in the same doc comment is renamed to the tag this +/// `missing-tag` issue reports. +/// +/// Inserting it as well would leave the comment carrying the tag twice. +fn supplied_by_a_rename(issue: &Issue, issues: &[Issue], config: &Config) -> bool { + let Issue::MissingTag { tag, doc, .. } = issue else { + return false; + }; + + issues.iter().any(|other| { + matches!( + other, + Issue::ForbiddenTag { + tag: forbidden, + doc: other_doc, + .. + } if other_doc == doc && config.rename_of(forbidden) == Some(tag) + ) + }) +} + /// The repair for one issue, or `None` where the rule has no safe fix. /// /// `constraints-format` and `constraints-placeholder` need a measurement a rewrite diff --git a/crates/compact-lint/tests/fix.rs b/crates/compact-lint/tests/fix.rs index fae2b52..c8b5d9e 100644 --- a/crates/compact-lint/tests/fix.rs +++ b/crates/compact-lint/tests/fix.rs @@ -151,6 +151,18 @@ fn a_forbidden_tag_with_a_replacement_is_renamed_in_place() { assert_case("rename"); } +#[test] +fn a_rename_that_supplies_a_required_tag_suppresses_the_insert() { + assert_case("rename-supplies-tag"); + + let directory = work("rename-supplies-tag"); + run(directory.path(), &["fix"]); + let fixed = std::fs::read_to_string(directory.path().join("Supplied.compact")) + .expect("the fixed source is readable"); + + assert_eq!(fixed.matches("@returns").count(), 1, "{fixed}"); +} + #[test] fn a_stale_module_tag_takes_the_module_name() { assert_case("module-name"); diff --git a/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/after/Supplied.compact b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/after/Supplied.compact new file mode 100644 index 0000000..a405a3b --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/after/Supplied.compact @@ -0,0 +1,12 @@ +/** + * @module Supplied + * @description A module whose circuit still uses the old return tag. + */ +export module Supplied { + /** + * @description Does nothing. + * @constraints k=6, rows=28 + * @returns {[]} - Empty tuple. + */ + export circuit run(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/before/Supplied.compact b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/before/Supplied.compact new file mode 100644 index 0000000..fa377d5 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/before/Supplied.compact @@ -0,0 +1,12 @@ +/** + * @module Supplied + * @description A module whose circuit still uses the old return tag. + */ +export module Supplied { + /** + * @description Does nothing. + * @constraints k=6, rows=28 + * @return {[]} - Empty tuple. + */ + export circuit run(): [] { } +} diff --git a/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/compact-lint.toml b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/compact-lint.toml new file mode 100644 index 0000000..815000c --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/compact-lint.toml @@ -0,0 +1,12 @@ +version = 1 +include = ["**/*.compact"] + +[tags] +forbid = ["@return"] +rename = { "@return" = "@returns" } + +[kinds.module] +tags = ["@module", "@description"] + +[kinds.circuit] +tags = ["@description", "@returns"] diff --git a/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/expected.txt b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/expected.txt new file mode 100644 index 0000000..9a3fad0 --- /dev/null +++ b/crates/compact-lint/tests/fixtures/fix-rename-supplies-tag/expected.txt @@ -0,0 +1 @@ +Supplied.compact:9:6: fix: renamed @return to @returns From e64fef967070e64e874321ccce9473cdebd0dbff Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:46:59 +0200 Subject: [PATCH 12/14] fix(compact-lint): validate the fix placeholder The placeholder is written onto a `*` line of a generated doc comment. An empty one produced a blank tag line, a multi-line one broke the comment layout, and one containing `*/` closed the comment early and corrupted the source. Reject all three at load time instead. --- crates/compact-lint/src/config.rs | 63 ++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/compact-lint/src/config.rs b/crates/compact-lint/src/config.rs index 002c6d1..6b68d94 100644 --- a/crates/compact-lint/src/config.rs +++ b/crates/compact-lint/src/config.rs @@ -38,6 +38,12 @@ pub enum ConfigError { "config {path} lists tag {tag:?}; tags are spelled with their `@`, like `@description`" )] Tag { path: PathBuf, tag: String }, + #[error("config {path} has an invalid fix.placeholder {placeholder:?}: {reason}")] + Placeholder { + path: PathBuf, + placeholder: String, + reason: &'static str, + }, #[error("config {path} has an invalid glob {pattern:?}")] Glob { path: PathBuf, @@ -198,6 +204,14 @@ impl Config { }); } + if let Some(reason) = placeholder_defect(&config.fix.placeholder) { + return Err(ConfigError::Placeholder { + path: path.to_owned(), + placeholder: config.fix.placeholder.clone(), + reason, + }); + } + Ok(config) } @@ -245,6 +259,20 @@ impl Config { } } +/// Why a placeholder cannot go on a `*` line of a doc comment, or `None` when it can. +fn placeholder_defect(placeholder: &str) -> Option<&'static str> { + if placeholder.trim().is_empty() { + return Some("it is empty"); + } + if placeholder.contains(['\n', '\r']) { + return Some("it spans more than one line"); + } + if placeholder.contains("*/") { + return Some("`*/` would close the comment early"); + } + None +} + fn glob_set(patterns: &[String], path: &Path) -> Result { let mut builder = GlobSetBuilder::new(); for pattern in patterns { @@ -276,9 +304,21 @@ pub fn discover(start: &Path) -> Option { #[cfg(test)] mod tests { - use super::{Config, DocsPolicy, SUPPORTED_VERSION}; + use super::{CONFIG_FILE_NAME, Config, DocsPolicy, SUPPORTED_VERSION}; use crate::doc::Tag; use crate::model::DeclKind; + use tempfile::TempDir; + + /// Loads `snippet` as a config file and returns the rejection it produced. + fn rejection(snippet: &str) -> String { + let directory = TempDir::new().expect("a temporary directory is available"); + let path = directory.path().join(CONFIG_FILE_NAME); + std::fs::write(&path, snippet).expect("the config is writable"); + + Config::load(&path) + .expect_err("the config is invalid") + .to_string() + } #[test] fn defaults_require_docs_on_exported_declarations_only() { @@ -360,6 +400,27 @@ mod tests { assert_eq!(config.fix.placeholder, "FIXME"); } + #[test] + fn an_empty_fix_placeholder_is_rejected() { + let error = rejection("[fix]\nplaceholder = \" \"\n"); + + assert!(error.contains("is empty"), "{error}"); + } + + #[test] + fn a_multi_line_fix_placeholder_is_rejected() { + let error = rejection("[fix]\nplaceholder = \"TO\\nDO\"\n"); + + assert!(error.contains("more than one line"), "{error}"); + } + + #[test] + fn a_fix_placeholder_closing_the_comment_is_rejected() { + let error = rejection("[fix]\nplaceholder = \"TODO */\"\n"); + + assert!(error.contains("close the comment"), "{error}"); + } + #[test] fn an_unknown_kind_is_rejected() { let error = From c4737e6b95bbd5ccc9e4b6dfd1d3f09a2d011f77 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:48:02 +0200 Subject: [PATCH 13/14] fix(compact-lint): validate rename against forbid A rename only fires from a forbidden-tag finding, so a key that tags.forbid does not list is dead config the author expects to work. A value that tags.forbid does list makes fix rewrite one finding into another, so a second run reports the replacement. Reject both at load time. --- crates/compact-lint/README.md | 7 +++--- crates/compact-lint/src/config.rs | 41 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/crates/compact-lint/README.md b/crates/compact-lint/README.md index 0c4f3b2..5f2cfd6 100644 --- a/crates/compact-lint/README.md +++ b/crates/compact-lint/README.md @@ -159,9 +159,10 @@ Defaults when no config file is found: `"all"` or `"none"` there. - `exclude` filters files found by walking a directory. A file named on the command line is always checked. -- `tags.rename` maps a forbidden tag to the tag `fix` writes in its place. Both spellings - are validated like every other tag. An unmapped forbidden tag is reported, never - rewritten. +- `tags.rename` maps a forbidden tag to the tag `fix` writes in its place. Every key + must appear in `tags.forbid` and no value may, or the config is rejected. Both + spellings are validated like every other tag. An unmapped forbidden tag is reported, + never rewritten. - `fix.placeholder` is the text `fix` writes where it has no value of its own. `examples/compact-contracts.toml` is the config for OpenZeppelin/compact-contracts. diff --git a/crates/compact-lint/src/config.rs b/crates/compact-lint/src/config.rs index 6b68d94..3bddff2 100644 --- a/crates/compact-lint/src/config.rs +++ b/crates/compact-lint/src/config.rs @@ -38,6 +38,14 @@ pub enum ConfigError { "config {path} lists tag {tag:?}; tags are spelled with their `@`, like `@description`" )] Tag { path: PathBuf, tag: String }, + #[error("config {path} renames {tag}, which tags.forbid does not list")] + RenameUnforbidden { path: PathBuf, tag: String }, + #[error("config {path} renames {from} to {to}, which tags.forbid also lists")] + RenameToForbidden { + path: PathBuf, + from: String, + to: String, + }, #[error("config {path} has an invalid fix.placeholder {placeholder:?}: {reason}")] Placeholder { path: PathBuf, @@ -204,6 +212,22 @@ impl Config { }); } + for (from, to) in &config.tags.rename { + if !config.tags.forbid.contains(from) { + return Err(ConfigError::RenameUnforbidden { + path: path.to_owned(), + tag: from.to_string(), + }); + } + if config.tags.forbid.contains(to) { + return Err(ConfigError::RenameToForbidden { + path: path.to_owned(), + from: from.to_string(), + to: to.to_string(), + }); + } + } + if let Some(reason) = placeholder_defect(&config.fix.placeholder) { return Err(ConfigError::Placeholder { path: path.to_owned(), @@ -400,6 +424,23 @@ mod tests { assert_eq!(config.fix.placeholder, "FIXME"); } + #[test] + fn a_rename_of_a_tag_that_is_not_forbidden_is_rejected() { + let error = rejection("[tags]\nrename = { \"@return\" = \"@returns\" }\n"); + + assert!(error.contains("@return"), "{error}"); + assert!(error.contains("tags.forbid does not list"), "{error}"); + } + + #[test] + fn a_rename_onto_a_forbidden_tag_is_rejected() { + let error = rejection( + "[tags]\nforbid = [\"@return\", \"@returns\"]\nrename = { \"@return\" = \"@returns\" }\n", + ); + + assert!(error.contains("tags.forbid also lists"), "{error}"); + } + #[test] fn an_empty_fix_placeholder_is_rejected() { let error = rejection("[fix]\nplaceholder = \" \"\n"); From aa06bbdcab7d57964f2fb70608351f86ca01cbc4 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 16 Sep 2026 06:49:23 +0200 Subject: [PATCH 14/14] fix(compact-lint): write through a unique temp file The staging file was `.compact.tmp` beside the source and written with fs::write, so a pre-existing file of that name was truncated, and the predictable name let anything else with write access to the directory plant one. Create a sibling `....tmp` exclusively instead, write through the handle, and remove it when a later step fails. --- crates/compact-lint/src/fix.rs | 77 +++++++++++++++++++++++++------- crates/compact-lint/tests/fix.rs | 15 +++++++ 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/crates/compact-lint/src/fix.rs b/crates/compact-lint/src/fix.rs index a2d21c1..4e44f5f 100644 --- a/crates/compact-lint/src/fix.rs +++ b/crates/compact-lint/src/fix.rs @@ -1,6 +1,10 @@ //! The `fix` subcommand: turn the fixable issues into edits and rewrite the files. +use std::ffi::{OsStr, OsString}; +use std::fs::{File, OpenOptions}; +use std::io::{ErrorKind, Write}; use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; @@ -308,27 +312,68 @@ fn skeleton( format!("{}{newline}{margin}", lines.join(newline)) } +/// How many unique names to try before giving up on the temporary file. +const TEMPORARY_ATTEMPTS: u32 = 8; + /// Writes through a sibling temporary file, so a failed write never truncates the source. fn write_atomically(path: &Path, text: &str) -> Result<(), FixError> { - let temporary = path.with_extension("compact.tmp"); - - std::fs::write(&temporary, text).map_err(|source| FixError::Write { - path: temporary.clone(), - source, - })?; - let permissions = std::fs::metadata(path) - .map_err(|source| FixError::Read { + let (temporary, mut handle) = create_temporary(path)?; + + let write = handle + .write_all(text.as_bytes()) + .and_then(|()| handle.sync_all()) + .and_then(|()| std::fs::metadata(path)) + .and_then(|metadata| std::fs::set_permissions(&temporary, metadata.permissions())) + .and_then(|()| std::fs::rename(&temporary, path)); + + write.map_err(|source| { + // The rename never happened, so the temporary file is ours to clean up. + let _ = std::fs::remove_file(&temporary); + FixError::Write { path: path.to_owned(), source, - })? - .permissions(); - std::fs::set_permissions(&temporary, permissions).map_err(|source| FixError::Write { - path: temporary.clone(), - source, - })?; - std::fs::rename(&temporary, path).map_err(|source| FixError::Write { + } + }) +} + +/// Creates a sibling temporary file under a name nothing else holds. +/// +/// A predictable name lets another process plant a file the write would truncate, so +/// the name carries the pid and a timestamp and the file is created exclusively. +fn create_temporary(path: &Path) -> Result<(PathBuf, File), FixError> { + let directory = path.parent().unwrap_or(Path::new(".")); + let stem = path.file_name().unwrap_or(OsStr::new("source")); + let pid = std::process::id(); + + let mut last = None; + for attempt in 0..TEMPORARY_ATTEMPTS { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |since| since.subsec_nanos()); + let mut name = OsString::from("."); + name.push(stem); + name.push(format!(".{pid}.{nanos}.{attempt}.tmp")); + let candidate = directory.join(name); + + match OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(handle) => return Ok((candidate, handle)), + Err(error) if error.kind() == ErrorKind::AlreadyExists => last = Some(error), + Err(source) => { + return Err(FixError::Write { + path: path.to_owned(), + source, + }); + } + } + } + + Err(FixError::Write { path: path.to_owned(), - source, + source: last.unwrap_or_else(|| ErrorKind::AlreadyExists.into()), }) } diff --git a/crates/compact-lint/tests/fix.rs b/crates/compact-lint/tests/fix.rs index c8b5d9e..a92aa1f 100644 --- a/crates/compact-lint/tests/fix.rs +++ b/crates/compact-lint/tests/fix.rs @@ -226,6 +226,21 @@ fn an_explicit_config_replaces_the_one_beside_the_sources() { ); } +#[test] +fn a_file_named_like_the_old_temporary_survives_a_fix() { + let directory = work("rename"); + let planted = directory.path().join("Returns.compact.tmp"); + std::fs::write(&planted, "not mine to truncate\n").expect("the plant is writable"); + + let (code, _) = run(directory.path(), &["fix"]); + + assert_eq!(code, 0); + assert_eq!( + std::fs::read_to_string(&planted).expect("the plant is still there"), + "not mine to truncate\n" + ); +} + #[test] fn a_missing_path_exits_two() { let directory = work("rename");