Skip to content

fix: grammar, scanner and query fixes - #77

Merged
dpezto merged 20 commits into
mainfrom
fix/codex-audit
Oct 10, 2026
Merged

dpezto merged 20 commits into
mainfrom
fix/codex-audit

Conversation

@dpezto

@dpezto dpezto commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Grammar, scanner, query and packaging fixes, each checked against gnuplot 6.0.5. Each commit fixes one issue, carries its corpus or highlight test and its regenerated parser.

  • fix: group operators the way gnuplot evaluates them
  • fix(scanner): leave assignments on the next line alone
  • fix: accept the else branch of an old-style if
  • fix(scanner): read whole datablock markers
  • fix(scanner): close datablocks where gnuplot does
  • fix(scanner): open the argument gate on a cardinality
  • fix: read a doubled single quote as a literal quote
  • fix: accept a macro where a value is expected
  • fix: accept the generic errorbars and errorlines plot styles
  • fix: accept abbreviations of every
  • fix: accept a single index on a string literal
  • fix(scanner): read p[c?a:b]=x as an assignment
  • fix: end an unterminated string at the end of the line
  • fix: register the advertised file extensions
  • fix(queries): inject the shell command of system without its quotes
  • fix(queries): stop injecting the variable of system as a command
  • fix(queries): mark columnheader(N) as a builtin function
  • fix(bindings): require a node runtime that loads ABI 15 parsers
  • docs: record grammar gaps that remain open

Operator precedence now follows gnuplot's parser: all pairs of the 18 binary operators were compared against gnuplot, from 38 mismatches to none. set style data now uses the shared plot_style rule, so its tree is (style (plot_style)). The node binding requires tree-sitter ^0.25 (ABI 15) and its load test is enabled in CI.

Parser: 12088 to 12100 states, parser.c +307 KB (+1.1%). tree-sitter test passes (259) on every commit, the oracle check passes, and regeneration leaves src/ unchanged. Remaining gaps are recorded in docs/deferred.md.

dpezto added 19 commits October 10, 2026 10:02
Several expressions parsed without errors but with the wrong tree.
`2**3**2` grouped to the left, though gnuplot evaluates it as
`2**(3**2)`. Factorial bound looser than `**`, `~` sat below the
additive operators, `^` shared a level with `|`, the comparison
operators sat below the bitwise ones, `==` shared a level with `<`, `.`
bound looser than `+` and `-`, and nested ternaries grouped to the left.

The precedence table now follows the levels of gnuplot's own expression
parser, which are the C levels plus `**` and factorial. Every pair of
binary operators was checked against gnuplot 6.0.5. Equality and
relational operators now need separate tokens.

STATE_COUNT 12088 to 12104, TOKEN_COUNT 500 to 501, parser.c +231 KB.
Corpus 247.
Option bodies and plot elements have no line boundary in the grammar, so
a keyword name at the start of the next line could still join them.
`set samples 5` followed by `offset=3` read `offset` as one more samples
sub-keyword and failed at `=`, and `lw=3` after a plot command became a
line width of the plot element.

Command keywords already declined when assignment syntax follows. The
plot style, style attribute and option body tables now do the same.
`if (0) print 1; else print 2` is the pre-5.0 form of if/else, still
accepted by gnuplot 6. The grammar only allowed a braced else, so the
old form failed at `else` and the else branch became an unconditional
statement.

The old-style body now takes an optional unbraced `else` statement. As
in gnuplot, a nested old-style `if` leaves the `else` to the outer one,
and an `else if` chain nests. The braced else moved into the braced
branch, since gnuplot rejects it after an old-style body.

STATE_COUNT 12104 to 12100. Corpus 249.
The opening marker of a datablock stopped at the first character that
was not a letter, so `$D << E1` opened a block ending at `E` and left
`1` behind, and `END_DATA` failed the same way. Markers longer than 99
characters were cut short and reported as matched.

