diff --git a/.release-notes/fix-union-constraint-cap.md b/.release-notes/fix-union-constraint-cap.md new file mode 100644 index 0000000000..c0725a3ffb --- /dev/null +++ b/.release-notes/fix-union-constraint-cap.md @@ -0,0 +1,22 @@ +## Fix incorrect `#any` capability for type parameters with union constraints + +A type parameter constrained by a union type — for example, +`[O: (Foo tag | Bar ref)]` — was being given the `#any` capability even when +every member's capability was in `#alias` (one of `ref`, `val`, `box`, or +`tag`). That made common patterns fail to type-check: + +```pony +primitive Writer[O: (Async tag | Sync ref)] + fun _write(out: O, data: String) => + None + + fun ok(out: O) => + _write(out, "+OK\r\n") // error: O #any ! is not a subtype of O #any +``` + +The compiler now derives the correct capability for these constraints. In the +example above, the capability is `#alias` (the smallest capability set that +contains both `tag` and `ref`), and the program compiles. + +The workaround of intersecting the union with `Any #alias` is no longer +needed. diff --git a/src/libponyc/type/typeparam.c b/src/libponyc/type/typeparam.c index 1719b99a76..2b7e73ffe6 100644 --- a/src/libponyc/type/typeparam.c +++ b/src/libponyc/type/typeparam.c @@ -23,7 +23,16 @@ static token_id cap_union_constraint(token_id a, token_id b) if(b == TK_NONE) return a; - // If we're in a set together, return the set. Otherwise, use #any. + // Return the smallest cap-set that contains both a and b. The reachable + // cap-sets and their members are: + // #read = ref | val | box + // #send = iso | val | tag + // #share = val | tag (a subset of both #send and #alias) + // #alias = ref | val | box | tag (a superset of #read and #share) + // #any = everything + // The result falls through to #any when no narrower set contains both. The + // function is called via a left-fold by cap_from_constraint, so the table + // must handle the result of a previous union as the `a` argument. switch(a) { case TK_ISO: @@ -47,6 +56,11 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_CAP_READ: return TK_CAP_READ; + case TK_TAG: + case TK_CAP_SHARE: + case TK_CAP_ALIAS: + return TK_CAP_ALIAS; + default: {} } break; @@ -67,8 +81,12 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_CAP_SEND: return TK_CAP_SEND; + case TK_CAP_ALIAS: + return TK_CAP_ALIAS; + default: {} } + break; case TK_BOX: switch(b) @@ -78,6 +96,11 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_CAP_READ: return TK_CAP_READ; + case TK_TAG: + case TK_CAP_SHARE: + case TK_CAP_ALIAS: + return TK_CAP_ALIAS; + default: {} } break; @@ -93,6 +116,12 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_CAP_SEND: return TK_CAP_SEND; + case TK_REF: + case TK_BOX: + case TK_CAP_READ: + case TK_CAP_ALIAS: + return TK_CAP_ALIAS; + default: {} } break; @@ -105,6 +134,11 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_BOX: return TK_CAP_READ; + case TK_TAG: + case TK_CAP_SHARE: + case TK_CAP_ALIAS: + return TK_CAP_ALIAS; + default: {} } break; @@ -133,6 +167,12 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_CAP_SEND: return TK_CAP_SEND; + case TK_REF: + case TK_BOX: + case TK_CAP_READ: + case TK_CAP_ALIAS: + return TK_CAP_ALIAS; + default: {} } break; @@ -145,6 +185,7 @@ static token_id cap_union_constraint(token_id a, token_id b) case TK_BOX: case TK_TAG: case TK_CAP_READ: + case TK_CAP_SHARE: return TK_CAP_ALIAS; default: {} diff --git a/test/full-program-tests/regression-2674/main.pony b/test/full-program-tests/regression-2674/main.pony new file mode 100644 index 0000000000..af480c643a --- /dev/null +++ b/test/full-program-tests/regression-2674/main.pony @@ -0,0 +1,159 @@ +""" +Regression test for https://github.com/ponylang/ponyc/issues/2674 + +A type parameter constrained by a union type where each member's cap is a +subset of #alias was being given cap #any. That made `_take(o)` style +re-aliasing fail with "argument type is O #any !" because #any can't be +re-aliased. The cap should be #alias. + +Each primitive below exercises a different switch arm in +`cap_union_constraint`. If the bug returns for any pair, the corresponding +primitive's body fails to type-check and the test fails to build. The +primitives are declared but not all reified — body type-checking happens at +declaration time regardless of reification. + +Naming convention: `_` names the first and second cap in the +union's source order. `cap_from_constraint` left-folds with +`cap_union_constraint(a, b)`, so the source `(X capA | Y capB)` exercises the +switch arm for `capA` with subcase `capB`. The symmetric switch arm is a +separate code path and gets its own helper. +""" + +interface tag _Async + be write(data: ByteSeq) + +interface ref _Sync + fun ref apply() + +interface box _Const + fun apply() + +// TK_REF switch arm. +primitive _RefTag[O: (_Sync ref | _Async tag)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _RefShare[O: (_Sync ref | Any #share)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _RefAlias[O: (_Sync ref | Any #alias)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// TK_VAL switch arm (the new TK_CAP_ALIAS entry is the only one this fix adds +// here; the pre-fix code also lacked a closing `break;` after TK_VAL, which +// the fix adds for structural correctness). +primitive _ValAlias[O: (_Const val | Any #alias)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// TK_BOX switch arm. +primitive _BoxTag[O: (_Const box | _Async tag)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _BoxShare[O: (_Const box | Any #share)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _BoxAlias[O: (_Const box | Any #alias)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// TK_TAG switch arm. +primitive _TagRef[O: (_Async tag | _Sync ref)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _TagBox[O: (_Async tag | _Const box)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _TagRead[O: (_Async tag | _Const #read)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _TagAlias[O: (_Async tag | Any #alias)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// TK_CAP_READ switch arm. +primitive _ReadTag[O: (_Const #read | _Async tag)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _ReadShare[O: (_Const #read | Any #share)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _ReadAlias[O: (_Const #read | Any #alias)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// TK_CAP_SHARE switch arm. +primitive _ShareRef[O: (Any #share | _Sync ref)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _ShareBox[O: (Any #share | _Const box)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _ShareRead[O: (Any #share | _Const #read)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _ShareAlias[O: (Any #share | Any #alias)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// TK_CAP_ALIAS switch arm. The pre-fix code lumped {ref, val, box, tag, +// cap_read} together with a single result; this fix adds cap_share to the +// same combined branch. Helpers for the pre-existing cases are included so a +// future regression that removes any one of them is caught. +primitive _AliasRef[O: (Any #alias | _Sync ref)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _AliasVal[O: (Any #alias | _Const val)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _AliasBox[O: (Any #alias | _Const box)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _AliasTag[O: (Any #alias | _Async tag)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _AliasRead[O: (Any #alias | _Const #read)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +primitive _AliasShare[O: (Any #alias | Any #share)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// Three-member fold chain. `cap_from_constraint` left-folds across the union, +// so this exercises `cap_union_constraint(#alias, _)` where the first +// argument was derived from a previous union step, not from a literal #alias +// member. +primitive _FoldChain[O: (_Sync ref | _Async tag | _Const box)] + fun take(o: O) => _take(o) + fun _take(o: O) => None + +// The exact pattern from the issue, exercising the iftype-then-recall flow. +primitive _IssueExample[O: (_Async tag | _Sync ref)] + fun _write(out: O, data: String) => + iftype O <: _Async tag then None + elseif O <: _Sync ref then None + end + + fun ok(out: O) => + _write(out, "ok") + +actor Main + new create(env: Env) => + _IssueExample[_Async].ok(env.out)