Skip to content

Commit

Permalink
Improve runtime errors on invalid type-mismatched Elm record access
Browse files Browse the repository at this point in the history
Improve errors in scenarios where Elm syntax was compiled with incomplete type-checking, resulting in an invalid record access using a nonexistent field name. In the previous version, such a program caused a stack overflow. The changes in this commit improve the errors as follows:
+ Expand the framework for testing using Elm Interactive scenarios to support expectations for errors: Add a syntax to encode the expected contents of an error in a test scenario step and adapt the processing of step results accordingly.
+ Change the PineVM implementation to include more details when crashing due to failed parsing of a value as expression: Include details on both function value and environment.
+ Change the Elm compiler to crash by invalid parsing instead of the infinite recursion used earlier. Use the function and environment properties of the parse-and-eval expression to specify the error, including the name of the searched record field.
  • Loading branch information
Viir committed May 5, 2024
1 parent 2272636 commit 5b21858
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 45 deletions.
6 changes: 3 additions & 3 deletions implement/Pine.Core/Pine.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<AssemblyVersion>0.3.5</AssemblyVersion>
<FileVersion>0.3.5</FileVersion>
<AssemblyVersion>0.3.6</AssemblyVersion>
<FileVersion>0.3.6</FileVersion>
</PropertyGroup>

<PropertyGroup>
<PackageId>Pine.Core</PackageId>
<Version>0.3.5</Version>
<Version>0.3.6</Version>
<Description>The cross-platform Elm runtime environment</Description>
<PackageTags>Functional;Elm;Runtime;Compiler;VM;DBMS</PackageTags>
<RepositoryUrl>https://github.com/pine-vm/pine.git</RepositoryUrl>
Expand Down
105 changes: 75 additions & 30 deletions implement/pine/ElmInteractive/TestElmInteractive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ public record Scenario(

public record ScenarioStep(
string Submission,
string? ExpectedResponse);
string? ExpectedResponse,
string? ExpectedErrorContains);

