This document defines how automated coding assistants (“agents”) should interact with this repository.
This repository contains the WurstScript compiler. Its main code lives in:
de.peeeq.wurstscript/
Other directories like WurstPack and HelperScripts exist but are largely deprecated and should not be modified unless explicitly requested.
Inside de.peeeq.wurstscript:
-
src/main/antlr/de/peeeq/wurstscript/antlr/Contains the ANTLR grammars (.g4) for Wurst and Jass. These produce concrete syntax trees (CSTs). -
parserspec/Contains .parseq grammars forabstractsyntaxgen(https://github.com/peterzeller/abstractsyntaxgen). These define the AST structure used by the compiler. Code is generated via the Gradle task:./gradlew :gen -
src/main/java/de/peeeq/Main compiler sources:- Parsing and AST infrastructure
- Type checking
- Intermediate language (IM)
- Jass and Lua backends
- Interpreter for executing IM at compile time (used for specific compile-time evaluations)
- Parse Wurst/Jass with ANTLR → CST
- Abstractsyntaxgen → AST
- Transform AST → IM
- Optionally: Run IM in the interpreter, Optimize
- Transform IM → Backend (Jass or Lua)
- Java 25
- Gradle (9.2.1)
- Unit tests define many entry points and expected behaviors.
Changes made by agents must follow these principles:
- All existing tests must continue to pass.
- If behavior changes intentionally, provide new tests that define the updated semantics.
Agents should:
- Fix concrete bugs with small, local patches.
- Add missing null-checks, defensive checks, or diagnostics where appropriate.
- Add tests when resolving issues or implementing requested features.
Agents should avoid:
- Large refactors (renaming packages, structural moves, mass rewrites).
- Modifying deprecated folders unless explicitly instructed.
- Altering public semantics or language rules without tests demonstrating the intended outcome.
- Any new behavior requires tests showing failure before the change and success after.
- Use existing test style and harnesses.
-
Do not modify files generated by
:gen. -
If modifying
.parseqfiles or grammars, regenerate via:./gradlew :gen
Use the conventions already present in the file you edit. Avoid introducing new patterns without reason.
- The IM is the central intermediate representation.
- Transformations should keep IM consistent and valid.
- Backends (Jass/Lua) expect well-formed IM; avoid breaking invariants.
- Interpreter should remain deterministic and side-effect free.
- Prefer explicit, descriptive diagnostic messages.
- Avoid silent fallbacks or suppressed exceptions.
- Don’t change the meaning of existing error messages unless required.
- Avoid algorithmic regressions in parsing, type checking, or transforms.
- Consider memory impact when manipulating large ASTs or IM graphs.
- Fix a crash or incorrect behavior in a specific compiler pass.
- Add a regression test that demonstrates a known issue.
- Improve clarity of error messages.
- Add a small new feature when fully specified by the user and backed by tests.
- Update Gradle/JDK usage only if part of a requested task.
- Unsolicited rewrites of ANTLR grammars.
- Modifying deprecated folders.
- Changing code generation semantics without explicit tests.
- Changing IM behavior without test coverage.
- Introducing new external dependencies unless requested.
Inside
de.peeeq.wurstscript/
run:
./gradlew test
./gradlew test --tests "tests.wurstscript.tests.GenericsWithTypeclassesTests.identity"
./gradlew :gen
./gradlew build
- Keep changes minimal, compatible, and tested.
- The authoritative behavior is defined by the existing test suite.
- The compiler architecture relies on CST → AST → IM → Backend; treat each stage carefully.
- Never modify generated files; modify the sources that generate them instead.
- New behavior must be documented through tests.
This repository has multiple entry points that may trigger compilation/build behavior:
- Language Server runtime
de.peeeq.wurstio.languageserver.* - LSP build request
de.peeeq.wurstio.languageserver.requests.BuildMap - CLI compiler entry point
de.peeeq.wurstio.Main - CLI map build request
de.peeeq.wurstio.languageserver.requests.CliBuildMap
WurstLanguageServerwires LSP protocol handlers.LanguageWorkerserializes requests and file-change reconciliation.ModelManagerImplowns project model state (wurst files, dependencies, diagnostics).- User actions like build/start/tests are implemented in
languageserver.requests.*.
Map build behavior is centralized in:
MapRequest.executeBuildMapPipeline(...)
Both:
BuildMap(VSCode/LSP build command), andCliBuildMap(CLI-build, used by grill)
must use that shared backend flow.
This pipeline handles:
- map/cached-map preparation
- script extraction/config application
- compilation (Jass/Lua)
- script + map data injection (including imports/w3i)
- final output map write + MPQ compression finalization
BuildMap(LSP/UI) may use interactive retry/rename behavior for locked output files.CliBuildMapmust fail fast with a clear error for locked files (non-interactive environments).
- Do not reintroduce separate build-map logic in
Mainor other call sites. - If map build behavior changes, update the shared
MapRequestpipeline first, then keep wrappers thin. - Ensure CLI and LSP builds remain behaviorally aligned unless a difference is explicitly required and tested.
Recent fixes established additional rules for backend work. Follow these for all future changes:
- New language/compiler features must be validated for both Jass and Lua backends.
- Behavior should be as close as possible across backends.
- If behavior differs, treat it as intentional only when:
- the reason is backend/runtime-specific, and
- the difference is documented in tests.
- Prefer matching Jass behavior semantically in Lua output.
- Be explicit that Lua is stricter in some runtime cases where Jass may silently default/swallow invalid operations.
- Do not rely on Lua strictness as a substitute for correct lowering/translation.
- On Lua target, do not inline across callback/function-reference-heavy sites (IM
ImFuncRef-containing callees). - This avoids breaking callback context semantics (e.g. wrapper/xpcall/callback-native interactions such as force/group enum callbacks).
- This is a structural rule, not a name-based exclusion.
- Lua has a hard local-variable limit per function.
- When a function exceeds the safe local threshold, rewrite locals to a locals-table fallback.
- Requirements for fallback correctness:
- locals-table declaration must be at function top before first use,
- rewritten accesses must target the declared table (no global fallback),
- nested block local initializations must be preserved,
- use deterministic numeric slot indices (
tbl[1],tbl[2], ...) rather than string keys.
- Any backend parity fix must add/adjust regression tests in
tests.wurstscript.tests.*. - Include tests that check:
- generated backend output shape for the affected backend,
- no behavioral regression in the other backend when relevant,
- known fragile cases (dispatch binding, inlining boundaries, locals spilling).
Recent regressions showed that virtual-slot binding can silently degrade to base/no-op implementations in generated Lua while still compiling. Follow these rules for all related changes:
- For FSM-style dispatch (
currentState.<rootSlot>(...)), each concrete subclass must bind that same root slot to its own most-specific implementation. - Never accept mappings where a subclass has its own update method but the dispatched root slot still points to
NoOpState_*(or another base implementation). - When verifying generated Lua, always inspect both:
- the slot invoked at call-site (
FSM_*update), and - class table assignments for each sibling state class.
- the slot invoked at call-site (
- If override wrappers/bridges are created, preserve transitive override links (
wrapper -> real override) so deeper subclasses remain reachable during slot/name normalization. - Avoid transformations that disconnect root methods from concrete overrides in the method union graph.
- Lua output must be deterministic for identical input (same input -> byte-identical output in test harness).
- Any iteration over methods/supertypes/union groups used for naming or table assignment must be deterministic (stable ordering).
- If multiple candidate methods exist for the same slot in a class, selection must be deterministic and must prefer the most specific non-abstract implementation for that class.
- Add a repro with:
State<T:>,NoOpState<T:>,FSM<T:>,- multiple sibling
NoOpState<Owner>subclasses (including at least 4+ siblings), - early constant state instantiation,
- root-slot call through
State<T>.
- In generated Lua assertions:
- extract the actual dispatched slot name from
FSM_*updatecall-site, - assert each concrete sibling class binds that slot to its own implementation,
- assert no sibling binds that dispatched slot to
NoOpState_*.
- extract the actual dispatched slot name from
- Add a compile-twice determinism assertion for the same repro input.