Code/Wasm/Text/ScriptRunner.cs
// using System;
// using System.Collections.Generic;
// using System.IO;
// using System.Linq;
// using WasmBox.Pixie;
// using WasmBox.Pixie.Markup;
// using WasmBox.Wasm.Interpret;

// namespace WasmBox.Wasm.Text
// {
//     /// <summary>
//     /// Maintains state for and runs a single WebAssembly test script.
//     /// </summary>
//     public sealed class ScriptRunner
//     {
//         /// <summary>
//         /// Creates a new script runner.
//         /// </summary>
//         /// <param name="log">A log to send diagnostics to.</param>
//         /// <param name="compiler">A compiler to use for compiling modules.</param>
//         public ScriptRunner(ILog log, Func<ModuleCompiler> compiler = null)
//         {
//             this.Log = log;
//             this.Assembler = new Assembler(log);
//             this.Compiler = compiler;
//             this.moduleInstances = new List<ModuleInstance>();
//             this.moduleInstancesByName = new Dictionary<string, ModuleInstance>();
//             this.importer = new NamespacedImporter();
//             this.importer.RegisterImporter("spectest", new SpecTestImporter(new StringWriter()));
//         }

//         /// <summary>
//         /// Gets a log to which this script runner sends diagnostics.
//         /// </summary>
//         /// <value>A log.</value>
//         public ILog Log { get; private set; }

//         /// <summary>
//         /// Gets the assembler that assembles modules for this script runner.
//         /// </summary>
//         /// <value>A WebAssembly text format assembler.</value>
//         public Assembler Assembler { get; private set; }

//         /// <summary>
//         /// Gets the type of compiler to use.
//         /// </summary>
//         /// <value>A function that produces a module compiler.</value>
//         public Func<ModuleCompiler> Compiler { get; private set; }

//         private NamespacedImporter importer;

//         private List<ModuleInstance> moduleInstances;
//         private Dictionary<string, ModuleInstance> moduleInstancesByName;

//         /// <summary>
//         /// A data structure that tallies the number of tests that were run.
//         /// </summary>
//         public struct TestStatistics
//         {
//             /// <summary>
//             /// Initializes an instance of the <see cref="TestStatistics"/> type.
//             /// </summary>
//             /// <param name="successfulCommandCount">
//             /// The number of commands that were executed successfully.
//             /// </param>
//             /// <param name="failedCommandCount">
//             /// The number of commands that were executed unsuccessfully.
//             /// </param>
//             /// <param name="unknownCommandCount">
//             /// The number of command expressions that were skipped because they were not recognized as a known command.
//             /// </param>
//             public TestStatistics(int successfulCommandCount, int failedCommandCount, int unknownCommandCount)
//             {
//                 this.SuccessfulCommandCount = successfulCommandCount;
//                 this.FailedCommandCount = failedCommandCount;
//                 this.UnknownCommandCount = unknownCommandCount;
//             }

//             /// <summary>
//             /// Gets the number of commands that were recognized and successfully executed.
//             /// </summary>
//             /// <value>The number of successfully executed commands.</value>
//             public int SuccessfulCommandCount { get; private set; }

//             /// <summary>
//             /// Gets the number of commands that were recognized and executed, but then errored in some way.
//             /// </summary>
//             /// <value>The number of commands that failed.</value>
//             public int FailedCommandCount { get; private set; }

//             /// <summary>
//             /// Gets the number of command expressions that could not be recognized as a
//             /// known command and were hence skipped.
//             /// </summary>
//             /// <value>The number of unrecognized commands.</value>
//             public int UnknownCommandCount { get; private set; }

//             /// <summary>
//             /// Gets the total number of commands expressions that were encountered.
//             /// </summary>
//             public int TotalCommandCount => SuccessfulCommandCount + FailedCommandCount + UnknownCommandCount;

//             /// <summary>
//             /// Computes the elementwise sum of two test statistics.
//             /// </summary>
//             /// <param name="left">A first set of test statistics.</param>
//             /// <param name="right">A second set of test statistics.</param>
//             /// <returns>The elementwise sum of <paramref name="left"/> and <paramref name="right"/>.</returns>
//             public static TestStatistics operator+(TestStatistics left, TestStatistics right)
//             {
//                 return new TestStatistics(
//                     left.SuccessfulCommandCount + right.SuccessfulCommandCount,
//                     left.FailedCommandCount + right.FailedCommandCount,
//                     left.UnknownCommandCount + right.UnknownCommandCount);
//             }

//             /// <summary>
//             /// An empty set of test statistics: the test statistics for a file that
//             /// does not execute any commands.
//             /// </summary>
//             public static readonly TestStatistics Empty = new TestStatistics(0, 0, 0);

//             /// <summary>
//             /// A set of test statistics that represent the successful execution of a single command.
//             /// </summary>
//             public static readonly TestStatistics SingleSuccess = new TestStatistics(1, 0, 0);

//             /// <summary>
//             /// A set of test statistics that represent a single failed command.
//             /// </summary>
//             public static readonly TestStatistics SingleFailure = new TestStatistics(0, 1, 0);

//             /// <summary>
//             /// A set of test statistics that represent a single unknown command.
//             /// </summary>
//             public static readonly TestStatistics SingleUnknown = new TestStatistics(0, 0, 1);

//             /// <inheritdoc/>
//             public override string ToString()
//             {
//                 return $"total: {TotalCommandCount}, successes: {SuccessfulCommandCount}, " +
//                     $"failures: {FailedCommandCount}, unknown: {UnknownCommandCount}";
//             }
//         }

//         /// <summary>
//         /// Runs a script, encoded as a sequence of expressions.
//         /// </summary>
//         /// <param name="expressions">The script, parsed as a sequence of expressions.</param>
//         public TestStatistics Run(IEnumerable<SExpression> expressions)
//         {
//             var results = TestStatistics.Empty;
//             foreach (var item in expressions)
//             {
//                 results += Run(item);
//             }
//             return results;
//         }

//         /// <summary>
//         /// Runs a script, encoded as a sequence of tokens.
//         /// </summary>
//         /// <param name="tokens">The script, parsed as a sequence of tokens.</param>
//         public TestStatistics Run(IEnumerable<Lexer.Token> tokens)
//         {
//             return Run(Parser.ParseAsSExpressions(tokens, Log));
//         }

//         /// <summary>
//         /// Runs a script, encoded as a string.
//         /// </summary>
//         /// <param name="script">The text of the script to run.</param>
//         /// <param name="scriptName">The file name of the script to run.</param>
//         public TestStatistics Run(string script, string scriptName = "<string>")
//         {
//             return Run(Lexer.Tokenize(script, scriptName));
//         }

//         /// <summary>
//         /// Runs a single expression in the script.
//         /// </summary>
//         /// <param name="expression">The expression to run.</param>
//         public TestStatistics Run(SExpression expression)
//         {
//             if (expression.IsCallTo("module"))
//             {
//                 var module = Assembler.AssembleModule(expression, out string moduleId);
//                 var instance = Wasm.Interpret.ModuleInstance.Instantiate(
//                     module,
//                     importer,
//                     policy: ExecutionPolicy.Create(maxMemorySize: 0x1000),
//                     compiler: Compiler);

//                 moduleInstances.Add(instance);
//                 if (moduleId != null)
//                 {
//                     moduleInstancesByName[moduleId] = instance;
//                 }
//                 if (module.StartFunctionIndex.HasValue)
//                 {
//                     instance.Functions[(int)module.StartFunctionIndex.Value].Invoke(Array.Empty<object>());
//                 }
//                 return TestStatistics.Empty;
//             }
//             else if (expression.IsCallTo("register"))
//             {
//                 var tail = expression.Tail;
//                 var name = Assembler.AssembleString(tail[0], Log);
//                 tail = tail.Skip(1).ToArray();
//                 var moduleId = Assembler.AssembleLabelOrNull(ref tail);
//                 if (moduleId == null)
//                 {
//                     importer.RegisterImporter(name, new ModuleExportsImporter(moduleInstances[moduleInstances.Count - 1]));
//                 }
//                 else
//                 {
//                     importer.RegisterImporter(name, new ModuleExportsImporter(moduleInstancesByName[moduleId]));
//                 }
//                 return TestStatistics.Empty;
//             }
//             else if (expression.IsCallTo("invoke") || expression.IsCallTo("get"))
//             {
//                 RunAction(expression);
//                 return TestStatistics.SingleSuccess;
//             }
//             else if (expression.IsCallTo("assert_return"))
//             {
//                 var results = RunAction(expression.Tail[0]);
//                 var expected = expression.Tail
//                     .Skip(1)
//                     .Zip(results, (expr, val) => EvaluateConstExpr(expr, val.GetType()))
//                     .ToArray();

//                 if (expected.Length != results.Count)
//                 {
//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "assertion failed",
//                             "action produced result ",
//                             string.Join(", ", results),
//                             "; expected ",
//                             string.Join(", ", expected),
//                             ".",
//                             Assembler.Highlight(expression)));
//                     return TestStatistics.SingleFailure;
//                 }

//                 bool failures = false;
//                 for (int i = 0; i < expected.Length; i++)
//                 {
//                     if (!object.Equals(results[i], expected[i]))
//                     {
//                         if (AlmostEquals(results[i], expected[i]))
//                         {
//                             Log.Log(
//                                 new LogEntry(
//                                     Severity.Warning,
//                                     "rounding error",
//                                     "action produced result ",
//                                     results[i].ToString(),
//                                     "; expected ",
//                                     expected[i].ToString(),
//                                     ".",
//                                     Assembler.Highlight(expression)));
//                         }
//                         else
//                         {
//                             Log.Log(
//                                 new LogEntry(
//                                     Severity.Error,
//                                     "assertion failed",
//                                     "action produced result ",
//                                     results[i].ToString(),
//                                     "; expected ",
//                                     expected[i].ToString(),
//                                     ".",
//                                     Assembler.Highlight(expression)));
//                             failures = true;
//                         }
//                     }
//                 }
//                 return failures ? TestStatistics.SingleFailure : TestStatistics.SingleSuccess;
//             }
//             else if (expression.IsCallTo("assert_trap") || expression.IsCallTo("assert_exhaustion"))
//             {
//                 var expected = Assembler.AssembleString(expression.Tail[1], Log);
//                 bool caught = false;
//                 Exception exception = null;
//                 try
//                 {
//                     if (expression.Tail[0].IsCallTo("module"))
//                     {
//                         Run(expression.Tail[0]);
//                     }
//                     else
//                     {
//                         RunAction(expression.Tail[0], false);
//                     }
//                 }
//                 catch (TrapException ex)
//                 {
//                     caught = ex.SpecMessage == expected;
//                     exception = ex;
//                 }
//                 catch (PixieException)
//                 {
//                     throw;
//                 }
//                 catch (Exception ex)
//                 {
//                     caught = false;
//                     exception = ex;
//                 }

//                 if (caught)
//                 {
//                     return TestStatistics.SingleSuccess;
//                 }
//                 else
//                 {
//                     if (exception == null)
//                     {
//                         Log.Log(
//                             new LogEntry(
//                                 Severity.Error,
//                                 "assertion failed",
//                                 "action should have trapped, but didn't.",
//                                 Assembler.Highlight(expression)));
//                     }
//                     else
//                     {
//                         Log.Log(
//                             new LogEntry(
//                                 Severity.Error,
//                                 "assertion failed",
//                                 "action trapped as expected, but with an unexpected exception. ",
//                                 exception.ToString(),
//                                 Assembler.Highlight(expression)));
//                     }
//                     return TestStatistics.SingleFailure;
//                 }
//             }
//             else if (expression.IsCallTo("assert_return_canonical_nan"))
//             {
//                 var results = RunAction(expression.Tail[0]);
//                 bool isCanonicalNaN;
//                 if (results.Count != 1)
//                 {
//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "assertion failed",
//                             "action produced ",
//                             results.Count.ToString(),
//                             " results (",
//                             string.Join(", ", results),
//                             "); expected a single canonical NaN.",
//                             Assembler.Highlight(expression)));
//                     return TestStatistics.SingleFailure;
//                 }
//                 else if (results[0] is double)
//                 {
//                     var val = Interpret.ValueHelpers.ReinterpretAsInt64((double)results[0]);
//                     isCanonicalNaN = val == Interpret.ValueHelpers.ReinterpretAsInt64((double)FloatLiteral.NaN(false))
//                         || val == Interpret.ValueHelpers.ReinterpretAsInt64((double)FloatLiteral.NaN(true));
//                 }
//                 else if (results[0] is float)
//                 {
//                     var val = Interpret.ValueHelpers.ReinterpretAsInt32((float)results[0]);
//                     isCanonicalNaN = val == Interpret.ValueHelpers.ReinterpretAsInt32((float)FloatLiteral.NaN(false))
//                         || val == Interpret.ValueHelpers.ReinterpretAsInt32((float)FloatLiteral.NaN(true));
//                 }
//                 else
//                 {
//                     isCanonicalNaN = false;
//                 }
//                 if (isCanonicalNaN)
//                 {
//                     return TestStatistics.SingleSuccess;
//                 }
//                 else
//                 {
//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "assertion failed",
//                             "action produced ",
//                             results[0].ToString(),
//                             "; expected a single canonical NaN.",
//                             Assembler.Highlight(expression)));
//                     return TestStatistics.SingleFailure;
//                 }
//             }
//             else if (expression.IsCallTo("assert_return_arithmetic_nan"))
//             {
//                 var results = RunAction(expression.Tail[0]);
//                 bool isNaN;
//                 if (results.Count != 1)
//                 {
//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "assertion failed",
//                             "action produced ",
//                             results.Count.ToString(),
//                             " results (",
//                             string.Join(", ", results),
//                             "); expected a single NaN.",
//                             Assembler.Highlight(expression)));
//                     return TestStatistics.SingleFailure;
//                 }
//                 else if (results[0] is double)
//                 {
//                     isNaN = double.IsNaN((double)results[0]);
//                 }
//                 else if (results[0] is float)
//                 {
//                     isNaN = float.IsNaN((float)results[0]);
//                 }
//                 else
//                 {
//                     isNaN = false;
//                 }
//                 if (isNaN)
//                 {
//                     return TestStatistics.SingleSuccess;
//                 }
//                 else
//                 {
//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "assertion failed",
//                             "action produced ",
//                             results[0].ToString(),
//                             "; expected a single NaN.",
//                             Assembler.Highlight(expression)));
//                     return TestStatistics.SingleFailure;
//                 }
//             }
//             else
//             {
//                 Log.Log(
//                     new LogEntry(
//                         Severity.Warning,
//                         "unknown script command",
//                         Quotation.QuoteEvenInBold(
//                             "expression ",
//                             expression.Head.Span.Text,
//                             " was not recognized as a known script command."),
//                         Assembler.Highlight(expression)));
//                 return TestStatistics.SingleUnknown;
//             }
//         }

//         private static bool AlmostEquals(object value, object expected)
//         {
//             if (value is float && expected is float)
//             {
//                 return AlmostEquals((float)value, (float)expected, 1);
//             }
//             else if (value is double && expected is double)
//             {
//                 return AlmostEquals((double)value, (double)expected, 1);
//             }
//             else
//             {
//                 return false;
//             }
//         }

//         private static bool AlmostEquals(double left, double right, long representationTolerance)
//         {
//             // Approximate comparison code suggested by Torbjörn Kalin on StackOverflow
//             // (https://stackoverflow.com/questions/10419771/comparing-doubles-with-adaptive-approximately-equal).
//             long leftAsBits = ToBitsTwosComplement(left);
//             long rightAsBits = ToBitsTwosComplement(right);
//             long floatingPointRepresentationsDiff = Math.Abs(leftAsBits - rightAsBits);
//             return (floatingPointRepresentationsDiff <= representationTolerance);
//         }

//         private static long ToBitsTwosComplement(double value)
//         {
//             // Approximate comparison code suggested by Torbjörn Kalin on StackOverflow
//             // (https://stackoverflow.com/questions/10419771/comparing-doubles-with-adaptive-approximately-equal).
//             long valueAsLong = Interpret.ValueHelpers.ReinterpretAsInt64(value);
//             return valueAsLong < 0
//                 ? (long)(0x8000000000000000 - (ulong)valueAsLong)
//                 : valueAsLong;
//         }

//         private static bool AlmostEquals(float left, float right, int representationTolerance)
//         {
//             // Approximate comparison code suggested by Torbjörn Kalin on StackOverflow
//             // (https://stackoverflow.com/questions/10419771/comparing-doubles-with-adaptive-approximately-equal).
//             long leftAsBits = ToBitsTwosComplement(left);
//             long rightAsBits = ToBitsTwosComplement(right);
//             long floatingPointRepresentationsDiff = Math.Abs(leftAsBits - rightAsBits);
//             return (floatingPointRepresentationsDiff <= representationTolerance);
//         }

//         private static int ToBitsTwosComplement(float value)
//         {
//             // Approximate comparison code suggested by Torbjörn Kalin on StackOverflow
//             // (https://stackoverflow.com/questions/10419771/comparing-doubles-with-adaptive-approximately-equal).
//             int valueAsInt = Interpret.ValueHelpers.ReinterpretAsInt32(value);
//             return valueAsInt < 0
//                 ? (int)(0x80000000 - (int)valueAsInt)
//                 : valueAsInt;
//         }

//         private object EvaluateConstExpr(SExpression expression, WasmValueType resultType)
//         {
//             var anonModule = new WasmFile();
//             var instructions = Assembler.AssembleInstructionExpression(expression, anonModule);
//             var inst = ModuleInstance.Instantiate(anonModule, new SpecTestImporter());
//             return inst.Evaluate(new InitializerExpression(instructions), resultType);
//         }

//         private object EvaluateConstExpr(SExpression expression, Type resultType)
//         {
//             return EvaluateConstExpr(expression, ValueHelpers.ToWasmValueType(resultType));
//         }

//         private IReadOnlyList<object> RunAction(SExpression expression, bool reportExceptions = true)
//         {
//             if (expression.IsCallTo("invoke"))
//             {
//                 var tail = expression.Tail;
//                 var moduleId = Assembler.AssembleLabelOrNull(ref tail);
//                 var name = Assembler.AssembleString(tail[0], Log);
//                 var args = tail.Skip(1);

//                 if (moduleId == null)
//                 {
//                     foreach (var inst in Enumerable.Reverse(moduleInstances))
//                     {
//                         if (TryInvokeNamedFunction(inst, name, args, expression, out IReadOnlyList<object> results, reportExceptions))
//                         {
//                             return results;
//                         }
//                     }

//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "undefined function",
//                             Quotation.QuoteEvenInBold(
//                                 "no function named ",
//                                 name,
//                                 " is defined here."),
//                             Assembler.Highlight(expression)));
//                     return Array.Empty<object>();
//                 }
//                 else
//                 {
//                     if (moduleInstancesByName.TryGetValue(moduleId, out ModuleInstance inst))
//                     {
//                         if (TryInvokeNamedFunction(inst, name, args, expression, out IReadOnlyList<object> results, reportExceptions))
//                         {
//                             return results;
//                         }
//                         else
//                         {
//                             Log.Log(
//                                 new LogEntry(
//                                     Severity.Error,
//                                     "undefined function",
//                                     Quotation.QuoteEvenInBold(
//                                         "no function named ",
//                                         name,
//                                         " is defined in module ",
//                                         moduleId,
//                                         "."),
//                                     Assembler.Highlight(expression)));
//                             return Array.Empty<object>();
//                         }
//                     }
//                     else
//                     {
//                         Log.Log(
//                             new LogEntry(
//                                 Severity.Error,
//                                 "undefined module",
//                                 Quotation.QuoteEvenInBold(
//                                     "no module named ",
//                                     moduleId,
//                                     " is defined here."),
//                                 Assembler.Highlight(expression)));
//                         return Array.Empty<object>();
//                     }
//                 }
//             }
//             else if (expression.IsCallTo("get"))
//             {
//                 var tail = expression.Tail;
//                 var moduleId = Assembler.AssembleLabelOrNull(ref tail);
//                 var name = Assembler.AssembleString(tail[0], Log);
//                 if (moduleId == null)
//                 {
//                     foreach (var inst in moduleInstances)
//                     {
//                         if (inst.ExportedGlobals.TryGetValue(name, out Variable def))
//                         {
//                             return new[] { def.Get<object>() };
//                         }
//                     }

//                     Log.Log(
//                         new LogEntry(
//                             Severity.Error,
//                             "undefined global",
//                             Quotation.QuoteEvenInBold(
//                                 "no global named ",
//                                 name,
//                                 " is defined here."),
//                             Assembler.Highlight(expression)));
//                     return Array.Empty<object>();
//                 }
//                 else
//                 {
//                     if (moduleInstancesByName.TryGetValue(moduleId, out ModuleInstance inst))
//                     {
//                         if (inst.ExportedGlobals.TryGetValue(name, out Variable def))
//                         {
//                             return new[] { def.Get<object>() };
//                         }
//                         else
//                         {
//                             Log.Log(
//                                 new LogEntry(
//                                     Severity.Error,
//                                     "undefined global",
//                                     Quotation.QuoteEvenInBold(
//                                         "no global named ",
//                                         name,
//                                         " is defined in module ",
//                                         moduleId,
//                                         "."),
//                                     Assembler.Highlight(expression)));
//                             return Array.Empty<object>();
//                         }
//                     }
//                     else
//                     {
//                         Log.Log(
//                             new LogEntry(
//                                 Severity.Error,
//                                 "undefined module",
//                                 Quotation.QuoteEvenInBold(
//                                     "no module named ",
//                                     moduleId,
//                                     " is defined here."),
//                                 Assembler.Highlight(expression)));
//                         return Array.Empty<object>();
//                     }
//                 }
//             }
//             else
//             {
//                 Log.Log(
//                     new LogEntry(
//                         Severity.Error,
//                         "unknown action",
//                         Quotation.QuoteEvenInBold(
//                             "expression ",
//                             expression.Head.Span.Text,
//                             " was not recognized as a known script action."),
//                         Assembler.Highlight(expression)));
//                 return Array.Empty<object>();
//             }
//         }

//         private bool TryInvokeNamedFunction(
//             ModuleInstance instance,
//             string name,
//             IEnumerable<SExpression> argumentExpressions,
//             SExpression expression,
//             out IReadOnlyList<object> results,
//             bool reportExceptions = true)
//         {
//             if (instance.ExportedFunctions.TryGetValue(name, out FunctionDefinition def))
//             {
//                 var args = argumentExpressions
//                     .Zip(def.ParameterTypes, (expr, type) => EvaluateConstExpr(expr, type))
//                     .ToArray();
//                 try
//                 {
//                     results = def.Invoke(args);
//                     return true;
//                 }
//                 catch (Exception ex)
//                 {
//                     if (reportExceptions)
//                     {
//                         Log.Log(
//                             new LogEntry(
//                                 Severity.Error,
//                                 "unhandled exception",
//                                 $"function invocation threw {ex.GetType().Name}",
//                                 new Paragraph(ex.ToString()),
//                                 Assembler.Highlight(expression)));
//                     }
//                     throw;
//                 }
//             }
//             else
//             {
//                 results = null;
//                 return false;
//             }
//         }
//     }
// }