public record InteractiveScenarioTestReport(
Scenario Scenario,
ImmutableList<(string name, Result<InteractiveScenarioTestStepFailure, IInteractiveSession.SubmissionResponse> result)> StepsReports,
ImmutableList<(string name, Result<InteractiveScenarioTestStepFailure, InteractiveScenarioTestStepSuccess> result)> StepsReports,
TimeSpan ElapsedTime)
{
public bool Passed => StepsReports.All(s => s.result.IsOk());
Expand All @@ -66,6 +67,15 @@ public record InteractiveScenarioTestStepFailure(
string submission,
string errorAsText);

public abstract record InteractiveScenarioTestStepSuccess
{
public record ErrorAsExpected(string ErrorAsText)
: InteractiveScenarioTestStepSuccess;

public record SubmissionResponseOk(IInteractiveSession.SubmissionResponse SubmissionResponse)
: InteractiveScenarioTestStepSuccess;
}

public static ParsedScenarios ParseElmInteractiveScenarios(
TreeNodeWithStringPath scenariosTree,
IConsole console)
Expand Down Expand Up @@ -224,7 +234,38 @@ public static InteractiveScenarioTestReport TestElmInteractiveScenario(
parsedScenario.Steps
.Select(sessionStep =>
{
Result<InteractiveScenarioTestStepFailure, IInteractiveSession.SubmissionResponse> getResult()
Result<InteractiveScenarioTestStepFailure, InteractiveScenarioTestStepSuccess> continueWithErrorMessage(
string errorMessage)
{
if (sessionStep.step.ExpectedErrorContains is { } expectedErrorContains)
{
if (!errorMessage.Contains(expectedErrorContains, StringComparison.InvariantCultureIgnoreCase))
{
var errorText =
"Error from interactive does not contain expected value. Expected:\n" +
expectedErrorContains +
"\nBut got this error:\n" +
errorMessage;

return
(Result<InteractiveScenarioTestStepFailure, InteractiveScenarioTestStepSuccess>)
new InteractiveScenarioTestStepFailure(
submission: sessionStep.step.Submission,
errorAsText: errorText);
}

return
(Result<InteractiveScenarioTestStepFailure, InteractiveScenarioTestStepSuccess>)
new InteractiveScenarioTestStepSuccess.ErrorAsExpected(errorMessage);
}

return
new InteractiveScenarioTestStepFailure(
submission: sessionStep.step.Submission,
errorAsText: errorMessage);
}

Result<InteractiveScenarioTestStepFailure, InteractiveScenarioTestStepSuccess> getResult()
{
try
{
Expand All @@ -241,42 +282,36 @@ public static InteractiveScenarioTestReport TestElmInteractiveScenario(
new TestInteractiveScenarioLogEntry.SubmissionResponseStruct(
Result: submissionResult)));

var submissionResultMappedErr =
submissionResult
.MapError(err => new InteractiveScenarioTestStepFailure(
submission: sessionStep.step.Submission,
errorAsText: "Submission result has error: " + err));

return
submissionResultMappedErr
.AndThen(submissionResultOk =>
{
if (sessionStep.step.ExpectedResponse is { } expectedResponse)
submissionResult
.Unpack(
fromErr: err => continueWithErrorMessage("Submission result has error: " + err),
fromOk: submissionResultOk =>
{
if (expectedResponse != submissionResultOk.InteractiveResponse?.DisplayText)
if (sessionStep.step.ExpectedResponse is { } expectedResponse)
{
var errorText =
"Response from interactive does not match expected value. Expected:\n" +
expectedResponse +
"\nBut got this response:\n" +
submissionResultOk.InteractiveResponse?.DisplayText;

return Result<InteractiveScenarioTestStepFailure, IInteractiveSession.SubmissionResponse>.err(
if (expectedResponse != submissionResultOk.InteractiveResponse?.DisplayText)
{
var errorText =
"Response from interactive does not match expected value. Expected:\n" +
expectedResponse +
"\nBut got this response:\n" +
submissionResultOk.InteractiveResponse?.DisplayText;

return
(Result<InteractiveScenarioTestStepFailure, InteractiveScenarioTestStepSuccess>)
new InteractiveScenarioTestStepFailure(
submission: sessionStep.step.Submission,
errorAsText: errorText));
errorAsText: errorText);
}
}
}

return submissionResultMappedErr;
});
return new InteractiveScenarioTestStepSuccess.SubmissionResponseOk(submissionResultOk);
});
}
catch (Exception e)
{
return Result<InteractiveScenarioTestStepFailure, IInteractiveSession.SubmissionResponse>.err(
new InteractiveScenarioTestStepFailure(
submission: sessionStep.step.Submission,
errorAsText: "Runtime exception:\n" + e));
return continueWithErrorMessage("Runtime exception:\n" + e);
}
}

Expand Down Expand Up @@ -344,6 +379,13 @@ public static Result<string, ScenarioStep> ParseScenarioStep(TreeNodeWithStringP
:
null;

var expectedErrorContains =
sessionStep.GetNodeAtPath(["expected-error-contains.txt"]) is TreeNodeWithStringPath.BlobNode expectedErrorContainsBlob
?
Encoding.UTF8.GetString(expectedErrorContainsBlob.Bytes.Span)
:
null;

return
(sessionStep.GetNodeAtPath(["submission.txt"]) switch
{
Expand All @@ -353,6 +395,9 @@ public static Result<string, ScenarioStep> ParseScenarioStep(TreeNodeWithStringP
_ =>
Result<string, string>.err("Missing submission"),
})
.Map(submission => new ScenarioStep(submission, expectedResponse));
.Map(submission => new ScenarioStep(
submission,
ExpectedResponse: expectedResponse,
ExpectedErrorContains: expectedErrorContains));
}
}
12 changes: 8 additions & 4 deletions implement/pine/ElmTime/compile-elm-program/src/ElmCompiler.elm
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,9 @@ expandEnvWithModulesRecursive environmentBefore parsedElmModules =
nextEnvironment =
Dict.insert moduleName moduleValue environmentBefore
in
expandEnvWithModulesRecursive
nextEnvironment
followingModules
expandEnvWithModulesRecursive
nextEnvironment
followingModules


compileElmModuleIntoNamedExports :
Expand Down Expand Up @@ -2784,7 +2784,11 @@ recursiveFunctionToLookupFieldInRecord =
[ Pine.ListExpression []
, remainingFieldsLocalExpression
]
, ifTrue = continueWithRemainingExpression
, ifTrue =
Pine.ParseAndEvalExpression
{ expression = Pine.LiteralExpression (Pine.valueFromString "Invalid record access")
, environment = fieldNameLocalExpression
}
, ifFalse =
Pine.ConditionalExpression
{ condition =
Expand Down
18 changes: 13 additions & 5 deletions implement/pine/Pine/PineVM/PineVM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,29 @@ public Result<string, PineValue> EvaluateExpressionDefault(
public Result<string, PineValue> EvaluateParseAndEvalExpression(
Expression.ParseAndEvalExpression parseAndEval,
PineValue environment) =>
EvaluateExpression(parseAndEval.environment, environment)
.AndThen(environmentValue =>
EvaluateExpression(parseAndEval.expression, environment)
.MapError(error => "Failed to evaluate function: " + error)
.AndThen(functionValue => ParseExpressionFromValue(functionValue)
.MapError(error => "Failed to parse expression from function value: " + error)
.AndThen(functionExpression => EvaluateExpression(parseAndEval.environment, environment)
.MapError(error =>
"Failed to parse expression from function value: " + error +
" - functionValue is " +
PineValueAsString.StringFromValue(functionValue)
.Unpack(fromErr: _ => "not a string", fromOk: asString => "string \'" + asString + "\'") +
" - environmentValue is " +
PineValueAsString.StringFromValue(environmentValue)
.Unpack(fromErr: _ => "not a string", fromOk: asString => "string \'" + asString + "\'"))
.MapError(error => "Failed to evaluate argument: " + error)
.AndThen(argumentValue =>
.AndThen(functionExpression =>
{
if (argumentValue is PineValue.ListValue list)
if (environmentValue is PineValue.ListValue list)
{
FunctionApplicationMaxEnvSize =
FunctionApplicationMaxEnvSize < list.Elements.Count ? list.Elements.Count : FunctionApplicationMaxEnvSize;
}

var evalResult = EvaluateExpression(environment: argumentValue, expression: functionExpression);
var evalResult = EvaluateExpression(environment: environmentValue, expression: functionExpression);

return evalResult;
})));
Expand Down
2 changes: 1 addition & 1 deletion implement/pine/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace ElmTime;

public class Program
{
public static string AppVersionId => "0.3.5";
public static string AppVersionId => "0.3.6";

private static int AdminInterfaceDefaultPort => 4000;

Expand Down
4 changes: 2 additions & 2 deletions implement/pine/pine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>pine</AssemblyName>
<AssemblyVersion>0.3.5</AssemblyVersion>
<FileVersion>0.3.5</FileVersion>
<AssemblyVersion>0.3.6</AssemblyVersion>
<FileVersion>0.3.6</FileVersion>
<Nullable>enable</Nullable>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid record access
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let
theRecord = { alpha = 1, beta = 2 }
in
theRecord.gamma

0 comments on commit 5b21858

Please sign in to comment.