Skip to content

Commit dbd4689

Browse files
feature(cairo): Support Span<T> destructuring via fixed-size array patterns
Add support for `let [a, b] = span else { ... }` syntax by introducing a SliceDestructure flow control node that calls tuple_from_span to convert Span<T> to [T; N] at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 30746c2 commit dbd4689

11 files changed

Lines changed: 595 additions & 27 deletions

File tree

corelib/src/test/language_features/match_test.cairo

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ fn test_match_multienum_binding() {
1212
panic!("Match expression did not return - this should be unreachable");
1313
}
1414

15+
#[test]
16+
fn test_match_span_to_fixed_size_array() {
17+
let span: Span<u32> = array![10, 20, 30].span();
18+
19+
match span {
20+
[_a, _b, _c, _d] => {
21+
panic!("Expected 3 elements, but got 4");
22+
},
23+
[_a, _b] => {
24+
panic!("Expected 3 elements, but got 2");
25+
},
26+
[a, b, c] => {
27+
assert_eq!(a, @10);
28+
assert_eq!(b, @20);
29+
assert_eq!(c, @30);
30+
},
31+
_ => panic!("Expected 3 elements, but got a different pattern"),
32+
}
33+
}
34+
1535
#[test]
1636
fn test_match_extern_multilevel() {
1737
if true {

crates/cairo-lang-lowering/src/lower/flow_control/create_graph/patterns.rs

Lines changed: 170 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ use cairo_lang_debug::DebugWithDb;
22
use cairo_lang_defs::ids::NamedLanguageElementId;
33
use cairo_lang_diagnostics::{DiagnosticNote, Maybe};
44
use cairo_lang_filesystem::flag::FlagsGroup;
5-
use cairo_lang_semantic::corelib::{CorelibSemantic, validate_literal};
5+
use cairo_lang_filesystem::ids::SmolStrId;
6+
use cairo_lang_semantic::corelib::{
7+
CorelibSemantic, get_usize_ty, try_get_core_ty_by_name, validate_literal,
8+
};
69
use cairo_lang_semantic::expr::compute::unwrap_pattern_type;
10+
use cairo_lang_semantic::items::constant::ConstValue;
711
use cairo_lang_semantic::items::enm::SemanticEnumEx;
812
use cairo_lang_semantic::items::structure::StructSemantic;
13+
use cairo_lang_semantic::types::wrap_in_snapshots;
914
use cairo_lang_semantic::{
1015
self as semantic, ConcreteEnumId, ConcreteStructId, ConcreteTypeId, ExprNumericLiteral,
11-
PatternEnumVariant, PatternLiteral, PatternStruct, PatternTuple, PatternWrappingInfo, TypeId,
12-
TypeLongId, corelib,
16+
GenericArgumentId, PatternEnumVariant, PatternLiteral, PatternStruct, PatternTuple,
17+
PatternWrappingInfo, TypeId, TypeLongId, corelib,
1318
};
1419
use cairo_lang_syntax::node::TypedStablePtr;
1520
use cairo_lang_syntax::node::ast::ExprPtr;
21+
use cairo_lang_utils::Intern;
1622
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
1723
use itertools::{Itertools, zip_eq};
1824
use num_bigint::BigInt;
@@ -26,7 +32,9 @@ use super::filtered_patterns::{Bindings, FilteredPatterns};
2632
use crate::diagnostic::{LoweringDiagnosticKind, MatchDiagnostic, MatchError};
2733
use crate::ids::LocationId;
2834
use crate::lower::context::LoweringContext;
29-
use crate::lower::flow_control::graph::{Downcast, EqualsLiteral, Upcast, ValueMatch};
35+
use crate::lower::flow_control::graph::{
36+
Downcast, EqualsLiteral, SliceDestructure, Upcast, ValueMatch,
37+
};
3038

3139
/// A callback that gets a [FilteredPatterns] and constructs a node that continues the pattern
3240
/// matching restricted to the filtered patterns.
@@ -339,6 +347,19 @@ fn create_node_for_struct<'db>(
339347
) -> NodeId {
340348
let CreateNodeParams { ctx, graph, patterns, build_node_callback, location } = params;
341349

350+
if let Some(node) = try_create_slice_destructure_chain(
351+
ctx,
352+
graph,
353+
patterns,
354+
build_node_callback,
355+
location,
356+
input_var,
357+
concrete_struct_id,
358+
wrapping_info,
359+
) {
360+
return node;
361+
}
362+
342363
let members = match ctx.db.concrete_struct_members(concrete_struct_id) {
343364
Ok(members) => members,
344365
Err(diag_added) => return graph.add_node(FlowControlNode::Missing(diag_added)),
@@ -375,6 +396,144 @@ fn create_node_for_struct<'db>(
375396
}))
376397
}
377398

