Thanks for your interest in the Panther Compiler Kit. This document covers the development loop, the invariants a change has to respect, and what CI expects before a pull request can merge.
Start with the README for setup, the build commands, and the
repository layout. The one thing to carry over from it: pncs/ (Scala) is the
canonical compiler, and pnc/ (Panther) is generated from it.
sbt test/test # establish a baseline before you change anythingThen, while working:
sbt pncs/compile # compile the Scala compiler
sbt test/test # run the suitesIterate on a single suite with:
sbt "test/testOnly BinderTests"User-library implementations are canonical in runtime/src/main/scala.
sbt pncs/transpile refreshes the selected stdlib/src sources and the embedded
bundle in pnc/src; commit both. Bootstrap builds pass --no-stdlib because
they already supply the runtime declarations. See stdlib/README.md.
Never hand-edit files under pnc/src/. They are overwritten by
sbt pncs/transpile. Fix the Scala source, re-transpile, and commit the result.
A change that touches pncs/, runtime/, metadata/, or text/ without a
matching pnc/src/ update will fail CI.
The transpiler handles a subset of Scala 3. Code that compiles under
sbt pncs/compile can still be untranspilable, and the failure shows up later,
in the transpile or self-hosting stages. Follow the conventions already in the
codebase:
- Use the
panther._types (string,int,char,unit,any) rather than their Scala equivalents, and thesystem.io._wrappers for file and path IO. - Prefer
case class,enum, plaindef,match, and explicit recursion. Avoid implicits/given, higher-kinded types, for-comprehensions over custom types, and the parts of the Scala collections library that the runtime does not mirror. - Prefer the project's own
List,Array, andStringBuilderhelpers overscala.collection. - When in doubt, find an existing file in
pncs/src/main/scala/that does something similar and follow its shape.
If you add a language construct to the compiler, make sure the transpiler can emit it before relying on it inside the compiler's own sources.
- Formatting is enforced by scalafmt 3.9.1 with the
scala3dialect (.scalafmt.conf). Runsbt scalafmtAll; CI runssbt scalafmtCheckAll. .editorconfigsets the rest: UTF-8, LF endings, final newline, trimmed trailing whitespace, 2-space indent, 120-column Scala lines.- The build compiles with
-Xfatal-warnings, so warnings break the build. Fix them rather than suppressing them. - One concept per file, named after the concept — the flat layout of
pncs/src/main/scala/is intentional and keeps the.pntwins aligned.
Tests live in test/src/test/scala/ and use ScalaTest with AnyFunSpec
and Matchers:
class LexerTests extends AnyFunSpec with Matchers {
describe("Lexer") {
it("should handle single token") {
val tokens = mkTokens("1")
tokens.length shouldEqual 2
}
}
}Shared construction helpers (mkTokens, mkSyntaxTree, mkCompilation,
mkBinaryExpr, and friends) are in
TestHelpers.scala — use them instead
of rebuilding a lexer or compilation by hand.
Suites are organized by pipeline stage: LexerTests, ParserTests,
BinderTests, TypeTests, MetadataTests, VmTests, ArgsParserTests. Add
new cases to the suite for the stage you changed, and add a test for every bug
you fix.
Every ```panther block under docs/ is compiled by
doccheck:
sbt "doccheck/run docs/src/content/docs"The latest check reports 201 blocks: 199 pass and 2 are explicitly skipped
for unsupported higher-order functions. tools/doccheck/baseline.txt is empty.
Keep it that way: a snippet you add or change has to pass its declared check.
The check fails in two directions — an unbaselined snippet producing a diagnostic, and a baselined snippet compiling cleanly — so if you ever do need to baseline something, regenerate it in the same commit:
sbt "doccheck/run --update-baseline docs/src/content/docs"A snippet that should not be compiled as written gets a directive on the line
above its fence — parse-only, expect-error, or skip reason="...". Prefer
fixing the snippet; see
tools/doccheck/README.md for what the checker
does and does not cover.
Commit messages follow a Conventional-Commits style, with an optional scope naming the pipeline stage:
feat(binder): improve bidirectional type checking
fix: use Type.Error for unknown identifier patterns
refactor(tests): convert tests to use AnyFunSpec
docs: update docs to use astro
Common types in this repo: feat, fix, refactor, docs, test, chore.
Keep the subject in the imperative mood and under ~72 characters.
Run these before you push — they are exactly what CI runs:
sbt scalafmtAllsbt pncs/transpilesbt pncs/compile test/test pnc/compile scalafmtCheckAllsbt "doccheck/run docs/src/content/docs"Then commit the regenerated .pn files along with your Scala changes.
The ci workflow runs five jobs on pull requests
and manual dispatch:
sbt pncs/compile, thensbt test/testsbt -J-Xmx1G pnc/compile stage3/run— build the generated compiler image and require the self-hosting fixed point, with a 15-minute job timeoutsbt pncs/transpile, thengit add -Aandgit diff --cached --exit-code— detect modified, added, or deleted generated sourcesdoccheckagainst its empty baselinesbt scalafmtCheckAll
For changes affecting VM execution, metadata/image output, compiler ordering, or self-hosting, run the same fixed-point check locally:
sbt pnc/compile stage3/runStage 3 must emit an image byte-for-byte identical to Stage 2. The CI compiler job runs this check with a 1 GiB cap on sbt and a separate 4 GiB cap on the verifier JVM. ADR 0007 records the resource measurements and latest verified result.
For the pull request itself:
- Keep it focused on one change; separate mechanical reformatting from behavior changes so the diff stays reviewable.
- Describe what changed and why, and note any new or changed diagnostics.
- Include the regenerated
pnc/src/output in the same PR. - Say explicitly if you changed a compiler invariant or the shape of a pipeline stage.
For non-trivial changes, the repo has a visual-recap skill
(.agents/skills/visual-recap/SKILL.md)
that generates a visual summary of which pipeline primitives a change touches
and how risky it is, rendered inline in the PR description. It's informational
and non-blocking — it supplements review, it doesn't replace reading the diff.
If your change adds, removes, or materially reshapes a primitive (a new
compilation stage, a split or renamed module, changed code roots), update
docs/architecture/primitives.yaml in the
same PR and validate it:
node .agents/skills/visual-recap/scripts/check-primitives-map.mjsOrdinary feature work inside an existing stage does not need a taxonomy edit — that's what commit messages and PR descriptions are for.