Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check return types #615

Merged
merged 6 commits into from
Sep 22, 2024
Merged

Check return types #615

merged 6 commits into from
Sep 22, 2024

Conversation

bakkot
Copy link
Contributor

@bakkot bakkot commented Sep 17, 2024

Based on #613.

This will give errors for cases like

<emu-clause id="example" type="abstract operation">
  <h1>
    Example (): a Number
  </h1>
  <dl class="header"></dl>
  <emu-alg>
    1. Return 0.
  </emu-alg>
</emu-clause>

such as

Error: test.emu:13:23: returned value (0) does not look plausibly assignable to algorithm's return type (Number)
hint: you returned a mathematical value, but this algorithm returns an ES language Number

Fixes #609 by moving the logic for checking for returned abrupt completions out of an ad-hoc bit of Algorithm.ts and into the typechecker as part of a more general facility for checking the return types from algorithms.

This also drops the special handling for GeneratorYield and instead adds a new attribute, "skip return checks", which will need to be added to it as well as Completion and NormalCompletion and any other algorithms which are doing weird stuff in the future. As such, this is a breaking change (but only very slightly).

This introduces no new errors on ecma262, ecma402, or proposal-temporal except for the three algorithms called out in the previous paragraph, which will be fixed by adding the new attribute when upstreaming this change.

The actual logic, in inspectReturns, is pleasingly simple: we just look for Return Foo., parse Foo the same way we parse arguments to AO calls, and compare its type to the algorithm's return type the same way we'd compare a call argument to the corresponding AO's corresponding parameter type. We also look for ? and Throw and etc so we can ensure the containing algorithm returns an abrupt completion.

src/type-logic.ts Outdated Show resolved Hide resolved
spec/index.html Outdated
@@ -212,6 +212,7 @@ <h1>Structured Headers</h1>
<li><b>for:</b> The type of value to which a clause of type "concrete method" or "internal method" applies.</li>
<li><b>redefinition:</b> If "true", the name of the operation will not automatically link (i.e., it will not automatically be given an aoid).</li>
<li><b>skip global checks:</b> If "true", disables consistency checks for this AO which require knowing every callsite.</li>
<li><b>skip return checks:</b> If "true", disables checking that the returned values from this AO correspond to its declared return type.</li>
Copy link
Member

Choose a reason for hiding this comment

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

Instead of skipping return checks, shouldn't we be asserting that the return check fails? That way if it is later changed in such a way that it passes, we know we can remove the annotation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would probably be better but I don't want to do the work to implement it. PRs welcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK actually that was trivial, done.

src/typechecker.ts Outdated Show resolved Hide resolved
@@ -179,6 +185,37 @@ const getExpressionVisitor =
}
};

type ErrorForUsingTypeXAsTypeY =
| null
| 'number-to-math'
Copy link
Member

Choose a reason for hiding this comment

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

For completion's sake, shouldn't we have bigint-to-real?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure I guess. That's not related to this PR but I can fold it in.

'\nhint: you passed a mathematical value, but this position takes an ES language BigInt';
break;
}
case 'other': {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of testing for null above, either use a default or (if you're looking to get an exhaustivity check from TS), fallthrough with case null: here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test for null guards the whole error-producing logic, not just this switch case.

src/typechecker.ts Outdated Show resolved Hide resolved
@@ -332,6 +435,152 @@ function isConsumedAsCompletion(expr: Expr, path: PathItem[]) {
return false;
}

// returns a boolean to indicate whether this line can return an abrupt completion, or null to indicate we should stop caring
Copy link
Member

Choose a reason for hiding this comment

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

I don't really like that. Can't we make an enum for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then we couldn't use ||=.

Copy link
Member

Choose a reason for hiding this comment

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

just implement Functor 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no

Copy link
Member

@michaelficarra michaelficarra left a comment

Choose a reason for hiding this comment

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

I think we're missing tests where the completiony-ness of an Abstract Closure doesn't match that of the containing algorithm (in both ways).

@bakkot
Copy link
Contributor Author

bakkot commented Sep 18, 2024

I think we're missing tests where the completiony-ness of an Abstract Closure doesn't match that of the containing algorithm (in both ways).

We have an assertion that this is not an error:

<emu-clause id="example" type="abstract operation">
<h1>
  ExampleAlg (): a mathematical value
</h1>
<dl class="header">
</dl>
<emu-alg>
  1. Let _addend_ be 41.
  1. Let _closure_ be a new Abstract Closure with parameters (_x_) that captures _addend_ and performs the following steps when called:
    1. Return _x_ + ? ExampleCompletionAO(_addend_).
  1. Let _val_ be ! _closure_(1).
  1. Return _val_.
</emu-alg>
</emu-clause>

What else are you looking for?

@michaelficarra
Copy link
Member

Ah that's right, I misread this test and thought they were both returning completion records. Then the only one we're missing is one where the algorithm returns a completion but the AC returns a non-completion.

@bakkot
Copy link
Contributor Author

bakkot commented Sep 18, 2024

Then the only one we're missing is one where the algorithm returns a completion but the AC returns a non-completion.

That wouldn't be an error even with no handling of ACs because values returned from AOs which return completions are automatically boxed into NormalCompletion.

@michaelficarra
Copy link
Member

I wouldn't expect the Return steps inside an AC to auto-wrap in NormalCompletion just because the containing algorithm is declared to return a completion. If that's how it currently works, it shouldn't be.

@bakkot
Copy link
Contributor Author

bakkot commented Sep 18, 2024

I wouldn't expect the Return steps inside an AC to auto-wrap in NormalCompletion just because the containing algorithm is declared to return a completion. If that's how it currently works, it shouldn't be.

It's not, but ACs don't have return types. The bodies of ACs are exempt from the return type analysis entirely.

My point is that if the body incorrectly weren't exempt from the analysis, such that a return in the body of the AC were to be incorrectly considered to be returning from the containing AO, and we had e.g. return 0 in an AO declared to return a normal completion containing a real number, that still wouldn't be an error (because of the auto-boxing logic).

So there's no test you could write here which would fail under any code we could have written. So there's no reason to have a test.

Copy link
Member

@michaelficarra michaelficarra left a comment

Choose a reason for hiding this comment

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

I might call this something else now, but LGTM either way.

@jmdyck
Copy link
Contributor

jmdyck commented Sep 19, 2024

I wouldn't expect the Return steps inside an AC to auto-wrap in NormalCompletion just because the containing algorithm is declared to return a completion. If that's how it currently works, it shouldn't be.

The spec isn't entirely clear on this point. 5.2.3.5 "Implicit Normal Completion" says "In algorithms within abstract operations which are declared to return a Completion Record, ..." which would seem to include an AC within such an AO.

Maybe that section should carve out an exception for ACs. But then the question might arise as to whether ACs have their own rule for implicit NormalCompletion. Some seem to be written under the assumption of auto-wrapping.

(Tangent: maybe ACs should explicitly state their return type.)

Base automatically changed from handle-completions-better to main September 22, 2024 17:47
@bakkot bakkot merged commit 36fc866 into main Sep 22, 2024
2 checks passed
@bakkot bakkot deleted the check-return-types branch September 22, 2024 17:50
@bakkot bakkot mentioned this pull request Sep 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Deleted lines should not be considered to return a completion record from an AO
3 participants