Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c56df8f
feat(parser): Allow computed indices in AST
Leo-Besancon Aug 14, 2025
426ffd2
feat(mir): Allow computed indices in MIR
Leo-Besancon Aug 14, 2025
124ca12
tests: Add computed indices E2E test
Leo-Besancon Aug 14, 2025
5456c4b
tests(parser): Add computed indices parser tests
Leo-Besancon Aug 14, 2025
00df247
fix(parser): fix binding type access on non constant index
Leo-Besancon Aug 14, 2025
b3bc934
tests(mir): add mir tests for computed indices
Leo-Besancon Aug 14, 2025
52dbd20
chore: update CHANGELOG.md
Leo-Besancon Aug 18, 2025
2891862
tests: Update E2E test to have correct trace
Leo-Besancon Aug 18, 2025
1202dec
chore: use chained if lets
Leo-Besancon Aug 18, 2025
6634e61
tests: add computed_indices_complex E2E test
Leo-Besancon Aug 18, 2025
f9f7689
tests: Sort winterfell tests alphabetically
Leo-Besancon Aug 18, 2025
d4a07ab
fix: duplicate_node for argument nodes in calls
Leo-Besancon Aug 19, 2025
8af9c70
fix: Run Mir inlining until fixed point
Leo-Besancon Aug 19, 2025
62f6ddd
Merge branch 'next' into allow-computed-indices-upstream
Leo-Besancon Aug 24, 2025
a150733
refactor: Handle range as constant, remove their propagation into MIR
Leo-Besancon Aug 25, 2025
9b01f26
refactor: multiple cleanups addressing review comments
Leo-Besancon Sep 2, 2025
b27ec35
refactor(mir translate): remove FIXMEs and factorize code
Leo-Besancon Sep 2, 2025
e63ae1a
refactor(mir): Factorize code between constant propagation and unrolling
Leo-Besancon Sep 2, 2025
2b0de7d
Merge branch 'next' into allow-computed-indices-upstream
Leo-Besancon Sep 4, 2025
85d4dc2
Merge branch 'next' into allow-computed-indices-upstream
Leo-Besancon Sep 22, 2025
b40cca5
fix: small fixes after merge
Leo-Besancon Sep 22, 2025
88bb70c
refactor: refactor MIR's constant_propagation visit_node
Leo-Besancon Sep 22, 2025
6e734dc
feat(mir): Value::get_inner_const
Soulthym Oct 1, 2025
90b5bdc
fix(mir): rename handle_accessor_visit arg
Soulthym Oct 9, 2025
abbf525
fix(parser): make AccessType::Matrix unreachable for TraceBinding
Soulthym Oct 9, 2025
c8adbf5
fix(mir): improve docs and unify `expect_constant_indices` accross APIs.
Soulthym Oct 9, 2025
82cd993
test(mir): adapt computed_indices_complex to use the trace as state
Soulthym Oct 9, 2025
770b6b6
tests: rework computed indices test based on review, add comments
Leo-Besancon Oct 13, 2025
c48bfd4
chore: lint fix
Leo-Besancon Oct 13, 2025
a988ae5
Merge branch 'next' into allow-computed-indices-upstream
Leo-Besancon Oct 13, 2025
d7b557f
refactor: remove unneeded Option
Leo-Besancon Oct 13, 2025
02610a4
Merge branch 'next' into allow-computed-indices-upstream
Leo-Besancon Oct 14, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Update documentation and tests thereof (#437).
- Add a constant propagation pass after other mir passes (#439).
- Removed `TraceSegmentId::index()` and replaced segment indexing with `TraceShape<T>`/`FullTraceShape<T>` across ACE codegen, MIR-to-AIR pass, and constraints (#442).
- Allow computed indices (#444).
- Fix regressions on MIR and list_comprehensions (#449).

## 0.4.0 (2025-06-20)
Expand Down
173 changes: 96 additions & 77 deletions air-script/tests/codegen/winterfell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ use expect_test::expect_file;

use super::helpers::{Target, Test};

// tests_wo_mir
// ================================================================================================

#[test]
fn binary() {
let generated_air = Test::new("tests/binary/binary.air".to_string())
Expand All @@ -16,21 +13,12 @@ fn binary() {
}

#[test]
fn buses_simple() {
let generated_air = Test::new("tests/buses/buses_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../buses/buses_simple.rs"];
expected.assert_eq(&generated_air);
}
#[test]
fn buses_simple_with_evaluators() {
let generated_air = Test::new("tests/buses/buses_simple_with_evaluators.air".to_string())
fn bitwise() {
let generated_air = Test::new("tests/bitwise/bitwise.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../buses/buses_simple.rs"];
let expected = expect_file!["../bitwise/bitwise.rs"];
expected.assert_eq(&generated_air);
}

Expand All @@ -45,22 +33,22 @@ fn buses_complex() {
}

#[test]
fn buses_varlen_boundary_first() {
let generated_air = Test::new("tests/buses/buses_varlen_boundary_first.air".to_string())
fn buses_simple() {
let generated_air = Test::new("tests/buses/buses_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../buses/buses_varlen_boundary_first.rs"];
let expected = expect_file!["../buses/buses_simple.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn buses_varlen_boundary_last() {
let generated_air = Test::new("tests/buses/buses_varlen_boundary_last.air".to_string())
fn buses_simple_with_evaluators() {
let generated_air = Test::new("tests/buses/buses_simple_with_evaluators.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../buses/buses_varlen_boundary_last.rs"];
let expected = expect_file!["../buses/buses_simple.rs"];
expected.assert_eq(&generated_air);
}

Expand All @@ -75,42 +63,53 @@ fn buses_varlen_boundary_both() {
}

#[test]
fn periodic_columns() {
let generated_air = Test::new("tests/periodic_columns/periodic_columns.air".to_string())
fn buses_varlen_boundary_first() {
let generated_air = Test::new("tests/buses/buses_varlen_boundary_first.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../periodic_columns/periodic_columns.rs"];
let expected = expect_file!["../buses/buses_varlen_boundary_first.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn pub_inputs() {
let generated_air = Test::new("tests/pub_inputs/pub_inputs.air".to_string())
fn buses_varlen_boundary_last() {
let generated_air = Test::new("tests/buses/buses_varlen_boundary_last.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../pub_inputs/pub_inputs.rs"];
let expected = expect_file!["../buses/buses_varlen_boundary_last.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn system() {
let generated_air = Test::new("tests/system/system.air".to_string())
fn computed_indices_complex() {
let generated_air =
Test::new("tests/computed_indices/computed_indices_complex.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../computed_indices/computed_indices_complex.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn computed_indices_simple() {
let generated_air = Test::new("tests/computed_indices/computed_indices_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../system/system.rs"];
let expected = expect_file!["../computed_indices/computed_indices_simple.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn bitwise() {
let generated_air = Test::new("tests/bitwise/bitwise.air".to_string())
fn constant_in_range() {
let generated_air = Test::new("tests/constant_in_range/constant_in_range.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../bitwise/bitwise.rs"];
let expected = expect_file!["../constant_in_range/constant_in_range.rs"];
expected.assert_eq(&generated_air);
}

Expand All @@ -125,12 +124,21 @@ fn constants() {
}

#[test]
fn constant_in_range() {
let generated_air = Test::new("tests/constant_in_range/constant_in_range.air".to_string())
.transpile(Target::Winterfell)
.unwrap();
fn constraint_comprehension() {
let generated_air =
Test::new("tests/constraint_comprehension/constraint_comprehension.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../constant_in_range/constant_in_range.rs"];
let expected = expect_file!["../constraint_comprehension/constraint_comprehension.rs"];
expected.assert_eq(&generated_air);

let generated_air =
Test::new("tests/constraint_comprehension/cc_with_evaluators.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../constraint_comprehension/constraint_comprehension.rs"];
expected.assert_eq(&generated_air);
}

Expand All @@ -154,28 +162,6 @@ fn fibonacci() {
expected.assert_eq(&generated_air);
}

#[test]
fn functions_simple() {
let generated_air = Test::new("tests/functions/functions_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../functions/functions_simple.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn functions_simple_inlined() {
// make sure that the constraints generated using inlined functions are the same as the ones
// generated using regular functions
let generated_air = Test::new("tests/functions/inlined_functions_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../functions/functions_simple.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn functions_complex() {
let generated_air = Test::new("tests/functions/functions_complex.air".to_string())
Expand All @@ -187,22 +173,24 @@ fn functions_complex() {
}

#[test]
fn variables() {
let generated_air = Test::new("tests/variables/variables.air".to_string())
fn functions_simple() {
let generated_air = Test::new("tests/functions/functions_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../variables/variables.rs"];
let expected = expect_file!["../functions/functions_simple.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn trace_col_groups() {
let generated_air = Test::new("tests/trace_col_groups/trace_col_groups.air".to_string())
fn functions_simple_inlined() {
// make sure that the constraints generated using inlined functions are the same as the ones
// generated using regular functions
let generated_air = Test::new("tests/functions/inlined_functions_simple.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../trace_col_groups/trace_col_groups.rs"];
let expected = expect_file!["../functions/functions_simple.rs"];
expected.assert_eq(&generated_air);
}

Expand Down Expand Up @@ -248,6 +236,26 @@ fn list_folding() {
expected.assert_eq(&generated_air);
}

#[test]
fn periodic_columns() {
let generated_air = Test::new("tests/periodic_columns/periodic_columns.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../periodic_columns/periodic_columns.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn pub_inputs() {
let generated_air = Test::new("tests/pub_inputs/pub_inputs.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../pub_inputs/pub_inputs.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn selectors() {
let generated_air = Test::new("tests/selectors/selectors.air".to_string())
Expand Down Expand Up @@ -297,20 +305,31 @@ fn selectors_combine_with_list_comprehensions() {
}

#[test]
fn constraint_comprehension() {
let generated_air =
Test::new("tests/constraint_comprehension/constraint_comprehension.air".to_string())
.transpile(Target::Winterfell)
.unwrap();
fn system() {
let generated_air = Test::new("tests/system/system.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../constraint_comprehension/constraint_comprehension.rs"];
let expected = expect_file!["../system/system.rs"];
expected.assert_eq(&generated_air);
}

let generated_air =
Test::new("tests/constraint_comprehension/cc_with_evaluators.air".to_string())
.transpile(Target::Winterfell)
.unwrap();
#[test]
fn trace_col_groups() {
let generated_air = Test::new("tests/trace_col_groups/trace_col_groups.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../constraint_comprehension/constraint_comprehension.rs"];
let expected = expect_file!["../trace_col_groups/trace_col_groups.rs"];
expected.assert_eq(&generated_air);
}

#[test]
fn variables() {
let generated_air = Test::new("tests/variables/variables.air".to_string())
.transpile(Target::Winterfell)
.unwrap();

let expected = expect_file!["../variables/variables.rs"];
expected.assert_eq(&generated_air);
}
61 changes: 61 additions & 0 deletions air-script/tests/computed_indices/computed_indices_complex.air
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
def ComputedIndicesAir

const MDS = [
[1, 2],
[2, 3],
[3, 4]
];

trace_columns {
main: [a[2], s[2]],
}

public_inputs {
input: [1],
}

fn double(a: felt) -> felt {
let x = 3 * a;
let y = a;
return x - y;
}

boundary_constraints {
enf a[0].first = 0;
}

# Note:
# In this test, we aim to test that computed indices work well even if:
# - The value of the index can only be known late during the compilation process (during MIR's constant propagation)
# - The
integrity_constraints {

# vec_1 is a list_comprehension that depends on the state
# vec_1 = [
# 1 * s[0] + 2 * s[1],
# 2 * s[0] + 3 * s[1],
# 3 * s[0] + 4 * s[1]
# ];
let vec_1 = apply_mds(s);

let state_2 = [2, 0];
# vec_2 is a list_comprehension that will not get constant-folded early, but will produce constant values
let vec_2 = apply_mds(state_2);

# x will get the value 2 * 2 - 4 = 0
let x = double(2) - vec_2[1];

# y will get the value vec_2[0] = 2
let y = vec_2[x];

# z will then be vec_1[2] = 3 * s[0] + 4 * s[1]
let z = vec_1[y];

# we enforce 3 * s[0] + 4 * s[1] = 0
enf z = 0;
}

# We use apply_mds function to produce a list comprehension that will not get constant-folded during AST
fn apply_mds(state: felt[2]) -> felt[3] {
return [sum([s * m for (s, m) in (state, mds_row)]) for mds_row in MDS];
}
Comment on lines +59 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the part that was not possible before, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

This may not be the right place, but I imagine this is actually the only place where we'd ever use matrices. For the rare cases where we want to evaluate a matrix (only hash functions I think), we could just manually implement it. If so it might be worth removing matrices entirely. WDYT?

Copy link
Collaborator

@Soulthym Soulthym Oct 9, 2025

Choose a reason for hiding this comment

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

This is the part that was not possible before, right?

Yes when coupled with a function. I think it was ok without a function before because it was computed before semantic analysis.

If so it might be worth removing matrices entirely. WDYT?

I agree, its already in the todo list for the frontend refactor. Ill gather those in a separate issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the part that was not possible before, right?

Not exactly, the apply_mds function was already tested in https://github.com/0xMiden/air-script/blob/allow-computed-indices-upstream/air-script/tests/list_comprehension/list_comprehension_nested.air
In the list_comprehension_nested test, it is indeed applied to the state as you suggested below, so we can check the rust code.

What is new is to be able to index into the vec_1 and vec_2 list comprehensions with indices whose can only be computed late into the compilation process (for instance, because they rely on list comprehensions)

In this test (computed_indices_complex), I needed a vector that would not be folded as a constant during AST's constant propagation, otherwise I would not be able to test the computed indices targetting list comprehensions properly.

I'll add some comments to make what this test does clearer.

Loading