Cart grammar: tolerate leading blank lines before the magic header

`extras: $ => []` in the cart grammar made the parser fail at byte 0
on any whitespace-only or empty line before `pico-8 cartridge //...`.
Real PICO-8 carts always start with the header at byte 0 so this
rarely surfaced in production, but it ( a ) broke the `tree-sitter
test` corpus harness, which prepends a newline to each fixture, and
( b ) would mis-flag a hand-edited cart that gained an accidental
blank line up top.

Fix: prefix the `cartridge` rule with `repeat($._blank_line)` and add
a hidden `_blank_line` token matching `[ \t]*\n`. Junk content before
the header ( a non-blank, non-magic line ) still produces an ERROR.

Restores the test corpus that was dropped in v0.1 ( previously failing
on this same edge case ) and adds a fixture for the unknown_section
fallback while the corpus is being rebuilt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 12:56:59 -07:00
parent 445aac4e30
commit 7557a34c89
5 changed files with 1190 additions and 894 deletions
+9 -4
View File
@@ -149,10 +149,15 @@ dev extension to pick up changes.
### Tests ### Tests
Sample carts live under `examples/`. Use `tree-sitter parse` directly for Sample carts live under `examples/`; parse them directly with
verification — corpus-style `tree-sitter test` is not currently set up `tree-sitter parse <file>` for ad-hoc checks.
because the cart grammar's strict `extras: []` doesn't tolerate the
leading newline that the test harness prepends to each fixture. The cart grammar has a corpus under `grammars/p8-cart/test/corpus/` —
run `( cd grammars/p8-cart && npx tree-sitter test )`. The corpus
covers the empty-section skeleton, normal Lua content, the case where
a Lua identifier resembles a section marker ( e.g. `local __foo__ = 1`
must remain a `line`, not be re-tokenized as a marker ), and the
fallback `unknown_section` rule.
## Roadmap ## Roadmap
+16 -1
View File
@@ -18,16 +18,31 @@
module.exports = grammar({ module.exports = grammar({
name: 'p8_cart', name: 'p8_cart',
// Whitespace is significant inside hex sections, so we don't skip it. // Whitespace is significant inside hex sections, so we don't skip it
// globally. Tolerance for stray leading blanks before the magic header
// is added explicitly via the `repeat($._blank_line)` at the top of
// `cartridge` ( see below ).
extras: $ => [], extras: $ => [],
rules: { rules: {
cartridge: $ => seq( cartridge: $ => seq(
// Tolerate stray whitespace / blank lines before the magic header.
// Real PICO-8 carts begin with the header on byte 0, but allowing
// a leading run of blanks ( a ) lets the `tree-sitter test` corpus
// framework, which prepends a newline to each fixture, run cleanly
// and ( b ) keeps the parser robust against a hand-edited cart that
// gained an accidental blank line up top.
repeat($._blank_line),
optional($.header), optional($.header),
optional($.version), optional($.version),
repeat($.section), repeat($.section),
), ),
// A line that has no content other than horizontal whitespace and a
// newline. Hidden ( underscore prefix ) so it does not appear in the
// syntax tree.
_blank_line: $ => token(/[ \t]*\n/),
header: $ => /pico-8 cartridge \/\/[^\n]*\n/, header: $ => /pico-8 cartridge \/\/[^\n]*\n/,
version: $ => /version[ \t]+\d+\n/, version: $ => /version[ \t]+\d+\n/,
+14
View File
@@ -5,6 +5,13 @@
"cartridge": { "cartridge": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_blank_line"
}
},
{ {
"type": "CHOICE", "type": "CHOICE",
"members": [ "members": [
@@ -38,6 +45,13 @@
} }
] ]
}, },
"_blank_line": {
"type": "TOKEN",
"content": {
"type": "PATTERN",
"value": "[ \\t]*\\n"
}
},
"header": { "header": {
"type": "PATTERN", "type": "PATTERN",
"value": "pico-8 cartridge \\/\\/[^\\n]*\\n" "value": "pico-8 cartridge \\/\\/[^\\n]*\\n"
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
==================
empty cart skeleton
==================
pico-8 cartridge // http://www.pico-8.com
version 42
__lua__
__gfx__
__map__
__sfx__
__music__
---
(cartridge
(header)
(version)
(section (lua_section (lua_marker)))
(section (gfx_section (gfx_marker)))
(section (map_section (map_marker)))
(section (sfx_section (sfx_marker)))
(section (music_section (music_marker))))
==================
cart with lua content
==================
pico-8 cartridge // http://www.pico-8.com
version 42
__lua__
function _draw()
cls()
end
__gfx__
00000000
---
(cartridge
(header)
(version)
(section
(lua_section
(lua_marker)
(lua_content
(line)
(line)
(line))))
(section
(gfx_section
(gfx_marker)
(body
(line)))))
==================
lua identifier resembling section marker
==================
pico-8 cartridge // http://www.pico-8.com
version 42
__lua__
local __foo__ = 1
local s = "__lua__"
__gfx__
00
---
(cartridge
(header)
(version)
(section
(lua_section
(lua_marker)
(lua_content
(line)
(line))))
(section
(gfx_section
(gfx_marker)
(body
(line)))))
==================
unknown section name
==================
pico-8 cartridge // http://www.pico-8.com
version 42
__lua__
__future_section__
opaque body
__gfx__
00
---
(cartridge
(header)
(version)
(section (lua_section (lua_marker)))
(section
(unknown_section
(section_marker)
(body (line))))
(section
(gfx_section
(gfx_marker)
(body (line)))))