Purpose: Enable an AI coding agent to be immediately productive in this repository by conveying architecture, workflows, and project-specific conventions. Keep changes focused, incremental, and validated by tests.
In the ZILF compiler, most argument parsers receive arguments as ZilObject[], which represent unevaluated ZIL forms or constants. However, many built-in routines (such as arithmetic operations) require their arguments as IOperand[], which are the compiled representations suitable for Z-machine code emission.
- ZilObject: Represents any ZIL value or form, including atoms, numbers, lists, and code expressions. Used as the universal argument type for parser entry points.
- IOperand: Represents a compiled operand (constant, variable, or temporary) that can be directly used in Z-machine instructions. Produced by compiling a
ZilObjectusing methods likeCompileAsOperand.
When writing argument parsers for builtins (2025 best practices):
- If the implementation method expects only
ZilObjectorparams ZilObject[], pass the arguments directly after validation and macro unwrapping. Never useCompileOperandsfor these. - If the implementation method expects any
IOperandorparams IOperand[], useCompileOperandsfor just the operand arguments (not all arguments). Forparams IOperand[], slice the operands array safely. - For mixed-parameter builtins, only pass operand arguments to
CompileOperands; assign non-operand parameters directly fromargswith type conversion as needed. - Always unwrap
ZilMacroResultobjects before compilation:(args[i] is ZilMacroResult zmr ? zmr.Inner : args[i]). - For string parameters, enforce that only
ZilStringis accepted, and useCompilation.TranslateStringfor conversion. - Match error handling patterns to the call type:
VoidCall/ValueCallusec.HandleMessage(),PredCall/ValuePredCallusec.cc.Context.HandleError(). - For variable parameters with
[Variable]attributes, useGetVariable()helper and dispatch based onIsHard/IsSoftproperties. - Always validate generated code by examining the actual output, not just compilation success. Test with all edge cases, especially for params arrays and mixed-parameter builtins.
- Side Effects: Use
HasSideEffect = truein[Builtin]attributes for builtins that modify state. The source generator automatically maintains theHasSideEffects(string name)method from these attributes. - No Runtime Reflection: Never use reflection at runtime to inspect method signatures or attributes; all such analysis must be done at compile time in source generators.
- No Special Cases: NEVER hardcode the name of any custom sequence/structure parameter type, or the name of any SUBR/FSUBR/ZBuiltin, or any logic for parsing a specific custom type in the source generator. The generator MUST work generically, based on the definitions of those types.
Example:
// Parser for an arithmetic builtin expecting IOperand[]
public static IOperand ADD_Generated(ValueCall c, ZilObject[] args)
{
if (args.Length < 1)
return c.cc.HandleError(CompilerMessages.WrongArgumentCount, "ADD", 1, args.Length);
using (var operands = c.cc.CompileOperands(c.rb, c.form.SourceLine, args))
{
return ArithmeticOp(c, BinaryOp.Add, operands.ToArray());
}
}Source Generator Implications: The generator must analyze method signatures to determine which parameters are operands, and only use CompileOperands for those. For params ZilObject[], never use CompileOperands—pass arguments directly. For params IOperand[], slice the operands array safely. The generator automatically handles macro unwrapping for all arguments before compilation. Variable parameters with [Variable] attributes generate runtime dispatch code using the GetVariable() helper. The ArgumentParserGenerator now automatically generates side effects detection from HasSideEffect = true attributes, eliminating manual maintenance. Always update documentation when generator logic changes.
Components (see src/):
Zilf/— Main executable front end: command-line modes (compiler, interpreter, expression eval, interactive REPL). Entry:src/Zilf/Program.csand pipeline coordinatorCompiler/FrontEnd.cs.Zilf.Common/— Shared utilities, data structures, filesystem abstractions (e.g.,IFileSystem,PhysicalFileSystem, in tests alsoInMemoryFileSystem). Use these instead of rawSystem.IOin cross-component logic for testability.Zilf.Emit/— Z-machine emission layer (game builder, zap stream generation) consumed byFrontEndwhen compiling.Zapf.Parsing/+Zapf/— Assembler for intermediate.zap/.xzaptextual Z-code fragments into final story format (multi-part.zap,_data.zap,_str.zap, optional_freq.zap).ZapfAssemblerorchestrates multi-pass assembly and restart logic.Dezapf/— Disassembler (currently minimal / limited tests). Not included in packaging (Build.projexcludes it from stage).Analyzers/— Roslyn analyzers & source generators specific to ZILF (e.g.,ZilfAnalyzers.csproj,ZilfSourceGenerators). They are conditionally loaded as analyzers in consuming projects if the compiled DLL exists.Zilf.Playground/— Web/interactive playground that wrapsFrontEnd(Services/Builds/BuildService.cs,Services/Repl/ReplService.cs). Useful reference for embedding patterns.zillib/&sample/— Bundled ZIL library include paths and example games;sample/*.zilused for manual or integration scenarios.sample/adventis a canonical test case.
Data / Control Flow (compile path): FrontEnd.Compile => evaluate ZIL (parsing + interpretation for definitions) => prepare GameBuilder + ZapStreamFactory => emit .zap segments => (optionally) external assembly (ZapfAssembler) to Z-machine story file (tests demonstrate chaining in ZlrHelper).
Fast local build (solution-wide): dotnet build Zilf.sln.
Core distribution packaging (multi-RID): tools\package-all.ps1 (targets only staged projects Zilf and Zapf; analyzers & dezapf excluded).
Stage only (no packaging): dotnet msbuild Build.proj -t:Stage -p:Configuration=Release → outputs under Package/<Config>/Stage/<packageName>/ with executables + library + samples + zillib.
CI-specific properties are centralized in Directory.Build.props (version stamping, signing, warnings as errors, language version). Avoid duplicating these settings in individual .csproj files.
zilf modes (see Program.BuildContext):
- Compile:
zilf build input.zil [output.z3] - Interpret (execute no output):
zilf exec input.zil - Expression:
zilf exec -e "<expr>" - REPL:
zilf repl
Important switches: -I <dir> (include path), -t (trace routines), -d (debug info), -ws code1,code2 (suppress diagnostics), -we (warnings as errors), -W (enable noisy warnings), case sensitivity --cs/--ci.
Include path auto-augmentation: Program.AddImplicitIncludePaths heuristically adds directory of input + nearby zillib (searches upward, ignoring test dirs); prefer not to reimplement—call existing logic.
- Evaluation (interpreting top-level forms) precedes emission; errors during evaluation abort emission (
FrontEnd.InterpretOrCompile). Only proceed whenctx.ErrorCount == 0. - Hooks:
ctx.RunHook("PRE-COMPILE")thenctx.SetDefaultConstants()before building game image; if extending pipeline (e.g., extra transformation), insert after PRE-COMPILE but beforeGameBuilderinstantiation. ZapStreamFactorynames related segment files using suffixes_data,_str, and_freq(orfreqw/o underscore) and will request frequent words generation if none exists. When adding new emission outputs, mirror this naming pattern.- Game options are version-dependent (Z-machine version influences
GameOptionssubclass). ExtendMakeGameOptionswith additional Z-machine flags carefully—maintain existing switch structure.
- Framework: MSTest v3 (
Microsoft.NET.Test.Sdk,MSTest.TestFramework); integration helpers intest/Zilf.Tests.Integration/ZlrHelper.csshow canonical compile→assemble→execute flow using in-memory FS. - Some of the integration tests take a long time to run, so long that the agent system will time out. Those tests are tagged with
[TestCategory("Slow")], so you should usually exclude them from running, especially while you're iterating on something. Only let the slow tests run when you're ready to do a full test pass. - Use
InMemoryFileSystem/OverlayFileSystemfor deterministic tests; don't write to the real filesystem unless staging packaging scenarios. - Analyzer and source generator behaviors are implicitly validated by normal builds (analyzers attached conditionally via
<Analyzer Condition="Exists(...)" ...>). When adding new diagnostics, place IDs inAnalyzers/DiagnosticIds.csand create Analyzer + CodeFix pair following existing patterns. - Important: Your solution to a problem must not break existing tests. Always run the fast test suite after making changes, and fix any failures before concluding that you're finished. It is unacceptable to fix one bug by creating another.
- Start by running the fast tests only: in the workspace root, execute
dotnet test Zilf.sln -c Debug --filter "TestCategory!=Slow".- If needed, you may add additional parameters as needed (e.g.,
--logger "console;verbosity=minimal"or--logger "trx;LogFileName=test_results.trx").
- If needed, you may add additional parameters as needed (e.g.,
- Optional: If the fast tests pass, proceed to run the full test suite with
dotnet test Zilf.sln -c Debug.- Note: the full test suite includes some slow tests, which may take a minute or more to complete. Only run the full test suite when you've finished work on a task and need to validate it. Don't run the full test suite while actively iterating on code changes.
- Important: Always run tests through
Zilf.sln, not individual test projects. Individual test projects have dependency issues when run directly and require the full solution build. NEVER run an individual test project (e.g.dotnet test test/Zilf.Tests/Zilf.Tests.csproj) because it will not work.
- Source generators are validated by normal builds and tests. If the regular test suite passes (see "How to Run Tests" above), the source generators are almost certainly functioning correctly.
- While actively iterating, you may quickly validate source generator changes by running
dotnet build Zilf.slnto ensure the generators execute without errors. You may then inspect the generated files to see if your intended changes were applied correctly.
Prefer IFileSystem (see usages in FrontEnd, ZapfAssembler, tests). This enables multi-environment operation (playground, tests, CLI). New features should accept an IFileSystem rather than assuming physical disk.
Context.DiagnosticManagerdrives warnings/errors; suppression list applied from CLI-ws. To emit new diagnostics, use existingDiagnosticfactories or follow analyzer patterns.- Quiet mode suppresses banner/prompt only—do not gate diagnostic emission on
ctx.Quiet. - Diagnostic IDs and messages are centralized as integer constants in
src/Zilf/Diagnostics/InterpreterMessages.cs(errors/warnings resulting from evaluating FORMs) andsrc/Zilf/Diagnostics/CompilerMessages.cs(errors/warnings from compilation; only raised when in "compile" mode), and grouped by category (e.g. interpreter messages 0300-0399 relate to structured values). - When adding new diagnostic messages, ensure they have unique IDs, they're in the correct section and in order, they have an
[Error(...)]attribute with a template message, and that their constant names follow the existing naming conventions (mirroring the template messages, with special characters replaced by underscores).
src/Analyzers/ZilfAnalyzers.csproj & ZilfSourceGenerators provide compile-time code analysis. They're not packaged for runtime; ensure changes remain incremental and respect conditional inclusion path defined in consuming .csproj (Exists('$(ZilfAnalyzersAssembly)')).
Key Lessons for ZILF Source Generators:
- Type Hierarchy Awareness: When analyzing parameter types via reflection, always check specific derived types (e.g.,
ZilAtom) before general base types (e.g.,ZilObject) to avoid inheritance classification issues. - [Data] Attribute Handling: Parameters with
[Data]attributes pass enum values as integers and require explicit enum casting in generated code:(EnumType)intValue. - Parameter vs Argument Separation: Distinguish between method parameters (including
[Data]attributes) and actual ZIL arguments when analyzing method signatures and counting expected arguments. - Call-Specific Error Handling: Different ZILF call types require different error handling patterns:
VoidCall/ValueCall: Usec.HandleMessage()PredCall/ValuePredCall: Usec.cc.Context.HandleError()
- Generated Code Validation: Always examine the actual generated source code, not just compilation success, to ensure correct type conversions and method calls.
- Incremental Development: Build and test source generators incrementally with small method subsets before attempting full generation to catch issues early.
- Indentation: When generating multi-line code blocks, use
IndentedStringBuilderto manage indentation levels cleanly and avoid formatting issues. Do not hardcode indentation into strings. - Side Effects Generation: The ArgumentParserGenerator automatically generates
HasSideEffects(string name)method from[Builtin(HasSideEffect = true)]attributes. This eliminates manual side effect list maintenance and ensures accuracy for compiler optimizations. - Attribute-Driven Architecture: Use attributes as the single source of truth for code generation. Collect all overloads of a method name to determine aggregate properties (like side effects).
- No Runtime Reflection: All method signature and attribute analysis must happen at compile time in source generators. Runtime reflection is prohibited for performance and trimming support.
For detailed implementation guidance, see devdoc/argument-parsing-systems.md "Implementation Lessons Learned" section and devdoc/source-generator-side-effects.md for side effects detection specifics.
Do not use reflection at runtime to inspect method signatures or attributes; all such analysis must be done at compile time in the source generator. Runtime reflection checks will not work correctly.
Central version numbers: CurrentVersion.props → referenced by Directory.Build.props to create InformationalVersion (DisplayVersion logic chooses compressed form per release level). Avoid embedding versions directly in code; call Program.GetVersion() for display.
Custom MSBuild targets (Build.proj) define: Stage, Package, PackageAllRids, with multi-RID iteration using RuntimeIdentifiers. When adding a new executable project that should enter distributions, add it to <StageProjects> and ensure it supports the same RIDs.
Deterministic is disabled (false) in several .csproj files; builds rely on runtime patch updates (<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>). Don't assume stable binary hashes between builds. For perf-sensitive code (parsers, emission), keep allocations minimal and reuse spans/iterators (see patterns in tokenizer & parser code—consult those files before altering).
Use events: FrontEnd.InitializeContext and ZapfAssembler.InitializingContext to inject context changes instead of modifying core constructors.
REPL integration contract: start via FrontEnd.StartRepl() (returns IReplSession). Avoid duplicating REPL loop logic from Program.DoREPL—use the interface.
- Forgetting to add include paths leads to missing symbol diagnostics; replicate test helper pattern (add main file directory + library paths) rather than hardcoding.
- Creating
.zapoutputs directly without frequent words file triggers automatic generation; if providing a custom_freq.zap, ensure naming matches either_freqorfreqsuffix. - Accessing
Contextglobal options: always usectx.GetGlobalOption(StdAtom.*)rather than reading internal collections.
dotnet restore Zilf.sln
dotnet build Zilf.sln -c Debug
dotnet test Zilf.sln -c Debug --logger "trx;LogFileName=test_results.trx"
tools\package-all.ps1
Note: Projects now target .NET 9 with C# 13. Ensure .NET 9 SDK is installed.
- Identify correct layer (interpretation vs emission vs assembly). If it affects Z-machine binary, likely belongs in
Zilf.Emitor assembler; if syntax/semantic evaluation, in interpreter/compiler. - Expose through
ContextorGameOptionsrather than adding ad-hoc globals. - Update tests using in-memory FS; prefer integration helper pattern for end-to-end.
- New commands for use at evaluation time (i.e., intepreter commands) go in one of the categorized
src/Zilf/Interpreter/Subrs.<category>.csfiles and are marked with[Subr(...)](or[FSubr(...)]for special forms). Arguments are automatically coerced based on method signature; seesrc/Zilf/Interpreter/ArgDecoder.csfor details. - When adding new interpreter commands, ensure they have corresponding tests in
test/Zilf.Tests/Interpreterand follow existing naming and documentation conventions. - New commands for use at compile time (i.e., Z-code built-in routines) go in
src/Zilf/Compiler/Builtins/ZBuiltins.csand are marked with[Builtin(...)]. Arguments are also automatically coerced, using a different mechanism; see theValidateArguments,MakeBuiltinMethodParams, andCompileBuiltinCallmethods in that file, as well assrc/zilf/Compiler/Builtins/ParameterTypeHandler.csfor details. - When adding new compile-time built-in routines, ensure they have corresponding tests in
test/Zilf.Tests.Integrationand follow existing naming and documentation conventions. - When checking for a specific atom whose name is known at compile time, use
StdAtomenum values (e.g.,StdAtom.TELL) rather than string comparisons for performance and consistency. Add newStdAtomentries as needed insrc/Zilf/Common/StdAtom.cs.
Reference: pattern from ZlrHelper:
var fe = new FrontEnd { FileSystem = new InMemoryFileSystem() };
fe.IncludePaths.Add("");
var result = fe.Compile("main.zil", "Output.zap", wantDebugInfo: false);
if (result.Success) new ZapfAssembler { FileSystem = fe.FileSystem }.Assemble("Output.zap", "Output.zcode");- Projects target .NET 9 (
net9.0) with C# 13 language features. Source generators remain onnetstandard2.0for VS compatibility but can still use C# 13 language features. - Use nullable reference types.
- Use expression-bodied members for simple property getters and methods.
- Prefer
varwhen the type is obvious from the right side of the assignment. - Use explicit access modifiers (
public,private, etc.) on all members. - Use
this.only when necessary for disambiguation. - Use pattern matching (
is,switch) instead ofasand null checks. - Use string interpolation (
$"...") instead ofstring.Format. - Use
nameofinstead of hard-coded parameter or property names. - Use
usingdeclarations instead ofusingstatements when possible. - Use collection expressions (
[ ... ]) instead of collection initializers (new[] { ... }). - Use target-typed
new()expressions. - Use standard C# formatting, as seen in Visual Studio defaults.
- Use a maximum line length of 120 characters.
- Do not mix braces styles in an individual flow control statement; either use braces for all clauses or none.
- Comments on types and members should use XML documentation comments with appropriate tagged sections (
<summary>,<param>,<returns>,<exception>, etc.). All public types and members that you add should have an XML doc comment with at least a<summary>.
ZILF consists of an interpreter for a fairly large subset of MDL, with some additional constructs built in, plus a compiler for an embedded language which is similar to, but distinct from, MDL.
MDL is not LISP, although it has some LISP-like syntax.
The embedded language implemented by the compiler (i.e. available inside a ROUTINE), is similar to but not the same as the language implemented by the interpreter (i.e. available outside a ROUTINE). The features of the embedded language are implemented in ZILF as methods in ZBuiltins.cs marked with the [Builtin] attribute, which emit assembly code to perform the operations. The features of the interpreted language are implemented in Subrs.*.cs files marked with the [Subr] or [FSubr] attribute, which perform the operations directly in C# code.
The interpreted language is dynamically typed, and all values which can be accessed by interpreted code are implemented as subclasses of ZilObject. The embedded language is untyped, and all values exist at runtime as 16-bit words; the compiler does some static typing to facilitate optimizations, but the Z-machine itself does not enforce types. The compiler represents values as IOperand instances, which translate directly to Z-machine instruction operands and can represent constants, local or global variables, or the stack.