Compare commits

...

308 commits

Author SHA1 Message Date
Christian Clason de98c6c970 build(deps): bump rquickjs to v0.13.0
Some checks failed
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 17s
CI / check-wasm-stdlib (push) Failing after 15s
CI / checks (push) Has been cancelled
2026-09-09 01:25:17 +02:00
Christian Clason 8351896bea build(deps): cargo update
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 17s
CI / check-wasm-stdlib (push) Failing after 15s
2026-09-08 09:46:26 +02:00
Will Lillis 072f68c829 feat(generate)!: derive PartialEq + Eq on GenerateError
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 14s
Also use `IoError` more consistently throughout the rest of the project
where its straightforward to do so.

BREAKING CHANGE: Changes public error types for config, generate, and
loader crates.
2026-09-07 17:53:09 -04:00
dependabot[bot] badf2882c6 build(deps): bump the npm group across 1 directory with 5 updates
Bumps the npm group with 5 updates in the /lib/binding_web directory:

| Package | From | To |
| --- | --- | --- |
| [@types/emscripten](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/emscripten) | `1.41.5` | `1.41.6` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.4.1` |
| [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` |
| [tsx](https://github.com/privatenumber/tsx) | `4.23.12` | `4.23.13` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.68.0` | `8.69.0` |



Updates `@types/emscripten` from 1.41.5 to 1.41.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/emscripten)

Updates `@types/node` from 26.4.0 to 26.4.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.9.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0)

Updates `tsx` from 4.23.12 to 4.23.13
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.12...v4.23.13)

Updates `typescript-eslint` from 8.68.0 to 8.69.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/emscripten"
  dependency-version: 1.41.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 23:46:17 +02:00
dependabot[bot] 9c2c5714cd build(deps): bump cc from 1.4.4 to 1.4.5 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.4.4 to 1.4.5
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.4...cc-v1.4.5)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 23:45:04 +02:00
Will Lillis f430449ab9 fix(rust): address manual-readme lint
Some checks failed
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
CI / checks (push) Has been cancelled
Cargo automatically resolves this, so the `Cargo.toml` entries are
redundant.
2026-09-04 14:48:46 -04:00
Will Lillis 068db86ca9 fix(rust): address new assert-is-empty nightly lint
The resulting code is arguably uglier, but there is a strong argument
for the improved message when the assert trips.
2026-09-04 14:48:46 -04:00
cui fliter ee0c20b1f5
fix(cli)!: fix query test capture range matching across lines (#5917)
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 14s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 15s
Signed-off-by: cuishuang <imcusg@gmail.com>
BREAKING CHANGE: changes the signature of `assert_expected_captures`, which is exposed publicly when tree-sitter-cli is consumed as a library.
2026-09-03 23:25:21 -04:00
dependabot[bot] 702ba49a6a build(deps): bump @humanfs/node in /crates/cli/eslint
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 15s
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 16:35:52 +02:00
dependabot[bot] b9c5eb2b73 build(deps): bump @humanfs/node in /lib/binding_web
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 16:35:42 +02:00
Yudai Takada 351bd71e52
fix(lib): decode UTF-16 surrogate pairs with input endianness (#5912)
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 15s
Problem: Parsing UTF-16BE source containing supplementary-plane characters, such as `let emoji = "😀"`, decoded the emoji as two isolated surrogates on little-endian hosts, causing incorrect lexer lookahead and potentially shifted token boundaries.

Soluton: Fix the trailing surrogate byte-order conversion in both UTF-16LE and UTF-16BE decoders and adds a regression test for U+1F600.
2026-09-03 08:21:08 +02:00
dependabot[bot] c206ad1e6a build(deps): bump the npm group across 1 directory with 3 updates
Some checks failed
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
CI / checks (push) Has been cancelled
Bumps the npm group with 3 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 26.2.0 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.9.0 to 10.9.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.0...v10.9.1)

Updates `typescript-eslint` from 8.67.0 to 8.68.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.68.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.9.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.68.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 23:52:08 +02:00
dependabot[bot] 51da0190f5 ci: bump vmactions/omnios-vm from 1.3.4 to 1.3.6 in the actions group
Bumps the actions group with 1 update: [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `vmactions/omnios-vm` from 1.3.4 to 1.3.6
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1.3.4...v1.3.6)

---
updated-dependencies:
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 23:51:47 +02:00
Amaan Qureshi e5cbeb74eb nix: fix the flake
Some checks failed
CI / checks (push) Waiting to run
CI / build (push) Failing after 16s
CI / sanitize (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 14s
Deploy Docs / deploy-docs (push) Has been cancelled
Check Wasm Exports / check-wasm-exports (push) Has been cancelled
2026-08-30 17:33:12 -04:00
Amaan Qureshi ea9ddbe190 docs: replace mdbook-admonish with mdBook admonitions 2026-08-30 17:33:12 -04:00
Will Lillis aa1b924997 fix(generate): defer token rewrites until lexical expansion
In a future feature, `RulePool` nodes can be shared between lexical and
syntax roots. No grammars generated by grammar.js/grammra.json are affected
by this issue, as they lead to a tree pool rather than a DAG.

With a DAG `RulePool` representation, however, rewriting tokens during
extraction can destroy a token body before lexical expansion consumes
it and can renumber shared syntax nodes more than once.

Record terminal rewrites while inspecting the original pool, expand tokens
and separators first, then commit the rewrites and renumber each reachable
syntax node once. Resolve grammar metadata against the pending rewrites
before mutating the pool.
2026-08-30 14:23:29 -04:00
Will Lillis 1b64a459ff perf(generate): flatten Choices directly into the children tail 2026-08-30 14:23:29 -04:00
Will Lillis 1e070eb28e perf(generate): build Seq and Repeat child ranges without temporaries
`try_seq` reserves a `Seq`'s child range up front and fills slots as
recursion produces each member. The `Repeat` arm builds
`Choice(repeat(x), blank)` directly, skipping the `choice` helper.

Slightly faster and less memory used.
2026-08-30 14:23:29 -04:00
Christian Clason 91e875ad7b chore: start working on v0.28 2026-08-30 19:50:28 +02:00
Arne Bertrand 6070dbfefd
fix(nix): remove Dockerfile reference from flake (#5892)
Fixup for https://github.com/tree-sitter/tree-sitter/pull/5872.
2026-08-30 16:49:12 +00:00
Christian Clason f52e78cb9f build(deps): cargo update
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
2026-08-30 09:29:09 +02:00
Will Lillis 49dbf70ebd fix(rust): ensure C returned pointers are non-null in
`Node::grammar_name` and `Node::kind`
2026-08-30 02:58:20 -04:00
Will Lillis 9540ea3b10 fix(lib): reject an old_tree from a different language 2026-08-30 02:58:20 -04:00
Will Lillis 3e7be91c46 fix(rust): reject out-of-range node kind ids
`node_kind_is_named`, `node_kind_is_visible` and `node_kind_is_supertype`
forwarded their `u16` straight to `ts_language_symbol_type`, which indexes
`symbol_metadata` unchecked. Bound the id by `node_kind_count()`.
2026-08-30 02:58:20 -04:00
Will Lillis f47170927d fix(lib): bounds-check symbol and state ids in language lookups
`ts_language_next_state` indexed the parse table with the caller's `state`
and `symbol` without validating either, and `ts_language_subtypes` read
`symbol_metadata` before checking `supertype` was in range.
2026-08-30 02:58:20 -04:00
Will Lillis 5ddf71b9f4 fix(rust)!: expose QueryMatch::captures as a method
The field handed out a slice whose lifetime outlived the streaming-iterator
loan it came from. An accessor reborrowing through `&self` bounds it correctly.

BREAKING CHANGE: In the rust bindings, replace `m.captures` with `m.captures()`.
2026-08-30 02:58:20 -04:00
cuishuang efa0dd9a36 fix(cli): correct zero-based positions for --edits
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-08-29 21:06:30 -04:00
Christian Clason 16d5e9d227 ci(checks): add lint-toml step using taplo
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 14s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 14s
Deploy Docs / deploy-docs (push) Has been cancelled
2026-08-29 11:10:37 +02:00
Will Lillis 3ac04a64f7 docs: add taplo instructions 2026-08-29 11:10:37 +02:00
Will Lillis d9cb739430 fix(toml): drop align_entries = true from .taplo.toml
This key was being applied inconsistently depending on nesting level.
2026-08-29 11:10:37 +02:00
Will Lillis 7a2dd61a7c fix(xtask): preserve format padding when updating Cargo.toml files 2026-08-29 11:10:37 +02:00
Christian Clason a520ad0fc3 chore: remove .dockerignore
Fixup for #5872, which missed the hidden file.
2026-08-29 10:30:35 +02:00
Christian Clason da219f40f8 chore: remove Zed editor config
Problem: Editor-specific settings should not be part of the global
repository. (Only config files that control standard tooling in order to
enforce, e.g., formatting policies are appropriate to commit -- and only
if covered by CI.)

Solution: Drop the Zed config files from the repo.
2026-08-29 10:30:35 +02:00
Christian Clason 664e9a6786 build(deps)!: bump wasi-sdk to v34
Some checks failed
CI / checks (push) Waiting to run
Deploy Docs / deploy-docs (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 14s
Check Bindgen Output / check-bindgen (push) Has been cancelled
Check Wasm Exports / check-wasm-exports (push) Has been cancelled
Breaking change: LLVM 23 replaces `wasm32-wasi` target with
`wasm32-wasip1`, requiring updating the flags used by
`tree-sitter build --wasm`.
2026-08-27 10:35:19 +02:00
Christian Clason fcef970e81 build(deps): bump wasmtime-c-api to v48.0.1 2026-08-27 09:37:57 +02:00
Kyuhwan Lee 8933cb7b40 docs(web): add WASM version compatibility section
Add documentation about version compatibility requirements between
web-tree-sitter parser ABIs for WASM file generation.

This helps users understand why Language.load() might fail when using
pre-built WASM files from third-party packages that were built with
incompatible tree-sitter-cli versions.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-25 17:34:56 -04:00
Will Lillis 82639170a7 docs: specify Emscripten version constraints for web binding builds 2026-08-25 17:34:37 -04:00
dependabot[bot] 129d8cd141 build(deps): bump eslint
Bumps the npm group with 1 update in the /lib/binding_web directory: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.8.1 to 10.9.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.9.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:59:01 +02:00
dependabot[bot] f3a9521894 build(deps): bump cc from 1.4.3 to 1.4.4 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.4.3 to 1.4.4
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.3...cc-v1.4.4)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:58:43 +02:00
Peter Stenger 0be5f898ad fix(web): map Node-only specifiers to false via browser field
Browser bundlers (esbuild, webpack, vite, rollup) statically discover the
`await import("fs/promises")` and `await import("module")` calls in the
shipped ESM bundle and try to resolve them, even though both sit in
Node-only branches that never execute in a browser.

Adding a top-level `browser` field tells bundlers to substitute empty
stubs for these specifiers when targeting the browser. The runtime gates
(`globalThis.process?.versions.node` and `ENVIRONMENT_IS_NODE`) already
prevent the stubs from ever being invoked.

Fixes #5545.
2026-08-25 00:27:50 -04:00
katrinafyi ac17c8d003 playground: set default LANGUAGE_BASE_URL to "." to fix subfolder hosting
Previously with the empty string base URL, the playground would not work when hosted at a subfolder of the domain. This is because playground.js has this code:

      const url = `${LANGUAGE_BASE_URL}/tree-sitter-${newLanguageName}.wasm`;

With an empty string, it would look for wasm at the root of the website and 404.

Github code search shows that this is an issue which lots of people have run into, and everyone has to work around it: https://github.com/search?q=%2FLANGUAGE_BASE_URL+%3D+%22%22%2F+-language%3AHTML&type=code

Related to https://www.github.com/tree-sitter/tree-sitter/issues/5230
2026-08-24 19:41:26 -04:00
Will Lillis c6c137a329 fix(generate): avoid mutating inherited reserved sets
Copy the base grammar's reserved set map before adding or replacing sets
in a derived grammar.
2026-08-23 17:08:31 -04:00
Will Lillis 1cb91662a5 fix(generate): validate symbol-only grammar fields
Require `word`, `conflicts`, `inline`, and `supertypes` callbacks
to return named grammar symbols instead of silently accepting values
that produce undefined names.

Restrict `precedences` lists to named rules and precedence names, and
preserve the existing handling of undefined and duplicate `inline`
rules.
2026-08-23 17:08:31 -04:00
Mike Arndt 7ca101cb7e fix(cli): align grammar DSL declarations with runtime
Model grammar() as returning the evaluated grammar beneath a `grammar`
property, with callbacks replaced by their normalized values and
runtime-initialized fields represented as required properties.

Correct the evaluated rule types for named precedences and regular
expression flags. Narrow symbol-only callbacks such as `conflicts`,
`inline`, `supertypes`, and `word`, and expose the inherited values
passed to `extras` and `reserved` callbacks.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-23 17:08:31 -04:00
Will Lillis dad7d0bd88 generate: use a single buffer for the string pool's backing store
This representation slightly lowers walltime and allocations, but also
makes `StrPool` `Send`.
2026-08-23 14:15:16 -04:00
Will Lillis f9dcb005cd fix(generate)!: use node identities in node-types bookkeeping
Some node-types bookkeeping still used the type name without its named
status. This caused anonymous nodes to inherit extra metadata from named
nodes with the same text, and could create false supertype cycles when a
named supertype and anonymous alias shared a name.

Key extra metadata and supertype dependencies by the complete
(type, named) identity.

Reject named aliases that collide with a canonical supertype. Such an
identity cannot be represented as both a concrete node and an abstract
supertype in node-types.json, and previously caused generation to panic.

All output entries now come from the map keyed by (type, named), so no
two entries can reach the final sort with the same identity. Remove the
unreachable root and extra tie-breakers and the now-redundant deduplication.

BREAKING CHANGE: Alters a public error enum for the generate crate.
2026-08-23 13:40:32 -04:00
Will Lillis 78267a0a96 fix(generate): merge anonymous token aliases in node-types.json
When an anonymous token alias and an anonymous syntax-node alias have
the same name, node-types.json emits two entries with the same type and
named status. For example,

```
alias($._node, "same")
```
and

```
alias("!", "same")
```

produce separate anonymous "same" entries. Add anonymous tokens to
the node-type map instead. When a token shares an identity with a
structured node, retain its possible children and fields but mark
them as optional because the token appearance is a leaf.
2026-08-23 13:40:32 -04:00
Will Lillis 00beb27f83 fix(generate): distinguish named and anonymous node types
A node-types entry is identified by both its type name and its `named`
value. The generator only keyed accumulated entries by name, so aliases
like

    alias($._node, $.same)
    alias($._node, "same")

were incorrectly merged into one entry. Whichever alias was processed
first determined the entry's `named` value, and the structures of two
distinct node types could be combined.

Key accumulated entries by `NodeTypeRef` so named and anonymous nodes
with the same type name remain separate.
2026-08-23 13:40:32 -04:00
Will Lillis 7bd12334c4 fix(generate): include aliased supertypes in node-types.json
Aliasing a supertype makes the aliased occurrence appear as a regular
node in the syntax tree. For example,

```
alias($.expression, $.expression_target)
```

can produce:

```
(expression_target (identifier))
```

Previously, node-types generation skipped the source supertype entirely.
It could reference `expression_target` as a field or child type without
emitting a corresponding top-level entry for it.

Continue emitting the canonical `expression` entry with its `subtypes`,
but pass aliased appearances through regular node generation so their
children and fields are recorded and merged normally.
2026-08-23 13:40:32 -04:00
Will Lillis 54a46eb49b fix(rust): remove must_use attribute from Tree::changed_ranges
`ExactSizeIterator` implements `std::iter::Iterator`, which is already
`must_use`.
2026-08-23 09:45:42 +02:00
Christian Clason 2452dc7a71 chore: remove Dockerfile
Problem: The committed Dockerfile is not used by the standard tooling in
this repo and therefore bitrots freely, which increases maintenance
burden on the (non-Docker-using) maintainers.

Solution: Drop the Dockerfile from the repo.
2026-08-23 00:35:12 -04:00
Amaan Qureshi 74b7d0c951 feat!: add an eof function
This commit adds an `eof()` function for grammars, which is easier to
use than the NUL byte directly. It compiles down to a constraint that
the enclosing production can only reduce at end of input, not to a
shiftable token.

BREAKING CHANGE: Public error types for `tree-sitter-generate` were modified.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-21 20:02:58 -04:00
Will Lillis baad4174e5 fix(generate)!: hold IoError inside GenerateError::GrammarPath 2026-08-20 22:11:42 -04:00
Will Lillis cde84508ac fix(generate)!: error when nonexistent _file_ path is passed to
`generate_parser_in_directory`.
2026-08-20 22:11:42 -04:00
Max Brunsfeld 43623ec9bf
Upgrade to latest wasmtime, improve robustness and perf of wasm-based parsing (#5847)
* build(deps): upgrade wasmtime C API to 48.0.0

Upgrade the Rust and Zig Wasmtime dependencies and enable reference values with the null GC collector.

Wasmtime 48 requires a newer Rust toolchain, whose Clippy version identifies three item helpers that can be const. Mark them const so the workspace continues to pass Clippy with warnings denied.

* feat(benchmark): support Wasm grammars

* perf(wasm): cache language function handles

* fix(wasm): improve validation and failure cleanup

Bounds-check dylink metadata parsing, require exact import and export names, and restore memory and function-table allocation offsets when language loading fails.

* fix(wasm): copy the complete supertype map
2026-08-20 21:02:11 +00:00
Will Lillis fe3fe327e2 fix(generate): correct test assertion for inlined supertype warning 2026-08-20 02:47:10 -04:00
Will Lillis 03ae6710cd fix(ci): run all workspace tests
Without `--workspace`, all generate tests were silently skipped.
2026-08-20 02:47:10 -04:00
Amaan Qureshi 067f85e5a2 generate: hold StrIds in node-types JSON output
The JSON output types now hold ids from the string pool instead of
cloning into owned `String`s, which makes `NodeTypeJSON` `Copy` and
drops most allocations when building the output.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-19 20:30:17 -04:00
Amaan Qureshi 5800563fe7 generate: refactor get_variable_info and generate_node_types_json
This splits both functions into small, focused helpers with no
behavioral change.
2026-08-19 20:30:17 -04:00
Amaan Qureshi 14c158bbba generate: warn when a symbol is both a supertype and inlined
Currently, a symbol listed in both `supertypes` and `inline` produces
a phantom supertype entry in `node-types.json` for a rule that can
never appear in a tree. `intern_symbols` now drops the supertype with
a warning. A hard error would break 29 published grammars, including
python, go, ruby, rust, and scala, so that that decision is deferred
to the next breaking release. Fixes #5218
2026-08-19 20:30:17 -04:00
dependabot[bot] f235c2f1c3 build(deps): bump the cargo group with 4 updates
Bumps the cargo group with 4 updates: [bstr](https://github.com/BurntSushi/bstr), [cc](https://github.com/rust-lang/cc-rs), [clap_complete_nushell](https://github.com/clap-rs/clap) and [thiserror](https://github.com/dtolnay/thiserror).


Updates `bstr` from 1.13.0 to 1.13.1
- [Commits](https://github.com/BurntSushi/bstr/compare/1.13.0...1.13.1)

Updates `cc` from 1.4.2 to 1.4.3
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.2...cc-v1.4.3)

Updates `clap_complete_nushell` from 4.6.1 to 4.6.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete_nushell-v4.6.1...clap_complete_nushell-v4.6.2)

Updates `thiserror` from 2.0.19 to 2.0.20
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.19...2.0.20)

---
updated-dependencies:
- dependency-name: bstr
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: cc
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete_nushell
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: thiserror
  dependency-version: 2.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 10:39:56 +02:00
dependabot[bot] 95eaaa2130 build(deps): bump the npm group across 1 directory with 3 updates
Bumps the npm group with 3 updates in the /lib/binding_web directory: [esbuild](https://github.com/evanw/esbuild), [tsx](https://github.com/privatenumber/tsx) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `esbuild` from 0.28.1 to 0.28.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.1...v0.28.2)

Updates `tsx` from 4.23.11 to 4.23.12
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.11...v4.23.12)

Updates `typescript-eslint` from 8.66.0 to 8.67.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.67.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 10:37:31 +02:00
dependabot[bot] dff1fd868c build(deps): bump esbuild and vite in /lib/binding_web
Bumps [esbuild](https://github.com/evanw/esbuild) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies needed to be updated together.

Updates `esbuild` from 0.27.7 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.1)

Updates `vite` from 7.3.2 to 7.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 7.3.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-15 10:13:38 +02:00
Max Brunsfeld 0e2af0d8d1
Allow using Tree-sitter rust lib in multi-threaded web apps compiled to wasm32-unknown-unknown (#5851)
* feat(wasm): make syntax trees sendable

* test(wasm): transfer trees across workers

* test(wasm): use JSON grammar for tree transfer

* test(wasm): edit trees across workers

* test(wasm): share dlmalloc with tree-sitter

* test(wasm): simplify worker tree exchange

* test(wasm): drive tree exchange from Rust

* test(wasm): split sendable-tree xtask

* test(wasm): generalize Rust web fixture

* test(wasm): exercise parallel Rust tree access

* fix(wasm): use Rust global allocator for C core

* test(wasm): use default Rust allocator

* feat(wasm): support external scanners in Rust web apps

* Simplify example further, add a readme

* Regenerate wasm-stdlib

* Fix wasm_stdlib check script

* Vendor the Wasm standard library subset

* Test Unicode Ruby scanner behavior in Wasm

* Make Wasm tree languages instance-aware

* Test multi-threaded use of queries in wasm32-unknown

* Refactor reference-counted language storage

* Check ABI version compat before loading rest of language

* 🎨 Remove redundant #ifdef block

* Reject unsupported Rust Wasm builds on 0.26
2026-08-14 17:05:42 -07:00
Will Lillis 21e3614b8d perf(lib): fast path for ASCII characters in ts_lexer__advance
Yields a 0-5% improvement on parse speed (very grammar dependent).
2026-08-14 18:11:27 +02:00
Will Lillis 730946cfd5 perf(lib): fast path for ASCII characters in ts_lexer__get_lookahead
Yields a ~2% improvement on parse speed.
2026-08-14 18:11:27 +02:00
Christian Clason 277f53f886 build(swift)!: drop Package.swift
Problem: `Package.swift` build script is unmaintained and leads to build
errors.

Solution: Remove `Package.swift`; downstream tools should rely on the
swift-tree-sitter bindings (or, if they want a custom bare-metal build,
handle this in their own build scripts).
2026-08-14 12:51:38 +02:00
Christian Clason f8e735212b build(deps): bump binaryen to v132 2026-08-14 09:33:39 +02:00
Tim Vermeulen f30ee2b300 fix(parser): nest error children during recovery 2026-08-14 00:51:11 -04:00
Tim Vermeulen 869638f6cf fix(lib): accumulate error costs through hidden error nodes 2026-08-14 00:51:11 -04:00
Will Lillis c279989118 fix(xtask): check for vitest rather than chai and mocha
`test::run_wasm` gated dependency installation on `node_modules/chai` and
`node_modules/mocha`. Neither is a dependency (vitest is used instead),
so the check never passed and every `cargo xtask test-wasm`
reran `npm install`.
2026-08-14 00:13:54 -04:00
Will Lillis c68605f18d fix(rust)!: tie language string lifetimes to the proper containers
`Node::kind`, `Node::grammar_name`, the field name accessors,
`TreeCursor::field_name`, and the `Language` name lookups returned `&'static
str` while pointing into storage owned by the `TSLanguage`. Releasing the last
handle to a Wasm language frees that storage.

`Node` and `TreeCursor` now return `&'tree str`, which is ok because
`ts_tree_new` takes a reference to the language via `ts_language_copy` and
holds it until `ts_tree_delete`. The `Language` lookups return strings borrowed
from `&self`.
2026-08-14 00:13:54 -04:00
Will Lillis 7be497a14e fix(web)!: report null when lookahead iterator is not positioned
`currentType` and `currentTypeId` return `null` before the first iteration
step, after exhaustion, and after a reset.

`currentType` also falls back to the language's own name table instead of the
literal 'ERROR' when `Language.types` has no entry (auxiliary symbols). Other
bindings report `end` where this one reported 'ERROR'.
2026-08-14 00:13:54 -04:00
Will Lillis 081929092f fix(rust)!: make lookahead accessors fallible and iteration fused
`current_symbol` and `current_symbol_name` return an `Option` (`None` when the
iterator is not positioned on a symbol). Change `iter_names`'s item from
`&'static str` to `&str`.
2026-08-14 00:13:54 -04:00
Will Lillis 7bf4a10995 fix(lib)!: make lookahead iterator exhaustion sticky and retain its language
Track the iterator's phase so exhaustion is sticky, and gate the symbol name on
it, so `NULL` means "not positioned on a symbol".

`ts_lookahead_iterator_new` was also the only language consuming constructor
that did not `ts_language_copy`, so an iterator outliving a wasm language read
freed memory. Retain in `_new` and `_reset`, release in `_delete`.

Previously:
- `ts_lookahead_iterator__next` left a small parse state's cursor one past its
group end, so re-advancing an exhausted iterator resumed returning `true` and
walked through the rest of `ts_small_parse_table` and off the end of it.

- `ts_lookahead_iterator_current_symbol_name` returned `NULL` only when the
exhausted symbol index happened to fall outside the names table, so a grammar
with aliases returned a real but wrong name instead.
2026-08-14 00:13:54 -04:00
Will Lillis 721b211168 fix(generate)!: reject non-ASCII byte classes in (?-u:...) patterns
`(?-u:...)` switches `regex_syntax` to matching raw bytes, but the lexer
dispatches on decoded characters, so `expand_regex` converted the byte
class with a u8 cast. This is exact for ASCII bytes, and silently
misleading for anything aboove 0x80.

This is not reachable from grammar.js (node and QuickJS both reject
`(?-u:...)`). This change is to guard against future JS runtime changes,
as well as alternative frontends to the generate crate.
2026-08-14 00:13:39 -04:00
Will Lillis 2b15649f93 fix(generate): fold case-insensitive patterns at the AST leaves
A previous fix moved case folding for `/i` patterns out of `regex_syntax` and
into `expand_regex`, so folding could drop the two non-ASCII code points
Unicode simple folding maps onto ASCII letters: the long s `ſ` (U+017F)
onto `s`, and the Kelvin sign `K` (U+212A) onto `k`. Left in, they leak
into otherwise-ASCII tokens and stop those tokens from being extracted as
keywords.

By that point, though, the HIR has already turned a negated class into a
complement, so folding it applies the fold on the wrong side of the
negation. `(?i)[^a-z]` folds a set that contains `A-Z`, which re-admits
`a-z` and leaves a class matching very nearly everything.

`regex_syntax` folds each leaf of a class expression before applying that
leaf's negation and the set algebra above it. Keep that order and change
only the fold: walk the AST, replace each leaf with its fold, and translate
with `case_insensitive(false)`.
2026-08-12 23:40:32 -04:00
Jason Boatman 18cc71a6d9 feat(lib): add state to missing nodes.
This allows them to return possible missing values from a `LookaheadIterator`.
2026-08-12 23:39:44 -04:00
Will Lillis 816a2a0c15
refactor(rust): trim unused imports (#5845)
* Importing `char` brings deprecated methods into scope in some cases.
* Importing `str` isn't necessary in many other cases.
2026-08-12 14:27:52 +02:00
Will Lillis 42f33fe2f8 fix(query): correctly set a state's skipped_quantifier flag when a
zero quantifier skip is performed.

The zero-skip branch currently sets skipped_quantifier unconditionally.
That flag is correct only when the quantified step and its skip target
are _siblings_ at the same query depth. Otherwise, setting it allows for
a "leak" and disables unrelated, "outer" anchors.
2026-08-10 23:59:11 -05:00
dependabot[bot] 5c2533bb7b build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [cc](https://github.com/rust-lang/cc-rs), [clap](https://github.com/clap-rs/clap) and [clap_complete](https://github.com/clap-rs/clap).


Updates `cc` from 1.4.0 to 1.4.1
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.0...cc-v1.4.1)

Updates `clap` from 4.6.5 to 4.6.6
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.5...clap_complete-v4.6.6)

Updates `clap_complete` from 4.6.8 to 4.6.9
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.8...clap_complete-v4.6.9)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.6.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.6.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:42:21 +02:00
dependabot[bot] 6ea9a7c031 build(deps): bump the npm group across 1 directory with 4 updates
Bumps the npm group with 4 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint), [tsx](https://github.com/privatenumber/tsx) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 26.1.2 to 26.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.8.0 to 10.8.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.0...v10.8.1)

Updates `tsx` from 4.23.1 to 4.23.11
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.1...v4.23.11)

Updates `typescript-eslint` from 8.65.0 to 8.66.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.66.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:40:39 +02:00
Will Lillis 543734d286 feat(test): allow corpus tests to use cst outputs
This is already exposed for consumers via the CLI, and is a natural way
to express some test expectations over the sexp form.

Also clean up some repeated logic in the internal test code, and narrow
the cst rendering return type to `std::io::Result` rather than
`anyhow::Result`.
2026-08-09 15:48:37 -05:00
Will Lillis 5fae914f8f fix(query): correctly identify MISSING nodes in queries 2026-08-09 15:48:07 -05:00
Will Lillis 1b916ab754 ci: increase nvim-ts generate max jobs to 10 2026-08-09 12:28:50 -05:00
Will Lillis fad0a62a35 perf(generate): pool parse-table action lists
Every (state, terminal) parse-table entry stored its action list inline as a
32-byte `ParseTableEntry`, but across a grammar those lists are ~98-99% _duplicates_.
The number of distinct lists is a few thousand regardless of grammar size, while
total entries scale into the hundreds of thousands.

Store each unique action list once in a shared `ActionListPool` (a flat arena of
actions plus `(offset, len)` ranges) and replace the inline entry with a 4-byte
`ActionListId` (a pool index with the `reusable` flag packed into the high bit).

- intern_table converts the freshly built `ParseTable<ParseTableEntry>` into
  `ParseTable<ActionListId>`.
- minimize carries and operates on the 4-byte ids. The three global state
  renumberings rewrite Shift targets once at the pool level
  (`remap_terminal_references`), while the per-state unit-reduction redirects
  copy the changed list into a new slot (COW). `mark_fragile_tokens` becomes a
  free bit flip on the id.
- `canonicalize` dedups and compacts the pool once before `render`, dropping the
  dead and duplicate slots the remaps and COW leave behind. `render` assigns the
  output action-list offsets directly.

Yields ~7% wall time reduction, ~12% peak rss reduction.
2026-08-09 12:28:50 -05:00
Will Lillis dbcdd0416b perf(generate): shrink parse/lex/production ids from usize to u32
`ParseStateId`, `LexStateId`, `ProductionInfoId`, and `ReservedWordSetId`
each held a `usize`, while the runtime stores state/field ids as `u16` (so
`u32` has plenty of headroom). Shrinking them saves space for every parse-table
entry, and a number of other data structures.

Reduces wall time by 2-5%, peak rss by 9-12%.
2026-08-09 12:28:50 -05:00
Will Lillis 87d14ddfc6 generate: use the LexStateId typedef for lex state ids
Lex state ids were passed as bare usize in several spots. Route them
through the existing `LexStateId` typedef.
2026-08-09 12:28:50 -05:00
Will Lillis 142c60dca1 perf(generate): memoize auxiliary node info within each state
`get_auxiliary_node_info` scans every entry in the item set to collect the
non-auxiliary parents of a given auxiliary symbol. It was called once per
entry whose next symbol is auxiliary, and the same auxiliary symbol recurs
across many entries in a single state (a state's GOTO on a repeat symbol is
shared by every item advancing over it), so the same full scan was repeated
many times per state. Memoize the result per symbol within a single
`add_actions` call.

Reduces wall time 1-4%, rss flat.
2026-08-09 12:28:50 -05:00
Will Lillis b14413e4f4 perf(generate): inline single-action parse table entries
`ParseTableEntry` held a `Vec<ParseAction>` per entry. `ActionList` keeps up
to one action inline and spills to a Vec only for conflict entries.

Reduces walltime by >20%, peak rss by ~30%.
2026-08-09 12:28:50 -05:00
Will Lillis 2a81995058 perf(generate): memoize get_production_id
The production info depends only on the production, but was recomputed and
deduplicated via a linear deep-equality scan over production_infos for every
reduce item in every state. Cache `prod_id` -> `ProductionInfoId`.

Wall time reduction 0-3%, peak rss neutral.
2026-08-09 12:28:50 -05:00
Will Lillis d6ae19cc6d perf(generate): intern lookahead TokenSets
`ParseItemSetEntry` stores a `LookaheadSetId` into a `LookaheadSetPool` instead
of an owned `TokenSet`. Ids are canonical, so entry hash/eq/clone are integer
ops and state dedup stops walking set words. Unions and single-token inserts
are memoized by id, so the transitive closure and successor-kernel construction
stop re-materializing the same unions per state. Closure additions carry interned
ids with word-token membership precomputed.

For cpp, 3566 distinct lookahead sets back all 49,992 states. For ruby 2000 sets
for 91,991 states, and rust 646 for 16,796.

Reduces wall time by 10-15%, peak rss by 3-5%.
2026-08-09 12:28:50 -05:00
Will Lillis 6659ff57eb perf(generate): vectorize minimize's token_conflicts with precomputed bitsets
The state-merge pass's `token_conflicts` scanned every terminal entry of the
other state per non-shared token. Precompute flat, word-aligned bitsets,
`ConflictBits`, so each call is a handful of word ANDs.

Yields a 5-6% wall time reduction on large grammars, and flat to ~1%
loss on small grammers. Peak RSS unchanged.
2026-08-09 12:28:50 -05:00
Will Lillis b304ac9e56 perf(generate): stop duplicating kernel item sets in parse_state_info
Every state's kernel set was cloned into `parse_state_info` even though the
identical set already lives as the state-dedup map key at the same index
(state ids are the map's insertion order). `ParseStateInfo` now holds the
preceding-symbol sequences plus the dedup map moved out of the builder,
allowing the report path to read kernels via `get_index`. Drops the
duplicate kernel storage, allowing for a small reduction in wall time
and peak rss (1-3%).
2026-08-09 12:28:50 -05:00
Will Lillis 2ce7b409a8 perf(generate): replace coincident state lists with a without-word bitset
`CoincidentTokenIndex` kept a `Vec<ParseStateId>` per token pair whose
only user was keyword identification asking one question: "do all states
where a given pair coincides also allow the word token?" Track that
directly as a second bitset built in the same pass.

Reduces walltime by ~7% for small grammars (i.e. Go), and 20-25% for
larger grammars (i.e. Rust, cpp). Reduces peak rss by ~20% for smaller
grammars and ~40-50% for larger grammars.
2026-08-09 12:28:50 -05:00
Will Lillis 110a8becb8 perf(generate): shrink Symbol::index from usize to u32
This cuts `Symbol`'s size from 16 to 8 bytes. Results in a ~2-7% wall time
reduction, and ~7-10% peak RSS reduction.
2026-08-09 12:28:50 -05:00
Will Lillis fb2052f02d perf(generate): represent the grammar IR as a flat, pooled arena
Replace the owned `Rule`-tree grammar with a flat, pooled representation. `Rule`
nodes live in a `RulePool` arena and reference their children and params by
index (`RuleId`). Strings are interned in a `StrPool` and referred to by `StrId`.
Productions are stored as flat `ProductionStep`/`Production` slices indexed per
variable, rather than as nested vectors hanging off each `SyntaxVariable`. This
drops the per-rule heap allocation of the owned-tree form and is the groundwork
for the later table-building optimizations.

`parse_grammar` builds the pool, the prepare passes rewrite nodes in place, and
`prepare_grammar` returns a `PreparedGrammar` bundle that owns the run's `StrPool`
alongside the grammars. `build_tables`, `node_types`, and `render` borrow that
pool and resolve `StrId`s only at the output boundary.

Shows an average ~10% walltime reduction when combined with the previous
commit, with nearly all of the savings in `build_tables`. The `prepare`
stage is also ~70% faster and much more stable, but this contributes
much less to overall generate time. Improving early stages such as
`prepare` will be important for future interactive uses.
2026-08-09 12:28:50 -05:00
Will Lillis e72c8594fb perf(generate): precompute order-preserving item identity keys
Parse-item identity dominates parse-table construction. `ParseItem`'s
`Hash`, `Eq`, and `Ord` walk the entire production on every call,
comparing per-step precedence, aliases, and field names. These are
stored as strings for named precedences and for every alias and field.

Precompute, once per table build, two dense `u32` ids for every
`(production, dot)` slot by ranking the whole slot universe with the
item-content comparator, now extracted as `ItemContent`. Equal-content
slots are adjacent after sorting and the id increments only at content
boundaries, so comparing the `cmp` id reproduces the structural order
exactly (both an ordering and an equality). A second id, `eq_with_syms`,
refines `cmp` by the preceding symbols, which enter equality only for
items with `has_preceding_inherited_fields`.

`ParseItem` now carries its production's key slice, and `Hash`/`Eq`/`Ord`
become integer ops.

On the current string-based production storage this is performance-neutral
in isolation (the up-front ranking pays back the per-comparison savings).
It is the foundation for the following change to a pooled, integer-keyed
production representation, which makes both the ranking and the item
comparisons cheaper integer work.
2026-08-09 12:28:50 -05:00
Christian Clason 4f7ab22584 ci(release): add nightly builds 2026-08-08 19:09:56 +02:00
Newosko 308aee0c90 fix(lib): continue search for later named siblings in
`ts_tree_cursor_current_status`

By terminating early on `has_later_siblings`, `has_later_named_siblings`
was incorrectly reported as `false` in some cases. This led to the
execution of some queries to terminate early.

Also remove some dead branches inside `ts_tree_cursor_current_status`.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-07 17:54:36 -04:00
Jason Boatman 003b10c280 feat(generate)!: expose OptLevel on generate_parser_for_grammar 2026-08-07 09:47:11 +02:00
Will Lillis 7511e3adaa fix(rust)!: tie query iterators to options lifetime
`QueryMatches` and `QueryCaptures` retain query cursor options while they are
being iterated. However, their types did not preserve the lifetime of the
options' progress callback, allowing the callback to be dropped while it
was still referenced by the cursor.

Add an explicit options lifetime to both iterator types and propagate it
through `matches_with_options` and `captures_with_options`. Use a static
options lifetime for iterators created without options.
2026-08-07 01:15:38 -04:00
Christian Clason 8966c69fcd build(deps): bump wasmtime-c-api to v36.0.13 2026-08-06 01:06:13 -04:00
dependabot[bot] 97e2eeb83c build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.8 to 5.0.9.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.8...v5.0.9)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:59:36 +02:00
dependabot[bot] fecce454cb build(deps): bump @types/node
Bumps the npm group with 1 update in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:38:07 +02:00
dependabot[bot] 36865d63bb build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [clap](https://github.com/clap-rs/clap) and [webbrowser](https://github.com/amodm/webbrowser-rs).


Updates `clap` from 4.6.4 to 4.6.5
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.4...clap_complete-v4.6.5)

Updates `webbrowser` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/amodm/webbrowser-rs/releases)
- [Changelog](https://github.com/amodm/webbrowser-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amodm/webbrowser-rs/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: webbrowser
  dependency-version: 1.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:37:48 +02:00
Christian Clason 963b5a5a97 build(deps): bump rquickjs to v0.12.2 2026-07-29 02:21:32 +02:00
dependabot[bot] 4deef2d5ef build(deps): bump eslint
Bumps the npm group with 1 update in the /lib/binding_web directory: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.7.0 to 10.8.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 00:29:36 +02:00
dependabot[bot] 049dee2634 build(deps): bump the cargo group with 6 updates
Bumps the cargo group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.103` | `1.0.104` |
| [clap](https://github.com/clap-rs/clap) | `4.6.3` | `4.6.4` |
| [glob](https://github.com/rust-lang/glob) | `0.3.3` | `0.3.4` |
| [serde](https://github.com/serde-rs/serde) | `1.0.228` | `1.0.229` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.150` | `1.0.151` |
| [thiserror](https://github.com/dtolnay/thiserror) | `2.0.18` | `2.0.19` |


Updates `anyhow` from 1.0.103 to 1.0.104
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.103...1.0.104)

Updates `clap` from 4.6.3 to 4.6.4
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.3...clap_complete-v4.6.4)

Updates `glob` from 0.3.3 to 0.3.4
- [Release notes](https://github.com/rust-lang/glob/releases)
- [Changelog](https://github.com/rust-lang/glob/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/glob/compare/v0.3.3...v0.3.4)

Updates `serde` from 1.0.228 to 1.0.229
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

Updates `serde_json` from 1.0.150 to 1.0.151
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.150...v1.0.151)

Updates `thiserror` from 2.0.18 to 2.0.19
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.18...2.0.19)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: glob
  dependency-version: 0.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: thiserror
  dependency-version: 2.0.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 23:49:19 +02:00
Sjoerd Langkemper 0900f84eab fix(generate): honor right associativity despite a lower-precedence shift
A SHIFT/REDUCE conflict can bundle several shift interpretations with
different precedences against a single reduce. `handle_conflict` weighed them
with only `shift_is_less` and `shift_is_more`, so a lone lower-precedence
shift set `shift_is_less` and the REDUCE won outright, even when another
interpretation tied the REDUCE in precedence and the REDUCE was declared
right-associative. That tie's associativity should have won by shifting, so a
right-associative rule silently became left-associative as soon as an
unrelated lower-precedence rule was added to the grammar.

Track the equal-precedence case explicitly, and in the reduce-wins branch
shift instead when a tying interpretation exists and the reduce actions are
purely right-associative.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-07-27 02:06:35 -04:00
Christian Clason 4d0f126996 test(web): fix new lint failure 2026-07-25 18:02:19 +02:00
dependabot[bot] ee3fa37bc3 build(deps): bump the npm group across 1 directory with 7 updates
Bumps the npm group with 7 updates in the /lib/binding_web directory:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.2` | `26.1.1` |
| [dts-buddy](https://github.com/sveltejs/dts-buddy) | `0.7.0` | `0.8.3` |
| [esbuild](https://github.com/evanw/esbuild) | `0.27.7` | `0.28.1` |
| [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.7.0` |
| [source-map](https://github.com/mozilla/source-map) | `0.7.6` | `0.8.0` |
| [tsx](https://github.com/privatenumber/tsx) | `4.21.0` | `4.23.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.58.0` | `8.65.0` |



Updates `@types/node` from 25.5.2 to 26.1.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dts-buddy` from 0.7.0 to 0.8.3
- [Release notes](https://github.com/sveltejs/dts-buddy/releases)
- [Changelog](https://github.com/sveltejs/dts-buddy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/dts-buddy/compare/v0.7.0...v0.8.3)

Updates `esbuild` from 0.27.7 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.1)

Updates `eslint` from 9.39.4 to 10.7.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.7.0)

Updates `source-map` from 0.7.6 to 0.8.0
- [Release notes](https://github.com/mozilla/source-map/releases)
- [Changelog](https://github.com/mozilla/source-map/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mozilla/source-map/compare/0.7.6...v0.8.0)

Updates `tsx` from 4.21.0 to 4.23.1
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.21.0...v4.23.1)

Updates `typescript-eslint` from 8.58.0 to 8.65.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.65.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: dts-buddy
  dependency-version: 0.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.7.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: source-map
  dependency-version: 0.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.65.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 18:02:19 +02:00
Will Lillis 15ea3328e1 fix(parser): restart recovery for invalid tokens in the error state
The early `ts_parser__recover` dispatch for `ERROR_STATE` in
`ts_parser__advance` was dropped in `201b41cf1`. Without it, tokens
with no valid action re-enter `ts_parser__handle_error` per token
and fragment the tree instead of extending one ERROR node. Restore
it to recover the old (pre-0.25) error recovery behavior.
2026-07-25 18:01:29 +02:00
dependabot[bot] 86e1fedfaf build(deps): bump postcss from 8.5.12 to 8.5.23 in /lib/binding_web
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.12 to 8.5.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.12...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:56:46 +02:00
dependabot[bot] 4777d410dd build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.7 to 5.0.8.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.7...v5.0.8)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:56:30 +02:00
dependabot[bot] 5d232ff77c build(deps): bump js-yaml from 4.2.0 to 4.3.0 in /lib/binding_web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:42:33 +02:00
dependabot[bot] 9beff8a998 build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.6 to 5.0.7.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.7)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:42:03 +02:00
Will Lillis 3a76a8c8ca feat(cli)!: rework HTML output modes in highlight
- `--layout <document|line-numbers|fragment>` (default `document`): the
  document structure. `document` is a self-contained page wrapping a plain
  `<div class="highlight"><pre><code>` block, `line-numbers` adds a line-number
  `<table>`, and `fragment` emits only the code markup with no surrounding
  `<head>`/`<style>`/`<body>` so it can be embedded in an existing page.

- `--style <classes|inline|minimal>` (default `classes`): how token colors
  are applied. `classes` uses `class="..."` spans plus a generated `<style>`,
  `inline` bakes colors onto each span so the markup is self-contained, and
  `minimal` emits bare classes with no colors (bring your own stylesheet).

Deprecates the `--css-classes` flag.
2026-07-24 20:56:56 -04:00
Will Lillis d205dc92de fix(rust)!: require parser loggers to be Send + 'static
`Parser::set_logger` boxes the logger callback into a type-erased C
pointer, but `Parser` carries no lifetime parameter. This allows for
two kinds of UB:

- Use-after-free: a logger could capture a non-`'static` borrow, let the
  borrowed value drop, and then dangle the next time the parser logged.
- Data race: a logger could capture a `!Send` value such as an `Rc`. The
  parser could then be moved to another thread and logged from there
  while the original thread still held a clone, racing on the reference
  count.

As a fix, we just require that logger is `Send + 'static`. The
alternative here is to attach a lifetime parameter to `Parser`, but this
is highly breaking for what is mostly a debugging utility. As an escape
hatch `set_logger_unchecked` is added to the public API as an `unsafe fn`
to correctly communicate the risks and invariants that must be held.
2026-07-24 20:56:16 -04:00
Alex Le Blanc 65b5b03c94
fix(highlight): use std::sync::OnceCell for various loader fields
`std::unsync::OnceCell` allows for data races and requires an (incorrect) manual impl of the `Sync` trait.
2026-07-23 18:30:15 -04:00
dependabot[bot] 0a0e77c375 ci: bump the actions group with 3 updates
Bumps the actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-node](https://github.com/actions/setup-node) and [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v7.0.0...v7.0.1)

Updates `actions/setup-node` from 6.4.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6.4.0...v7.0.0)

Updates `vmactions/omnios-vm` from 1.3.3 to 1.3.4
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 23:46:28 +02:00
dependabot[bot] 5e37a97684 build(deps): bump the cargo group with 4 updates
Bumps the cargo group with 4 updates: [cc](https://github.com/rust-lang/cc-rs), [clap](https://github.com/clap-rs/clap), [clap_complete_nushell](https://github.com/clap-rs/clap) and [bitflags](https://github.com/bitflags/bitflags).


Updates `cc` from 1.2.66 to 1.2.67
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.66...cc-v1.2.67)

Updates `clap` from 4.6.1 to 4.6.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.2)

Updates `clap_complete_nushell` from 4.6.0 to 4.6.1
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete_nushell-v4.6.0...clap_complete_nushell-v4.6.1)

Updates `bitflags` from 2.13.0 to 2.13.1
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.13.0...2.13.1)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.67
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete_nushell
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: bitflags
  dependency-version: 2.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 23:39:49 +02:00
Will Lillis f45a488dea feat(cli)!: make parent flags a hard requirements via clap
Also correct a parameter name in `print_tree_graph`
2026-07-18 17:00:53 -04:00
Will Lillis 78481a8dfc fix(init): don't lowercase repository url 2026-07-18 15:35:49 -04:00
Will Lillis 1ffd612be5 fix(query): transfer a leading boundary anchor across a zero-matched quantifier
`(P . Q* Y)` with zero `Q` dropped the leading `.`, so `Y` matched at any
position instead of being pinned to the parent's first named child. On a `?`/`*`
zero-skip, when the skipped step is the parent's first child and carries a
leading anchor, transfer that first-child requirement to the skip target.
2026-07-17 17:53:45 -04:00
Will Lillis dfcf73921c fix(query): keep the trailing anchor when a zero-matched quantifier is anchored
on both sides

In `A . Q* . B`, a zero-matched `Q` made both anchors vacuous, so `A` and `B`
were no longer required to be immediate siblings. Only relax the following
anchor on a `?`/`*` zero-skip when the skipped step has no leading anchor of
its own; otherwise the adjacency transfers through the empty run.
2026-07-17 17:53:45 -04:00
Philipp 8fe4f218fb
fix(cli): init --update should not update setup.py after replacing it (#5760) 2026-07-17 17:53:14 -04:00
Christian Clason 8b7c213443 build(deps): bump binaryen to v131 2026-07-17 02:16:46 -04:00
Will Lillis 0e76a0bd53 fix(generate): fold case-insensitive patterns ourselves
Unicode simple case folding maps two non-ASCII code points onto ASCII
letters: the long s `ſ` (U+017F) onto `s`, and the Kelvin sign `K` (U+212A)
onto `k`. So `regex_syntax` pulls them into any case-insensitive pattern,
which is virtually never intended and has two bad effects:

  * such tokens can no longer be extracted as keywords, because they are not
    a subset of an ASCII `word` token (#5607)
  * a broad class like `[^"]` or `\p{L}` carrying `/i` loses `ſ`/`K`, even
    though it legitimately contains them (#5755)

Rather than let `regex_syntax` fold, parse patterns unfolded and fold them
ourselves in `case_fold_ascii_safe`: fold via `regex_syntax`, then drop
`ſ`/`K` only when folding introduced them (they were not already in the
base set). A class that already contains them keeps them.
2026-07-16 21:32:24 -04:00
Kamal Chandra Upreti 49dd7b4079
docs(init): fix broken link 2026-07-15 12:13:07 +00:00
Will Lillis b23c9fceb7 feat(tags)!: include underlying LanguageError in tag
crate's error enum
2026-07-14 19:19:49 -04:00
Will Lillis db078ba439 feat(highlight)!: include underlying LanguageError in highlight
crate's error enum
2026-07-14 19:19:49 -04:00
dependabot[bot] da2838c39c build(deps): bump memchr from 2.8.2 to 2.8.3 in the cargo group
Bumps the cargo group with 1 update: [memchr](https://github.com/BurntSushi/memchr).


Updates `memchr` from 2.8.2 to 2.8.3
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.2...2.8.3)

---
updated-dependencies:
- dependency-name: memchr
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-13 23:47:28 +02:00
Will Lillis d11d18f746 feat(web): expand Language::load to accept URL filepath
Some checks failed
CI / check-wasm-stdlib (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / checks (push) Has been cancelled
2026-07-11 12:56:21 -04:00
Will Lillis ee0847d605 fix(ci): re-set executable permission on release artifacts before zipping
Some checks are pending
CI / checks (push) Waiting to run
CI / sanitize (push) Waiting to run
CI / build (push) Waiting to run
CI / check-wasm-stdlib (push) Waiting to run
2026-07-11 10:25:33 +02:00
ObserverOfTime 08fcb7f2ef fix(templates): replace deprecated method in Package.swift
Some checks are pending
CI / checks (push) Waiting to run
CI / sanitize (push) Waiting to run
CI / build (push) Waiting to run
CI / check-wasm-stdlib (push) Waiting to run
2026-07-11 00:50:56 -04:00
ObserverOfTime 805a7cd0ae fix(templates): add C source files to Python sdist 2026-07-11 00:50:56 -04:00
Andrew Mayo d2193f4dba docs(comment): misleading/wrong comment stating that initial state TSStateId is 0 rather than 1 2026-07-11 00:49:54 -04:00
Will Lillis 07c4ed220e fix(generate): strip non-ASCII case folds
Regexes with the case-insensitive 'i' flag caused unicode simple case folding,
which maps two non-ASCII code points onto ASCII letters:

- `ſ` (U+017F) onto `s`
- the Kelvin sign `K` (U+212A) onto `k`

This pushed such tokens outside an ASCII `word` token, so they failed to
extract as keywords.
2026-07-11 00:46:53 -04:00
Will Lillis 7027979c2b feat(cli)!: don't load all languages via config when lib path is passed
Also, make `--lang-name` without `--lib-path` a hard error
2026-07-11 00:27:13 -04:00
Will Lillis 17a02fef5e fix(rust): address new clippy lints 2026-07-11 00:03:44 -04:00
Julia Hansbrough cc7be1fd47
fix(templates): generated array macros do not compile with C++
Some checks failed
CI / checks (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / check-wasm-stdlib (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
Problem: A set of `array_*` macros (`array_push`, `array_extend`, etc) implicitly convert a `void*` into a different pointer type.  In environments that compile these headers as C++, this implicit conversion is an error.

Solution: This commit adds an `_array_cast` macro that uses `decltype` to cast the `void*` to the proper type when compiling as C++.
2026-07-08 08:38:37 +00:00
Will Lillis cce7768b27 docs: specify valid children of supertype nodes 2026-07-08 10:36:50 +02:00
Will Lillis 15da6ddf12 feat(generate): improve error message for supertypes made invalid by
hidden children
2026-07-08 10:36:50 +02:00
Christian Clason e470c32c24 build(deps): bump rquickjs to v0.12.1 2026-07-08 10:35:41 +02:00
dependabot[bot] a1f55783e6 ci: bump korthout/backport-action in the actions group
Some checks failed
CI / checks (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / check-wasm-stdlib (push) Has been cancelled
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.5.2 to 4.6.0
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.5.2...v4.6.0)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-07 00:33:16 +02:00
dependabot[bot] bf6f829c83 build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [clap_complete](https://github.com/clap-rs/clap), [rand](https://github.com/rust-random/rand) and [rustc-hash](https://github.com/rust-lang/rustc-hash).


Updates `clap_complete` from 4.6.5 to 4.6.7
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.5...clap_complete-v4.6.7)

Updates `rand` from 0.10.1 to 0.10.2
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.1...0.10.2)

Updates `rustc-hash` from 2.1.2 to 2.1.3
- [Changelog](https://github.com/rust-lang/rustc-hash/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/rustc-hash/compare/v2.1.2...v2.1.3)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: rand
  dependency-version: 0.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: rustc-hash
  dependency-version: 2.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-07 00:06:55 +02:00
Will Lillis 9fc2f486a8 docs(queries): clarify behavior of first child anchor example
Some checks failed
Check Bindgen Output / check-bindgen (push) Has been cancelled
CI / checks (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / check-wasm-stdlib (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
Check Wasm Exports / check-wasm-exports (push) Has been cancelled
2026-07-01 19:31:05 -04:00
Will Lillis ec872bca82 docs(queries): specify anchor behavior with quantifiers and groups
- An anchor between two patterns is vacuous when an adjacent quantifier
matches zero

- A leading or trailing anchor on a node applies to the nearest matched node
when an adjacent optional is skipped

- An anchor at the edge of a group or alternation is not allowed.
2026-07-01 19:31:05 -04:00
Will Lillis 139b801cce fix(query): apply a trailing anchor when its optional node is skipped
A trailing `.` after an optional node, e.g. `(p (a)+ @a . (b)? @b .)`, sets
`is_last_child` on the `(b)?` step. When `(b)?` matched zero, the zero-skip
jumped past that step to completion, so the last-child requirement was never
enforced.

When a zero-skip would bypass a step carrying `is_last_child`, require that
the last matched node really is the last named child. Gated on the
`alternative_is_skip` edge marker.
2026-07-01 19:31:05 -04:00
Will Lillis a6bc724748 fix(query): make an anchor after a zero-matched quantifier vacuous
A `.` anchor between a quantified pattern and a following node, e.g.
`(parent (comment)* @c . (decl))`, was treated as a leading anchor when the
quantifier matched zero. The zero-skip jumps a state directly onto the anchored
step, where `is_immediate` was enforced even though no sibling had matched before
it. Name that skip edge (`alternative_is_skip`) and, when a state follows it,
record that it skipped the quantifier (`skipped_quantifier`). The
immediate anchor is then supressoed for that state's next match.
2026-07-01 19:31:05 -04:00
Will Lillis b69d00b0fe fix(query): forbid an anchor at the end of a group
A `.` at the end of a group, such as `((comment)+ @c .)`, was silently dropped
during compilation. Reject rather than dropping.
2026-07-01 19:31:05 -04:00
Will Lillis 4915d1bd09 feat(query): enhanced step dumper
Extract the step dump into `ts_query__dump_steps` and enrich it with the control-flow
fields and analysis annotations.
2026-07-01 19:31:05 -04:00
Will Lillis 91fd04f96a fix(query): short-circuit longest-match dedup for disjoint states
An unanchored quantified sibling like `(program (comment)+ @doc (class))`
keeps one match state per matching sibling. A recent fix stopped over-pruning
them, but the per-node longest-match dedup is pairwise, so with n live states
matching became O(n^3).

Two states can only be capture subsets of one another if their captured
byte ranges overlap, so keep each group ordered by first-capture position
and stop the pairwise scan once the rest of the group is disjoint.

Refs neovim/neovim#40517
2026-07-01 18:07:14 -04:00
Will Lillis 57ea5340c6 fix(xtask): eliminate silent errors in zig fetch 2026-06-30 19:37:55 -04:00
dependabot[bot] 5b591babd1 ci: bump actions/cache from 5 to 6 in the actions group
Bumps the actions group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 23:39:20 +02:00
Christian Clason ae2081989e fix(deps): zig manifest for wasmtime is missing windows hashes
Problem: `zig fetch` on 0.16 can't download zip archives, which wasmtime
uses for Windows. This makes `cargo xtask upgrade-wastime` fail silently
with empty hashes for these platforms.

Solution: Re-run xtask with Zig 0.17 nightly.
2026-06-29 23:18:32 +02:00
Will Lillis 41d3e16eac test(query): regression test for anchored siblings inside a parent 2026-06-28 13:43:53 +02:00
Will Lillis 98c25b9aa0 feat(rust)!: change ts_set_allocator to accept one optional Allocator
object rather than 4 separate optional function pointers.

Helps prevent misuse via mixing different allocators. Also update the
doc comment with relevant safety information.
2026-06-27 23:22:11 -04:00
Will Lillis aec288f83c feat(generate)!: collect warnings as structured data
Allows for use by library consumers, CLI users when the `--json` flag is
passed, and cleans up core logic.
2026-06-27 22:39:24 -04:00
Michael Davis 9991c0f46f Add ts_query_copy for cloning queries
This allows duplicating a query so that it can be modified by using
disable_capture or disable_pattern, without re-parsing the query. The
new `ts_query_copy` creates a deep copy of the entire query object.

A motivation for this is tags.scm code navigation queries. You might
want to have one version of the compiled query capture only definitions
and the other only capture references, since references are much much
more common than definitions in a typical codebase. Instead of
re-parsing and analyzing the query and then calling
`ts_query_disable_capture`, we can clone the built query all-at-once and
then disable captures / patterns.
2026-06-27 17:53:13 -04:00
Lucas M. de Jong Larrarte 99bceec689 fix(query): skip passthrough steps when checking for fallible step splitting
Problem: In queries matching a parent node with anchored children
(siblings) where the first sibling has a quantifier and is not
captured, the check for fallible steps skips splitting the state because
the next node is a passthrough node (and not an is_immediate one). This
prevents the query from matching beyond the first occurrence.

Solution: In the check for fallible steps, skip the next steps if they
are passthrough steps.
2026-06-27 23:44:02 +02:00
Lucas M. de Jong Larrarte 8b43f42dca fix(query): avoid overriding states not seeking immediate match with states seeking immediate match
Problem: In queries matching a parent node with anchored children
(siblings) where the first anchored sibling has a quantifier, the
deduplication logic for states is overly aggressive. The logic causes
the state split on the first capture (with the parent) to be dropped in
favor of the loopback state. This results in the query not being able to
match beyond the first occurrence.

Solution: Prevent the deduplication logic from dropping a state that is
not seeking an immediate match for one that is seeking an immediate
match, since the one not seeking for an immediate match could still
match later nodes.
2026-06-27 23:44:02 +02:00
Lucas M. de Jong Larrarte 1b110f62dd fix(query): take anchors between siblings into account for fallible step splitting
Problem: In queries matching a parent node with anchored children
(siblings), the check for fallible steps only considers nodes with
children, which results in no state split being made for anchored
siblings (node1) . (node2). This prevents the query from matching beyond
the first occurrence.

Solution: Make the check for fallible steps consider also the case where
the next step is at the same depth and must be matched immediately
after.
2026-06-27 23:44:02 +02:00
Christian Clason cdae49be40 ci: update workflows for breaking actions/checkout change
Problem: actions/checkout@v7 refuses to checkout fork pull request code
on `pull_request_target` trigger, breaking backport and reviewers-remove
workflows on external PRs.

Solution:
* backport: Checkout the merged-into base branch by name.
* reviewers-remove: inline script so no checkout needed.
2026-06-27 23:25:18 +02:00
Andrew Gibb cbee467266
docs(readme): add link for cargo-binstall
* Update cli README.md

Added a link to CLI readme to make it clear to an outsider that
cargo-binstall is a different tool than cargo.

* Apply suggestion from @WillLillis

Co-authored-by: Will Lillis <will.lillis24@gmail.com>

---------

Co-authored-by: Christian Clason <ch.clason+github@icloud.com>
Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-06-26 08:10:43 -04:00
Christian Clason 8668f2c74f build(deps): bump wasmtime-c-api to v36.0.12 2026-06-26 14:07:35 +02:00
Christian Clason cc85df9440 build(deps): bump wasmtime-c-api to v36.0.11 2026-06-25 22:54:45 -04:00
ALancki1 b360d655cc Clarify behavior of advance(..., skip) in external scanner docs
Clarifies the behavior of `TSLexer::advance` when `skip=true`.

The previous documentation didn't make it clear that `skip=true` should only be used before a token starts. Using it after `mark_end` can affect the token’s starting position and lead to incorrect or zero-length ranges.

Fixes #2315
2026-06-25 22:54:18 -04:00
Will Lillis a8338eccef fix(dsl): forbid invalid field names 2026-06-24 17:38:21 -04:00
dependabot[bot] 68316916e0 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.3...v7.0.0)

Updates `vmactions/omnios-vm` from 1.3.2 to 1.3.3
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1.3.2...v1.3.3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:04:47 +02:00
dependabot[bot] 29e38c284e build(deps): bump cc from 1.2.64 to 1.2.65 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.64 to 1.2.65
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.64...cc-v1.2.65)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.65
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 23:46:36 +02:00
Will Lillis d861eab22b fix(build): use std helper rather than passing -std=c11 flag
MSVC expects `/std:c11`
2026-06-16 23:55:43 -04:00
Will Lillis 6b7e298549 fix(lib): address strict aliasing violations in TreeCursor type 2026-06-16 23:55:43 -04:00
dependabot[bot] 04bac19343 ci: bump vmactions/omnios-vm from 1 to 1.3.2 in the actions group
Bumps the actions group with 1 update: [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `vmactions/omnios-vm` from 1 to 1.3.2
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1...v1.3.2)

---
updated-dependencies:
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 10:40:26 +02:00
dependabot[bot] 6ad626c686 build(deps): bump js-yaml from 4.1.1 to 4.2.0 in /lib/binding_web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 10:39:58 +02:00
Christian Clason d3314cb62e build(deps): bump rquickjs to v0.12.0 2026-06-15 18:49:48 -04:00
dependabot[bot] d77f764dcd build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [cc](https://github.com/rust-lang/cc-rs) and [memchr](https://github.com/BurntSushi/memchr).


Updates `cc` from 1.2.63 to 1.2.64
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.63...cc-v1.2.64)

Updates `memchr` from 2.8.1 to 2.8.2
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.1...2.8.2)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.64
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: memchr
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 23:44:16 +02:00
buckynbrocko 49f5027158 Added tree-sitter test option --show-diff-markers to force use of + & - while still coloring output
Also cleanup the trailing newline special casing in the `TestDiff`
`Display` impl.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-06-13 10:29:37 -04:00
Will Lillis 3ed12db8f0 fix(rust): use more accurate types for various IoError structs 2026-06-13 09:35:12 -04:00
Christian Clason de4b622223 build(deps): bump wasmtime-c-api to v36.0.10 2026-06-10 17:36:02 -04:00
Christian Clason 2fb79a4325 build(deps): bump binaryen to v130 2026-06-10 17:25:15 -04:00
jannschu 519d511488 fix(cli): highlight test did not properly check for spans on later rows
The early exit condition to skip remaining highlights was incorrectly
checking the lexicographical order, which could lead to false
passes if a highlight was on a later row with smaller column number.
2026-06-08 18:09:03 -04:00
dependabot[bot] 811b5308f8 ci: bump actions/checkout from 6.0.2 to 6.0.3 in the actions group
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.2...v6.0.3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 23:50:59 +02:00
Will Lillis 4455da7102 fix(rust): more clippy fixes in generate
Additional clippy lints fire when certain modules are temporarily made
public, i.e. for testing purposes. Fixing these reduces clutter and
helps with internal development.
2026-06-05 17:31:40 -04:00
Will Lillis 4154ff23c0 fix(docs): explicitly warn against aliasing supertypes 2026-06-05 17:31:26 -04:00
Will Lillis d10b7272d2 fix(cli): warn user for various malformed tests
- attribute on same line of name
- typo'd attribute
- no closing `=====` line after the test body
2026-06-05 17:31:09 -04:00
Will Lillis d9acc99734 fix(cli): display warning to user if parser test corpus dir isn't found 2026-06-05 17:30:52 -04:00
Will Lillis 0617a59ed6 fix: add DESCRIPTION to grammar Makefile template 2026-06-05 09:43:49 +02:00
dependabot[bot] 5742052982 build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [cc](https://github.com/rust-lang/cc-rs), [log](https://github.com/rust-lang/log) and [memchr](https://github.com/BurntSushi/memchr).


Updates `cc` from 1.2.62 to 1.2.63
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.62...cc-v1.2.63)

Updates `log` from 0.4.29 to 0.4.30
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

Updates `memchr` from 2.8.0 to 2.8.1
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.0...2.8.1)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.63
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: log
  dependency-version: 0.4.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: memchr
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 12:06:15 +02:00
dependabot[bot] 94d419832e ci: bump actions/checkout from 6 to 6.0.2 in the actions group
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v6.0.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 12:05:42 +02:00
Trevor Burnham ff4dbc0589
perf(bindings): defer importlib.resources import in python template (#5589)
Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
2026-06-01 23:08:00 -04:00
Will Lillis 15ddfb21ed fix(lib): Consider subtree lookahead bytes when determining whether the
parser can reuse a node.

Lookahead bytes can be used to decide what a node is parsed as, so it's
resaonable to consider this as part of a node's "range" when deciding
which edits affect it.
2026-06-01 18:12:56 -04:00
jannschu f541fec5ad fix(cli): infinite loop in highlight test 2026-05-31 12:37:38 -04:00
Will Lillis cd338a7a43 fix(loader): skip --no-undefined for sanitizer grammar builds
Compiling a grammar with `-fsanitize=address` (or any other sanitizer)
emits a module constructor in the parser object file that references
runtime symbols like `__asan_init`. These runtime-resolved symbols are
incompatible with `-Wl,--no-undefined`, which breaking sanitized test
runs on clang. (gcc happens to paper over this by auto-linking libasan
into the .so)

To fix, detect `-fsanitize=` in the compile flags and omit `--no-undefined`
in that case.
2026-05-31 10:56:40 -04:00
Will Lillis ceef7d179f fix(lib): rewrite ts_subtree__write_to_string to be iterative 2026-05-31 10:56:12 -04:00
Will Lillis 797d2bddc4 fix(cli): make check_consistent_sizes iterative
The recursive helper could overflow with deeply nested trees.
2026-05-31 10:56:12 -04:00
Will Lillis e00bf7bc84 fix(cli): print fuzzing seed unconditionally 2026-05-31 10:56:12 -04:00
Will Lillis f1db12c31c fix(cli): improve soundness of cst range calculation 2026-05-31 10:55:47 -04:00
Will Lillis 4d362b4b98 feat(generate): derive Deserialize on GenerateError and all subtypes
These types all derive `Serialize` already.
2026-05-31 08:45:11 +02:00
Georges Savoundararadj 1da46327b3 fix(lib): use correct TREE_SITTER_HIDE_SYMBOLS macro name in alloc.h
The alloc.h header checked for TREE_SITTER_HIDDEN_SYMBOLS, but the
canonical macro name used everywhere else (api.h, render.rs, setup.py)
is TREE_SITTER_HIDE_SYMBOLS. This mismatch meant that defining
TREE_SITTER_HIDE_SYMBOLS (as the Python binding build does) would not
actually hide the allocator symbols in alloc.h.

Fixes tree-sitter/tree-sitter#5625

Signed-off-by: Georges Savoundararadj <savoundg@amazon.com>
2026-05-30 21:42:31 -04:00
Will Lillis 17125cefa6 fix(lib): update doc comment for ts_parser_parse 2026-05-30 04:11:53 -04:00
dependabot[bot] e39ef3e700 build(deps): bump serde_json from 1.0.149 to 1.0.150 in the cargo group
Bumps the cargo group with 1 update: [serde_json](https://github.com/serde-rs/json).


Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 09:59:47 +02:00
Will Lillis 30c00bc4fe fix(cli): place generate artifacts next to grammar file rather than cwd
- If grammar.js is passed, place it in the parent directory
- If grammar.json  is passed, place it in the grandparent directory
2026-05-24 22:12:56 -04:00
Will Lillis a5c0d24749 fix(test): restore corpus test header blank line and divider tie behavior 2026-05-23 12:32:46 -04:00
dependabot[bot] b1fa9725cf build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.5...v5.0.6)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 10:42:31 +02:00
dependabot[bot] edae7391ed ci: bump korthout/backport-action in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.5.1 to 4.5.2
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 09:51:24 +02:00
Max Brunsfeld a53c3b03a0
fix(wasm): load supertype tables for ABI 15 grammars (#5605)
The wasm store gated supertype_symbols / supertype_map_slices /
supertype_map_entries copies on abi_version > LANGUAGE_VERSION_WITH_RESERVED_WORDS,
but every other consumer (language.c, query.c) treats those tables as
present when abi_version >= LANGUAGE_VERSION_WITH_RESERVED_WORDS.

A Wasm grammar built at ABI exactly 15 with supertype_count > 0 ends up
with supertype_count copied into the native TSLanguage but supertype_map_slices
left NULL. ts_query__analyze_patterns then calls ts_language_subtypes,
which dereferences self->supertype_map_slices[supertype] and crashes.
2026-05-18 16:33:55 -04:00
Will Lillis f535c3bb97 fix(test): write fixture headers once
Several test functions compile the same grammar fixture.
Tests sharing a grammar name also share a src_dir. Each test also
unconditionally writes the three tree-sitter headers into
src_dir/tree_sitter/, leading to a race.

Write the three headers exactly once per src_dir, controlled via a
global `HashSet`.
2026-05-17 19:55:37 -04:00
Will Lillis 71040925fe perf(cli): stream CST rendering via Display wrappers
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-05-17 15:02:08 -04:00
Will Lillis ffea4e8e58 fix(cli): rework coloring abstraction and fix NO_COLOR handling
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-05-17 15:02:08 -04:00
Christian Clason d4e7f9793c build(deps): bump wasi-sdk to v33 2026-05-16 11:45:48 +02:00
Christian Clason 3a260021dd build(deps): bump wasmtime-c-api to v36.0.9 2026-05-16 11:45:39 +02:00
dependabot[bot] a858378ce7 build(deps): bump cc from 1.2.61 to 1.2.62 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.61 to 1.2.62
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.61...cc-v1.2.62)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.62
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 09:13:21 +02:00
dependabot[bot] aa83bf0402 ci: bump korthout/backport-action in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.5.0 to 4.5.1
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 09:13:01 +02:00
Will Lillis 50bb81484d fix(generate): consider reserved words when removing unused rules 2026-05-10 23:53:48 -04:00
Will Lillis 3b328005c9 fix(generate): rewrite parse_grammar with forward DFS
The previous implementation of `InputGrammar::normalize` (inlined  in
`parse_grammar` iterated over every variable, checking whether  it was
reachable _backwards_ from teh root by recursing over rules that
referenced it.

This means that each and every top level call re-traversed the entire
graph. The runtime performance of this backwards walk was dependent on
the _order_ of rules as declared in `grammar.js`. All existing grammars
have an ordering that's reasonably friendly to this iteration pattern
(BFS-ish order, top down from the start rule), but this leaves us open
to a catastrophic performance cliff.

Instead, seed a `used` set with the start rule, the word token, and an
names referenced from `extras`/`externals`. Then propagate via direct rule
references. This yields anywhere from a 2-~2200x speedup for
`parse_grammar`. This greatly speeds up `--no-parser` runs, but is
relatively unimportant for `parser.c` generation for _existing_
grammars. The important piece is eliminating the potential cliff.
2026-05-10 23:53:48 -04:00
Will Lillis a376ad491f fix(generate): improve error message for nonterminals used in immediate token rule 2026-05-10 10:39:48 -04:00
𝙽!𝙻 21cfae7b56
Validate Wasm language memory reads (#5569) 2026-05-06 11:54:31 -07:00
Will Lillis 5cac4316db fix(cli): account for process versions > 5 in the parse command's pretty
debug output.

The initial implementation of `--debug pretty` assumed process version
was bounded by `MAX_VERSION_COUNT`. However, we also have to account for
`MAX_VERSION_COUNT_OVERFLOW` as well as `halted_version_count`.
2026-05-05 18:17:07 -04:00
dependabot[bot] bc2802abed build(deps): bump clap_complete from 4.6.2 to 4.6.3 in the cargo group
Bumps the cargo group with 1 update: [clap_complete](https://github.com/clap-rs/clap).


Updates `clap_complete` from 4.6.2 to 4.6.3
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.2...clap_complete-v4.6.3)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 08:19:48 +02:00
dependabot[bot] 16e48018b3 ci: bump korthout/backport-action in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.4.0 to 4.5.0
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 08:19:21 +02:00
Christian Clason 85f985a778 build(deps): bump wasmtime-c-api to v36.0.8 2026-05-05 08:19:02 +02:00
Will Lillis 8d737aa238 docs: note zero point unbounded behavior in query functions 2026-04-30 04:28:59 -04:00
dependabot[bot] aedab6e0f2 build(deps): bump postcss from 8.5.8 to 8.5.12 in /lib/binding_web
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.12.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.12)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-29 11:12:37 +02:00
dependabot[bot] fb3e029911 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [korthout/backport-action](https://github.com/korthout/backport-action) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `korthout/backport-action` from 4 to 4.4.0
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4...v4.4.0)

Updates `actions/setup-node` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6.3.0...v6.4.0)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 09:30:25 +02:00
dependabot[bot] 6c674cb5bd build(deps): bump cc from 1.2.60 to 1.2.61 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.60 to 1.2.61
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.60...cc-v1.2.61)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.61
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 09:29:50 +02:00
Wilco Kruijer fb348c3493 wasm: add Language.loadSync for synchronous loading
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-04-26 18:58:50 -04:00
Amaan Qureshi 43dc8eadbe query: fix finished state heap invariants 2026-04-26 18:03:55 -04:00
Will Lillis 123fb1c13c perf(query): use min-heap for finished_states in next_capture
`ts_query_cursor_next_capture` linearly scanned all finished states to
find the one with the earliest next capture byte offset. With deeply
nested code, this O(n) scan per capture caused the highlight crate to
hang for minutes on large files.

Replace the linear scan with a min-heap over the finished_states array,
keyed by (next_capture_byte_offset, pattern_index, id). The heap is
maintained lazily: ts_query_cursor__advance uses plain array_push
(preserving FIFO insertion order), and next_capture sifts new elements
into the heap on entry via a tracked heap_size boundary. This preserves
the documented "order found" guarantee for next_match while giving
next_capture O(log n) per call.
2026-04-26 18:03:55 -04:00
Will Lillis 361f293a73 fix(query): widen capture list pool from uint16_t to uint32_t
Commit 1f6eac55 ("query: Use uint32_t for capture list IDs") widened
QueryState.capture_list_id to uint32_t and removed the 65536 pool cap,
but left the pool function signatures as uint16_t. This caused silent
truncation when the pool exceeded 65535 entries, leading to a segfault.
2026-04-26 18:03:55 -04:00
Amaan Qureshi e24c109c82 ci: run on illumos 2026-04-26 13:23:32 -04:00
Petr Sumbera 77cb23fccc lib: add Solaris support to the portable endian header
Solaris does not provide <endian.h> or <sys/endian.h>, but it does expose
byte-order definitions and conversion helpers via <sys/isa_defs.h> and
<sys/byteorder.h>.

Add a __sun branch so the portable header defines __BYTE_ORDER and the
htobe*/le*toh conversions on Solaris.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-04-26 13:23:32 -04:00
Daniel Jalkut 0f6780b9a3 lexer: pass code-unit length to U16_NEXT in UTF-16 decoders
`ts_decode_utf16_le` and `ts_decode_utf16_be` passed a byte length to
`U16_NEXT_LE` and `U16_NEXT_BE`, but those macros count `uint16_t` code
units. If a lead surrogate was the last code unit in a chunk, the decoder
could peek past the chunk and combine it with adjacent memory.

This commit passes the code-unit length to the UTF-16 macros, and fails
with `TS_DECODE_ERROR` when a chunk is too short to contain even one full
code unit. This lets the lexer retry with a fresh chunk or advance through
the invalid byte as it already does.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-04-26 03:38:47 -04:00
Amaan Qureshi 475c48d1e3 docs: add AI policy
Co-authored-by: Christian Clason <c.clason@uni-graz.at>
2026-04-26 02:58:56 -04:00
Will Lillis 457eb295b7 fix(generate): pass default optimization level in
`generate_parser_for_grammar`

Passing `empty` here causes some grammars (i.e. tree-sitter-cpp) to fail
to generate.
2026-04-24 08:52:18 +02:00
Antonin Delpeuch eccef997df
feat(dist): enable install via cargo binstall (#5533) 2026-04-24 08:52:01 +02:00
Will Lillis 15154504de fix(ci): include fixture lockfile in cache hash 2026-04-23 03:31:00 -04:00
Will Lillis d6e33d0722 test: bump c test fixture to v0.24.2 2026-04-23 03:31:00 -04:00
Volker Mische 4cb11acd46
fix(loader): allow filenames with dots (#5529) 2026-04-22 03:41:04 -04:00
dependabot[bot] 4701df97c0 build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [clap_complete](https://github.com/clap-rs/clap), [webbrowser](https://github.com/amodm/webbrowser-rs) and [bitflags](https://github.com/bitflags/bitflags).


Updates `clap_complete` from 4.6.1 to 4.6.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.2)

Updates `webbrowser` from 1.2.0 to 1.2.1
- [Release notes](https://github.com/amodm/webbrowser-rs/releases)
- [Changelog](https://github.com/amodm/webbrowser-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amodm/webbrowser-rs/compare/v1.2.0...v1.2.1)

Updates `bitflags` from 2.11.0 to 2.11.1
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.11.0...2.11.1)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: webbrowser
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: bitflags
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:38:08 +02:00
dependabot[bot] 5c1dd38c45 ci: bump actions/setup-node from 6 to 6.3.0 in the actions group
Bumps the actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 6 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v6.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:37:49 +02:00
Chinmay Dalal 20bdf3a8a0
feat(bindings): update zig template to 0.16 2026-04-20 09:31:18 +02:00
changw98ic aff9b9d92e fix: update broken link to Parser.Language in template
The link `https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html` returns a 404 error.

In the current typedoc output, the Language interface is at `/interfaces/Language.html` (without the `Parser.` prefix), as the documentation flattens namespace members.

Fixes #5525
2026-04-17 08:20:05 +02:00
Chinmay Dalal 24a64db7cc build(zig): update to zig 0.16 2026-04-15 14:54:53 +02:00
dependabot[bot] b3a752aa5b ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) and [actions/github-script](https://github.com/actions/github-script).


Updates `actions/upload-pages-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

Updates `actions/github-script` from 8 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 08:19:47 +02:00
dependabot[bot] 63b7d3b811 build(deps): bump rand from 0.10.0 to 0.10.1
Bumps [rand](https://github.com/rust-random/rand) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 08:18:47 +02:00
Christian Clason 75fe13e7aa build(deps): cargo update 2026-04-11 18:29:00 -04:00
Christian Clason 8178cfddd8 build(deps): bump wasmtime-c-api to v36.0.7 2026-04-10 00:41:06 +02:00
Christian Clason 78714eb1b8 ci(nvim-ts): further improvements to Neovim build
* Build Neovim on Windows as well
* Use latest stable release instead of `HEAD` as base
* Run parser check (prints ABI)
2026-04-07 22:55:22 +02:00
dependabot[bot] b018c20018 build(deps): bump vite from 7.3.1 to 7.3.2 in /lib/binding_web
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:10:18 +02:00
Christian Clason aa2c18895b ci(nvim-ts): build nvim against PR lib
Problem: The nvim-treesitter tests use a prebuilt Neovim, which means
changes to the library are not tested and parser generation is locked at
the ABI Neovim supports.

Solution: Build Neovim against the PR version of the library on Ubuntu
and macOS. On Windows, we still download binaries (because one does not
simply build Neovim on Windows...) so we still can check backward
compatibility.
2026-04-05 13:27:17 +02:00
Will Lillis ec0f2b59ee perf(generate): use bitsets for coincident tokens and TokenConflictMap
- replace `Vec<Vec<ParseStateId>>` with a flat bitset in
  `CoincidentTokenIndex` for membership queries

The previous representation stored full lists of parse state IDs for
each token pair. Since only membership (yes/no) is needed for most
queries, a flat bitset (`Vec<u64>` indexed as `a * n + b`) is
better. Both `(a,b)` and `(b,a)` bits are set during construction
so `contains()` needs no min/max normalization. The original
`entries: Vec<Vec<ParseStateId>>` and `states_with()` method are
retained alongside the bitset, because `identify_keywords` needs
to iterate over the specific parse states where two tokens co-occur
rather than scanning all states. ~8% walltime reduction for
tree-sitter-bash.

- add word-aligned per-row bitsets to `CoincidentTokenIndex` for
  vectorized intersection checks

Row `a` spans `[a * row_words .. (a+1) * row_words]` where
`row_words = n.div_ceil(64)`. This enables callers in
`build_lex_table` to perform word-level AND operations against
`TokenSet::terminal_bits_words()` instead of iterating individual
token indices. ~6% walltime reduction on tree-sitter-bash.

- add `TokenSet::terminal_bits_words()` accessor and `#[inline]` on
  `BitVec::insert_all`

Expose the raw `&[u64]` backing the terminal bitset so callers can
perform word-level bitwise intersection. Mark `insert_all` as
`#[inline]` to allow the compiler to optimize the hot OR loop.
2026-04-04 17:58:29 -04:00
Will Lillis 13a7a04e79 perf(generate): optimize NFA operations and token conflict precomputation
- precompute a flat NFA-state-to-variable-index lookup table

Build a `Vec<usize>` mapping each NFA state ID to its owning
variable index up front in `TokenConflictMap::new`, replacing
repeated binary searches in `compute_conflict_status` with
array lookups. Also simplifies `mark_fragile_tokens` to
collect terminal indices into a `Vec` rather than maintaining
a boolean mask.

- replace `FxHashSet<Vec<u32>>` visited set with `FxHashSet<u64>`
  using a hash-of-states key

In `compute_conflict_status`, the visited-state-set previously
stored cloned `Vec<u32>` NFA state sets. Replace this with an
`FxHashSet<u64>` keyed by hashing the sorted state slice via
`FxHasher`. This eliminates the `Vec` clone on every BFS step.

- reduce allocations in `NfaCursor::group_transitions`

Reuse a single `CharacterSet` buffer across iterations via `assign`
and `mem::take` instead of cloning the input on every raw
transition. Also, replace `Vec::insert` with `Vec::push` when
splitting intersection transitions. The final sort makes
mid-loop ordering irrelevant, and the disjointness of the split
sets guarantees correctness.

- pre-allocate `NfaTransition` result vectors with a capacity of 8
  and use `swap_remove` instead of `remove` when merging duplicate
  transition entries

- lazily compute `within_separator` in `compute_conflict_status`

Most BFS states have no completions, so the separator check
(which iterates all cursor transitions) is never needed in those
iterations. Wrap it in `Option::get_or_insert_with` to defer
computation until first use.

- add `NfaCursor::transitions_and_any_sep` to fuse the transitions
  and separator-check passes

Callers like `build_lex_table::add_state` previously called
`transitions()` and then `transition_chars().any(|sep|)` separately,
iterating the raw NFA transitions twice. The fused method computes
both in a single pass.
2026-04-04 17:58:29 -04:00
Will Lillis f1142b6e42 perf(generate): use FxHasher over default hash implementation
Replace `std::collections::{HashMap, HashSet}` with
`rustc_hash::{FxHashMap, FxHashSet}` throughout `generate`. `FxHash`
is _much_ faster than the default SipHash for small integer keys,
which are used extensively through `generate`. The one "risk" here is
that `FxHash` is more vulnerable to DDOS attacks, but that isn't as much
of a concern for us here.
2026-04-04 17:58:29 -04:00
Will Lillis 6c8ac80be9 perf(generate): optimize token conflict analysis and symbol handling
- use bitflags for `TokenConflictStatus`

Replace the individual boolean fields on the conflict status type
with a `bitflags!` representation. This takes up significantly less
space and allows for faster combination/testing in `token_conflicts`.

- hoist loop bounds in the merge-join loops of `states_conflict`

Extract `Vec::len()` outside of `while` loops to avoid recomputing
bounds on every iteration. Remove the now-unused `row_offset` /
`does_conflict_at` indirection on `TokenConflictMap`, inlining the
conflict check directly.

- optimize `variable_index_for_nfa_state` in `grammars.rs`

Small optimization to the NFA state-to-variable index mapping.

- refactor symbol key into a proper newtype

Extract the raw `u64` symbol key bit manipulation into a `key_index`
helper function for some extra type safety.
2026-04-04 17:58:29 -04:00
Will Lillis d4db0a32f0 perf(generate): optimize minimize_parse_table with precomputation and compact types
- use a boolean `Vec` instead of `Vec::contains` in `dedup`

Replace linear containment checks (`split_state_ids.contains()`)
with a constant lookup indexed by state ID.

- narrow parse state IDs from `usize` to `u16`

Reduces memory footprint and improves packing in `ParseTable` and
`build_parse_table`. This is more cache-friendly and reduces peak rss by
~1-2%.

- precompute lookup tables for parse table minimization

Build several maps up front (symbol-to-action, nonterminal indices,
symbol keys) rather than recomputing them in the hot minimization
loop. The `SymbolKey` optimization is the largest single win: it
packs each `Symbol`'s type tag and index into a single u64, enabling
single-instruction comparisons in the merge-join that dominates
`states_conflict`. This gives a significant speedup for larger
grammars.

- store ParseTableEntry` refs in the precomputed entry maps to
  avoid `IndexMap::get_index` lookups in the inner loop of
  `states_conflict`

This eliminates an indirect lookup per comparison in the merge-join.

- use unchecked array accesses in hot loops

Where indices are known valid from construction (e.g. the merge-join
in `states_conflict` and dedup), skip bounds checks in the innermost
loops. The compiler can _usually_ prove this and remove the checks
itself, but not always.
2026-04-04 17:58:29 -04:00
Will Lillis 86d99e4f8e fix(ci): include wasi-sdk and binaryen version files in fixture cache
hash
2026-04-04 17:03:58 -04:00
Christian Clason 83d61ea20e build(deps): bump binaryen to v129 2026-04-04 21:32:49 +02:00
Christian Clason 35d4954030 feat(generate): don't require tree-sitter.json for ABI 15
Problem: Many parser projects (about 100 out of ~330 tracked by
nvim-treesitter) still do not contain a `tree-sitter.json` config file,
so `tree-sitter generate` refuses to generate ABI 15 parsers due to the
lack of version information. This prevents these parsers from profiting
from other improvements in ABI 15+ and will eventually make them
unusable when ABI 14 support is dropped.

Solution: Simply use a default version `0.0.0` if no `tree-sitter.json`
is found.
2026-04-04 11:14:43 +02:00
Christian Clason bf37664420 ci(nvim-ts): minor improvements
* remove obsolete msvc-dev-cmd action (runners come with build tools)
* build tree-sitter CLI with --profile optimize (longer build time due
  to LTO, but generate workflow should profit)
* increase generate parallelism to 3
2026-04-04 04:38:21 -04:00
dependabot[bot] 422d7cc503 build(deps): bump the npm group across 1 directory with 4 updates
Bumps the npm group with 4 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [dts-buddy](https://github.com/sveltejs/dts-buddy), [esbuild](https://github.com/evanw/esbuild) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).

Updates `@types/node` from 24.10.1 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dts-buddy` from 0.6.2 to 0.7.0
- [Release notes](https://github.com/sveltejs/dts-buddy/releases)
- [Changelog](https://github.com/sveltejs/dts-buddy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/dts-buddy/compare/v0.6.2...v0.7.0)

Updates `esbuild` from 0.27.1 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/v0.27.4/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.4)

Updates `typescript-eslint` from 8.48.1 to 8.58.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: dts-buddy
  dependency-version: 0.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-04 01:21:32 -04:00
Christian Clason 3a4245ba23 build(deps): cargo update 2026-04-04 01:20:58 -04:00
dependabot[bot] 7c1e6861fb build(deps): bump the npm group across 1 directory with 4 updates
Bumps the npm group with 4 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [dts-buddy](https://github.com/sveltejs/dts-buddy), [esbuild](https://github.com/evanw/esbuild) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 24.10.1 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dts-buddy` from 0.6.2 to 0.7.0
- [Release notes](https://github.com/sveltejs/dts-buddy/releases)
- [Changelog](https://github.com/sveltejs/dts-buddy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/dts-buddy/compare/v0.6.2...v0.7.0)

Updates `esbuild` from 0.27.1 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/v0.27.4/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.4)

Updates `typescript-eslint` from 8.48.1 to 8.58.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: dts-buddy
  dependency-version: 0.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 17:46:45 +02:00
Christian Clason 64698af1ac ci(actions): bump actions/cache to v5
Apparently dependabot doesn't cover the actions/cache/action.yml.
2026-04-01 01:35:49 -04:00
Will Lillis ba17ca14a0 fix(loader): remove CROSS_RUNNER temorary directory path
This isn't used in CI anymore.
2026-03-31 04:00:43 -04:00
Will Lillis 8eafafaa16 fix(test): split up cli testing test
Now that the loader race condition is resolved, these tests no longer
need to be grouped together.
2026-03-31 04:00:43 -04:00
Will Lillis f303a957a3 fix(loader): replace flock-based compilation lock with atomic lock file
The previous two-phase locking scheme (probe existing lock file, then
create and compile) had TOCTOU races between phases that caused
spurious failures in CI when tests compiled grammars concurrently.

Replace with a single-phase approach using `create_new` as the sole
synchronization primitive. An RAII `LockFile` guard ensures cleanup
on drop (including panics). Only "builders" attempt to acquire the
lock. Loaders simply load the file. Builders compile a temporary path
and then rename, so loaders are guaranteed a valid shared library.
The winning builder compiles and then drops the lock. Losers poll for
lock file removal, then load. Stale locks from killed processes are
detected via a timeout and an appropriate error message is displayed
to the user.
2026-03-31 04:00:43 -04:00
Will Lillis 3294a64027 perf(cli): replace regex with manual parsing in test file parser
This started as a simple one to one rewrite, just removing the regexes,
and quickly devolved into a rewrite of the test parsing logic. In
addition to the memory enhancements, the general flow should be much
clearer now. A few data points:

- JS:   walltime -1.3%, peak rss -10.2%
- C:    walltime -5.7%, peak rss -4.5%
- Rust: walltime -3.8%, peak rss -2.2%
2026-03-31 04:00:43 -04:00
Franklin Chen 7c2e757c03 generate: avoid panicking when a supertype only has hidden external token children
This commit skips adding entries to the subtype map when the subtypes
list is empty to avoid a lookup failure in the topological sort during
node type generation.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-03-31 02:12:56 -04:00
futsuuu 0d4f5cb13f fix(loader): don't discard CC wrapper command specified in CC variable 2026-03-31 01:43:43 -04:00
Max Brunsfeld d3ff0ce81d
Fix wasm loading of languages w/ multiple reserved word sets (#5475) 2026-03-30 22:27:57 -07:00
Will Lillis 791d7cead4 perf(cli): minor allocation and write call reductions
- Cache tree byte range length to avoid redundant FFI calls per test
- Combine 5 separate XML attribute write! calls into one
- Pre-allocate format_sexp output buffer to avoid repeated growth
2026-03-31 01:27:05 -04:00
Will Lillis 827bcdabd9 perf(cli): buffer stdout in parse and query output
Wrap stdout in a 64KB BufWriter when writing `parse` output. The tree
walking loop makes many small write calls (parentheses, indentation,
node kinds, ranges, etc.) which are expensive without buffering.

When parsing the jquery.js corpus file, cuts the total time roughly in
half. These savings only show when piping the result to a file,
otherwise terminal rendering time usually dominates, hiding all gains.

Also do the same for the `query` command's output.
2026-03-31 01:27:05 -04:00
Will Lillis 1ec3b3aabe generate: remove comment cleaning for grammar.json
A quick search through github shows that nobody utilizes this, so it's
effectively dead code.
2026-03-31 01:23:15 -04:00
Will Lillis b53ddd3d54 perf(loader): extract name from grammar.json without regex
We have to pay the cost of compiling the regex at runtime, and the
`LazyLock` overhead for each access. The pattern is simple enough that
we can manually parse and extract.

`grammar_json_name` is now ~116x faster on cold start, ~5.6x faster warm
(after the regex has been compiled). Both are fast enough to not matter
much in practice, but some perf gains and eliminating global state is a
win.
2026-03-31 01:23:15 -04:00
dependabot[bot] 1fcbacfff9 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/configure-pages](https://github.com/actions/configure-pages) and [actions/deploy-pages](https://github.com/actions/deploy-pages).


Updates `actions/configure-pages` from 5 to 6
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

Updates `actions/deploy-pages` from 4 to 5
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 00:34:05 +02:00
dependabot[bot] e65b7f2875 build(deps): bump cc from 1.2.57 to 1.2.58 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.57 to 1.2.58
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.57...cc-v1.2.58)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.58
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 00:24:03 +02:00
Will Lillis 01dc3dba18 fix(parser): sort error recovery reductions for deterministic ordering
The `LookaheadIterator` visits symbols in group order for small parse
states, which can produce `reduce_actions` in a different order than the
original linear symbol scan (pre c1379718). Since reductions are applied
sequentially and the last reduction version survives, different orderings
lead to different error recovery outcomes (e.g. losing nodes from ERROR
trees).

Sort the reduce_actions array by symbol (descending) after collection.
The array is typically 1-5 entries, so the insertion sort cost is
negligible and the full optimization speedup is preserved.
2026-03-29 21:36:29 -04:00
Will Lillis 0535b0ca37 fix(lib): correct various typos 2026-03-28 05:23:10 -04:00
Will Lillis 9a48405a87 fix(docs): correct various typos 2026-03-28 05:23:10 -04:00
Will Lillis cf8719f132 fix(rust): correct various typos 2026-03-28 05:23:10 -04:00
Will Lillis 1a82df3cac fix(rust): address new nightly lints 2026-03-28 05:23:10 -04:00
Will Lillis c1379718ab perf(parser): use LookaheadIterator in error recovery's all-symbol scan
In `ts_parser__do_all_potential_reductions`, when `lookahead_symbol` is
0 (error recovery), the code scanned every symbol from 1 to
`token_count` calling ts_language_table_entry for each with most returning
empty. Replace with `LookaheadIterator` which efficiently visits only symbols
with valid actions, yielding 7-20x fewer lookups for typical grammars.

Error recovery throughput improves ~29% (JS) / ~39% (C). Valid-code
parsing is unaffected.
2026-03-28 05:21:50 -04:00
Aaron Paterson cb01ffcc3f docs: update WASM build requirements for wasi-sdk
Since v0.26.1, `tree-sitter build --wasm` uses wasi-sdk instead of
Emscripten. The CLI automatically downloads wasi-sdk on first use,
so Emscripten, Docker, and Podman are no longer required.

Update the web binding README to reflect this change.

See #4393 for the original switch to wasi-sdk.
2026-03-27 15:07:54 +01:00
dependabot[bot] cc2ed5e85f build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.2 to 5.0.5.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.2...v5.0.5)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 14:57:11 +01:00
dependabot[bot] e957e8ce0d build(deps): bump flatted from 3.4.1 to 3.4.2 in /crates/cli/eslint
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.4.1 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.4.1...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 14:55:25 +01:00
dependabot[bot] 6821fa7611 build(deps): bump flatted from 3.3.2 to 3.4.2 in /lib/binding_web
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.2 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.2...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 14:55:06 +01:00
dependabot[bot] fd77bda97a build(deps): bump cc from 1.2.56 to 1.2.57 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.56 to 1.2.57
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.56...cc-v1.2.57)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.57
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 23:02:18 +01:00
dependabot[bot] 8769254bda ci: bump actions/create-github-app-token in the actions group
Bumps the actions group with 1 update: [actions/create-github-app-token](https://github.com/actions/create-github-app-token).


Updates `actions/create-github-app-token` from 2 to 3
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 23:01:49 +01:00
Will Lillis 08d676b417 fix(lib): document invariants that must be upheld for TSInputEdit 2026-03-22 23:17:01 -04:00
Will Lillis df3fa372a6 fix(generate): allow disabling qjs-rt feature from CLI
The workspace dependency for `tree-sitter-generate` did not set
`default-features = false`, so Cargo always enabled its default
features (including `qjs-rt` and thus `rquickjs`) regardless of
the CLI's `--no-default-features` flag.

Additionally, `tree-sitter-generate` failed to compile without the
`load` feature due to unconditional references to `cfg`-gated items.

- Set `default-features = false` on the workspace `tree-sitter-generate`
  dependency so the CLI's feature forwarding actually takes effect.
- Explicitly enable the `load` feature in the CLI's dependency on
  `tree-sitter-generate`, since the CLI needs `load`-gated functions
  unconditionally.
- Gate necessary imports behind `#[cfg(feature = "load")]` to fix
  `tree-sitter-generate`'s build without the `load` feature.
2026-03-21 05:51:10 -04:00
Will Lillis 6011985a6c fix(loader): unconditionally import Mutex
Also, remove  the `LazyLock` for `WASM_TOOL_LOCK`. This isn't necessary
as `Mutex::new` is valid in const contexts
2026-03-17 02:27:50 -04:00
dependabot[bot] af67543764 build(deps): bump anstyle from 1.0.13 to 1.0.14 in the cargo group
Bumps the cargo group with 1 update: [anstyle](https://github.com/rust-cli/anstyle).


Updates `anstyle` from 1.0.13 to 1.0.14
- [Commits](https://github.com/rust-cli/anstyle/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: anstyle
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 22:56:08 +01:00
dependabot[bot] 94f48e3ebf build(deps): bump flatted from 3.3.1 to 3.4.1 in /crates/cli/eslint
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.1 to 3.4.1.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.1...v3.4.1)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 19:19:06 +01:00
Christian Clason 0dd08ce747 ci(release): publish zip archives for tree-sitter-cli
Problem: Publishing the built tree-sitter-cli artifacts as gzipped
binaries prevents installing them with `cargo binstall` or packaging
them for Windows Package Manager (`winget`).

Solution: Also create a zip archive for each binary, named
`tree-sitter-cli` to match now-common downstream usage. (Most package
managers have separate `tree-sitter-cli` and `tree-sitter` (library)
packages.)
2026-03-13 15:52:21 -04:00
Will Lillis 30d9c675a4 docs: indicate that dashes are not permitted in parser names 2026-03-13 15:35:14 -04:00
Will Lillis 30ed8da439 Revert "feat: allow - in grammar names"
This reverts commit 7d3c321253.
2026-03-13 15:35:14 -04:00
Christian Clason 5f7326afd5 build(deps): bump binaryen to v127 2026-03-13 15:31:37 -04:00
Christian Clason 2c7375bdb2 build(deps): bump wasi-sdk to v32 2026-03-13 15:31:37 -04:00
Christian Clason a8845c066c build(deps): cargo update 2026-03-13 15:28:46 -04:00
Christian Clason 8d656ed837 ci(tests): run required tests on all PRs
Problem: `build` tests are marked as "required" but skipped when only
touching documentation, leading to such PRs not being mergable.

Solution: Run tests on all PRs and all commits to the `master` or
`release-0.x` branches.
2026-03-13 15:21:47 -04:00
Riley Bruins cf302b07d1 fix(query): don't add copies for quantifier steps outside alternations 2026-03-04 10:32:50 +01:00
Riley Bruins a95fff5477 refactor(query): remove alternative_is_immediate
This is implied by `is_pass_through`.
2026-03-04 02:44:53 -05:00
dependabot[bot] 495b069667 ci: bump the actions group with 3 updates
Bumps the actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/upload-artifact` from 6 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

Updates `actions/download-artifact` from 7 to 8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

Updates `actions/attest-build-provenance` from 3 to 4
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/attest-build-provenance
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 00:17:34 +01:00
Will Lillis b27de1038f fix(rust): remove unused dependencies from loader and lib 2026-03-02 09:14:28 +01:00
Laurent Cheylus 2f747dc9b1 fix(loader): link with libc on OpenBSD to compile parser
Fix tree-sitter/tree-sitter#5333

Signed-off-by: Laurent Cheylus <foxy@free.fr>
2026-03-01 17:07:10 -05:00
John Eismeier 1f9b274dfb
chore: fix typos in messages and documentation
Signed-off-by: John E <jeis4wpi@outlook.com>
2026-03-01 11:40:15 +01:00
301 changed files with 23402 additions and 11473 deletions

View file

@ -1,2 +0,0 @@
target
.git

View file

@ -2,6 +2,14 @@ name: Bug Report
description: Report a problem
type: Bug
body:
- type: checkboxes
attributes:
label: AI Policy
description: Review our [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy).
options:
- label: I have read the AI Policy and this issue complies with it.
required: true
- type: textarea
attributes:
label: "Problem"

View file

@ -2,6 +2,14 @@ name: Feature request
description: Request an enhancement
type: Feature
body:
- type: checkboxes
attributes:
label: AI Policy
description: Review our [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy).
options:
- label: I have read the AI Policy and this issue complies with it.
required: true
- type: markdown
attributes:
value: |

View file

@ -10,7 +10,7 @@ outputs:
runs:
using: composite
steps:
- uses: actions/cache@v4
- uses: actions/cache@v5
id: cache
with:
path: |
@ -21,6 +21,9 @@ runs:
'lib/src/parser.h',
'lib/src/array.h',
'lib/src/alloc.h',
'lib/src/wasm/wasm-stdlib.h',
'lib/src/wasm-stdlib/external_scanner_stdlib.h',
'crates/loader/wasi-sdk-version',
'crates/loader/binaryen-version',
'test/fixtures/grammars/*/**/src/*.c',
'test/fixtures/fixtures.json',
'.github/actions/cache/action.yml') }}

2
.github/cliff.toml vendored
View file

@ -69,4 +69,4 @@ sort_commits = "oldest"
[remote.github]
owner = "tree-sitter"
repo = "tree-sitter"
repo = "tree-sitter"

6
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,6 @@
### AI Policy
- [ ] I have read the [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy) and this PR complies with it.
- [ ] If AI tools were used: I have disclosed the tool and extent of usage below.
<!-- If you used AI tools, state which tool and how it was used. Delete this section if not applicable. -->

View file

@ -1,16 +0,0 @@
module.exports = async ({ github, context }) => {
const requestedReviewers = await github.rest.pulls.listRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const reviewers = requestedReviewers.data.users.map((e) => e.login);
github.rest.pulls.removeRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: reviewers,
});
};

View file

@ -5,21 +5,31 @@ module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: files } = await github.rest.pulls.listFiles({
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber
pull_number: prNumber,
per_page: 100
});
const changedFiles = files.map(file => file.filename);
const wasmStdLibSrc = 'crates/language/wasm/';
const dirChanged = changedFiles.some(file => file.startsWith(wasmStdLibSrc));
const wasmStdLibSources = [
'lib/src/wasm-stdlib/external_scanner_allocator.c',
'lib/src/wasm-stdlib/imports.txt',
'lib/src/wasm-stdlib/libc.c',
'lib/src/wasm-stdlib/stdio.c'
];
const dirChanged = changedFiles.some(file =>
wasmStdLibSources.includes(file) ||
file.startsWith('lib/src/wasm-stdlib/libc/ctype/') ||
file.startsWith('lib/src/wasm-stdlib/libc/string/')
);
if (!dirChanged) return;
const wasmStdLibHeader = 'lib/src/wasm/wasm-stdlib.h';
const wasmStdLibHeader = 'lib/src/wasm-stdlib/external_scanner_stdlib.h';
const requiredChanged = changedFiles.includes(wasmStdLibHeader);
if (!requiredChanged) core.setFailed(`Changes detected in ${wasmStdLibSrc} but ${wasmStdLibHeader} was not modified.`);
if (!requiredChanged) core.setFailed(`Changes detected in the Wasm stdlib sources but ${wasmStdLibHeader} was not modified.`);
};

View file

@ -14,17 +14,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
with:
persist-credentials: true
ref: ${{ github.event.pull_request.base.ref }}
- name: Create app token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.BACKPORT_APP }}
private-key: ${{ secrets.BACKPORT_KEY }}
- name: Create backport PR
uses: korthout/backport-action@v4
uses: korthout/backport-action@v4.6.0
with:
pull_title: "${pull_title}"
label_pattern: "^ci:backport ([^ ]+)$"

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1

View file

@ -26,12 +26,14 @@ jobs:
- windows-x86
- macos-arm64
- macos-x64
- illumos-x64
- wasm32
include:
# When adding a new `target`:
# 1. Define a new platform alias above
# 2. Add a new record to the matrix map in `crates/cli/npm/install.js`
# 3. Consider adding the mapping at the end of 'crates/cli/Cargo.toml' for cargo-binstall support
- { platform: linux-arm64 , target: aarch64-unknown-linux-gnu , os: ubuntu-24.04-arm }
- { platform: linux-arm , target: armv7-unknown-linux-gnueabihf , os: ubuntu-24.04-arm }
- { platform: linux-x64 , target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
@ -44,6 +46,9 @@ jobs:
- { platform: macos-x64 , target: x86_64-apple-darwin , os: macos-15-intel }
- { platform: wasm32 , target: wasm32-unknown-unknown , os: ubuntu-24.04 }
# illumos is not supported OOTB, it runs in a vm
- { platform: illumos-x64 , target: x86_64-unknown-illumos , os: ubuntu-24.04 , vm: true , no-run: true }
# Extra features
- { platform: linux-arm64 , features: wasm }
- { platform: linux-x64 , features: wasm , run-wasm-test: true }
@ -68,7 +73,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up cross-compilation
if: matrix.cross
@ -90,7 +95,7 @@ jobs:
- name: Cache Emscripten SDK
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: emsdk
key: emsdk-${{ env.EMSCRIPTEN_VERSION }}-${{ runner.os }}-${{ runner.arch }}
@ -111,17 +116,22 @@ jobs:
- name: Set up Node.js
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/setup-node@v6
uses: actions/setup-node@v7.0.0
with:
node-version: 24
cache: npm
cache-dependency-path: lib/binding_web/package-lock.json
- name: Set up Rust
if: ${{ !matrix.vm }}
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- name: Install Rust Wasm test target
if: matrix.run-wasm-test
run: rustup toolchain install nightly --profile minimal --component rust-src
- name: Install cross-compilation toolchain
if: matrix.cross
run: |
@ -191,7 +201,7 @@ jobs:
WASMTIME_REPO: https://github.com/bytecodealliance/wasmtime
- name: Build C library (make)
if: runner.os != 'Windows'
if: runner.os != 'Windows' && !matrix.vm
run: |
if [[ $PLATFORM == linux-arm ]]; then
CC=arm-linux-gnueabihf-gcc; AR=arm-linux-gnueabihf-ar
@ -205,10 +215,10 @@ jobs:
make -j CFLAGS="$CFLAGS" CC=$CC AR=$AR
env:
PLATFORM: ${{ matrix.platform }}
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
- name: Build C library (CMake)
if: "!matrix.cross"
if: "!matrix.cross && !matrix.vm"
run: |
cmake -S . -B build/static \
-DBUILD_SHARED_LIBS=OFF \
@ -227,6 +237,20 @@ jobs:
CC: ${{ contains(matrix.platform, 'linux') && 'clang' || '' }}
WASM: ${{ contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test) && 'ON' || 'OFF' }}
- name: Build C library and Rust crate (illumos gmake)
if: matrix.platform == 'illumos-x64'
uses: vmactions/omnios-vm@v1.3.6
with:
release: r151056-build
copyback: false
prepare: |
pkg install -q build-essential || [ $? -eq 4 ]
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
run: |
. "$HOME/.cargo/env"
gmake -j
cargo build -p tree-sitter
- name: Build Wasm library
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
shell: bash
@ -245,7 +269,7 @@ jobs:
run: cargo check --no-default-features --target='${{ matrix.target }}'
- name: Build target
if: "!inputs.run-test"
if: "!inputs.run-test && !matrix.vm"
run: cargo build --release --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}' $PACKAGE
env:
PACKAGE: ${{ matrix.platform == 'wasm32' && '-p tree-sitter' || '' }}
@ -269,15 +293,19 @@ jobs:
- name: Run main tests
if: inputs.run-test && !matrix.no-run
run: cargo test --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
run: cargo test --workspace --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
- name: Run Wasm tests
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
run: cargo run -p xtask --target='${{ matrix.target }}' -- test-wasm
- name: Run Rust Wasm web test
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
run: cargo run -p xtask --target='${{ matrix.target }}' -- test-rust-wasm-web
- name: Upload CLI artifact
if: "!inputs.run-test && !matrix.no-run"
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: tree-sitter.${{ matrix.platform }}
path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }}
@ -286,7 +314,7 @@ jobs:
- name: Upload Wasm artifacts
if: "!inputs.run-test && matrix.platform == 'linux-x64'"
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: tree-sitter.wasm
path: |

View file

@ -1,21 +1,14 @@
name: CI
on:
pull_request:
paths-ignore:
- docs/**
- "**/README.md"
- CONTRIBUTING.md
- LICENSE
- cli/src/templates
push:
branches: [master]
paths-ignore:
- docs/**
- "**/README.md"
- CONTRIBUTING.md
- LICENSE
- cli/src/templates
branches:
- 'master'
- 'release-[0-9]+.[0-9]+'
pull_request:
branches:
- 'master'
- 'release-[0-9]+.[0-9]+'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -26,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -34,10 +27,19 @@ jobs:
toolchain: stable
components: clippy, rustfmt
- name: Lint files
run: |
make lint
make lint-web
- name: Lint Rust files
run: make lint
- name: Install Taplo
uses: taiki-e/install-action@v2
with:
tool: taplo@0.10.0
- name: Lint TOML files
run: make lint-toml
- name: Lint web files
run: make lint-web
sanitize:
uses: ./.github/workflows/sanitize.yml

View file

@ -11,7 +11,7 @@ jobs:
if: contains(github.event.pull_request.labels.*.name, 'ci:check release') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1

View file

@ -16,35 +16,29 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions/checkout@v7.0.1
- name: Install mdbook
env:
GH_TOKEN: ${{ github.token }}
run: |
jq_expr='.assets[] | select(.name | contains("x86_64-unknown-linux-gnu")) | .browser_download_url'
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.4.52 --jq "$jq_expr")
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.5.4 --jq "$jq_expr")
mkdir mdbook
curl -sSL "$url" | tar -xz -C mdbook
printf '%s/mdbook\n' "$PWD" >> "$GITHUB_PATH"
- name: Install mdbook-admonish
run: cargo install mdbook-admonish
- name: Build Book
run: mdbook build docs
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: docs/book
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5

View file

@ -3,6 +3,7 @@ name: nvim-treesitter parser tests
on:
pull_request:
paths:
- 'lib/**'
- 'crates/cli/**'
- 'crates/config/**'
- 'crates/generate/**'
@ -25,34 +26,54 @@ jobs:
name: ${{ matrix.os }} - ${{ matrix.type }}
runs-on: ${{ matrix.os }}
env:
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim-win64\\bin\\nvim.exe' || 'nvim' }}
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim.exe' || 'nvim' }}
NVIM_TAG: stable
NVIM_DIR: neovim
NVIM_TS_DIR: nvim-treesitter
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo build --profile optimize
- uses: actions/checkout@v6
- name: Clone Neovim
uses: actions/checkout@v7.0.1
with:
repository: neovim/neovim
ref: ${{ env.NVIM_TAG }}
path: ${{ env.NVIM_DIR }}
- if: runner.os != 'Windows'
name: Setup environment (Posix)
run: |
echo ${{ github.workspace }}/target/optimize >> "$GITHUB_PATH"
echo ${{ github.workspace }}/neovim/build/bin >> "$GITHUB_PATH"
echo "VIMRUNTIME=${{ github.workspace }}/neovim/runtime" >> "$GITHUB_ENV"
- if: runner.os == 'Windows'
name: Setup environment (why can't you just be normal?!)
run: |
${{ env.NVIM_DIR }}/.github/scripts/env.ps1
echo ${{ github.workspace }}/target/optimize >> "$env:GITHUB_PATH"
echo ${{ github.workspace }}/neovim/build/bin >> "$env:GITHUB_PATH"
echo "VIMRUNTIME=${{ github.workspace }}/neovim/runtime" >> "$env:GITHUB_ENV"
- name: Build Neovim
working-directory: ${{ env.NVIM_DIR }}
run: |
cmake -S cmake.deps -B .deps -G Ninja -D CMAKE_BUILD_TYPE=Release -D TREESITTER_URL=https://github.com/tree-sitter/tree-sitter/archive/${{ github.event.pull_request.head.sha }}.tar.gz -D DEPS_IGNORE_SHA=TRUE
cmake --build .deps --config Release
cmake -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
cmake --build build --config Release
- name: Clone nvim-treesitter
uses: actions/checkout@v7.0.1
with:
repository: nvim-treesitter/nvim-treesitter
path: ${{ env.NVIM_TS_DIR }}
ref: main
- if: runner.os != 'Windows'
run: echo ${{ github.workspace }}/target/release >> $GITHUB_PATH
- if: runner.os == 'Windows'
run: echo ${{ github.workspace }}/target/release >> $env:GITHUB_PATH
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo build --release
- uses: ilammy/msvc-dev-cmd@v1
- name: Install and prepare Neovim
run: bash ./scripts/ci-install.sh
working-directory: ${{ env.NVIM_TS_DIR }}
- if: matrix.type == 'generate'
name: Generate and compile parsers
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=2
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=10
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash
@ -63,7 +84,13 @@ jobs:
shell: bash
- if: "!cancelled()"
name: Check query files
name: Test parsers
run: $NVIM -l ./scripts/check-parsers.lua
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash
- if: "!cancelled()"
name: Test queries
run: $NVIM -l ./scripts/check-queries.lua
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash

View file

@ -1,7 +1,14 @@
name: Release
on:
schedule:
- cron: '5 5 * * *'
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name for release'
required: false
default: nightly
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
@ -21,11 +28,22 @@ jobs:
attestations: write
contents: write
steps:
- if: github.event_name == 'workflow_dispatch'
env:
TAG_NAME: ${{ github.event.inputs.tag_name }}
run: echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_ENV
- if: github.event_name == 'schedule'
run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV
- if: github.event_name == 'push'
run: echo "TAG_NAME=${GITHUB_REF_NAME}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Download build artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
path: artifacts
@ -44,28 +62,41 @@ jobs:
for platform in $(cd artifacts; ls | sed 's/^tree-sitter\.//'); do
exe=$(ls artifacts/tree-sitter.$platform/tree-sitter*)
chmod +x $exe
gzip --stdout --name $exe > target/tree-sitter-$platform.gz
zip -j9 target/tree-sitter-cli-$platform.zip $exe
done
rm -rf artifacts
ls -l target/
- name: Generate attestations
uses: actions/attest-build-provenance@v3
uses: actions/attest-build-provenance@v4
with:
subject-path: |
target/tree-sitter-*.gz
target/tree-sitter-cli-*.zip
target/web-tree-sitter.tar.gz
- if: env.TAG_NAME == 'nightly'
run: |
echo 'PRERELEASE=--prerelease' >> $GITHUB_ENV
gh release delete nightly --yes || true
git push https://${GITHUB_ACTOR}:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY} :nightly || true
env:
GH_TOKEN: ${{ github.token }}
- name: Create release
run: |-
gh release create $GITHUB_REF_NAME \
gh release create ${{ env.TAG_NAME }} $PRERELEASE \
target/tree-sitter-*.gz \
target/tree-sitter-cli-*.zip \
target/web-tree-sitter.tar.gz
env:
GH_TOKEN: ${{ github.token }}
crates_io:
name: Publish packages to Crates.io
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
runs-on: ubuntu-latest
environment: crates
permissions:
@ -74,7 +105,7 @@ jobs:
needs: release
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -90,6 +121,7 @@ jobs:
npm:
name: Publish packages to npmjs.com
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
runs-on: ubuntu-latest
environment: npm
permissions:
@ -102,10 +134,10 @@ jobs:
directory: [crates/cli/npm, lib/binding_web]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up Node
uses: actions/setup-node@v6
uses: actions/setup-node@v7.0.0
with:
node-version: 24
registry-url: https://registry.npmjs.org

View file

@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout script
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github/scripts/close_unresponsive.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const script = require('./.github/scripts/close_unresponsive.js')
@ -35,13 +35,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout script
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github/scripts/remove_response_label.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const script = require('./.github/scripts/remove_response_label.js')

View file

@ -11,15 +11,21 @@ jobs:
remove-reviewers:
runs-on: ubuntu-latest
steps:
- name: Checkout script
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts/reviewers_remove.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
- name: Remove reviewers
uses: actions/github-script@v9
with:
script: |
const script = require('./.github/scripts/reviewers_remove.js')
await script({github, context})
const requestedReviewers = await github.rest.pulls.listRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const reviewers = requestedReviewers.data.users.map((e) => e.login);
github.rest.pulls.removeRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: reviewers,
});

View file

@ -15,7 +15,7 @@ jobs:
TREE_SITTER: ${{ github.workspace }}/target/release/tree-sitter
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Install UBSAN library
run: sudo apt-get update -y && sudo apt-get install -y libubsan1

View file

@ -16,13 +16,13 @@ jobs:
if: github.event.label.name == 'spam'
steps:
- name: Checkout script
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github/scripts/close_spam.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const script = require('./.github/scripts/close_spam.js')

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -31,7 +31,7 @@ jobs:
- name: Build C library (make)
run: make -j CFLAGS="$CFLAGS"
env:
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
- name: Build Wasm Library
working-directory: lib/binding_web

View file

@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Check directory changes
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const scriptPath = `${process.env.GITHUB_WORKSPACE}/.github/scripts/wasm_stdlib.js`;

View file

@ -1,20 +1,19 @@
[formatting]
align_entries = true
column_width = 100
compact_arrays = false
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "profile" ]
keys = [ "profile" ]
[rule.formatting]
reorder_keys = false

View file

@ -1,11 +0,0 @@
{
"lsp": {
"rust-analyzer": {
"initialization_options": {
"cargo": {
"features": "all"
}
}
}
}
}

View file

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.13)
project(tree-sitter
VERSION "0.27.0"
VERSION "0.28.0"
DESCRIPTION "An incremental parsing system for programming tools"
HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/"
LANGUAGES C)
@ -33,7 +33,8 @@ if(MSVC)
else()
target_compile_options(tree-sitter PRIVATE
-Wall -Wextra -Wshadow -Wpedantic
-Werror=incompatible-pointer-types)
-Werror=incompatible-pointer-types
-Werror=strict-aliasing -Wstrict-aliasing=2)
endif()
if(TREE_SITTER_FEATURE_WASM)

BIN
Cargo.lock generated

Binary file not shown.

View file

@ -14,22 +14,22 @@ members = [
resolver = "2"
[workspace.package]
authors = [ "Max Brunsfeld <maxbrunsfeld@gmail.com>", "Amaan Qureshi <amaanq12@gmail.com>" ]
categories = [ "command-line-utilities", "parsing" ]
edition = "2024"
homepage = "https://tree-sitter.github.io/tree-sitter"
keywords = [ "incremental", "parsing" ]
license = "MIT"
repository = "https://github.com/tree-sitter/tree-sitter"
authors = [ "Max Brunsfeld <maxbrunsfeld@gmail.com>", "Amaan Qureshi <amaanq12@gmail.com>" ]
categories = [ "command-line-utilities", "parsing" ]
edition = "2024"
homepage = "https://tree-sitter.github.io/tree-sitter"
keywords = [ "incremental", "parsing" ]
license = "MIT"
repository = "https://github.com/tree-sitter/tree-sitter"
rust-version = "1.90"
version = "0.27.0"
version = "0.28.0"
[workspace.lints.clippy]
cargo = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
dbg_macro = "deny"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
todo = "deny"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
todo = "deny"
# The lints below are a specific subset of the pedantic+nursery lints
# that we explicitly allow in the tree-sitter codebase because they either:
@ -39,49 +39,50 @@ todo = "deny"
# 3. Worsen the code
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
if_not_else = "allow"
items_after_statements = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
multiple_crate_versions = "allow"
option_if_let_else = "allow"
similar_names = "allow"
string_lit_as_bytes = "allow"
struct_excessive_bools = "allow"
too_many_lines = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
if_not_else = "allow"
items_after_statements = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
multiple_crate_versions = "allow"
option_if_let_else = "allow"
similar_names = "allow"
string_lit_as_bytes = "allow"
struct_excessive_bools = "allow"
too_many_lines = "allow"
tuple_array_conversions = "allow"
[workspace.lints.rust]
mismatched_lifetime_syntaxes = "allow"
[profile.optimize]
inherits = "release"
codegen-units = 1 # Maximum size reduction optimizations.
lto = true # Link-time optimization.
opt-level = 3 # Optimization level 3.
strip = true # Automatically strip symbols from the binary.
inherits = "release"
codegen-units = 1 # Maximum size reduction optimizations.
lto = true # Link-time optimization.
opt-level = 3 # Optimization level 3.
strip = true # Automatically strip symbols from the binary.
[profile.size]
inherits = "optimize"
opt-level = "s" # Optimize for size.
inherits = "optimize"
opt-level = "s" # Optimize for size.
[profile.release-dev]
inherits = "release"
codegen-units = 256
debug = true
inherits = "release"
codegen-units = 256
debug = true
debug-assertions = true
incremental = true
lto = false
overflow-checks = true
incremental = true
lto = false
overflow-checks = true
[workspace.dependencies]
ansi_colours = "1.2.3"
anstyle = "1.0.13"
anstyle = "1.0.14"
anyhow = "1.0.102"
bstr = "1.12.1"
cc = "1.2.56"
bstr = "1.12.1"
cc = "1.2.63"
clap = { features = [
"cargo",
"derive",
@ -90,31 +91,32 @@ clap = { features = [
"string",
"unstable-styles",
], version = "4.5.58" }
clap_complete = "4.5.66"
clap_complete = "4.6.3"
clap_complete_nushell = "4.5.10"
crc32fast = "1.5.0"
ctor = "0.6.3"
ctrlc = { features = [ "termination" ], version = "3.5.2" }
ctor = "0.6.3"
ctrlc = { features = [ "termination" ], version = "3.5.2" }
dialoguer = { features = [ "fuzzy-select" ], version = "0.12.0" }
etcetera = "0.11.0"
glob = "0.3.3"
hashbrown = { default-features = false, version = "0.17.1" }
heck = "0.5.0"
html-escape = "0.2.13"
indexmap = "2.13.0"
indoc = "2.0.7"
libloading = "0.9.0"
log = { features = [ "std" ], version = "0.4.29" }
memchr = "2.8.0"
log = { features = [ "std" ], version = "0.4.30" }
memchr = "2.8.1"
once_cell = "1.21.3"
pretty_assertions = "1.4.1"
rand = "0.10.0"
regex = "1.12.3"
rand = "0.10.1"
regex = "1.12.3"
regex-syntax = "0.8.9"
rustc-hash = "2.1.1"
schemars = "1.2.1"
semver = { features = [ "serde" ], version = "1.0.27" }
serde = { features = [ "derive" ], version = "1.0.228" }
serde_json = { features = [ "preserve_order" ], version = "1.0.149" }
schemars = "1.2.1"
semver = { features = [ "serde" ], version = "1.0.27" }
serde = { features = [ "derive" ], version = "1.0.228" }
serde_json = { features = [ "preserve_order" ], version = "1.0.150" }
similar = "2.7.0"
streaming-iterator = "0.1.9"
tempfile = "3.25.0"
@ -124,13 +126,13 @@ topological-sort = "0.2.2"
unindent = "0.2.4"
walkdir = "2.5.0"
wasmparser = "0.244.0"
webbrowser = "1.1.0"
webbrowser = "1.2.1"
tree-sitter = { path = "./lib", version = "0.27.0" }
tree-sitter-config = { path = "./crates/config", version = "0.27.0" }
tree-sitter-generate = { path = "./crates/generate", version = "0.27.0" }
tree-sitter-highlight = { path = "./crates/highlight", version = "0.27.0" }
tree-sitter-loader = { path = "./crates/loader", version = "0.27.0" }
tree-sitter-tags = { path = "./crates/tags", version = "0.27.0" }
tree-sitter = { path = "./lib", version = "0.28.0" }
tree-sitter-config = { path = "./crates/config", version = "0.28.0" }
tree-sitter-generate = { default-features = false, path = "./crates/generate", version = "0.28.0" }
tree-sitter-highlight = { path = "./crates/highlight", version = "0.28.0" }
tree-sitter-loader = { path = "./crates/loader", version = "0.28.0" }
tree-sitter-tags = { path = "./crates/tags", version = "0.28.0" }
tree-sitter-language = { path = "./crates/language", version = "0.1" }
tree-sitter-language = { path = "./crates/language", version = "0.1.8" }

View file

@ -1,10 +0,0 @@
FROM rust:1.76-buster
WORKDIR /app
RUN apt-get update
RUN apt-get install -y nodejs
COPY . .
CMD cargo test --all-features

View file

@ -1,4 +1,4 @@
VERSION := 0.27.0
VERSION := 0.28.0
DESCRIPTION := An incremental parsing system for programming tools
HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/
@ -22,7 +22,7 @@ OBJ := $(SRC:.c=.o)
# define default flags, and override to append mandatory flags
ARFLAGS := rcs
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
override CFLAGS += -std=c11 -fPIC -fvisibility=hidden
override CFLAGS += -D_POSIX_C_SOURCE=200112L -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_DARWIN_C_SOURCE
override CFLAGS += -Ilib/src -Ilib/src/wasm -Ilib/include
@ -129,8 +129,13 @@ lint-web:
npm --prefix lib/binding_web ci
npm --prefix lib/binding_web run lint
lint-toml:
taplo check
taplo format --check --diff
format:
cargo fmt --all
taplo format
changelog:
@git-cliff --config .github/cliff.toml --prepend CHANGELOG.md --latest --github-token $(shell gh auth token)

View file

@ -1,35 +0,0 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "TreeSitter",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "TreeSitter",
targets: ["TreeSitter"]),
],
targets: [
.target(name: "TreeSitter",
path: "lib",
exclude: [
"src/unicode/ICU_SHA",
"src/unicode/README.md",
"src/unicode/LICENSE",
"src/wasm/stdlib-symbols.txt",
"src/lib.c",
],
sources: ["src"],
publicHeadersPath: "include",
cSettings: [
.headerSearchPath("src"),
.define("_POSIX_C_SOURCE", to: "200112L"),
.define("_DEFAULT_SOURCE"),
.define("_BSD_SOURCE"),
.define("_DARWIN_C_SOURCE"),
]),
],
cLanguageStandard: .c11
)

View file

@ -4,51 +4,56 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var threaded: std.Io.Threaded = .init(b.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
const wasm = b.option(bool, "enable-wasm", "Enable Wasm support") orelse false;
const shared = b.option(bool, "build-shared", "Build a shared library") orelse false;
const amalgamated = b.option(bool, "amalgamated", "Build using an amalgamated source") orelse false;
var tree_sitter = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
});
const lib: *std.Build.Step.Compile = b.addLibrary(.{
.name = "tree-sitter",
.linkage = if (shared) .dynamic else .static,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
}),
.root_module = tree_sitter,
});
if (amalgamated) {
lib.addCSourceFile(.{
tree_sitter.addCSourceFile(.{
.file = b.path("lib/src/lib.c"),
.flags = &.{"-std=c11"},
});
} else {
const files = try findSourceFiles(b);
const files = try findSourceFiles(b, io);
defer b.allocator.free(files);
lib.addCSourceFiles(.{
tree_sitter.addCSourceFiles(.{
.root = b.path("lib/src"),
.files = files,
.flags = &.{"-std=c11"},
});
}
lib.addIncludePath(b.path("lib/include"));
lib.addIncludePath(b.path("lib/src"));
lib.addIncludePath(b.path("lib/src/wasm"));
tree_sitter.addIncludePath(b.path("lib/include"));
tree_sitter.addIncludePath(b.path("lib/src"));
tree_sitter.addIncludePath(b.path("lib/src/wasm"));
lib.root_module.addCMacro("_POSIX_C_SOURCE", "200112L");
lib.root_module.addCMacro("_DEFAULT_SOURCE", "");
lib.root_module.addCMacro("_BSD_SOURCE", "");
lib.root_module.addCMacro("_DARWIN_C_SOURCE", "");
tree_sitter.addCMacro("_POSIX_C_SOURCE", "200112L");
tree_sitter.addCMacro("_DEFAULT_SOURCE", "");
tree_sitter.addCMacro("_BSD_SOURCE", "");
tree_sitter.addCMacro("_DARWIN_C_SOURCE", "");
if (wasm) {
if (b.lazyDependency(wasmtimeDep(target.result), .{})) |wasmtime| {
lib.root_module.addCMacro("TREE_SITTER_FEATURE_WASM", "");
lib.addSystemIncludePath(wasmtime.path("include"));
lib.addLibraryPath(wasmtime.path("lib"));
if (shared) lib.linkSystemLibrary("wasmtime");
tree_sitter.addCMacro("TREE_SITTER_FEATURE_WASM", "");
tree_sitter.addSystemIncludePath(wasmtime.path("include"));
tree_sitter.addLibraryPath(wasmtime.path("lib"));
if (shared) tree_sitter.linkSystemLibrary("wasmtime", .{});
}
}
@ -122,14 +127,14 @@ pub fn wasmtimeDep(target: std.Target) []const u8 {
);
}
fn findSourceFiles(b: *std.Build) ![]const []const u8 {
fn findSourceFiles(b: *std.Build, io: std.Io) ![]const []const u8 {
var sources: std.ArrayListUnmanaged([]const u8) = .empty;
var dir = try b.build_root.handle.openDir("lib/src", .{ .iterate = true });
var dir = try b.build_root.handle.openDir(io, "lib/src", .{ .iterate = true });
var iter = dir.iterate();
defer dir.close();
defer dir.close(io);
while (try iter.next()) |entry| {
while (try iter.next(io)) |entry| {
if (entry.kind != .file) continue;
const file = entry.name;
const ext = std.fs.path.extension(file);

View file

@ -1,8 +1,8 @@
.{
.name = .tree_sitter,
.fingerprint = 0x841224b447ac0d4f,
.version = "0.27.0",
.minimum_zig_version = "0.14.1",
.version = "0.28.0",
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
@ -13,83 +13,83 @@
},
.dependencies = .{
.wasmtime_c_api_aarch64_android = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-android-c-api.tar.xz",
.hash = "N-V-__8AAIfPIgdw2YnV3QyiFQ2NHdrxrXzzCdjYJyxJDOta",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-android-c-api.tar.xz",
.hash = "N-V-__8AAIp_mQVzQOITXcYcWxYLJkvB1W1SvLlrdiU2G7fj",
.lazy = true,
},
.wasmtime_c_api_aarch64_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-linux-c-api.tar.xz",
.hash = "N-V-__8AAIt97QZi7Pf7nNJ2mVY6uxA80Klyuvvtop3pLMRK",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-linux-c-api.tar.xz",
.hash = "N-V-__8AAMztsgU5Aj4oI3MRHXJVe5rW72op-kT_78I3kZVM",
.lazy = true,
},
.wasmtime_c_api_aarch64_macos = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-macos-c-api.tar.xz",
.hash = "N-V-__8AAAO48QQf91w9RmmUDHTja8DrXZA1n6Bmc8waW3qe",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-macos-c-api.tar.xz",
.hash = "N-V-__8AANZxOwT27sdrKxDDGGKsiwtcZlHy204xAWNIgDBH",
.lazy = true,
},
.wasmtime_c_api_aarch64_musl = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-musl-c-api.tar.xz",
.hash = "N-V-__8AAI196wa9pwADoA2RbCDp5F7bKQg1iOPq6gIh8-FH",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-musl-c-api.tar.xz",
.hash = "N-V-__8AAJL1zQW9yxC98uc60lSuVUHhH77QHTto0zwIQnBj",
.lazy = true,
},
.wasmtime_c_api_aarch64_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-windows-c-api.zip",
.hash = "N-V-__8AAC9u4wXfqd1Q6XyQaC8_DbQZClXux60Vu5743N05",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-windows-c-api.zip",
.hash = "N-V-__8AAHRCtQU93hJcRFOgVcof3IQpRV9stT2Pp54wpJc2",
.lazy = true,
},
.wasmtime_c_api_armv7_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-armv7-linux-c-api.tar.xz",
.hash = "N-V-__8AAHXe8gWs3s83Cc5G6SIq0_jWxj8fGTT5xG4vb6-x",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-armv7-linux-c-api.tar.xz",
.hash = "N-V-__8AAJaW6gT8QdULOU0jxX4a_DOCA5YD6cxWBC8IqhQF",
.lazy = true,
},
.wasmtime_c_api_i686_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-i686-linux-c-api.tar.xz",
.hash = "N-V-__8AAN2pzgUUfulRCYnipSfis9IIYHoTHVlieLRmKuct",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-linux-c-api.tar.xz",
.hash = "N-V-__8AANguMgVX4XMhdOVkdj4yfFKXrG8RTgZDs3nQB8J8",
.lazy = true,
},
.wasmtime_c_api_i686_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-i686-windows-c-api.zip",
.hash = "N-V-__8AAJu0YAUUTFBLxFIOi-MSQVezA6MMkpoFtuaf2Quf",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-windows-c-api.zip",
.hash = "N-V-__8AANY9ggXg4rK2_1o3EIlrCq124l5RfykPv-DPforq",
.lazy = true,
},
.wasmtime_c_api_riscv64gc_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-riscv64gc-linux-c-api.tar.xz",
.hash = "N-V-__8AAG8m-gc3E3AIImtTZ3l1c7HC6HUWazQ9OH5KACX4",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-riscv64gc-linux-c-api.tar.xz",
.hash = "N-V-__8AAPDtCAdQ0dD9Rs-qWl-kPr2c7L3PVsUVjwy12Iz1",
.lazy = true,
},
.wasmtime_c_api_s390x_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-s390x-linux-c-api.tar.xz",
.hash = "N-V-__8AAH314gd-gE4IBp2uvAL3gHeuW1uUZjMiLLeUdXL_",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-s390x-linux-c-api.tar.xz",
.hash = "N-V-__8AANA3BwY1ZOoGCWCR_tTY9G1vfIX128RGVxufN3ov",
.lazy = true,
},
.wasmtime_c_api_x86_64_android = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-android-c-api.tar.xz",
.hash = "N-V-__8AAIPNRwfNkznebrcGb0IKUe7f35bkuZEYOjcx6q3f",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-android-c-api.tar.xz",
.hash = "N-V-__8AAF4AIgY0ltjevj1ybGfvMU1ErPRnNve5X1TmvCru",
.lazy = true,
},
.wasmtime_c_api_x86_64_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-linux-c-api.tar.xz",
.hash = "N-V-__8AAI8EDwcyTtk_Afhk47SEaqfpoRqGkJeZpGs69ChF",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-linux-c-api.tar.xz",
.hash = "N-V-__8AAIR0cAbjf3DkrTbu81Oq_zociz-0lCpb5DR0lIC9",
.lazy = true,
},
.wasmtime_c_api_x86_64_macos = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-macos-c-api.tar.xz",
.hash = "N-V-__8AAGtGNgVaOpHSxC22IjrampbRIy6lLwscdcAE8nG1",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-macos-c-api.tar.xz",
.hash = "N-V-__8AAFJ4lgRgCBnYdz8-Yfc4hLve45Hv-0RICAEuCp4s",
.lazy = true,
},
.wasmtime_c_api_x86_64_mingw = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-mingw-c-api.zip",
.hash = "N-V-__8AAPS2PAbVix50L6lnddlgazCPTz3whLUFk1qnRtnZ",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-mingw-c-api.zip",
.hash = "N-V-__8AAMxZxQZUpp1cU8J5zgLiMNq4e4dy0hcchlQFy03J",
.lazy = true,
},
.wasmtime_c_api_x86_64_musl = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-musl-c-api.tar.xz",
.hash = "N-V-__8AAF-WEQe0nzvi09PgusM5i46FIuCKJmIDWUleWgQ3",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-musl-c-api.tar.xz",
.hash = "N-V-__8AAN5pWgZrZBt8VYWkN82WjyFe3DkGcE7uLn3jpt38",
.lazy = true,
},
.wasmtime_c_api_x86_64_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-windows-c-api.zip",
.hash = "N-V-__8AAKGNXwbpJQsn0_6kwSIVDDWifSg8cBzf7T2RzsC9",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-windows-c-api.zip",
.hash = "N-V-__8AAEIJkgaVHFETgakbognNUpFELuV17vpjw6NjsWhQ",
.lazy = true,
},
},

View file

@ -1,18 +1,17 @@
[package]
name = "tree-sitter-cli"
version.workspace = true
description = "CLI tool for developing, testing, and using Tree-sitter parsers"
authors.workspace = true
edition.workspace = true
name = "tree-sitter-cli"
version.workspace = true
description = "CLI tool for developing, testing, and using Tree-sitter parsers"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-cli"
license.workspace = true
keywords.workspace = true
categories.workspace = true
include = [ "build.rs", "README.md", "LICENSE", "benches/*", "src/**" ]
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-cli"
license.workspace = true
keywords.workspace = true
categories.workspace = true
include = [ "build.rs", "README.md", "LICENSE", "benches/*", "src/**" ]
[lints]
workspace = true
@ -21,63 +20,96 @@ workspace = true
path = "src/tree_sitter_cli.rs"
[[bin]]
doc = false
doc = false
name = "tree-sitter"
path = "src/main.rs"
[[bench]]
harness = false
name = "benchmark"
name = "benchmark"
[features]
default = [ "qjs-rt" ]
qjs-rt = [ "tree-sitter-generate/qjs-rt" ]
wasm = [ "tree-sitter/wasm", "tree-sitter-loader/wasm" ]
qjs-rt = [ "tree-sitter-generate/qjs-rt" ]
wasm = [ "tree-sitter/wasm", "tree-sitter-loader/wasm" ]
[dependencies]
ansi_colours.workspace = true
anstyle.workspace = true
anyhow.workspace = true
bstr.workspace = true
clap.workspace = true
clap_complete.workspace = true
ansi_colours.workspace = true
anstyle.workspace = true
anyhow.workspace = true
bstr.workspace = true
clap.workspace = true
clap_complete.workspace = true
clap_complete_nushell.workspace = true
crc32fast.workspace = true
ctor.workspace = true
ctrlc.workspace = true
dialoguer.workspace = true
glob.workspace = true
heck.workspace = true
html-escape.workspace = true
indoc.workspace = true
log.workspace = true
memchr.workspace = true
rand.workspace = true
regex.workspace = true
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
similar.workspace = true
streaming-iterator.workspace = true
thiserror.workspace = true
tiny_http.workspace = true
walkdir.workspace = true
wasmparser.workspace = true
webbrowser.workspace = true
crc32fast.workspace = true
ctor.workspace = true
ctrlc.workspace = true
dialoguer.workspace = true
glob.workspace = true
heck.workspace = true
html-escape.workspace = true
indoc.workspace = true
log.workspace = true
memchr.workspace = true
rand.workspace = true
regex.workspace = true
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
similar.workspace = true
streaming-iterator.workspace = true
thiserror.workspace = true
tiny_http.workspace = true
walkdir.workspace = true
wasmparser.workspace = true
webbrowser.workspace = true
tree-sitter.workspace = true
tree-sitter-config.workspace = true
tree-sitter-generate.workspace = true
tree-sitter.workspace = true
tree-sitter-config.workspace = true
tree-sitter-generate = { features = [ "load" ], workspace = true }
tree-sitter-highlight.workspace = true
tree-sitter-loader.workspace = true
tree-sitter-tags.workspace = true
tree-sitter-loader.workspace = true
tree-sitter-tags.workspace = true
[dev-dependencies]
encoding_rs = "0.8.35"
encoding_rs = "0.8.35"
tree_sitter_proc_macro = { package = "tree-sitter-tests-proc-macro", path = "src/tests/proc_macro" }
widestring = "1.2.1"
widestring = "1.2.1"
pretty_assertions.workspace = true
tempfile.workspace = true
unindent.workspace = true
tempfile.workspace = true
unindent.workspace = true
[package.metadata.binstall]
pkg-fmt = "zip"
[package.metadata.binstall.overrides.aarch64-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm64{ archive-suffix }"
[package.metadata.binstall.overrides.armv7-unknown-linux-gnueabihf]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm{ archive-suffix }"
[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x64{ archive-suffix }"
[package.metadata.binstall.overrides.i686-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x86{ archive-suffix }"
[package.metadata.binstall.overrides.powerpc64-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-powerpc64{ archive-suffix }"
[package.metadata.binstall.overrides.aarch64-pc-windows-msvc]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-arm64{ archive-suffix }"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-x64{ archive-suffix }"
[package.metadata.binstall.overrides.i686-pc-windows-msvc]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-x86{ archive-suffix }"
[package.metadata.binstall.overrides.aarch64-apple-darwin]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macos-arm64{ archive-suffix }"
[package.metadata.binstall.overrides.x86_64-apple-darwin]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macos-x64{ archive-suffix }"

View file

@ -12,16 +12,15 @@ The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars fr
### Installation
You can install the `tree-sitter-cli` with `cargo`:
You can install the `tree-sitter-cli` with [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):
```sh
cargo install --locked tree-sitter-cli
cargo binstall tree-sitter-cli
```
or with `npm`:
or you can build it from source:
```sh
npm install tree-sitter-cli
cargo install --locked tree-sitter-cli
```
You can also download a pre-built binary for your platform from [the releases page].

View file

@ -2,7 +2,6 @@ use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
str,
sync::LazyLock,
time::Instant,
};
@ -10,6 +9,8 @@ use std::{
use anyhow::Context;
use log::info;
use tree_sitter::{Language, Parser, Query};
#[cfg(feature = "wasm")]
use tree_sitter::{WasmStore, wasmtime};
use tree_sitter_loader::{CompileConfig, Loader};
include!("../src/tests/helpers/dirs.rs");
@ -21,8 +22,11 @@ static EXAMPLE_FILTER: LazyLock<Option<String>> =
static REPETITION_COUNT: LazyLock<usize> = LazyLock::new(|| {
env::var("TREE_SITTER_BENCHMARK_REPETITION_COUNT").map_or(5, |s| s.parse::<usize>().unwrap())
});
static WASM: LazyLock<bool> = LazyLock::new(|| env::var_os("TREE_SITTER_BENCHMARK_WASM").is_some());
static TEST_LOADER: LazyLock<Loader> =
LazyLock::new(|| Loader::with_parser_lib_path(SCRATCH_DIR.clone()));
#[cfg(feature = "wasm")]
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(Default::default);
#[expect(
clippy::type_complexity,
@ -95,7 +99,11 @@ fn main() {
}
info!("\nLanguage: {language_name}");
let language = get_language(language_path);
let language = if *WASM {
get_wasm_language(language_name, &mut parser)
} else {
get_language(language_path)
};
parser.set_language(&language).unwrap();
info!(" Constructing Queries");
@ -215,3 +223,32 @@ fn get_language(path: &Path) -> Language {
.with_context(|| format!("Failed to load language at path {}", src_path.display()))
.unwrap()
}
#[cfg(feature = "wasm")]
fn get_wasm_language(language_name: &str, parser: &mut Parser) -> Language {
let wasm_language_name = language_name.replace('-', "_");
let wasm_path = ROOT_DIR
.join("target")
.join("release")
.join(format!("tree-sitter-{language_name}.wasm"));
let wasm = fs::read(&wasm_path)
.with_context(|| {
format!(
"Failed to read {}. Generate Wasm fixtures with `cargo xtask generate-fixtures --wasm`",
wasm_path.display()
)
})
.unwrap();
let mut store = WasmStore::new(&WASM_ENGINE).expect("Failed to create Wasm store");
let language = store
.load_language(&wasm_language_name, &wasm)
.with_context(|| format!("Failed to load Wasm language at {}", wasm_path.display()))
.unwrap();
parser.set_wasm_store(store).unwrap();
language
}
#[cfg(not(feature = "wasm"))]
fn get_wasm_language(_language_name: &str, _parser: &mut Parser) -> Language {
panic!("Wasm benchmarking requires the `wasm` feature");
}

View file

@ -135,29 +135,43 @@
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@ -289,9 +303,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -629,9 +643,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"license": "ISC",
"peer": true
},

View file

@ -3,18 +3,22 @@ type BlankRule = { type: 'BLANK' };
type ChoiceRule = { type: 'CHOICE'; members: Rule[] };
type FieldRule = { type: 'FIELD'; name: string; content: Rule };
type ImmediateTokenRule = { type: 'IMMEDIATE_TOKEN'; content: Rule };
type PatternRule = { type: 'PATTERN'; value: string };
type PatternRule = { type: 'PATTERN'; value: string; flags?: string };
type PrecedenceValue = string | number;
type PrecDynamicRule = { type: 'PREC_DYNAMIC'; content: Rule; value: number };
type PrecLeftRule = { type: 'PREC_LEFT'; content: Rule; value: number };
type PrecRightRule = { type: 'PREC_RIGHT'; content: Rule; value: number };
type PrecRule = { type: 'PREC'; content: Rule; value: number };
type PrecLeftRule = { type: 'PREC_LEFT'; content: Rule; value: PrecedenceValue };
type PrecRightRule = { type: 'PREC_RIGHT'; content: Rule; value: PrecedenceValue };
type PrecRule = { type: 'PREC'; content: Rule; value: PrecedenceValue };
type Repeat1Rule = { type: 'REPEAT1'; content: Rule };
type RepeatRule = { type: 'REPEAT'; content: Rule };
type ReservedRule = { type: 'RESERVED'; content: Rule; context_name: string };
type SeqRule = { type: 'SEQ'; members: Rule[] };
type StringRule = { type: 'STRING'; value: string };
type SymbolRule<Name extends string> = { type: 'SYMBOL'; name: Name };
type PrecedenceEntry = StringRule | SymbolRule<string>;
type TokenRule = { type: 'TOKEN'; content: Rule };
type EOFRule = { type: 'EOF' };
type Rule =
| AliasRule
@ -33,7 +37,8 @@ type Rule =
| SeqRule
| StringRule
| SymbolRule<string>
| TokenRule;
| TokenRule
| EOFRule;
declare class RustRegex {
value: string;
@ -86,8 +91,8 @@ interface Grammar<
*/
precedences?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[][],
) => RuleOrLiteral[][],
previous: PrecedenceEntry[][],
) => (string | PrecedenceEntry)[][],
/**
* An array of arrays of rule names. Each inner array represents a set of
@ -101,8 +106,8 @@ interface Grammar<
*/
conflicts?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[][],
) => RuleOrLiteral[][];
previous: SymbolRule<string>[][],
) => SymbolRule<string>[][];
/**
* An array of token names which can be returned by an _external scanner_.
@ -127,9 +132,11 @@ interface Grammar<
* specify extras: `$ => []` in your grammar.
*
* @param $ grammar rules
* @param previous array of extras from the base grammar
*/
extras?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[],
) => RuleOrLiteral[];
/**
@ -142,8 +149,8 @@ interface Grammar<
*/
inline?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[],
) => RuleOrLiteral[];
previous: SymbolRule<string>[],
) => SymbolRule<string>[];
/**
* A list of hidden rule names that should be considered supertypes in the
@ -155,8 +162,8 @@ interface Grammar<
*/
supertypes?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[],
) => RuleOrLiteral[];
previous: SymbolRule<string>[],
) => SymbolRule<string>[];
/**
* The name of a token that will match keywords for the purpose of the
@ -166,24 +173,47 @@ interface Grammar<
*
* @see https://tree-sitter.github.io/tree-sitter/creating-parsers/3-writing-the-grammar#keyword-extraction
*/
word?: ($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral;
word?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
) => SymbolRule<string>;
/**
* Mapping of names to reserved word sets. The first reserved word set is the
* global word set, meaning it applies to every rule in every parse state.
* The other word sets can be used with the `reserved` function.
* The other word sets can be used with the `reserved` function. Each callback
* receives the base grammar's reserved word set of the same name as its second
* argument, or `undefined` if no matching set exists.
*/
reserved?: Record<
string,
($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral[]
(
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[] | undefined,
) => RuleOrLiteral[]
>;
}
/**
* Return type of grammar(). The runtime evaluates and normalizes the grammar
* beneath a "grammar" key. Optional input fields become required output fields
* with default values when not provided.
*/
type GrammarSchema<RuleName extends string> = {
[K in keyof Grammar<RuleName>]: K extends 'rules'
? Record<RuleName, Rule>
: Grammar<RuleName>[K];
grammar: {
name: string;
/** Base grammar name when extending; undefined for root grammars. */
inherits: string | undefined;
rules: Record<RuleName, Rule>;
precedences: PrecedenceEntry[][];
conflicts: string[][];
externals: Rule[];
extras: Rule[];
inline: string[];
supertypes: string[];
word: string | undefined;
reserved: Record<string, Rule[]>;
};
};
/**
@ -311,7 +341,7 @@ declare const prec: {
*
* @see https://www.gnu.org/software/bison/manual/html_node/Generalized-LR-Parsing.html
*/
dynamic(value: string | number, rule: RuleOrLiteral): PrecDynamicRule;
dynamic(value: number, rule: RuleOrLiteral): PrecDynamicRule;
};
/**
@ -382,6 +412,19 @@ declare const token: {
immediate(rule: RuleOrLiteral): ImmediateTokenRule;
};
/**
* Matches the end of input. May only appear as the final symbol of a
* (possibly nested) sequence; a production ending in `eof()` reduces only
* when the lookahead is end-of-input, rather than shifting a token.
*
* Choice branches that continue past `eof()` are dropped as unreachable,
* and `eof()` is not allowed inside `token()`.
*
* Useful when a rule should match either an explicit terminator (e.g. a
* newline) or the end of the file.
*/
declare function eof(): EOFRule;
/**
* Creates a new language grammar with the provided schema.
*

View file

@ -1,12 +1,12 @@
{
"name": "tree-sitter-cli",
"version": "0.27.0",
"version": "0.28.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-cli",
"version": "0.27.0",
"version": "0.28.0",
"hasInstallScript": true,
"license": "MIT",
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "tree-sitter-cli",
"version": "0.27.0",
"version": "0.28.0",
"author": {
"name": "Max Brunsfeld",
"email": "maxbrunsfeld@gmail.com"

View file

@ -13,7 +13,7 @@
installShellFiles,
}:
let
isCross = stdenv.targetPlatform == stdenv.buildPlatform;
canRunHost = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
in
rustPlatform.buildRustPackage {
pname = "tree-sitter-cli";
@ -28,7 +28,7 @@ rustPlatform.buildRustPackage {
pkg-config
nodejs_22
]
++ lib.optionals (!isCross) [ installShellFiles ];
++ lib.optionals canRunHost [ installShellFiles ];
cargoLock.lockFile = ../../Cargo.lock;
@ -42,9 +42,9 @@ rustPlatform.buildRustPackage {
'';
preCheck = "export HOME=$TMPDIR";
doCheck = !isCross;
doCheck = canRunHost;
postInstall = lib.optionalString (!isCross) ''
postInstall = lib.optionalString canRunHost ''
installShellCompletion --cmd tree-sitter \
--bash <($out/bin/tree-sitter complete --shell bash) \
--zsh <($out/bin/tree-sitter complete --shell zsh) \

View file

@ -25,7 +25,7 @@ use crate::{
random::Rand,
},
parse::perform_edit,
test::{DiffKey, TestDiff, TestEntry, parse_tests, strip_sexp_fields},
test::{DiffKey, TestDiff, TestEntry, TestExpectation, parse_tests, render_test_output},
};
pub static LOG_ENABLED: LazyLock<bool> = LazyLock::new(|| env::var("TREE_SITTER_LOG").is_ok());
@ -65,7 +65,7 @@ pub fn new_seed() -> usize {
int_env_var("TREE_SITTER_SEED").unwrap_or_else(|| {
let mut rng = rand::rng();
let seed = rng.random_range(0..=usize::MAX);
info!("Seed: {seed}");
eprintln!("fuzz seed: {seed}");
seed
})
}
@ -144,7 +144,7 @@ pub fn fuzz_language_corpus(
.take()
.unwrap_or_default()
.into_iter()
.chain(tests.iter().filter(|x| x.skip).map(get_test_name))
.chain(tests.iter().filter(|t| t.skip()).map(get_test_name))
.map(|x| (x, 0))
.collect::<HashMap<String, usize>>();
@ -169,31 +169,8 @@ pub fn fuzz_language_corpus(
println!(" {test_index}. {test_name}");
let passed = allocations::record_checked(|| {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(language).unwrap();
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
let tree = parser.parse(&test.input, None).unwrap();
if test.error {
return true;
}
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output {
println!("Incorrect initial parse for {test_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
println!();
return false;
}
true
let check_output = !test.error();
test.check_initial_parse(language, &test_name, check_output)
})
.unwrap_or_else(|e| {
error!("{e}");
@ -257,7 +234,7 @@ pub fn fuzz_language_corpus(
// Check that the new tree is consistent.
check_consistent_sizes(&tree2, &input);
if let Err(message) = check_changed_ranges(&tree, &tree2, &input) {
error!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",);
error!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n");
return false;
}
@ -273,12 +250,9 @@ pub fn fuzz_language_corpus(
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
// Verify that the final tree matches the expectation from the corpus.
let mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
let actual_output = render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
if actual_output != test.output && !test.error {
if actual_output != test.output && !test.error() {
println!("Incorrect parse for {test_name} - seed {seed}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
@ -326,12 +300,56 @@ pub struct FlattenedTest {
pub input: Vec<u8>,
pub output: String,
pub languages: Vec<Box<str>>,
pub error: bool,
pub skip: bool,
pub expectation: TestExpectation,
pub has_fields: bool,
pub cst: bool,
pub template_delimiters: Option<(&'static str, &'static str)>,
}
impl FlattenedTest {
#[must_use]
fn skip(&self) -> bool {
self.expectation == TestExpectation::Skip
}
#[must_use]
fn error(&self) -> bool {
self.expectation == TestExpectation::Error
}
#[must_use]
pub(crate) fn check_initial_parse(
&self,
language: &Language,
display_name: &str,
check_output: bool,
) -> bool {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(language).unwrap();
set_included_ranges(&mut parser, &self.input, self.template_delimiters);
let tree = parser.parse(&self.input, None).unwrap();
if !check_output {
return true;
}
let actual_output =
render_test_output(&self.input, &tree, self.cst, self.has_fields).unwrap();
if actual_output == self.output {
true
} else {
println!("Incorrect initial parse for {display_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &self.output));
println!();
false
}
}
}
#[must_use]
pub fn flatten_tests(
test: TestEntry,
@ -374,10 +392,10 @@ pub fn flatten_tests(
name,
input,
output,
has_fields,
languages: attributes.languages,
error: attributes.error,
skip: attributes.skip,
expectation: attributes.expectation,
has_fields,
cst: attributes.cst,
template_delimiters: None,
});
}

View file

@ -10,12 +10,12 @@ use std::{
#[ctor::ctor]
unsafe fn initialize_allocation_recording() {
unsafe {
tree_sitter::set_allocator(
Some(ts_record_malloc),
Some(ts_record_calloc),
Some(ts_record_realloc),
Some(ts_record_free),
);
tree_sitter::set_allocator(Some(tree_sitter::Allocator {
malloc: ts_record_malloc,
calloc: ts_record_calloc,
realloc: ts_record_realloc,
free: ts_record_free,
}));
}
}

View file

@ -3,8 +3,20 @@ use tree_sitter::{LogType, Node, Parser, Point, Range, Tree};
use super::{LOG_ENABLED, LOG_GRAPH_ENABLED, scope_sequence::ScopeSequence};
use crate::util;
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
fn check(node: Node, line_offsets: &[usize]) {
struct SizeCheckFrame<'a> {
node: Node<'a>,
end_byte: usize,
end_point: Point,
child_count: u32,
child_index: u32,
last_child_end_byte: usize,
last_child_end_point: Point,
some_child_has_changes: bool,
actual_named_child_count: usize,
}
impl SizeCheckFrame<'_> {
fn new<'a>(node: Node<'a>, line_offsets: &[usize]) -> SizeCheckFrame<'a> {
let start_byte = node.start_byte();
let end_byte = node.end_byte();
let start_point = node.start_position();
@ -18,37 +30,21 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
);
assert_eq!(end_byte, line_offsets[end_point.row] + end_point.column);
let mut last_child_end_byte = start_byte;
let mut last_child_end_point = start_point;
let mut some_child_has_changes = false;
let mut actual_named_child_count = 0;
for i in 0..node.child_count() {
let child = node.child(i).unwrap();
assert!(child.start_byte() >= last_child_end_byte);
assert!(child.start_position() >= last_child_end_point);
check(child, line_offsets);
if child.has_changes() {
some_child_has_changes = true;
}
if child.is_named() {
actual_named_child_count += 1;
}
last_child_end_byte = child.end_byte();
last_child_end_point = child.end_position();
}
assert_eq!(actual_named_child_count, node.named_child_count());
if node.child_count() > 0 {
assert!(end_byte >= last_child_end_byte);
assert!(end_point >= last_child_end_point);
}
if some_child_has_changes {
assert!(node.has_changes());
SizeCheckFrame {
node,
end_byte,
end_point,
child_count: node.child_count(),
child_index: 0,
last_child_end_byte: start_byte,
last_child_end_point: start_point,
some_child_has_changes: false,
actual_named_child_count: 0,
}
}
}
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
let mut line_offsets = vec![0];
for (i, c) in input.iter().enumerate() {
if *c == b'\n' {
@ -56,7 +52,41 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
}
}
check(tree.root_node(), &line_offsets);
let mut stack: Vec<SizeCheckFrame> = vec![SizeCheckFrame::new(tree.root_node(), &line_offsets)];
while let Some(top) = stack.last_mut() {
if top.child_index < top.child_count {
let i = top.child_index;
let child = top.node.child(i).unwrap();
assert!(child.start_byte() >= top.last_child_end_byte);
assert!(child.start_position() >= top.last_child_end_point);
if child.has_changes() {
top.some_child_has_changes = true;
}
if child.is_named() {
top.actual_named_child_count += 1;
}
top.last_child_end_byte = child.end_byte();
top.last_child_end_point = child.end_position();
top.child_index += 1;
stack.push(SizeCheckFrame::new(child, &line_offsets));
continue;
}
let frame = stack.pop().unwrap();
assert_eq!(
frame.actual_named_child_count,
frame.node.named_child_count()
);
if frame.child_count > 0 {
assert!(frame.end_byte >= frame.last_child_end_byte);
assert!(frame.end_point >= frame.last_child_end_point);
}
if frame.some_child_has_changes {
assert!(frame.node.has_changes());
}
}
}
pub fn check_changed_ranges(old_tree: &Tree, new_tree: &Tree, input: &[u8]) -> Result<(), String> {

View file

@ -1,13 +1,13 @@
use tree_sitter::{Point, Range, Tree};
#[derive(Debug)]
pub struct ScopeSequence(Vec<ScopeStack>);
pub struct ScopeSequence<'a>(Vec<ScopeStack<'a>>);
type ScopeStack = Vec<&'static str>;
type ScopeStack<'a> = Vec<&'a str>;
impl ScopeSequence {
impl<'a> ScopeSequence<'a> {
#[must_use]
pub fn new(tree: &Tree) -> Self {
pub fn new(tree: &'a Tree) -> Self {
let mut result = Self(Vec::new());
let mut scope_stack = Vec::new();
@ -49,7 +49,7 @@ impl ScopeSequence {
for i in 0..(self.0.len().max(other.0.len())) {
let stack = &self.0.get(i);
let other_stack = &other.0.get(i);
if *stack != *other_stack && ![b'\r', b'\n'].contains(&text[i]) {
if *stack != *other_stack && !b"\r\n".contains(&text[i]) {
let containing_range = known_changed_ranges
.iter()
.find(|range| range.start_point <= position && position < range.end_point);

View file

@ -4,7 +4,6 @@ use std::{
fs,
io::{self, Write as _},
path::{self, Path, PathBuf},
str,
sync::{Arc, atomic::AtomicUsize},
time::Instant,
};
@ -12,6 +11,7 @@ use std::{
use ansi_colours::{ansi256_from_rgb, rgb_from_ansi256};
use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor};
use anyhow::Result;
use clap::ValueEnum;
use log::{info, warn};
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
use serde_json::{Value, json};
@ -26,8 +26,9 @@ pub const HTML_HEAD_HEADER: &str = "
<style>
body {
font-family: monospace
}
.line-number {
}";
pub const HTML_LINE_NUMBER_STYLE: &str = " .line-number {
user-select: none;
text-align: right;
color: rgba(27,31,35,.3);
@ -35,8 +36,7 @@ pub const HTML_HEAD_HEADER: &str = "
}
.line {
white-space: pre;
}
</style>";
}";
pub const HTML_BODY_HEADER: &str = "
</head>
@ -191,20 +191,14 @@ fn parse_style(style: &mut Style, json: Value) {
if let Value::Object(entries) = json {
for (property_name, value) in entries {
match property_name.as_str() {
"bold" => {
if value == Value::Bool(true) {
style.ansi = style.ansi.bold();
}
"bold" if value == Value::Bool(true) => {
style.ansi = style.ansi.bold();
}
"italic" => {
if value == Value::Bool(true) {
style.ansi = style.ansi.italic();
}
"italic" if value == Value::Bool(true) => {
style.ansi = style.ansi.italic();
}
"underline" => {
if value == Value::Bool(true) {
style.ansi = style.ansi.underline();
}
"underline" if value == Value::Bool(true) => {
style.ansi = style.ansi.underline();
}
"color" => {
if let Some(color) = parse_color(value) {
@ -314,12 +308,36 @@ fn terminal_supports_truecolor() -> bool {
.is_ok_and(|truecolor| truecolor == "truecolor" || truecolor == "24bit")
}
/// The kind of HTML emitted when highlighting to HTML.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum HtmlOutput {
/// A complete, self-contained document wrapping a plain
/// `<div class="highlight"><pre><code>` block.
Document,
/// A complete document with a line-number column (a `<table>` layout).
#[value(name = "line-numbers")]
NumberedDocument,
/// Only the code markup, without the surrounding document.
Fragment,
}
/// How token colors are applied in HTML output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum HtmlStyling {
/// `class="..."` spans plus a generated `<style>` carrying the theme's colors.
Classes,
/// `style="..."` spans with the colors inlined.
Inline,
/// `class="..."` spans with no colors emitted (supply your own stylesheet).
Minimal,
}
pub struct HighlightOptions {
pub theme: Theme,
pub check: bool,
pub captures_path: Option<PathBuf>,
pub inline_styles: bool,
pub html: bool,
/// `None` for regular output, `Some((layout, style))` when emitting HTML.
pub html: Option<(HtmlOutput, HtmlStyling)>,
pub quiet: bool,
pub print_time: bool,
pub cancellation_flag: Arc<AtomicUsize>,
@ -401,19 +419,25 @@ pub fn highlight(
)?;
let theme = &opts.theme;
if !opts.quiet && print_name {
// A fragment is pure code markup, so it must not be prefixed with the filename.
let html_fragment = opts
.html
.is_some_and(|(layout, _)| layout == HtmlOutput::Fragment);
if !opts.quiet && print_name && !html_fragment {
writeln!(&mut stdout, "{name}")?;
}
if opts.html {
if !opts.quiet {
if let Some((layout, style)) = opts.html {
if !opts.quiet && layout != HtmlOutput::Fragment {
writeln!(&mut stdout, "{HTML_HEAD_HEADER}")?;
writeln!(&mut stdout, " <style>")?;
let names = theme.highlight_names.iter();
let styles = theme.styles.iter();
for (name, style) in names.zip(styles) {
if let Some(css) = &style.css {
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
if layout == HtmlOutput::NumberedDocument {
writeln!(&mut stdout, "{HTML_LINE_NUMBER_STYLE}")?;
}
if style == HtmlStyling::Classes {
for (name, style) in theme.highlight_names.iter().zip(&theme.styles) {
if let Some(css) = &style.css {
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
}
}
}
writeln!(&mut stdout, " </style>")?;
@ -422,7 +446,7 @@ pub fn highlight(
let mut renderer = HtmlRenderer::new();
renderer.render(events, &source, &move |highlight, output| {
if opts.inline_styles {
if style == HtmlStyling::Inline {
output.extend(b"style='");
output.extend(
theme.styles[highlight.0]
@ -444,16 +468,29 @@ pub fn highlight(
})?;
if !opts.quiet {
writeln!(&mut stdout, "<table>")?;
for (i, line) in renderer.lines().enumerate() {
if layout == HtmlOutput::NumberedDocument {
writeln!(&mut stdout, "<table>")?;
for (i, line) in renderer.lines().enumerate() {
writeln!(
&mut stdout,
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
i + 1,
)?;
}
writeln!(&mut stdout, "</table>")?;
} else {
let mut body = renderer.lines().collect::<String>();
if body.ends_with('\n') {
body.pop();
}
writeln!(
&mut stdout,
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
i + 1,
"<div class=\"highlight\">\n<pre><code>{body}</code></pre>\n</div>",
)?;
}
writeln!(&mut stdout, "</table>")?;
writeln!(&mut stdout, "{HTML_FOOTER}")?;
if layout != HtmlOutput::Fragment {
writeln!(&mut stdout, "{HTML_FOOTER}")?;
}
}
} else {
let mut style_stack = vec![theme.default_style().ansi];

View file

@ -1,7 +1,7 @@
use std::{
fs,
path::{Path, PathBuf},
str::{self, FromStr},
str::FromStr as _,
};
use anyhow::{Context, Result, anyhow};
@ -836,6 +836,8 @@ fn generate_java_bindings(
Ok(())
}
// TODO: remove old migrations
fn update_package_json(path: &Path) -> Result<()> {
let mut contents = fs::read_to_string(path)?
.replace(
@ -867,7 +869,7 @@ fn update_package_json(path: &Path) -> Result<()> {
fn update_grammar_js(path: &Path) -> Result<()> {
let mut contents = fs::read_to_string(path)?;
if contents.contains("module.exports") {
info!("Migrating grammars.js to ESM");
info!("Migrating grammar.js to ESM");
contents = contents.replace("module.exports =", "export default");
write_file(path, contents)?;
}
@ -974,18 +976,8 @@ fn update_rust_build_rs(path: &Path, language_name: &str, opts: &GenerateOpts) -
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
};
let Ok(wasm_src) =
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
};
c_config.include(&wasm_headers);
c_config.files([
wasm_src.join("stdio.c"),
wasm_src.join("stdlib.c"),
wasm_src.join("string.c"),
]);
}
"#}
.lines()
@ -1060,6 +1052,19 @@ fn update_c_makefile(path: &Path, language_name: &str, opts: &GenerateOpts) -> R
"},
);
}
if !contents.contains("\nDESCRIPTION :=")
&& let Some(version_line) = contents.lines().find(|l| l.starts_with("VERSION := "))
{
info!("Adding DESCRIPTION to Makefile");
let description = opts.description.map_or_else(
|| format!("{} grammar for tree-sitter", opts.camel_parser_name),
str::to_string,
);
contents = contents.replace(
version_line,
&format!("{version_line}\nDESCRIPTION := {description}"),
);
}
write_file(path, contents)?;
}
Ok(())
@ -1200,14 +1205,23 @@ fn update_python_setup_py(path: &Path, language_name: &str, opts: &GenerateOpts)
if !contents.contains("build_ext") {
info!("Replacing setup.py");
generate_file(path, SETUP_PY_TEMPLATE, language_name, opts)?;
}
if !contents.contains(" and not get_config_var") {
info!("Updating Python free-threading support in setup.py");
contents = contents.replace(
r#"startswith("cp"):"#,
r#"startswith("cp") and not get_config_var("Py_GIL_DISABLED"):"#,
);
write_file(path, contents)?;
} else {
if !contents.contains(" and not get_config_var") {
info!("Updating Python free-threading support in setup.py");
contents = contents.replace(
r#"startswith("cp"):"#,
r#"startswith("cp") and not get_config_var("Py_GIL_DISABLED"):"#,
);
write_file(path, &contents)?;
}
if !contents.contains("include(\"src/*.c\")") {
info!("Updating sdist file list in setup.py");
let contents = contents.replace(
"include(\"src/tree_sitter/*.h\")",
"include(\"src/tree_sitter/*.h\")\n self.filelist.include(\"src/*.c\")",
);
write_file(path, &contents)?;
}
}
Ok(())
}
@ -1228,8 +1242,13 @@ fn update_swift_package(path: &Path) -> Result<()> {
"atPath: \"src/scanner.c\"",
"atPath: \"\\(dir)/src/scanner.c\"",
)
.replace("version: \"0.8.0\")", "version: \"0.9.0\")")
.replace("(url:", "(name: \"SwiftTreeSitter\", url:");
.replace("version: \"0.8.0\")", "version: \"0.10.0\")")
.replace("version: \"0.9.0\")", "version: \"0.10.0\")")
.replace("(name: \"SwiftTreeSitter\", url:", "(url:")
.replace(
" \"SwiftTreeSitter\"",
" .product(name: \"SwiftTreeSitter\", package: \"swift-tree-sitter\")",
);
if !replaced_contents.eq(&contents) {
info!("Updating Package.swift");
write_file(path, replaced_contents)?;
@ -1496,24 +1515,18 @@ fn generate_file(
replacement = replacement
.replace(
PARSER_URL_STRIPPED_PLACEHOLDER,
&repository.replace("https://", "").to_lowercase(),
&repository.replace("https://", ""),
)
.replace(PARSER_URL_PLACEHOLDER, &repository.to_lowercase());
.replace(PARSER_URL_PLACEHOLDER, repository);
} else {
replacement = replacement
.replace(
PARSER_URL_STRIPPED_PLACEHOLDER,
&format!(
"github.com/tree-sitter/tree-sitter-{}",
language_name.to_lowercase()
),
&format!("github.com/tree-sitter/tree-sitter-{language_name}"),
)
.replace(
PARSER_URL_PLACEHOLDER,
&format!(
"https://github.com/tree-sitter/tree-sitter-{}",
language_name.to_lowercase()
),
&format!("https://github.com/tree-sitter/tree-sitter-{language_name}"),
);
}

View file

@ -1,12 +1,8 @@
use std::io::Write;
use anstyle::{AnsiColor, Color, Style};
use log::{Level, LevelFilter, Log, Metadata, Record};
pub fn paint(color: Option<impl Into<Color>>, text: &str) -> String {
let style = Style::new().fg_color(color.map(Into::into));
format!("{style}{text}{style:#}")
}
use crate::paint::{Paint, RED, YELLOW};
struct Logger;
@ -17,16 +13,8 @@ impl Log for Logger {
fn log(&self, record: &Record) {
match record.level() {
Level::Error => eprintln!(
"{} {}",
paint(Some(AnsiColor::Red), "Error:"),
record.args()
),
Level::Warn => eprintln!(
"{} {}",
paint(Some(AnsiColor::Yellow), "Warning:"),
record.args()
),
Level::Error => eprintln!("{} {}", Paint(RED, "Error:"), record.args()),
Level::Warn => eprintln!("{} {}", Paint(YELLOW, "Warning:"), record.args()),
Level::Info | Level::Debug => eprintln!("{}", record.args()),
Level::Trace => eprintln!(
"[{}] {}",

View file

@ -6,7 +6,7 @@ use std::{
use anstyle::{AnsiColor, Color, Style};
use anyhow::{Context, Result, anyhow};
use clap::{Args, Command, FromArgMatches as _, Subcommand, ValueEnum, crate_authors};
use clap::{ArgGroup, Args, Command, FromArgMatches as _, Subcommand, ValueEnum, crate_authors};
use clap_complete::generate;
use dialoguer::{Confirm, FuzzySelect, Input, MultiSelect, theme::ColorfulTheme};
use heck::ToUpperCamelCase;
@ -19,10 +19,10 @@ use tree_sitter_cli::{
DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT, FuzzOptions, ITERATION_COUNT,
LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED, fuzz_language_corpus,
},
highlight::{self, HighlightOptions},
highlight::{self, HighlightOptions, HtmlOutput, HtmlStyling},
init::{JsonConfigOpts, TREE_SITTER_JSON_SCHEMA, generate_grammar_files},
input::{CliInput, get_input, get_tmp_source_file},
logger,
logger, paint,
parse::{self, ParseDebugType, ParseFileOptions, ParseOutput, ParseTheme},
playground,
query::{self, QueryFileOptions},
@ -33,7 +33,7 @@ use tree_sitter_cli::{
wasm,
};
use tree_sitter_config::Config;
use tree_sitter_generate::OptLevel;
use tree_sitter_generate::{Diagnostic, GenerateError, OptLevel};
use tree_sitter_highlight::Highlighter;
use tree_sitter_loader::{self as loader, Bindings, TreeSitterJSON};
use tree_sitter_tags::TagsContext;
@ -195,6 +195,7 @@ struct Build {
#[derive(Args)]
#[command(alias = "p")]
#[command(group(ArgGroup::new("graph_output").multiple(true)))]
struct Parse {
/// The path to a file with paths to source file(s)
#[arg(long = "paths")]
@ -210,7 +211,7 @@ struct Parse {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
/// Select a language by the scope instead of a file extension
#[arg(long)]
@ -226,13 +227,13 @@ struct Parse {
#[arg(long, short = '0')]
pub debug_build: bool,
/// Produce the log.html file with debug graphs
#[arg(long, short = 'D')]
#[arg(long, short = 'D', group = "graph_output")]
pub debug_graph: bool,
/// Compile parsers to Wasm instead of native dynamic libraries
#[arg(long, hide = cfg!(not(feature = "wasm")))]
pub wasm: bool,
/// Output the parse data with graphviz dot
#[arg(long = "dot")]
#[arg(long = "dot", group = "graph_output")]
pub output_dot: bool,
/// Output the parse data in XML format
#[arg(long = "xml", short = 'x')]
@ -240,7 +241,7 @@ struct Parse {
/// Output the parse data in a pretty-printed CST format
#[arg(long = "cst", short = 'c')]
pub output_cst: bool,
/// Show parsing statistic
/// Show parsing statistics
#[arg(long, short, conflicts_with = "json", conflicts_with = "json_summary")]
pub stat: bool,
/// Interrupt the parsing process by timeout (µs)
@ -266,8 +267,8 @@ struct Parse {
/// The encoding of the input files
#[arg(long)]
pub encoding: Option<Encoding>,
/// Open `log.html` in the default browser, if `--debug-graph` is supplied
#[arg(long)]
/// Open `log.html` in the default browser, if `--debug-graph` or `--dot` is supplied
#[arg(long, requires = "graph_output")]
pub open_log: bool,
/// Deprecated: use --json-summary
#[arg(long, conflicts_with = "json_summary", conflicts_with = "stat")]
@ -317,7 +318,7 @@ struct Test {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
/// Update all syntax trees in corpus files with current parser output
#[arg(long, short)]
@ -335,7 +336,7 @@ struct Test {
#[arg(long, hide = cfg!(not(feature = "wasm")))]
pub wasm: bool,
/// Open `log.html` in the default browser, if `--debug-graph` is supplied
#[arg(long)]
#[arg(long, requires = "debug_graph")]
pub open_log: bool,
/// The path to an alternative config.json file
#[arg(long)]
@ -343,6 +344,9 @@ struct Test {
/// Force showing fields in test diffs
#[arg(long)]
pub show_fields: bool,
/// Force showing '+' and '-' in test diffs
#[arg(long)]
pub show_diff_markers: bool,
/// Show parsing statistics
#[arg(long)]
pub stat: Option<TestStats>,
@ -402,7 +406,7 @@ struct Fuzz {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
#[arg(
long,
@ -449,7 +453,7 @@ struct Query {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
/// Measure execution time
#[arg(long, short)]
@ -504,14 +508,20 @@ struct Highlight {
/// Generate highlighting as an HTML document
#[arg(long, short = 'H')]
pub html: bool,
/// When generating HTML, use css classes rather than inline styles
#[arg(long)]
/// Deprecated: use `--style classes`
#[arg(long, requires = "html", conflicts_with = "style")]
pub css_classes: bool,
/// When generating HTML, the document structure to emit
#[arg(long, requires = "html", value_enum, default_value = "document")]
pub layout: HtmlOutput,
/// When generating HTML, how token colors are applied
#[arg(long, requires = "html", value_enum, default_value = "classes")]
pub style: HtmlStyling,
/// Check that highlighting captures conform strictly to standards
#[arg(long)]
pub check: bool,
/// The path to a file with captures
#[arg(long)]
#[arg(long, requires = "check")]
pub captures_path: Option<PathBuf>,
/// The paths to files with queries
#[arg(long, num_args = 1..)]
@ -901,7 +911,7 @@ impl Init {
let new_config = format!("{}\n", serde_json::to_string_pretty(&json)?);
// Write the re-serialized config back, as newly added optional boolean fields
// will be included with explicit `false`s rather than implict `null`s
// will be included with explicit `false`s rather than implicit `null`s
if self.update && !old_config.trim().eq(new_config.trim()) {
info!("Updating tree-sitter.json");
fs::write(
@ -948,7 +958,8 @@ impl Generate {
self.json_summary
};
if let Err(err) = tree_sitter_generate::generate_parser_in_directory(
let mut diagnostics = Vec::new();
let result = tree_sitter_generate::generate_parser_in_directory(
current_dir,
self.output.as_deref(),
self.grammar_path.as_deref(),
@ -961,16 +972,33 @@ impl Generate {
} else {
OptLevel::default()
},
) {
if json_summary {
eprintln!("{}", serde_json::to_string_pretty(&err)?);
&mut diagnostics,
);
if json_summary {
#[derive(serde::Serialize)]
struct Envelope<'a> {
diagnostics: &'a [Diagnostic],
error: Option<&'a GenerateError>,
}
let envelope = Envelope {
diagnostics: &diagnostics,
error: result.as_ref().err(),
};
eprintln!("{}", serde_json::to_string_pretty(&envelope)?);
if result.is_err() {
// Exit early to prevent errors from being printed a second time in the caller
std::process::exit(1);
} else {
}
} else {
for d in &diagnostics {
warn!("{d}");
}
if let Err(err) = result {
// Removes extra context associated with the error
Err(anyhow!(err.to_string())).with_context(|| "Error when generating parser")?;
}
}
if self.build {
warn!("--build is deprecated, use the `build` command");
if let Some(path) = self.libdir {
@ -1006,7 +1034,7 @@ impl Build {
.context("Output path must have a parent")?;
let name = full_path
.file_name()
.context("Ouput path must have a filename")?;
.context("Output path must have a filename")?;
fs::create_dir_all(parent_path).context("Failed to create output path")?;
let mut canon_path = parent_path.canonicalize().context("Invalid output path")?;
canon_path.push(name);
@ -1044,7 +1072,6 @@ impl Build {
impl Parse {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
let json_summary = if self.json {
warn!("--json is deprecated, use --json-summary instead");
true
@ -1063,7 +1090,7 @@ impl Parse {
ParseOutput::Normal
};
let parse_theme = if color {
let parse_theme = if paint::color_enabled() {
config
.get::<parse::Config>()
.with_context(|| "Failed to parse CST theme")?
@ -1101,9 +1128,6 @@ impl Parse {
let timeout = self.timeout.unwrap_or_default();
let mut has_error = false;
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
let should_track_stats = self.stat;
let mut stats = parse::ParseStats::default();
let debug: ParseDebugType = match self.debug {
@ -1146,10 +1170,11 @@ impl Parse {
has_error |= !parse_result.successful;
};
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
}
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
if lib_info.is_none() {
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
}
let input = get_input(
self.paths_file.as_deref(),
@ -1300,7 +1325,6 @@ fn check_test(
impl Test {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
let stat = self.stat.unwrap_or_default();
loader.debug_build(self.debug_build);
@ -1318,9 +1342,6 @@ impl Test {
});
}
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
}
let languages = loader.languages_at_path(current_dir)?;
let language = if let Some(ref lib_path) = self.lib_path {
let lib_info =
@ -1342,13 +1363,9 @@ impl Test {
parser.set_language(language)?;
let test_dir = current_dir.join("test");
let mut test_summary = TestSummary::new(
color,
stat,
self.update,
self.overview_only,
self.json_summary,
);
let mut test_summary =
TestSummary::new(stat, self.update, self.overview_only, self.json_summary);
test_summary.use_markers = self.show_diff_markers;
// Run the corpus tests. Look for them in `test/corpus`.
let test_corpus_dir = test_dir.join("corpus");
@ -1363,7 +1380,6 @@ impl Test {
update: self.update,
open_log: self.open_log,
languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(),
color,
show_fields: self.show_fields,
overview_only: self.overview_only,
};
@ -1374,16 +1390,20 @@ impl Test {
self.json_summary,
)?;
test_summary.test_num = 1;
} else {
warn!("Test corpus not found at {}", test_corpus_dir.display());
}
// Check that all of the queries are valid.
let query_dir = current_dir.join("queries");
check_test(
test::check_queries_at_path(language, &query_dir),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
if query_dir.is_dir() {
check_test(
test::check_queries_at_path(language, &query_dir),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
}
// Run the syntax highlighting tests.
let test_highlight_dir = test_dir.join("highlight");
@ -1497,9 +1517,6 @@ impl Fuzz {
loader.sanitize_build(true);
loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
}
let languages = loader.languages_at_path(current_dir)?;
let (language, language_name) = if let Some(ref lib_path) = self.lib_path {
let lib_info = get_lib_info(Some(lib_path), self.lang_name.as_ref(), current_dir)
@ -1547,9 +1564,12 @@ impl Fuzz {
impl Query {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
let loader_config = config.get()?;
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
if lib_info.is_none() {
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
}
loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
loader.find_all_languages(&loader_config)?;
let query_path = Path::new(&self.query_path);
let byte_range = parse_range(self.byte_range.as_deref(), |x| x)?;
@ -1561,11 +1581,6 @@ impl Query {
let cancellation_flag = util::cancel_on_signal();
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name specified without --lib-path. This argument will be ignored.");
}
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
let input = get_input(
self.paths_file.as_deref(),
self.paths,
@ -1695,12 +1710,19 @@ impl Highlight {
Encoding::Utf16BE => ffi::TSInputEncodingUTF16BE,
});
let style = if self.css_classes {
// TODO: Remove during the 0.28 release cycle
warn!("--css-classes is deprecated, use --style classes instead");
HtmlStyling::Classes
} else {
self.style
};
let options = HighlightOptions {
theme: theme_config.theme,
check: self.check,
captures_path: self.captures_path,
inline_styles: !self.css_classes,
html: self.html,
html: self.html.then_some((self.layout, style)),
quiet: self.quiet,
print_time: self.time,
cancellation_flag: cancellation_flag.clone(),
@ -1985,7 +2007,7 @@ impl DumpLanguages {
concat!(
"name: {}\n",
"scope: {}\n",
"parser: {:?}\n",
"parser: {}\n",
"highlights: {:?}\n",
"file_types: {:?}\n",
"content_regex: {:?}\n",
@ -1993,7 +2015,7 @@ impl DumpLanguages {
),
configuration.language_name,
configuration.scope.as_ref().unwrap_or(&String::new()),
language_path,
language_path.display(),
configuration.highlights_filenames,
configuration.file_types,
configuration.content_regex,

27
crates/cli/src/paint.rs Normal file
View file

@ -0,0 +1,27 @@
use anstyle::{AnsiColor, Color, Style};
pub const RED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
pub const YELLOW: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
/// Wraps a `Display` value with a style; emits ANSI codes only when
/// [`color_enabled`] is true.
pub struct Paint<T>(pub Style, pub T);
pub fn color_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty()))
}
pub fn paint<T>(color: Option<impl Into<Color>>, text: T) -> Paint<T> {
Paint(Style::new().fg_color(color.map(Into::into)), text)
}
impl<T: std::fmt::Display> std::fmt::Display for Paint<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if color_enabled() {
write!(f, "{}{}{:#}", self.0, self.1, self.0)
} else {
self.1.fmt(f)
}
}
}

View file

@ -18,7 +18,7 @@ use tree_sitter::{
ffi,
};
use crate::{fuzz::edits::Edit, logger::paint, util};
use crate::{fuzz::edits::Edit, paint::paint, util};
#[derive(Debug, Default, Serialize, JsonSchema)]
pub struct Stats {
@ -301,7 +301,6 @@ pub fn parse_file_at_path(
// Log to stderr if `--debug` was passed
else if opts.debug != ParseDebugType::Quiet {
let mut curr_version: usize = 0;
let use_color = std::env::var("NO_COLOR").map_or(true, |v| v != "1");
let debug = opts.debug;
parser.set_logger(Some(Box::new(move |log_type, message| {
if debug == ParseDebugType::Normal {
@ -310,13 +309,13 @@ pub fn parse_file_at_path(
}
writeln!(&mut io::stderr(), "{message}").unwrap();
} else {
#[rustfmt::skip]
let colors = &[
AnsiColor::White,
AnsiColor::Red,
AnsiColor::Blue,
AnsiColor::Green,
AnsiColor::Cyan,
AnsiColor::Yellow,
AnsiColor::White, AnsiColor::Red, AnsiColor::Blue, AnsiColor::Green,
AnsiColor::Cyan, AnsiColor::Yellow, AnsiColor::Magenta,
AnsiColor::BrightWhite, AnsiColor::BrightRed, AnsiColor::BrightBlue,
AnsiColor::BrightGreen, AnsiColor::BrightCyan, AnsiColor::BrightYellow,
AnsiColor::BrightMagenta,
];
if message.starts_with("process version:") {
let comma_idx = message.find(',').unwrap();
@ -324,18 +323,9 @@ pub fn parse_file_at_path(
.parse()
.unwrap();
}
let color = if use_color {
Some(colors[curr_version])
} else {
None
};
let mut out = if log_type == LogType::Lex {
" ".to_string()
} else {
String::new()
};
out += &paint(color, message);
writeln!(&mut io::stderr(), "{out}").unwrap();
let color = Some(colors[curr_version % colors.len()]);
let prefix = if log_type == LogType::Lex { " " } else { "" };
writeln!(&mut io::stderr(), "{prefix}{}", paint(color, message)).unwrap();
}
})));
}
@ -390,8 +380,10 @@ pub fn parse_file_at_path(
let tree = match encoding {
Some(encoding) if encoding == ffi::TSInputEncodingUTF16LE => {
let source_code_utf16 = source_code
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.as_chunks::<2>()
.0
.iter()
.map(|&chunk| u16::from_le_bytes(chunk))
.collect::<Vec<_>>();
parser.parse_utf16_le_with_options(
&mut |i, _| {
@ -407,8 +399,10 @@ pub fn parse_file_at_path(
}
Some(encoding) if encoding == ffi::TSInputEncodingUTF16BE => {
let source_code_utf16 = source_code
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.as_chunks::<2>()
.0
.iter()
.map(|&chunk| u16::from_be_bytes(chunk))
.collect::<Vec<_>>();
parser.parse_utf16_be_with_options(
&mut |i, _| {
@ -437,7 +431,7 @@ pub fn parse_file_at_path(
let parse_duration = parse_time.elapsed();
let stdout = io::stdout();
let mut stdout = stdout.lock();
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
if let Some(mut tree) = tree {
if opts.debug_graph && !opts.edits.is_empty() {
@ -514,7 +508,7 @@ pub fn parse_file_at_path(
}
}
cursor.reset(tree.root_node());
println!();
writeln!(&mut stdout)?;
}
if opts.output == ParseOutput::Cst {
@ -585,11 +579,11 @@ pub fn parse_file_at_path(
}
let start = node.start_position();
let end = node.end_position();
write!(&mut stdout, " srow=\"{}\"", start.row)?;
write!(&mut stdout, " scol=\"{}\"", start.column)?;
write!(&mut stdout, " erow=\"{}\"", end.row)?;
write!(&mut stdout, " ecol=\"{}\"", end.column)?;
write!(&mut stdout, ">")?;
write!(
&mut stdout,
" srow=\"{}\" scol=\"{}\" erow=\"{}\" ecol=\"{}\">",
start.row, start.column, end.row, end.column
)?;
tags.push(node.kind());
needs_newline = true;
}
@ -780,12 +774,16 @@ pub fn render_cst<'a, 'b: 'a>(
cursor: &mut TreeCursor<'a>,
opts: &ParseFileOptions,
out: &mut impl Write,
) -> Result<()> {
) -> io::Result<()> {
let lossy_source_code = String::from_utf8_lossy(source_code);
let total_width = lossy_source_code
.lines()
.enumerate()
.map(|(row, col)| (row as f64).log10() as usize + (col.len() as f64).log10() as usize + 1)
.map(|(row, col)| {
row.checked_ilog10().unwrap_or(0) as usize
+ col.len().checked_ilog10().unwrap_or(0) as usize
+ 1
})
.max()
.unwrap_or(1);
let mut indent_level = usize::from(!opts.no_ranges);
@ -829,19 +827,19 @@ pub fn render_cst<'a, 'b: 'a>(
Ok(())
}
fn render_node_text(source: &str) -> String {
source
.chars()
.fold(String::with_capacity(source.len()), |mut acc, c| {
if let Some(esc) = escape_invisible(c) {
acc.push_str(esc);
} else if let Some(esc) = escape_delimiter(c) {
acc.push_str(esc);
} else {
acc.push(c);
struct CstNodeText<'a>(&'a str);
impl std::fmt::Display for CstNodeText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write as _;
for c in self.0.chars() {
match escape_invisible(c).or_else(|| escape_delimiter(c)) {
Some(esc) => f.write_str(esc)?,
None => f.write_char(c)?,
}
acc
})
}
Ok(())
}
}
fn write_node_text(
@ -852,7 +850,7 @@ fn write_node_text(
source: &str,
color: Option<impl Into<Color> + Copy>,
text_info: (usize, usize),
) -> Result<()> {
) -> io::Result<()> {
let (total_width, indent_level) = text_info;
let (quote, quote_color) = if is_named {
('`', opts.parse_theme.backtick)
@ -864,9 +862,9 @@ fn write_node_text(
write!(
out,
"{}{}{}",
paint(quote_color, &String::from(quote)),
paint(color, &render_node_text(source)),
paint(quote_color, &String::from(quote)),
paint(quote_color, quote),
paint(color, CstNodeText(source)),
paint(quote_color, quote),
)?;
} else {
let multiline = source.contains('\n');
@ -885,24 +883,34 @@ fn write_node_text(
} else {
0
};
let formatted_line = render_line_feed(line, opts);
if multiline {
writeln!(out)?;
if !opts.no_ranges {
write!(
out,
"{}",
CstNodeRange {
opts,
has_field_name: cursor.field_name().is_some(),
is_named,
is_multiline: true,
total_width,
range: node_range,
}
)?;
}
for _ in 0..=indent_level {
write!(out, " ")?;
}
} else {
write!(out, " ")?;
}
write!(
out,
"{}{}{}{}{}{}",
if multiline { "\n" } else { " " },
if multiline && !opts.no_ranges {
render_node_range(opts, cursor, is_named, true, total_width, node_range)
} else {
String::new()
},
if multiline {
" ".repeat(indent_level + 1)
} else {
String::new()
},
paint(quote_color, &String::from(quote)),
paint(color, &render_node_text(&formatted_line)),
paint(quote_color, &String::from(quote)),
"{}{}{}",
paint(quote_color, quote),
paint(color, CstLineFeed { source: line, opts }),
paint(quote_color, quote),
)?;
}
}
@ -910,49 +918,68 @@ fn write_node_text(
Ok(())
}
fn render_line_feed(source: &str, opts: &ParseFileOptions) -> String {
if cfg!(windows) {
source.replace("\r\n", &paint(opts.parse_theme.line_feed, "\r\n"))
} else {
source.replace('\n', &paint(opts.parse_theme.line_feed, "\n"))
struct CstLineFeed<'src, 'opt> {
source: &'src str,
opts: &'src ParseFileOptions<'opt>,
}
impl std::fmt::Display for CstLineFeed<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(windows)]
let lf = "\r\n";
#[cfg(not(windows))]
let lf = "\n";
let painted = paint(self.opts.parse_theme.line_feed, CstNodeText(lf));
let mut parts = self.source.split(lf);
if let Some(first) = parts.next() {
write!(f, "{}", CstNodeText(first))?;
}
for part in parts {
write!(f, "{painted}{}", CstNodeText(part))?;
}
Ok(())
}
}
fn render_node_range(
opts: &ParseFileOptions,
cursor: &TreeCursor,
struct CstNodeRange<'src, 'opt> {
opts: &'src ParseFileOptions<'opt>,
has_field_name: bool,
is_named: bool,
is_multiline: bool,
total_width: usize,
range: Range,
) -> String {
let has_field_name = cursor.field_name().is_some();
let range_color = if is_named && !is_multiline && !has_field_name {
opts.parse_theme.row_color_named
} else {
opts.parse_theme.row_color
};
}
let remaining_width_start = (total_width
- (range.start_point.row as f64).log10() as usize
- (range.start_point.column as f64).log10() as usize)
.max(1);
let remaining_width_end = (total_width
- (range.end_point.row as f64).log10() as usize
- (range.end_point.column as f64).log10() as usize)
.max(1);
paint(
range_color,
&format!(
"{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
range.start_point.row,
range.start_point.column,
' ',
range.end_point.row,
range.end_point.column,
' ',
),
)
impl std::fmt::Display for CstNodeRange<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let start = self.range.start_point;
let end = self.range.end_point;
let range_color = if self.is_named && !self.is_multiline && !self.has_field_name {
self.opts.parse_theme.row_color_named
} else {
self.opts.parse_theme.row_color
};
let remaining_width = |row: usize, col: usize| {
(self
.total_width
.saturating_sub(row.checked_ilog10().unwrap_or(0) as usize)
.saturating_sub(col.checked_ilog10().unwrap_or(0) as usize))
.max(1)
};
let remaining_width_start = remaining_width(start.row, start.column);
let remaining_width_end = remaining_width(end.row, end.column);
write!(
f,
"{}",
paint(
range_color,
format_args!(
"{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
start.row, start.column, ' ', end.row, end.column, ' ',
),
)
)
}
}
fn cst_render_node(
@ -963,14 +990,21 @@ fn cst_render_node(
total_width: usize,
indent_level: usize,
in_error: bool,
) -> Result<()> {
) -> io::Result<()> {
let node = cursor.node();
let is_named = node.is_named();
if !opts.no_ranges {
write!(
out,
"{}",
render_node_range(opts, cursor, is_named, false, total_width, node.range())
CstNodeRange {
opts,
has_field_name: cursor.field_name().is_some(),
is_named,
is_multiline: false,
total_width,
range: node.range(),
}
)?;
}
write!(
@ -988,7 +1022,7 @@ fn cst_render_node(
write!(
out,
"{}",
paint(opts.parse_theme.field, &format!("{field_name}: "))
paint(opts.parse_theme.field, format_args!("{field_name}: "))
)?;
}
@ -1103,30 +1137,25 @@ fn parse_edit_flag(source_code: &[u8], flag: &str) -> Result<Edit> {
pub fn offset_for_position(input: &[u8], position: Point) -> Result<usize> {
let mut row = 0;
let mut offset = 0;
let mut iter = memchr::memchr_iter(b'\n', input);
loop {
if let Some(pos) = iter.next()
&& row < position.row
{
row += 1;
offset = pos;
continue;
let mut line_start = 0;
for line_end in memchr::memchr_iter(b'\n', input) {
if row == position.row {
if position.column > line_end - line_start {
return Err(anyhow!("Failed to address a column: {}", position.column));
}
return Ok(line_start + position.column);
}
offset += 1;
break;
row += 1;
line_start = line_end + 1;
}
if position.row - row > 0 {
if row != position.row {
return Err(anyhow!("Failed to address a row: {}", position.row));
}
if let Some(pos) = iter.next() {
if (pos - offset < position.column) || (input[offset] == b'\n' && position.column > 0) {
return Err(anyhow!("Failed to address a column: {}", position.column));
}
} else if input.len() - offset < position.column {
if position.column > input.len() - line_start {
return Err(anyhow!("Failed to address a column over the end"));
}
Ok(offset + position.column)
Ok(line_start + position.column)
}
pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
@ -1146,3 +1175,45 @@ pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
};
Ok(result)
}
#[cfg(test)]
mod tests {
use super::{offset_for_position, parse_edit_flag};
use tree_sitter::Point;
#[test]
fn offset_for_position_uses_zero_based_line_and_column_coordinates() {
let input = b"abc\n";
assert_eq!(
offset_for_position(input, Point { row: 0, column: 0 }).unwrap(),
0
);
assert_eq!(
offset_for_position(input, Point { row: 0, column: 1 }).unwrap(),
1
);
assert_eq!(
offset_for_position(input, Point { row: 0, column: 3 }).unwrap(),
3
);
assert_eq!(
offset_for_position(input, Point { row: 1, column: 0 }).unwrap(),
4
);
}
#[test]
fn offset_for_position_rejects_out_of_bounds_coordinates() {
let input = b"abc\ndef";
assert!(offset_for_position(input, Point { row: 0, column: 4 }).is_err());
assert!(offset_for_position(input, Point { row: 2, column: 0 }).is_err());
}
#[test]
fn parse_edit_flag_resolves_first_line_positions() {
let edit = parse_edit_flag(b"abc\n", "0,0 0 X").unwrap();
assert_eq!(edit.position, 0);
assert_eq!(edit.deleted_length, 0);
assert_eq!(edit.inserted_text, b"X");
}
}

View file

@ -470,7 +470,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script>
<script>LANGUAGE_BASE_URL = "";</script>
<script>LANGUAGE_BASE_URL = ".";</script>
<script type="module" src="playground.js"></script>
<script type="module">
import * as TreeSitter from './web-tree-sitter.js';

View file

@ -3,7 +3,7 @@ use std::{
env, fs,
net::TcpListener,
path::{Path, PathBuf},
str::{self, FromStr as _},
str::FromStr as _,
};
use anyhow::{Context, Result, anyhow};

View file

@ -37,7 +37,7 @@ pub fn query_file_at_path(
test_summary: Option<&mut TestSummary>,
) -> Result<()> {
let stdout = io::stdout();
let mut stdout = stdout.lock();
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
let query_source = fs::read_to_string(query_path)
.with_context(|| format!("Error reading query file {}", query_path.display()))?;
@ -75,7 +75,7 @@ pub fn query_file_at_path(
if opts.ordered_captures {
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
while let Some((mat, capture_index)) = captures.next() {
let capture = mat.captures[*capture_index];
let capture = mat.captures()[*capture_index];
let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test {
writeln!(
@ -102,7 +102,7 @@ pub fn query_file_at_path(
if !opts.quiet && !should_test {
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
}
for capture in m.captures {
for capture in m.captures() {
let start = capture.node.start_position();
let end = capture.node.end_position();
let capture_name = &query.capture_names()[capture.index as usize];
@ -142,7 +142,9 @@ pub fn query_file_at_path(
};
// Invariant: `test_summary` will always be `Some` when `should_test` is true
let test_summary = test_summary.unwrap();
match query_testing::assert_expected_captures(&results, path, &mut parser, language) {
let assertions =
query_testing::parse_position_comments(&mut parser, language, source_code.as_slice())?;
match query_testing::assert_expected_captures(&results, &assertions) {
Ok(assertion_count) => {
test_summary.query_results.add_case(TestResult {
name: path_name.to_string(),

View file

@ -1,4 +1,4 @@
use std::{fs, path::Path, sync::LazyLock};
use std::sync::LazyLock;
use anyhow::{Result, anyhow};
use bstr::{BStr, ByteSlice};
@ -219,19 +219,14 @@ pub fn parse_position_comments(
Ok(result)
}
pub fn assert_expected_captures(
infos: &[CaptureInfo],
path: &Path,
parser: &mut Parser,
language: &Language,
) -> Result<usize> {
let contents = fs::read_to_string(path)?;
let pairs = parse_position_comments(parser, language, contents.as_bytes())?;
for assertion in &pairs {
pub fn assert_expected_captures(infos: &[CaptureInfo], assertions: &[Assertion]) -> Result<usize> {
for assertion in assertions {
if let Some(found) = &infos.iter().find(|p| {
assertion.position >= p.start
&& (assertion.position.row < p.end.row
|| assertion.position.column + assertion.length - 1 < p.end.column)
let assertion_end = Utf8Point::new(
assertion.position.row,
assertion.position.column + assertion.length - 1,
);
assertion.position >= p.start && assertion_end < p.end
}) {
if assertion.expected_capture_name != found.name && found.name != "name" {
return Err(anyhow!(
@ -250,5 +245,24 @@ pub fn assert_expected_captures(
));
}
}
Ok(pairs.len())
Ok(assertions.len())
}
#[cfg(test)]
mod tests {
use super::{Assertion, CaptureInfo, Utf8Point, assert_expected_captures};
#[test]
fn test_assertion_after_multiline_capture_does_not_match() {
let captures = [CaptureInfo {
name: "foo".to_string(),
start: Utf8Point::new(0, 0),
end: Utf8Point::new(1, 1),
}];
let assertions = [Assertion::new(2, 0, 1, false, "foo".to_string())];
let result = assert_expected_captures(&captures, &assertions);
assert!(result.is_err());
}
}

View file

@ -2,7 +2,6 @@ use std::{
fs,
io::{self, Write},
path::Path,
str,
sync::{Arc, atomic::AtomicUsize},
time::Instant,
};
@ -49,7 +48,7 @@ pub fn generate_tags(
&mut stdout,
"{indent_str}{:<10}\t | {:<8}\t{} {} - {} `{}`",
str::from_utf8(&source[tag.name_range]).unwrap_or(""),
&config.syntax_type_name(tag.syntax_type_id),
config.syntax_type_name(tag.syntax_type_id),
if tag.is_definition { "def" } else { "ref" },
tag.span.start,
tag.span.end,
@ -59,7 +58,7 @@ pub fn generate_tags(
if docs.len() > 120 {
write!(&mut stdout, "\t{:?}...", docs.get(0..120).unwrap_or(""))?;
} else {
write!(&mut stdout, "\t{:?}", &docs)?;
write!(&mut stdout, "\t{docs:?}")?;
}
}
writeln!(&mut stdout)?;

View file

@ -1,13 +1,15 @@
"""PARSER_DESCRIPTION"""
from importlib.resources import files as _files
from ._binding import language
def _get_query(name, file):
files = globals().get("_files")
if files is None:
from importlib.resources import files
globals()["_files"] = files
try:
query = _files(f"{__package__}") / file
query = files(f"{__package__}") / file
globals()[name] = query.read_text()
except FileNotFoundError:
globals()[name] = None

View file

@ -1,17 +1,17 @@
[package]
authors = [ "PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL" ]
authors = [ "PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL" ]
autoexamples = false
categories = [ "parser-implementations", "parsing", "text-editors" ]
description = "PARSER_DESCRIPTION"
edition = "2024"
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
license = "PARSER_LICENSE"
name = "tree-sitter-PARSER_NAME"
readme = "README.md"
repository = "PARSER_URL"
version = "PARSER_VERSION"
categories = [ "parser-implementations", "parsing", "text-editors" ]
description = "PARSER_DESCRIPTION"
edition = "2024"
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
license = "PARSER_LICENSE"
name = "tree-sitter-PARSER_NAME"
readme = "README.md"
repository = "PARSER_URL"
version = "PARSER_VERSION"
build = "bindings/rust/build.rs"
build = "bindings/rust/build.rs"
include = [ "bindings/rust/*", "grammar.js", "queries/*", "src/*", "tree-sitter.json", "/LICENSE" ]
[lib]

View file

@ -11,18 +11,8 @@ fn main() {
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
};
let Ok(wasm_src) =
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
};
c_config.include(&wasm_headers);
c_config.files([
wasm_src.join("stdio.c"),
wasm_src.join("stdlib.c"),
wasm_src.join("string.c"),
]);
}
let parser_path = src_dir.join("parser.c");

View file

@ -4,46 +4,51 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var threaded: std.Io.Threaded = .init(b.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
const shared = b.option(bool, "build-shared", "Build a shared library") orelse true;
const reuse_alloc = b.option(bool, "reuse-allocator", "Reuse the library allocator") orelse false;
const library_name = "tree-sitter-PARSER_NAME";
var grammar = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
});
const lib: *std.Build.Step.Compile = b.addLibrary(.{
.name = library_name,
.linkage = if (shared) .dynamic else .static,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
}),
.root_module = grammar,
});
lib.addCSourceFile(.{
grammar.addCSourceFile(.{
.file = b.path("src/parser.c"),
.flags = &.{"-std=c11"},
});
if (fileExists(b, "src/scanner.c")) {
lib.addCSourceFile(.{
if (fileExists(b, io, "src/scanner.c")) {
grammar.addCSourceFile(.{
.file = b.path("src/scanner.c"),
.flags = &.{"-std=c11"},
});
}
if (reuse_alloc) {
lib.root_module.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
grammar.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
}
if (optimize == .Debug) {
lib.root_module.addCMacro("TREE_SITTER_DEBUG", "");
grammar.addCMacro("TREE_SITTER_DEBUG", "");
}
lib.addIncludePath(b.path("src"));
grammar.addIncludePath(b.path("src"));
b.installArtifact(lib);
b.installFile("src/node-types.json", "node-types.json");
if (fileExists(b, "queries")) {
if (fileExists(b, io, "queries")) {
b.installDirectory(.{
.source_dir = b.path("queries"),
.install_dir = .prefix,
@ -69,16 +74,10 @@ pub fn build(b: *std.Build) !void {
tests.root_module.addImport(library_name, module);
// HACK: fetch tree-sitter dependency only when testing this module
if (b.pkg_hash.len == 0) {
var args = try std.process.argsWithAllocator(b.allocator);
defer args.deinit();
while (args.next()) |a| {
if (std.mem.eql(u8, a, "test")) {
const ts_dep = b.lazyDependency("tree_sitter", .{}) orelse continue;
tests.root_module.addImport("tree-sitter", ts_dep.module("tree-sitter"));
break;
}
}
if (b.option(bool, "test", "Fetch test dependencies") orelse false) {
const ts_dep = b.lazyDependency("tree_sitter", .{});
if (ts_dep) |dep|
tests.root_module.addImport("tree-sitter", dep.module("tree_sitter"));
}
const run_tests = b.addRunArtifact(tests);
@ -86,8 +85,8 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_tests.step);
}
inline fn fileExists(b: *std.Build, filename: []const u8) bool {
inline fn fileExists(b: *std.Build, io: std.Io, filename: []const u8) bool {
const dir = b.build_root.handle;
dir.access(filename, .{}) catch return false;
dir.access(io, filename, .{}) catch return false;
return true;
}

View file

@ -1,11 +1,12 @@
.{
.name = .tree_sitter_PARSER_NAME,
.fingerprint = PARSER_FINGERPRINT,
.minimum_zig_version = "0.16.0",
.version = "PARSER_VERSION",
.dependencies = .{
.tree_sitter = .{
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#b4b72c903e69998fc88e27e154a5e3cc9166551b",
.hash = "tree_sitter-0.25.0-8heIf51vAQConvVIgvm-9mVIbqh7yabZYqPXfOpS3YoG",
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#0cf58172e61f6fdd16f681cde42b4acb531a23db",
.hash = "tree_sitter-0.26.0-8heIf3CaAQDeVTQc0DMSBhbQAEx5aF-dTen4_LPxMgrv",
.lazy = true,
},
},

View file

@ -21,7 +21,7 @@ type NodeInfo =
/**
* The tree-sitter language object for this grammar.
*
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html Parser.Language}
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Language.html Parser.Language}
*
* @example
* import Parser from "tree-sitter";

View file

@ -1,6 +1,7 @@
LANGUAGE_NAME := tree-sitter-KEBAB_PARSER_NAME
HOMEPAGE_URL := PARSER_URL
VERSION := PARSER_VERSION
DESCRIPTION := PARSER_DESCRIPTION
# repository
SRC_DIR := src

View file

@ -15,7 +15,7 @@ let package = Package(
.library(name: "PARSER_CLASS_NAME", targets: ["PARSER_CLASS_NAME"]),
],
dependencies: [
.package(name: "SwiftTreeSitter", url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.9.0"),
.package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.10.0"),
],
targets: [
.target(
@ -32,7 +32,7 @@ let package = Package(
.testTarget(
name: "PARSER_CLASS_NAMETests",
dependencies: [
"SwiftTreeSitter",
.product(name: "SwiftTreeSitter", package: "swift-tree-sitter"),
"PARSER_CLASS_NAME",
],
path: "bindings/swift/PARSER_CLASS_NAMETests"

View file

@ -1,6 +1,6 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = [ "setuptools>=62.4.0", "wheel" ]
requires = [ "setuptools>=62.4.0", "wheel" ]
[project]
authors = [ { email = "PARSER_AUTHOR_EMAIL", name = "PARSER_AUTHOR_NAME" } ]
@ -19,12 +19,12 @@ requires-python = ">=3.10"
version = "PARSER_VERSION"
[project.urls]
Funding = "FUNDING_URL"
Funding = "FUNDING_URL"
Homepage = "PARSER_URL"
[project.optional-dependencies]
core = [ "tree-sitter~=0.24" ]
[tool.cibuildwheel]
build = "cp310-*"
build = "cp310-*"
build-frontend = "build"

View file

@ -42,6 +42,7 @@ class EggInfo(egg_info):
super().find_sources()
self.filelist.recursive_include("queries", "*.scm")
self.filelist.include("src/tree_sitter/*.h")
self.filelist.include("src/*.c")
setup(

File diff suppressed because it is too large Load diff

View file

@ -13,10 +13,10 @@ use crate::{
#[derive(Debug)]
pub struct Failure {
row: usize,
column: usize,
expected_highlight: String,
actual_highlights: Vec<String>,
pub(crate) row: usize,
pub(crate) column: usize,
pub(crate) expected_highlight: String,
pub(crate) actual_highlights: Vec<String>,
}
impl std::error::Error for Failure {}
@ -122,6 +122,7 @@ pub fn test_highlights(
if failed { Err(anyhow!("")) } else { Ok(()) }
}
pub fn iterate_assertions(
assertions: &[Assertion],
highlights: &[(Utf8Point, Utf8Point, Highlight)],
@ -138,49 +139,48 @@ pub fn iterate_assertions(
expected_capture_name: expected_highlight,
} in assertions
{
let mut passed = false;
let mut end_column = position.column + length - 1;
// Iterate through all of the highlights that start at or before this assertion's
// position, looking for one that matches the assertion.
actual_highlights.clear();
// The assertions are ordered by position, so skip past all of the highlights that
// end at or before this assertion's position.
'highlight_loop: while let Some(highlight) = highlights.get(i) {
let mut passed = false;
let end_column = position.column + length - 1;
for highlight in &highlights[i..] {
// The assertions are ordered by position, so skip past all of the highlights that
// end at or before this assertion's position.
if highlight.1 <= *position {
i += 1;
continue;
}
if (highlight.0.row > position.row)
|| (highlight.0.row == position.row && highlight.0.column > end_column)
{
break;
}
// Iterate through all of the highlights that start at or before this assertion's
// position, looking for one that matches the assertion.
let mut j = i;
while let (false, Some(highlight)) = (passed, highlights.get(j)) {
end_column = position.column + length - 1;
if highlight.0.row >= position.row && highlight.0.column > end_column {
break 'highlight_loop;
}
// If the highlight matches the assertion, or if the highlight doesn't
// match the assertion but it's negative, this test passes. Otherwise,
// add this highlight to the list of actual highlights that span the
// assertion's position, in order to generate an error message in the event
// of a failure.
let highlight_name = &highlight_names[(highlight.2).0];
if (*highlight_name == *expected_highlight) == *negative {
actual_highlights.push(highlight_name);
} else {
passed = true;
break 'highlight_loop;
}
j += 1;
// If the highlight matches the assertion, or if the highlight doesn't
// match the assertion but it's negative, this test passes. Otherwise,
// add this highlight to the list of actual highlights that span the
// assertion's position, in order to generate an error message in the event
// of a failure.
let highlight_name = &highlight_names[(highlight.2).0];
if (*highlight_name == *expected_highlight) == *negative {
actual_highlights.push(highlight_name);
} else {
passed = true;
break;
}
}
if !passed {
let mut expected = String::with_capacity(expected_highlight.len() + 1);
if *negative {
expected.push('!');
}
expected.push_str(expected_highlight);
return Err(Failure {
row: position.row,
column: end_column,
expected_highlight: expected_highlight.clone(),
expected_highlight: expected,
actual_highlights: actual_highlights.into_iter().cloned().collect(),
}
.into());

View file

@ -17,7 +17,7 @@ mod tree_test;
#[cfg(feature = "wasm")]
mod wasm_language_test;
use tree_sitter_generate::GenerateResult;
use tree_sitter_generate::{GenerateResult, OptLevel};
pub use crate::fuzz::{
ITERATION_COUNT, allocations,
@ -30,5 +30,10 @@ pub use helpers::fixtures::get_language;
/// This is a simple wrapper around [`tree_sitter_generate::generate_parser_for_grammar`], because
/// our tests do not need to pass in a version number, only the grammar JSON.
fn generate_parser(grammar_json: &str) -> GenerateResult<(String, String)> {
tree_sitter_generate::generate_parser_for_grammar(grammar_json, Some((0, 0, 0)))
tree_sitter_generate::generate_parser_for_grammar(
grammar_json,
Some((0, 0, 0)),
OptLevel::default(),
&mut Vec::new(),
)
}

View file

@ -2,6 +2,7 @@ use std::{collections::HashMap, env, fs};
use anyhow::Context;
use tree_sitter::Parser;
use tree_sitter_generate::OptLevel;
use tree_sitter_proc_macro::test_with_seed;
use crate::{
@ -16,7 +17,7 @@ use crate::{
random::Rand,
},
parse::perform_edit,
test::{DiffKey, TestDiff, parse_tests, strip_sexp_fields},
test::{DiffKey, TestDiff, parse_tests, render_test_output},
tests::{
allocations,
helpers::fixtures::{SCRATCH_BASE_DIR, fixtures_dir, get_language, get_test_language},
@ -195,28 +196,7 @@ pub fn test_language_corpus(
println!(" {test_index}. {test_name}");
let passed = allocations::record(|| {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(&language).unwrap();
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
let tree = parser.parse(&test.input, None).unwrap();
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output {
println!("Incorrect initial parse for {test_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
println!();
return false;
}
true
});
let passed = allocations::record(|| test.check_initial_parse(&language, &test_name, true));
if !passed {
failure_count += 1;
@ -292,10 +272,8 @@ pub fn test_language_corpus(
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
// Verify that the final tree matches the expectation from the corpus.
let mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
let actual_output =
render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
if actual_output != test.output {
println!("Incorrect parse for {test_name} - seed {seed}");
@ -375,8 +353,12 @@ fn test_feature_corpus_files() {
)
})
.unwrap();
let generate_result =
tree_sitter_generate::generate_parser_for_grammar(&grammar_json, Some((0, 0, 0)));
let generate_result = tree_sitter_generate::generate_parser_for_grammar(
&grammar_json,
Some((0, 0, 0)),
OptLevel::default(),
&mut Vec::new(),
);
if error_message_path.exists() {
if EXAMPLE_INCLUDE.is_some() || EXAMPLE_EXCLUDE.is_some() {
@ -397,12 +379,12 @@ fn test_feature_corpus_files() {
failure_count += 1;
}
} else {
eprintln!("Expected error message but got none for test grammar '{language_name}'",);
eprintln!("Expected error message but got none for test grammar '{language_name}'");
failure_count += 1;
}
} else {
if let Err(e) = &generate_result {
eprintln!("Unexpected error for test grammar '{language_name}':\n{e}",);
eprintln!("Unexpected error for test grammar '{language_name}':\n{e}");
failure_count += 1;
continue;
}
@ -420,24 +402,8 @@ fn test_feature_corpus_files() {
for test in tests {
eprintln!(" example: {:?}", test.name);
let passed = allocations::record(|| {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(&language).unwrap();
let tree = parser.parse(&test.input, None).unwrap();
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output == test.output {
true
} else {
DiffKey::print();
print!("{}", TestDiff::new(&actual_output, &test.output));
println!();
false
}
});
let passed =
allocations::record(|| test.check_initial_parse(&language, &test.name, true));
if !passed {
failure_count += 1;

View file

@ -127,6 +127,52 @@ fn detect_language_by_double_barrel_file_extension() {
);
}
#[test]
fn detect_language_with_dots_in_filename() {
let blade_dir = tree_sitter_dir(
r#"{
"grammars": [
{
"name": "blade_dots",
"path": ".",
"scope": "source.blade",
"file-types": [
"blade.php"
]
},
{
"name": "php_dots",
"path": ".",
"scope": "source.php",
"file-types": [
"php"
]
}
],
"metadata": {
"version": "0.0.1"
}
}
"#,
"blade_dots",
);
let mut loader = Loader::with_parser_lib_path(scratch_dir().to_path_buf());
let config = loader
.find_language_configurations_at_path(blade_dir.path(), false)
.unwrap();
// this is just to validate that we can read the tree-sitter.json correctly
assert_eq!(config[0].scope.as_ref().unwrap(), "source.blade");
let file_name = blade_dir.path().join("foo.bar.baz.blade.php");
fs::write(&file_name, "").unwrap();
assert_eq!(
get_lang_scope(&loader, &file_name),
Some("source.blade".into())
);
}
#[test]
fn detect_language_without_filename() {
let gitignore_dir = tree_sitter_dir(

View file

@ -1,4 +1,4 @@
use std::{ops::Range, str};
use std::ops::Range;
#[derive(Debug)]
pub struct ReadRecorder<'a> {

View file

@ -1,7 +1,8 @@
use std::{
collections::HashSet,
env, fs,
path::{Path, PathBuf},
sync::LazyLock,
sync::{LazyLock, Mutex},
};
use anyhow::Context;
@ -23,6 +24,10 @@ static TEST_LOADER: LazyLock<Loader> = LazyLock::new(|| {
loader
});
// Prevents parallel tests from racing on the same per-grammar
// `src_dir/tree_sitter/` and observing a half-rewritten header.
static WRITTEN_HEADER_DIRS: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
#[cfg(feature = "wasm")]
pub static ENGINE: LazyLock<tree_sitter::wasmtime::Engine> = LazyLock::new(Default::default);
@ -134,17 +139,22 @@ fn get_test_language_internal(
};
let header_path = src_dir.join("tree_sitter");
fs::create_dir_all(&header_path).unwrap();
for (file, content) in [
("alloc.h", ALLOC_HEADER),
("array.h", ARRAY_HEADER),
("parser.h", tree_sitter::PARSER_HEADER),
] {
let file = header_path.join(file);
fs::write(&file, content)
.with_context(|| format!("Failed to write {:?}", file.file_name().unwrap()))
.unwrap();
if WRITTEN_HEADER_DIRS
.lock()
.unwrap()
.insert(header_path.clone())
{
fs::create_dir_all(&header_path).unwrap();
for (file, content) in [
("alloc.h", ALLOC_HEADER),
("array.h", ARRAY_HEADER),
("parser.h", tree_sitter::PARSER_HEADER),
] {
let path = header_path.join(file);
fs::write(&path, content)
.with_context(|| format!("Failed to write {}", path.display()))
.unwrap();
}
}
let paths_to_check = if let Some(scanner_path) = &scanner_path {

View file

@ -7,10 +7,10 @@ use tree_sitter::{
};
#[derive(Debug)]
pub struct Pattern {
kind: Option<&'static str>,
pub struct Pattern<'a> {
kind: Option<&'a str>,
named: bool,
field: Option<&'static str>,
field: Option<&'a str>,
capture: Option<String>,
children: Vec<Self>,
}
@ -25,8 +25,8 @@ const CAPTURE_NAMES: &[&str] = &[
"one", "two", "three", "four", "five", "six", "seven", "eight",
];
impl Pattern {
pub fn random_pattern_in_tree(tree: &Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
impl<'a> Pattern<'a> {
pub fn random_pattern_in_tree(tree: &'a Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
let mut cursor = tree.walk();
// Descend to the node at a random byte offset and depth.
@ -75,7 +75,7 @@ impl Pattern {
(pattern, pattern_start..pattern_end)
}
fn random_pattern_for_node(cursor: &mut TreeCursor, rng: &mut impl Rng) -> Self {
fn random_pattern_for_node(cursor: &mut TreeCursor<'a>, rng: &mut impl Rng) -> Self {
let node = cursor.node();
// Sometimes specify the node's type, sometimes use a wildcard.
@ -268,7 +268,7 @@ impl Pattern {
}
}
impl std::fmt::Display for Pattern {
impl std::fmt::Display for Pattern<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut result = String::new();
self.write_to_string(&mut result, 0);
@ -336,7 +336,7 @@ pub fn collect_matches<'a>(
while let Some(m) = matches.next() {
result.push((
m.pattern_index,
format_captures(m.captures.iter().into_streaming_iter_ref(), query, source),
format_captures(m.captures().iter().into_streaming_iter_ref(), query, source),
));
}
result
@ -347,7 +347,7 @@ pub fn collect_captures<'a>(
query: &'a Query,
source: &'a str,
) -> Vec<(&'a str, &'a str)> {
format_captures(captures.map(|(m, i)| m.captures[*i]), query, source)
format_captures(captures.map(|(m, i)| m.captures()[*i]), query, source)
}
fn format_captures<'a>(

View file

@ -2,7 +2,7 @@ use std::{
ffi::CString,
fs,
os::raw::c_char,
ptr, slice, str,
ptr, slice,
sync::{
LazyLock,
atomic::{AtomicUsize, Ordering},
@ -496,7 +496,7 @@ fn test_highlighting_cancellation() {
let found_cancellation_error = events.any(|event| match event {
Ok(_) => false,
Err(Error::Cancelled) => true,
Err(Error::InvalidLanguage | Error::Unknown) => {
Err(Error::InvalidLanguage(_) | Error::Unknown) => {
unreachable!("Unexpected error type while iterating events")
}
});

View file

@ -31,11 +31,12 @@ fn test_lookahead_iterator() {
let mut lookahead = language.lookahead_iterator(next_state).unwrap();
assert_eq!(*lookahead.language(), language);
assert!(lookahead.iter_names().eq(expected_symbols));
assert_eq!(lookahead.iter_names().count(), 0);
lookahead.reset_state(next_state);
assert!(lookahead.reset_state(next_state));
assert!(lookahead.iter_names().eq(expected_symbols));
lookahead.reset(&language, next_state);
assert!(lookahead.reset(&language, next_state));
assert!(
lookahead
.map(|s| language.node_kind_for_id(s).unwrap())
@ -66,6 +67,33 @@ fn test_lookahead_iterator_modifiable_only_by_mut() {
let _ = names.next();
}
#[test]
fn test_lookahead_iterator_exhaustion() {
let language = get_language("json");
for state in 0..language.parse_state_count() {
let state = u16::try_from(state).unwrap();
let mut lookahead = language.lookahead_iterator(state).unwrap();
// A fresh iterator is not positioned on a symbol.
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.current_symbol_name(), None);
let count = lookahead.by_ref().count();
// An exhausted iterator is not positioned on a symbol, and stays exhausted.
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.current_symbol_name(), None);
assert_eq!(lookahead.by_ref().count(), 0);
assert_eq!(lookahead.iter_names().count(), 0);
// Resetting restores it exactly.
assert!(lookahead.reset_state(state));
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.by_ref().count(), count);
}
}
#[test]
fn test_symbol_metadata_checks() {
let language = get_language("rust");

View file

@ -454,7 +454,7 @@ fn test_node_child_by_field_name_with_extra_hidden_children() {
// In the Python grammar, some fields are applied to `suite` nodes,
// which consist of an invisible `indent` token followed by a block.
// Check that when searching for a child with a field name, we don't
//
// return a hidden child node.
let tree = parser.parse("while a:\n pass", None).unwrap();
let while_node = tree.root_node().child(0).unwrap();
assert_eq!(while_node.kind(), "while_statement");
@ -950,6 +950,13 @@ fn test_node_sexp() {
#[test]
fn test_node_field_names() {
// - "x":
// This isn't used in the test, but prevents `_hidden_rule1` from being eliminated as a
// unit reduction.
// - "_hidden_rule1":
// Fields pointing to hidden nodes with a single child resolve to the child.
// - "_hidden_rule2":
// Fields within hidden nodes can be referenced through the parent node.
let (parser_name, parser_code) = generate_parser(
r#"
{
@ -972,8 +979,6 @@ fn test_node_field_names() {
{"type": "STRING", "value": "child-1"},
{"type": "BLANK"},
// This isn't used in the test, but prevents `_hidden_rule1`
// from being eliminated as a unit reduction.
{
"type": "ALIAS",
"value": "x",
@ -994,7 +999,6 @@ fn test_node_field_names() {
]
},
// Fields pointing to hidden nodes with a single child resolve to the child.
"_hidden_rule1": {
"type": "CHOICE",
"members": [
@ -1003,7 +1007,6 @@ fn test_node_field_names() {
]
},
// Fields within hidden nodes can be referenced through the parent node.
"_hidden_rule2": {
"type": "SEQ",
"members": [

View file

@ -66,9 +66,13 @@ fn test_parsing_with_logging() {
parser.set_language(&get_language("rust")).unwrap();
let mut messages = Vec::new();
parser.set_logger(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
// SAFETY: the logger borrows `messages` and is only invoked during the
// `parse` call below while `messages` is in scope.
unsafe {
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
}
parser
.parse(
@ -249,6 +253,79 @@ fn test_parsing_with_custom_utf16_be_input() {
assert_eq!(root.child(0).unwrap().kind(), "function_item");
}
#[test]
fn test_utf16_decodes_surrogate_pairs() {
let mut parser = Parser::new();
let language = get_test_fixture_language("utf16_surrogate_oob");
parser.set_language(&language).unwrap();
let le = [0xD83D_u16.to_le(), 0xDE00_u16.to_le()];
let tree = parser.parse_utf16_le(le, None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
let be = [0xD83D_u16.to_be(), 0xDE00_u16.to_be()];
let tree = parser.parse_utf16_be(be, None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
}
#[test]
fn test_utf16_decode_does_not_read_oob() {
// Test for a buffer over-read in ts_decode_utf16_le/be when a lead surrogate
// is the last code unit in a chunk. The test grammar's external scanner
// distinguishes surrogate code points from supplementary-plane characters,
// making the over-read directly observable in the parse tree.
//
// Buffer layout:
// buf[0] = 0xD83E (lead surrogate)
// buf[1] = 0xDD8B (POISON: fake trail surrogate, adjacent in memory)
//
// The callback returns only buf[0..1] (one code unit = 2 bytes).
//
// When functioning correctly, this test passes a length of 2 bytes, which is
// interpreted as 2/2 = 1 code unit, and thus doesn't over-read into the "poison"
// fake trail surrogate. If an over-read does occur, the scanner sees a
// supplementary token.
let mut parser = Parser::new();
let language = get_test_fixture_language("utf16_surrogate_oob");
parser.set_language(&language).unwrap();
let buf = vec![
0xD83E, // lead surrogate (the only "visible" code unit)
0xDD8B, // POISON: adjacent in Vec memory, past the chunk
];
assert_eq!("🦋", String::from_utf16(&buf).unwrap());
let mut callback = |offset: usize, _position: Point| -> &[u16] {
// only expose buf[0], never buf[1]
if offset >= 1 {
return [].as_slice();
}
&buf[0..1]
};
// Use the parse function matching the host endianness, since the
// buffer contains native u16 values.
#[cfg(target_endian = "little")]
let tree = parser
.parse_utf16_le_with_options(&mut callback, None, None)
.unwrap();
#[cfg(target_endian = "big")]
let tree = parser
.parse_utf16_be_with_options(&mut callback, None, None)
.unwrap();
let root = tree.root_node();
// Correct: scanner sees raw surrogate (0xD83E) -> `surrogate` node
// Incorrect: scanner sees supplementary (U+1F98B, aka 🦋) -> `supplementary` node
assert_eq!(
root.to_sexp(),
"(program (surrogate))",
"buffer over-read: decoder read past chunk boundary and formed a \
supplementary character from OOB adjacent memory"
);
}
#[test]
fn test_parsing_with_callback_returning_owned_strings() {
let mut parser = Parser::new();
@ -1059,7 +1136,7 @@ fn test_parsing_with_timeout_during_balancing() {
Some(ParseOptions::new().progress_callback(&mut |state| {
// Because we've already finished parsing, we should only be resuming the
// balancing phase.
assert!(state.current_byte_offset() == current_byte_offset);
assert_eq!(state.current_byte_offset(), current_byte_offset);
ControlFlow::Continue(())
})),
)
@ -1727,11 +1804,15 @@ fn test_parsing_with_scanner_logging() {
.unwrap();
let mut found = false;
parser.set_logger(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
// SAFETY: the logger borrows `found` and is only invoked during the `parse`
// call below, while `found` is in scope.
unsafe {
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
}
let source_code = "x + %(sup (external) scanner?)";

View file

@ -1,9 +1,9 @@
[package]
name = "tree-sitter-tests-proc-macro"
version = "0.0.0"
edition.workspace = true
name = "tree-sitter-tests-proc-macro"
version = "0.0.0"
edition.workspace = true
rust-version.workspace = true
publish = false
publish = false
[lints]
workspace = true
@ -13,5 +13,5 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0.93"
quote = "1.0.38"
syn = { features = [ "full" ], version = "2.0.96" }
quote = "1.0.38"
syn = { features = [ "full" ], version = "2.0.96" }

View file

@ -5,8 +5,8 @@ use rand::{SeedableRng, prelude::StdRng};
use streaming_iterator::StreamingIterator;
use tree_sitter::{
CaptureQuantifier, InputEdit, Language, Node, Parser, Point, Query, QueryCursor,
QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg,
QueryProperty, Range,
QueryCursorOptions, QueryCursorState, QueryError, QueryErrorKind, QueryPredicate,
QueryPredicateArg, QueryProperty, Range,
};
use tree_sitter_generate::load_grammar_file;
use unindent::Unindent;
@ -256,6 +256,51 @@ fn test_query_errors_on_invalid_syntax() {
});
}
#[test]
fn test_query_errors_on_anchor_at_group_edge() {
allocations::record(|| {
let language = get_language("javascript");
// Anchors between siblings, or at the first/last position of a *node*
// pattern, are valid.
assert!(Query::new(&language, "((_) . (_))").is_ok());
assert!(Query::new(&language, "(program (_) (_) .)").is_ok());
assert!(Query::new(&language, "(program (_)* @x . (_))").is_ok());
// A `.` at the edge of a *group* is rejected. A group is not a node, so it
// has no last child to anchor, and there is no sibling within the group to
// anchor to.
assert_eq!(
Query::new(&language, "((_) .)").unwrap_err(),
QueryError {
row: 0,
offset: 5,
column: 5,
kind: QueryErrorKind::Syntax,
message: [
"((_) .)", //
" ^"
]
.join("\n")
}
);
assert_eq!(
Query::new(&language, "(program ((_)+ .)? (_))").unwrap_err(),
QueryError {
row: 0,
offset: 15,
column: 15,
kind: QueryErrorKind::Syntax,
message: [
"(program ((_)+ .)? (_))", //
" ^"
]
.join("\n")
}
);
});
}
#[test]
fn test_query_errors_on_invalid_symbols() {
allocations::record(|| {
@ -371,6 +416,16 @@ fn test_query_errors_on_invalid_symbols() {
message: "\"fakefield\"".to_string()
}
);
assert_eq!(
Query::new(&language, "(MISS)").unwrap_err(),
QueryError {
row: 0,
offset: 1,
column: 1,
kind: QueryErrorKind::NodeType,
message: "\"MISS\"".to_string(),
}
);
});
}
@ -1108,8 +1163,8 @@ fn test_query_matches_with_immediate_siblings() {
// siblings before that child node.
// 2. After the last child node in a pattern, it means that there cannot be any named
// sibling after that child node.
// 2. Between two child nodes in a pattern, it specifies that there cannot be any named
// siblings between those two child snodes.
// 3. Between two child nodes in a pattern, it specifies that there cannot be any named
// siblings between those two child nodes.
let query = Query::new(
&language,
"
@ -1177,6 +1232,209 @@ fn test_query_matches_with_immediate_siblings() {
});
}
#[test]
fn test_query_matches_with_anchor_after_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
"(program (comment)* @doc . (function_declaration name: (identifier) @name))",
)
.unwrap();
// No comments and the function is not the first child. An anchor after a
// zero-matched quantifier is vacuous, so the function still matches.
assert_query_matches(
&language,
&query,
"
class X {}
function foo() {}
",
&[(0, vec![("name", "foo")])],
);
// With at least one comment the anchor applies, so the comments must
// immediately precede the function.
assert_query_matches(
&language,
&query,
"
// c
function foo() {}
",
&[(0, vec![("doc", "// c"), ("name", "foo")])],
);
});
}
#[test]
fn test_query_matches_with_anchor_after_nested_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
r#"
(_
(field_definition
property: (_) @name
value: (_)? @value
) @field
.
";" @semicolon
)
"#,
)
.unwrap();
assert_query_matches(
&language,
&query,
"class Foo { bar; baz = 0; }",
&[
(
0,
vec![("field", "bar"), ("name", "bar"), ("semicolon", ";")],
),
(
0,
vec![
("field", "baz = 0"),
("name", "baz"),
("value", "0"),
("semicolon", ";"),
],
),
],
);
});
}
#[test]
fn test_query_matches_with_last_child_anchor_after_optional() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(
&language,
"(preproc_if (preproc_def)+ @def . (preproc_else)? @else .)",
)
.unwrap();
// The optional `(preproc_else)?` is absent, so the trailing anchor's
// last-child requirement transfers to the last `preproc_def`. A trailing
// comment means the def is not the last child, so nothing matches.
assert_query_matches(
&language,
&query,
"
#if X
#define A
// c
#endif
",
&[],
);
// With the def as the last child, the (else-less) match is allowed.
assert_query_matches(
&language,
&query,
"
#if X
#define A
#endif
",
&[(0, vec![("def", "#define A\n")])],
);
});
}
#[test]
fn test_query_matches_with_anchors_on_both_sides_of_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
"(program (lexical_declaration) @a . (comment)* . (function_declaration) @b)",
)
.unwrap();
// Anchors on both sides of a zero-matched quantifier collapse into a single
// adjacency constraint: with no comments, the declaration must be immediately
// followed by the function.
assert_query_matches(
&language,
&query,
"
const a = 1;
const b = 2;
function foo() {}
",
&[(0, vec![("a", "const b = 2;"), ("b", "function foo() {}")])],
);
// With a comment present the quantifier is non-zero, so the anchors apply
// normally: the comment must sit immediately between the declaration and the
// function.
assert_query_matches(
&language,
&query,
"
const b = 2;
// c
function foo() {}
",
&[(0, vec![("a", "const b = 2;"), ("b", "function foo() {}")])],
);
});
}
#[test]
fn test_query_matches_with_leading_anchor_before_zero_quantifier() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(
&language,
"(translation_unit . (comment)* (function_definition) @f)",
)
.unwrap();
// The leading `.` anchors the comment run to the parent's first child. When the
// run matches zero comments, that first-child requirement transfers to the
// function, so it matches only when it is itself the first child.
assert_query_matches(
&language,
&query,
"
int main() {}
",
&[(0, vec![("f", "int main() {}")])],
);
// The function is the second child, so with no leading comments it must not match.
assert_query_matches(
&language,
&query,
"
int a;
int main() {}
",
&[],
);
// With a leading comment the run starts at the first child and the function follows.
assert_query_matches(
&language,
&query,
"
// c
int main() {}
",
&[(0, vec![("f", "int main() {}")])],
);
});
}
#[test]
fn test_query_matches_with_last_named_child() {
allocations::record(|| {
@ -1349,6 +1607,35 @@ fn test_query_matches_with_repeated_leaf_nodes() {
});
}
#[test]
fn test_query_matches_optional_capture_before_uncaptured_required_sibling() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(&language, "(block (line_comment)? @doc (line_comment))").unwrap();
// The optional `(line_comment)? @doc` before an *uncaptured* required
// `(line_comment)` yields two candidate completions at the block:
// - one where the optional captured `// a` (the required node is then `// b`),
// - one where the optional matched zero (the required node absorbs `// a`,
// leaving `@doc` unbound).
// The zero-match completion's captures are a strict subset of the other's, so the
// longest-match rule must drop it (there is exactly one match). This regressed when
// the dedup pass gained an early-break. Without the accompanying capture-position
// sort, the break skips the subset "loser" and it leaks as an extra empty match.
assert_query_matches(
&language,
&query,
"
fn f() {
// a
// b
}
",
&[(0, vec![("doc", "// a")])],
);
});
}
#[test]
fn test_query_matches_with_optional_nodes_inside_of_repetitions() {
allocations::record(|| {
@ -1593,6 +1880,148 @@ fn test_query_matches_with_leading_zero_or_more_repeated_leaf_nodes() {
});
}
#[test]
fn test_matches_with_anchor_sibling_inside_parent() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(
&language,
"
(source_file
(line_comment)
.
(function_item
name: (identifier) @name)
)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
// A
fn a() {}
// B
fn b() {}
",
&[(0, vec![("name", "a")]), (0, vec![("name", "b")])],
);
});
}
#[test]
fn test_matches_with_anchor_sibling_with_quantifier_inside_parent() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(
&language,
"
(source_file
(line_comment)+
.
(function_item
name: (identifier) @name)
)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
// A
fn a() {}
// B
fn b() {}
",
&[(0, vec![("name", "a")]), (0, vec![("name", "b")])],
);
});
}
#[test]
fn test_matches_with_anchor_sibling_with_quantifier_captured_inside_parent() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(
&language,
"
(source_file
(line_comment)+ @doc
.
(function_item
name: (identifier) @name)
)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
// A
fn a() {}
// B
fn b() {}
",
&[
(0, vec![("doc", "// A"), ("name", "a")]),
(0, vec![("doc", "// B"), ("name", "b")]),
],
);
});
}
#[test]
fn test_matches_anchored_quantified_sibling_inside_parent() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(
&language,
"(translation_unit (comment)* @comment . (declaration) @decl)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
void foo() {}
// this one has
// two comments
extern int baz;
// this one has a comment
extern int bar;
",
&[
(
0,
vec![
("comment", "// this one has"),
("comment", "// two comments"),
("decl", "extern int baz;"),
],
),
(
0,
vec![
("comment", "// this one has a comment"),
("decl", "extern int bar;"),
],
),
],
);
});
}
#[test]
fn test_query_matches_with_trailing_optional_nodes() {
allocations::record(|| {
@ -1941,7 +2370,7 @@ fn test_query_matches_with_too_many_permutations_to_track() {
let matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
// For this pathological query, some match permutations will be dropped.
// Just check that a subset of the results are returned, and crash or
// Just check that a subset of the results are returned, and no crash or
// leak occurs.
assert_eq!(
collect_matches(matches, &query, source.as_str())[0],
@ -2489,7 +2918,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2505,7 +2934,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2521,7 +2950,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2579,7 +3008,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
let mut results = Vec::new();
let mut first_five = captures.by_ref().take(5);
while let Some((mat, capture_ix)) = first_five.next() {
let capture = mat.captures[*capture_ix];
let capture = mat.captures()[*capture_ix];
results.push((
query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()],
@ -2602,7 +3031,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
results.clear();
captures.set_byte_range(source.find("Ok").unwrap()..source.len());
while let Some((mat, capture_ix)) = captures.next() {
let capture = mat.captures[*capture_ix];
let capture = mat.captures()[*capture_ix];
results.push((
query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()],
@ -2901,7 +3330,7 @@ fn test_query_matches_with_captured_wildcard_at_root() {
while let Some(m) = match_iter.next() {
let captures = m
.captures
.captures()
.iter()
.map(|c| {
(
@ -3111,6 +3540,39 @@ fn test_query_alternation_with_inner_quantifier() {
assert_query_matches(&language, &query, source_code, matches);
}
#[test]
fn test_query_alternation_with_outer_quantifier() {
let language = get_language("c");
let source_code = "#include <foo>
#include <bar>
#include <baz>
// comment";
let matches = &[(
0,
vec![
("capture", "#include <foo>\n"),
("capture", "#include <bar>\n"),
("capture", "#include <baz>\n"),
("capture", "// comment"),
],
)];
let query = "[
(preproc_include)
(comment)
]+ @capture";
let query = Query::new(&language, query).unwrap();
assert_query_matches(&language, &query, source_code, matches);
let query = "([
(preproc_include)
(comment)
] (_)?)+ @capture";
let query = Query::new(&language, query).unwrap();
assert_query_matches(&language, &query, source_code, matches);
}
#[test]
fn test_query_matches_with_alternations_and_predicates() {
allocations::record(|| {
@ -3849,7 +4311,7 @@ fn test_query_captures_with_matches_removed() {
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() {
let capture = m.captures[*i];
let capture = m.captures()[*i];
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "a" {
m.remove();
@ -3894,7 +4356,7 @@ fn test_query_captures_with_matches_removed_before_they_finish() {
let mut captured_strings = Vec::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() {
let capture = m.captures[*i];
let capture = m.captures()[*i];
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "as" {
m.remove();
@ -3935,18 +4397,18 @@ fn test_query_captures_and_matches_iterators_are_fused() {
let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert!(captures.next().is_none());
assert!(captures.next().is_none());
assert!(captures.next().is_none());
drop(captures);
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert!(matches.next().is_none());
assert!(matches.next().is_none());
assert!(matches.next().is_none());
@ -4126,7 +4588,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
.matches(&query, node, source.as_bytes())
.next()
.unwrap()
.captures[0]
.captures()[0]
.node
}
@ -4147,7 +4609,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
.next()
.unwrap()
.0
.captures[0]
.captures()[0]
.node
}
@ -4161,7 +4623,7 @@ fn test_query_with_no_patterns() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(&language, "").unwrap();
assert!(query.capture_names().is_empty());
assert_eq!(query.capture_names(), [] as [&str; 0]);
assert_eq!(query.pattern_count(), 0);
});
}
@ -4234,6 +4696,54 @@ fn test_query_disable_pattern() {
});
}
#[test]
fn test_query_deep_clone() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
"
(function_declaration
name: (identifier) @name)
(function_declaration
body: (statement_block) @body)
",
)
.unwrap();
let mut clone = query.deep_clone();
clone.disable_pattern(1);
let source = "function foo() { return 1; }";
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let tree = parser.parse(source, None).unwrap();
let mut cursor = QueryCursor::new();
// The clone with pattern 1 disabled only produces the @name match.
let clone_matches = collect_matches(
cursor.matches(&clone, tree.root_node(), source.as_bytes()),
&clone,
source,
);
assert_eq!(clone_matches, &[(0, vec![("name", "foo")])]);
// The original is unaffected and still produces both @name and @body.
let original_matches = collect_matches(
cursor.matches(&query, tree.root_node(), source.as_bytes()),
&query,
source,
);
assert_eq!(
original_matches,
&[
(0, vec![("name", "foo")]),
(1, vec![("body", "{ return 1; }")]),
]
);
});
}
#[test]
fn test_query_alternative_predicate_prefix() {
allocations::record(|| {
@ -4309,7 +4819,7 @@ fn test_query_random() {
let transformed_match = Match {
last_node: None,
captures: mat
.captures
.captures()
.iter()
.map(|c| (query.capture_names()[c.index as usize], c.node))
.collect::<Vec<_>>(),
@ -5323,10 +5833,10 @@ fn test_consecutive_zero_or_modifiers() {
let mut len_1 = false;
while let Some(m) = matches.next() {
if m.captures.len() == 3 {
if m.captures().len() == 3 {
len_3 = true;
}
if m.captures.len() == 1 {
if m.captures().len() == 1 {
len_1 = true;
}
}
@ -5388,7 +5898,7 @@ fn test_query_max_start_depth_more() {
let query = Query::new(&language, "(compound_statement) @capture").unwrap();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let node = matches.next().unwrap().captures[0].node;
let node = matches.next().unwrap().captures()[0].node;
assert_eq!(node.kind(), "compound_statement");
for row in rows {
@ -5564,21 +6074,21 @@ fn test_query_execution_with_timeout() {
let tree = parser.parse(&source_code, None).unwrap();
let query = Query::new(&language, "(function_declaration) @function").unwrap();
let mut cursor = QueryCursor::new();
let start_time = std::time::Instant::now();
let mut progress_callback = |_: &QueryCursorState| {
if start_time.elapsed().as_micros() > 1000 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
};
let mut cursor = QueryCursor::new();
let matches = cursor
.matches_with_options(
&query,
tree.root_node(),
source_code.as_bytes(),
QueryCursorOptions::new().progress_callback(&mut |_| {
if start_time.elapsed().as_micros() > 1000 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}),
QueryCursorOptions::new().progress_callback(&mut progress_callback),
)
.count();
assert!(matches < 1000);
@ -5589,6 +6099,33 @@ fn test_query_execution_with_timeout() {
assert_eq!(matches, 1000);
}
#[test]
fn test_query_progress_callback_lives_as_long_as_matches() {
let language = get_language("javascript");
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let source_code = "function foo() {}\n".repeat(1000);
let tree = parser.parse(&source_code, None).unwrap();
let query = Query::new(&language, "(function_declaration) @function").unwrap();
let mut cursor = QueryCursor::new();
let mut callback_was_called = false;
let mut progress_callback = |_: &QueryCursorState| {
callback_was_called = true;
ControlFlow::Continue(())
};
let matches = cursor.matches_with_options(
&query,
tree.root_node(),
source_code.as_bytes(),
QueryCursorOptions::new().progress_callback(&mut progress_callback),
);
assert_eq!(matches.count(), 1000);
assert!(callback_was_called);
}
#[test]
fn test_query_execution_with_points_causing_underflow() {
let language = get_language("rust");
@ -6000,3 +6537,64 @@ export default grammar({
assert_query_matches(&language, &query, source, &[(0, vec![("tuple", "()")])]);
}
#[test]
fn test_last_child_anchor_looks_past_hidden_repeat() {
let language = get_test_fixture_language("last_child_anchor_past_hidden_repeat");
let source = "T a.b.c\nL a.b.c\nN a!.b!.c\n";
let query = Query::new(
&language,
"
(trailing_sep (name) @last .)
(leading_sep (name) @last .)
(trailing_named (name) @last .)
",
)
.unwrap();
assert_query_matches(
&language,
&query,
source,
&[
(0, vec![("last", "c")]),
(1, vec![("last", "c")]),
(2, vec![("last", "c")]),
],
);
let query = Query::new(
&language,
"
(trailing_sep . (name) @first)
(trailing_sep (name) @a . (name) @b)
",
)
.unwrap();
assert_query_matches(
&language,
&query,
source,
&[
(0, vec![("first", "a")]),
(1, vec![("a", "a"), ("b", "b")]),
(1, vec![("a", "b"), ("b", "c")]),
],
);
}
#[test]
fn test_last_child_anchor_looks_past_hidden_node() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(&language, "(translation_unit (_) @last .)").unwrap();
let source = "enum E { A };\nint x;\nint y;\n";
assert_query_matches(&language, &query, source, &[(0, vec![("last", "int y;")])]);
});
}

View file

@ -1,6 +1,6 @@
use std::{
ffi::{CStr, CString},
fs, ptr, slice, str,
fs, ptr, slice,
sync::atomic::{AtomicUsize, Ordering},
};

View file

@ -4,7 +4,7 @@ use tree_sitter_highlight::{Highlight, Highlighter};
use super::helpers::fixtures::{get_highlight_config, get_language, test_loader};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments},
test_highlight::get_highlight_positions,
test_highlight::{Failure, get_highlight_positions, iterate_assertions},
};
#[test]
@ -68,3 +68,195 @@ fn test_highlight_test_with_basic_test() {
]
);
}
#[test]
fn test_assertion_with_non_matching_highlight_at_same_position() {
// Test that an assertion fails when the highlight at the position does not match
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![Assertion::new(1, 0, 1, false, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(1)), // "variable" highlight
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 1);
assert_eq!(err.column, 0);
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, vec!["variable".to_string()]);
}
#[test]
fn test_assertion_with_exact_matching_highlight() {
// Test exact match: assertion and highlight have same start and end
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertion_contained_within_highlight() {
// Test where assertion is fully contained within a larger highlight
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 3, 2, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertion_overlapping_highlight_start() {
// Test where assertion starts before highlight but overlaps with it
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 3, 4, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 10), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertion_with_no_highlights() {
// Test that an assertion fails when there are no highlights at all
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 0, 1, false, String::from("keyword"))];
let highlights = vec![];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 0);
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, Vec::<String>::new());
}
#[test]
fn test_assertion_with_highlight_ending_before() {
// Test where highlight ends before the assertion starts
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 10, 1, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 10);
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, Vec::<String>::new());
}
#[test]
fn test_negative_assertion_with_non_matching_highlight() {
// Test that a negative assertion passes when the specified highlight is NOT present
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(1)), // "variable" highlight
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_negative_assertion_with_matching_highlight() {
// Test that a negative assertion fails when the specified highlight IS present
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0)), // "keyword" highlight
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 0);
assert_eq!(err.expected_highlight, "!keyword");
assert_eq!(err.actual_highlights, vec!["keyword".to_string()]);
}
#[test]
fn test_multiple_assertions_sequential() {
// Test multiple assertions in sequence with non-overlapping highlights
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![
Assertion::new(0, 0, 3, false, String::from("keyword")),
Assertion::new(0, 10, 1, false, String::from("variable")),
];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 3), Highlight(0)), // "keyword"
(Utf8Point::new(0, 10), Utf8Point::new(0, 11), Highlight(1)), // "variable"
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2);
}
#[test]
fn test_multiple_highlights_at_same_position() {
// Test where multiple highlights overlap at the assertion position
let highlight_names = vec![
"keyword".to_string(),
"variable".to_string(),
"function".to_string(),
];
let assertions = vec![Assertion::new(0, 5, 1, false, String::from("variable"))];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0)), // "keyword" spans entire range
(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(1)), // "variable" at assertion position
(Utf8Point::new(0, 7), Utf8Point::new(0, 12), Highlight(2)), // "function" overlaps
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertions_across_multiple_rows() {
// Test assertions on different rows
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![
Assertion::new(0, 5, 3, false, String::from("keyword")),
Assertion::new(2, 10, 1, false, String::from("variable")),
];
let highlights = vec![
(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0)), // "keyword" on row 0
(Utf8Point::new(2, 10), Utf8Point::new(2, 11), Highlight(1)), // "variable" on row 2
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2);
}
#[test]
fn test_assertion_should_not_match_highlight_on_later_row() {
// Test logic for early exit when highlight is on a later row than the assertion
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(0)), // wrong row
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 7); // end_column
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, Vec::<String>::new());
}

View file

@ -30,7 +30,7 @@ fn tree_query<I: AsRef<[u8]>>(tree: &Tree, text: impl TextProvider<I>, language:
let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), text);
let (match_, idx) = captures.next().unwrap();
let capture = match_.captures[*idx];
let capture = match_.captures()[*idx];
assert_eq!(capture.index as usize, *idx);
assert_eq!("comment", capture.node.kind());
}

View file

@ -1,5 +1,3 @@
use std::str;
use tree_sitter::{InputEdit, Parser, Point, Range, Tree};
use super::helpers::fixtures::get_language;
@ -795,3 +793,44 @@ fn get_changed_ranges(
*tree = new_tree;
result
}
// Regression test for an incremental reparse bug where an external
// scanner's choice depends on lexer->eof() (and thus on the parser's
// current included ranges). The cached token at byte 0 was emitted when
// the included range stopped just after the opener. Widening the range
// to include a later matching delimiter must invalidate that cached
// token so the scanner re-runs and emits the open form instead of the
// unclosed form.
#[test]
fn test_reuse_invalidates_scanner_token_when_included_range_expands() {
let language = get_test_fixture_language("external_lookahead_eof_boundary");
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let source = "``";
parser
.set_included_ranges(&[Range {
start_byte: 0,
end_byte: 1,
start_point: Point::new(0, 0),
end_point: Point::new(0, 1),
}])
.unwrap();
let tree1 = parser.parse(source, None).unwrap();
assert_eq!(tree1.root_node().to_sexp(), "(document (unclosed_delim))");
parser
.set_included_ranges(&[Range {
start_byte: 0,
end_byte: 2,
start_point: Point::new(0, 0),
end_point: Point::new(0, 2),
}])
.unwrap();
let tree2 = parser.parse(source, Some(&tree1)).unwrap();
assert_eq!(
tree2.root_node().to_sexp(),
"(document (span (open_delim) (close_delim)))"
);
}

View file

@ -314,6 +314,55 @@ fn test_load_wasm_errors() {
});
}
#[test]
fn test_load_wasm_language_with_reserved_words() {
// This test exercises a grammar with multiple reserved word sets loaded via WASM.
allocations::record(|| {
let store = WasmStore::new(&ENGINE).unwrap();
let language = get_test_fixture_language_wasm("reserved_words");
let mut parser = Parser::new();
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
// "if" and "while" are globally reserved, so using them as identifiers
// should produce an error recovery.
let tree = parser
.parse("var a =\n\nif (something) {\n c();\n}", None)
.unwrap();
assert_eq!(
tree.root_node().to_sexp(),
concat!(
"(program ",
"(ERROR (identifier)) ",
"(if_statement (parenthesized_expression (identifier)) ",
"(block (expression_statement (call_expression (identifier))))))",
)
);
// "if" and "while" are NOT reserved in the 'property' context, so they
// can appear as object keys without error.
let tree = parser
.parse("var x = {\n if: a,\n while: b,\n};", None)
.unwrap();
assert_eq!(
tree.root_node().to_sexp(),
concat!(
"(program (var_declaration (identifier) (object ",
"(pair (identifier) (identifier)) (pair (identifier) (identifier)))))"
)
);
// "var" IS reserved in the 'property' context, so using it as a property
// key triggers error recovery.
let tree = parser.parse("var x = {\nvar y = z;", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(program (ERROR (identifier)) (var_declaration (identifier) (identifier)))"
);
});
}
#[test]
fn test_wasm_oom() {
allocations::record(|| {
@ -338,3 +387,18 @@ fn test_wasm_oom() {
);
});
}
#[test]
fn test_lookahead_iterator_outlives_wasm_language() {
allocations::record(|| {
let mut store = WasmStore::new(&ENGINE).unwrap();
let wasm = fs::read(WASM_DIR.join("tree-sitter-ruby.wasm")).unwrap();
let language = store.load_language("ruby", &wasm).unwrap();
let mut lookahead = language.lookahead_iterator(0).unwrap();
drop(language);
// The iterator retains the language, so the names are still live.
assert!(lookahead.iter_names().count() > 0);
});
}

View file

@ -5,6 +5,7 @@ pub mod highlight;
pub mod init;
pub mod input;
pub mod logger;
pub mod paint;
pub mod parse;
pub mod playground;
pub mod query;

View file

@ -71,8 +71,8 @@ pub struct LogSession {
open_log: bool,
}
pub fn print_tree_graph(tree: &Tree, path: &str, quiet: bool) -> Result<()> {
let session = LogSession::new(path, quiet)?;
pub fn print_tree_graph(tree: &Tree, path: &str, open_log: bool) -> Result<()> {
let session = LogSession::new(path, open_log)?;
tree.print_dot_graph(session.dot_process_stdin.as_ref().unwrap());
Ok(())
}

View file

@ -15,7 +15,7 @@ pub fn load_language_wasm_file(language_dir: &Path) -> Result<(String, Vec<u8>)>
.unwrap();
let wasm_filename = format!("tree-sitter-{grammar_name}.wasm");
let contents = fs::read(language_dir.join(&wasm_filename)).with_context(|| {
format!("Failed to read {wasm_filename}. Run `tree-sitter build --wasm` first.",)
format!("Failed to read {wasm_filename}. Run `tree-sitter build --wasm` first.")
})?;
Ok((grammar_name, contents))
}

View file

@ -1,17 +1,16 @@
[package]
name = "tree-sitter-config"
version.workspace = true
description = "User configuration of tree-sitter's command line programs"
authors.workspace = true
edition.workspace = true
name = "tree-sitter-config"
version.workspace = true
description = "User configuration of tree-sitter's command line programs"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-config"
license.workspace = true
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-config"
license.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
path = "src/tree_sitter_config.rs"
@ -20,8 +19,8 @@ path = "src/tree_sitter_config.rs"
workspace = true
[dependencies]
etcetera.workspace = true
log.workspace = true
serde.workspace = true
etcetera.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
thiserror.workspace = true

View file

@ -28,14 +28,24 @@ pub enum ConfigError {
#[derive(Debug, Error)]
pub struct IoError {
pub error: std::io::Error,
pub path: Option<String>,
pub path: Option<PathBuf>,
}
impl PartialEq for IoError {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
&& self.error.kind() == other.error.kind()
&& self.error.raw_os_error() == other.error.raw_os_error()
}
}
impl Eq for IoError {}
impl IoError {
fn new(error: std::io::Error, path: Option<&Path>) -> Self {
Self {
error,
path: path.map(|p| p.to_string_lossy().to_string()),
path: path.map(Path::to_path_buf),
}
}
}
@ -44,7 +54,7 @@ impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)?;
if let Some(ref path) = self.path {
write!(f, " ({path})")?;
write!(f, " ({})", path.display())?;
}
Ok(())
}

View file

@ -1,17 +1,16 @@
[package]
name = "tree-sitter-generate"
version.workspace = true
description = "Library for generating C source code from a tree-sitter grammar"
authors.workspace = true
edition.workspace = true
name = "tree-sitter-generate"
version.workspace = true
description = "Library for generating C source code from a tree-sitter grammar"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-generate"
license.workspace = true
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-generate"
license.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
path = "src/generate.rs"
@ -21,24 +20,19 @@ workspace = true
[features]
default = [ "qjs-rt" ]
load = [ "dep:semver" ]
qjs-rt = [ "load", "rquickjs", "pathdiff" ]
load = [ "dep:semver" ]
qjs-rt = [ "load", "rquickjs", "pathdiff" ]
[dependencies]
bitflags = "2.10.0"
bitflags = "2.11.1"
dunce = "1.0.5"
hashbrown.workspace = true
indexmap.workspace = true
indoc.workspace = true
log.workspace = true
pathdiff = { optional = true, version = "0.2.3" }
regex.workspace = true
regex-syntax.workspace = true
rquickjs = { features = [
"bindgen",
"loader",
"macro",
"phf",
], optional = true, version = "0.11.0" }
rquickjs = { features = [ "bindgen", "loader", "macro", "phf" ], optional = true, version = "0.13" }
rustc-hash.workspace = true
semver = { optional = true, workspace = true }
serde.workspace = true

View file

@ -93,6 +93,7 @@ pub struct BitVec {
}
impl BitVec {
#[must_use]
pub const fn new() -> Self {
Self {
data: ptr::NonNull::dangling().as_ptr(),
@ -101,6 +102,7 @@ impl BitVec {
}
}
#[must_use]
pub fn with_capacity(n_bits: usize) -> Self {
let n_words = n_bits.div_ceil(64);
if n_words == 0 {
@ -120,6 +122,7 @@ impl BitVec {
/// View the in-use words as a slice.
#[inline]
#[must_use]
pub const fn as_slice(&self) -> &[u64] {
let n = self.words_in_use();
if n == 0 {
@ -142,18 +145,21 @@ impl BitVec {
unsafe { std::slice::from_raw_parts_mut(self.data, n) }
}
#[must_use]
#[allow(clippy::len_without_is_empty)]
pub const fn len(&self) -> usize {
self.num_bits as usize
}
pub fn get(&self, index: usize) -> Option<bool> {
#[must_use]
pub const fn get(&self, index: usize) -> Option<bool> {
if index >= self.num_bits as usize {
return None;
}
Some(self.as_slice()[index / 64] >> (index % 64) & 1 != 0)
}
pub fn set(&mut self, index: usize, val: bool) {
pub const fn set(&mut self, index: usize, val: bool) {
let word_idx = index / 64;
let bit_idx = index % 64;
let words = self.as_full_slice_mut();
@ -206,14 +212,15 @@ impl BitVec {
self.num_bits = new_len as u32;
}
pub fn last(&self) -> Option<bool> {
#[must_use]
pub const fn last(&self) -> Option<bool> {
if self.num_bits == 0 {
return None;
}
self.get(self.num_bits as usize - 1)
}
pub fn pop(&mut self) -> Option<bool> {
pub const fn pop(&mut self) -> Option<bool> {
if self.num_bits == 0 {
return None;
}
@ -234,6 +241,7 @@ impl BitVec {
}
/// Word-level OR: self |= other. Returns true if any new bits were set.
#[inline]
pub fn insert_all(&mut self, other: &Self) -> bool {
let other_words = other.words_in_use();
if other_words == 0 {
@ -379,6 +387,7 @@ pub struct SetBitsIter<'a> {
}
impl<'a> SetBitsIter<'a> {
#[must_use]
pub fn new(data: &'a [u64]) -> Self {
Self {
data,

View file

@ -6,28 +6,31 @@ mod item_set_builder;
mod minimize_parse_table;
mod token_conflicts;
use std::collections::{BTreeSet, HashMap};
use std::collections::BTreeSet;
pub use build_lex_table::LARGE_CHARACTER_RANGE_COUNT;
use build_parse_table::BuildTableResult;
pub use build_parse_table::ParseTableBuilderError;
use log::{debug, info};
use rustc_hash::FxHashMap;
use self::{
build_lex_table::build_lex_table,
build_parse_table::{ParseStateInfo, build_parse_table},
coincident_tokens::CoincidentTokenIndex,
item::ItemKeyMap,
item_set_builder::ParseItemSetBuilder,
minimize_parse_table::minimize_parse_table,
token_conflicts::TokenConflictMap,
};
use crate::{
OptLevel,
Diagnostic, OptLevel,
grammars::{InlinedProductionMap, LexicalGrammar, SyntaxGrammar},
nfa::{CharacterSet, NfaCursor},
node_types::VariableInfo,
rules::{AliasMap, Symbol, SymbolType, TokenSet},
tables::{LexTable, ParseAction, ParseTable, ParseTableEntry},
strpool::StrPool,
tables::{ActionList, ActionListPool, LexTable, ParseAction, ParseTable, ParseTableEntry},
};
pub struct Tables {
@ -37,32 +40,42 @@ pub struct Tables {
pub large_character_sets: Vec<(Option<Symbol>, CharacterSet)>,
}
#[expect(
clippy::too_many_arguments,
reason = "all parameters are required for table building"
)]
pub fn build_tables(
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
simple_aliases: &AliasMap,
variable_info: &[VariableInfo],
inlines: &InlinedProductionMap,
str_pool: &StrPool,
report_symbol_name: Option<&str>,
optimizations: OptLevel,
diagnostics: &mut Vec<Diagnostic>,
) -> BuildTableResult<Tables> {
let item_set_builder = ParseItemSetBuilder::new(syntax_grammar, lexical_grammar, inlines);
let following_tokens =
get_following_tokens(syntax_grammar, lexical_grammar, inlines, &item_set_builder);
let item_key_map = ItemKeyMap::new(syntax_grammar, str_pool);
let item_set_builder =
ParseItemSetBuilder::new(syntax_grammar, lexical_grammar, inlines, &item_key_map);
let following_tokens = get_following_tokens(syntax_grammar, lexical_grammar, &item_set_builder);
let (mut parse_table, parse_state_info) = build_parse_table(
syntax_grammar,
lexical_grammar,
item_set_builder,
variable_info,
str_pool,
diagnostics,
)?;
let token_conflict_map = TokenConflictMap::new(lexical_grammar, following_tokens);
let coincident_token_index = CoincidentTokenIndex::new(&parse_table, lexical_grammar);
let coincident_token_index =
CoincidentTokenIndex::new(&parse_table, lexical_grammar, syntax_grammar.word_token);
let keywords = identify_keywords(
lexical_grammar,
&parse_table,
syntax_grammar.word_token,
&token_conflict_map,
&coincident_token_index,
str_pool,
);
populate_error_state(
&mut parse_table,
@ -71,8 +84,10 @@ pub fn build_tables(
&coincident_token_index,
&token_conflict_map,
&keywords,
str_pool,
);
populate_used_symbols(&mut parse_table, syntax_grammar, lexical_grammar);
let mut parse_table = ActionListPool::intern_table(parse_table);
minimize_parse_table(
&mut parse_table,
syntax_grammar,
@ -80,6 +95,7 @@ pub fn build_tables(
simple_aliases,
&token_conflict_map,
&keywords,
str_pool,
optimizations,
);
let lex_tables = build_lex_table(
@ -91,7 +107,10 @@ pub fn build_tables(
&token_conflict_map,
);
populate_external_lex_states(&mut parse_table, syntax_grammar);
mark_fragile_tokens(&mut parse_table, lexical_grammar, &token_conflict_map);
mark_fragile_tokens(&mut parse_table, &token_conflict_map);
parse_table
.action_lists
.canonicalize(&mut parse_table.states);
if let Some(report_symbol_name) = report_symbol_name {
report_state_info(
@ -99,6 +118,7 @@ pub fn build_tables(
lexical_grammar,
&parse_table,
&parse_state_info,
str_pool,
report_symbol_name,
);
}
@ -118,30 +138,25 @@ pub fn build_tables(
fn get_following_tokens(
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
inlines: &InlinedProductionMap,
builder: &ParseItemSetBuilder,
) -> Vec<TokenSet> {
let n_terminals = lexical_grammar.variables.len();
let n_externals = syntax_grammar.external_tokens.len();
let mut result = vec![TokenSet::with_capacity(n_terminals, n_externals); n_terminals];
let productions = syntax_grammar
.variables
.iter()
.flat_map(|v| &v.productions)
.chain(&inlines.productions);
let all_tokens = (0..result.len())
.map(Symbol::terminal)
.collect::<TokenSet>();
for production in productions {
for i in 1..production.steps.len() {
let left_tokens = builder.last_set(&production.steps[i - 1].symbol);
let right_tokens = builder.first_set(&production.steps[i].symbol);
let right_reserved_tokens = builder.reserved_first_set(&production.steps[i].symbol);
for production in &syntax_grammar.productions {
let steps = &syntax_grammar.steps[production.step_range()];
for i in 1..steps.len() {
let left_tokens = builder.last_set(steps[i - 1].symbol());
let right_tokens = builder.first_set(steps[i].symbol());
let right_reserved_tokens = builder.reserved_first_set(steps[i].symbol());
for left_token in left_tokens.iter() {
if left_token.is_terminal() {
result[left_token.index].insert_all_terminals(right_tokens);
result[left_token.index as usize].insert_all_terminals(right_tokens);
if let Some(reserved_tokens) = right_reserved_tokens {
result[left_token.index].insert_all_terminals(reserved_tokens);
result[left_token.index as usize].insert_all_terminals(reserved_tokens);
}
}
}
@ -152,19 +167,20 @@ fn get_following_tokens(
for entry in &mut result {
entry.insert(*extra);
}
result[extra.index] = all_tokens.clone();
result[extra.index as usize] = all_tokens.clone();
}
}
result
}
fn populate_error_state(
parse_table: &mut ParseTable,
parse_table: &mut ParseTable<ParseTableEntry>,
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
coincident_token_index: &CoincidentTokenIndex,
token_conflict_map: &TokenConflictMap,
keywords: &TokenSet,
str_pool: &StrPool,
) {
let state = &mut parse_table.states[0];
let n = lexical_grammar.variables.len();
@ -183,7 +199,7 @@ fn populate_error_state(
} else {
debug!(
"error recovery - token {} has no conflicts",
lexical_grammar.variables[i].name
str_pool.resolve(lexical_grammar.variables[i].name)
);
Some(Symbol::terminal(i))
}
@ -192,30 +208,31 @@ fn populate_error_state(
let recover_entry = ParseTableEntry {
reusable: false,
actions: vec![ParseAction::Recover],
actions: ActionList::One(ParseAction::Recover),
};
// Exclude from the error-recovery state any token that conflicts with one of
// the *conflict-free tokens* identified above.
for i in 0..n {
let symbol = Symbol::terminal(i);
if !conflict_free_tokens.contains(&symbol)
&& !keywords.contains(&symbol)
if !conflict_free_tokens.contains(symbol)
&& !keywords.contains(symbol)
&& syntax_grammar.word_token != Some(symbol)
&& let Some(t) = conflict_free_tokens.iter().find(|t| {
!coincident_token_index.contains(symbol, *t)
&& token_conflict_map.does_conflict(symbol.index, t.index)
&& token_conflict_map.does_conflict(symbol.index as usize, t.index as usize)
})
{
debug!(
"error recovery - exclude token {} because of conflict with {}",
lexical_grammar.variables[i].name, lexical_grammar.variables[t.index].name
str_pool.resolve(lexical_grammar.variables[i].name),
str_pool.resolve(lexical_grammar.variables[t.index as usize].name)
);
continue;
}
debug!(
"error recovery - include token {}",
lexical_grammar.variables[i].name
str_pool.resolve(lexical_grammar.variables[i].name)
);
state
.terminal_entries
@ -236,7 +253,7 @@ fn populate_error_state(
}
fn populate_used_symbols(
parse_table: &mut ParseTable,
parse_table: &mut ParseTable<ParseTableEntry>,
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
) {
@ -246,13 +263,13 @@ fn populate_used_symbols(
for state in &parse_table.states {
for symbol in state.terminal_entries.keys() {
match symbol.kind {
SymbolType::Terminal => terminal_usages[symbol.index] = true,
SymbolType::External => external_usages[symbol.index] = true,
SymbolType::Terminal => terminal_usages[symbol.index as usize] = true,
SymbolType::External => external_usages[symbol.index as usize] = true,
_ => {}
}
}
for symbol in state.nonterminal_entries.keys() {
non_terminal_usages[symbol.index] = true;
non_terminal_usages[symbol.index as usize] = true;
}
}
parse_table.symbols.push(Symbol::end());
@ -264,7 +281,10 @@ fn populate_used_symbols(
// ensure that a subtree's symbol can be successfully reassigned to the word token
// without having to move the subtree to the heap.
// See https://github.com/tree-sitter/tree-sitter/issues/258
if syntax_grammar.word_token.is_some_and(|t| t.index == i) {
if syntax_grammar
.word_token
.is_some_and(|t| t.index as usize == i)
{
parse_table.symbols.insert(1, Symbol::terminal(i));
} else {
parse_table.symbols.push(Symbol::terminal(i));
@ -284,7 +304,7 @@ fn populate_used_symbols(
}
fn populate_external_lex_states(parse_table: &mut ParseTable, syntax_grammar: &SyntaxGrammar) {
let mut external_tokens_by_corresponding_internal_token = HashMap::new();
let mut external_tokens_by_corresponding_internal_token = FxHashMap::default();
for (i, external_token) in syntax_grammar.external_tokens.iter().enumerate() {
if let Some(symbol) = external_token.corresponding_internal_token {
external_tokens_by_corresponding_internal_token.insert(symbol.index, i);
@ -315,16 +335,16 @@ fn populate_external_lex_states(parse_table: &mut ParseTable, syntax_grammar: &S
.unwrap_or_else(|| {
parse_table.external_lex_states.push(external_tokens);
parse_table.external_lex_states.len() - 1
});
}) as u32;
}
}
fn identify_keywords(
lexical_grammar: &LexicalGrammar,
parse_table: &ParseTable,
word_token: Option<Symbol>,
token_conflict_map: &TokenConflictMap,
coincident_token_index: &CoincidentTokenIndex,
str_pool: &StrPool,
) -> TokenSet {
if word_token.is_none() {
return TokenSet::new();
@ -342,12 +362,12 @@ fn identify_keywords(
.filter_map(|(i, variable)| {
cursor.reset(vec![variable.start_state]);
if all_chars_are_alphabetical(&cursor)
&& token_conflict_map.does_match_same_string(i, word_token.index)
&& !token_conflict_map.does_match_different_string(i, word_token.index)
&& token_conflict_map.does_match_same_string(i, word_token.index as usize)
&& !token_conflict_map.does_match_different_string(i, word_token.index as usize)
{
debug!(
"Keywords - add candidate {}",
lexical_grammar.variables[i].name
str_pool.resolve(lexical_grammar.variables[i].name)
);
Some(Symbol::terminal(i))
} else {
@ -362,12 +382,14 @@ fn identify_keywords(
.filter(|token| {
for other_token in keyword_candidates.iter() {
if other_token != *token
&& token_conflict_map.does_match_same_string(other_token.index, token.index)
&& token_conflict_map
.does_match_same_string(other_token.index as usize, token.index as usize)
{
debug!(
"Keywords - exclude {} because it matches the same string as {}",
lexical_grammar.variables[token.index].name,
lexical_grammar.variables[other_token.index].name
str_pool.resolve(lexical_grammar.variables[token.index as usize].name),
str_pool
.resolve(lexical_grammar.variables[other_token.index as usize].name)
);
return false;
}
@ -383,7 +405,7 @@ fn identify_keywords(
.iter()
.filter(|token| {
for other_index in 0..lexical_grammar.variables.len() {
if keyword_candidates.contains(&Symbol::terminal(other_index)) {
if keyword_candidates.contains(Symbol::terminal(other_index)) {
continue;
}
@ -391,26 +413,20 @@ fn identify_keywords(
// this keyword candidate, then substituting the word token won't
// introduce any new lexical conflicts.
if coincident_token_index
.states_with(*token, Symbol::terminal(other_index))
.iter()
.all(|state_id| {
parse_table.states[*state_id]
.terminal_entries
.contains_key(&word_token)
})
.all_coincident_states_have_word(*token, Symbol::terminal(other_index))
{
continue;
}
if !token_conflict_map.has_same_conflict_status(
token.index,
word_token.index,
token.index as usize,
word_token.index as usize,
other_index,
) {
debug!(
"Keywords - exclude {} because of conflict with {}",
lexical_grammar.variables[token.index].name,
lexical_grammar.variables[other_index].name
str_pool.resolve(lexical_grammar.variables[token.index as usize].name),
str_pool.resolve(lexical_grammar.variables[other_index].name)
);
return false;
}
@ -418,33 +434,27 @@ fn identify_keywords(
debug!(
"Keywords - include {}",
lexical_grammar.variables[token.index].name,
str_pool.resolve(lexical_grammar.variables[token.index as usize].name),
);
true
})
.collect()
}
fn mark_fragile_tokens(
parse_table: &mut ParseTable,
lexical_grammar: &LexicalGrammar,
token_conflict_map: &TokenConflictMap,
) {
let n = lexical_grammar.variables.len();
let mut valid_tokens_mask = Vec::with_capacity(n);
fn mark_fragile_tokens(parse_table: &mut ParseTable, token_conflict_map: &TokenConflictMap) {
let mut valid_terminal_indices = Vec::new();
for state in &mut parse_table.states {
valid_tokens_mask.clear();
valid_tokens_mask.resize(n, false);
valid_terminal_indices.clear();
for token in state.terminal_entries.keys() {
if token.is_terminal() {
valid_tokens_mask[token.index] = true;
valid_terminal_indices.push(token.index);
}
}
for (token, entry) in &mut state.terminal_entries {
for (token, id) in &mut state.terminal_entries {
if token.is_terminal() {
for (i, is_valid) in valid_tokens_mask.iter().enumerate() {
if *is_valid && token_conflict_map.does_overlap(i, token.index) {
entry.reusable = false;
for &i in &valid_terminal_indices {
if token_conflict_map.does_overlap(i as usize, token.index as usize) {
id.set_reusable(false);
break;
}
}
@ -457,7 +467,8 @@ fn report_state_info<'a>(
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
parse_table: &ParseTable,
parse_state_info: &[ParseStateInfo<'a>],
parse_state_info: &ParseStateInfo<'a>,
str_pool: &StrPool,
report_symbol_name: &'a str,
) {
let mut all_state_indices = BTreeSet::new();
@ -467,8 +478,8 @@ fn report_state_info<'a>(
for (i, state) in parse_table.states.iter().enumerate() {
all_state_indices.insert(i);
let item_set = &parse_state_info[state.id];
for entry in &item_set.1.entries {
let item_set = parse_state_info.item_set(state.id);
for entry in &item_set.entries {
if !entry.item.is_augmented() {
symbols_with_state_indices[entry.item.variable_index as usize]
.1
@ -482,13 +493,13 @@ fn report_state_info<'a>(
let max_symbol_name_length = syntax_grammar
.variables
.iter()
.map(|v| v.name.len())
.map(|v| str_pool.resolve(v.name).len())
.max()
.unwrap();
for (symbol, states) in &symbols_with_state_indices {
info!(
"{:width$}\t{}",
syntax_grammar.variables[symbol.index].name,
str_pool.resolve(syntax_grammar.variables[symbol.index as usize].name),
states.len(),
width = max_symbol_name_length
);
@ -501,7 +512,9 @@ fn report_state_info<'a>(
symbols_with_state_indices
.iter()
.find_map(|(symbol, state_indices)| {
if syntax_grammar.variables[symbol.index].name == report_symbol_name {
if str_pool.resolve(syntax_grammar.variables[symbol.index as usize].name)
== report_symbol_name
{
Some(state_indices)
} else {
None
@ -515,7 +528,8 @@ fn report_state_info<'a>(
for state_index in state_indices {
let id = parse_table.states[state_index].id;
let (preceding_symbols, item_set) = &parse_state_info[id];
let preceding_symbols = &parse_state_info.preceding_symbols_by_id[id as usize];
let item_set = parse_state_info.item_set(id);
info!("state index: {state_index}");
info!("state id: {id}");
info!(
@ -524,11 +538,12 @@ fn report_state_info<'a>(
.iter()
.map(|symbol| {
if symbol.is_terminal() {
lexical_grammar.variables[symbol.index].name.clone()
str_pool.resolve(lexical_grammar.variables[symbol.index as usize].name)
} else if symbol.is_external() {
syntax_grammar.external_tokens[symbol.index].name.clone()
str_pool
.resolve(syntax_grammar.external_tokens[symbol.index as usize].name)
} else {
syntax_grammar.variables[symbol.index].name.clone()
str_pool.resolve(syntax_grammar.variables[symbol.index as usize].name)
}
})
.collect::<Vec<_>>()
@ -536,7 +551,13 @@ fn report_state_info<'a>(
);
info!(
"\nitems:\n{}",
item::ParseItemSetDisplay(item_set, syntax_grammar, lexical_grammar),
item::ParseItemSetDisplay(
item_set,
syntax_grammar,
lexical_grammar,
str_pool,
&parse_state_info.lookaheads
),
);
}
}

View file

@ -1,8 +1,10 @@
use std::{
collections::{HashMap, VecDeque, hash_map::Entry},
collections::{VecDeque, hash_map::Entry},
mem,
};
use rustc_hash::FxHashMap;
use log::debug;
use super::{coincident_tokens::CoincidentTokenIndex, token_conflicts::TokenConflictMap};
@ -11,7 +13,7 @@ use crate::{
grammars::{LexicalGrammar, SyntaxGrammar},
nfa::{CharacterSet, NfaCursor},
rules::{Symbol, TokenSet},
tables::{AdvanceAction, LexState, LexTable, ParseStateId, ParseTable},
tables::{AdvanceAction, LexState, LexStateId, LexTable, ParseStateId, ParseTable},
};
pub const LARGE_CHARACTER_RANGE_COUNT: usize = 8;
@ -47,7 +49,7 @@ pub fn build_lex_table(
.chain(state.reserved_words.iter())
.filter_map(|token| {
if token.is_terminal() {
if keywords.contains(&token) {
if keywords.contains(token) {
syntax_grammar.word_token
} else {
Some(token)
@ -69,13 +71,13 @@ pub fn build_lex_table(
coincident_token_index,
) {
did_merge = true;
entry.1.push(i);
entry.1.push(i as u32);
break;
}
}
if !did_merge {
parse_state_ids_by_token_set.push((tokens, vec![i]));
parse_state_ids_by_token_set.push((tokens, vec![i as u32]));
}
}
@ -83,7 +85,7 @@ pub fn build_lex_table(
for (tokens, parse_state_ids) in parse_state_ids_by_token_set {
let lex_state_id = builder.add_state_for_tokens(&tokens);
for id in parse_state_ids {
parse_table.states[id].lex_state_id = lex_state_id;
parse_table.states[id as usize].lex_state_id = lex_state_id;
}
}
@ -129,7 +131,7 @@ pub fn build_lex_table(
}
struct QueueEntry {
state_id: usize,
state_id: LexStateId,
nfa_states: Vec<u32>,
eof_valid: bool,
}
@ -139,7 +141,7 @@ struct LexTableBuilder<'a> {
cursor: NfaCursor<'a>,
table: LexTable,
state_queue: VecDeque<QueueEntry>,
state_ids_by_nfa_state_set: HashMap<(Vec<u32>, bool), usize>,
state_ids_by_nfa_state_set: FxHashMap<(Vec<u32>, bool), LexStateId>,
}
impl<'a> LexTableBuilder<'a> {
@ -149,7 +151,7 @@ impl<'a> LexTableBuilder<'a> {
cursor: NfaCursor::new(&lexical_grammar.nfa, vec![]),
table: LexTable::default(),
state_queue: VecDeque::new(),
state_ids_by_nfa_state_set: HashMap::new(),
state_ids_by_nfa_state_set: FxHashMap::default(),
}
}
@ -159,13 +161,13 @@ impl<'a> LexTableBuilder<'a> {
self.state_ids_by_nfa_state_set.clear();
}
fn add_state_for_tokens(&mut self, tokens: &TokenSet) -> usize {
fn add_state_for_tokens(&mut self, tokens: &TokenSet) -> LexStateId {
let mut eof_valid = false;
let nfa_states = tokens
.iter()
.filter_map(|token| {
if token.is_terminal() {
Some(self.lexical_grammar.variables[token.index].start_state)
Some(self.lexical_grammar.variables[token.index as usize].start_state)
} else {
eof_valid = true;
None
@ -179,7 +181,7 @@ impl<'a> LexTableBuilder<'a> {
"entry point state: {state_id}, tokens: {:?}",
tokens
.iter()
.map(|t| &self.lexical_grammar.variables[t.index].name)
.map(|t| &self.lexical_grammar.variables[t.index as usize].name)
.collect::<Vec<_>>()
);
}
@ -195,7 +197,7 @@ impl<'a> LexTableBuilder<'a> {
state_id
}
fn add_state(&mut self, nfa_states: Vec<u32>, eof_valid: bool) -> (usize, bool) {
fn add_state(&mut self, nfa_states: Vec<u32>, eof_valid: bool) -> (LexStateId, bool) {
self.cursor.reset(nfa_states);
match self
.state_ids_by_nfa_state_set
@ -203,7 +205,7 @@ impl<'a> LexTableBuilder<'a> {
{
Entry::Occupied(o) => (*o.get(), false),
Entry::Vacant(v) => {
let state_id = self.table.states.len();
let state_id = self.table.states.len() as u32;
self.table.states.push(LexState::default());
self.state_queue.push_back(QueueEntry {
state_id,
@ -216,7 +218,7 @@ impl<'a> LexTableBuilder<'a> {
}
}
fn populate_state(&mut self, state_id: usize, nfa_states: Vec<u32>, eof_valid: bool) {
fn populate_state(&mut self, state_id: LexStateId, nfa_states: Vec<u32>, eof_valid: bool) {
self.cursor.force_reset(nfa_states);
// The EOF state is represented as an empty list of NFA states.
@ -234,14 +236,13 @@ impl<'a> LexTableBuilder<'a> {
completion = Some((id, prec));
}
let transitions = self.cursor.transitions();
let has_sep = self.cursor.transition_chars().any(|(_, sep)| sep);
let (transitions, has_sep) = self.cursor.transitions_and_any_sep();
// If EOF is a valid lookahead token, add a transition predicated on the null
// character that leads to the empty set of NFA states.
if eof_valid {
let (next_state_id, _) = self.add_state(Vec::new(), false);
self.table.states[state_id].eof_action = Some(AdvanceAction {
self.table.states[state_id as usize].eof_action = Some(AdvanceAction {
state: next_state_id,
in_main_token: true,
});
@ -262,7 +263,7 @@ impl<'a> LexTableBuilder<'a> {
let (next_state_id, _) =
self.add_state(transition.states, eof_valid && transition.is_separator);
self.table.states[state_id].advance_actions.push((
self.table.states[state_id as usize].advance_actions.push((
transition.characters,
AdvanceAction {
state: next_state_id,
@ -272,9 +273,10 @@ impl<'a> LexTableBuilder<'a> {
}
if let Some((complete_id, _)) = completion {
self.table.states[state_id].accept_action = Some(Symbol::terminal(complete_id));
self.table.states[state_id as usize].accept_action =
Some(Symbol::terminal(complete_id));
} else if self.cursor.state_ids.is_empty() {
self.table.states[state_id].accept_action = Some(Symbol::end());
self.table.states[state_id as usize].accept_action = Some(Symbol::end());
}
}
}
@ -285,20 +287,27 @@ fn check_token_conflicts(
token_conflict_map: &TokenConflictMap,
coincident_token_index: &CoincidentTokenIndex,
) -> bool {
let symbol = Symbol::terminal(i);
for existing_token in set_without_terminal.terminals() {
if token_conflict_map.does_conflict(i, existing_token.index)
|| token_conflict_map.does_match_prefix(i, existing_token.index)
{
return true;
}
if !coincident_token_index.contains(symbol, existing_token)
&& (token_conflict_map.does_overlap(existing_token.index, i)
|| token_conflict_map.does_overlap(i, existing_token.index))
{
let wpr = token_conflict_map.row_words;
let row_start = i * wpr;
let set_bits = set_without_terminal.terminal_bits_words();
// Does terminal i conflict with or match-prefix any terminal in the set?
let conflict_row = &token_conflict_map.conflict_or_prefix_bits[row_start..row_start + wpr];
for (&c, &s) in conflict_row.iter().zip(set_bits) {
if c & s != 0 {
return true;
}
}
// Does terminal i overlap (in either direction) with any non-coincident terminal in the set?
let overlap_row = &token_conflict_map.overlap_either_bits[row_start..row_start + wpr];
let coincident_row = &coincident_token_index.row_bits[row_start..row_start + wpr];
for ((&o, &s), &c) in overlap_row.iter().zip(set_bits).zip(coincident_row) {
if o & s & !c != 0 {
return true;
}
}
false
}
@ -310,10 +319,10 @@ fn merge_token_set(
) -> bool {
if tokens
.terminals()
.filter(|terminal| !other.contains_terminal(terminal.index))
.filter(|terminal| !other.contains_terminal(terminal.index as usize))
.any(|terminal| {
check_token_conflicts(
terminal.index,
terminal.index as usize,
other,
token_conflict_map,
coincident_token_index,
@ -325,10 +334,10 @@ fn merge_token_set(
if other
.terminals()
.filter(|terminal| !tokens.contains_terminal(terminal.index))
.filter(|terminal| !tokens.contains_terminal(terminal.index as usize))
.any(|terminal| {
check_token_conflicts(
terminal.index,
terminal.index as usize,
tokens,
token_conflict_map,
coincident_token_index,
@ -345,7 +354,7 @@ fn merge_token_set(
fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
// Initially group the states by their accept action and their
// valid lookahead characters.
let mut state_ids_by_signature = HashMap::new();
let mut state_ids_by_signature = FxHashMap::default();
for (i, state) in table.states.iter().enumerate() {
let signature = (
i == 0,
@ -360,7 +369,7 @@ fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
state_ids_by_signature
.entry(signature)
.or_insert(Vec::new())
.push(i);
.push(i as u32);
}
let mut state_ids_by_group_id = state_ids_by_signature
.into_iter()
@ -373,10 +382,10 @@ fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
.unwrap();
state_ids_by_group_id.swap(error_group_index, 0);
let mut group_ids_by_state_id = vec![0; table.states.len()];
let mut group_ids_by_state_id = vec![0u32; table.states.len()];
for (group_id, state_ids) in state_ids_by_group_id.iter().enumerate() {
for state_id in state_ids {
group_ids_by_state_id[*state_id] = group_id;
group_ids_by_state_id[*state_id as usize] = group_id as u32;
}
}
@ -391,30 +400,35 @@ fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
let mut new_states = Vec::with_capacity(state_ids_by_group_id.len());
for state_ids in &state_ids_by_group_id {
let mut new_state = LexState::default();
mem::swap(&mut new_state, &mut table.states[state_ids[0]]);
mem::swap(&mut new_state, &mut table.states[state_ids[0] as usize]);
for (_, advance_action) in &mut new_state.advance_actions {
advance_action.state = group_ids_by_state_id[advance_action.state];
advance_action.state = group_ids_by_state_id[advance_action.state as usize];
}
if let Some(eof_action) = &mut new_state.eof_action {
eof_action.state = group_ids_by_state_id[eof_action.state];
eof_action.state = group_ids_by_state_id[eof_action.state as usize];
}
new_states.push(new_state);
}
for state in &mut parse_table.states {
state.lex_state_id = group_ids_by_state_id[state.lex_state_id];
state.lex_state_id = group_ids_by_state_id[state.lex_state_id as usize];
}
table.states = new_states;
}
fn lex_states_differ(left: &LexState, right: &LexState, group_ids_by_state_id: &[usize]) -> bool {
fn lex_states_differ(
left: &LexState,
right: &LexState,
group_ids_by_state_id: &[LexStateId],
) -> bool {
left.advance_actions
.iter()
.zip(right.advance_actions.iter())
.any(|(left, right)| {
group_ids_by_state_id[left.1.state] != group_ids_by_state_id[right.1.state]
group_ids_by_state_id[left.1.state as usize]
!= group_ids_by_state_id[right.1.state as usize]
})
}
@ -424,9 +438,9 @@ fn sort_states(table: &mut LexTable, parse_table: &mut ParseTable) {
old_ids_by_new_id[1..].sort_by_key(|id| &table.states[*id]);
// Get the inverse mapping
let mut new_ids_by_old_id = vec![0; old_ids_by_new_id.len()];
let mut new_ids_by_old_id = vec![0u32; old_ids_by_new_id.len()];
for (id, old_id) in old_ids_by_new_id.iter().enumerate() {
new_ids_by_old_id[*old_id] = id;
new_ids_by_old_id[*old_id] = id as u32;
}
// Reorder the parse states and update their references to reflect
@ -437,10 +451,10 @@ fn sort_states(table: &mut LexTable, parse_table: &mut ParseTable) {
let mut state = LexState::default();
mem::swap(&mut state, &mut table.states[*old_id]);
for (_, advance_action) in &mut state.advance_actions {
advance_action.state = new_ids_by_old_id[advance_action.state];
advance_action.state = new_ids_by_old_id[advance_action.state as usize];
}
if let Some(eof_action) = &mut state.eof_action {
eof_action.state = new_ids_by_old_id[eof_action.state];
eof_action.state = new_ids_by_old_id[eof_action.state as usize];
}
state
})
@ -448,6 +462,6 @@ fn sort_states(table: &mut LexTable, parse_table: &mut ParseTable) {
// Update the parse table's lex state references
for state in &mut parse_table.states {
state.lex_state_id = new_ids_by_old_id[state.lex_state_id];
state.lex_state_id = new_ids_by_old_id[state.lex_state_id as usize];
}
}

View file

@ -1,13 +1,12 @@
use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
collections::{BTreeMap, BTreeSet, VecDeque},
hash::BuildHasherDefault,
};
use indexmap::{IndexMap, map::Entry};
use log::warn;
use rustc_hash::FxHasher;
use serde::Serialize;
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::{
@ -15,12 +14,15 @@ use super::{
item_set_builder::ParseItemSetBuilder,
};
use crate::{
Diagnostic,
build_tables::item::{LookaheadSetPool, START_PRODUCTION_ID, prec_display},
grammars::{LexicalGrammar, PrecedenceEntry, ReservedWordSetId, SyntaxGrammar, VariableType},
node_types::VariableInfo,
rules::{Associativity, Precedence, Symbol, SymbolType, TokenSet},
strpool::StrPool,
tables::{
FieldLocation, GotoAction, ParseAction, ParseState, ParseStateId, ParseTable,
ParseTableEntry, ProductionInfo, ProductionInfoId,
ActionList, ActionListPool, FieldLocation, GotoAction, ParseAction, ParseState,
ParseStateId, ParseTable, ParseTableEntry, ProductionInfo, ProductionInfoId,
},
};
@ -29,7 +31,19 @@ use crate::{
type SymbolSequence = Vec<Symbol>;
type AuxiliarySymbolSequence = Vec<AuxiliarySymbolInfo>;
pub type ParseStateInfo<'a> = (SymbolSequence, ParseItemSet<'a>);
pub struct ParseStateInfo<'a> {
pub preceding_symbols_by_id: Vec<SymbolSequence>,
item_sets_by_ids: IndexMap<ParseItemSet<'a>, ParseStateId, BuildHasherDefault<FxHasher>>,
pub lookaheads: LookaheadSetPool,
}
impl<'a> ParseStateInfo<'a> {
#[must_use]
pub fn item_set(&self, id: ParseStateId) -> &ParseItemSet<'a> {
self.item_sets_by_ids.get_index(id as usize).unwrap().0
}
}
#[derive(Clone, PartialEq)]
struct AuxiliarySymbolInfo {
@ -56,18 +70,20 @@ struct ParseTableBuilder<'a> {
syntax_grammar: &'a SyntaxGrammar,
lexical_grammar: &'a LexicalGrammar,
variable_info: &'a [VariableInfo],
core_ids_by_core: HashMap<ParseItemSetCore<'a>, usize>,
core_ids_by_core: FxHashMap<ParseItemSetCore<'a>, u32>,
state_ids_by_item_set: IndexMap<ParseItemSet<'a>, ParseStateId, BuildHasherDefault<FxHasher>>,
parse_state_info_by_id: Vec<ParseStateInfo<'a>>,
preceding_symbols_by_id: Vec<SymbolSequence>,
production_info_ids_by_prod_id: Vec<Option<ProductionInfoId>>,
parse_state_queue: VecDeque<ParseStateQueueEntry>,
non_terminal_extra_states: Vec<(Symbol, usize)>,
actual_conflicts: HashSet<Vec<Symbol>>,
parse_table: ParseTable,
non_terminal_extra_states: Vec<(Symbol, ParseStateId)>,
actual_conflicts: FxHashSet<Vec<Symbol>>,
parse_table: ParseTable<ParseTableEntry>,
str_pool: &'a StrPool,
}
pub type BuildTableResult<T> = Result<T, ParseTableBuilderError>;
#[derive(Debug, Error, Serialize)]
#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq)]
pub enum ParseTableBuilderError {
#[error("Unresolved conflict for symbol sequence:\n\n{0}")]
Conflict(#[from] ConflictError),
@ -81,7 +97,7 @@ pub enum ParseTableBuilderError {
StateCount(usize),
}
#[derive(Default, Debug, Serialize, Error)]
#[derive(Default, Debug, Serialize, Error, Deserialize, PartialEq, Eq)]
pub struct ConflictError {
pub symbol_sequence: Vec<String>,
pub conflicting_lookahead: String,
@ -89,7 +105,7 @@ pub struct ConflictError {
pub possible_resolutions: Vec<Resolution>,
}
#[derive(Default, Debug, Serialize, Error)]
#[derive(Default, Debug, Serialize, Error, Deserialize, PartialEq, Eq)]
pub struct Interpretation {
pub preceding_symbols: Vec<String>,
pub variable_name: String,
@ -99,16 +115,17 @@ pub struct Interpretation {
pub conflicting_lookahead: String,
pub precedence: Option<String>,
pub associativity: Option<String>,
pub requires_eof_lookahead: bool,
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Resolution {
Precedence { symbols: Vec<String> },
Associativity { symbols: Vec<String> },
AddConflict { symbols: Vec<String> },
}
#[derive(Debug, Serialize, Error)]
#[derive(Debug, Serialize, Deserialize, Error, PartialEq, Eq)]
pub struct AmbiguousExtraError {
pub parent_symbols: Vec<String>,
}
@ -126,16 +143,21 @@ impl std::fmt::Display for ConflictError {
.iter()
.map(|i| {
let line = i.to_string();
let prec_line = if let (Some(precedence), Some(associativity)) =
(&i.precedence, &i.associativity)
{
Some(format!(
let mut annotations = Vec::new();
if let (Some(precedence), Some(associativity)) = (&i.precedence, &i.associativity) {
annotations.push(format!(
"(precedence: {precedence}, associativity: {associativity})",
))
));
} else if let Some(precedence) = &i.precedence {
annotations.push(format!("(precedence: {precedence})"));
}
if i.requires_eof_lookahead {
annotations.push("(reduces only at end of input)".to_string());
}
let prec_line = if annotations.is_empty() {
None
} else {
i.precedence
.as_ref()
.map(|precedence| format!("(precedence: {precedence})"))
Some(annotations.join(" "))
};
(line, prec_line)
@ -244,6 +266,7 @@ impl<'a> ParseTableBuilder<'a> {
lexical_grammar: &'a LexicalGrammar,
item_set_builder: ParseItemSetBuilder<'a>,
variable_info: &'a [VariableInfo],
str_pool: &'a StrPool,
) -> Self {
Self {
syntax_grammar,
@ -252,21 +275,27 @@ impl<'a> ParseTableBuilder<'a> {
variable_info,
non_terminal_extra_states: Vec::new(),
state_ids_by_item_set: IndexMap::default(),
core_ids_by_core: HashMap::new(),
parse_state_info_by_id: Vec::new(),
core_ids_by_core: FxHashMap::default(),
preceding_symbols_by_id: Vec::new(),
production_info_ids_by_prod_id: vec![None; syntax_grammar.productions.len()],
parse_state_queue: VecDeque::new(),
actual_conflicts: syntax_grammar.expected_conflicts.iter().cloned().collect(),
parse_table: ParseTable {
states: Vec::new(),
action_lists: ActionListPool::default(),
symbols: Vec::new(),
external_lex_states: Vec::new(),
production_infos: Vec::new(),
max_aliased_production_length: 1,
},
str_pool,
}
}
fn build(mut self) -> BuildTableResult<(ParseTable, Vec<ParseStateInfo<'a>>)> {
fn build(
mut self,
diagnostics: &mut Vec<Diagnostic>,
) -> BuildTableResult<(ParseTable<ParseTableEntry>, ParseStateInfo<'a>)> {
// Ensure that the empty alias sequence has index 0.
self.parse_table
.production_infos
@ -276,13 +305,14 @@ impl<'a> ParseTableBuilder<'a> {
self.add_parse_state(&Vec::new(), &Vec::new(), ParseItemSet::default());
// Add the starting state at index 1.
let end_lookaheads = self.item_set_builder.lookaheads.singleton(Symbol::end());
self.add_parse_state(
&Vec::new(),
&Vec::new(),
ParseItemSet {
entries: vec![ParseItemSetEntry {
item: ParseItem::start(),
lookaheads: std::iter::once(Symbol::end()).collect(),
item: ParseItem::start(self.item_set_builder.key_map),
lookaheads: end_lookaheads,
following_reserved_word_set: ReservedWordSetId::default(),
}],
},
@ -296,33 +326,39 @@ impl<'a> ParseTableBuilder<'a> {
.iter()
.filter(|s| s.is_non_terminal())
{
let variable = &self.syntax_grammar.variables[extra_non_terminal.index];
for production in &variable.productions {
non_terminal_extra_item_sets_by_first_terminal
for prod_id in self
.syntax_grammar
.variable_prod_ids(extra_non_terminal.index as usize)
{
let production = self.syntax_grammar.production(prod_id);
let entry = non_terminal_extra_item_sets_by_first_terminal
.entry(production.first_symbol().unwrap())
.or_insert_with(ParseItemSet::default)
.insert(ParseItem {
variable_index: extra_non_terminal.index as u32,
production,
variable_index: extra_non_terminal.index,
prod_id,
step_index: 1,
keys: self.item_set_builder.key_map.keys_for(prod_id),
has_preceding_inherited_fields: false,
})
});
entry.lookaheads = self
.item_set_builder
.lookaheads
.insert(Symbol::end_of_nonterminal_extra());
.insert(entry.lookaheads, Symbol::end_of_nonterminal_extra());
}
}
let non_terminal_sets_len = non_terminal_extra_item_sets_by_first_terminal.len();
self.non_terminal_extra_states
.reserve(non_terminal_sets_len);
self.parse_state_info_by_id.reserve(non_terminal_sets_len);
self.preceding_symbols_by_id.reserve(non_terminal_sets_len);
self.parse_table.states.reserve(non_terminal_sets_len);
self.parse_state_queue.reserve(non_terminal_sets_len);
// Add a state for each starting terminal of a non-terminal extra rule.
for (terminal, item_set) in non_terminal_extra_item_sets_by_first_terminal {
if terminal.is_non_terminal() {
Err(ParseTableBuilderError::ImproperNonTerminalExtra(
self.symbol_name(&terminal),
self.symbol_name(terminal),
))?;
}
@ -333,12 +369,18 @@ impl<'a> ParseTableBuilder<'a> {
}
while let Some(entry) = self.parse_state_queue.pop_front() {
let item_set = self
.item_set_builder
.transitive_closure(&self.parse_state_info_by_id[entry.state_id].1);
// The dedup-map key is each state's kernel (the GOTO result, pre closure).
// Two states are identical iff their kernels match.
let kernel = self
.state_ids_by_item_set
.get_index(entry.state_id as usize)
// Invariant: `state_id` is the map's insertion index
.unwrap()
.0;
let item_set = self.item_set_builder.transitive_closure(kernel);
self.add_actions(
self.parse_state_info_by_id[entry.state_id].0.clone(),
self.preceding_symbols_by_id[entry.state_id as usize].clone(),
entry.preceding_auxiliary_symbols,
entry.state_id,
&item_set,
@ -346,24 +388,23 @@ impl<'a> ParseTableBuilder<'a> {
}
if !self.actual_conflicts.is_empty() {
warn!(
"unnecessary conflicts:\n {}",
&self
.actual_conflicts
.iter()
.map(|conflict| {
conflict
.iter()
.map(|symbol| format!("`{}`", self.symbol_name(symbol)))
.collect::<Vec<_>>()
.join(", ")
})
.collect::<Vec<_>>()
.join("\n ")
);
let mut conflicts = self
.actual_conflicts
.iter()
.map(|conf| conf.iter().map(|&s| self.symbol_name(s)).collect())
.collect::<Vec<_>>();
conflicts.sort_unstable();
diagnostics.push(Diagnostic::UnnecessaryConflicts(conflicts));
}
Ok((self.parse_table, self.parse_state_info_by_id))
Ok((
self.parse_table,
ParseStateInfo {
preceding_symbols_by_id: self.preceding_symbols_by_id,
item_sets_by_ids: self.state_ids_by_item_set,
lookaheads: self.item_set_builder.lookaheads,
},
))
}
fn add_parse_state(
@ -381,12 +422,11 @@ impl<'a> ParseTableBuilder<'a> {
// parse states to populate.
Entry::Vacant(v) => {
let core = v.key().core();
let core_count = self.core_ids_by_core.len();
let core_count = self.core_ids_by_core.len() as u32;
let core_id = *self.core_ids_by_core.entry(core).or_insert(core_count);
let state_id = self.parse_table.states.len();
self.parse_state_info_by_id
.push((preceding_symbols.clone(), v.key().clone()));
let state_id = self.parse_table.states.len() as u32;
self.preceding_symbols_by_id.push(preceding_symbols.clone());
self.parse_table.states.push(ParseState {
id: state_id,
@ -396,6 +436,7 @@ impl<'a> ParseTableBuilder<'a> {
nonterminal_entries: IndexMap::default(),
reserved_words: TokenSet::default(),
core_id,
has_eof_gated_reduce: false,
});
self.parse_state_queue.push_back(ParseStateQueueEntry {
state_id,
@ -417,7 +458,11 @@ impl<'a> ParseTableBuilder<'a> {
let mut terminal_successors = BTreeMap::new();
let mut non_terminal_successors = BTreeMap::new();
let mut lookaheads_with_conflicts = TokenSet::new();
let mut reduction_infos = HashMap::<Symbol, ReductionInfo>::new();
let mut reduction_infos = FxHashMap::<Symbol, ReductionInfo>::default();
// `get_auxiliary_node_info` scans every entry in `item_set`, and the same auxiliary
// symbol typically appears across many entries in a state. Memoize per symbol.
let mut aux_node_info = FxHashMap::<Symbol, AuxiliarySymbolInfo>::default();
// Each item in the item set contributes to either or a Shift action or a Reduce
// action in this state.
@ -430,17 +475,23 @@ impl<'a> ParseTableBuilder<'a> {
// If the item is unfinished, then this state has a transition for the item's
// next symbol. Advance the item to its next step and insert the resulting
// item into the successor item set.
if let Some(next_symbol) = item.symbol() {
if let Some(next_symbol) = item.symbol(self.syntax_grammar) {
let mut successor = item.successor();
let successor_set = if next_symbol.is_non_terminal() {
let variable = &self.syntax_grammar.variables[next_symbol.index];
let variable = &self.syntax_grammar.variables[next_symbol.index as usize];
// Keep track of where auxiliary non-terminals (repeat symbols) are
// used within visible symbols. This information may be needed later
// for conflict resolution.
if variable.is_auxiliary() {
preceding_auxiliary_symbols
.push(self.get_auxiliary_node_info(item_set, next_symbol));
preceding_auxiliary_symbols.push(
aux_node_info
.entry(next_symbol)
.or_insert_with(|| {
self.get_auxiliary_node_info(item_set, next_symbol)
})
.clone(),
);
}
// For most parse items, the symbols associated with the preceding children
@ -453,7 +504,9 @@ impl<'a> ParseTableBuilder<'a> {
// of its preceding children need to be taken into account when comparing
// it with other items.
if variable.is_hidden()
&& !self.variable_info[next_symbol.index].fields.is_empty()
&& !self.variable_info[next_symbol.index as usize]
.fields
.is_empty()
{
successor.has_preceding_inherited_fields = true;
}
@ -467,7 +520,10 @@ impl<'a> ParseTableBuilder<'a> {
.or_insert_with(ParseItemSet::default)
};
let successor_entry = successor_set.insert(successor);
successor_entry.lookaheads.insert_all(lookaheads);
successor_entry.lookaheads = self
.item_set_builder
.lookaheads
.union(successor_entry.lookaheads, *lookaheads);
successor_entry.following_reserved_word_set = successor_entry
.following_reserved_word_set
.max(*reserved_lookaheads);
@ -479,18 +535,37 @@ impl<'a> ParseTableBuilder<'a> {
let action = if item.is_augmented() {
ParseAction::Accept
} else {
// These values are narrowed to u16 to reduce the size of
// ParseAction. No real-world grammar approaches these limits.
debug_assert!(
u16::try_from(item.step_index).is_ok(),
"production step count exceeds u16::MAX"
);
let production_id = self.get_production_id(item);
debug_assert!(
u16::try_from(production_id).is_ok(),
"production info id exceeds u16::MAX"
);
ParseAction::Reduce {
symbol,
child_count: item.step_index as usize,
dynamic_precedence: item.production.dynamic_precedence,
production_id: self.get_production_id(item),
child_count: item.step_index as u16,
dynamic_precedence: item.production(self.syntax_grammar).dynamic_precedence,
production_id: production_id as u16,
}
};
let precedence = item.precedence();
let associativity = item.associativity();
for lookahead in lookaheads.iter() {
let table_entry = self.parse_table.states[state_id]
let precedence = item.precedence(self.syntax_grammar);
let associativity = item.associativity(self.syntax_grammar);
if item.production(self.syntax_grammar).requires_eof_lookahead {
self.parse_table.states[state_id as usize].has_eof_gated_reduce = true;
}
for lookahead in self.item_set_builder.lookaheads.get(*lookaheads).iter() {
if item.production(self.syntax_grammar).requires_eof_lookahead
&& lookahead != Symbol::end()
{
continue;
}
let table_entry = self.parse_table.states[state_id as usize]
.terminal_entries
.entry(lookahead)
.or_insert_with(ParseTableEntry::new);
@ -506,24 +581,30 @@ impl<'a> ParseTableBuilder<'a> {
self.syntax_grammar,
precedence,
&[symbol],
&reduction_info.precedence,
reduction_info.precedence,
&reduction_info.symbols,
) {
Ordering::Greater => {
table_entry.actions.clear();
table_entry.actions.push(action);
lookaheads_with_conflicts.remove(&lookahead);
lookaheads_with_conflicts.remove(lookahead);
*reduction_info = ReductionInfo::default();
}
// Two items that reduce identically build the same tree, so
// there is nothing for the user to resolve. Precedence is
// still compared first, because an item that only repeats an
// existing action can still outrank it and clear the entry.
Ordering::Equal => {
table_entry.actions.push(action);
lookaheads_with_conflicts.insert(lookahead);
if !table_entry.actions.contains(&action) {
table_entry.actions.push(action);
lookaheads_with_conflicts.insert(lookahead);
}
}
Ordering::Less => continue,
}
}
reduction_info.precedence.clone_from(precedence);
reduction_info.precedence = precedence;
if let Err(i) = reduction_info.symbols.binary_search(&symbol) {
reduction_info.symbols.insert(i, symbol);
}
@ -550,7 +631,7 @@ impl<'a> ParseTableBuilder<'a> {
);
preceding_symbols.pop();
let entry = self.parse_table.states[state_id]
let entry = self.parse_table.states[state_id as usize]
.terminal_entries
.entry(symbol);
if let Entry::Occupied(e) = &entry
@ -576,7 +657,7 @@ impl<'a> ParseTableBuilder<'a> {
next_item_set,
);
preceding_symbols.pop();
self.parse_table.states[state_id]
self.parse_table.states[state_id as usize]
.nonterminal_entries
.insert(symbol, GotoAction::Goto(next_state_id));
}
@ -598,7 +679,7 @@ impl<'a> ParseTableBuilder<'a> {
}
// Add actions for the grammar's `extra` symbols.
let state = &mut self.parse_table.states[state_id];
let state = &mut self.parse_table.states[state_id as usize];
let is_end_of_non_terminal_extra = state.is_end_of_non_terminal_extra();
// If this state represents the end of a non-terminal extra rule, then make sure that
@ -616,13 +697,13 @@ impl<'a> ParseTableBuilder<'a> {
None
}
})
.collect::<HashSet<_>>();
.collect::<FxHashSet<_>>();
let parent_symbol_names = parent_symbols
.iter()
.map(|&variable_index| {
self.syntax_grammar.variables[variable_index as usize]
.name
.clone()
self.str_pool
.resolve(self.syntax_grammar.variables[variable_index as usize].name)
.to_string()
})
.collect::<Vec<_>>();
@ -639,10 +720,10 @@ impl<'a> ParseTableBuilder<'a> {
.entry(*terminal)
.or_insert(ParseTableEntry {
reusable: true,
actions: vec![ParseAction::Shift {
actions: ActionList::One(ParseAction::Shift {
state: *state_id,
is_repetition: false,
}],
}),
});
}
@ -660,7 +741,7 @@ impl<'a> ParseTableBuilder<'a> {
.entry(*extra_token)
.or_insert(ParseTableEntry {
reusable: true,
actions: vec![ParseAction::ShiftExtra],
actions: ActionList::One(ParseAction::ShiftExtra),
});
}
}
@ -671,13 +752,18 @@ impl<'a> ParseTableBuilder<'a> {
.entries
.iter()
.filter_map(|entry| {
if let Some(next_step) = entry.item.step() {
if next_step.symbol == keyword_capture_token {
Some(next_step.reserved_word_set_id)
if let Some(next_step) = entry.item.step(self.syntax_grammar) {
if next_step.symbol() == keyword_capture_token {
Some(ReservedWordSetId(u32::from(next_step.reserved)))
} else {
None
}
} else if entry.lookaheads.contains(&keyword_capture_token) {
} else if self
.item_set_builder
.lookaheads
.get(entry.lookaheads)
.contains(keyword_capture_token)
{
Some(entry.following_reserved_word_set)
} else {
None
@ -686,7 +772,7 @@ impl<'a> ParseTableBuilder<'a> {
.max();
if let Some(reserved_word_set_id) = reserved_word_set_id {
state.reserved_words =
self.syntax_grammar.reserved_word_sets[reserved_word_set_id.0].clone();
self.syntax_grammar.reserved_word_sets[reserved_word_set_id.0 as usize].clone();
}
}
@ -702,7 +788,7 @@ impl<'a> ParseTableBuilder<'a> {
conflicting_lookahead: Symbol,
reduction_info: &ReductionInfo,
) -> BuildTableResult<()> {
let entry = self.parse_table.states[state_id]
let entry = self.parse_table.states[state_id as usize]
.terminal_entries
.get_mut(&conflicting_lookahead)
.unwrap();
@ -714,32 +800,37 @@ impl<'a> ParseTableBuilder<'a> {
// REDUCE-REDUCE conflicts where all actions have the *same*
// precedence, and there can still be SHIFT/REDUCE conflicts.
let mut considered_associativity = false;
let mut shift_precedence = Vec::<(&Precedence, Symbol)>::new();
let mut shift_precedence = Vec::<(Precedence, Symbol)>::new();
let mut conflicting_items = BTreeSet::new();
for ParseItemSetEntry {
item, lookaheads, ..
} in &item_set.entries
{
if let Some(step) = item.step() {
if let Some(step) = item.step(self.syntax_grammar) {
if item.step_index > 0
&& self
.item_set_builder
.first_set(&step.symbol)
.contains(&conflicting_lookahead)
.first_set(step.symbol())
.contains(conflicting_lookahead)
{
if item.variable_index != u32::MAX {
conflicting_items.insert(item);
}
let p = (
item.precedence(),
item.precedence(self.syntax_grammar),
Symbol::non_terminal(item.variable_index as usize),
);
if let Err(i) = shift_precedence.binary_search(&p) {
shift_precedence.insert(i, p);
}
}
} else if lookaheads.contains(&conflicting_lookahead) && item.variable_index != u32::MAX
} else if self
.item_set_builder
.lookaheads
.get(*lookaheads)
.contains(conflicting_lookahead)
&& item.variable_index != u32::MAX
{
conflicting_items.insert(item);
}
@ -764,30 +855,51 @@ impl<'a> ParseTableBuilder<'a> {
// If the SHIFT action has higher precedence, remove all the REDUCE actions.
let mut shift_is_less = false;
let mut shift_is_equal = false;
let mut shift_is_more = false;
for p in shift_precedence {
match Self::compare_precedence(
self.syntax_grammar,
p.0,
&[p.1],
&reduction_info.precedence,
reduction_info.precedence,
&reduction_info.symbols,
) {
Ordering::Greater => shift_is_more = true,
Ordering::Less => shift_is_less = true,
Ordering::Equal => {}
Ordering::Equal => shift_is_equal = true,
}
}
if shift_is_more && !shift_is_less {
entry.actions.drain(0..entry.actions.len() - 1);
entry.actions.keep_last();
}
// If the REDUCE actions have higher precedence, remove the SHIFT action.
else if shift_is_less && !shift_is_more {
entry.actions.pop();
conflicting_items.retain(|item| item.is_done());
// Exception: if one SHIFT interpretation ties the REDUCE actions in
// precedence while another has lower precedence, and the REDUCE
// actions are purely right associative, honor that right
// associativity by shifting rather than reducing. The
// lower-precedence interpretation coexists with the tying one, so on
// its own it must not force a REDUCE that would flip the tie to left
// associative.
if shift_is_equal
&& matches!(
(
reduction_info.has_left_assoc,
reduction_info.has_non_assoc,
reduction_info.has_right_assoc,
),
(false, false, true)
)
{
entry.actions.keep_last();
} else {
entry.actions.pop();
conflicting_items.retain(|item| item.is_done());
}
}
// If the SHIFT and REDUCE actions have the same predence, consider
// If the SHIFT and REDUCE actions have the same precedence, consider
// the REDUCE actions' associativity.
else if !shift_is_less && !shift_is_more {
considered_associativity = true;
@ -804,7 +916,7 @@ impl<'a> ParseTableBuilder<'a> {
conflicting_items.retain(|item| item.is_done());
}
(false, false, true) => {
entry.actions.drain(0..entry.actions.len() - 1);
entry.actions.keep_last();
}
_ => {}
}
@ -812,7 +924,7 @@ impl<'a> ParseTableBuilder<'a> {
}
// If all of the actions but one have been eliminated, then there's no problem.
let entry = self.parse_table.states[state_id]
let entry = self.parse_table.states[state_id as usize]
.terminal_entries
.get_mut(&conflicting_lookahead)
.unwrap();
@ -824,7 +936,7 @@ impl<'a> ParseTableBuilder<'a> {
let mut actual_conflict = Vec::new();
for item in &conflicting_items {
let symbol = Symbol::non_terminal(item.variable_index as usize);
if self.syntax_grammar.variables[symbol.index].is_auxiliary() {
if self.syntax_grammar.variables[symbol.index as usize].is_auxiliary() {
actual_conflict.extend(
preceding_auxiliary_symbols
.iter()
@ -857,12 +969,12 @@ impl<'a> ParseTableBuilder<'a> {
}
let mut conflict_error = ConflictError::default();
for symbol in preceding_symbols {
for &symbol in preceding_symbols {
conflict_error
.symbol_sequence
.push(self.symbol_name(symbol));
}
conflict_error.conflicting_lookahead = self.symbol_name(&conflicting_lookahead);
conflict_error.conflicting_lookahead = self.symbol_name(conflicting_lookahead);
let interpretations = conflicting_items
.iter()
@ -870,26 +982,32 @@ impl<'a> ParseTableBuilder<'a> {
let preceding_symbols = preceding_symbols
.iter()
.take(preceding_symbols.len() - item.step_index as usize)
.map(|symbol| self.symbol_name(symbol))
.map(|&symbol| self.symbol_name(symbol))
.collect::<Vec<_>>();
let variable_name = self.syntax_grammar.variables[item.variable_index as usize]
.name
.clone();
let variable_name = self
.str_pool
.resolve(self.syntax_grammar.variables[item.variable_index as usize].name)
.to_string();
let production_step_symbols = item
.production
.production(self.syntax_grammar)
.steps
.iter()
.map(|step| self.symbol_name(&step.symbol))
.map(|step| self.symbol_name(step.symbol()))
.collect::<Vec<_>>();
let precedence = match item.precedence() {
let precedence = match item.precedence(self.syntax_grammar) {
Precedence::None => None,
_ => Some(item.precedence().to_string()),
_ => Some(prec_display(
item.precedence(self.syntax_grammar),
self.str_pool,
)),
};
let associativity = item.associativity().map(|assoc| format!("{assoc:?}"));
let associativity = item
.associativity(self.syntax_grammar)
.map(|assoc| format!("{assoc:?}"));
Interpretation {
preceding_symbols,
@ -897,9 +1015,12 @@ impl<'a> ParseTableBuilder<'a> {
production_step_symbols,
step_index: item.step_index,
done: item.is_done(),
conflicting_lookahead: self.symbol_name(&conflicting_lookahead),
conflicting_lookahead: self.symbol_name(conflicting_lookahead),
precedence,
associativity,
requires_eof_lookahead: item
.production(self.syntax_grammar)
.requires_eof_lookahead,
}
})
.collect::<Vec<_>>();
@ -925,7 +1046,7 @@ impl<'a> ParseTableBuilder<'a> {
continue;
}
last_rule_id = Some(item.variable_index);
result.push(self.symbol_name(&Symbol::non_terminal(item.variable_index as usize)));
result.push(self.symbol_name(Symbol::non_terminal(item.variable_index as usize)));
}
result
@ -940,7 +1061,7 @@ impl<'a> ParseTableBuilder<'a> {
}
for item in &reduce_items {
let name = self.symbol_name(&Symbol::non_terminal(item.variable_index as usize));
let name = self.symbol_name(Symbol::non_terminal(item.variable_index as usize));
conflict_error
.possible_resolutions
.push(Resolution::Precedence {
@ -961,7 +1082,7 @@ impl<'a> ParseTableBuilder<'a> {
.push(Resolution::AddConflict {
symbols: actual_conflict
.iter()
.map(|s| self.symbol_name(s))
.map(|&s| self.symbol_name(s))
.collect(),
});
@ -972,33 +1093,33 @@ impl<'a> ParseTableBuilder<'a> {
fn compare_precedence(
grammar: &SyntaxGrammar,
left: &Precedence,
left: Precedence,
left_symbols: &[Symbol],
right: &Precedence,
right: Precedence,
right_symbols: &[Symbol],
) -> Ordering {
let precedence_entry_matches =
|entry: &PrecedenceEntry, precedence: &Precedence, symbols: &[Symbol]| -> bool {
|entry: &PrecedenceEntry, precedence: Precedence, symbols: &[Symbol]| -> bool {
match entry {
PrecedenceEntry::Name(n) => {
if let Precedence::Name(p) = precedence {
n == p
*n == p
} else {
false
}
}
PrecedenceEntry::Symbol(n) => symbols
.iter()
.any(|s| &grammar.variables[s.index].name == n),
.any(|s| &grammar.variables[s.index as usize].name == n),
}
};
match (left, right) {
// Integer precedences can be compared to other integer precedences,
// and to the default precedence, which is zero.
(Precedence::Integer(l), Precedence::Integer(r)) if *l != 0 || *r != 0 => l.cmp(r),
(Precedence::Integer(l), Precedence::None) if *l != 0 => l.cmp(&0),
(Precedence::None, Precedence::Integer(r)) if *r != 0 => 0.cmp(r),
(Precedence::Integer(l), Precedence::Integer(r)) if l != 0 || r != 0 => l.cmp(&r),
(Precedence::Integer(l), Precedence::None) if l != 0 => l.cmp(&0),
(Precedence::None, Precedence::Integer(r)) if r != 0 => 0.cmp(&r),
// Named precedences can be compared to other named precedences.
_ => grammar
@ -1038,7 +1159,7 @@ impl<'a> ParseTableBuilder<'a> {
.iter()
.filter_map(|ParseItemSetEntry { item, .. }| {
let variable_index = item.variable_index as usize;
if item.symbol() == Some(symbol)
if item.symbol(self.syntax_grammar) == Some(symbol)
&& !self.syntax_grammar.variables[variable_index].is_auxiliary()
{
Some(Symbol::non_terminal(variable_index))
@ -1054,37 +1175,46 @@ impl<'a> ParseTableBuilder<'a> {
}
fn get_production_id(&mut self, item: &ParseItem) -> ProductionInfoId {
debug_assert_ne!(item.prod_id, START_PRODUCTION_ID);
if let Some(id) = self.production_info_ids_by_prod_id[item.prod_id as usize] {
return id;
}
let mut production_info = ProductionInfo {
alias_sequence: Vec::new(),
field_map: BTreeMap::new(),
};
for (i, step) in item.production.steps.iter().enumerate() {
production_info.alias_sequence.push(step.alias.clone());
if let Some(field_name) = &step.field_name {
for (i, step) in item
.production(self.syntax_grammar)
.steps
.iter()
.enumerate()
{
production_info.alias_sequence.push(step.alias());
if let Some(field_name) = step.field() {
production_info
.field_map
.entry(field_name.clone())
.entry(field_name)
.or_default()
.push(FieldLocation {
index: i,
index: i as u32,
inherited: false,
});
}
if step.symbol.kind == SymbolType::NonTerminal
&& !self.syntax_grammar.variables[step.symbol.index]
if step.symbol().kind == SymbolType::NonTerminal
&& !self.syntax_grammar.variables[step.symbol().index as usize]
.kind
.is_visible()
{
let info = &self.variable_info[step.symbol.index];
for field_name in info.fields.keys() {
let info = &self.variable_info[step.symbol().index as usize];
for &field_name in info.fields.keys() {
production_info
.field_map
.entry(field_name.clone())
.entry(field_name)
.or_default()
.push(FieldLocation {
index: i,
index: i as u32,
inherited: true,
});
}
@ -1095,11 +1225,14 @@ impl<'a> ParseTableBuilder<'a> {
production_info.alias_sequence.pop();
}
if item.production.steps.len() > self.parse_table.max_aliased_production_length {
self.parse_table.max_aliased_production_length = item.production.steps.len();
if item.production(self.syntax_grammar).steps.len()
> self.parse_table.max_aliased_production_length
{
self.parse_table.max_aliased_production_length =
item.production(self.syntax_grammar).steps.len();
}
if let Some(index) = self
let id = if let Some(index) = self
.parse_table
.production_infos
.iter()
@ -1109,22 +1242,28 @@ impl<'a> ParseTableBuilder<'a> {
} else {
self.parse_table.production_infos.push(production_info);
self.parse_table.production_infos.len() - 1
}
} as ProductionInfoId;
self.production_info_ids_by_prod_id[item.prod_id as usize] = Some(id);
id
}
fn symbol_name(&self, symbol: &Symbol) -> String {
fn symbol_name(&self, symbol: Symbol) -> String {
match symbol.kind {
SymbolType::End | SymbolType::EndOfNonTerminalExtra => "EOF".to_string(),
SymbolType::External => self.syntax_grammar.external_tokens[symbol.index]
.name
.clone(),
SymbolType::NonTerminal => self.syntax_grammar.variables[symbol.index].name.clone(),
SymbolType::External => self
.str_pool
.resolve(self.syntax_grammar.external_tokens[symbol.index as usize].name)
.to_string(),
SymbolType::NonTerminal => self
.str_pool
.resolve(self.syntax_grammar.variables[symbol.index as usize].name)
.to_string(),
SymbolType::Terminal => {
let variable = &self.lexical_grammar.variables[symbol.index];
let variable = &self.lexical_grammar.variables[symbol.index as usize];
if variable.kind == VariableType::Named {
variable.name.clone()
self.str_pool.resolve(variable.name).to_string()
} else {
format!("'{}'", variable.name)
format!("'{}'", self.str_pool.resolve(variable.name))
}
}
}
@ -1136,12 +1275,15 @@ pub fn build_parse_table<'a>(
lexical_grammar: &'a LexicalGrammar,
item_set_builder: ParseItemSetBuilder<'a>,
variable_info: &'a [VariableInfo],
) -> BuildTableResult<(ParseTable, Vec<ParseStateInfo<'a>>)> {
str_pool: &'a StrPool,
diagnostics: &mut Vec<Diagnostic>,
) -> BuildTableResult<(ParseTable<ParseTableEntry>, ParseStateInfo<'a>)> {
ParseTableBuilder::new(
syntax_grammar,
lexical_grammar,
item_set_builder,
variable_info,
str_pool,
)
.build()
.build(diagnostics)
}

View file

@ -1,29 +1,40 @@
use std::fmt;
use crate::{grammars::LexicalGrammar, rules::Symbol, strpool::StrPool, tables::ParseTable};
use crate::{
grammars::LexicalGrammar,
rules::Symbol,
tables::{ParseStateId, ParseTable},
};
pub struct CoincidentTokenIndex<'a> {
entries: Vec<Vec<ParseStateId>>,
grammar: &'a LexicalGrammar,
pub struct CoincidentTokenIndex {
/// Flat bitset for fast [`contains()`](Self::contains) checks. Indexed as `a * n + b`
/// (both `(a,b)` and `(b,a)` bits are set, so no min/max normalization needed).
contains_bits: Vec<u64>,
/// Flat biset for fast [`Self::all_coincident_states_have_word`] checks.
/// Bit `(a, b)` is set iff tokens `a` and `b` are coincident in some parse state
/// where the grammar's word token is _not_ a valid lookahead. Allows for fast
/// answers to "do all states containing this pair also contain the word token?".
without_word_bits: Vec<u64>,
/// Word-aligned per-row bitsets for vectorized intersection checks.
/// Row `a` spans `[a * row_words .. (a+1) * row_words]`.
/// Bit `b` is set iff tokens `a` and `b` are coincident in some parse state.
pub(crate) row_bits: Vec<u64>,
n: usize,
}
impl<'a> CoincidentTokenIndex<'a> {
pub fn new(table: &ParseTable, lexical_grammar: &'a LexicalGrammar) -> Self {
impl<'a> CoincidentTokenIndex {
#[must_use]
pub fn new<T>(
table: &ParseTable<T>,
lexical_grammar: &'a LexicalGrammar,
word_token: Option<Symbol>,
) -> Self {
let n = lexical_grammar.variables.len();
let row_words = n.div_ceil(64);
let mut result = Self {
n,
grammar: lexical_grammar,
entries: vec![Vec::new(); n * n],
contains_bits: vec![0u64; (n * n).div_ceil(64)],
without_word_bits: vec![0u64; (n * n).div_ceil(64)],
row_bits: vec![0u64; n * row_words],
};
// Pre-collect terminal indices up front rather than continuously recomputing within the
// loop below.
let mut terminal_indices = Vec::new();
for (i, state) in table.states.iter().enumerate() {
for state in &table.states {
terminal_indices.clear();
terminal_indices.extend(
state
@ -32,54 +43,64 @@ impl<'a> CoincidentTokenIndex<'a> {
.filter(|s| s.is_terminal())
.map(|s| s.index),
);
for (j, &a) in terminal_indices.iter().enumerate() {
for &b in &terminal_indices[j..] {
let index = result.index(a, b);
if result.entries[index].last().copied() != Some(i) {
result.entries[index].push(i);
let has_word = word_token.is_some_and(|w| state.terminal_entries.contains_key(&w));
for (i, &a) in terminal_indices.iter().enumerate() {
for &b in &terminal_indices[i..] {
let (a, b) = (a as usize, b as usize);
// Set both (a,b) and (b,a) bits so `contains()` needs
// no min/max normalization.
let ab = a * n + b;
let ba = b * n + a;
result.contains_bits[ab / 64] |= 1u64 << (ab % 64);
result.contains_bits[ba / 64] |= 1u64 << (ba % 64);
if !has_word {
result.without_word_bits[ab / 64] |= 1u64 << (ab % 64);
result.without_word_bits[ba / 64] |= 1u64 << (ba % 64);
}
// Also populate the word-aligned row bitsets.
result.row_bits[a * row_words + b / 64] |= 1u64 << (b % 64);
result.row_bits[b * row_words + a / 64] |= 1u64 << (a % 64);
}
}
}
result
}
pub fn states_with(&self, a: Symbol, b: Symbol) -> &[ParseStateId] {
&self.entries[self.index(a.index, b.index)]
}
pub fn contains(&self, a: Symbol, b: Symbol) -> bool {
!self.entries[self.index(a.index, b.index)].is_empty()
#[must_use]
pub fn all_coincident_states_have_word(&self, a: Symbol, b: Symbol) -> bool {
let bit_index = a.index as usize * self.n + b.index as usize;
self.without_word_bits[bit_index / 64] & (1u64 << (bit_index % 64)) == 0
}
#[must_use]
const fn index(&self, a: usize, b: usize) -> usize {
if a < b {
a * self.n + b
} else {
b * self.n + a
}
pub fn contains(&self, a: Symbol, b: Symbol) -> bool {
let bit_index = a.index as usize * self.n + b.index as usize;
self.contains_bits[bit_index / 64] & (1u64 << (bit_index % 64)) != 0
}
}
impl fmt::Debug for CoincidentTokenIndex<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "CoincidentTokenIndex {{")?;
#[expect(dead_code, reason = "Debugging aid")]
pub struct CoincidentTokenIndexDisplay<'a>(CoincidentTokenIndex, &'a LexicalGrammar, StrPool);
writeln!(f, " entries: {{")?;
for i in 0..self.n {
writeln!(f, " {}: {{", self.grammar.variables[i].name)?;
for j in 0..self.n {
impl std::fmt::Debug for CoincidentTokenIndexDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "CoincidentTokenIndex {{")?;
for i in 0..self.0.n {
let mut coincident = Vec::new();
for j in 0..self.0.n {
if self.0.contains(Symbol::terminal(i), Symbol::terminal(j)) {
coincident.push(self.2.resolve(self.1.variables[j].name));
}
}
if !coincident.is_empty() {
writeln!(
f,
" {}: {:?},",
self.grammar.variables[j].name,
self.entries[self.index(i, j)].len()
" {}: {:?},",
self.2.resolve(self.1.variables[i].name),
coincident
)?;
}
writeln!(f, " }},")?;
}
write!(f, " }},")?;
write!(f, "}}")?;
Ok(())
}

View file

@ -1,32 +1,247 @@
use std::{
cmp::Ordering,
fmt,
hash::{Hash, Hasher},
sync::LazyLock,
hash::{BuildHasherDefault, Hash, Hasher},
};
use indexmap::IndexSet;
use rustc_hash::{FxHashMap, FxHasher};
use crate::{
grammars::{
LexicalGrammar, NO_RESERVED_WORDS, Production, ProductionStep, ReservedWordSetId,
SyntaxGrammar,
},
grammars::{LexicalGrammar, ProdRef, ProductionStep, ReservedWordSetId, SyntaxGrammar},
rules::{Associativity, Precedence, Symbol, SymbolType, TokenSet},
strpool::StrPool,
};
static START_PRODUCTION: LazyLock<Production> = LazyLock::new(|| Production {
dynamic_precedence: 0,
steps: vec![ProductionStep {
symbol: Symbol {
index: 0,
kind: SymbolType::NonTerminal,
},
precedence: Precedence::None,
associativity: None,
alias: None,
field_name: None,
reserved_word_set_id: NO_RESERVED_WORDS,
}],
});
const START_STEPS: [ProductionStep; 1] = [ProductionStep {
sym_index: 0,
prec_val: 0,
alias: 0,
field: 0,
reserved: ProductionStep::NO_RESERVED_WORDS,
flags: SymbolType::NonTerminal as u8,
}];
pub const START_PRODUCTION_ID: u32 = u32::MAX;
const fn start_production() -> ProdRef<'static> {
ProdRef {
steps: &START_STEPS,
dynamic_precedence: 0,
requires_eof_lookahead: false,
}
}
/// Precomputed identity keys for one `(production, dot)` pair.
///
/// `cmp` is the rank of the content tuple `Ord` compared (dynamic precedence,
/// `requires_eof_lookahead`, length, precedence/associativity at the dot, then
/// completed steps' aliases and fields and remaining steps in full). Equal ranks
/// hold _exactly_ when the tuple is equal, so it doubles as the equality class
/// for items without preceding inherited fields. `eq_with_syms` subdivides `cmp`
/// by the completed steps' symbols, which participate in equality only when
/// `has_preceding_inherited_fields` is set.
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
pub struct DotKeys {
pub cmp: u32,
pub eq_with_syms: u32,
}
/// Identity keys for every `(production, dot)` in a grammar (all grammar productions,
/// every inlined production, and the augmented start production).
pub struct ItemKeyMap {
keys: Vec<Box<[DotKeys]>>,
start: Box<[DotKeys]>,
}
impl ItemKeyMap {
pub fn new(grammar: &SyntaxGrammar, str_pool: &StrPool) -> Self {
let prod = |slot: usize| -> ProdRef {
if slot == 0 {
start_production()
} else {
grammar.production(slot as u32 - 1)
}
};
let slot_count = grammar.productions.len() + 1;
let mut contents: Vec<(u32, u32)> = Vec::with_capacity(slot_count);
for pi in 0..slot_count {
for dot in 0..=prod(pi).steps.len() {
contents.push((pi as u32, dot as u32));
}
}
let content = |&(pi, dot): &(u32, u32)| ItemContent {
production: prod(pi as usize),
dot: dot as usize,
str_pool,
};
contents.sort_unstable_by(|a, b| content(a).cmp(&content(b)));
let mut slot_keys: Vec<Box<[DotKeys]>> = (0..slot_count)
.map(|pi| vec![DotKeys::default(); prod(pi).steps.len() + 1].into_boxed_slice())
.collect::<Vec<_>>();
// Dense ids in sorted order: equal content shares an id, distinct content gets the next up
let mut cmp_id = 0;
let mut prev = None;
for &(pi, dot) in &contents {
if let Some(p) = prev
&& content(&p) != content(&(pi, dot))
{
cmp_id += 1;
}
slot_keys[pi as usize][dot as usize].cmp = cmp_id;
prev = Some((pi, dot));
}
// Refine each cmp class by preceding symbols: read only under
// `has_preceding_inherited_fields`.
let mut sym_classes = FxHashMap::default();
for &(pi, dot) in &contents {
let syms = prod(pi as usize).steps[..dot as usize]
.iter()
.map(|s| s.symbol())
.collect::<Vec<_>>();
let next = sym_classes.len() as u32;
let keys = &mut slot_keys[pi as usize][dot as usize];
keys.eq_with_syms = *sym_classes.entry((keys.cmp, syms)).or_insert(next);
}
let start = slot_keys.remove(0);
Self {
keys: slot_keys,
start,
}
}
/// The keys slice (indexed by `dot`) for a production of this grammar.
pub fn keys_for(&self, id: u32) -> &[DotKeys] {
if id == START_PRODUCTION_ID {
&self.start
} else {
&self.keys[id as usize]
}
}
pub fn start_keys(&self) -> &[DotKeys] {
&self.start
}
}
/// The content tuple that `ParseItem`'s `Ord` (and flagless `Eq`) observe, evaluated on
/// `(production, dot)` directly. Ranking the set of all keys by this order makes
/// the dense `cmp` order-preserving. Item ordering only ever compares same-dot pairs,
/// and the dot comparison keeps the order total across dots so ranks are well defined.
struct ItemContent<'a> {
production: ProdRef<'a>,
dot: usize,
str_pool: &'a StrPool,
}
impl Eq for ItemContent<'_> {}
impl ItemContent<'_> {
const fn prec(&self) -> Precedence {
if self.dot > 0 {
self.production.steps[self.dot - 1].precedence()
} else {
Precedence::None
}
}
const fn assoc(&self) -> Option<Associativity> {
if self.dot > 0 {
self.production.steps[self.dot - 1].associativity()
} else {
None
}
}
fn prec_cmp(&self, a: Precedence, b: Precedence) -> Ordering {
match (a, b) {
(Precedence::None, Precedence::None) => Ordering::Equal,
(Precedence::Integer(a), Precedence::Integer(b)) => a.cmp(&b),
(Precedence::Name(a), Precedence::Name(b)) => {
self.str_pool.resolve(a).cmp(self.str_pool.resolve(b))
}
(Precedence::None, _) | (Precedence::Integer(_), Precedence::Name(_)) => Ordering::Less,
(_, Precedence::None) | (Precedence::Name(_), Precedence::Integer(_)) => {
Ordering::Greater
}
}
}
fn alias_cmp(&self, a: ProductionStep, b: ProductionStep) -> Ordering {
let key = |step: ProductionStep| {
step.alias()
.map(|alias| (self.str_pool.resolve(alias.value), alias.is_named))
};
key(a).cmp(&key(b))
}
fn field_cmp(&self, a: ProductionStep, b: ProductionStep) -> Ordering {
let key = |step: ProductionStep| step.field().map(|field| self.str_pool.resolve(field));
key(a).cmp(&key(b))
}
fn step_cmp(&self, a: ProductionStep, b: ProductionStep) -> Ordering {
a.symbol()
.cmp(&b.symbol())
.then_with(|| self.prec_cmp(a.precedence(), b.precedence()))
.then_with(|| a.associativity().cmp(&b.associativity()))
.then_with(|| self.alias_cmp(a, b))
.then_with(|| self.field_cmp(a, b))
.then_with(|| a.reserved.cmp(&b.reserved))
}
}
impl Ord for ItemContent<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.production
.dynamic_precedence
.cmp(&other.production.dynamic_precedence)
.then_with(|| {
self.production
.requires_eof_lookahead
.cmp(&other.production.requires_eof_lookahead)
})
.then_with(|| {
self.production
.steps
.len()
.cmp(&other.production.steps.len())
})
.then_with(|| self.prec_cmp(self.prec(), other.prec()))
.then_with(|| self.assoc().cmp(&other.assoc()))
.then_with(|| self.dot.cmp(&other.dot))
.then_with(|| {
let steps = self.production.steps.iter().zip(other.production.steps);
for (i, (&sa, &sb)) in steps.enumerate() {
let o = if i < self.dot {
self.alias_cmp(sa, sb).then_with(|| self.field_cmp(sa, sb))
} else {
self.step_cmp(sa, sb)
};
if o != Ordering::Equal {
return o;
}
}
Ordering::Equal
})
}
}
impl PartialOrd for ItemContent<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for ItemContent<'_> {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
/// A [`ParseItem`] represents an in-progress match of a single production in a grammar.
#[derive(Clone, Copy, Debug)]
@ -35,8 +250,10 @@ pub struct ParseItem<'a> {
pub variable_index: u32,
/// The number of symbols that have already been matched.
pub step_index: u32,
/// The production being matched.
pub production: &'a Production,
/// The id of the production being matched.
pub prod_id: u32,
/// The `production`'s identity keys, indexed by `step_index`.
pub keys: &'a [DotKeys],
/// A boolean indicating whether any of the already-matched children were
/// hidden nodes and had fields. Ordinarily, a parse item's behavior is not
/// affected by the symbols of its preceding children; it only needs to
@ -53,9 +270,97 @@ pub struct ParseItem<'a> {
pub has_preceding_inherited_fields: bool,
}
/// A [`ParseItemSet`] represents a set of in-progress matches of productions in a
/// grammar, and for each in-progress match, a set of "lookaheads" - tokens that
/// are allowed to *follow* the in-progress rule. This object corresponds directly
/// Interned lookahead set. An index into a [`LookaheadPool`]. Ids are canonical,
/// so id equality is equivalent to underlying [`TokenSet`] equality.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LookaheadSetId(u32);
impl Default for LookaheadSetId {
fn default() -> Self {
LookaheadSetPool::EMPTY
}
}
/// Interner for lookahead [`TokenSet`]s. Item-set entries store ids.
pub struct LookaheadSetPool {
sets: IndexSet<TokenSet, BuildHasherDefault<FxHasher>>,
union_memo: FxHashMap<(LookaheadSetId, LookaheadSetId), LookaheadSetId>,
insert_memo: FxHashMap<(LookaheadSetId, Symbol), LookaheadSetId>,
}
impl LookaheadSetPool {
pub const EMPTY: LookaheadSetId = LookaheadSetId(0);
#[must_use]
pub fn new() -> Self {
let mut pool = Self {
sets: IndexSet::default(),
union_memo: FxHashMap::default(),
insert_memo: FxHashMap::default(),
};
let empty = pool.intern(TokenSet::new());
debug_assert_eq!(empty, Self::EMPTY);
pool
}
#[must_use]
pub fn get(&self, id: LookaheadSetId) -> &TokenSet {
&self.sets[id.0 as usize]
}
pub fn intern(&mut self, set: TokenSet) -> LookaheadSetId {
let (index, _) = self.sets.insert_full(set);
LookaheadSetId(index as u32)
}
pub fn intern_ref(&mut self, set: &TokenSet) -> LookaheadSetId {
if let Some(index) = self.sets.get_index_of(set) {
return LookaheadSetId(index as u32);
}
self.intern(set.clone())
}
pub fn singleton(&mut self, symbol: Symbol) -> LookaheadSetId {
self.insert(Self::EMPTY, symbol)
}
pub fn insert(&mut self, id: LookaheadSetId, symbol: Symbol) -> LookaheadSetId {
if self.get(id).contains(symbol) {
return id;
}
if let Some(&result) = self.insert_memo.get(&(id, symbol)) {
return result;
}
let mut set = self.get(id).clone();
set.insert(symbol);
let result = self.intern(set);
self.insert_memo.insert((id, symbol), result);
result
}
pub fn union(&mut self, left: LookaheadSetId, right: LookaheadSetId) -> LookaheadSetId {
if left == right || right == Self::EMPTY {
return left;
}
if left == Self::EMPTY {
return right;
}
let key = (left.min(right), left.max(right));
if let Some(&result) = self.union_memo.get(&key) {
return result;
}
let mut set = self.get(key.0).clone();
set.insert_all(self.get(key.1));
let result = self.intern(set);
self.union_memo.insert(key, result);
result
}
}
/// Represents a set of in-progress matches of productions in a grammar.
///
/// For each in-progress match, a set of "lookaheads" (tokens that are allowed to
/// *follow* the in-progress rule) are included. This object corresponds directly
/// to a state in the final parse table.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ParseItemSet<'a> {
@ -65,7 +370,7 @@ pub struct ParseItemSet<'a> {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseItemSetEntry<'a> {
pub item: ParseItem<'a>,
pub lookaheads: TokenSet,
pub lookaheads: LookaheadSetId,
pub following_reserved_word_set: ReservedWordSetId,
}
@ -80,50 +385,75 @@ pub struct ParseItemDisplay<'a>(
pub &'a ParseItem<'a>,
pub &'a SyntaxGrammar,
pub &'a LexicalGrammar,
pub &'a StrPool,
);
pub struct TokenSetDisplay<'a>(
pub &'a TokenSet,
pub &'a SyntaxGrammar,
pub &'a LexicalGrammar,
pub &'a StrPool,
);
pub struct ParseItemSetDisplay<'a>(
pub &'a ParseItemSet<'a>,
pub &'a SyntaxGrammar,
pub &'a LexicalGrammar,
pub &'a StrPool,
pub &'a LookaheadSetPool,
);
impl<'a> ParseItem<'a> {
pub fn start() -> Self {
#[must_use]
pub fn start(key_map: &'a ItemKeyMap) -> Self {
ParseItem {
variable_index: u32::MAX,
production: &START_PRODUCTION,
prod_id: START_PRODUCTION_ID,
keys: key_map.start_keys(),
step_index: 0,
has_preceding_inherited_fields: false,
}
}
pub fn step(&self) -> Option<&'a ProductionStep> {
self.production.steps.get(self.step_index as usize)
/// The production being matched.
#[must_use]
pub fn production<'g>(&self, grammar: &'g SyntaxGrammar) -> ProdRef<'g> {
if self.prod_id == START_PRODUCTION_ID {
start_production()
} else {
grammar.production(self.prod_id)
}
}
pub fn symbol(&self) -> Option<Symbol> {
self.step().map(|step| step.symbol)
#[must_use]
pub fn step(&self, grammar: &SyntaxGrammar) -> Option<ProductionStep> {
self.production(grammar)
.steps
.get(self.step_index as usize)
.copied()
}
pub fn associativity(&self) -> Option<Associativity> {
self.prev_step().and_then(|step| step.associativity)
#[must_use]
pub fn symbol(&self, grammar: &SyntaxGrammar) -> Option<Symbol> {
self.step(grammar).map(ProductionStep::symbol)
}
pub fn precedence(&self) -> &Precedence {
self.prev_step()
.map_or(&Precedence::None, |step| &step.precedence)
#[must_use]
pub fn associativity(&self, grammar: &SyntaxGrammar) -> Option<Associativity> {
self.prev_step(grammar)
.and_then(ProductionStep::associativity)
}
pub fn prev_step(&self) -> Option<&'a ProductionStep> {
#[must_use]
pub fn precedence(&self, grammar: &SyntaxGrammar) -> Precedence {
self.prev_step(grammar)
.map_or(Precedence::None, ProductionStep::precedence)
}
#[must_use]
pub fn prev_step(&self, grammar: &SyntaxGrammar) -> Option<ProductionStep> {
if self.step_index > 0 {
Some(&self.production.steps[self.step_index as usize - 1])
Some(self.production(grammar).steps[self.step_index as usize - 1])
} else {
None
}
@ -131,7 +461,7 @@ impl<'a> ParseItem<'a> {
#[must_use]
pub const fn is_done(&self) -> bool {
self.step_index as usize == self.production.steps.len()
self.step_index as usize + 1 == self.keys.len()
}
#[must_use]
@ -144,7 +474,8 @@ impl<'a> ParseItem<'a> {
pub const fn successor(&self) -> Self {
ParseItem {
variable_index: self.variable_index,
production: self.production,
prod_id: self.prod_id,
keys: self.keys,
step_index: self.step_index + 1,
has_preceding_inherited_fields: self.has_preceding_inherited_fields,
}
@ -152,11 +483,19 @@ impl<'a> ParseItem<'a> {
/// Create an item identical to this one, but with a different production.
/// This is used when dynamically "inlining" certain symbols in a production.
pub const fn substitute_production(&self, production: &'a Production) -> Self {
#[must_use]
pub const fn substitute_production(&self, prod_id: u32, keys: &'a [DotKeys]) -> Self {
let mut result = *self;
result.production = production;
result.prod_id = prod_id;
result.keys = keys;
result
}
/// This item's identity keys at the current dot.
#[must_use]
const fn dot_keys(&self) -> DotKeys {
self.keys[self.step_index as usize]
}
}
impl<'a> ParseItemSet<'a> {
@ -168,7 +507,7 @@ impl<'a> ParseItemSet<'a> {
i,
ParseItemSetEntry {
item,
lookaheads: TokenSet::new(),
lookaheads: LookaheadSetPool::EMPTY,
following_reserved_word_set: ReservedWordSetId::default(),
},
);
@ -178,6 +517,7 @@ impl<'a> ParseItemSet<'a> {
}
}
#[must_use]
pub fn core(&self) -> ParseItemSetCore<'a> {
ParseItemSetCore {
entries: self.entries.iter().map(|e| e.item).collect(),
@ -185,6 +525,14 @@ impl<'a> ParseItemSet<'a> {
}
}
pub fn prec_display(prec: Precedence, str_pool: &StrPool) -> String {
match prec {
Precedence::None => "none".to_string(),
Precedence::Integer(i) => i.to_string(),
Precedence::Name(sid) => format!("'{}'", str_pool.resolve(sid)),
}
}
impl fmt::Display for ParseItemDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
if self.0.is_augmented() {
@ -193,60 +541,76 @@ impl fmt::Display for ParseItemDisplay<'_> {
write!(
f,
"{} →",
self.1.variables[self.0.variable_index as usize].name
self.3
.resolve(self.1.variables[self.0.variable_index as usize].name)
)?;
}
for (i, step) in self.0.production.steps.iter().enumerate() {
let production = self.0.production(self.1);
for (i, step) in production.steps.iter().enumerate() {
let symbol = step.symbol();
if i == self.0.step_index as usize {
write!(f, "")?;
if !step.precedence.is_none()
|| step.associativity.is_some()
|| step.reserved_word_set_id != ReservedWordSetId::default()
if step.precedence() != Precedence::None
|| step.associativity().is_some()
|| step.reserved != 0
{
write!(f, " (")?;
if !step.precedence.is_none() {
write!(f, " {}", step.precedence)?;
if step.precedence() != Precedence::None {
write!(f, " {}", prec_display(step.precedence(), self.3))?;
}
if let Some(associativity) = step.associativity {
if let Some(associativity) = step.associativity() {
write!(f, " {associativity:?}")?;
}
if step.reserved_word_set_id != ReservedWordSetId::default() {
write!(f, "reserved: {}", step.reserved_word_set_id)?;
if step.reserved != 0 {
write!(f, "reserved: {}", step.reserved)?;
}
write!(f, " )")?;
}
}
write!(f, " ")?;
if step.symbol.is_terminal() {
if let Some(variable) = self.2.variables.get(step.symbol.index) {
write!(f, "{}", variable.name)?;
if symbol.is_terminal() {
if let Some(variable) = self.2.variables.get(symbol.index as usize) {
write!(f, "{}", self.3.resolve(variable.name))?;
} else {
write!(f, "terminal-{}", step.symbol.index)?;
write!(f, "terminal-{}", symbol.index)?;
}
} else if step.symbol.is_external() {
write!(f, "{}", self.1.external_tokens[step.symbol.index].name)?;
} else if symbol.is_external() {
write!(
f,
"{}",
self.3
.resolve(self.1.external_tokens[symbol.index as usize].name)
)?;
} else {
write!(f, "{}", self.1.variables[step.symbol.index].name)?;
write!(
f,
"{}",
self.3.resolve(self.1.variables[symbol.index as usize].name)
)?;
}
if let Some(alias) = &step.alias {
write!(f, "@{}", alias.value)?;
if let Some(alias) = &step.alias() {
write!(f, "@{}", self.3.resolve(alias.value))?;
}
}
if self.0.is_done() {
write!(f, "")?;
if let Some(step) = self.0.production.steps.last() {
if let Some(associativity) = step.associativity {
if step.precedence.is_none() {
if let Some(&step) = production.steps.last() {
if let Some(associativity) = step.associativity() {
if step.precedence() == Precedence::None {
write!(f, " ({associativity:?})")?;
} else {
write!(f, " ({} {associativity:?})", step.precedence)?;
write!(
f,
" ({} {associativity:?})",
prec_display(step.precedence(), self.3)
)?;
}
} else if !step.precedence.is_none() {
write!(f, " ({})", step.precedence)?;
} else if step.precedence() != Precedence::None {
write!(f, " ({})", prec_display(step.precedence(), self.3))?;
}
}
}
@ -290,15 +654,28 @@ impl fmt::Display for TokenSetDisplay<'_> {
}
if symbol.is_terminal() {
if let Some(variable) = self.2.variables.get(symbol.index) {
write!(f, "{}", display_variable_name(&variable.name))?;
if let Some(variable) = self.2.variables.get(symbol.index as usize) {
write!(
f,
"{}",
display_variable_name(self.3.resolve(variable.name))
)?;
} else {
write!(f, "terminal-{}", symbol.index)?;
}
} else if symbol.is_external() {
write!(f, "{}", self.1.external_tokens[symbol.index].name)?;
write!(
f,
"{}",
self.3
.resolve(self.1.external_tokens[symbol.index as usize].name)
)?;
} else {
write!(f, "{}", self.1.variables[symbol.index].name)?;
write!(
f,
"{}",
self.3.resolve(self.1.variables[symbol.index as usize].name)
)?;
}
}
write!(f, "]")?;
@ -312,8 +689,8 @@ impl fmt::Display for ParseItemSetDisplay<'_> {
write!(
f,
"{}\t{}",
ParseItemDisplay(&entry.item, self.1, self.2),
TokenSetDisplay(&entry.lookaheads, self.1, self.2),
ParseItemDisplay(&entry.item, self.1, self.2, self.3),
TokenSetDisplay(self.4.get(entry.lookaheads), self.1, self.2, self.3),
)?;
if entry.following_reserved_word_set != ReservedWordSetId::default() {
write!(
@ -332,11 +709,6 @@ impl Hash for ParseItem<'_> {
fn hash<H: Hasher>(&self, hasher: &mut H) {
hasher.write_u32(self.variable_index);
hasher.write_u32(self.step_index);
hasher.write_i32(self.production.dynamic_precedence);
hasher.write_usize(self.production.steps.len());
hasher.write_i32(i32::from(self.has_preceding_inherited_fields));
self.precedence().hash(hasher);
self.associativity().hash(hasher);
// The already-matched children don't play any role in the parse state for
// this item, unless any of the following are true:
@ -344,15 +716,14 @@ impl Hash for ParseItem<'_> {
// * the children have aliases
// * the children are hidden and represent rules that have fields.
// See the docs for `has_preceding_inherited_fields`.
for step in &self.production.steps[0..self.step_index as usize] {
step.alias.hash(hasher);
step.field_name.hash(hasher);
if self.has_preceding_inherited_fields {
step.symbol.hash(hasher);
}
}
for step in &self.production.steps[self.step_index as usize..] {
step.hash(hasher);
// Preceding symbols participate only in `eq_with_syms`.
let keys = self.dot_keys();
if self.has_preceding_inherited_fields {
hasher.write_u8(1);
hasher.write_u32(keys.eq_with_syms);
} else {
hasher.write_u8(0);
hasher.write_u32(keys.cmp);
}
}
}
@ -362,36 +733,16 @@ impl PartialEq for ParseItem<'_> {
fn eq(&self, other: &Self) -> bool {
if self.variable_index != other.variable_index
|| self.step_index != other.step_index
|| self.production.dynamic_precedence != other.production.dynamic_precedence
|| self.production.steps.len() != other.production.steps.len()
|| self.precedence() != other.precedence()
|| self.associativity() != other.associativity()
|| self.has_preceding_inherited_fields != other.has_preceding_inherited_fields
{
return false;
}
for (i, step) in self.production.steps.iter().enumerate() {
// See the previous comment (in the `Hash::hash` impl) regarding comparisons
// of parse items' already-completed steps.
if i < self.step_index as usize {
if step.alias != other.production.steps[i].alias {
return false;
}
if step.field_name != other.production.steps[i].field_name {
return false;
}
if self.has_preceding_inherited_fields
&& step.symbol != other.production.steps[i].symbol
{
return false;
}
} else if *step != other.production.steps[i] {
return false;
}
if self.has_preceding_inherited_fields {
self.dot_keys().eq_with_syms == other.dot_keys().eq_with_syms
} else {
self.dot_keys().cmp == other.dot_keys().cmp
}
true
}
}
@ -401,37 +752,19 @@ impl Ord for ParseItem<'_> {
self.step_index
.cmp(&other.step_index)
.then_with(|| self.variable_index.cmp(&other.variable_index))
.then_with(|| self.dot_keys().cmp.cmp(&other.dot_keys().cmp))
.then_with(|| {
self.production
.dynamic_precedence
.cmp(&other.production.dynamic_precedence)
self.has_preceding_inherited_fields
.cmp(&other.has_preceding_inherited_fields)
})
.then_with(|| {
self.production
.steps
.len()
.cmp(&other.production.steps.len())
})
.then_with(|| self.precedence().cmp(other.precedence()))
.then_with(|| self.associativity().cmp(&other.associativity()))
.then_with(|| {
for (i, step) in self.production.steps.iter().enumerate() {
// See the previous comment (in the `Hash::hash` impl) regarding comparisons
// of parse items' already-completed steps.
let o = if i < self.step_index as usize {
step.alias
.cmp(&other.production.steps[i].alias)
.then_with(|| {
step.field_name.cmp(&other.production.steps[i].field_name)
})
} else {
step.cmp(&other.production.steps[i])
};
if o != Ordering::Equal {
return o;
}
if self.has_preceding_inherited_fields {
self.dot_keys()
.eq_with_syms
.cmp(&other.dot_keys().eq_with_syms)
} else {
Ordering::Equal
}
Ordering::Equal
})
}
}

View file

@ -1,20 +1,35 @@
use std::{
collections::{HashMap, HashSet},
fmt,
};
use std::fmt;
use super::item::{ParseItem, ParseItemDisplay, ParseItemSet, ParseItemSetEntry, TokenSetDisplay};
use rustc_hash::{FxHashMap, FxHashSet};
use super::item::{
ItemKeyMap, ParseItem, ParseItemDisplay, ParseItemSet, ParseItemSetEntry, TokenSetDisplay,
};
use crate::{
build_tables::item::{LookaheadSetId, LookaheadSetPool},
grammars::{InlinedProductionMap, LexicalGrammar, ReservedWordSetId, SyntaxGrammar},
rules::{Symbol, SymbolType, TokenSet},
strpool::StrPool,
};
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TransitiveClosureAddition<'a> {
item: ParseItem<'a>,
info: FollowSetInfo,
info: AdditionInfo,
}
/// [`FollowSetInfo`] with an interned lookahead set and word-token membership precomputed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct AdditionInfo {
lookaheads: LookaheadSetId,
reserved_lookaheads: ReservedWordSetId,
propagates_lookaheads: bool,
contains_word: bool,
}
/// the mutable accumulator for a non-terminal's follow set. `lookaheads` is a
/// working `TokenSet` that can be unioned incrementally during traversal, to be
/// interned into [`AdditionInfo`] once complete.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct FollowSetInfo {
lookaheads: TokenSet,
@ -24,11 +39,14 @@ struct FollowSetInfo {
pub struct ParseItemSetBuilder<'a> {
syntax_grammar: &'a SyntaxGrammar,
lexical_grammar: &'a LexicalGrammar,
first_sets: HashMap<Symbol, TokenSet>,
reserved_first_sets: HashMap<Symbol, ReservedWordSetId>,
last_sets: HashMap<Symbol, TokenSet>,
first_sets: FxHashMap<Symbol, TokenSet>,
/// Each FIRST set interned, for closure propagation by id
first_set_ids: FxHashMap<Symbol, LookaheadSetId>,
reserved_first_sets: FxHashMap<Symbol, ReservedWordSetId>,
last_sets: FxHashMap<Symbol, TokenSet>,
inlines: &'a InlinedProductionMap,
pub key_map: &'a ItemKeyMap,
pub lookaheads: LookaheadSetPool,
transitive_closure_additions: Vec<Vec<TransitiveClosureAddition<'a>>>,
}
@ -39,18 +57,22 @@ fn find_or_push<T: Eq>(vector: &mut Vec<T>, value: T) {
}
impl<'a> ParseItemSetBuilder<'a> {
#[must_use]
pub fn new(
syntax_grammar: &'a SyntaxGrammar,
lexical_grammar: &'a LexicalGrammar,
inlines: &'a InlinedProductionMap,
key_map: &'a ItemKeyMap,
) -> Self {
let mut result = Self {
syntax_grammar,
lexical_grammar,
first_sets: HashMap::new(),
reserved_first_sets: HashMap::new(),
last_sets: HashMap::new(),
first_sets: FxHashMap::default(),
first_set_ids: FxHashMap::default(),
reserved_first_sets: FxHashMap::default(),
last_sets: FxHashMap::default(),
inlines,
key_map,
lookaheads: LookaheadSetPool::new(),
transitive_closure_additions: vec![Vec::new(); syntax_grammar.variables.len()],
};
@ -92,7 +114,7 @@ impl<'a> ParseItemSetBuilder<'a> {
// Rather than computing these sets using recursion, we use an explicit stack
// called `symbols_to_process`.
let mut symbols_to_process = Vec::new();
let mut processed_non_terminals = HashSet::new();
let mut processed_non_terminals = FxHashSet::default();
for i in 0..syntax_grammar.variables.len() {
let symbol = Symbol::non_terminal(i);
let first_set = result.first_sets.entry(symbol).or_default();
@ -102,14 +124,16 @@ impl<'a> ParseItemSetBuilder<'a> {
symbols_to_process.clear();
symbols_to_process.push(symbol);
while let Some(sym) = symbols_to_process.pop() {
for production in &syntax_grammar.variables[sym.index].productions {
if let Some(step) = production.steps.first() {
if step.symbol.is_terminal() || step.symbol.is_external() {
first_set.insert(step.symbol);
} else if processed_non_terminals.insert(step.symbol) {
symbols_to_process.push(step.symbol);
for prod_id in syntax_grammar.variable_prod_ids(sym.index as usize) {
if let Some(step) = syntax_grammar.production(prod_id).steps.first() {
let symbol = step.symbol();
if symbol.is_terminal() || symbol.is_external() {
first_set.insert(symbol);
} else if processed_non_terminals.insert(symbol) {
symbols_to_process.push(symbol);
}
*reserved_first_set = (*reserved_first_set).max(step.reserved_word_set_id);
*reserved_first_set =
(*reserved_first_set).max(ReservedWordSetId(u32::from(step.reserved)));
}
}
}
@ -120,18 +144,26 @@ impl<'a> ParseItemSetBuilder<'a> {
symbols_to_process.clear();
symbols_to_process.push(symbol);
while let Some(sym) = symbols_to_process.pop() {
for production in &syntax_grammar.variables[sym.index].productions {
if let Some(step) = production.steps.last() {
if step.symbol.is_terminal() || step.symbol.is_external() {
last_set.insert(step.symbol);
} else if processed_non_terminals.insert(step.symbol) {
symbols_to_process.push(step.symbol);
for prod_id in syntax_grammar.variable_prod_ids(sym.index as usize) {
if let Some(step) = syntax_grammar.production(prod_id).steps.last() {
let symbol = step.symbol();
if symbol.is_terminal() || symbol.is_external() {
last_set.insert(symbol);
} else if processed_non_terminals.insert(symbol) {
symbols_to_process.push(symbol);
}
}
}
}
}
// Intern each FIRST set so closure propagation can union by id.
for (symbol, set) in &result.first_sets {
result
.first_set_ids
.insert(*symbol, result.lookaheads.intern_ref(set));
}
// To compute an item set's transitive closure, we find each item in the set
// whose next symbol is a non-terminal, and we add new items to the set for
// each of that symbol's productions. These productions might themselves begin
@ -152,8 +184,8 @@ impl<'a> ParseItemSetBuilder<'a> {
// * `lookaheads` - the set of possible lookahead tokens that can always come after `item`
// in an expansion of symbol `i`.
//
// * `reserved_lookaheads` - the set of reserved lookahead lookahead tokens that can
// always come after `item` in the expansion of symbol `i`.
// * `reserved_lookaheads` - the set of reserved lookahead tokens that can always come
// after `item` in the expansion of symbol `i`.
//
// * `propagates_lookaheads` - a boolean indicating whether or not `item` can occur at the
// *end* of the expansion of symbol `i`, so that i's own current lookahead tokens can
@ -161,8 +193,10 @@ impl<'a> ParseItemSetBuilder<'a> {
//
// Rather than computing these additions recursively, we use an explicit stack.
let empty_lookaheads = TokenSet::new();
let mut eof_lookaheads = TokenSet::new();
eof_lookaheads.insert(Symbol::end());
let mut stack = Vec::new();
let mut follow_set_info_by_non_terminal = HashMap::<usize, FollowSetInfo>::new();
let mut follow_set_info_by_non_terminal = FxHashMap::<usize, FollowSetInfo>::default();
for i in 0..syntax_grammar.variables.len() {
// First, build up a map whose keys are all of the non-terminals that can
// appear at the beginning of non-terminal `i`, and whose values store
@ -186,20 +220,28 @@ impl<'a> ParseItemSetBuilder<'a> {
continue;
}
for production in &syntax_grammar.variables[sym_ix].productions {
for prod_id in syntax_grammar.variable_prod_ids(sym_ix) {
let production = syntax_grammar.production(prod_id);
if let Some(symbol) = production.first_symbol()
&& symbol.is_non_terminal()
{
if let Some(next_step) = production.steps.get(1) {
stack.push((
symbol.index,
&result.first_sets[&next_step.symbol],
result.reserved_first_sets[&next_step.symbol],
symbol.index as usize,
&result.first_sets[&next_step.symbol()],
result.reserved_first_sets[&next_step.symbol()],
false,
));
} else if production.requires_eof_lookahead {
stack.push((
symbol.index as usize,
&eof_lookaheads,
ReservedWordSetId::default(),
false,
));
} else {
stack.push((
symbol.index,
symbol.index as usize,
lookaheads,
reserved_word_set_id,
propagates_lookaheads,
@ -211,40 +253,60 @@ impl<'a> ParseItemSetBuilder<'a> {
// Store all of those non-terminals' productions, along with their associated
// lookahead info, as *additions* associated with non-terminal `i`.
let additions_for_non_terminal = &mut result.transitive_closure_additions[i];
for (&variable_index, follow_set_info) in &follow_set_info_by_non_terminal {
let variable = &syntax_grammar.variables[variable_index];
let non_terminal = Symbol::non_terminal(variable_index);
let variable_index = variable_index as u32;
if syntax_grammar.variables_to_inline.contains(&non_terminal) {
continue;
}
for production in &variable.productions {
let info = AdditionInfo {
lookaheads: result.lookaheads.intern_ref(&follow_set_info.lookaheads),
reserved_lookaheads: follow_set_info.reserved_lookaheads,
propagates_lookaheads: follow_set_info.propagates_lookaheads,
contains_word: syntax_grammar
.word_token
.is_some_and(|w| follow_set_info.lookaheads.contains(w)),
};
let additions_for_non_terminal = &mut result.transitive_closure_additions[i];
for prod_id in syntax_grammar.variable_prod_ids(variable_index) {
let item = ParseItem {
variable_index,
production,
variable_index: variable_index as u32,
prod_id,
keys: key_map.keys_for(prod_id),
step_index: 0,
has_preceding_inherited_fields: false,
};
if let Some(inlined_productions) =
inlines.inlined_productions(item.production, item.step_index)
{
for production in inlined_productions {
if let Some(ids) = inlines.inlined_prod_ids(item.prod_id, item.step_index) {
for &id in ids {
let mut item_info = info;
if syntax_grammar.production(id).requires_eof_lookahead {
item_info.lookaheads =
result.lookaheads.intern_ref(&eof_lookaheads);
item_info.reserved_lookaheads = ReservedWordSetId::default();
item_info.propagates_lookaheads = false;
item_info.contains_word = false;
}
find_or_push(
additions_for_non_terminal,
TransitiveClosureAddition {
item: item.substitute_production(production),
info: follow_set_info.clone(),
item: item.substitute_production(id, key_map.keys_for(id)),
info: item_info,
},
);
}
} else {
let mut item_info = info;
if syntax_grammar.production(prod_id).requires_eof_lookahead {
item_info.lookaheads = result.lookaheads.intern_ref(&eof_lookaheads);
item_info.reserved_lookaheads = ReservedWordSetId::default();
item_info.propagates_lookaheads = false;
item_info.contains_word = false;
}
find_or_push(
additions_for_non_terminal,
TransitiveClosureAddition {
item,
info: follow_set_info.clone(),
info: item_info,
},
);
}
@ -255,19 +317,22 @@ impl<'a> ParseItemSetBuilder<'a> {
result
}
pub fn transitive_closure(&self, item_set: &ParseItemSet<'a>) -> ParseItemSet<'a> {
#[must_use]
pub fn transitive_closure(&mut self, item_set: &ParseItemSet<'a>) -> ParseItemSet<'a> {
let mut result = ParseItemSet::default();
for entry in &item_set.entries {
if let Some(productions) = self
if let Some(ids) = self
.inlines
.inlined_productions(entry.item.production, entry.item.step_index)
.inlined_prod_ids(entry.item.prod_id, entry.item.step_index)
{
for production in productions {
for &id in ids {
self.add_item(
&mut result,
&ParseItemSetEntry {
item: entry.item.substitute_production(production),
lookaheads: entry.lookaheads.clone(),
item: entry
.item
.substitute_production(id, self.key_map.keys_for(id)),
lookaheads: entry.lookaheads,
following_reserved_word_set: entry.following_reserved_word_set,
},
);
@ -279,114 +344,133 @@ impl<'a> ParseItemSetBuilder<'a> {
result
}
pub fn first_set(&self, symbol: &Symbol) -> &TokenSet {
&self.first_sets[symbol]
#[must_use]
pub fn first_set(&self, symbol: Symbol) -> &TokenSet {
&self.first_sets[&symbol]
}
pub fn reserved_first_set(&self, symbol: &Symbol) -> Option<&TokenSet> {
let id = *self.reserved_first_sets.get(symbol)?;
Some(&self.syntax_grammar.reserved_word_sets[id.0])
#[must_use]
pub fn reserved_first_set(&self, symbol: Symbol) -> Option<&TokenSet> {
let id = *self.reserved_first_sets.get(&symbol)?;
Some(&self.syntax_grammar.reserved_word_sets[id.0 as usize])
}
pub fn last_set(&self, symbol: &Symbol) -> &TokenSet {
&self.last_sets[symbol]
#[must_use]
pub fn last_set(&self, symbol: Symbol) -> &TokenSet {
&self.last_sets[&symbol]
}
fn add_item(&self, set: &mut ParseItemSet<'a>, entry: &ParseItemSetEntry<'a>) {
if let Some(step) = entry.item.step()
&& step.symbol.is_non_terminal()
fn add_item(&mut self, set: &mut ParseItemSet<'a>, entry: &ParseItemSetEntry<'a>) {
if let Some(step) = entry.item.step(self.syntax_grammar)
&& step.symbol().is_non_terminal()
{
let next_step = entry.item.successor().step();
let next_step = entry.item.successor().step(self.syntax_grammar);
// Determine which tokens can follow this non-terminal.
let (following_tokens, following_reserved_tokens) = if let Some(next_step) = next_step {
(
self.first_sets.get(&next_step.symbol).unwrap(),
*self.reserved_first_sets.get(&next_step.symbol).unwrap(),
self.first_set_ids[&next_step.symbol()],
self.reserved_first_sets[&next_step.symbol()],
)
} else {
(&entry.lookaheads, entry.following_reserved_word_set)
(entry.lookaheads, entry.following_reserved_word_set)
};
// Use the pre-computed *additions* to expand the non-terminal.
for addition in &self.transitive_closure_additions[step.symbol.index] {
let entry = set.insert(addition.item);
entry.lookaheads.insert_all(&addition.info.lookaheads);
for addition in &self.transitive_closure_additions[step.symbol().index as usize] {
let e = set.insert(addition.item);
e.lookaheads = self
.lookaheads
.union(e.lookaheads, addition.info.lookaheads);
if let Some(word_token) = self.syntax_grammar.word_token
&& addition.info.lookaheads.contains(&word_token)
{
entry.following_reserved_word_set = entry
if addition.info.contains_word {
e.following_reserved_word_set = e
.following_reserved_word_set
.max(addition.info.reserved_lookaheads);
}
if addition.info.propagates_lookaheads {
entry.lookaheads.insert_all(following_tokens);
e.lookaheads = self.lookaheads.union(e.lookaheads, following_tokens);
if let Some(word_token) = self.syntax_grammar.word_token
&& following_tokens.contains(&word_token)
&& self.lookaheads.get(following_tokens).contains(word_token)
{
entry.following_reserved_word_set = entry
.following_reserved_word_set
.max(following_reserved_tokens);
e.following_reserved_word_set =
e.following_reserved_word_set.max(following_reserved_tokens);
}
}
}
}
let e = set.insert(entry.item);
e.lookaheads.insert_all(&entry.lookaheads);
e.lookaheads = self.lookaheads.union(e.lookaheads, entry.lookaheads);
e.following_reserved_word_set = e
.following_reserved_word_set
.max(entry.following_reserved_word_set);
}
}
impl fmt::Debug for ParseItemSetBuilder<'_> {
#[expect(dead_code, reason = "Debugging aid")]
struct ParseItemSetBuilderDisplay<'a>(
pub &'a ParseItemSetBuilder<'a>,
pub &'a LexicalGrammar,
pub &'a StrPool,
);
impl fmt::Debug for ParseItemSetBuilderDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "ParseItemSetBuilder {{")?;
writeln!(f, " first_sets: {{")?;
for (symbol, first_set) in &self.first_sets {
for (symbol, first_set) in &self.0.first_sets {
let symbol_index = symbol.index as usize;
let name = match symbol.kind {
SymbolType::NonTerminal => &self.syntax_grammar.variables[symbol.index].name,
SymbolType::External => &self.syntax_grammar.external_tokens[symbol.index].name,
SymbolType::Terminal => &self.lexical_grammar.variables[symbol.index].name,
SymbolType::NonTerminal => self
.2
.resolve(self.0.syntax_grammar.variables[symbol_index].name),
SymbolType::External => self
.2
.resolve(self.0.syntax_grammar.external_tokens[symbol_index].name),
SymbolType::Terminal => self.2.resolve(self.1.variables[symbol_index].name),
SymbolType::End | SymbolType::EndOfNonTerminalExtra => "END",
};
writeln!(
f,
" first({name:?}): {}",
TokenSetDisplay(first_set, self.syntax_grammar, self.lexical_grammar)
TokenSetDisplay(first_set, self.0.syntax_grammar, self.1, self.2)
)?;
}
writeln!(f, " }}")?;
writeln!(f, " last_sets: {{")?;
for (symbol, last_set) in &self.last_sets {
for (symbol, last_set) in &self.0.last_sets {
let symbol_index = symbol.index as usize;
let name = match symbol.kind {
SymbolType::NonTerminal => &self.syntax_grammar.variables[symbol.index].name,
SymbolType::External => &self.syntax_grammar.external_tokens[symbol.index].name,
SymbolType::Terminal => &self.lexical_grammar.variables[symbol.index].name,
SymbolType::NonTerminal => self
.2
.resolve(self.0.syntax_grammar.variables[symbol_index].name),
SymbolType::External => self
.2
.resolve(self.0.syntax_grammar.external_tokens[symbol_index].name),
SymbolType::Terminal => self.2.resolve(self.1.variables[symbol_index].name),
SymbolType::End | SymbolType::EndOfNonTerminalExtra => "END",
};
writeln!(
f,
" last({name:?}): {}",
TokenSetDisplay(last_set, self.syntax_grammar, self.lexical_grammar)
TokenSetDisplay(last_set, self.0.syntax_grammar, self.1, self.2)
)?;
}
writeln!(f, " }}")?;
writeln!(f, " additions: {{")?;
for (i, variable) in self.syntax_grammar.variables.iter().enumerate() {
writeln!(f, " {}: {{", variable.name)?;
for addition in &self.transitive_closure_additions[i] {
for (i, variable) in self.0.syntax_grammar.variables.iter().enumerate() {
writeln!(f, " {}: {{", self.2.resolve(variable.name))?;
for addition in &self.0.transitive_closure_additions[i] {
writeln!(
f,
" {}",
ParseItemDisplay(&addition.item, self.syntax_grammar, self.lexical_grammar)
ParseItemDisplay(&addition.item, self.0.syntax_grammar, self.1, self.2)
)?;
}
writeln!(f, " }},")?;

View file

@ -1,7 +1,6 @@
use std::{
collections::{HashMap, HashSet},
mem,
};
use std::{cmp::Ordering, mem};
use rustc_hash::{FxHashMap, FxHashSet};
use log::debug;
@ -10,10 +9,75 @@ use crate::{
OptLevel,
dedup::split_state_id_groups,
grammars::{LexicalGrammar, SyntaxGrammar, VariableType},
rules::{AliasMap, Symbol, TokenSet},
tables::{GotoAction, ParseAction, ParseState, ParseStateId, ParseTable, ParseTableEntry},
rules::{AliasMap, Symbol, SymbolType, TokenSet},
strpool::StrPool,
tables::{
ActionList, ActionListId, GotoAction, ParseAction, ParseState, ParseStateId, ParseTable,
},
};
/// Index into [`SyntaxGrammar::variables`]. All nonterminal [`Symbol`]s share
/// the same `kind`, so storing the index alone is sufficient for ordering.
type NonterminalIndex = u32;
/// A [`Symbol`] packed into a `u64` for O(1) sort-key comparison.
///
/// Layout: high 3 bits = `kind` discriminant (5 variants fit in 3 bits), low 61 bits = `index`.
/// This preserves [`Symbol`]'s derived [`Ord`] ordering (kind first, then index) as a single
/// integer comparison, and halves each entry's size vs storing a full `(Symbol, _)` tuple.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct SymbolKey(u64);
const KEY_TAG_SHIFT: u32 = 61;
const KEY_INDEX_MASK: u64 = (1u64 << KEY_TAG_SHIFT) - 1;
impl SymbolKey {
#[inline]
fn new(sym: Symbol) -> Self {
debug_assert!(
u64::from(sym.index) <= KEY_INDEX_MASK,
"symbol index too large"
);
Self((sym.kind as u64) << KEY_TAG_SHIFT | u64::from(sym.index))
}
#[inline]
const fn symbol(self) -> Symbol {
let kind = match self.0 >> KEY_TAG_SHIFT {
0 => SymbolType::External,
1 => SymbolType::End,
2 => SymbolType::EndOfNonTerminalExtra,
3 => SymbolType::Terminal,
_ => SymbolType::NonTerminal,
};
Symbol {
kind,
index: self.index(),
}
}
#[inline]
const fn index(self) -> u32 {
(self.0 & KEY_INDEX_MASK) as u32
}
#[inline]
#[expect(dead_code)]
const fn is_terminal(self) -> bool {
(self.0 >> KEY_TAG_SHIFT) == SymbolType::Terminal as u64
}
#[inline]
#[expect(dead_code)]
const fn is_non_terminal(self) -> bool {
(self.0 >> KEY_TAG_SHIFT) == SymbolType::NonTerminal as u64
}
}
#[expect(
clippy::too_many_arguments,
reason = "all parameters are required for parse table minimization"
)]
pub fn minimize_parse_table(
parse_table: &mut ParseTable,
syntax_grammar: &SyntaxGrammar,
@ -21,6 +85,7 @@ pub fn minimize_parse_table(
simple_aliases: &AliasMap,
token_conflict_map: &TokenConflictMap,
keywords: &TokenSet,
str_pool: &StrPool,
optimizations: OptLevel,
) {
let mut minimizer = Minimizer {
@ -30,6 +95,7 @@ pub fn minimize_parse_table(
token_conflict_map,
keywords,
simple_aliases,
str_pool,
};
if optimizations.contains(OptLevel::MergeStates) {
minimizer.merge_compatible_states();
@ -39,34 +105,67 @@ pub fn minimize_parse_table(
minimizer.reorder_states_by_descending_size();
}
/// Word-aligned bitsets precomputed for `token_conflicts`, all indexed by terminal
/// index. `state_terminals` and `conflict_rows` are flattened two dimensional vectors:
/// row `i` spans `[i * row_words, (i + 1) * row_words)`.
struct ConflictBits {
row_words: usize,
/// Per state: which terminals have entries
state_terminals: Vec<u64>,
/// Per token: which terminals it lexically conflicts with
conflict_rows: Vec<u64>,
/// The keyword set.
keywords: Vec<u64>,
/// Tokens that are also external tokens.
internal_external: Vec<u64>,
}
impl ConflictBits {
#[inline]
fn get_conflict_row(&self, token: usize) -> &[u64] {
let base = token * self.row_words;
&self.conflict_rows[base..base + self.row_words]
}
#[inline]
fn get_state_row(&self, state: usize) -> &[u64] {
let base = state * self.row_words;
&self.state_terminals[base..base + self.row_words]
}
}
struct Minimizer<'a> {
parse_table: &'a mut ParseTable,
syntax_grammar: &'a SyntaxGrammar,
lexical_grammar: &'a LexicalGrammar,
token_conflict_map: &'a TokenConflictMap<'a>,
token_conflict_map: &'a TokenConflictMap,
keywords: &'a TokenSet,
simple_aliases: &'a AliasMap,
str_pool: &'a StrPool,
}
impl Minimizer<'_> {
fn remove_unit_reductions(&mut self) {
let mut aliased_symbols = HashSet::new();
for variable in &self.syntax_grammar.variables {
for production in &variable.productions {
for step in &production.steps {
if step.alias.is_some() {
aliased_symbols.insert(step.symbol);
let mut aliased_symbols = FxHashSet::default();
for i in 0..self.syntax_grammar.variables.len() {
for prod_id in self.syntax_grammar.variable_prod_ids(i) {
for step in self.syntax_grammar.production(prod_id).steps {
if step.alias().is_some() {
aliased_symbols.insert(step.symbol());
}
}
}
}
let mut unit_reduction_symbols_by_state = HashMap::new();
let mut unit_reduction_symbols_by_state = FxHashMap::default();
for (i, state) in self.parse_table.states.iter().enumerate() {
if state.has_eof_gated_reduce {
continue;
}
let mut only_unit_reductions = true;
let mut unit_reduction_symbol = None;
for (_, entry) in &state.terminal_entries {
for action in &entry.actions {
for (_, id) in &state.terminal_entries {
for action in self.parse_table.action_lists.get(*id) {
match action {
ParseAction::ShiftExtra => continue,
ParseAction::Reduce {
@ -78,7 +177,7 @@ impl Minimizer<'_> {
&& !self.syntax_grammar.supertype_symbols.contains(symbol)
&& !self.syntax_grammar.extra_symbols.contains(symbol)
&& !aliased_symbols.contains(symbol)
&& self.syntax_grammar.variables[symbol.index].kind
&& self.syntax_grammar.variables[symbol.index as usize].kind
!= VariableType::Named
&& (unit_reduction_symbol.is_none()
|| unit_reduction_symbol == Some(symbol)) =>
@ -100,15 +199,27 @@ impl Minimizer<'_> {
if let Some(symbol) = unit_reduction_symbol
&& only_unit_reductions
{
unit_reduction_symbols_by_state.insert(i, *symbol);
unit_reduction_symbols_by_state.insert(i as u32, *symbol);
}
}
for state in &mut self.parse_table.states {
if unit_reduction_symbols_by_state.is_empty() {
return;
}
let mut action_list_ids = FxHashMap::default();
for state_index in 0..self.parse_table.states.len() {
let mut done = false;
while !done {
done = true;
state.update_referenced_states(|other_state_id, state| {
let ParseTable {
states,
action_lists,
..
} = self.parse_table;
let state = &mut states[state_index];
state.update_nonterminal_references(|other_state_id, state| {
unit_reduction_symbols_by_state.get(&other_state_id).map_or(
other_state_id,
|symbol| {
@ -120,6 +231,32 @@ impl Minimizer<'_> {
},
)
});
for i in 0..state.terminal_entries.len() {
let old_id = state.terminal_entries.get_index(i).unwrap().1;
let mut actions = ActionList::from_slice(action_lists.get(*old_id));
let mut changed = false;
for action in &mut *actions {
// A Shift onto a unit-reduction state (one whose only action reduces a
// single `symbol`) can skip it. Shift then reduce then goto is equivalent
// to shifting straight to the the goto target for `symbol` in this case.
if let ParseAction::Shift { state: target, .. } = action
&& let Some(symbol) = unit_reduction_symbols_by_state.get(target)
&& let Some(GotoAction::Goto(new_target)) =
state.nonterminal_entries.get(symbol)
&& *new_target != *target
{
*target = *new_target;
changed = true;
done = false;
}
}
if changed {
let index = action_lists.intern(&mut action_list_ids, actions);
*state.terminal_entries.get_index_mut(i).unwrap().1 =
ActionListId::new(index, old_id.reusable());
}
}
}
}
}
@ -135,26 +272,144 @@ impl Minimizer<'_> {
// Initially group the states by their parse item set core.
let mut group_ids_by_state_id = Vec::with_capacity(self.parse_table.states.len());
let mut state_ids_by_group_id = vec![Vec::<ParseStateId>::new(); core_count];
// Pre-allocate for the maximum possible number of groups (one per state) to
// avoid reallocs as split_state_id_groups pushes new groups.
let mut state_ids_by_group_id = Vec::with_capacity(self.parse_table.states.len());
state_ids_by_group_id.resize(core_count as usize, Vec::new());
for (i, state) in self.parse_table.states.iter().enumerate() {
state_ids_by_group_id[state.core_id].push(i);
state_ids_by_group_id[state.core_id as usize].push(i as u32);
group_ids_by_state_id.push(state.core_id);
}
// Precompute sorted terminal entry references for merge-join in states_conflict.
// entry_maps[state_id][i] = (symbol_key, action_list_id). Keys are packed u64s
// (symbol_key) for easy comparison.
let entry_maps = self
.parse_table
.states
.iter()
.map(|state| {
let mut entries = state
.terminal_entries
.iter()
.map(|(sym, id)| (SymbolKey::new(*sym), *id))
.collect::<Vec<(SymbolKey, ActionListId)>>();
entries.sort_unstable_by_key(|&(key, _)| key);
entries
})
.collect::<Vec<_>>();
// Precompute word-aligned bitsets so `token_conflicts` can test a candidate
// token against a whole state's terminals.
// - per state: which terminal indices have entries
// - per token: which terminal indices it lexically conflicts with
// - the keyword set as bits
let n_terminals = self.lexical_grammar.variables.len();
let row_words = n_terminals.div_ceil(64);
let set = |bits: &mut [u64], index: usize| bits[index / 64] |= 1 << (index % 64);
let mut state_terminals = vec![0u64; self.parse_table.states.len() * row_words];
for (s, state) in self.parse_table.states.iter().enumerate() {
let base = s * row_words;
let row = &mut state_terminals[base..base + row_words];
for symbol in state.terminal_entries.keys() {
if symbol.is_terminal() {
set(row, symbol.index as usize);
}
}
}
let mut conflict_rows = vec![0u64; n_terminals * row_words];
for i in 0..n_terminals {
let base = i * row_words;
let row = &mut conflict_rows[base..base + row_words];
for j in 0..n_terminals {
if self.token_conflict_map.does_conflict(i, j) {
set(row, j);
}
}
}
let mut keywords = vec![0u64; row_words];
for symbol in self.keywords.iter() {
if symbol.is_terminal() {
set(&mut keywords, symbol.index as usize);
}
}
let mut internal_external = vec![0u64; row_words];
for external in &self.syntax_grammar.external_tokens {
if let Some(token) = external.corresponding_internal_token {
set(&mut internal_external, token.index as usize);
}
}
let bits = ConflictBits {
row_words,
state_terminals,
conflict_rows,
keywords,
internal_external,
};
split_state_id_groups(
&self.parse_table.states,
&mut state_ids_by_group_id,
&mut group_ids_by_state_id,
0,
|left, right, groups| self.states_conflict(left, right, groups),
|left, right, groups| self.states_conflict(left, right, groups, &entry_maps, &bits),
);
// Precompute per-state sorted shift actions and nonterminal goto actions.
// State actions are stable across loop iterations; only group assignments change.
// Keys are packed u64s (symbol_key) for single-instruction comparison.
let shift_maps = self
.parse_table
.states
.iter()
.map(|state| {
let mut shifts = state
.terminal_entries
.iter()
.filter_map(|(sym, entry)| {
let action = self.parse_table.action_lists.get(*entry).last()?;
if let ParseAction::Shift { state: s, .. } = action {
Some((SymbolKey::new(*sym), *s))
} else {
None
}
})
.collect::<Vec<(SymbolKey, ParseStateId)>>();
shifts.sort_unstable_by_key(|&(key, _)| key);
shifts
})
.collect::<Vec<_>>();
// Store only the symbol index: all nonterminal entries share the same kind,
// so index alone is sufficient for sorting and comparison.
let nonterminal_maps = self
.parse_table
.states
.iter()
.map(|state| {
let mut entries = state
.nonterminal_entries
.iter()
.map(|(sym, action)| (sym.index, *action))
.collect::<Vec<(NonterminalIndex, GotoAction)>>();
entries.sort_unstable_by_key(|&(idx, _)| idx);
entries
})
.collect::<Vec<_>>();
while split_state_id_groups(
&self.parse_table.states,
&mut state_ids_by_group_id,
&mut group_ids_by_state_id,
0,
|left, right, groups| self.state_successors_differ(left, right, groups),
|left, right, groups| {
self.state_successors_differ(left, right, groups, &shift_maps, &nonterminal_maps)
},
) {}
let error_group_index = state_ids_by_group_id
@ -172,13 +427,14 @@ impl Minimizer<'_> {
let mut new_states = Vec::with_capacity(state_ids_by_group_id.len());
for state_ids in &state_ids_by_group_id {
// Initialize the new state based on the first old state in the group.
let mut parse_state = mem::take(&mut self.parse_table.states[state_ids[0]]);
let mut parse_state = mem::take(&mut self.parse_table.states[state_ids[0] as usize]);
// Extend the new state with all of the actions from the other old states
// in the group.
for state_id in &state_ids[1..] {
let other_parse_state = mem::take(&mut self.parse_table.states[*state_id]);
let other_parse_state = mem::take(&mut self.parse_table.states[*state_id as usize]);
parse_state.has_eof_gated_reduce |= other_parse_state.has_eof_gated_reduce;
parse_state
.terminal_entries
.extend(other_parse_state.terminal_entries);
@ -188,50 +444,88 @@ impl Minimizer<'_> {
parse_state
.reserved_words
.insert_all(&other_parse_state.reserved_words);
for symbol in parse_state.terminal_entries.keys() {
for &symbol in parse_state.terminal_entries.keys() {
parse_state.reserved_words.remove(symbol);
}
}
// Update the new state's outgoing references using the new grouping.
parse_state.update_referenced_states(|state_id, _| group_ids_by_state_id[state_id]);
parse_state.update_nonterminal_references(|state_id, _| {
group_ids_by_state_id[state_id as usize]
});
new_states.push(parse_state);
}
self.parse_table.states = new_states;
self.parse_table
.remap_terminal_references(|state_id| group_ids_by_state_id[state_id as usize]);
}
fn states_conflict(
&self,
left_state: &ParseState,
right_state: &ParseState,
state1: &ParseState,
state2: &ParseState,
group_ids_by_state_id: &[ParseStateId],
entry_maps: &[Vec<(SymbolKey, ActionListId)>],
bits: &ConflictBits,
) -> bool {
for (token, left_entry) in &left_state.terminal_entries {
if let Some(right_entry) = right_state.terminal_entries.get(token) {
if self.entries_conflict(
left_state.id,
right_state.id,
token,
left_entry,
right_entry,
group_ids_by_state_id,
) {
return true;
let entries1 = &entry_maps[state1.id as usize];
let entries2 = &entry_maps[state2.id as usize];
let len1 = entries1.len();
let len2 = entries2.len();
let mut i = 0;
let mut j = 0;
while i < len1 || j < len2 {
// SAFETY: each branch only accesses entries1[i] when i < len1
// and entries2[j] when j < len2, both of which hold by construction.
let ord = if i < len1 && j < len2 {
unsafe { entries1.get_unchecked(i) }
.0
.cmp(&unsafe { entries2.get_unchecked(j) }.0)
} else if i < len1 {
Ordering::Less
} else {
Ordering::Greater
};
match ord {
Ordering::Equal => {
// SAFETY: Equal is only reachable when i < len1 && j < len2.
let e1 = unsafe { entries1.get_unchecked(i) };
let e2 = unsafe { entries2.get_unchecked(j) };
let token = e1.0.symbol();
if self.entries_conflict(
state1.id,
state2.id,
token,
e1.1,
e2.1,
group_ids_by_state_id,
) {
return true;
}
i += 1;
j += 1;
}
Ordering::Less => {
// SAFETY: Less is only reachable when i < len1.
let e1 = unsafe { entries1.get_unchecked(i) };
let token = e1.0.symbol();
if self.token_conflicts(state1.id, state2.id, state2, bits, token) {
return true;
}
i += 1;
}
Ordering::Greater => {
// SAFETY: Greater is only reachable when j < len2.
let e2 = unsafe { entries2.get_unchecked(j) };
let token = e2.0.symbol();
if self.token_conflicts(state1.id, state2.id, state1, bits, token) {
return true;
}
j += 1;
}
} else if self.token_conflicts(left_state.id, right_state.id, right_state, *token) {
return true;
}
}
for token in right_state.terminal_entries.keys() {
if !left_state.terminal_entries.contains_key(token)
&& self.token_conflicts(left_state.id, right_state.id, left_state, *token)
{
return true;
}
}
false
}
@ -240,44 +534,70 @@ impl Minimizer<'_> {
state1: &ParseState,
state2: &ParseState,
group_ids_by_state_id: &[ParseStateId],
shift_maps: &[Vec<(SymbolKey, ParseStateId)>],
nonterminal_maps: &[Vec<(NonterminalIndex, GotoAction)>],
) -> bool {
for (token, entry1) in &state1.terminal_entries {
if let ParseAction::Shift { state: s1, .. } = entry1.actions.last().unwrap()
&& let Some(entry2) = state2.terminal_entries.get(token)
&& let ParseAction::Shift { state: s2, .. } = entry2.actions.last().unwrap()
{
let group1 = group_ids_by_state_id[*s1];
let group2 = group_ids_by_state_id[*s2];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.symbol_name(token),
);
return true;
let shifts1 = &shift_maps[state1.id as usize];
let shifts2 = &shift_maps[state2.id as usize];
let mut i = 0;
let mut j = 0;
while i < shifts1.len() && j < shifts2.len() {
// SAFETY: loop condition ensures i < shifts1.len() and j < shifts2.len().
let (k1, s1) = *unsafe { shifts1.get_unchecked(i) };
let (k2, s2) = *unsafe { shifts2.get_unchecked(j) };
match k1.cmp(&k2) {
Ordering::Less => i += 1,
Ordering::Greater => j += 1,
Ordering::Equal => {
let group1 = group_ids_by_state_id[s1 as usize];
let group2 = group_ids_by_state_id[s2 as usize];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.symbol_name(k1.symbol()),
);
return true;
}
i += 1;
j += 1;
}
}
}
for (symbol, s1) in &state1.nonterminal_entries {
if let Some(s2) = state2.nonterminal_entries.get(symbol) {
match (s1, s2) {
(GotoAction::ShiftExtra, GotoAction::ShiftExtra) => {}
(GotoAction::Goto(s1), GotoAction::Goto(s2)) => {
let group1 = group_ids_by_state_id[*s1];
let group2 = group_ids_by_state_id[*s2];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.symbol_name(symbol),
);
return true;
let nonterms1 = &nonterminal_maps[state1.id as usize];
let nonterms2 = &nonterminal_maps[state2.id as usize];
let mut i = 0;
let mut j = 0;
while i < nonterms1.len() && j < nonterms2.len() {
// SAFETY: loop condition ensures i < nonterms1.len() and j < nonterms2.len().
let (idx1, s1) = *unsafe { nonterms1.get_unchecked(i) };
let (idx2, s2) = *unsafe { nonterms2.get_unchecked(j) };
match idx1.cmp(&idx2) {
Ordering::Less => i += 1,
Ordering::Greater => j += 1,
Ordering::Equal => {
match (s1, s2) {
(GotoAction::ShiftExtra, GotoAction::ShiftExtra) => {}
(GotoAction::Goto(s1), GotoAction::Goto(s2)) => {
let group1 = group_ids_by_state_id[s1 as usize];
let group2 = group_ids_by_state_id[s2 as usize];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.str_pool
.resolve(self.syntax_grammar.variables[idx1 as usize].name),
);
return true;
}
}
_ => return true,
}
_ => return true,
i += 1;
j += 1;
}
}
}
@ -289,14 +609,17 @@ impl Minimizer<'_> {
&self,
state_id1: ParseStateId,
state_id2: ParseStateId,
token: &Symbol,
entry1: &ParseTableEntry,
entry2: &ParseTableEntry,
token: Symbol,
id1: ActionListId,
id2: ActionListId,
group_ids_by_state_id: &[ParseStateId],
) -> bool {
// To be compatible, entries need to have the same actions.
let actions1 = &entry1.actions;
let actions2 = &entry2.actions;
if id1.index() == id2.index() {
return false;
}
let actions1 = self.parse_table.action_lists.get(id1);
let actions2 = self.parse_table.action_lists.get(id2);
if actions1.len() != actions2.len() {
debug!(
"split states {state_id1} {state_id2} - differing action counts for token {}",
@ -318,8 +641,8 @@ impl Minimizer<'_> {
},
) = (action1, action2)
{
let group1 = group_ids_by_state_id[*s1];
let group2 = group_ids_by_state_id[*s2];
let group1 = group_ids_by_state_id[*s1 as usize];
let group2 = group_ids_by_state_id[*s2 as usize];
if group1 == group2 && is_repetition1 == is_repetition2 {
continue;
}
@ -346,10 +669,11 @@ impl Minimizer<'_> {
left_id: ParseStateId,
right_id: ParseStateId,
right_state: &ParseState,
bits: &ConflictBits,
new_token: Symbol,
) -> bool {
if new_token == Symbol::end_of_nonterminal_extra() {
debug!("split states {left_id} {right_id} - end of non-terminal extra",);
debug!("split states {left_id} {right_id} - end of non-terminal extra");
return true;
}
@ -358,52 +682,58 @@ impl Minimizer<'_> {
if new_token.is_external() {
debug!(
"split states {left_id} {right_id} - external token {}",
self.symbol_name(&new_token),
self.symbol_name(new_token),
);
return true;
}
if right_state.reserved_words.contains(&new_token) {
if right_state.reserved_words.contains(new_token) {
return false;
}
// Do not add tokens which are both internal and external. Their validity could
// influence the behavior of the external scanner.
if self
.syntax_grammar
.external_tokens
.iter()
.any(|external| external.corresponding_internal_token == Some(new_token))
// influence the behavior of the external scanner. `bits.internal_external` is
// indexed by terminal index only, so gate on the kind
if new_token.is_terminal()
&& bits.internal_external[new_token.index as usize / 64]
& (1 << (new_token.index as usize % 64))
!= 0
{
debug!(
"split states {left_id} {right_id} - internal/external token {}",
self.symbol_name(&new_token),
self.symbol_name(new_token),
);
return true;
}
// Do not add a token if it conflicts with an existing token.
for token in right_state.terminal_entries.keys().copied() {
if !token.is_terminal() {
continue;
}
if self.syntax_grammar.word_token == Some(token) && self.keywords.contains(&new_token) {
continue;
}
if self.syntax_grammar.word_token == Some(new_token) && self.keywords.contains(&token) {
continue;
}
if self
.token_conflict_map
.does_conflict(new_token.index, token.index)
let word_token = self.syntax_grammar.word_token;
let new_token_is_word = word_token == Some(new_token);
let new_token_is_keyword = word_token.is_some() && self.keywords.contains(new_token);
// Do not add a token if it conflicts with an existing token. Test the candidate's
// conflict row against the state's terminal bits, masking out the word/keyword
// exemptions.
let row = bits.get_conflict_row(new_token.index as usize);
let right_terminal_bits = bits.get_state_row(right_state.id as usize);
for (w, &row_word) in row.iter().enumerate() {
let mut candidates = right_terminal_bits[w] & row_word;
if new_token_is_keyword
&& let Some(word) = word_token
&& word.index as usize / 64 == w
{
candidates &= !(1u64 << (word.index as usize % 64));
}
if new_token_is_word {
candidates &= !bits.keywords[w];
}
if candidates != 0 {
debug!(
"split states {} {} - token {} conflicts with {}",
left_id,
right_id,
self.symbol_name(&new_token),
self.symbol_name(&token),
self.symbol_name(new_token),
self.symbol_name(Symbol::terminal(
w * 64 + candidates.trailing_zeros() as usize
)),
);
return true;
}
@ -412,13 +742,17 @@ impl Minimizer<'_> {
false
}
fn symbol_name(&self, symbol: &Symbol) -> &String {
fn symbol_name(&self, symbol: Symbol) -> &str {
let symbol_index = symbol.index as usize;
if symbol.is_non_terminal() {
&self.syntax_grammar.variables[symbol.index].name
self.str_pool
.resolve(self.syntax_grammar.variables[symbol_index].name)
} else if symbol.is_external() {
&self.syntax_grammar.external_tokens[symbol.index].name
self.str_pool
.resolve(self.syntax_grammar.external_tokens[symbol_index].name)
} else {
&self.lexical_grammar.variables[symbol.index].name
self.str_pool
.resolve(self.lexical_grammar.variables[symbol_index].name)
}
}
@ -429,14 +763,14 @@ impl Minimizer<'_> {
state_usage_map[1] = true;
for state in &self.parse_table.states {
for referenced_state in state.referenced_states() {
state_usage_map[referenced_state] = true;
for referenced_state in state.referenced_states(&self.parse_table.action_lists) {
state_usage_map[referenced_state as usize] = true;
}
}
let mut removed_predecessor_count = 0;
let mut state_replacement_map = vec![0; self.parse_table.states.len()];
for state_id in 0..self.parse_table.states.len() {
state_replacement_map[state_id] = state_id - removed_predecessor_count;
state_replacement_map[state_id] = (state_id - removed_predecessor_count) as u32;
if !state_usage_map[state_id] {
removed_predecessor_count += 1;
}
@ -445,22 +779,24 @@ impl Minimizer<'_> {
let mut original_state_id = 0;
while state_id < self.parse_table.states.len() {
if state_usage_map[original_state_id] {
self.parse_table.states[state_id].update_referenced_states(|other_state_id, _| {
state_replacement_map[other_state_id]
});
self.parse_table.states[state_id].update_nonterminal_references(
|other_state_id, _| state_replacement_map[other_state_id as usize],
);
state_id += 1;
} else {
self.parse_table.states.remove(state_id);
}
original_state_id += 1;
}
self.parse_table
.remap_terminal_references(|state_id| state_replacement_map[state_id as usize]);
}
fn reorder_states_by_descending_size(&mut self) {
// Get a mapping of old state index -> new_state_index
let mut old_ids_by_new_id = (0..self.parse_table.states.len()).collect::<Vec<_>>();
old_ids_by_new_id.sort_unstable_by_key(|i| {
// Don't changes states 0 (the error state) or 1 (the start state).
// Don't change states 0 (the error state) or 1 (the start state).
if *i <= 1 {
return *i as i64 - 1_000_000;
}
@ -473,7 +809,7 @@ impl Minimizer<'_> {
// Get the inverse mapping
let mut new_ids_by_old_id = vec![0; old_ids_by_new_id.len()];
for (id, old_id) in old_ids_by_new_id.iter().enumerate() {
new_ids_by_old_id[*old_id] = id;
new_ids_by_old_id[*old_id] = id as u32;
}
// Reorder the parse states and update their references to reflect
@ -483,9 +819,11 @@ impl Minimizer<'_> {
.map(|old_id| {
let mut state = ParseState::default();
mem::swap(&mut state, &mut self.parse_table.states[*old_id]);
state.update_referenced_states(|id, _| new_ids_by_old_id[id]);
state.update_nonterminal_references(|id, _| new_ids_by_old_id[id as usize]);
state
})
.collect();
self.parse_table
.remap_terminal_references(|id| new_ids_by_old_id[id as usize]);
}
}

View file

@ -1,65 +1,126 @@
use std::{cmp::Ordering, collections::HashSet, fmt};
use std::cmp::Ordering;
use rustc_hash::FxHashSet;
use bitflags::bitflags;
use crate::{
build_tables::item::TokenSetDisplay,
grammars::{LexicalGrammar, SyntaxGrammar},
nfa::{CharacterSet, NfaCursor, NfaTransition},
rules::TokenSet,
strpool::StrPool,
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct TokenConflictStatus {
matches_prefix: bool,
does_match_continuation: bool,
does_match_valid_continuation: bool,
does_match_separators: bool,
matches_same_string: bool,
matches_different_string: bool,
bitflags! {
/// Per-(i,j) token conflict status, packed into a single byte.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct TokenConflictStatus: u8 {
const MATCHES_PREFIX = 1 << 0;
const DOES_MATCH_CONTINUATION = 1 << 1;
const DOES_MATCH_VALID_CONT = 1 << 2;
const DOES_MATCH_SEPARATORS = 1 << 3;
const MATCHES_SAME_STRING = 1 << 4;
const MATCHES_DIFFERENT_STRING = 1 << 5;
}
}
pub struct TokenConflictMap<'a> {
pub struct TokenConflictMap {
n: usize,
status_matrix: Vec<TokenConflictStatus>,
#[expect(dead_code, reason = "Debugging aid")]
following_tokens: Vec<TokenSet>,
#[allow(dead_code, reason = "Debugging/test aid")]
starting_chars_by_index: Vec<CharacterSet>,
#[expect(dead_code, reason = "Debugging aid")]
following_chars_by_index: Vec<CharacterSet>,
grammar: &'a LexicalGrammar,
/// Per-row bitsets for fast batch conflict checks.
/// Row `i` spans `[i * row_words .. (i+1) * row_words]`.
/// Bit `j` is set iff [`does_conflict(i, j)`](Self::does_conflict) || [`does_match_prefix(i, j)`](Self::does_match_prefix).
pub(crate) conflict_or_prefix_bits: Vec<u64>,
/// Bit `j` is set iff [`does_overlap(i, j)`](Self::does_overlap) || [`does_overlap(j, i)`](Self::does_overlap).
pub(crate) overlap_either_bits: Vec<u64>,
pub(crate) row_words: usize,
}
impl<'a> TokenConflictMap<'a> {
impl TokenConflictMap {
/// Create a token conflict map based on a lexical grammar, which describes the structure
/// of each token, and a `following_token` map, which indicates which tokens may be appear
/// of each token, and a `following_token` map, which indicates which tokens may appear
/// immediately after each other token.
///
/// This analyzes the possible kinds of overlap between each pair of tokens and stores
/// them in a matrix.
pub fn new(grammar: &'a LexicalGrammar, following_tokens: Vec<TokenSet>) -> Self {
#[must_use]
pub fn new(grammar: &LexicalGrammar, following_tokens: Vec<TokenSet>) -> Self {
let mut cursor = NfaCursor::new(&grammar.nfa, Vec::new());
let starting_chars = get_starting_chars(&mut cursor, grammar);
let following_chars = get_following_chars(&starting_chars, &following_tokens);
// Pre-compute O(1) lookup: NFA state ID->owning variable index.
// Replaces repeated O(log n) binary searches in compute_conflict_status.
let nfa_state_to_var = (0..grammar.nfa.states.len())
.map(|id| grammar.variable_index_for_nfa_state(id as u32))
.collect::<Vec<usize>>();
let n = grammar.variables.len();
let mut status_matrix = vec![TokenConflictStatus::default(); n * n];
let mut status_matrix = vec![TokenConflictStatus::empty(); n * n];
for i in 0..grammar.variables.len() {
for j in 0..i {
let status = compute_conflict_status(&mut cursor, grammar, &following_chars, i, j);
let status = compute_conflict_status(
&mut cursor,
grammar,
&following_chars,
&nfa_state_to_var,
i,
j,
);
status_matrix[matrix_index(n, i, j)] = status.0;
status_matrix[matrix_index(n, j, i)] = status.1;
}
}
TokenConflictMap {
// Precompute per-row bitsets for vectorized check_token_conflicts.
let row_words = n.div_ceil(64);
let conflict_mask = TokenConflictStatus::DOES_MATCH_VALID_CONT
| TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_SAME_STRING
| TokenConflictStatus::MATCHES_PREFIX;
let overlap_mask = TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_PREFIX
| TokenConflictStatus::MATCHES_SAME_STRING
| TokenConflictStatus::DOES_MATCH_CONTINUATION;
let mut conflict_or_prefix_bits = vec![0u64; n * row_words];
let mut overlap_either_bits = vec![0u64; n * row_words];
for i in 0..n {
let row_base = i * row_words;
for j in 0..n {
let entry_ij = status_matrix[matrix_index(n, i, j)];
if entry_ij.intersects(conflict_mask) {
conflict_or_prefix_bits[row_base + j / 64] |= 1u64 << (j % 64);
}
let entry_ji = status_matrix[matrix_index(n, j, i)];
if entry_ij.intersects(overlap_mask) || entry_ji.intersects(overlap_mask) {
overlap_either_bits[row_base + j / 64] |= 1u64 << (j % 64);
}
}
}
Self {
n,
status_matrix,
following_tokens,
starting_chars_by_index: starting_chars,
following_chars_by_index: following_chars,
grammar,
conflict_or_prefix_bits,
overlap_either_bits,
row_words,
}
}
/// Does token `i` match any strings that token `j` also matches, such that token `i`
/// is preferred over token `j`?
#[must_use]
pub fn has_same_conflict_status(&self, a: usize, b: usize, other: usize) -> bool {
let left = &self.status_matrix[matrix_index(self.n, a, other)];
let right = &self.status_matrix[matrix_index(self.n, b, other)];
@ -67,47 +128,63 @@ impl<'a> TokenConflictMap<'a> {
}
/// Does token `i` match any strings that token `j` does *not* match?
#[must_use]
pub fn does_match_different_string(&self, i: usize, j: usize) -> bool {
self.status_matrix[matrix_index(self.n, i, j)].matches_different_string
self.status_matrix[matrix_index(self.n, i, j)]
.contains(TokenConflictStatus::MATCHES_DIFFERENT_STRING)
}
/// Does token `i` match any strings that token `j` also matches, where
/// token `i` is preferred over token `j`?
#[inline]
#[must_use]
pub fn does_match_same_string(&self, i: usize, j: usize) -> bool {
self.status_matrix[matrix_index(self.n, i, j)].matches_same_string
self.status_matrix[matrix_index(self.n, i, j)]
.contains(TokenConflictStatus::MATCHES_SAME_STRING)
}
#[inline]
#[must_use]
pub fn does_conflict(&self, i: usize, j: usize) -> bool {
let entry = &self.status_matrix[matrix_index(self.n, i, j)];
entry.does_match_valid_continuation
|| entry.does_match_separators
|| entry.matches_same_string
debug_assert!(i < self.n && j < self.n, "token indices out of bounds");
// Safety: i < n and j < n imply n*i+j < n*n == status_matrix.len().
let entry = unsafe { *self.status_matrix.get_unchecked(matrix_index(self.n, i, j)) };
entry.intersects(
TokenConflictStatus::DOES_MATCH_VALID_CONT
| TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_SAME_STRING,
)
}
/// Does token `i` match any strings that are *prefixes* of strings matched by `j`?
#[inline]
#[must_use]
#[allow(dead_code)]
pub fn does_match_prefix(&self, i: usize, j: usize) -> bool {
self.status_matrix[matrix_index(self.n, i, j)].matches_prefix
self.status_matrix[matrix_index(self.n, i, j)].contains(TokenConflictStatus::MATCHES_PREFIX)
}
#[must_use]
pub fn does_match_shorter_or_longer(&self, i: usize, j: usize) -> bool {
let entry = &self.status_matrix[matrix_index(self.n, i, j)];
let reverse_entry = &self.status_matrix[matrix_index(self.n, j, i)];
(entry.does_match_valid_continuation || entry.does_match_separators)
&& !reverse_entry.does_match_separators
let entry = self.status_matrix[matrix_index(self.n, i, j)];
let reverse_entry = self.status_matrix[matrix_index(self.n, j, i)];
entry.intersects(
TokenConflictStatus::DOES_MATCH_VALID_CONT | TokenConflictStatus::DOES_MATCH_SEPARATORS,
) && !reverse_entry.contains(TokenConflictStatus::DOES_MATCH_SEPARATORS)
}
#[inline]
#[must_use]
pub fn does_overlap(&self, i: usize, j: usize) -> bool {
let status = &self.status_matrix[matrix_index(self.n, i, j)];
status.does_match_separators
|| status.matches_prefix
|| status.matches_same_string
|| status.does_match_continuation
self.status_matrix[matrix_index(self.n, i, j)].intersects(
TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_PREFIX
| TokenConflictStatus::MATCHES_SAME_STRING
| TokenConflictStatus::DOES_MATCH_CONTINUATION,
)
}
#[must_use]
pub fn prefer_token(grammar: &LexicalGrammar, left: (i32, usize), right: (i32, usize)) -> bool {
match left.0.cmp(&right.0) {
Ordering::Less => false,
@ -123,6 +200,7 @@ impl<'a> TokenConflictMap<'a> {
}
}
#[must_use]
pub fn prefer_transition(
grammar: &LexicalGrammar,
t: &NfaTransition,
@ -149,52 +227,59 @@ impl<'a> TokenConflictMap<'a> {
}
}
impl fmt::Debug for TokenConflictMap<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
#[expect(dead_code, reason = "Debugging aid")]
struct TokenConflictMapDisplay<'a>(
pub &'a TokenConflictMap,
pub &'a LexicalGrammar,
pub &'a StrPool,
);
impl std::fmt::Debug for TokenConflictMapDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "TokenConflictMap {{")?;
let syntax_grammar = SyntaxGrammar::default();
writeln!(f, " following_tokens: {{")?;
for (i, following_tokens) in self.following_tokens.iter().enumerate() {
for (i, following_tokens) in self.0.following_tokens.iter().enumerate() {
writeln!(
f,
" follow({:?}): {},",
self.grammar.variables[i].name,
TokenSetDisplay(following_tokens, &syntax_grammar, self.grammar)
self.1.variables[i].name,
TokenSetDisplay(following_tokens, &syntax_grammar, self.1, self.2)
)?;
}
writeln!(f, " }},")?;
writeln!(f, " starting_characters: {{")?;
for i in 0..self.n {
for i in 0..self.0.n {
writeln!(
f,
" {:?}: {:?},",
self.grammar.variables[i].name, self.starting_chars_by_index[i]
self.1.variables[i].name, self.0.starting_chars_by_index[i]
)?;
}
writeln!(f, " }},")?;
writeln!(f, " following_characters: {{")?;
for i in 0..self.n {
for i in 0..self.0.n {
writeln!(
f,
" {:?}: {:?},",
self.grammar.variables[i].name, self.following_chars_by_index[i]
self.1.variables[i].name, self.0.following_chars_by_index[i]
)?;
}
writeln!(f, " }},")?;
writeln!(f, " status_matrix: {{")?;
for i in 0..self.n {
writeln!(f, " {:?}: {{", self.grammar.variables[i].name)?;
for j in 0..self.n {
for i in 0..self.0.n {
writeln!(f, " {:?}: {{", self.1.variables[i].name)?;
for j in 0..self.0.n {
writeln!(
f,
" {:?}: {:?},",
self.grammar.variables[j].name,
self.status_matrix[matrix_index(self.n, i, j)]
self.1.variables[j].name,
self.0.status_matrix[matrix_index(self.0.n, i, j)]
)?;
}
writeln!(f, " }},")?;
@ -233,7 +318,7 @@ fn get_following_chars(
let mut chars = CharacterSet::empty();
for token in following_tokens.iter() {
if token.is_terminal() {
chars = chars.add(&starting_chars[token.index]);
chars = chars.add(&starting_chars[token.index as usize]);
}
}
chars
@ -241,51 +326,76 @@ fn get_following_chars(
.collect()
}
/// Hash a sorted slice of NFA state IDs to a single `u64` for use as a
/// visited-set key.
///
/// NOTE: This trades exact equality for speed. Two distinct state sets that
/// hash to the same `u64` will be treated as "already visited," potentially
/// causing the BFS to skip a state set. In practice the collision probability
/// is negligible (1 in 2^64 per comparison), but this is technically a
/// probabilistic optimization rather than an exact one.
#[inline]
fn hash_state_set(states: &[u32]) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = rustc_hash::FxHasher::default();
states.hash(&mut h);
h.finish()
}
fn compute_conflict_status(
cursor: &mut NfaCursor,
grammar: &LexicalGrammar,
following_chars: &[CharacterSet],
nfa_state_to_var: &[usize],
i: usize,
j: usize,
) -> (TokenConflictStatus, TokenConflictStatus) {
let mut visited_state_sets = HashSet::new();
let mut state_set_queue = vec![vec![
let mut visited_state_sets = FxHashSet::<u64>::default();
let mut state_set_queue = Vec::with_capacity(4);
state_set_queue.push(vec![
grammar.variables[i].start_state,
grammar.variables[j].start_state,
]];
let mut result = (
TokenConflictStatus::default(),
TokenConflictStatus::default(),
);
]);
let mut result = (TokenConflictStatus::empty(), TokenConflictStatus::empty());
while let Some(state_set) = state_set_queue.pop() {
let mut live_variable_indices = grammar.variable_indices_for_nfa_states(&state_set);
// If only one of the two tokens could possibly match from this state, then
// there is no reason to analyze any of its successors. Just record the fact
// that the token matches a string that the other token does not match.
let first_live_variable_index = live_variable_indices.next().unwrap();
if live_variable_indices.count() == 0 {
let first_live_variable_index = nfa_state_to_var[state_set[0] as usize];
if state_set
.iter()
.all(|&s| nfa_state_to_var[s as usize] == first_live_variable_index)
{
if first_live_variable_index == i {
result.0.matches_different_string = true;
result
.0
.insert(TokenConflictStatus::MATCHES_DIFFERENT_STRING);
} else {
result.1.matches_different_string = true;
result
.1
.insert(TokenConflictStatus::MATCHES_DIFFERENT_STRING);
}
continue;
}
// Don't pursue states where there's no potential for conflict.
cursor.reset(state_set);
let within_separator = cursor.transition_chars().any(|(_, sep)| sep);
// Compute lazily: most BFS states have no completions, so
// `within_separator` is never needed in those iterations.
let mut within_separator: Option<bool> = None;
// Examine each possible completed token in this state.
let mut completion = None;
for (id, precedence) in cursor.completions() {
if within_separator {
let sep = *within_separator
.get_or_insert_with(|| cursor.transition_chars().any(|(_, sep)| sep));
if sep {
if id == i {
result.0.does_match_separators = true;
result.0.insert(TokenConflictStatus::DOES_MATCH_SEPARATORS);
} else {
result.1.does_match_separators = true;
result.1.insert(TokenConflictStatus::DOES_MATCH_SEPARATORS);
}
}
@ -310,9 +420,9 @@ fn compute_conflict_status(
}
if preferred_id == i {
result.0.matches_same_string = true;
result.0.insert(TokenConflictStatus::MATCHES_SAME_STRING);
} else {
result.1.matches_same_string = true;
result.1.insert(TokenConflictStatus::MATCHES_SAME_STRING);
}
} else {
completion = Some((id, precedence));
@ -329,12 +439,18 @@ fn compute_conflict_status(
if let Some((completed_id, completed_precedence)) = completion {
let mut advanced_id = None;
let mut successor_contains_completed_id = false;
for variable_id in grammar.variable_indices_for_nfa_states(&transition.states) {
if variable_id == completed_id {
let mut prev_var = None;
for &state_id in &transition.states {
let var_id = nfa_state_to_var[state_id as usize];
if prev_var == Some(var_id) {
continue;
}
prev_var = Some(var_id);
if var_id == completed_id {
successor_contains_completed_id = true;
break;
}
advanced_id = Some(variable_id);
advanced_id = Some(var_id);
}
// Determine which action is preferred: matching the already complete
@ -345,29 +461,33 @@ fn compute_conflict_status(
&transition,
completed_id,
completed_precedence,
within_separator,
within_separator.unwrap_or(false),
) {
can_advance = true;
if advanced_id == i {
result.0.does_match_continuation = true;
result
.0
.insert(TokenConflictStatus::DOES_MATCH_CONTINUATION);
if transition.characters.does_intersect(&following_chars[j]) {
result.0.does_match_valid_continuation = true;
result.0.insert(TokenConflictStatus::DOES_MATCH_VALID_CONT);
}
} else {
result.1.does_match_continuation = true;
result
.1
.insert(TokenConflictStatus::DOES_MATCH_CONTINUATION);
if transition.characters.does_intersect(&following_chars[i]) {
result.1.does_match_valid_continuation = true;
result.1.insert(TokenConflictStatus::DOES_MATCH_VALID_CONT);
}
}
} else if completed_id == i {
result.0.matches_prefix = true;
result.0.insert(TokenConflictStatus::MATCHES_PREFIX);
} else {
result.1.matches_prefix = true;
result.1.insert(TokenConflictStatus::MATCHES_PREFIX);
}
}
}
if can_advance && visited_state_sets.insert(transition.states.clone()) {
if can_advance && visited_state_sets.insert(hash_state_set(&transition.states)) {
state_set_queue.push(transition.states);
}
}
@ -379,29 +499,36 @@ fn compute_conflict_status(
mod tests {
use super::*;
use crate::{
grammars::{Variable, VariableType},
prepare_grammar::{ExtractedLexicalGrammar, expand_tokens},
rules::{Precedence, Rule, Symbol},
grammars::VariableType,
prepare_grammar::{LexicalToken, expand_tokens},
rules::{Precedence, RulePool, Symbol},
};
#[test]
fn test_starting_characters() {
let grammar = expand_tokens(ExtractedLexicalGrammar {
separators: Vec::new(),
variables: vec![
Variable {
name: "token_0".to_string(),
kind: VariableType::Named,
rule: Rule::pattern("[a-f]1|0x\\d", ""),
},
Variable {
name: "token_1".to_string(),
kind: VariableType::Named,
rule: Rule::pattern("d*ef", ""),
},
],
})
.unwrap();
let mut pool = RulePool::default();
let empty = pool.intern("");
let t0 = {
let v = pool.intern("[a-f]1|0x\\d");
pool.pattern(v, empty)
};
let t1 = {
let v = pool.intern("d*ef");
pool.pattern(v, empty)
};
let vars = vec![
LexicalToken {
name: pool.intern("token_0"),
kind: VariableType::Named,
root: t0,
},
LexicalToken {
name: pool.intern("token_1"),
kind: VariableType::Named,
root: t1,
},
];
let grammar = expand_tokens(&mut pool, &vars, &[]).unwrap();
let token_map = TokenConflictMap::new(&grammar, Vec::new());
@ -417,29 +544,40 @@ mod tests {
#[test]
fn test_token_conflicts() {
let grammar = expand_tokens(ExtractedLexicalGrammar {
separators: Vec::new(),
variables: vec![
Variable {
name: "in".to_string(),
kind: VariableType::Named,
rule: Rule::string("in"),
},
Variable {
name: "identifier".to_string(),
kind: VariableType::Named,
rule: Rule::pattern("\\w+", ""),
},
Variable {
name: "instanceof".to_string(),
kind: VariableType::Named,
rule: Rule::string("instanceof"),
},
],
})
.unwrap();
let mut pool = RulePool::default();
let empty = pool.intern("");
let in_tok = {
let s = pool.intern("in");
pool.string(s)
};
let ident = {
let v = pool.intern("\\w+");
pool.pattern(v, empty)
};
let instanceof = {
let s = pool.intern("instanceof");
pool.string(s)
};
let vars = vec![
LexicalToken {
name: pool.intern("in"),
kind: VariableType::Named,
root: in_tok,
},
LexicalToken {
name: pool.intern("identifier"),
kind: VariableType::Named,
root: ident,
},
LexicalToken {
name: pool.intern("instanceof"),
kind: VariableType::Named,
root: instanceof,
},
];
let grammar = expand_tokens(&mut pool, &vars, &[]).unwrap();
let var = |name| index_of_var(&grammar, name);
let var = |name| index_of_var(&pool, &grammar, name);
let token_map = TokenConflictMap::new(
&grammar,
@ -456,40 +594,51 @@ mod tests {
],
);
// Given the string "in", the `in` token is preferred over the `identifier` token
// Given the string "in", the `in` token is preferrred over the `identifier` token
assert!(token_map.does_match_same_string(var("in"), var("identifier")));
assert!(!token_map.does_match_same_string(var("identifier"), var("in")));
// Depending on what character follows, the string "in" may be treated as part of an
// `identifier` token.
// `identifier` token
assert!(token_map.does_conflict(var("identifier"), var("in")));
// Depending on what character follows, the string "instanceof" may be treated as part of
// an `identifier` token.
// Depending on what character follows, the string "instanceof" may be treated as part
// of an `identifier` token
assert!(token_map.does_conflict(var("identifier"), var("instanceof")));
assert!(token_map.does_conflict(var("instanceof"), var("in")));
}
#[test]
fn test_token_conflicts_with_separators() {
let grammar = expand_tokens(ExtractedLexicalGrammar {
separators: vec![Rule::pattern("\\s", "")],
variables: vec![
Variable {
name: "x".to_string(),
kind: VariableType::Named,
rule: Rule::string("x"),
},
Variable {
name: "newline".to_string(),
kind: VariableType::Named,
rule: Rule::string("\n"),
},
],
})
.unwrap();
let mut pool = RulePool::default();
let empty = pool.intern("");
let sep = {
let v = pool.intern("\\s");
pool.pattern(v, empty)
};
let x = {
let s = pool.intern("x");
pool.string(s)
};
let newline = {
let s = pool.intern("\n");
pool.string(s)
};
let vars = vec![
LexicalToken {
name: pool.intern("x"),
kind: VariableType::Named,
root: x,
},
LexicalToken {
name: pool.intern("newline"),
kind: VariableType::Named,
root: newline,
},
];
let grammar = expand_tokens(&mut pool, &vars, &[sep]).unwrap();
let var = |name| index_of_var(&grammar, name);
let var = |name| index_of_var(&pool, &grammar, name);
let token_map = TokenConflictMap::new(&grammar, vec![TokenSet::new(); 4]);
@ -499,24 +648,36 @@ mod tests {
#[test]
fn test_token_conflicts_with_open_ended_tokens() {
let grammar = expand_tokens(ExtractedLexicalGrammar {
separators: vec![Rule::pattern("\\s", "")],
variables: vec![
Variable {
name: "x".to_string(),
kind: VariableType::Named,
rule: Rule::string("x"),
},
Variable {
name: "anything".to_string(),
kind: VariableType::Named,
rule: Rule::prec(Precedence::Integer(-1), Rule::pattern(".*", "")),
},
],
})
.unwrap();
let mut pool = RulePool::default();
let empty = pool.intern("");
let sep = {
let v = pool.intern("\\s");
pool.pattern(v, empty)
};
let x = {
let s = pool.intern("x");
pool.string(s)
};
let anything = {
let v = pool.intern(".*");
let pat = pool.pattern(v, empty);
pool.prec(Precedence::Integer(-1), pat)
};
let vars = vec![
LexicalToken {
name: pool.intern("x"),
kind: VariableType::Named,
root: x,
},
LexicalToken {
name: pool.intern("anything"),
kind: VariableType::Named,
root: anything,
},
];
let grammar = expand_tokens(&mut pool, &vars, &[sep]).unwrap();
let var = |name| index_of_var(&grammar, name);
let var = |name| index_of_var(&pool, &grammar, name);
let token_map = TokenConflictMap::new(&grammar, vec![TokenSet::new(); 4]);
@ -524,11 +685,11 @@ mod tests {
assert!(!token_map.does_match_shorter_or_longer(var("x"), var("anything")));
}
fn index_of_var(grammar: &LexicalGrammar, name: &str) -> usize {
fn index_of_var(pool: &RulePool, grammar: &LexicalGrammar, name: &str) -> usize {
grammar
.variables
.iter()
.position(|v| v.name == name)
.position(|v| pool.resolve(v.name) == name)
.unwrap()
}
}

View file

@ -1,22 +1,25 @@
#[inline]
pub fn split_state_id_groups<S>(
states: &[S],
state_ids_by_group_id: &mut Vec<Vec<usize>>,
group_ids_by_state_id: &mut [usize],
start_group_id: usize,
mut should_split: impl FnMut(&S, &S, &[usize]) -> bool,
state_ids_by_group_id: &mut Vec<Vec<u32>>,
group_ids_by_state_id: &mut [u32],
start_group_id: u32,
mut should_split: impl FnMut(&S, &S, &[u32]) -> bool,
) -> bool {
let mut result = false;
// Use a flat bitset instead of Vec::contains for O(1) membership tests.
let mut is_split = vec![false; states.len()];
let mut group_id = start_group_id;
while group_id < state_ids_by_group_id.len() {
let state_ids = &state_ids_by_group_id[group_id];
while (group_id as usize) < state_ids_by_group_id.len() {
let state_ids = &state_ids_by_group_id[group_id as usize];
let mut split_state_ids = Vec::new();
let mut i = 0;
while i < state_ids.len() {
let left_state_id = state_ids[i];
if split_state_ids.contains(&left_state_id) {
let left_state_id = state_ids[i] as usize;
if is_split[left_state_id] {
i += 1;
continue;
}
@ -27,15 +30,16 @@ pub fn split_state_id_groups<S>(
// this state.
let mut j = i + 1;
while j < state_ids.len() {
let right_state_id = state_ids[j];
if split_state_ids.contains(&right_state_id) {
let right_state_id = state_ids[j] as usize;
if is_split[right_state_id] {
j += 1;
continue;
}
let right_state = &states[right_state_id];
if should_split(left_state, right_state, group_ids_by_state_id) {
split_state_ids.push(right_state_id);
split_state_ids.push(right_state_id as u32);
is_split[right_state_id] = true;
}
j += 1;
@ -47,11 +51,12 @@ pub fn split_state_id_groups<S>(
// If any states were removed from the group, add them all as a new group.
if !split_state_ids.is_empty() {
result = true;
state_ids_by_group_id[group_id].retain(|i| !split_state_ids.contains(i));
state_ids_by_group_id[group_id as usize].retain(|i| !is_split[*i as usize]);
let new_group_id = state_ids_by_group_id.len();
let new_group_id = state_ids_by_group_id.len() as u32;
for id in &split_state_ids {
group_ids_by_state_id[*id] = new_group_id;
group_ids_by_state_id[*id as usize] = new_group_id;
is_split[*id as usize] = false;
}
state_ids_by_group_id.push(split_state_ids);

Some files were not shown because too many files have changed in this diff Show more