Markers are now read as whole identifiers, with digits and underscores,
and the closing marker is compared the same way. The storage grows to
the 1024 bytes the serialized state allows, and a marker that does not
fit fails the scan instead of being truncated.
gnuplot ends a datablock at the first line that starts with the marker
in column zero and ignores the rest of that line. The scanner matched
the marker anywhere after skipped whitespace and required a word
boundary after it, so `1 EOD` closed the block early and `EODxxx` never
closed it.

The closing marker now has to start in column zero, is compared as a
prefix of the line, and takes the rest of the line with it.
`print |A|` and `set samples |A|+10` are valid, but `|` was not among
the characters the same-line gate accepts as the start of a value, so
the command ended at its keyword and the expression failed to parse.
gnuplot reads `''` inside a single-quoted string as one quote character,
so `print 'a''b'` prints `a'b`. The grammar closed the string at the
first quote and failed on the rest.

The pair now parses as an escape_sequence. As in gnuplot, it only counts
after other text in the string: at the start, or right after another
pair, the first quote still closes the string.

STATE_COUNT 12100 to 12101, TOKEN_COUNT 501 to 502.
gnuplot expands `@name` before it parses the line, so a macro can stand
for an operand: `print @s`, `set xrange [@s:5]`, `plot @f with lines`.
The grammar only accepted a macro as a whole statement and failed on
every other use.

A macro is now also an expression atom and a plot data source.
STATE_COUNT unchanged, parser.c +61 KB.
`plot ... with errorbars` and its abbreviation `with e` are valid and
plot y error bars, and `errorlines` (from `errorl`) does the same with
lines. Only `set style data` knew these names, so every plot command
using them failed.

They are now rows of the plot style table, with gnuplot's minimum
lengths, and `set style data` uses the shared plot_style rule, so its
tree now has a plot_style node like the other style names.
keywords.json is unchanged.

STATE_COUNT 12101 to 12100, TOKEN_COUNT 502 to 500.
gnuplot accepts `every` down to `ev` in plot, splot, stats and fit, but
the grammar only took the full word. keywords.json records the new
minimum.
`print "abc"[2]` prints `b`, but subscript required a colon, so a
literal string with a single index failed to parse. subscript now has a
single-index form for string literals. On an identifier the same shape
stays an array element.

STATE_COUNT unchanged, parser.c +3 KB.
A word that is also a command abbreviation (`p` for plot) followed by
brackets is either an array element assignment or a command with a
range. The scanner took any colon inside the brackets as range syntax,
so an index written as a ternary made `p[1?1:2]=3` a plot command.
It now decides on the `=` after the closing bracket alone.
gnuplot accepts a string with no closing quote and ends it at the end of
the line, so `print "a` prints `a`. The grammar failed on it.

A zero-width external token now closes a string at a newline or at the
end of the file. It is only valid inside a string, so it costs no
states. Making the closing quote optional in the grammar was measured first and
grew the parser to 41459 states.

TOKEN_COUNT 500 to 501, parser.c +13 KB.
The README lists .gnuplot, .gp, .plt, .plot and .gnu, but tree-sitter.json
only registered .gnuplot, so the tree-sitter CLI found no language for
the other four. The parser does not change.
`system "printf hi"` runs `printf hi`, but the whole string literal was
injected as bash, quotes included, so the shell parse saw one quoted
word instead of a command and an argument. The same held for
`system("...")`. Both now offset the injected range past the quotes.

`! "cmd"` keeps the quotes: `!` hands the rest of the line to the shell
as written.

test/highlight-check.lua now also checks injected regions on small
snippets.
In `system cmd`, `cmd` is a variable whose value is the shell command,
but the identifier itself was injected as bash. Identifier injection is
now limited to `!`, where a bare word is the command (`! pwd`).
The builtin list had `columnhead` but not its full spelling, so a call
written `columnheader(1)` was captured only as a function call. The
`(columnheader)` pattern covers the bare `title columnheader` form, not
calls.
The generated parser is ABI 15, but the declared `tree-sitter` peer was
^0.21, whose runtime stops at ABI 14, so `setLanguage` threw in
initializeLanguageNodeClasses. The peer is now ^0.25. The
peerDependenciesMeta key was misspelled `tree_sitter`, so the peer was
never optional; it now is, and the runtime is also a devDependency so
the binding test has it.

