Skip to content

Fix issue #841#919

Draft
jcp19 wants to merge 9 commits into
masterfrom
fix-841
Draft

Fix issue #841#919
jcp19 wants to merge 9 commits into
masterfrom
fix-841

Conversation

@jcp19

@jcp19 jcp19 commented Apr 22, 2025

Copy link
Copy Markdown
Contributor

This PR fixes issue #841: pure method signatures declared in an interface are now held to the same requirements as pure function/method implementations.

Since this PR was first opened, master merged #1019, which already generalises the rejection of conditional termination measures to ghost and pure members (including interface method signatures) and requires termination measures for pure/ghost implementations. This branch has been rebased on top of that, so its remaining, unique contribution is focused on interfaces:

  • Unifies the signature/contract checking of pure functions, pure methods, and interface method signatures into a shared wellDefIfPureSpec (exactly one result, non-variadic parameters, pure postconditions, no preserves clauses).
  • Requires pure interface method signatures to carry a termination measure, gated behind disableCheckTerminationPureFns exactly like pure functions and methods.
  • Adds decreases to the pure methods of the ByteOrder stdlib stub, and restructures the 000841 regression test.

Note: because the regression-test suite runs with disableCheckTerminationPureFns = true, the "must have a termination measure" case is not exercised by the suite (same as for pure functions); the 000841 test covers the always-checked rules (single result, non-variadic parameters, rejection of conditional measures).

@jcp19
jcp19 requested a review from ArquintL April 22, 2025 14:42
@jcp19 jcp19 linked an issue Apr 22, 2025 that may be closed by this pull request

@ArquintL ArquintL left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks overall good to me but I've left some requests for small changes

Comment on lines +47 to +48
// (João) Conditional termination measures are a very rarely used feature. They allow defining termination measures
// case-per-case. However, for pure or ghost functions and methods, we need to show that all conditions provided

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// (João) Conditional termination measures are a very rarely used feature. They allow defining termination measures
// case-per-case. However, for pure or ghost functions and methods, we need to show that all conditions provided
// (João) Conditional termination measures are a very rarely used feature. They allow defining termination measures
// case-by-case. However, for pure or ghost functions and methods, we need to show that all conditions provided

