fix[codegen]: widen ternary arms to the result type layout - #5236
Conversation
Gas ChangesNo changes detected. Summary
|
📊 Bytecode Size Changes (venom)No changes detected. Full bytecode sizes
|
Sporarum
left a comment
There was a problem hiding this comment.
Good find !
It might be possible to remove the need for a lot of added logic by setting the type in metadata in a smarter way instead, see one of the comments below
| def punnable(src_typ, dst_typ): | ||
| """ | ||
| Return True if memory laid out as `src_typ` can be read (punned) as `dst_typ`. | ||
|
|
||
| Compatible types can differ in element or member layout, e.g. | ||
| DynArray[Bytes[10], 5] vs DynArray[Bytes[512], 5] (element stride 64 | ||
| vs 544). A wider bytestring bound or DynArray capacity at the top level | ||
| does not change the layout of the data that is present. | ||
| """ | ||
| if isinstance(dst_typ, (DArrayT, SArrayT)): | ||
| assert isinstance(src_typ, (DArrayT, SArrayT)) | ||
| pairs = [(src_typ.value_type, dst_typ.value_type)] | ||
| elif isinstance(dst_typ, TupleT): | ||
| assert isinstance(src_typ, TupleT) | ||
| n = len(dst_typ.member_types) | ||
| assert len(src_typ.member_types) == n | ||
| pairs = [(src_typ.member_types[i], dst_typ.member_types[i]) for i in range(n)] | ||
| else: | ||
| # primitive words, bytestrings ([length][data]) and (nominal) structs | ||
| return True | ||
|
|
||
| # nested values also need equal sizes: they determine strides and offsets | ||
| return all( | ||
| s.memory_bytes_required == d.memory_bytes_required and punnable(s, d) for s, d in pairs | ||
| ) |
There was a problem hiding this comment.
I think this can be simplified by moving the length check to the beginning:
| def punnable(src_typ, dst_typ): | |
| """ | |
| Return True if memory laid out as `src_typ` can be read (punned) as `dst_typ`. | |
| Compatible types can differ in element or member layout, e.g. | |
| DynArray[Bytes[10], 5] vs DynArray[Bytes[512], 5] (element stride 64 | |
| vs 544). A wider bytestring bound or DynArray capacity at the top level | |
| does not change the layout of the data that is present. | |
| """ | |
| if isinstance(dst_typ, (DArrayT, SArrayT)): | |
| assert isinstance(src_typ, (DArrayT, SArrayT)) | |
| pairs = [(src_typ.value_type, dst_typ.value_type)] | |
| elif isinstance(dst_typ, TupleT): | |
| assert isinstance(src_typ, TupleT) | |
| n = len(dst_typ.member_types) | |
| assert len(src_typ.member_types) == n | |
| pairs = [(src_typ.member_types[i], dst_typ.member_types[i]) for i in range(n)] | |
| else: | |
| # primitive words, bytestrings ([length][data]) and (nominal) structs | |
| return True | |
| # nested values also need equal sizes: they determine strides and offsets | |
| return all( | |
| s.memory_bytes_required == d.memory_bytes_required and punnable(s, d) for s, d in pairs | |
| ) | |
| def punnable(src_typ, dst_typ): | |
| """ | |
| Return True if memory laid out as `src_typ` can be read (punned) as `dst_typ`. | |
| Compatible types can differ in element or member layout, e.g. | |
| DynArray[Bytes[10], 5] vs DynArray[Bytes[512], 5] (element stride 64 | |
| vs 544). A wider bytestring bound or DynArray capacity at the top level | |
| does not change the layout of the data that is present. | |
| """ | |
| if src_typ.memory_bytes_required == dst_typ.memory_bytes_required: | |
| return False | |
| if isinstance(dst_typ, (DArrayT, SArrayT)): | |
| assert isinstance(src_typ, (DArrayT, SArrayT)) | |
| pairs = [(src_typ.value_type, dst_typ.value_type)] | |
| elif isinstance(dst_typ, TupleT): | |
| assert isinstance(src_typ, TupleT) | |
| n = len(dst_typ.member_types) | |
| assert len(src_typ.member_types) == n | |
| pairs = [(src_typ.member_types[i], dst_typ.member_types[i]) for i in range(n)] | |
| else: | |
| # primitive words, bytestrings ([length][data]) and (nominal) structs | |
| return True | |
| # nested values also need equal sizes: they determine strides and offsets | |
| return all(punnable(s, d) for s, d in pairs) |
Which then allows us to recurse immediately:
| def punnable(src_typ, dst_typ): | |
| """ | |
| Return True if memory laid out as `src_typ` can be read (punned) as `dst_typ`. | |
| Compatible types can differ in element or member layout, e.g. | |
| DynArray[Bytes[10], 5] vs DynArray[Bytes[512], 5] (element stride 64 | |
| vs 544). A wider bytestring bound or DynArray capacity at the top level | |
| does not change the layout of the data that is present. | |
| """ | |
| if isinstance(dst_typ, (DArrayT, SArrayT)): | |
| assert isinstance(src_typ, (DArrayT, SArrayT)) | |
| pairs = [(src_typ.value_type, dst_typ.value_type)] | |
| elif isinstance(dst_typ, TupleT): | |
| assert isinstance(src_typ, TupleT) | |
| n = len(dst_typ.member_types) | |
| assert len(src_typ.member_types) == n | |
| pairs = [(src_typ.member_types[i], dst_typ.member_types[i]) for i in range(n)] | |
| else: | |
| # primitive words, bytestrings ([length][data]) and (nominal) structs | |
| return True | |
| # nested values also need equal sizes: they determine strides and offsets | |
| return all( | |
| s.memory_bytes_required == d.memory_bytes_required and punnable(s, d) for s, d in pairs | |
| ) | |
| def punnable(src_typ, dst_typ): | |
| """ | |
| Return True if memory laid out as `src_typ` can be read (punned) as `dst_typ`. | |
| Compatible types can differ in element or member layout, e.g. | |
| DynArray[Bytes[10], 5] vs DynArray[Bytes[512], 5] (element stride 64 | |
| vs 544). A wider bytestring bound or DynArray capacity at the top level | |
| does not change the layout of the data that is present. | |
| """ | |
| if src_typ.memory_bytes_required == dst_typ.memory_bytes_required: | |
| return False | |
| if isinstance(dst_typ, (DArrayT, SArrayT)): | |
| assert isinstance(src_typ, (DArrayT, SArrayT)) | |
| return punnable(src_typ.value_type, dst_typ.value_type) | |
| elif isinstance(dst_typ, TupleT): | |
| assert isinstance(src_typ, TupleT) | |
| assert len(src_typ.member_types) == len(dst_typ.member_types) | |
| return all(punnable(s, d) for s,d in zip(src_typ.member_types, dst_typ.member_types)) | |
| else: | |
| # primitive words, bytestrings ([length][data]) and (nominal) structs | |
| return True |
There was a problem hiding this comment.
The top-level size is deliberately not compared. Bytes[10] vs Bytes[40] and DynArray[uint256, 2] vs DynArray[uint256, 5] are punnable despite different sizes (the same-type test asserts no copy for those), and size_in_bytes panics for a DynArray[..., INF] result. Sizes only matter one level down, where they fix the stride.
Also the suggested early return is inverted (returns False on equal sizes), and the second version brings zip back, which @charles-cooper asked to remove above.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 22ada0e45d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
What I did
Fix a silent miscompile in both codegen pipelines: a ternary (
x if c else y) over an aggregate returned data laid out per the arm's type but read with the widened result type. For examplereturned
[b"a", b"", b""]fora = [b"a", b"0123456789", b""]. Same for nested DynArray capacity widening, static arrays of DynArrays, String elements and tuple members (via locals or internal call results). Legacy and venom codegen are both affected.How I did it
Each arm is converted to the result layout inside its own branch, before the join. Legacy
parse_IfExpcopies the arm into an internal variable of the result type withmake_setter; venomlower_IfExpuses its typed memory store. Both already widen element-wise.The gate is a shared structural layout check,
same_memory_layoutincodegen/core.py, not type equality. Same-type arms, bytestring arms with different bounds, capacity-only widening and unbounded result types produce byte-identical IR to master on both pipelines.How to verify it
Commit message
Description for the changelog
Fix ternary expressions over aggregates whose arms are narrower than the result type.
Cute Animal Picture