Skip to content

Support arrays, structs, and identifiers in ternary operators - #3222

Open
mohammadfawaz wants to merge 3 commits into
stagingfrom
mohammadfawaz/complex_ternary
Open

mohammadfawaz wants to merge 3 commits into
stagingfrom
mohammadfawaz/complex_ternary

Conversation

@mohammadfawaz

@mohammadfawaz mohammadfawaz commented Apr 17, 2026 •

Copy link
Copy Markdown
Collaborator

Extends ternary to accept arrays, structs, and identifier literals. This change requires ConsensusVersion::V23.

Before V23, deployment verification rejects the new operand types in functions, finalize scopes, closures, constructors, and views. Existing literal operands remain supported.

The console and circuit implementations select array elements and struct members recursively. Type checking requires matching array shapes and struct layouts. Strings, including string fields inside arrays or structs, remain unsupported.

Literal ternary operations retain the existing finalize cost of 500 microcredits. Array and struct ternary costs scale with the recursive size of the branch operands.

@mohammadfawaz mohammadfawaz changed the title Support arrays and structs and ternary operators Support arrays and structs in ternary operators Apr 17, 2026
@mohammadfawaz mohammadfawaz self-assigned this Apr 17, 2026
@mohammadfawaz
mohammadfawaz force-pushed the mohammadfawaz/complex_ternary branch 2 times, most recently from be16d8f to f333d2e Compare April 17, 2026 03:14
@mohammadfawaz
mohammadfawaz marked this pull request as ready for review April 17, 2026 14:46
@mohammadfawaz
mohammadfawaz force-pushed the mohammadfawaz/complex_ternary branch from f333d2e to f6e5a6b Compare April 17, 2026 16:42
@mohammadfawaz
mohammadfawaz requested review from d0cd and vicsn April 17, 2026 16:42
@mohammadfawaz
mohammadfawaz force-pushed the mohammadfawaz/complex_ternary branch from f6e5a6b to d794d30 Compare April 17, 2026 16:45
@mohammadfawaz mohammadfawaz changed the title Support arrays and structs in ternary operators Support arrays, structs, and identifiers in ternary operators Apr 17, 2026
@mohammadfawaz

mohammadfawaz commented Apr 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Support for this in Leo is here: aleo-labs-protocol/leo#29349

Shows on average 7.8% drop in Aleo program sizes across the Leo test suite.

@mohammadfawaz
mohammadfawaz force-pushed the mohammadfawaz/complex_ternary branch from d794d30 to fa9c2c2 Compare May 12, 2026 17:36
@mohammadfawaz
mohammadfawaz force-pushed the mohammadfawaz/complex_ternary branch 4 times, most recently from 3b0ed49 to bc6da40 Compare May 29, 2026 23:04
@mohammadfawaz
mohammadfawaz requested review from cbeck88 and removed request for d0cd October 27, 2026 11:52
@veria-ai

veria-ai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@cbeck88

cbeck88 commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

I reviewed this at 775e55e4d. I found no consensus problems. Literal ternary bytecode and constraints do not change. Pre-V23 nodes and post-PR nodes agree on which ternaries to reject before V23.

Blocking

1. Restore the console/circuit equivalence tests for literal ternary.
The PR removes TernaryOperation from operation! in operation/mod.rs. This also removes the generated test_execute! tests. Those tests compared console evaluate with circuit execute for every literal type and every mode combination. The new circuit literal test checks only the selected value, and it does not cover Signature. Please add tests that compare console with circuit for each literal type and mode, including Signature. Please add the same for the new array and struct cases.

2. Pin the Ternary wire format to the Literals<N, O, 3> format.
The new Ternary struct copies Parser, Display, FromBytes and ToBytes from Literals<N, O, 3>. Deployed bytecode depends on these formats. Now there are two copies, and they can drift apart. Please add tests that show that Ternary and Literals<N, O, 3> produce the same bytes and the same text, and that each parses the other's output. Another option is to share the operand and destination container and override only evaluate, execute and output_types.

