A small linter for Forth source trees.
flint's only check (for now) is duplicate word definitions across files,
including dependencies. It scans every *.4th under the current
directory, collects every named definition (:, variable, create,
defer, value, marker, field, …) and prints a warning for every
name that appears in more than one file.
flint is intentionally dumb in the first pass:
- No conditional-compilation awareness (
[IFDEF]/[IFUNDEF]are ignored — every: foo …inside any branch is counted). - No dependency-version dedup (if you have
forth-packages/ttester/1.2.0/andforth-packages/ttester/1.2.1/in your tree, you'll see warnings for every word ttester defines — that's the intended signal). - No deep Forth semantics (we don't run any code; this is pure text tokenising with comment/string skipping).
By default output is warn-level only — flint exits with status 0. Use
flint lint . --strict for a hard gate (exit 1 on any [WARN]). Use
--project-only to skip forth-packages/ (project sources only).
Part of the VitaSound Forth tooling family: fmix (build tool / package manager / test runner), ttester (testing utility, upstream Hayes/Ertl + VitaSound extensions), fenum (universal containers, used by flint for its records list), flint.
cd ~ && git clone git@github.com:VitaSound/flint.git
cd flint && fmix packages.getAdd to ~/.bashrc (or ~/.zshrc) — two lines for this tool only (VitaSound convention: one tool per PATH line; do not merge with siblings):
export FLINT_HOME="<install-dir>/flint"
export PATH="$FLINT_HOME/bin:$PATH"<install-dir> is the parent of your clones ($HOME if you cloned beside ~/feco, or e.g. /opt/vitasound for an isolated workspace). Bulk install: VitaSound/feco — ./scripts/clone-ecosystem.sh. Canonical rules: feco shell setup.
Then source ~/.bashrc and run flint version.
Sibling CLI tools (fmix, fcov, fmcp, fhdlgen) each need their own two-line block — see feco shell setup.
flint requires Gforth ≥ 0.7.9 — no other OS dependencies.
flint # lint current dir; warn lines + summary
flint lint <path> # lint a different dir
flint version # print version
flint help # print usageTypical output:
* flint: scanned 312 word definitions
[WARN] duplicate word `module-new` defined in:
./fhdlgen/core/module.4th
./projects/old/legacy.4th
[WARN] duplicate word `ERROR` defined in:
./forth-packages/ttester/1.2.0/ttester.4th
./forth-packages/ttester/1.2.1/ttester.4th
* flint: 2 duplicate group(s) reported.
: variable 2variable fvariable
constant 2constant fconstant
value 2value fvalue
create defer marker
field field: cfield: nfield: ufield:
code synonym
Add more in flint/scan.4th : flint.defining?.
| Situation | What flint does | Workaround |
|---|---|---|
Same dep at two versions in forth-packages/ |
Reports every word | Clean up old versions: rm -rf forth-packages/<name>/<old> |
Re-definitions inside [IFUNDEF] foo … [THEN] (intentional polyfill) |
Reports as duplicate | Pin the load order; consider extracting the polyfill into its own file and only loading it once |
:noname lambdas |
Not counted (no name → nothing to clash) | — |
: ( name-shadowed-by-comment ) bar ; (paren comment in name slot) |
Correctly looks past the comment and records bar |
— |
Words defined inside strings (s" : not-a-defn") |
Correctly ignored | — |
| File | What |
|---|---|
bin/flint |
bash launcher (TTY reset, env-var passing to gforth, fpath extension) |
flint.4th |
entry point: arg parsing, command dispatch |
flint/util.4th |
string + case helpers |
flint/scan.4th |
per-file token scanner with defer flint.on-defined-word hook |
flint/collect.4th |
records storage on top of fenum's ulist (one struct per (file, word) pair) |
flint/walk.4th |
recursive directory walk using gforth's native open-dir / read-dir / close-dir (no shell-out to find) |
flint/report.4th |
group records by name, print one WARN per real duplicate |
flint/version-check.4th |
read key-value flint <req> from ./package.4th and warn (don't fail) if the installed flint doesn't match. Parsing / matching delegated to fsemver (shared with fmix). |
A project's package.4th can declare a minimum flint version using the
same Elixir/Hex grammar fmix uses:
forth-package
key-value name myproj
key-value version 0.1.0
key-value main myproj.4th
key-value flint ~> 0.2
end-forth-package| Form | Means |
|---|---|
key-value flint ~> 0.2 |
>= 0.2.0 and < 1.0.0 (MAJOR pinned) |
key-value flint ~> 0.2.3 |
>= 0.2.3 and < 0.3.0 (MAJOR+MINOR pinned) |
key-value flint >= 0.2.0 |
minimum, no upper bound |
key-value flint == 0.2.0 |
exact match |
key-value flint > 0.2.0 |
strictly greater |
key-value flint < 1.0.0 |
strictly less |
key-value flint <= 0.2.5 |
less-or-equal |
key-value flint 0.2.0 |
bare = >= 0.2.0 |
Parsing / matching is delegated to the standalone fsemver package — the very same engine fmix uses, so the operator grammar can't drift between tools.
If your installed flint doesn't satisfy the requirement, flint prints a
[WARN] line and keeps running — flint is a linter, not a gate. A
future major bump may make this strict, but for now you'll see e.g.:
[WARN] This project requires flint ~> 0.3, but you have 0.2.0
Continuing anyway — flint won't block your lint.
The legacy form key-list dependencies flint <ver> is detected and
surfaced as a WARN with the same migration target.
bash tests/flint_integration_test.shFixtures live under tests/fixtures/with_dupes/ and tests/fixtures/no_dupes/.
COPL (Communist Public License). Use freely; share alike.