399+
/// Tries to create a chain of [`SliceDestructure`] nodes for matching a `Span<T>` against
400+
/// fixed-size array patterns with different sizes.
401+
///
402+
/// Returns `None` if no `FixedSizeArray` patterns are present or the struct is not a `Span`.
403+
/// Each size is tried in order. On failure, the next size is attempted. If all sizes fail,
404+
/// the wildcard/otherwise patterns are used.
405+
#[allow(clippy::too_many_arguments)]
406+
fn try_create_slice_destructure_chain<'db>(
407+
ctx: &LoweringContext<'db, '_>,
408+
graph: &mut FlowControlGraphBuilder<'db>,
409+
patterns: &[PatternOption<'_, 'db>],
410+
build_node_callback: BuildNodeCallback<'db, '_>,
411+
location: LocationId<'db>,
412+
input_var: FlowControlVar,
413+
concrete_struct_id: ConcreteStructId<'db>,
414+
wrapping_info: PatternWrappingInfo,
415+
) -> Option<NodeId> {
416+
if !patterns.iter().any(|p| matches!(p, Some(semantic::Pattern::FixedSizeArray(..)))) {
417+
return None;
418+
}
419+
let [GenericArgumentId::Type(elem_ty)] = concrete_struct_id.long(ctx.db).generic_args[..]
420+
else {
421+
return None;
422+
};
423+
if try_get_core_ty_by_name(
424+
ctx.db,
425+
SmolStrId::from(ctx.db, "Span"),
426+
vec![GenericArgumentId::Type(elem_ty)],
427+
)
428+
.is_err()
429+
{
430+
// Not a Span - report error on the first FixedSizeArray pattern.
431+
let first_fsa = patterns.iter().find_map(|p| match p {
432+
Some(semantic::Pattern::FixedSizeArray(p)) => Some(p),
433+
_ => None,
434+
});
435+
return Some(graph.report_with_missing_node(
436+
first_fsa.unwrap().stable_ptr.untyped(),
437+
LoweringDiagnosticKind::UnexpectedError,
438+
));
439+
}
440+
// Deconstruct Span<T> to get its single member @Array<T>.
441+
let members = ctx.db.concrete_struct_members(concrete_struct_id).ok()?;
442+
let snapshot_array_ty = members.iter().next().unwrap().1.ty;
443+
let snapshot_array_var = graph.new_var(snapshot_array_ty, location);
444+
445+
// Group patterns by array size. Wildcards/otherwise are added to all groups.
446+
// Use an OrderedHashMap to preserve insertion order (first-seen size first).
447+
let mut size_groups: OrderedHashMap<usize, SizeGroupInfo> = OrderedHashMap::default();
448+
let mut wildcard_filter = FilteredPatterns::default();
449+
450+
for (idx, pattern) in patterns.iter().enumerate() {
451+
match pattern {
452+
Some(semantic::Pattern::FixedSizeArray(p)) => {
453+
let n = p.elements_patterns.len();
454+
let group = size_groups.entry(n).or_default();
455+
group.filter.add(idx);
456+
group.patterns.push(*pattern);
457+
}
458+
Some(semantic::Pattern::Otherwise(..)) | None => {
459+
wildcard_filter.add(idx);
460+
for group in size_groups.values_mut() {
461+
group.filter.add(idx);
462+
group.patterns.push(None);
463+
}
464+
}
465+
_ => unreachable!("Non-FixedSizeArray/Otherwise pattern in slice destructure chain"),
466+
}
467+
}
468+
469+
let sizes: Vec<usize> = size_groups.keys().copied().collect();
470+
471+
// Build the chain from back to front. The final fallback is the wildcard-only callback.
472+
let mut failure_node = build_node_callback(graph, wildcard_filter, "[slice_no_match]".into());
473+
474+
for &size in sizes.iter().rev() {
475+
let group = size_groups.swap_remove(&size).unwrap();
476+
let n = size;
477+
let types = vec![wrap_in_snapshots(ctx.db, elem_ty, 1); n];
478+
let inner_vars = types
479+
.iter()
480+
.map(|ty| graph.new_var(wrapping_info.wrap(ctx.db, *ty), location))
481+
.collect_vec();
482+
483+
// Build the success path: process element patterns within this size group.
484+
let group_filter = group.filter;
485+
let group_patterns: Vec<PatternOption<'_, 'db>> = group.patterns;
486+
let success = create_node_for_tuple_inner(
487+
CreateNodeParams {
488+
ctx,
489+
graph,
490+
patterns: &group_patterns,
491+
build_node_callback: &mut |graph, pattern_indices, path| {
492+
build_node_callback(
493+
graph,
494+
pattern_indices.lift(&group_filter),
495+
format!("[{path}]"),
496+
)
497+
},
498+
location,
499+
},
500+
&inner_vars,
501+
&types,
502+
0,
503+
None,
504+
);
505+
506+
let fixed_array_ty = TypeLongId::FixedSizeArray {
507+
type_id: elem_ty,
508+
size: ConstValue::Int(n.into(), get_usize_ty(ctx.db)).intern(ctx.db),
509+
}
510+
.intern(ctx.db);
511+
512+
failure_node = graph.add_node(FlowControlNode::SliceDestructure(SliceDestructure {
513+
input: snapshot_array_var,
514+
fixed_array_ty,
515+
outputs: inner_vars,
516+
success,
517+
failure: failure_node,
518+
}));
519+
}
520+
521+
// Wrap in a Deconstruct to extract @Array<T> from Span<T> once.
522+
let chain = graph.add_node(FlowControlNode::Deconstruct(Deconstruct {
523+
input: input_var,
524+
outputs: vec![snapshot_array_var],
525+
next: failure_node,
526+
}));
527+
528+
Some(chain)
529+
}
530+
531+
#[derive(Default)]
532+
struct SizeGroupInfo<'a, 'db> {
533+
filter: FilteredPatterns,
534+
patterns: Vec<PatternOption<'a, 'db>>,
535+
}
536+
378537
/// Helper function for [create_node_for_tuple].
379538
///
380539
/// `item_idx` is the index of the current member that is being processed in the tuple.
@@ -429,6 +588,13 @@ fn create_node_for_tuple_inner<'db>(
429588
patterns_on_current_item.push(Some(inner_pattern))
430589
}
431590
}
591+
Some(semantic::Pattern::FixedSizeArray(semantic::PatternFixedSizeArray {
592+
elements_patterns,
593+
..
594+
})) if current_member.is_none() => {
595+
patterns_on_current_item
596+
.push(Some(get_pattern(ctx, elements_patterns[item_idx]).clone()));
597+
}
432598
Some(
433599
pattern @ (semantic::Pattern::StringLiteral(..)
434600
| semantic::Pattern::EnumVariant(..)

crates/cairo-lang-lowering/src/lower/flow_control/graph.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,24 @@ pub struct Downcast {
224224
pub out_of_range: NodeId,
225225
}
226226

227+
/// Destructures a `@Array<T>` into a fixed-size array `[T; N]` by calling `tuple_from_span`.
228+
///
229+
/// On success, the array has exactly `N` elements and the output variables are bound to them.
230+
/// On failure (wrong number of elements), execution continues to the `failure` node.
231+
#[derive(Debug)]
232+
pub struct SliceDestructure<'db> {
233+
/// The input `@Array<T>` variable (already extracted from the Span).
234+
pub input: FlowControlVar,
235+
/// The fixed-size array type `[T; N]`.
236+
pub fixed_array_ty: semantic::TypeId<'db>,
237+
/// The output element variables (if the slice has the right size).
238+
pub outputs: Vec<FlowControlVar>,
239+
/// The next node if the slice has the right number of elements.
240+
pub success: NodeId,
241+
/// The next node if the slice doesn't have the right number of elements.
242+
pub failure: NodeId,
243+
}
244+
227245
/// An arm (final node) that returns a tuple of bound variables for the let-else success arm.
228246
///
229247
/// See [crate::lower::lower_let_else::lower_let_else] for more details.
@@ -257,6 +275,8 @@ pub enum FlowControlNode<'db> {
257275
Upcast(Upcast),
258276
/// Downcasts a value to a smaller type.
259277
Downcast(Downcast),
278+
/// Unpacks a Span<T> into a fixed-size array [T; N].
279+
SliceDestructure(SliceDestructure<'db>),
260280
/// An arm (final node) that returns a tuple of bound variables for the let-else success arm.
261281
LetElseSuccess(LetElseSuccess<'db>),
262282
/// An arm (final node) that returns a unit value - `()`.
@@ -285,6 +305,7 @@ impl<'db> FlowControlNode<'db> {
285305
FlowControlNode::BindVar(node) => Some(node.input),
286306
FlowControlNode::Upcast(node) => Some(node.input),
287307
FlowControlNode::Downcast(node) => Some(node.input),
308+
FlowControlNode::SliceDestructure(node) => Some(node.input),
288309
FlowControlNode::LetElseSuccess(..) => None,
289310
FlowControlNode::UnitResult => None,
290311
FlowControlNode::Missing(_) => None,
@@ -306,6 +327,7 @@ impl<'db> Debug for FlowControlNode<'db> {
306327
FlowControlNode::BindVar(node) => node.fmt(f),
307328
FlowControlNode::Upcast(node) => node.fmt(f),
308329
FlowControlNode::Downcast(node) => node.fmt(f),
330+
FlowControlNode::SliceDestructure(node) => node.fmt(f),
309331
FlowControlNode::LetElseSuccess(node) => node.fmt(f),
310332
FlowControlNode::UnitResult => write!(f, "UnitResult"),
311333
FlowControlNode::Missing(_) => write!(f, "Missing"),

0 commit comments

Comments
 (0)