CI runs the node binding test again.
Old-style if bodies with more than one command, raw datablock and inline
plot data, raw `!` shell text, backtick substitution, function blocks,
and a postfix factorial that reads across a newline.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 19:24
@github-actions github-actions Bot added the size/xl huge — split or scrutinize label Oct 10, 2026
@codecov

codecov Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.29730% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/scanner.c 97.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to run its full agentic suite in this review.

Copilot review overview

4 open findings
What changed in this PR

This PR aligns the gnuplot Tree-sitter grammar/scanner and Neovim query behavior more closely with gnuplot 6.0.5, and updates packaging/CI to match ABI 15 requirements.

Changes:

  • Adjust expression/operator precedence, string handling, old-style if ... else, plot styles, and macro/value parsing in the grammar/scanner.
  • Refine Neovim injection/highlight queries (notably system / ! bash injections) and add targeted query checks in the test harness.
  • Update registry metadata (file extensions) and Node binding/CI dependencies for tree-sitter ABI 15.
File Description
tree-sitter.json Registers additional gnuplot file extensions for editor integration.
src/​scanner.c Updates datablock scanning, assignment context detection, and unterminated string handling.
grammar.js /​ src/​grammar.json Brings grammar behavior in line with gnuplot parsing rules (precedence, if/else, strings, macros, subscripts).
queries/​injections.scm Changes bash injection extraction rules for system and !.
queries/​highlights.scm Updates builtin function highlight set (e.g., columnheader).
test/​highlight-check.lua Adds pinned injection/capture checks using Neovim runtime.
test/​corpus/​*.txt Adds/updates corpus cases to lock in new parsing behavior.
package.json Bumps Node peer dependency expectation to tree-sitter ^0.25 (ABI 15).
.github/​workflows/​ci.yml Re-enables Node binding smoke tests in CI.
docs/​deferred.md Records remaining known grammar gaps.

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread queries/injections.scm
Comment on lines 6 to 11
(cmd_system
"cmd" @_head
(string_literal) @injection.content
(#eq? @_head "system")
(#offset! @injection.content 0 1 0 -1)
(#set! injection.language "bash"))
Comment thread src/scanner.c
Comment on lines 916 to 932
@@ -905,13 +923,12 @@ static bool is_assignment_context(TSLexer* lexer) {
depth++;
else if (c == ']')
depth--;
else if (c == ':' && depth == 1)
has_colon = true;
}
if (has_colon)
return false;
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') consume(lexer);
return lexer->lookahead == '=';
if (lexer->lookahead != '=')
return false;
consume(lexer);
return lexer->lookahead != '='; // '=' but not '=='
}
Comment thread src/scanner.c
Comment on lines +7 to +9
// Datablock marker capacity, NUL included. Bounded by tree-sitter's 1024-byte
// serialization buffer, since the open marker is the serialized state.
#define MAX_WORD_LENGTH 1024
Comment thread test/highlight-check.lua
Comment on lines +80 to +84
local range = vim.treesitter.get_range(node, text, metadata[id])
out[#out + 1] = {
lang = metadata["injection.language"],
text = text:sub(range[3] + 1, range[6]),
}
The factorial `!` was an internal token, so after `print 3` a `! ls` on the next line was read as `3!` followed by an error. The scanner now emits it only when it sits on the same logical line, leaving a `!` after a newline to the shell command. Two conflicts that only the old token needed are dropped.
@dpezto
dpezto merged commit 49082c8 into main Oct 10, 2026
13 checks passed
@dpezto
dpezto deleted the fix/codex-audit branch October 10, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl huge — split or scrutinize

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants