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); } }
/// <summary> /// Parses a sequence of tokens as S-expressions. /// </summary> /// <param name="tokens">The tokens to parse.</param> /// <param name="log">A log to send errors to.</param> /// <param name="isNested">Tells if this parsing action is a nested rather than a top-level action.</param> /// <returns>A list of parsed S-expressions.</returns> private static IReadOnlyList <SExpression> ParseAsSExpressions(IEnumerator <Lexer.Token> tokens, ILog log, bool isNested) { var results = new List <SExpression>(); while (tokens.MoveNext()) { var token = tokens.Current; if (token.Kind == Lexer.TokenKind.LeftParenthesis) { if (tokens.MoveNext()) { var head = tokens.Current; if (head.Kind != Lexer.TokenKind.Keyword) { log.Log( new LogEntry( Severity.Error, "expected a keyword", "all S-expressions should begin with a keyword, but this one doesn't.", new HighlightedSource(new SourceRegion(head.Span)))); } var tail = ParseAsSExpressions(tokens, log, true); if (tokens.Current.Kind != Lexer.TokenKind.RightParenthesis) { log.Log( new LogEntry( Severity.Error, "no closing parenthesis", "left parenthesis indicates the start of an S-expression, but that expression is never closed.", new HighlightedSource(new SourceRegion(token.Span)))); } results.Add(SExpression.Create(head, tail)); } else { log.Log( new LogEntry( Severity.Error, "no closing parenthesis", "left parenthesis indicates the start of an S-expression, but the file ends immediately after.", new HighlightedSource(new SourceRegion(token.Span)))); } } else if (token.Kind == Lexer.TokenKind.RightParenthesis) { if (!isNested) { log.Log( new LogEntry( Severity.Error, "excess parenthesis", "right parenthesis does not close a left parenthesis.", new HighlightedSource(new SourceRegion(token.Span)))); } break; } else { results.Add(SExpression.Create(token)); } } return(results); }
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>()); } }
/// <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); } }