val hasConditionalTerminationMeasureErrors = {
val conditionalMeasure = spec.terminationMeasures.find(_.isConditional)
conditionalMeasure match {
case Some(n) => error(n, "Conditional termination measures are not allowed in pure members.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
case Some(n) => error(n, "Conditional termination measures are not allowed in pure members.")
case Some(n) => error(n, "Conditional termination measures are not allowed for pure or ghost functions and methods.")

}

private[typing] def wellDefIfPureMethod(member: PMethodDecl): Messages = {
if (member.spec.isPure) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this if condition is redundant

@@ -61,15 +74,23 @@ trait GhostMemberTyping extends BaseTyping { this: TypeInfoImpl =>

private[typing] def wellDefIfPureFunction(member: PFunctionDecl): Messages = {
if (member.spec.isPure) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this if condition is redundant

Comment on lines +34 to +35
// spec must come from a ghost or pure function
private[typing] def wellFoundedIfNeeded(spec: PFunctionSpec): Messages = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

three comments:

  • can we change the behavior such that this function can be called for any spec and internally checks whether it's a spec for a ghost or pure function / method? That's the intuition I have for these wellFoundedIf... functions. Alternatively, please add a require stmt that checks this assumption
  • Maybe I'm missing something but isn't this function only called for explicitly ghost functions and methods? To me, it seems like we do not call this function for actual pure functions and methods
  • can we rename this function? Without looking at its implementation, I do not have any clue what Needed is supposed to do. What about wellFoundedIfGhostOrPure?

Comment on lines +16 to +17
//:: ExpectedOutput(type_error)
decreases _ if b

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
//:: ExpectedOutput(type_error)
decreases _ if b
//:: ExpectedOutput(type_error)
decreases _ if b // we syntactically disallow conditional termination measures for members that must terminate

Comment on lines +7 to +12
// Type error, pure function does not have termination measures.
//:: ExpectedOutput(type_error)
pure
// Type error, pure function cannot have variadic parameters.
//:: ExpectedOutput(type_error)
M(a ...int) int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd split the errors into separate interface functions. Furthermore, I find it a bit funny that one error occurs at the pure keyword whereas the other errors occur at the parameter declaration site but that's just an artifact of how we type-check. We could consider making the following declarations one-liners such that the testcases are not overly restrictive (should we ever indicate missing termination measures, e.g., at the function name

Suggested change
// Type error, pure function does not have termination measures.
//:: ExpectedOutput(type_error)
pure
// Type error, pure function cannot have variadic parameters.
//:: ExpectedOutput(type_error)
M(a ...int) int
// Type error, pure function does not have termination measures.
//:: ExpectedOutput(type_error)
pure
M1(a int) int
// Type error, we do not permit conditional termination measures for members that must terminate
//:: ExpectedOutput(type_error)
decreases if a == 42
pure
M2(a int) int
decreases
pure
// Type error, pure function cannot have variadic parameters.
//:: ExpectedOutput(type_error)
M3(a ...int) int
decreases
pure
// Type error, pure function must have exactly one return parameter
//:: ExpectedOutput(type_error)
M4(a int)

@jcp19 jcp19 mentioned this pull request Apr 23, 2025
@jcp19
jcp19 marked this pull request as draft June 3, 2025 10:14
jcp19 added 2 commits July 17, 2026 00:23
Rebase the fix for issue #841 onto master, whose PR #1019 already introduced
the general rejection of conditional termination measures for ghost and pure
members (including interface method signatures). The remaining, still-open part
of issue #841 is that pure interface method signatures were not required to have
termination measures and their signatures were not validated.

This resolves the merge by:
- Extracting the signature-level pure checks into a shared 'wellDefIfPureSpec',
  now used by pure functions, pure methods, and pure interface method sigs.
- Requiring pure interface method sigs to have a termination measure.
- Keeping master's conditional/wildcard-measure and mayInit handling.
- Adding 'decreases' to the pure ByteOrder interface methods in the stdlib stub.
- Restructuring the regression test so each case triggers exactly one error.
…ddedInterfaces-fail3

The requirement that pure members carry a termination measure is gated behind
'disableCheckTerminationPureFns', which the regression test suite disables, so
that case cannot be exercised here. The test now covers the checks that always
apply to pure interface method signatures: exactly one result, non-variadic
parameters, and rejection of conditional termination measures.

embeddedInterfaces-fail3 declared 'pure g()' with no result, which is now
rejected at the interface itself (issue #841). Give it a result so the test
keeps exercising its intended error: a non-pure method implementing a pure
interface method.

jcp19 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

I've rebased this branch on top of that, which changes the picture for most of your comments:

  • case-per-casecase-by-case and the "pure members" error message: those came from this branch's own conditional-measure implementation, which is now superseded by Disallow conditional termination measures in ghost and pure members #1019's version — so both are moot.
  • Redundant if (member.spec.isPure) in wellDefIfPure{Method,Function}: addressed — the pure-signature checks are now factored into a single wellDefIfPureSpec, so isPure is guarded once per call site.
  • Rename wellFoundedIfNeeded / make it self-checking: wellFoundedIfNeeded is now master's version (from Disallow conditional termination measures in ghost and pure members #1019); this branch no longer touches it. The shared helper this branch introduces is wellDefIfPureSpec, used by pure functions, pure methods, and pure interface method signatures.
  • Test restructuring: done — 000841.gobra now splits the cases into separate one-liner interface methods, each triggering exactly one error, and keeps the conditional-measure comment you suggested.

The remaining unique change is the interface part of #841: wellDefIfPureSpec is now also applied to interface method signatures (exactly one result, non-variadic parameters, pure postconditions, no preserves), and pure interface method sigs are required to carry a termination measure — gated behind disableCheckTerminationPureFns, exactly like pure functions/methods.

One thing worth flagging: because the regression suite forces disableCheckTerminationPureFns = true (and the config merge can't turn it back off per-file), the "must have a termination measure" requirement can't be exercised by a regression test — the same limitation that already applies to pure functions. The 000841 test therefore covers the always-checked rules (single result, non-variadic parameters, conditional measures). Happy to revisit if you'd prefer a different approach there.

CI is green. PTAL when you get a chance.


Generated by Claude Code

@jcp19
jcp19 marked this pull request as ready for review July 17, 2026 01:18
@jcp19
jcp19 requested a review from ArquintL July 17, 2026 01:18
@ArquintL

Copy link
Copy Markdown
Member

Note: because the regression-test suite runs with disableCheckTerminationPureFns = true, the "must have a termination measure" case is not exercised by the suite (same as for pure functions); the 000841 test covers the always-checked rules (single result, non-variadic parameters, rejection of conditional measures).

You could add a testcase that uses an in-file config to set disableCheckTerminationPureFns to false or would that not work?

…ace methods

The regression-test suite runs with disableCheckTerminationPureFns = true, so the
requirement that a pure interface method carry a termination measure cannot be
exercised there. The type-checking unit tests, however, use the default Config()
in which the check is enabled, so we add two tests: a pure interface method
without a termination measure is rejected, and one with a measure is accepted.

Addresses ArquintL's review comment on issue #841.
// (e.g., `mayInit`, or the requirement to have a termination measure) are added by the callers.
// Precondition: `spec.isPure`.
private[typing] def wellDefIfPureSpec(node: PCodeRootWithResult, args: Vector[PParameter], spec: PFunctionSpec): Messages = {
isSingleResultArg(node) ++

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
isSingleResultArg(node) ++
requires(spec.isPure)
isSingleResultArg(node) ++

isSingleResultArg(node) ++
isPurePostcondition(spec) ++
pureMembersCannotHavePreserves(spec) ++
nonVariadicArguments(args)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why don't we check here the presence of termination measures in a uniform way?

Comment on lines 104 to +106
sigsWithWildcardMeasuresErrors ++
sigsWithConditionalMeasuresErrors ++
pureSigErrors ++

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I really don't want to blame anyone here as I guess things happened incrementally, but I'm not happy with this design:

  • wellDefIfPureSpec is supposed to make the checks uniform for pure members...
  • but then pureSigErrors checks for the presence of termination measures...
  • without however checking whether the present measures are meaningful (non-wildecard & non-conditional)

Comment on lines +18 to +20
decreases
pure
M1(a int)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
decreases
pure
M1(a int)
decreases pure M1(a int)

Comment on lines +25 to +26
pure
M2(a int) int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
pure
M2(a int) int
pure M2(a int) int

Comment on lines +30 to +32
pure
//:: ExpectedOutput(type_error)
M3(a ...int) int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
pure
//:: ExpectedOutput(type_error)
M3(a ...int) int
//:: ExpectedOutput(type_error)
pure M3(a ...int) int

Comment on lines +36 to +39
pure
//:: ExpectedOutput(type_error)
decreases _ if b // we syntactically disallow conditional termination measures for members that must terminate
func f(b bool) bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
pure
//:: ExpectedOutput(type_error)
decreases _ if b // we syntactically disallow conditional termination measures for members that must terminate
func f(b bool) bool
//:: ExpectedOutput(type_error)
decreases _ if b // we syntactically disallow conditional termination measures for members that must terminate
pure func f(b bool) bool

Comment on lines +363 to +370
test("Typing: a pure interface method without a termination measure is not well-defined") {
assert (!frontend.isWellDef(pureInterfaceType(Vector.empty)).valid)
}

test("Typing: a pure interface method with a termination measure is well-defined") {
val measure = PTupleTerminationMeasure(Vector.empty, None)
assert (frontend.isWellDef(pureInterfaceType(Vector(measure))).valid)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

haha not quite what I had in mind but I guess it tests the same ^^

@jcp19
jcp19 marked this pull request as draft July 17, 2026 08:53
@jcp19

jcp19 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Oops, I saw that Claude went ahead and commented on this thread and marked this for review without me expecting or requesting any of these actions. Sorry that this led you to spend your time on something that was not ready for review @ArquintL

jcp19 added 2 commits July 17, 2026 09:07
Address review feedback on the scattered measure checks for pure interface
methods:

- wellDefIfPureSpec now validates the termination measure of every pure member
  (function, method, and interface method signature): it must be present (unless
  disableCheckTerminationPureFns) and non-conditional. It also asserts its
  precondition that the spec is pure.
- wellFoundedIfNeeded / noConditionalMeasureIfGhostOrPure are renamed to
  wellFoundedIfGhost / noConditionalMeasureIfGhost and restricted to ghost
  (non-pure) members, since pure members are now handled uniformly; this avoids
  double-reporting.
- The interface handling in wellDefType delegates pure-signature checks entirely
  to wellDefIfPureSpec and only rejects conditional measures for ghost (non-pure)
  signatures; wildcard measures remain rejected for all interface signatures.
- Restructure the 000841 regression test into one-liner method signatures.

Removes the TypeTypingUnitTests cases added earlier: the anonymous-interface
harness does not exercise interface method-signature checks, so they did not
test the intended behaviour.
'decreases pure M1(a int)' does not parse: the decreases clause consumes the
following tokens as the measure and rejects the 'pure' keyword. Put 'decreases'
on its own line; 'pure' directly before the method name parses fine.
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.

Requiring termination measures for pure interface methods

2 participants