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
61 changes: 47 additions & 14 deletions src/libponyc/codegen/gentrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -814,20 +814,14 @@ static void trace_dynamic_tuple(compile_t* c, LLVMValueRef ctx,
LLVMPositionBuilderAtEnd(c->builder, trace_block);

// If we are (A, B), turn (_, _) into (A, _).
// If the element is a type alias for a tuple, unfold it once so the
// If the element is a type alias for a tuple, unfold it so the
// recursive call sees the concrete TK_TUPLETYPE; iterating the alias
// ref's children (TK_ID, typeargs, cap, eph) as tuple elements would
// trip trace_type's default assertion. typealias_unfold is guaranteed
// to succeed here because trace_type already unfolded the same child
// to classify it as TRACE_TUPLE; reify is deterministic. Chained
// aliases (alias of alias of tuple) are a known gap tracked in
// ponylang/ponyc#5195 — typealias_unfold only unfolds one level, so
// swap is still a TK_TYPEALIASREF and the recursive call hits the
// same assertion. Constructing a test that reaches this site with a
// chained alias is currently blocked by other PR #5145 regressions
// elsewhere in codegen (the static trace_tuple path in this file and
// gen_digestof_value in genreference.c), which fire first for every
// configuration tried.
// to classify it as TRACE_TUPLE; reify is deterministic. The unfold
// is transitive, so chained aliases (alias of alias of tuple) also
// resolve to a concrete TK_TUPLETYPE here.
ast_t* swap;

if(ast_id(child) == TK_TYPEALIASREF)
Expand Down Expand Up @@ -1048,24 +1042,63 @@ bool gentrace_needed(compile_t* c, ast_t* src_type, ast_t* dst_type)

case TRACE_TUPLE:
{
// Unfold type aliases so the child iteration sees concrete tuple
// elements rather than the alias ref's children (TK_ID, typeargs,
// cap, eph). Without this, a tuple-aliased operand causes the loop
// to walk the wrong children and trip the trailing assertion.
// Note: although `trace_type` handles TK_TYPEALIASREF recursively
// and returned TRACE_TUPLE above, it takes src_type by value and
// doesn't mutate the caller's pointer, so src_type and dst_type
// here are still the original (possibly alias) ASTs.
ast_t* src_unfolded = NULL;
ast_t* dst_unfolded = NULL;

if(ast_id(src_type) == TK_TYPEALIASREF)
{
src_unfolded = typealias_unfold(src_type);
if(src_unfolded != NULL)
src_type = src_unfolded;
}

if(ast_id(dst_type) == TK_TYPEALIASREF)
{
dst_unfolded = typealias_unfold(dst_type);
if(dst_unfolded != NULL)
dst_type = dst_unfolded;
}

bool result;
if(ast_id(dst_type) == TK_TUPLETYPE)
{
result = false;
ast_t* src_child = ast_child(src_type);
ast_t* dst_child = ast_child(dst_type);
while((src_child != NULL) && (dst_child != NULL))
{
if(gentrace_needed(c, src_child, dst_child))
return true;
{
result = true;
break;
}
src_child = ast_sibling(src_child);
dst_child = ast_sibling(dst_child);
}
pony_assert(src_child == NULL && dst_child == NULL);
// The assert only holds when the loop ran to completion. If we
// broke early with `result = true`, src_child and dst_child
// point to the element that needs tracing, not NULL.
if(!result)
pony_assert(src_child == NULL && dst_child == NULL);
} else {
// This is a boxed tuple. We'll have to trace the box anyway.
return true;
result = true;
}

return false;
if(src_unfolded != NULL)
ast_free_unattached(src_unfolded);
if(dst_unfolded != NULL)
ast_free_unattached(dst_unfolded);

return result;
}

default:
Expand Down
6 changes: 6 additions & 0 deletions src/libponyc/pass/names.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
#include "../pkg/package.h"
#include "ponyassert.h"

// Resolve a type alias definition, rejecting cyclic chains. Note that
// `typealias_unfold` (in src/libponyc/type/typealias.c) relies on this
// rejection: its transitive recursion is bounded by the assumption that
// alias chains form a finite DAG, which only holds because cycles are
// rejected here. If you change the cycle detection mechanism, also update
// the safety argument in `typealias_unfold`.
static bool names_resolvealias(pass_opt_t* opt, ast_t* def, ast_t** type)
{
int state = ast_checkflag(def,
Expand Down
14 changes: 14 additions & 0 deletions src/libponyc/type/typealias.c
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,19 @@ ast_t* typealias_unfold(ast_t* typealiasref)
if((tcap != TK_NONE) || (teph != TK_NONE))
apply_cap_to_type(r_alias, tcap, teph);

// If the result is itself a TK_TYPEALIASREF (a chained alias like
// `type A is B; type B is (U64, U64)`), unfold transitively. This is
// bounded by the length of the alias chain: pass/names.c's
// names_resolvealias uses AST_FLAG_RECURSE_1 to detect and reject cyclic
// alias defs before any TK_TYPEALIASREF is emitted, so the chain is a
// finite DAG. Callers rely on the returned head not being a
// TK_TYPEALIASREF (see typealias.h).
if(ast_id(r_alias) == TK_TYPEALIASREF)
{
ast_t* next = typealias_unfold(r_alias);
ast_free_unattached(r_alias);
return next;
}

return r_alias;
}
12 changes: 11 additions & 1 deletion src/libponyc/type/typealias.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@ PONY_EXTERN_C_BEGIN
/**
* Unfold a TK_TYPEALIASREF to its underlying type. Reifies the alias
* definition with the stored type arguments and applies the cap and
* ephemeral modifiers.
* ephemeral modifiers. If the result is itself a TK_TYPEALIASREF (a chained
* alias), unfolds transitively until the head is a concrete type.
*
* Guarantees that a non-NULL result has a head that is not TK_TYPEALIASREF.
* Callers dispatching on the head of the result can therefore rely on the
* full set of non-alias heads (TK_NOMINAL, TK_UNIONTYPE, TK_ISECTTYPE,
* TK_TUPLETYPE, TK_TYPEPARAMREF, TK_ARROW, etc.) without a TK_TYPEALIASREF
* case. Nested aliases inside a compound type (a member of a TK_UNIONTYPE,
* TK_ISECTTYPE, or TK_TUPLETYPE that is itself an alias) are not unfolded;
* callers that recurse into compound types must still dispatch on
* TK_TYPEALIASREF for interior positions.
*
* Returns a newly allocated AST node, or NULL if reification fails.
* The caller must free a non-NULL result with ast_free_unattached when done.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
38 changes: 38 additions & 0 deletions test/full-program-tests/type-alias-chained-actor-message/main.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Regression test for chained type aliases in behavior message argument
// tracing.
//
// Sending a chained-alias-tuple argument to a behavior whose parameter
// is also typed with the same alias triggers `gen_send_message` in
// `gencall.c` to emit a GC-trace call for the argument. The trace
// codegen runs via `gentrace` -> `trace_type` (TRACE_TUPLE) ->
// `trace_tuple` at `gentrace.c:611`. The `if(ast_id(dst_type) ==
// TK_TUPLETYPE)` check is FALSE for an alias-typed parameter, so
// control falls into the else (boxed tuple) branch which iterates
// `ast_child(src_type)`. For a single-level or chained TK_TYPEALIASREF
// `src_type`, `ast_child` walks (TK_ID, typeargs, cap, eph); the
// recursive `gentrace` call on TK_ID hits `trace_type`'s default
// `pony_assert(0)` at `gentrace.c:383`.
//
// The transitive unfold in `typealias_unfold` (together with gentrace's
// top-level unfold of src/dst) resolves the chain to a concrete
// TK_TUPLETYPE before `trace_tuple` runs, so the iteration sees real
// tuple element children.
//
// This test exercises the message-send codegen path with both src and
// dst being alias types, distinct from the asymmetric pattern covered
// by type-alias-tuple-gentrace-needed and from the actor-field path
// covered by type-alias-chained-static-trace. See ponylang/ponyc#5195.

use @pony_exitcode[None](code: I32)

type _Pair is (U64, U64)
type _Middle is _Pair

actor Helper
be receive(x: _Middle) =>
@pony_exitcode(1)

actor Main
new create(env: Env) =>
let h = Helper
h.receive((U64(1), U64(2)))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
33 changes: 33 additions & 0 deletions test/full-program-tests/type-alias-chained-array-element/main.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Regression test for chained type aliases as array element types.
//
// Building an `Array[_Middle]` where `_Middle` is a chained alias to a
// tuple triggers generation of the array's element trace function. The
// trace codegen calls `gentrace` on the element type, which dispatches
// via `trace_type` (`src/libponyc/codegen/gentrace.c:354`). For a chained
// alias, the one-level unfold path reaches `trace_type`'s default
// `pony_assert(0)` at line 383.
//
// This test exercises the array-element trace codegen path for chained
// aliases, distinct from the actor-field path covered by
// type-alias-chained-static-trace and the actor-message path covered by
// type-alias-chained-actor-message. See ponylang/ponyc#5195.

use @pony_exitcode[None](code: I32)

type _Pair is (U64, U64)
type _Middle is _Pair

actor Main
new create(env: Env) =>
let arr: Array[_Middle] = [(U64(1), U64(2)); (U64(3), U64(4))]
try
let first = arr(0)?
let second = arr(1)?
if ((first._1 + first._2) == 3) and ((second._1 + second._2) == 7) then
@pony_exitcode(1)
end
else
// Unreachable: arr(0) and arr(1) cannot fail on a 2-element array.
// Use a distinct exit code so the test fails visibly if it ever does.
@pony_exitcode(2)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
48 changes: 48 additions & 0 deletions test/full-program-tests/type-alias-chained-class-cap/main.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Positive-coverage test for chained type aliases over a class type with
// cap-restricted method dispatch.
//
// **Note on counterfactual sensitivity:** this test passes both with and
// without the transitive `typealias_unfold` fix, so it does not catch a
// regression in this PR's specific change on its own. The reason is that
// every code path that resolves chained class aliases for method
// dispatch (lookup.c, subtype.c) recurses on TK_TYPEALIASREF results
// from `typealias_unfold`, so the per-caller recursion handles a chain
// of length N regardless of whether the helper itself is single-level or
// transitive. Tuple-based tests in this PR exercise the actual crash
// sites; this test exists to document that chained class aliases work
// end-to-end (allocation through the chain, ref-cap method dispatch
// through the chain, box-cap read through the chain) and to guard
// against future regressions in that surface area. See ponylang/ponyc#5195.

use @pony_exitcode[None](code: I32)

class Counter
var _count: U64 = 0

fun ref bump() =>
_count = _count + 1

fun box current(): U64 =>
_count

type _CounterAlias is Counter
type _ChainedCounter is _CounterAlias

actor Main
new create(env: Env) =>
// Allocate via the chained alias type. Resolution must walk the chain
// (_ChainedCounter -> _CounterAlias -> Counter) and produce a value
// whose cap permits the ref-restricted bump() call below.
let c: _ChainedCounter = Counter

// Calls a ref method. If cap propagation through the chain breaks,
// this fails to compile (cap mismatch on the receiver).
c.bump()
c.bump()
c.bump()

// Calls a box method. Verifies the receiver is also box-compatible
// (ref <: box) and the mutation observed by bump() actually happened.
if c.current() == 3 then
@pony_exitcode(1)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
26 changes: 26 additions & 0 deletions test/full-program-tests/type-alias-chained-digestof/main.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Regression test for chained type aliases in gen_digestof_value.
//
// A chained alias (alias of alias of tuple) used as the declared type of a
// digestof operand reached LLVMStructTypeKind in gen_digestof_value, which
// unfolded the alias only one level before iterating tuple element types.
// The one-level unfold returned a still-TK_TYPEALIASREF, whose children are
// (TK_ID, typeargs, cap, eph) rather than tuple elements, tripping the
// `child == NULL` assertion after the iteration count reached the struct
// width. See ponylang/ponyc#5195.
//
// The fix made typealias_unfold transitive; the test exercises the
// digestof-of-local path specifically (a local variable so the actor has
// no fields, avoiding the companion static trace_tuple regression).

use @pony_exitcode[None](code: I32)

type _Pair is (U64, U64)
type _Middle is _Pair

actor Main
new create(env: Env) =>
let aliased: _Middle = (U64(1), U64(2))
let plain: (U64, U64) = (U64(1), U64(2))
if (digestof aliased) == (digestof plain) then
@pony_exitcode(1)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
28 changes: 28 additions & 0 deletions test/full-program-tests/type-alias-chained-dynamic-trace/main.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Regression test for chained type aliases inside dynamically-traced tuples.
//
// An outer tuple wrapped in a union (which routes trace generation through
// trace_dynamic rather than the static trace_tuple path) with a child that
// is a chained alias to a tuple. The fix in ponylang/ponyc#5196 added a
// typealias_unfold inside trace_dynamic_tuple's TRACE_TUPLE branch but
// unfolded only one level, so a chained alias still arrived at the
// recursive trace_dynamic_tuple call as a TK_TYPEALIASREF and the
// iteration of alias-ref children as tuple elements tripped trace_type's
// default assertion. See ponylang/ponyc#5195.
//
// The fix made typealias_unfold transitive, so the swap value at the
// unfold site is guaranteed to be a concrete TK_TUPLETYPE even for
// chained aliases. This test exercises that path — the field type is
// `((String, _Middle) | None)` so trace dispatch goes through
// trace_dynamic, and `_Middle` is a chained alias to `_Pair`.

use @pony_exitcode[None](code: I32)

type _Pair is (U64, U64)
type _Middle is _Pair

actor Main
var _field: ((String, _Middle) | None) = None

new create(env: Env) =>
_field = ("hello", (U64(1), U64(2)))
@pony_exitcode(1)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Regression test for chained type aliases as tuple element types in
// boxed tuple identity comparisons.
//
// `tuple_is_box_element` in `src/libponyc/codegen/genident.c:245-269`
// classifies a tuple element's kind based on `ast_id(check_field)` after
// a one-level unfold. If the element is a chained alias, the unfold
// returns another TK_TYPEALIASREF, the TK_TUPLETYPE check at line 263
// is false, `is_machine_word` is also false, and `field_kind` stays at
// SUBTYPE_KIND_UNBOXED. The generated identity comparison then takes
// the wrong branch (pointer equality instead of numeric equality on the
// element), which is a silent miscompilation rather than a crash.
//
// The `type-alias-chained-tuple-is` test covers `tuple_is` and
// `tuple_is_box` at the outer tuple level, but those sites iterate
// element types that are unfolded by their caller. To reach
// `tuple_is_box_element` with a chained-alias element, the element
// itself must be a chained alias. This test uses a chained alias to
// U64 as one element of a pair, then compares an unboxed value against
// a union-boxed value to force dispatch through
// `tuple_is_box` -> `tuple_is_box_element`.
//
// See ponylang/ponyc#5195.

use @pony_exitcode[None](code: I32)

type _InnerNum is U64
type _ChainedNum is _InnerNum
type _Pair is (_ChainedNum, U64)

actor Main
new create(env: Env) =>
let a: _Pair = (U64(1), U64(2))
let boxed_equal: (_Pair | None) = (U64(1), U64(2))
let boxed_unequal: (_Pair | None) = (U64(3), U64(4))

if not (a is boxed_equal) then return end
if a is boxed_unequal then return end

@pony_exitcode(1)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Loading
Loading