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.
- All existing tests must continue to pass. The authoritative behavior is defined by the existing test suite.
- Test-driven: new behavior requires tests showing failure before the change and success after, in the existing test style. Bug fixes start with a failing repro.
- Minimal, well-scoped edits: small local patches; no large refactors (renames, structural moves, mass rewrites), no changes to deprecated folders, no altered language semantics without tests demonstrating the intended outcome, no new external dependencies unless requested.
- Generated code: never modify files generated by
:gen; change the.parseqspecs or grammars and regenerate (./gradlew :gen). Adding a node type to a.parseqsum type breaks every exhaustive matcher at compile time — fix those compile errors, they are the complete checklist.
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.
The Gradle wrapper lives inside de.peeeq.wurstscript/ (not the repo root); run all commands from there.
./gradlew test # all tests
./gradlew test --tests "tests.wurstscript.tests.SomeTestClass.someMethod" # one test
./gradlew :gen # regenerate AST (parseq) + ANTLR
./gradlew build # build the compiler
test().executeProg()runs the compiled program in the interpreter and requires atestSuccess()call.test().testLua(true).executeProg()additionally syntax-checks the emitted Lua with luac and executes it with a real Lua 5.3 interpreter against the WC3 runtime insrc/test/resources/luaruntime/(wc3shim + Reforgedcommon.j.lua/blizzard.j.luadumps). Interpreter discovery: bundledsrc/test/resources/lua.exeon Windows,lua53on Linux, else PATH; tests skip visibly when none is found.- Use
LuaBackendAuditTestsas the reference style for backend regression repros.
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 Grill/compiler integration work moved wurst.build parsing rules into a tiny shared dependency. Keep compiler behavior aligned with that shared model.
de.peeeq.wurstscript/build.gradledepends oncom.github.wurstscript:wurst-project-config.de.peeeq.wurstio.languageserver.WurstBuildConfigis a compiler adapter around the shared model, not a second config DAO.- Do not duplicate YAML parsing rules, patch aliases, or script-mode behavior in compiler-only code unless it is truly compiler-specific.
- Preserve exact
wc3Patchnames for cache invalidation and diagnostics. Broad patch kind is useful for behavior choices, but not enough for hashes.
- Use the shared
Wc3PatchTargetparser forwc3Patch. - Patch family boundaries:
- below
1.29=> pre-1.29 behavior 1.29through1.31=> classic1.32+,1.36,2.0, andReforged-*=> Reforged
- below
- Friendly names and jass-history dump names should resolve through shared config. Do not add one-off aliases in compiler code.
- If jass-history has a broken folder name, fix
wurstscript/jass-historyinstead of compensating here.
- Build/typecheck should prefer pinned
wc3Patchfromwurst.buildand should not parse the installed Warcraft executable just to decide target patch data. - Config injection should use the pinned project patch when available, not the locally installed game patch.
- User-facing executable version parsing failures must stay short. Do not print PE parser stack traces unless explicit debug logging is requested.
- Run/launch is different from build: the selected Warcraft executable controls launch arguments and map placement.
- When project patch family and selected client family differ, warn and allow the user to choose a different Warcraft III folder.
- If launch folder selection changes the client, all launch decisions must use that selected
W3InstallationData, not stale request-levelw3data. - Legacy clients that need install-dir map placement must copy to the selected launch install's
Maps/Testfolder.
For config and run-pipeline changes, prefer these focused checks before broader test runs:
./gradlew test --tests tests.wurstscript.tests.WurstBuildConfigTests
./gradlew test --tests tests.wurstscript.tests.MapRequestPatchTargetTests
./gradlew make_for_userdir
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.
- Integer/real division and modulo semantics are centralized:
WurstOperator.moduloInteger/moduloRealimplement the Blizzard.j formula (truncated remainder, plus divisor if negative) and Jassdivtruncates toward zero. - The Lua polyfills (
intDiv/wurstMod), the interpreter'sMathProvidermocks, and constant folding (SimpleRewrites,ConstantAndCopyPropagation) must all stay consistent with those helpers — never reimplement div/mod locally.
- 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.
The compiler surface used by serialization libraries is intentionally general-purpose and contains no knowledge
of save formats, ChunkedString, hashes, or Serializable.
- The public Wurst names are
wurstForFields,wurstMapFields, andwurstNewInstance<T>(); thewurstprefix makes the compiler-provided surface collision-resistant without underscore-prefixed names. The original unprefixed spellings remain supported as compatibility fallbacks. Internal markers must never survive backend lowering. - Names beginning with the compiler-internal
__wurstprefix are reserved. Generated temporaries must be fresh against user-visible enclosing declarations, but nested callback locals deliberately using that prefix are not supported. - An applicable visible ordinary function with one of these names must resolve normally. Compiler handling is only the fallback when no user-visible overload accepts the call.
wurstForFieldsincludes accessible, non-static instance fields, including inherited, module-injected, readonly, and constant fields.wurstMapFieldsadditionally requires each included field to be mutable.- Explicit targets are evaluated exactly once. Generated temporaries must be proven fresh in the enclosing scope.
- Preserve module qualification in both field keys and generated accesses so sibling modules with equal field names remain distinct.
wurstNewInstance<T>()must invoke the normal accessible zero-argument constructor of a concrete, non-abstract class. Never replace it with uninitialized allocation or runtime type lookup.
- Field iteration expands after module expansion, when inherited and injected fields are concrete.
- Jass may use normal generic elimination. Lua specialization remains targeted to paths that reach generic construction; do not turn this into general Lua generic monomorphization.
- Lua reachability must traverse both
ImFunctionCallandImMethodCall, including dispatch submethods. - Targeted specialized methods needed by Lua dispatch must remain attached to the IM classes consumed by
LuaDispatchPreparation. Concrete implementations for erased generic objects must bind the same root slot used by the call site. - Do not promise Lua support for a method which combines type parameters from its owning generic class with independent method type parameters. Serialization loaders should be free generic functions, or class methods parameterized only by their owning class.
- A method invoked directly on a freshly constructed generic receiver is supported on Lua. The receiver's declared type is still generic at that point, so specialization takes the instantiation from the construction. Binding the receiver to a typed local first is no longer required.
- Lua generic-construction dispatch through multi-parameter generic interfaces is outside the supported loader shape. The supported generic loader has a single construction type parameter.
- Do not call
wurstNewInstance<T>()from the constructor of a generic class. Construct the simple state object in the generic loader, then initialize any nested state explicitly after construction. wurstNewInstance<T>()is a runtime Jass/Lua construction surface and is not supported insidecompiletime(...)evaluation. Do not expand interpreter behavior for compile-time construction.- Nested modules whose sibling submodules declare equal field names are outside the supported field-key model. Dedicated state classes should use direct fields, ordinary inheritance, or non-conflicting shallow module fields.
- Generate no runtime reflection registry, type-name lookup, type-id switch, or serialization-specific metadata.
Use FieldIterationTests as the focused suite. Cover both Jass and Lua, direct and explicit targets, target
evaluation count, inherited/module fields, readonly versus mutable behavior, overload selection, constructor
execution and diagnostics, ordinary same-name functions, generic functions and class methods, transitive method
reachability, and generic interface dispatch. Assert generated output contains direct construction/accesses and no
source intrinsic names or runtime reflection machinery.