7.9 KiB
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
# Build the entire solution
dotnet build
# Build a specific project
dotnet build WoofWare.PawPrint/WoofWare.PawPrint.fsproj
Testing
# 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
# Format F# code using Fantomas
dotnet tool restore
dotnet fantomas .
Running the Application
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 togetherUnaryConstIlOp.fs
,UnaryMetadataIlOp.fs
,UnaryStringTokenIlOp.fs
, andNullaryIlOp.fs
IlMachineState.fs
: Manages the complete state of the abstract machineMethodState.fs
: Tracks execution state of individual methodsManagedHeap.fs
: Implements the managed memory modelAssembly.fs
: Handles reading and parsing .NET assembliesTypeInfo.fs
,TypeDefn.fs
,TypeRef.fs
: Type system implementationIlOp.fs
: IL instruction definitions and mungingEvalStack.fs
: Evaluation stack implementationCorelib.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
- Immutable State: The interpreter uses immutable F# records for all state, with state transitions returning new state objects
- Assembly Loading: Assemblies are loaded on-demand as types are referenced
- Thread Management: Each thread has its own execution state, managed through the
IlMachineState
- 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
notInt16
). - 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 (likeWithLoadedAssembly
).
Development Workflow
When adding new IL instruction support:
- Add the instruction to
IlOp.fs
- Implement execution logic in
AbstractMachine.fs
- Add a test case in
sources/
(C# file) that exercises the instruction - Add the test case to
TestCases.fs
- 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<T>()
calling AsRef<T>
, the T
refers to the outer method's generic parameter. Pass the current executing method's generics as context:
let currentMethod = state.ThreadState.[thread].MethodState.ExecutingMethod
concretizeMethodWithTypeGenerics ... currentMethod.Generics state
Field Access in Generic Contexts
When accessing EmptyArray<T>.Value
from within Array.Empty<T>()
, use both type and method generics:
let contextTypeGenerics = currentMethod.DeclaringType.Generics
let contextMethodGenerics = currentMethod.Generics
Call vs CallMethod
callMethodInActiveAssembly
expects unconcretized methods and does concretization internallycallMethod
expects already-concretized methods- The refactoring changed to concretizing before calling to ensure types are loaded
Common Pitfalls
-
Don't create new generic parameters when they already exist. It's very rarely correct to instantiate
TypeDefn.Generic{Type,Method}Parameter
yourself:// Wrong: field.DeclaringType.Generics |> List.mapi (fun i _ -> TypeDefn.GenericTypeParameter i) // Right: field.DeclaringType.Generics
-
Assembly loading context: The
loadAssembly
function expects the assembly that contains the reference as the first parameter, not the target assembly -
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 pointconcretizeGenericInstantiation
: Handles generic instantiations likeList<T>
ConcretizationContext
: Tracks state during concretization
-
IlMachineState.fs:
concretizeMethodForExecution
: Prepares methods for executionconcretizeFieldForExecution
: Prepares fields for access- Manages the flow of generic contexts through execution
-
Assembly.fs:
resolveTypeRef
: Resolves type references across assembliesresolveTypeFromName
: Handles type forwarding and exported typesresolveTypeFromExport
: Follows type forwarding chains
Debugging Type Concretization Issues
When encountering errors:
- Check the generic context (method name, generic parameters)
- Verify the assembly context being used
- Identify the TypeDefn variant being concretized
- Add logging to see generic contexts:
failwithf "Failed to concretize: %A" typeDefn
- Check if you're in a generic method calling another generic method
- Verify TypeRefs are being resolved in the correct assembly