flatbuffers-format
v0.5.0
Published
Opinionated FlatBuffers (.fbs) schema formatter, built on an ANTLR4 grammar. Pure TypeScript runtime (antlr4ng) — no JVM, no codegen at install time. Runs in Node and the browser, ships a CLI.
Downloads
62
Maintainers
Readme
flatbuffers-format
The opinionated, zero-config formatter for FlatBuffers (.fbs) schemas — fast, byte-stable, type-safe, runs anywhere.
▸ Try it in your browser — no install. Paste a
.fbs, see canonical output as you type. API docs are on the same site.
# Format a single file (prints to stdout)
npx flatbuffers-format schema.fbs
# Fix a tree in place — recurses; skips node_modules / .git / dist
npx flatbuffers-format --write src/
# Use as a CI check (silent / diff / version)
npx flatbuffers-format --check src/
npx flatbuffers-format --diff src/
npx flatbuffers-format --versionSee docs/cookbook.md for 8 copy-paste recipes — CI gates,
pre-commit hooks, programmatic Node/browser use, Prettier integration, repo
migration tips.
Contents
- Why
- What it does
- CLI
- Install
- Editor integration
- API
- Performance
- Formatting rules
- Standards conformance
- Build from source
- Tests
- Releases
- Compatibility
- Versioning policy
- Roadmap
- Architecture
- Notes / learnings
Why
A FlatBuffers schema is a small declarative DSL — like Protobuf, Cap'n Proto,
or Thrift. Unlike Protobuf and the rest, there's no official formatter. Schemas
drift into per-author whitespace styles, code-review noise from "whitespace
only" PRs accumulates, and CI has no --check-style gate. flatbuffers-format
fixes that: one binary, zero config, deterministic output, exit-code semantics
that drop straight into existing CI patterns.
Opinionated for the same reason gofmt and rustfmt are opinionated — the
formatter's value is in not relitigating style. Two knobs (indent width,
newline) and that's it.
What it does
- A canonical FlatBuffers schema grammar lives in
grammar/FlatBuffers.g4(~155 lines of ANTLR4 EBNF, mirrors the official grammar). antlr-ng— a pure-TS reimplementation of the ANTLR tool — generatesFlatBuffersLexer.ts,FlatBuffersParser.ts, andFlatBuffersListener.tsintogenerated/. No Java at any point.src/printer.tswalks the ANTLR parse tree (SchemaContext,TableDeclContext, etc.) and emits formatted source.src/trivia.tsrecovers comments and blank lines from ANTLR's hidden channel viaBufferedTokenStream.getHiddenTokensToLeft / Right.format(source)is a fixed point: running it twice gives the same output as running it once.
Supported FlatBuffers features
table,struct,enum,union,rpc_servicedeclarations- Namespaced type references (
field: a.b.Foo;) - Fixed-size arrays (
pts: [float:3];) native_includedirectives- Keywords as field names (
table T { enum: int; }) - Object literals (the
flatctext format) - All metadata:
(deprecated),(key: "value"), etc. - Everything in flatbuffers' own
tests/andsamples/schemas, checked in CI:+inf/-nandefaults, the empty-vector default= [], union values with an explicit value or metadata (A = 5,f: T (id: 0)), single-quoted strings
CLI
flatbuffers-format [options] <file-or-dir...> # print formatted output to stdout
flatbuffers-format --write <file-or-dir...> # rewrite files in place
flatbuffers-format --check <file-or-dir...> # exit 1 if any file is unformatted
flatbuffers-format fix <file-or-dir...> # alias for --write
cat foo.fbs | flatbuffers-format - # read source from stdinOptions:
| Flag | Meaning |
|---|---|
| -w, --write | Rewrite files in place |
| -c, --check | Check formatting; exit 1 on diff (silent) |
| -d, --diff | Print unified diff for each file that would change; exit 1 if any |
| --indent <n> | Spaces (or tabs, with --use-tabs) per indent level (default: 2) |
| --use-tabs | Indent with tab characters instead of spaces |
| --line-width <n> | Target column for compact/wrap decisions (default: 80) |
| --no-compact-single-line | Disable single-line collapsing of small enum/union/single-field bodies |
| --max-blank-lines <n> | Max consecutive blank lines kept between decls (default: 1) |
| --wrap-comments | Reflow long line comments at whitespace |
| --comment-width <n> | Wrap column for --wrap-comments (defaults to --line-width) |
| --trailing-comma <all\|none> | Comma after the last value of an expanded enum/union (default: all) |
| --object-wrap <preserve\|collapse> | Keep a body expanded when the source breaks after its { (default: preserve) |
| --no-bracket-spacing | Print one-line bodies as {a, b} instead of { a, b } |
| --end-of-line <lf\|crlf\|auto> | Line endings; auto keeps the file's own (default: lf) |
| --check-ignore-pragma | Leave files whose top comment has @noformat or @noprettier |
| --require-pragma | Format only files whose top comment has @format or @prettier |
| --unsafe | Skip the checks that every comment and code token survives (see below) |
| --config <path> | Use this config file instead of looking one up |
| --no-config | Ignore config files |
| --stdin-filepath <path> | With -, look up the config as if stdin were this file |
| --no-gitignore | Don't consult .gitignore when walking directories |
| -V, --version | Print version and exit |
| -h, --help | Show help |
On a parse error, the CLI emits an IDE-link-friendly report
(path:line:col: …) with a source-snippet and a caret under the
offending column, so editors that auto-link compiler diagnostics
(VS Code's terminal, neovim's :cw, IntelliJ's run console) jump
straight to the problem.
Config file
Commit a .flatbuffers-format.json so the CLI, CI and editor plugins
all format the same way without per-developer flags or IDE settings:
{
"indent": 4,
"compactSingleLine": false
}Keys are the format options: indent, useTabs, lineWidth,
compactSingleLine, maxBlankLines, wrapComments, commentWidth,
trailingComma, objectWrap, bracketSpacing, endOfLine,
checkIgnorePragma, requirePragma. Omitted keys keep their defaults.
minVersion (for example "0.5.0") makes an older flatbuffers-format
refuse the file with a message to update, instead of formatting with
different output. Set it when the config uses a key a teammate's older
copy may not know.
The last six have the names, values and defaults of the Prettier options they copy, so a team used to Prettier gets what it expects.
- Lookup: for each file, the nearest
.flatbuffers-format.jsonin its directory or any parent, up to the filesystem root. Only that one file applies; files higher up are not merged in. For stdin, the lookup starts at--stdin-filepathif given, else the current directory. - Precedence: flags, then the config file, then the defaults.
--write,--checkand--diffall use it. - Validation: an unknown key or a value of the wrong type is an error naming the file and the key, and the CLI exits 2 without touching any file.
Editor plugins can call the same lookup:
import { resolveConfig } from "flatbuffers-format/config";
const options = resolveConfig("/repo/schemas/monster.fbs"); // FormatOptions | nullresolveConfig reads the filesystem, so it lives in the Node-only
flatbuffers-format/config entry point; the main entry point stays
browser-safe. It throws ConfigError (with .file) on an invalid
config file.
Directories are walked recursively. Inside a git repository,
.gitignore is respected via git ls-files — pass --no-gitignore
to disable. Outside a repo (or if git isn't in PATH),
node_modules, .git, dist, build, out, .next, .turbo,
.cache, .hg, and .svn are skipped automatically.
Install
npm i -D flatbuffers-format # local dev dep
npm i -g flatbuffers-format # global CLI
npx flatbuffers-format ... # one-shot, no installNative binary (no Node required)
Prebuilt single-file binaries are attached to every GitHub Release
(starting with flatbuffers-format-v0.2.0). Drop one on PATH and run
— the embedded Node 22 runtime is byte-glued in via Node SEA,
so consumers don't need to install Node or npm:
| Platform | Asset filename |
|--------------|------------------------------------------|
| Linux x64 | flatbuffers-format-linux-x64 |
| Linux arm64 | flatbuffers-format-linux-arm64 |
| macOS x64 | flatbuffers-format-macos-x64 |
| macOS arm64 | flatbuffers-format-macos-arm64 |
| Windows x64 | flatbuffers-format-windows-x64.exe |
curl -fsSL -o flatbuffers-format \
https://github.com/emindeniz99/flatbuffers-format/releases/latest/download/flatbuffers-format-linux-x64
chmod +x flatbuffers-format
./flatbuffers-format --versionThe binaries are produced by .github/workflows/flatbuffers-native-binaries.yml
using scripts/build-native.mjs; run that script locally to build the
binary for your own machine (npm run build:native). The macOS
binaries carry an ad-hoc signature only — Gatekeeper will quarantine
the file on first download until you approve it via Right-click →
Open in Finder, or xattr -dr com.apple.quarantine ./flatbuffers-format.
WebAssembly (runs anywhere with WASI)
A portable flatbuffers-format.wasm is attached to every GitHub
Release. Hand it to any WASI-compatible runtime — wasmtime,
wasmer, wazero (Go), wasmtime-py, etc. — and it reads .fbs on
stdin, writes formatted output on stdout. No Node, no npm, no
language-specific bindings.
curl -fsSL -o flatbuffers-format.wasm \
https://github.com/emindeniz99/flatbuffers-format/releases/latest/download/flatbuffers-format.wasm
wasmtime flatbuffers-format.wasm < schema.fbs > schema.formatted.fbsUse cases: embedding the formatter from non-Node hosts (Rust/Go/Python
build tools), running in sandboxed serverless environments
(Cloudflare Workers Pages, Vercel Edge), or shipping a
reproducible-by-hash formatter in tooling pipelines. The .wasm is built
by .github/workflows/flatbuffers-wasm-binary.yml
using Javy
(QuickJS-compiled-to-WASM) from
scripts/build-wasm.mjs; reproduce locally
with npm run build:wasm (requires javy on PATH).
See docs/cookbook.md for copy-paste recipes (CI gates, pre-commit hooks, programmatic use, browser, Prettier integration, migration tips).
Editor integration
There's no dedicated editor plugin yet. Until there is, run the CLI from your editor's "format on save" or "run command" hook.
VS Code — install
emeraldwalk.runonsave
and add to .vscode/settings.json:
{
"emeraldwalk.runonsave": {
"commands": [
{
"match": "\\.fbs$",
"cmd": "npx flatbuffers-format --write ${file}"
}
]
}
}Neovim — drop in an autocmd:
vim.api.nvim_create_autocmd("BufWritePost", {
pattern = "*.fbs",
callback = function(args)
vim.fn.jobstart({ "npx", "flatbuffers-format", "--write", args.file })
end,
})Pre-commit hook (with lint-staged):
{
"lint-staged": {
"*.fbs": "flatbuffers-format --write"
}
}GitHub Actions — fail the build on unformatted .fbs:
- run: npx flatbuffers-format --check .Browser
A hosted playground is available at https://emindeniz99.github.io/flatbuffers-format/playground/ — paste a schema and watch it format live, no install needed. The page also ships a curated set of corpus fixtures behind a load example dropdown, a share button (encodes the current input into the URL fragment, copies the link to the clipboard) and a copy button on the output pane.
To run the same page locally: src/index.ts and the generated parser
only depend on antlr4ng, which ships an ES-module build. After
npm run build, serve the project root statically and open
web/index.html:
npx http-server -p 8080 .
# then open http://localhost:8080/web/The page resolves the antlr4ng bare specifier via an import map
pointing at node_modules/antlr4ng/dist/index.mjs. If you bundle for
production with esbuild / Rollup / Vite, the bare specifier resolves
normally and no import map is needed.
You can also embed the formatter in your own page:
<script type="module">
import { format } from "flatbuffers-format";
document.getElementById("out").textContent = format(src);
</script>Keeping code as written
A // flatbuffers-format-ignore comment keeps the next top-level
declaration exactly as written; Prettier's // prettier-ignore works
too, and either may be followed by a reason:
// flatbuffers-format-ignore -- aligned by hand
enum Opcode : ubyte {
Nop = 0x00,
Load = 0x01,
}To skip a whole file, put @noformat (or @noprettier) in a comment at
its top and turn on checkIgnorePragma. requirePragma works the other
way round: only files with @format (or @prettier) there are
formatted.
API
import { format, check, hasPragma, hasIgnorePragma, FormatError, CommentLossError, CodeChangeError, VERSION } from "flatbuffers-format";
format(source: string, opts?: FormatOptions): string
check(source: string, opts?: FormatOptions): boolean // true if already formatted
hasPragma(source: string): boolean // top comment has @format / @prettier
hasIgnorePragma(source: string): boolean // top comment has @noformat / @noprettier
type FormatOptions = {
indent?: number; // default 2
useTabs?: boolean; // default false — tab char instead of spaces
newline?: "\n" | "\r\n"; // default "\n"
lineWidth?: number; // default 80 — used by compactSingleLine and wrapComments
compactSingleLine?: boolean; // default true — collapse small enum/union/single-field bodies
maxBlankLines?: number; // default 1 — cap on consecutive blank lines between decls
wrapComments?: boolean; // default false — reflow long line comments
commentWidth?: number; // default = lineWidth — wrap column for wrapComments
trailingComma?: "all" | "none"; // default "all" — comma after the last expanded enum/union value
objectWrap?: "preserve" | "collapse"; // default "preserve" — keep bodies the source broke after `{`
bracketSpacing?: boolean; // default true — `{ a, b }` vs `{a, b}`
endOfLine?: "lf" | "crlf" | "auto"; // default "lf" — overrides `newline`
checkIgnorePragma?: boolean; // default false — skip files with @noformat / @noprettier
requirePragma?: boolean; // default false — format only files with @format / @prettier
unsafe?: boolean; // default false — skip the comment and code-token checks
};Both format and check throw FormatError (with .line and
.column) on invalid input.
They throw CommentLossError if the formatted output would not contain
every comment of the input, compared as a multiset of comment text
(word by word for line comments when wrapComments is on). That is a
formatter bug, not a problem with your schema: the check exists so a
bug refuses to format instead of deleting comments. .missing lists the
comments that would be dropped and .added any that would be
duplicated.
They throw CodeChangeError if the output's code tokens would differ
from the input's: a formatter may change whitespace, comments' places
and a comma before a }, nothing else. .index, .expected and
.actual name the first differing token.
For both, the CLI reports the file and the problem, exits 1, and leaves
the file untouched (--write) or prints nothing (stdin). --unsafe
(unsafe: true) skips both checks, for working around a bug in them.
Performance
Measured against the 24-file test corpus (22.6 kB total) on a modern Linux
laptop; reproduce with node scripts/bench.mjs after npm run build.
| | Measurement |
|---|---|
| In-process throughput | 3,164 files/sec, ~316 µs/file |
| Cold-start CLI (single small file, includes Node startup) | ~164 ms median, ~155 ms min |
| Browser bundle (esbuild --bundle --minify, includes antlr4ng runtime) | 234 kB min · 58 kB min+gz |
What this means in practice:
- Pre-commit hook on a repo with a few
.fbsfiles: cost is dominated by Node startup; you're paying ~160 ms regardless of file count. Once warm in a watcher process, formatting a typical schema is sub-millisecond. - CI
--checkgate: a 100-file.fbsrepo formats in well under a second of wall-clock plus Node startup. Cheap to wire into every PR. - Browser usage: 58 kB gzipped includes the entire ANTLR4 runtime. The
hand-rolled sibling (kept in this repo as a differential oracle, not
published) bundles to ~4 kB gzip if you ever need a lighter alternative —
see
docs/grammar-comparison.md.
Regression gate
Every PR runs scripts/bench-compare.mjs against
scripts/bench-baseline.json and fails if
throughput regresses by more than 25% (in-process µs/file), cold-start
median by more than 30%, or bundle min+gz by more than 15%. The comparison
posts a Markdown table to the PR for visibility. To accept slower numbers
legitimately, re-baseline locally
(npm run bench -- --json --repeat 3 > scripts/bench-baseline.json) and
commit the new baseline in the same PR.
Formatting rules
| Construct | Rule | Configurable? |
|---|---|---|
| Top-level | One blank line between block declarations (table/struct/enum/union/rpc_service). Single statements collapse together. | maxBlankLines (default 1) |
| Fields | name:Type (no space before colon, one space after type). Defaults: = value. Metadata: (key, key: value). Trailing ;. | fixed by design |
| Enum/union/single-field bodies | Collapse to one line if the result fits in lineWidth; expand otherwise. A body with a line break after its { in the source stays expanded, like a Prettier object. Any comment inside the body, and per-value metadata, force expansion. | compactSingleLine (default on), objectWrap (default preserve), lineWidth (default 80), bracketSpacing (default on) |
| Enum values | One per line (when expanded), comma-separated, with a comma after the last value. A one-line body never has one. | trailingComma (default all) |
| Union variants | Same as enum values. Aliases written as Alias: Type. | fixed by design |
| Metadata | Always inline: (deprecated, key: "x"). | fixed by design |
| Comments | //, ///, /* */ all preserved, in order. Doc comments stay attached to the following declaration. Trailing comments stay on their owning line, after the comma for enum/union values (Red, // first). Comments before a } and in comment-only bodies stay inside the braces. A comment inside a declaration, where the output has no slot for it (between a field name and its :, inside metadata, inside an object literal), moves onto its own line above that declaration. Long // lines optionally reflowed. | wrapComments (default off), commentWidth (default = lineWidth) |
| Indent | 2 spaces. | indent (count), useTabs (char) |
| Newlines | LF, single trailing newline at EOF. | endOfLine (default lf) |
What changes besides whitespace
Formatting never changes what a schema means. Apart from spacing, indentation and line breaks, the output differs from the input only in:
- The comma after the last value of an enum or union written one
value per line: added by
trailingComma: "all"(the default), removed by"none". A body on one line never has one. - Comments the output has no slot for (between a field's name and
its
:, inside a metadata list) move onto their own line above the declaration, unchanged. - Long
//comments are rewrapped, only withwrapCommentson. - Line endings, per
endOfLine.
Three checks hold that line. format() compares every comment and code
token of the output with the input and refuses to return output that
loses or changes one (CommentLossError, CodeChangeError). CI
compiles every corpus file and every schema in flatbuffers' own
tests/ with flatc before and after formatting, with the default
options and with every option changed, and requires byte-identical C++
code and binary schemas. To check your own schemas the same way:
flatc --cpp -o before/ schema.fbs
flatbuffers-format --write schema.fbs
flatc --cpp -o after/ schema.fbs
diff -r before/ after/flatc --conform old.fbs new.fbs answers a different question: whether
new.fbs is a compatible evolution of old.fbs (no removed fields, no
changed ids). A formatted schema always passes it against its own
original, but it does not prove the two are the same schema; the
codegen diff above does.
Standards conformance
The canonical FlatBuffers grammar is the EBNF at
flatbuffers.dev/flatbuffers_grammar.html.
This project's grammar/FlatBuffers.g4 is an ANTLR4 encoding of that
EBNF, kept declaratively close to the upstream spec — the file is the
auditable source of truth, not the TypeScript that consumes it.
A sibling implementation (hand-rolled recursive-descent parser, no
runtime deps) lives at
../flatbuffers-formatter-handrolled in the same
monorepo. Both formatters are differential-tested: every file in
test/corpus/ must produce byte-identical output from both engines, on
every commit, via bash test/crosscheck.sh — currently 28/28 pass.
That cross-check runs in prepublishOnly before any release, so a
grammar bug in either implementation cannot ship.
Validating the corpus against flatc
A second, independent check verifies that the corpus is built out of
real FlatBuffers schemas — not just schemas our two parsers happen to
agree on. Every test/corpus/*.fbs file is fed to Google's official
flatc compiler via:
npm run test:flatc-conformThe script (scripts/flatc-conform.sh) runs
flatc -b --schema --no-warnings on each corpus file and reports a
pass/fail summary. For every file flatc accepts, it then formats the
file (with the defaults, and with every option changed) and requires
flatc's C++ code and binary schema to come out byte-identical; this
part needs the engine built. scripts/upstream-schemas.sh <flatbuffers
checkout> runs the same check over flatbuffers' own test schemas,
which CI pins to the flatc release it uses. It is deliberately not part of prepublishOnly,
because flatc is a system dependency (installed via
apt-get install flatbuffers-compiler, brew install flatbuffers,
etc.), not an npm one — wiring it into the publish chain would break
maintainer machines that don't have it. The script skips with a clear
warning (exit 0) when flatc isn't on PATH, so it stays friendly to
fresh checkouts.
The grammar is kept up to date with the latest flatc release
(currently 25.12.x), which adds syntax not present in the Ubuntu/Debian
apt package (still flatc 2.0.8 as of writing). Fixtures that exercise
post-2.0.8 features — per-enum-value metadata, the (offset64) /
(vector64) field attributes, and union with explicit underlying type
(union W : uint8 { … }) — carry a MIN_FLATC_MAJOR entry in the
conformance script, so contributors on older flatc see a clear
SKIP — needs flatc >= 23.x message and the script still exits 0. To
test against the dialect this formatter targets, install flatc from
source or grab a prebuilt binary from
google/flatbuffers/releases.
This check is intentionally stricter than the formatter's own parser: flatc applies semantic rules (root-type required for object literals, attributes must be declared before use, fixed vector nesting, etc.) on top of the grammar. Files that pass the grammar but fail flatc are flagged for human review — they may be genuine formatter-edge-case schemas (which is fine), or they may indicate the corpus has drifted from real-world FlatBuffers (which is not).
Build from source
git clone https://github.com/emindeniz99/flatbuffers-format.git
cd flatbuffers-format/flatbuffers-formatter
npm install
npm run build # antlr-ng codegen → tsc
node dist/src/cli.js examples/sample.fbsTests
npm test # local suite (14 tests)
npm run test:crosscheck # differential check vs hand-rolled siblingBoth suites use the built-in node --test runner — no extra test deps.
Releases
Releases are automated via release-please. The CHANGELOG is generated from Conventional Commits on main.
Compatibility
| Runtime | Status | Notes |
|---|---|---|
| Node.js 20.x | ✅ supported | Minimum supported version (engines.node: ">=20"). EOL upstream since April 2026, kept for slower-moving users until 2027; verified in CI by installing the packed tarball with npm. |
| Node.js 22.x / 24.x / 26.x | ✅ supported | Covered by the CI matrix on Linux, macOS and Windows. |
| Modern browsers | ✅ supported | ESM-only. The core (src/index.ts) has zero Node imports; the CLI is the only Node-only entry. |
| Bun | ✅ supported | Installs from npm via bun add flatbuffers-format; the ESM entry resolves natively. |
| Deno | ✅ supported | Installs from npm via npm:flatbuffers-format or deno add npm:flatbuffers-format. |
| Cloudflare Workers | ✅ supported | ESM + zero Node imports in the core. |
| OS | ✅ Linux / macOS / Windows | All three covered in CI (node 22/24/26 × ubuntu + macos + windows). |
Versioning policy
Standard semver. Before 1.0, the API surface is
experimental: minor versions may include small breaking changes
(noted in CHANGELOG.md and the release notes).
The "public API" — the surface where the semver promise applies once we
reach 1.0 — is exactly the named exports of
src/index.ts (format, check, FormatError,
CommentLossError, FormatOptions), of
src/config.ts (resolveConfig, loadConfig,
findConfigFile, ConfigError, CONFIG_FILE_NAME) and the CLI's documented flags. Everything else
(internal printer/lexer/parser classes, the generated parser, the
trivia helpers, the scripts/ directory) is implementation detail
and may change in any release.
Roadmap
No firm dates; this is a side project. The order below reflects what's likely to land next, not a commitment.
0.1.x (current) — bugfixes, more corpus fixtures as real-world schemas surface in the wild, minor CLI ergonomics polish. No breaking changes.
Landed during 0.1.x:
- ✅ Sibling
prettier-plugin-flatbuffers— drop into.prettierrcand.fbsfiles format alongside the rest of your codebase - ✅ Sibling
vscode-flatbuffers— TextMate syntax highlighting + native format-on-save via this engine, no third-party "Run on Save" needed - ✅ Sibling
tree-sitter-flatbuffers— incremental-parse + highlights for Neovim, Helix, Zed, GitHub.com, and any tree-sitter consumer
0.2.x — under consideration:
- Performance: experiment with parallel parsing for multi-file
--write/--checkruns - Real-world conformance: a curated list of public
.fbsschemas fetched at CI time and validated against the formatter - A
--validatemode that pipes throughflatc(when installed) for semantic-layer checks beyond the grammar
1.0 — when:
- The 0.x API has been stable for two consecutive minors without changes
- The public-API surface has had time to attract real-world feedback
- A documented
docs/migrating-to-1.0.mdexists for any breaking changes between the last 0.x and 1.0
Contributions targeting any roadmap item are welcome — open an issue first so we can discuss design before you spend time on a PR.
Architecture
See docs/architecture.md for a tour of how
the parts fit together — lexer → ANTLR parser → printer, plus the
trivia-attachment pipeline that makes comments round-trip cleanly.
Notes / learnings
antlr-ngis a pure-TypeScript reimplementation of the ANTLR tool; generated parsers use theantlr4ngruntime, which is the active TypeScript successor to the dormantantlr4ts. The whole pipeline is JS — no JVM, no*.jar, nothing to install beyondnpm.- ANTLR routes whitespace and comments to a hidden channel. Recovering
them is straightforward but deduping is tricky: a trailing comment
after
;is also a leading hidden token of the next declaration. If you don't filter, comments get emitted twice. Fix insrc/trivia.ts: only treat hidden tokens before the first newline as already-claimed by the previous node. - ANTLR's
INT_LITERAL/FLOAT_LITERALtokens are scoped per lexer rule, so you can't easily share a singleNUMBERrule between them. Keeping them separate matchesflatc's lexer and makes the parser rules read more naturally (('+' | '-')? INT_LITERAL). - The grammar uses inline string literals (
'table','struct',':', …) so ANTLR generates anonymousT__0,T__1, … tokens. That's fine for parsing but means the generated.tokensfile isn't human-readable — a named lexer pass (TABLE: 'table';) would be the polished move if you ever need cross-language reuse via the.g4.
