# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview WoofWare.PawPrint is an experimental .NET runtime implementation written in F#. It's an IL interpreter designed to be: - Fully deterministic (supporting time-travel debugging and fuzzing over thread execution order) - Fully managed (reimplementing P/Invoke methods to avoid native code) - Fully in-memory except for explicit filesystem operations This is NOT a high-performance runtime - it's a very slow IL interpreter prioritizing determinism over speed. ## Common Commands ### Building ```bash # Build the entire solution dotnet build # Build a specific project dotnet build WoofWare.PawPrint/WoofWare.PawPrint.fsproj ``` ### Testing ```bash # Run all tests dotnet test # Run tests for a specific project dotnet test WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj # Run a specific test dotnet test --filter "FullyQualifiedName~TestCases" ``` ### Formatting ```bash # Format F# code using Fantomas dotnet tool restore dotnet fantomas . ``` ### Running the Application ```bash dotnet publish --self-contained --runtime-id osx-arm64 CSharpExample/ && dotnet run --project WoofWare.PawPrint.App/WoofWare.PawPrint.App.fsproj -- CSharpExample/bin/Debug/net9.0/osx-arm64/publish/CSharpExample.dll ``` ## Architecture ### Core Components **WoofWare.PawPrint** (Main Library) - `AbstractMachine.fs`: Core IL interpreter execution engine, knitting together `UnaryConstIlOp.fs`, `UnaryMetadataIlOp.fs`, `UnaryStringTokenIlOp.fs`, and `NullaryIlOp.fs` - `IlMachineState.fs`: Manages the complete state of the abstract machine - `MethodState.fs`: Tracks execution state of individual methods - `ManagedHeap.fs`: Implements the managed memory model - `Assembly.fs`: Handles reading and parsing .NET assemblies - `TypeInfo.fs`, `TypeDefn.fs`, `TypeRef.fs`: Type system implementation - `IlOp.fs`: IL instruction definitions and munging - `EvalStack.fs`: Evaluation stack implementation - `Corelib.fs`: Core library type definitions (String, Array, etc.) **WoofWare.PawPrint.Test** - Uses NUnit as the test framework - Test cases are defined in `TestCases.fs` - C# source files in `sources/` are compiled and executed by the runtime as test cases - `TestHarness.fs` provides infrastructure for running test assemblies through the interpreter **WoofWare.PawPrint.App** - Entry point application for running the interpreter ### Key Design Patterns 1. **Immutable State**: The interpreter uses immutable F# records for all state, with state transitions returning new state objects 2. **Assembly Loading**: Assemblies are loaded on-demand as types are referenced 3. **Thread Management**: Each thread has its own execution state, managed through the `IlMachineState` 4. **Type Initialization**: Classes are initialized lazily when first accessed, following .NET semantics ### Code style * Functions should be fully type-annotated, to give the most helpful error messages on type mismatches. * Generally, prefer to fully-qualify discriminated union cases in `match` statements. * ALWAYS fully-qualify enum cases when constructing them and matching on them (e.g., `PrimitiveType.Int16` not `Int16`). * When writing a "TODO" `failwith`, specify in the error message what the condition is that triggers the failure, so that a failing run can easily be traced back to its cause. * If a field name begins with an underscore (like `_LoadedAssemblies`), do not mutate it directly. Only mutate it via whatever intermediate methods have been defined for that purpose (like `WithLoadedAssembly`). ### Development Workflow When adding new IL instruction support: 1. Add the instruction to `IlOp.fs` 2. Implement execution logic in `AbstractMachine.fs` 3. Add a test case in `sources/` (C# file) that exercises the instruction 4. Add the test case to `TestCases.fs` 5. Run tests to verify implementation The project uses deterministic builds and treats warnings as errors to maintain code quality. It strongly prefers to avoid special-casing to get around problems, but instead to implement general correct solutions; cases where this has failed to happen are considered to be tech debt and at some point in the future we'll be cleaning them up. ### Common Gotchas * I've named several types in such a way as to overlap with built-in types, e.g. MethodInfo is in both WoofWare.PawPrint and System.Reflection.Metadata namespaces. Build errors can usually be fixed by fully-qualifying the type. ## Type Concretization System ### Overview Type concretization converts abstract type definitions (`TypeDefn`) to concrete runtime types (`ConcreteTypeHandle`). This is essential because IL operations need exact types at runtime, including all generic instantiations. The system separates type concretization from IL execution, ensuring types are properly loaded before use. ### Key Concepts #### Generic Parameters - **Common error**: "Generic type/method parameter X out of range" probably means you're missing the proper generic context: some caller has passed the wrong list of generics through somewhere. #### Assembly Context TypeRefs must be resolved in the context of the assembly where they're defined, not where they're used. When resolving a TypeRef, always use the assembly that contains the TypeRef in its metadata. ### Common Scenarios and Solutions #### Nested Generic Contexts When inside `Array.Empty()` calling `AsRef`, the `T` refers to the outer method's generic parameter. Pass the current executing method's generics as context: ```fsharp let currentMethod = state.ThreadState.[thread].MethodState.ExecutingMethod concretizeMethodWithTypeGenerics ... currentMethod.Generics state ``` #### Field Access in Generic Contexts When accessing `EmptyArray.Value` from within `Array.Empty()`, use both type and method generics: ```fsharp let contextTypeGenerics = currentMethod.DeclaringType.Generics let contextMethodGenerics = currentMethod.Generics ``` #### Call vs CallMethod - `callMethodInActiveAssembly` expects unconcretized methods and does concretization internally - `callMethod` expects already-concretized methods - The refactoring changed to concretizing before calling to ensure types are loaded ### Common Pitfalls 1. **Don't create new generic parameters when they already exist**. It's *very rarely* correct to instantiate `TypeDefn.Generic{Type,Method}Parameter` yourself: ```fsharp // Wrong: field.DeclaringType.Generics |> List.mapi (fun i _ -> TypeDefn.GenericTypeParameter i) // Right: field.DeclaringType.Generics ``` 2. **Assembly loading context**: The `loadAssembly` function expects the assembly that contains the reference as the first parameter, not the target assembly 3. **Type forwarding**: Use `Assembly.resolveTypeRef` which handles type forwarding and exported types correctly ### Key Files for Type System - **TypeConcretisation.fs**: Core type concretization logic - `concretizeType`: Main entry point - `concretizeGenericInstantiation`: Handles generic instantiations like `List` - `ConcretizationContext`: Tracks state during concretization - **IlMachineState.fs**: - `concretizeMethodForExecution`: Prepares methods for execution - `concretizeFieldForExecution`: Prepares fields for access - Manages the flow of generic contexts through execution - **Assembly.fs**: - `resolveTypeRef`: Resolves type references across assemblies - `resolveTypeFromName`: Handles type forwarding and exported types - `resolveTypeFromExport`: Follows type forwarding chains ### Debugging Type Concretization Issues When encountering errors: 1. Check the generic context (method name, generic parameters) 2. Verify the assembly context being used 3. Identify the TypeDefn variant being concretized 4. Add logging to see generic contexts: `failwithf "Failed to concretize: %A" typeDefn` 5. Check if you're in a generic method calling another generic method 6. Verify TypeRefs are being resolved in the correct assembly