Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions compiler/lambda_position_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package compiler_test

import "testing"

// A failure inside a lambda has to be reported at the line that failed, as it
// is inside a named function. The compiled program handed a boxed result back
// through meow.Returning, which put the program back at the caller's line
// whatever came back — so a Furball returned by a lambda, or by any function
// whose result is boxed, was blamed on the line that called it, and a lambda
// passed to lick was blamed on the whole call (#163). The playground reported
// the line that failed.
func TestAFailureInsideALambdaIsBlamedOnItsOwnLine(t *testing.T) {
tests := []struct {
name string
source string
output string
failure string
}{
{
"a lambda called by name",
"nyan g = paw() { bring to_float(\"bad\") }\nnya(g())\n",
"",
"prog.nyan:1:18: Hiss! Cannot read \"bad\" as a Float, nya~",
},
{
"a lambda passed to lick",
"nyan xs = [1, 2]\nnya(lick(xs, paw(x) {\n bring to_float(\"bad\")\n}))\n",
"",
"prog.nyan:3:3: Hiss! Cannot read \"bad\" as a Float, nya~",
},
{
"the last line of a longer lambda",
"nyan g = paw() {\n nya(\"in\")\n bring to_float(\"bad\")\n}\nnya(g())\n",
"in\n",
"prog.nyan:3:3: Hiss! Cannot read \"bad\" as a Float, nya~",
},
{
"a lambda called inside a typed function",
"meow f() float {\n nyan h = paw() { bring to_float(\"bad\") }\n bring h()\n}\nnya(to_string(f()))\n",
"",
"prog.nyan:2:20: Hiss! Cannot read \"bad\" as a Float, nya~",
},
{
// Not a lambda, but the same return: a function whose result is
// boxed hands a failure back as a value too.
"a named function returning a kitty",
"kitty P { x: float }\nmeow f() P {\n nya(\"in f\")\n bring P(to_float(\"bad\"))\n}\nnya(f())\n",
"in f\n",
"prog.nyan:4:3: Hiss! Cannot read \"bad\" as a Float, nya~",
},
{
// A lambda that succeeds still leaves the program where it was
// called, so a failure later in the same statement is blamed on
// that statement and not on the lambda's last line.
"a failure after a lambda that succeeded",
"nyan g = paw() {\n bring to_float(\"1.5\")\n}\nnya(g(), to_float(\"bad\"))\n",
"",
"prog.nyan:4:1: Hiss! Cannot read \"bad\" as a Float, nya~",
},
{
// A failure the lambda caught is a value it returned, not a
// failure, so the program goes back to the caller as for any
// other result.
"a failure after a lambda that caught its own",
"nyan g = paw() {\n bring to_float(\"bad\") ~> 0.0\n}\nnya(g())\nnya(to_float(\"x\"))\n",
"0\n",
"prog.nyan:5:1: Hiss! Cannot read \"x\" as a Float, nya~",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, backend := range []struct {
name string
run func(*testing.T, string) (string, string)
}{
{"compiled", runCompiled},
{"interpreted", runInterpreted},
} {
output, failure := backend.run(t, tt.source)
if output != tt.output {
t.Errorf("%s printed %q, want %q", backend.name, output, tt.output)
}
if failure != tt.failure {
t.Errorf("%s failed with %q, want %q", backend.name, failure, tt.failure)
}
}
})
}
}
15 changes: 12 additions & 3 deletions runtime/meowrt/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,19 @@ func Located(message string) string {
// failure later in the same statement would be blamed on the callee's last
// line, which is code that worked.
//
// It restores only on the way back from a call that returned. A call that fails
// never reaches it, so the innermost position — where the failure actually
// happened — is the one that survives.
// It restores only on the way back from a call that succeeded, the rule Call
// keeps too. A typed body that fails panics and never reaches it, but a boxed
// body hands its failure back as a value — an unhandled Furball — and that
// comes through here like any result. Restoring then blamed a failure inside a
// lambda, or inside any function whose result is boxed, on the line that
// called it (#163), so the innermost position — where the failure actually
// happened — is left to survive.
func Returning[T any](pos string, v T) T {
if value, ok := any(v).(Value); ok {
if _, failed := AsFurball(value); failed {
return v
Comment on lines +52 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the caller position when a returned failure is caught

When the caller catches this Furball with ~>, GagOr turns the call into a successful value but never restores here, so any later failure in the same statement is blamed on the callee. For example, if g fails on line 2, nya(g() ~> 0.0, to_float("x")) on line 4 now reports the to_float("x") failure at line 2; before this change the compiled backend correctly reported line 4. The recovery path needs to restore the position captured before evaluating the caught expression.

Useful? React with 👍 / 👎.

}
}
Here(pos)
return v
}
47 changes: 47 additions & 0 deletions runtime/meowrt/position_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,50 @@ func TestHereIsWhatWhereReports(t *testing.T) {
t.Errorf("got %q, want b.nyan:2:2", got)
}
}

// A call that comes back goes back to where it was made from, whatever type its
// result is.
func TestReturningGoesBackToTheCaller(t *testing.T) {
atPosition(t, "prog.nyan:3:3")

if got := Returning("prog.nyan:5:1", NewInt(1)); got.Val != 1 {
t.Errorf("got %v, want the value handed back unchanged", got)
}
if got := Where(); got != "prog.nyan:5:1" {
t.Errorf("after a boxed result the program is at %q, want the caller's prog.nyan:5:1", got)
}

Here("prog.nyan:3:3")
Returning("prog.nyan:5:1", "native")
if got := Where(); got != "prog.nyan:5:1" {
t.Errorf("after a native result the program is at %q, want the caller's prog.nyan:5:1", got)
}
}

// A boxed body hands its failure back as a value. That is a call that failed,
// so the program stays where it failed instead of going back to the caller,
// which would blame the failure on the line that called the function (#163).
func TestReturningAFailureStaysWhereItFailed(t *testing.T) {
atPosition(t, "prog.nyan:3:3")

failure := NewFurball("Hiss! boom, nya~")
if got := Returning[Value]("prog.nyan:5:1", failure); got != failure {
t.Errorf("got %v, want the Furball handed back unchanged", got)
}
if got := Where(); got != "prog.nyan:3:3" {
t.Errorf("after a failure the program is at %q, want where it failed, prog.nyan:3:3", got)
}
}

// A failure that was caught is a value like any other, so the program goes back
// to the caller as it does for any result.
func TestReturningACaughtFailureGoesBackToTheCaller(t *testing.T) {
atPosition(t, "prog.nyan:3:3")

caught := NewFurball("Hiss! boom, nya~")
caught.Handled = true
Returning[Value]("prog.nyan:5:1", caught)
if got := Where(); got != "prog.nyan:5:1" {
t.Errorf("after a caught failure the program is at %q, want the caller's prog.nyan:5:1", got)
}
}
Loading