3. Rewrite the doc comments in identifier_literal.rs.
The doc comments at L771 and L814 say the deployment is "aborted at block verification rather than rejected at deploy time". That compares the code with an earlier state. Please describe only what the test checks today.

Non-blocking

4. unreachable! on a shape mismatch.
The Plaintext/Literal ternary in console/program/src/data/plaintext/ternary.rs and its circuit twin panic on a shape mismatch. The old Literals path returned an error. Nothing can reach this today, because register stores check each value against its declared type. But a future gap would crash a validator in finalize instead of rejecting a transaction. Consider a shape check in Ternary::evaluate/execute that returns an error.

5. Ternary finalize cost.
For non-literal branches, the cost adds the sizes of both branch operands. Cast charges by output size. So the two do not match. I think charging for both branches is more correct, because both values are read. I mention it only so that the difference is intentional.

6. Build the stack once in verify_deployment.
The V23 gate at verify.rs#L369 builds a new Stack. The V20 block at L446 builds another one. Mainnet is at V22, so until V23 each deployment builds both stacks. verify_deployment also type-checks the program again. You can build one stack before these checks and reuse it. Also, a Stack::new error at L369 does not get the "Invalid deployment transaction '{id}'" prefix.

7. Simpler literal check.
branches_are_literal in cost.rs and the loop in check_no_non_literal_ternary look up the types of both branches. output_types already proves that the branches are equivalent. You can check operands()[1] once, as Cast does.

@cbeck88

cbeck88 commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

May be good to on rebase on latest staging to get full ci run. But generally looks fine to me

Mohammad Fawaz and others added 3 commits November 2, 2026 09:32
Restore console and circuit equivalence coverage and pin the ternary wire format to the literal operand container. Return errors for invalid runtime branch shapes. Reuse the deployment stack, simplify literal checks, and clarify the cost and V14 test comments.
@mohammadfawaz
mohammadfawaz force-pushed the mohammadfawaz/complex_ternary branch from 8434bdd to 933aced Compare November 2, 2026 14:40

@Antonio95 Antonio95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Only two minor comments, one of which may merit a bit more discussion.

Comment thread synthesizer/process/src/cost.rs Resolved

/// Returns whether this instruction refers to an external struct.
#[inline]
pub fn contains_external_struct(&self) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am not familiar with the preexisting contains_external_struct machinery, but is it correct for this to return false in the case of instructions such as ternary r0 r1 r2 into r3; where r1 is an external struct?

};

// Ensure the second and third input types are equivalent.
if !register_types_equivalent(stack, &input_types[1], stack, &input_types[2])? {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This answers some of the questions I had regarding the behaviour as far as types and external structs are concerned. I think it might be good to very briefly document (maybe in code and also in the Aleo docs) what that behaviour is.

I'm not sure if register_types_equivalent is the best call here. It seems that: for local structs, it only accepts if the two struct types are the exact same (same name, in the same (local) program). Yet for (local, external), (external, local) and (external, external) combinations, it performs the recursive "same entry names and (recursively) same types" check - which is more flexible than that in the (local, local) case. If I'm not mistaken (but please correct me if I'm misunderstanding sth), the current check means one can write programs a.aleo and b.aleo such that

  • in a.aleo, ternary r0 r1 r2 into r3 with r1 of (local) type struct_1 and r2 of (local) type struct_2, is rejected
  • in b.aleo, ternary r0 r1 r2 into r3 with r1 of (ext) type a.aleo/struct_1 and r2 of (ext) type a.aleo/struct_2 is allowed

This feels a bit odd. Is that the case? If so, we can use a variant of register_types_equivalent which treats the (local, local) case like the rest. In any case (and even if my understanding is wrong), it would be good to have a test for that specific case in order to pin (and show) the expected behaviour.


/// Returns `first` if `condition` is `true`, otherwise returns `second`.
/// The `first` and `second` literals must be the same variant, and must be a variant for which
/// ternary selection is supported. Callers are expected to enforce this via type-checking

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I have double-checked with AI that this is indeed the case 👌

This branch has not been deployed

No deployments
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.

3 participants