Add test harness and run the first program (#5)

This commit is contained in:
Patrick Stevens
2025-04-28 20:23:05 +01:00
committed by GitHub
parent 90f95ed6c6
commit 3415b7a73d
14 changed files with 703 additions and 116 deletions

View File

@@ -7,32 +7,6 @@ open Microsoft.Extensions.Logging
open WoofWare.DotnetRuntimeLocator
module Program =
/// Returns the pointer to the resulting array on the heap.
let allocateArgs (args : string list) (state : IlMachineState) : ManagedHeapAddress * IlMachineState =
let argsAllocations, state =
(state, args)
||> Seq.mapFold (fun state arg -> IlMachineState.allocate (ReferenceType.String arg) state
// TODO: set the char values in memory
)
let arrayAllocation, state =
IlMachineState.allocate
(ReferenceType.Array (args.Length, Type.ReferenceType ReferenceType.ManagedObject))
state
// TODO: set the length of the array
let state =
((state, 0), argsAllocations)
||> Seq.fold (fun (state, i) arg ->
let state =
IlMachineState.setArrayValue arrayAllocation (CliObject.OfManagedObject arg) i state
state, i + 1
)
|> fst
arrayAllocation, state
let reallyMain (argv : string[]) : int =
let loggerFactory =
LoggerFactory.Create (fun builder ->
@@ -54,54 +28,8 @@ module Program =
ImmutableArray.Create (FileInfo(dllPath).Directory.FullName)
use fileStream = new FileStream (dllPath, FileMode.Open, FileAccess.Read)
let dumped = Assembly.read loggerFactory fileStream
let entryPoint =
match dumped.MainMethod with
| None -> failwith $"No entry point in {dllPath}"
| Some d -> d
let mainMethod = dumped.Methods.[entryPoint]
if mainMethod.Signature.GenericParameterCount > 0 then
failwith "Refusing to execute generic main method"
let state = IlMachineState.initial dotnetRuntimes dumped
let arrayAllocation, state =
match mainMethod.Signature.ParameterTypes |> Seq.toList with
| [ TypeDefn.OneDimensionalArrayLowerBoundZero (TypeDefn.PrimitiveType PrimitiveType.String) ] ->
allocateArgs args state
| _ -> failwith "Main method must take an array of strings; other signatures not yet implemented"
match mainMethod.Signature.ReturnType with
| TypeDefn.PrimitiveType PrimitiveType.Int32 -> ()
| _ -> failwith "Main method must return int32; other types not currently supported"
let state, mainThread =
state
|> IlMachineState.addThread
// TODO: we need to load the main method's class first, and that's a faff with the current layout
{ MethodState.Empty mainMethod None with
Arguments = ImmutableArray.Create (CliObject.OfManagedObject arrayAllocation)
}
dumped.Name
let mutable state = state
while true do
let state', whatWeDid =
AbstractMachine.executeOneStep loggerFactory state mainThread
state <- state'
match whatWeDid with
| WhatWeDid.Executed -> logger.LogInformation "Executed one step."
| WhatWeDid.SuspendedForClassInit ->
logger.LogInformation "Suspended execution of current method for class initialisation."
| WhatWeDid.NotTellingYou -> logger.LogInformation "(Execution outcome missing.)"
| WhatWeDid.BlockedOnClassInit threadBlockingUs ->
logger.LogInformation "Unable to execute because class has not yet initialised."
let terminalState = Program.run loggerFactory fileStream dotnetRuntimes args
0
| _ ->

View File

@@ -0,0 +1,21 @@
namespace WoofWare.PawPrint.Test
open System
open System.IO
open System.Reflection
[<RequireQualifiedAccess>]
module Assembly =
let getEmbeddedResource (name : string) (assy : Assembly) : Stream =
let resourceName =
assy.GetManifestResourceNames ()
|> Seq.filter (fun a -> a.EndsWith (name, StringComparison.Ordinal))
|> Seq.exactlyOne
assy.GetManifestResourceStream resourceName
let getEmbeddedResourceAsString (name : string) (assy : Assembly) : string =
use stream = getEmbeddedResource name assy
use reader = new StreamReader (stream, leaveOpen = true)
reader.ReadToEnd ()

View File

@@ -0,0 +1,64 @@
namespace WoofWare.PawPrint.Test
open System
open Microsoft.Extensions.Logging
type LogLine =
{
Level : LogLevel
LoggerName : string
Message : string
}
/// Very small, in-memory implementation of `ILoggerFactory` for unit tests.
[<RequireQualifiedAccess>]
module LoggerFactory =
/// Returns a pair: `(getLogs, loggerFactory)` where `getLogs ()` retrieves all
/// log messages emitted so far, in chronological order.
let makeTest () : (unit -> LogLine list) * ILoggerFactory =
// Shared sink for all loggers created by the factory.
let sink = ResizeArray ()
let createLogger (category : string) : ILogger =
{ new ILogger with
member _.BeginScope _state =
{ new IDisposable with
member _.Dispose () = ()
}
member _.IsEnabled _logLevel = true
member _.Log (logLevel, eventId, state, ex, formatter) =
let message =
try
formatter.Invoke (state, ex)
with _ ->
"<formatter threw>"
lock
sink
(fun () ->
{
Level = logLevel
LoggerName = category
Message = message
}
|> sink.Add
)
}
// Minimal `ILoggerFactory` that just hands out instances of the above.
let factory =
{ new ILoggerFactory with
member _.CreateLogger categoryName = createLogger categoryName
member _.AddProvider _provider = ()
member _.Dispose () = ()
}
// Expose accessor that snapshots the current sink contents.
let getLogs () = lock sink (fun () -> List.ofSeq sink)
getLogs, factory

View File

@@ -0,0 +1,58 @@
namespace WoofWare.PawPrint.Test
open System
open System.IO
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.CSharp
[<RequireQualifiedAccess>]
module Roslyn =
/// Compiles the supplied C# source strings into an in-memory PE image.
/// Raises if compilation fails.
let compile (sources : string list) : byte[] =
// Create a syntax tree per source snippet.
let parseOptions =
CSharpParseOptions.Default.WithLanguageVersion (LanguageVersion.Preview)
let syntaxTrees : SyntaxTree[] =
sources
|> List.mapi (fun idx src ->
let fileName = $"File{idx}.cs"
CSharpSyntaxTree.ParseText (src, parseOptions, fileName)
)
|> List.toArray
// Reference every assembly found in the runtime directory crude but
// guarantees we can resolve System.* et al.
let runtimeDir =
System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory ()
let metadataReferences : MetadataReference[] =
Directory.GetFiles (runtimeDir, "*.dll")
|> Array.map (fun path -> MetadataReference.CreateFromFile path :> MetadataReference)
let compilationOptions = CSharpCompilationOptions (OutputKind.ConsoleApplication)
let compilation =
CSharpCompilation.Create (
assemblyName = "PawPrintTestAssembly",
syntaxTrees = syntaxTrees,
references = metadataReferences,
options = compilationOptions
)
use peStream = new MemoryStream ()
let emitResult = compilation.Emit (peStream)
if emitResult.Success then
peStream.ToArray ()
else
let diagnostics =
emitResult.Diagnostics
|> Seq.filter (fun d -> d.Severity = DiagnosticSeverity.Error)
|> Seq.map (fun d -> d.ToString ())
|> String.concat Environment.NewLine
failwith $"Compilation failed:\n{diagnostics}"

View File

@@ -0,0 +1,17 @@
namespace WoofWare.PawPrint.Test
open WoofWare.PawPrint
/// Result of executing (some steps of) the program under PawPrint.
type RunResult =
{
/// Value that was left on the evaluation stack when execution stopped, **if**
/// the program executed a `ret` that produced a value and PawPrint
/// subsequently pushed it onto the stack. This is only an early-stage
/// approximation: once PawPrint supports a proper process-exit story we
/// can promote this to a real exitcode.
ExitCode : int option
/// Final interpreter state after we stopped executing.
FinalState : IlMachineState
}

View File

@@ -1,9 +1,40 @@
namespace WoofWare.Pawprint.Test
open System.Collections.Immutable
open System.IO
open FsUnitTyped
open NUnit.Framework
open WoofWare.PawPrint
open WoofWare.PawPrint.Test
[<TestFixture>]
module TestThing =
let assy = typeof<RunResult>.Assembly
[<Test>]
let ``foo is a thing`` () = 1 |> shouldEqual 1
let ``Can run a no-op`` () : unit =
let source = Assembly.getEmbeddedResourceAsString "NoOp.cs" assy
let image = Roslyn.compile [ source ]
let messages, loggerFactory = LoggerFactory.makeTest ()
let dotnetRuntimes =
// TODO: work out which runtime it expects to use, parsing the runtimeconfig etc and using DotnetRuntimeLocator. For now we assume we're self-contained.
// DotnetEnvironmentInfo.Get().Frameworks
// |> Seq.map (fun fi -> Path.Combine (fi.Path, fi.Version.ToString ()))
// |> ImmutableArray.CreateRange
ImmutableArray.Create (FileInfo(assy.Location).Directory.FullName)
use peImage = new MemoryStream (image)
let terminalState, terminatingThread =
Program.run loggerFactory peImage (ImmutableArray.CreateRange []) []
let exitCode =
match terminalState.ThreadState.[terminatingThread].MethodState.EvaluationStack.Values with
| [] -> failwith "expected program to return 1, but it returned void"
| head :: _ ->
match head with
| EvalStackValue.Int32 i -> i
| _ -> failwith "TODO"
exitCode |> shouldEqual 1

View File

@@ -10,7 +10,12 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="LoggerFactory.fs" />
<Compile Include="Assembly.fs" />
<Compile Include="Roslyn.fs" />
<Compile Include="TestHarness.fs"/>
<Compile Include="TestThing.fs"/>
<EmbeddedResource Include="sources\NoOp.cs" />
</ItemGroup>
<ItemGroup>
@@ -22,6 +27,8 @@
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0"/>
<PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
using System;
namespace HelloWorldApp
{
class Program
{
static int Main(string[] args)
{
return 1;
}
}
}

View File

@@ -88,6 +88,17 @@ type EvalStack =
Values = []
}
static member Pop (stack : EvalStack) : EvalStackValue * EvalStack =
match stack.Values with
| [] -> failwith "eval stack was empty on pop instruction"
| v :: rest ->
let stack =
{
Values = rest
}
v, stack
static member Push (v : CliObject) (stack : EvalStack) =
let v =
match v with
@@ -134,25 +145,110 @@ and MethodState =
ReturnState : MethodReturnState option
}
static member advanceProgramCounter (state : MethodState) =
static member jumpProgramCounter (bytes : int) (state : MethodState) =
{ state with
IlOpIndex =
state.IlOpIndex
+ (IlOp.NumberOfBytes state.ExecutingMethod.Locations.[state.IlOpIndex])
IlOpIndex = state.IlOpIndex + bytes
}
static member advanceProgramCounter (state : MethodState) =
MethodState.jumpProgramCounter (IlOp.NumberOfBytes state.ExecutingMethod.Locations.[state.IlOpIndex]) state
static member loadArgument (index : int) (state : MethodState) : MethodState =
// Correct CIL guarantees that we are loading an argument from an index that exists.
{ state with
EvaluationStack = state.EvaluationStack |> EvalStack.Push state.Arguments.[index]
}
static member popFromStack (localVariableIndex : int) (state : MethodState) : MethodState =
if localVariableIndex >= state.LocalVariables.Length then
failwith
$"Tried to access zero-indexed local variable %i{localVariableIndex} but only %i{state.LocalVariables.Length} exist"
if localVariableIndex < 0 || localVariableIndex >= 65535 then
failwith $"Incorrect CIL encountered: local variable index has value %i{localVariableIndex}"
let popped, newStack = EvalStack.Pop state.EvaluationStack
let desiredValue =
match state.LocalVariables.[localVariableIndex] with
| Basic (BasicCliObject.Int32 _) ->
match popped with
| EvalStackValue.Int32 i -> CliObject.Basic (BasicCliObject.Int32 i)
| EvalStackValue.Int64 int64 -> failwith "todo"
| EvalStackValue.NativeInt int64 -> failwith "todo"
| EvalStackValue.Float f -> failwith "todo"
| EvalStackValue.ManagedPointer managedHeapAddressOption -> failwith "todo"
| EvalStackValue.ObjectRef managedHeapAddress -> failwith "todo"
| EvalStackValue.TransientPointer i -> failwith "todo"
| EvalStackValue.UserDefinedValueType -> failwith "todo"
| Basic (BasicCliObject.Int64 _) -> failwith "todo"
| Basic (BasicCliObject.NativeFloat _) -> failwith "todo"
| Basic (BasicCliObject.NativeInt _) -> failwith "todo"
| Basic (BasicCliObject.ObjectReference _) -> failwith "todo"
| Basic (BasicCliObject.PointerType _) -> failwith "todo"
| Bool b -> failwith "todo"
| Char (b, b1) -> failwith "todo"
| UInt8 b -> failwith "todo"
| UInt16 s -> failwith "todo"
| Int8 b -> failwith "todo"
| Int16 s -> failwith "todo"
| Float32 f -> failwith "todo"
| Float64 f -> failwith "todo"
{ state with
EvaluationStack = newStack
LocalVariables = state.LocalVariables.SetItem (localVariableIndex, desiredValue)
}
static member Empty (method : WoofWare.PawPrint.MethodInfo) (returnState : MethodReturnState option) =
let localVariableSig =
match method.LocalVars with
| None -> ImmutableArray.Empty
| Some vars -> vars
// I think valid code should remain valid if we unconditionally localsInit - it should be undefined
// to use an uninitialised value? Not checked this; TODO.
let localVars =
localVariableSig
|> Seq.map (fun var ->
match var with
| TypeDefn.PrimitiveType primitiveType ->
match primitiveType with
| PrimitiveType.Void -> failwith "todo"
| PrimitiveType.Boolean -> CliObject.Bool 0uy
| PrimitiveType.Char -> failwith "todo"
| PrimitiveType.SByte -> failwith "todo"
| PrimitiveType.Byte -> failwith "todo"
| PrimitiveType.Int16 -> failwith "todo"
| PrimitiveType.UInt16 -> failwith "todo"
| PrimitiveType.Int32 -> CliObject.Basic (BasicCliObject.Int32 0)
| PrimitiveType.UInt32 -> failwith "todo"
| PrimitiveType.Int64 -> CliObject.Basic (BasicCliObject.Int64 0L)
| PrimitiveType.UInt64 -> failwith "todo"
| PrimitiveType.Single -> failwith "todo"
| PrimitiveType.Double -> failwith "todo"
| PrimitiveType.String -> failwith "todo"
| PrimitiveType.TypedReference -> failwith "todo"
| PrimitiveType.IntPtr -> failwith "todo"
| PrimitiveType.UIntPtr -> failwith "todo"
| PrimitiveType.Object -> failwith "todo"
| TypeDefn.Array (elt, shape) -> failwith "todo"
| TypeDefn.Pinned typeDefn -> failwith "todo"
| TypeDefn.Pointer typeDefn -> failwith "todo"
| TypeDefn.Byref typeDefn -> failwith "todo"
| TypeDefn.OneDimensionalArrayLowerBoundZero elements -> failwith "todo"
| TypeDefn.Modified (original, afterMod, modificationRequired) -> failwith "todo"
| TypeDefn.FromReference signatureTypeKind -> CliObject.Basic (BasicCliObject.ObjectReference None)
| TypeDefn.FromDefinition signatureTypeKind -> failwith "todo"
| TypeDefn.GenericInstantiation (generic, args) -> failwith "todo"
| TypeDefn.FunctionPointer typeMethodSignature -> failwith "todo"
| TypeDefn.GenericTypeParameter index -> failwith "todo"
| TypeDefn.GenericMethodParameter index -> failwith "todo"
)
|> ImmutableArray.CreateRange
{
EvaluationStack = EvalStack.Empty
LocalVariables =
// TODO: use method.LocalsInit
ImmutableArray.Empty
LocalVariables = localVars
IlOpIndex = 0
Arguments = Array.zeroCreate method.Parameters.Length |> ImmutableArray.ToImmutableArray
ExecutingMethod = method
@@ -749,6 +845,30 @@ module IlMachineState =
)
}
let popFromStackToLocalVariable
(thread : ThreadId)
(localVariableIndex : int)
(state : IlMachineState)
: IlMachineState
=
let threadState =
match Map.tryFind thread state.ThreadState with
| None -> failwith "Logic error: tried to pop from stack of nonexistent thread"
| Some threadState -> threadState
let methodState =
MethodState.popFromStack localVariableIndex threadState.MethodState
{ state with
ThreadState =
state.ThreadState
|> Map.add
thread
{ threadState with
MethodState = methodState
}
}
let setArrayValue
(arrayAllocation : ManagedHeapAddress)
(v : CliObject)
@@ -780,6 +900,23 @@ module IlMachineState =
)
}
let jumpProgramCounter (thread : ThreadId) (bytes : int) (state : IlMachineState) : IlMachineState =
{ state with
ThreadState =
state.ThreadState
|> Map.change
thread
(fun state ->
match state with
| None -> failwith "expected state"
| Some (state : ThreadState) ->
{ state with
MethodState = state.MethodState |> MethodState.jumpProgramCounter bytes
}
|> Some
)
}
let loadArgument (thread : ThreadId) (index : int) (state : IlMachineState) : IlMachineState =
{ state with
ThreadState =
@@ -797,6 +934,10 @@ module IlMachineState =
)
}
type ExecutionResult =
| Terminated of IlMachineState * terminatingThread : ThreadId
| Stepped of IlMachineState * WhatWeDid
[<RequireQualifiedAccess>]
module AbstractMachine =
type private Dummy = class end
@@ -805,43 +946,76 @@ module AbstractMachine =
(state : IlMachineState)
(currentThread : ThreadId)
(op : NullaryIlOp)
: IlMachineState * WhatWeDid
: ExecutionResult
=
match op with
| Nop -> IlMachineState.advanceProgramCounter currentThread state, WhatWeDid.Executed
| Nop ->
(IlMachineState.advanceProgramCounter currentThread state, WhatWeDid.Executed)
|> ExecutionResult.Stepped
| LdArg0 ->
state
|> IlMachineState.loadArgument currentThread 0
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdArg1 ->
state
|> IlMachineState.loadArgument currentThread 1
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdArg2 ->
state
|> IlMachineState.loadArgument currentThread 2
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdArg3 ->
state
|> IlMachineState.loadArgument currentThread 3
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
| Ldloc_0 -> failwith "todo"
| Ldloc_1 -> failwith "todo"
| Ldloc_2 -> failwith "todo"
| Ldloc_3 -> failwith "todo"
|> ExecutionResult.Stepped
| Ldloc_0 ->
let localVar = state.ThreadState.[currentThread].MethodState.LocalVariables.[0]
state
|> IlMachineState.pushToEvalStack localVar currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Ldloc_1 ->
let localVar = state.ThreadState.[currentThread].MethodState.LocalVariables.[1]
state
|> IlMachineState.pushToEvalStack localVar currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Ldloc_2 ->
let localVar = state.ThreadState.[currentThread].MethodState.LocalVariables.[2]
state
|> IlMachineState.pushToEvalStack localVar currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Ldloc_3 ->
let localVar = state.ThreadState.[currentThread].MethodState.LocalVariables.[3]
state
|> IlMachineState.pushToEvalStack localVar currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Pop -> failwith "todo"
| Dup -> failwith "todo"
| Ret ->
let threadStateAtEndOfMethod = state.ThreadState.[currentThread]
let returnState =
match threadStateAtEndOfMethod.MethodState.ReturnState with
| None -> failwith "Program finished execution?"
| Some returnState -> returnState
match threadStateAtEndOfMethod.MethodState.ReturnState with
| None -> ExecutionResult.Terminated (state, currentThread)
| Some returnState ->
let state =
match returnState.WasInitialising with
@@ -868,7 +1042,7 @@ module AbstractMachine =
match threadStateAtEndOfMethod.MethodState.EvaluationStack.Values with
| [] ->
// no return value
state, WhatWeDid.Executed
(state, WhatWeDid.Executed) |> ExecutionResult.Stepped
| [ retVal ] ->
let retType =
threadStateAtEndOfMethod.MethodState.ExecutingMethod.Signature.ReturnType
@@ -876,19 +1050,65 @@ module AbstractMachine =
state
|> IlMachineState.pushToStackCoerced retVal retType currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| vals ->
failwith
"Unexpected interpretation result has a local evaluation stack with more than one element on RET"
| LdcI4_0 -> failwith "todo"
| LdcI4_1 -> failwith "todo"
| LdcI4_2 -> failwith "todo"
| LdcI4_3 -> failwith "todo"
| LdcI4_4 -> failwith "todo"
| LdcI4_5 -> failwith "todo"
| LdcI4_6 -> failwith "todo"
| LdcI4_7 -> failwith "todo"
| LdcI4_8 -> failwith "todo"
| LdcI4_0 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 0)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_1 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 1)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_2 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 2)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_3 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 3)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_4 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 4)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_5 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 5)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_6 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 6)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_7 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 7)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_8 ->
state
|> IlMachineState.pushToEvalStack (CliObject.Basic (BasicCliObject.Int32 8)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| LdcI4_m1 -> failwith "todo"
| LdNull -> failwith "todo"
| Ceq -> failwith "todo"
@@ -896,10 +1116,30 @@ module AbstractMachine =
| Cgt_un -> failwith "todo"
| Clt -> failwith "todo"
| Clt_un -> failwith "todo"
| Stloc_0 -> failwith "todo"
| Stloc_1 -> failwith "todo"
| Stloc_2 -> failwith "todo"
| Stloc_3 -> failwith "todo"
| Stloc_0 ->
state
|> IlMachineState.popFromStackToLocalVariable currentThread 0
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Stloc_1 ->
state
|> IlMachineState.popFromStackToLocalVariable currentThread 1
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Stloc_2 ->
state
|> IlMachineState.popFromStackToLocalVariable currentThread 2
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Stloc_3 ->
state
|> IlMachineState.popFromStackToLocalVariable currentThread 3
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Sub -> failwith "todo"
| Sub_ovf -> failwith "todo"
| Sub_ovf_un -> failwith "todo"
@@ -1197,12 +1437,73 @@ module AbstractMachine =
|> IlMachineState.advanceProgramCounter thread
|> Tuple.withRight WhatWeDid.Executed
let executeOneStep
(loggerFactory : ILoggerFactory)
let private executeUnaryConst
(state : IlMachineState)
(thread : ThreadId)
(currentThread : ThreadId)
(op : UnaryConstIlOp)
: IlMachineState * WhatWeDid
=
match op with
| Stloc s ->
state
|> IlMachineState.popFromStackToLocalVariable currentThread (int s)
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
| Stloc_s b ->
state
|> IlMachineState.popFromStackToLocalVariable currentThread (int b)
|> IlMachineState.advanceProgramCounter currentThread
|> Tuple.withRight WhatWeDid.Executed
| Ldc_I8 int64 -> failwith "todo"
| Ldc_I4 i -> failwith "todo"
| Ldc_R4 f -> failwith "todo"
| Ldc_R8 f -> failwith "todo"
| Ldc_I4_s b -> failwith "todo"
| Br i -> failwith "todo"
| Br_s b ->
state
|> IlMachineState.advanceProgramCounter currentThread
|> IlMachineState.jumpProgramCounter currentThread (int b)
|> Tuple.withRight WhatWeDid.Executed
| Brfalse_s b -> failwith "todo"
| Brtrue_s b -> failwith "todo"
| Brfalse i -> failwith "todo"
| Brtrue i -> failwith "todo"
| Beq_s b -> failwith "todo"
| Blt_s b -> failwith "todo"
| Ble_s b -> failwith "todo"
| Bgt_s b -> failwith "todo"
| Bge_s b -> failwith "todo"
| Beq i -> failwith "todo"
| Blt i -> failwith "todo"
| Ble i -> failwith "todo"
| Bgt i -> failwith "todo"
| Bge i -> failwith "todo"
| Bne_un_s b -> failwith "todo"
| Bge_un_s b -> failwith "todo"
| Bgt_un_s b -> failwith "todo"
| Ble_un_s b -> failwith "todo"
| Blt_un_s b -> failwith "todo"
| Bne_un i -> failwith "todo"
| Bge_un i -> failwith "todo"
| Bgt_un i -> failwith "todo"
| Ble_un i -> failwith "todo"
| Blt_un i -> failwith "todo"
| Ldloc_s b -> failwith "todo"
| Ldloca_s b -> failwith "todo"
| Ldarga s -> failwith "todo"
| Ldarg_s b -> failwith "todo"
| Ldarga_s b -> failwith "todo"
| Leave i -> failwith "todo"
| Leave_s b -> failwith "todo"
| Starg_s b -> failwith "todo"
| Starg s -> failwith "todo"
| Unaligned b -> failwith "todo"
| Ldloc s -> failwith "todo"
| Ldloca s -> failwith "todo"
| Ldarg s -> failwith "todo"
let executeOneStep (loggerFactory : ILoggerFactory) (state : IlMachineState) (thread : ThreadId) : ExecutionResult =
let logger = loggerFactory.CreateLogger typeof<Dummy>.DeclaringType
let instruction = state.ThreadState.[thread].MethodState
@@ -1219,9 +1520,11 @@ module AbstractMachine =
match instruction.ExecutingMethod.Locations.[instruction.IlOpIndex] with
| IlOp.Nullary op -> executeNullary state thread op
| UnaryConst unaryConstIlOp -> failwith "todo"
| UnaryMetadataToken (unaryMetadataTokenIlOp, bytes) ->
| IlOp.UnaryConst unaryConstIlOp -> executeUnaryConst state thread unaryConstIlOp |> ExecutionResult.Stepped
| IlOp.UnaryMetadataToken (unaryMetadataTokenIlOp, bytes) ->
executeUnaryMetadata loggerFactory unaryMetadataTokenIlOp bytes state thread
| Switch immutableArray -> failwith "todo"
| UnaryStringToken (unaryStringTokenIlOp, stringHandle) ->
|> ExecutionResult.Stepped
| IlOp.Switch immutableArray -> failwith "todo"
| IlOp.UnaryStringToken (unaryStringTokenIlOp, stringHandle) ->
executeUnaryStringToken unaryStringTokenIlOp stringHandle state thread
|> ExecutionResult.Stepped

View File

@@ -2,6 +2,7 @@ namespace WoofWare.PawPrint
#nowarn "9"
open System
open System.Collections.Immutable
open System.Reflection
open System.Reflection.Metadata
@@ -56,7 +57,7 @@ type GenericParameter =
/// <summary>
/// The zero-based index of the generic parameter in the generic parameter list.
/// For example, in Dictionary<TKey, TValue>, TKey has index 0 and TValue has index 1.
/// For example, in Dictionary&lt;TKey, TValue&rt;, TKey has index 0 and TValue has index 1.
/// </summary>
SequenceNumber : int
}
@@ -136,6 +137,8 @@ type MethodInfo =
/// </summary>
LocalsInit : bool
LocalVars : ImmutableArray<TypeDefn> option
/// <summary>
/// Whether this method is static (true) or an instance method (false).
/// </summary>
@@ -150,6 +153,7 @@ module MethodInfo =
{
Instructions : (IlOp * int) list
LocalInit : bool
LocalSig : ImmutableArray<TypeDefn> option
MaxStackSize : int
ExceptionRegions : ImmutableArray<ExceptionRegion>
}
@@ -171,11 +175,26 @@ module MethodInfo =
else
LanguagePrimitives.EnumOfValue (uint16 op)
let private readMethodBody (peReader : PEReader) (methodDef : MethodDefinition) : MethodBody option =
let private readMethodBody
(peReader : PEReader)
(metadataReader : MetadataReader)
(methodDef : MethodDefinition)
: MethodBody option
=
if methodDef.RelativeVirtualAddress = 0 then
None
else
let methodBody = peReader.GetMethodBody methodDef.RelativeVirtualAddress
let localSig =
let s = methodBody.LocalSignature |> metadataReader.GetStandaloneSignature
// :sob: why are all the useful methods internal
try
s.Signature |> ignore<BlobHandle> |> Some
with :? BadImageFormatException ->
None
|> Option.map (fun () -> s.DecodeLocalSignature (TypeDefn.typeProvider, ()))
let ilBytes = methodBody.GetILBytes ()
use bytes = fixed ilBytes
let mutable reader : BlobReader = BlobReader (bytes, ilBytes.Length)
@@ -459,6 +478,7 @@ module MethodInfo =
{
Instructions = instructions
LocalInit = methodBody.LocalVariablesInitialized
LocalSig = localSig
MaxStackSize = methodBody.MaxStack
ExceptionRegions = methodBody.ExceptionRegions
}
@@ -475,7 +495,7 @@ module MethodInfo =
let methodDef = metadataReader.GetMethodDefinition methodHandle
let methodName = metadataReader.GetString methodDef.Name
let methodSig = methodDef.DecodeSignature (TypeDefn.typeProvider, ())
let methodBody = readMethodBody peReader methodDef
let methodBody = readMethodBody peReader metadataReader methodDef
let declaringType = methodDef.GetDeclaringType ()
match methodBody with
@@ -501,6 +521,7 @@ module MethodInfo =
Signature = TypeMethodSignature.make methodSig
IsPinvokeImpl = methodDef.Attributes.HasFlag MethodAttributes.PinvokeImpl
LocalsInit = methodBody.LocalInit
LocalVars = methodBody.LocalSig
IsStatic = not methodSig.Header.IsInstance
}
|> Some

