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
22 changes: 22 additions & 0 deletions .release-notes/fix-union-constraint-cap.md
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 42 additions & 1 deletion src/libponyc/type/typeparam.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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: {}
Expand Down
159 changes: 159 additions & 0 deletions test/full-program-tests/regression-2674/main.pony
Original file line number Diff line number Diff line change
@@ -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: `_<CapA><CapB>` 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)
Loading