View File

@@ -0,0 +1,94 @@
namespace WoofWare.PawPrint
open System.Collections.Immutable
open System.IO
open Microsoft.Extensions.Logging
[<RequireQualifiedAccess>]
module Program =
/// Returns the pointer to the resulting array on the heap.
let allocateArgs (args : string list) (state : IlMachineState) : ManagedHeapAddress * IlMachineState =
let argsAllocations, state =
(state, args)
||> Seq.mapFold (fun state arg -> IlMachineState.allocate (ReferenceType.String arg) state
// TODO: set the char values in memory
)
let arrayAllocation, state =
IlMachineState.allocate
(ReferenceType.Array (args.Length, Type.ReferenceType ReferenceType.ManagedObject))
state
// TODO: set the length of the array
let state =
((state, 0), argsAllocations)
||> Seq.fold (fun (state, i) arg ->
let state =
IlMachineState.setArrayValue arrayAllocation (CliObject.OfManagedObject arg) i state
state, i + 1
)
|> fst
arrayAllocation, state
/// Returns the abstract machine's state at the end of execution, together with the thread which
/// caused execution to end.
let run
(loggerFactory : ILoggerFactory)
(fileStream : Stream)
(dotnetRuntimeDirs : ImmutableArray<string>)
(argv : string list)
: IlMachineState * ThreadId
=
let logger = loggerFactory.CreateLogger "Program"
let dumped = Assembly.read loggerFactory fileStream
let entryPoint =
match dumped.MainMethod with
| None -> failwith "No entry point in input DLL"
| Some d -> d
let mainMethod = dumped.Methods.[entryPoint]
if mainMethod.Signature.GenericParameterCount > 0 then
failwith "Refusing to execute generic main method"
let state = IlMachineState.initial dotnetRuntimeDirs dumped
let arrayAllocation, state =
match mainMethod.Signature.ParameterTypes |> Seq.toList with
| [ TypeDefn.OneDimensionalArrayLowerBoundZero (TypeDefn.PrimitiveType PrimitiveType.String) ] ->
allocateArgs argv state
| _ -> failwith "Main method must take an array of strings; other signatures not yet implemented"
match mainMethod.Signature.ReturnType with
| TypeDefn.PrimitiveType PrimitiveType.Int32 -> ()
| _ -> failwith "Main method must return int32; other types not currently supported"
let state, mainThread =
state
|> IlMachineState.addThread
// TODO: we need to load the main method's class first, and that's a faff with the current layout
{ MethodState.Empty mainMethod None with
Arguments = ImmutableArray.Create (CliObject.OfManagedObject arrayAllocation)
}
dumped.Name
let rec go (state : IlMachineState) =
match AbstractMachine.executeOneStep loggerFactory state mainThread with
| ExecutionResult.Terminated (state, terminatingThread) -> state, terminatingThread
| ExecutionResult.Stepped (state', whatWeDid) ->
match whatWeDid with
| WhatWeDid.Executed -> logger.LogInformation "Executed one step."
| WhatWeDid.SuspendedForClassInit ->
logger.LogInformation "Suspended execution of current method for class initialisation."
| WhatWeDid.NotTellingYou -> logger.LogInformation "(Execution outcome missing.)"
| WhatWeDid.BlockedOnClassInit threadBlockingUs ->
logger.LogInformation "Unable to execute because class has not yet initialised."
go state'
go state

View File

@@ -158,7 +158,7 @@ module TypeDefn =
| SignatureTypeCode.Pinned -> failwith "todo"
| x -> failwith $"Unrecognised type code: {x}"
let typeProvider =
let typeProvider : ISignatureTypeProvider<TypeDefn, unit> =
{ new ISignatureTypeProvider<TypeDefn, unit> with
member this.GetArrayType (elementType : TypeDefn, shape : ArrayShape) : TypeDefn =
TypeDefn.Array (elementType, shape)

View File

@@ -21,6 +21,7 @@
<Compile Include="TypeInfo.fs" />
<Compile Include="Assembly.fs" />
<Compile Include="AbstractMachine.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>

View File

@@ -54,6 +54,21 @@
"version": "8.0.12",
"hash": "sha256-P7gJnRkTZT0+xx+VOjn0MX/CURjk/JdT5ArGC6mQ3Qw="
},
{
"pname": "Microsoft.CodeAnalysis.Analyzers",
"version": "3.3.4",
"hash": "sha256-qDzTfZBSCvAUu9gzq2k+LOvh6/eRvJ9++VCNck/ZpnE="
},
{
"pname": "Microsoft.CodeAnalysis.Common",
"version": "4.8.0",
"hash": "sha256-3IEinVTZq6/aajMVA8XTRO3LTIEt0PuhGyITGJLtqz4="
},
{
"pname": "Microsoft.CodeAnalysis.CSharp",
"version": "4.8.0",
"hash": "sha256-MmOnXJvd/ezs5UPcqyGLnbZz5m+VedpRfB+kFZeeqkU="
},
{
"pname": "Microsoft.CodeCoverage",
"version": "17.13.0",
@@ -184,6 +199,11 @@
"version": "5.0.0",
"hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0="
},
{
"pname": "System.Collections.Immutable",
"version": "7.0.0",
"hash": "sha256-9an2wbxue2qrtugYES9awshQg+KfJqajhnhs45kQIdk="
},
{
"pname": "System.Diagnostics.DiagnosticSource",
"version": "5.0.0",
@@ -198,5 +218,15 @@
"pname": "System.Reflection.Metadata",
"version": "1.6.0",
"hash": "sha256-JJfgaPav7UfEh4yRAQdGhLZF1brr0tUWPl6qmfNWq/E="
},
{
"pname": "System.Reflection.Metadata",
"version": "7.0.0",
"hash": "sha256-GwAKQhkhPBYTqmRdG9c9taqrKSKDwyUgOEhWLKxWNPI="
},
{
"pname": "System.Runtime.CompilerServices.Unsafe",
"version": "6.0.0",
"hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
}
]