Skip to content

Commit 344d7d3

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) <[email protected]>
1 parent 8d7164b commit 344d7d3

7 files changed

Lines changed: 1042 additions & 22 deletions

File tree

corelib/src/test/language_features/match_test.cairo

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ 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] => { panic!("Expected 3 elements, but got 4"); },
21+
[_a, _b] => { panic!("Expected 3 elements, but got 2"); },
22+
[a, b, c] => { assert_eq!((*a, *b, *c), (10, 20, 30)); },
23+
_ => panic!("Expected 3 elements, but got a different pattern"),
24+
}
25+
}
26+
27+
#[test]
28+
fn test_match_span_empty_pattern() {
29+
let span: Span<u32> = array![].span();
30+
31+
match span {
32+
[_a] => { panic!("Expected 0 elements, but got 1"); },
33+
[] => {},
34+
_ => panic!("Expected 0 elements, but got a different count"),
35+
}
36+
}
37+
1538
#[test]
1639
fn test_match_extern_multilevel() {
1740
if true {
@@ -24,3 +47,17 @@ fn test_match_extern_multilevel() {
2447
}
2548
panic!("Match expression did not return - this should be unreachable");
2649
}
50+
51+
52+
#[test]
53+
fn test_match_span_inner_pattern_mismatch() {
54+
let matcher = |s: Array<Option<felt252>>| match s.span() {
55+
[Some(_)] => 1,
56+
[None] => 2,
57+
_ => 0,
58+
};
59+
60+
assert_eq!(matcher(array![Some(42)]), 1);
61+
assert_eq!(matcher(array![None]), 2);
62+
assert_eq!(matcher(array![Some(1), Some(2)]), 0);
63+
}

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

Lines changed: 185 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ 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::{CorelibSemantic, try_get_core_ty_by_name, validate_literal};
67
use cairo_lang_semantic::expr::compute::unwrap_pattern_type;
78
use cairo_lang_semantic::items::enm::SemanticEnumEx;
89
use cairo_lang_semantic::items::structure::StructSemantic;
10+
use cairo_lang_semantic::types::wrap_in_snapshots;
911
use cairo_lang_semantic::{
1012
self as semantic, ConcreteEnumId, ConcreteStructId, ConcreteTypeId, ExprNumericLiteral,
11-
PatternEnumVariant, PatternLiteral, PatternStruct, PatternTuple, PatternWrappingInfo, TypeId,
12-
TypeLongId, corelib,
13+
GenericArgumentId, PatternEnumVariant, PatternLiteral, PatternStruct, PatternTuple,
14+
PatternWrappingInfo, TypeId, TypeLongId, corelib,
1315
};
1416
use cairo_lang_syntax::node::TypedStablePtr;
1517
use cairo_lang_syntax::node::ast::ExprPtr;
@@ -26,7 +28,9 @@ use super::filtered_patterns::{Bindings, FilteredPatterns};
2628
use crate::diagnostic::{LoweringDiagnosticKind, MatchDiagnostic, MatchError};
2729
use crate::ids::LocationId;
2830
use crate::lower::context::LoweringContext;
29-
use crate::lower::flow_control::graph::{Downcast, EqualsLiteral, Upcast, ValueMatch};
31+
use crate::lower::flow_control::graph::{
32+
Downcast, EqualsLiteral, SliceDestructure, Upcast, ValueMatch,
33+
};
3034

3135
/// A callback that gets a [FilteredPatterns] and constructs a node that continues the pattern
3236
/// matching restricted to the filtered patterns.
@@ -150,21 +154,6 @@ pub fn create_node_for_patterns<'db>(
150154
create_node_for_enum(params, input_var, concrete_enum_id, wrapping_info)
151155
}
152156
TypeLongId::Concrete(ConcreteTypeId::Struct(concrete_struct_id)) => {
153-
// Check if any non-any pattern is a FixedSizeArray (i.e. Span destructure).
154-
// Span destructuring in match/if-let is not yet supported in lowering.
155-
let has_fixed_size_array_pattern = patterns
156-
.iter()
157-
.flatten()
158-
.any(|p| matches!(p, semantic::Pattern::FixedSizeArray(..)));
159-
if has_fixed_size_array_pattern {
160-
return graph.report_with_missing_node(
161-
first_non_any_pattern.stable_ptr(),
162-
LoweringDiagnosticKind::MatchError(MatchError {
163-
kind: graph.kind(),
164-
error: MatchDiagnostic::UnsupportedMatchedType(long_ty.format(ctx.db)),
165-
}),
166-
);
167-
}
168157
create_node_for_struct(params, input_var, concrete_struct_id, wrapping_info)
169158
}
170159
TypeLongId::Tuple(types) => create_node_for_tuple(params, input_var, &types, wrapping_info),
@@ -354,6 +343,19 @@ fn create_node_for_struct<'db>(
354343
) -> NodeId {
355344
let CreateNodeParams { ctx, graph, patterns, build_node_callback, location } = params;
356345

346+
if let Some(node) = try_create_slice_destructure_chain(
347+
ctx,
348+
graph,
349+
patterns,
350+
build_node_callback,
351+
location,
352+
input_var,
353+
concrete_struct_id,
354+
wrapping_info,
355+
) {
356+
return node;
357+
}
358+
357359
let members = match ctx.db.concrete_struct_members(concrete_struct_id) {
358360
Ok(members) => members,
359361
Err(diag_added) => return graph.add_node(FlowControlNode::Missing(diag_added)),
@@ -390,6 +392,163 @@ fn create_node_for_struct<'db>(
390392
}))
391393
}
392394

395+
/// Tries to create a chain of [`SliceDestructure`] nodes for matching a `Span<T>` against
396+
/// fixed-size array patterns with different sizes.
397+
///
398+
/// Returns `None` if no `FixedSizeArray` patterns are present or the struct is not a `Span`.
399+
/// Each size is tried in order. On failure, the next size is attempted. If all sizes fail,
400+
/// the wildcard/otherwise patterns are used.
401+
#[allow(clippy::too_many_arguments)]
402+
fn try_create_slice_destructure_chain<'db>(
403+
ctx: &LoweringContext<'db, '_>,
404+
graph: &mut FlowControlGraphBuilder<'db>,
405+
patterns: &[PatternOption<'_, 'db>],
406+
build_node_callback: BuildNodeCallback<'db, '_>,
407+
location: LocationId<'db>,
408+
input_var: FlowControlVar,
409+
concrete_struct_id: ConcreteStructId<'db>,
410+
wrapping_info: PatternWrappingInfo,
411+
) -> Option<NodeId> {
412+
if !patterns.iter().any(|p| matches!(p, Some(semantic::Pattern::FixedSizeArray(..)))) {
413+
return None;
414+
}
415+
let [GenericArgumentId::Type(elem_ty)] = concrete_struct_id.long(ctx.db).generic_args[..]
416+
else {
417+
return None;
418+
};
419+
if try_get_core_ty_by_name(
420+
ctx.db,
421+
SmolStrId::from(ctx.db, "Span"),
422+
vec![GenericArgumentId::Type(elem_ty)],
423+
)
424+
.is_err()
425+
{
426+
// Not a Span - report error on the first FixedSizeArray pattern.
427+
let first_fsa = patterns.iter().find_map(|p| match p {
428+
Some(semantic::Pattern::FixedSizeArray(p)) => Some(p),
429+
_ => None,
430+
});
431+
return Some(graph.report_with_missing_node(
432+
first_fsa.unwrap().stable_ptr.untyped(),
433+
LoweringDiagnosticKind::UnexpectedError,
434+
));
435+
}
436+
// Deconstruct Span<T> to get its single member @Array<T>.
437+
let members = ctx.db.concrete_struct_members(concrete_struct_id).ok()?;
438+
let snapshot_array_ty = members.iter().next().unwrap().1.ty;
439+
let snapshot_array_var = graph.new_var(snapshot_array_ty, location);
440+
441+
// Group patterns by array size. Wildcards/otherwise are added to all groups.
442+
// Use an OrderedHashMap to preserve insertion order (first-seen size first).
443+
let mut size_groups: OrderedHashMap<usize, SizeGroupInfo<'_, '_>> = OrderedHashMap::default();
444+
let mut wildcard_filter = FilteredPatterns::default();
445+
446+
for (idx, pattern) in patterns.iter().enumerate() {
447+
match pattern {
448+
Some(semantic::Pattern::FixedSizeArray(p)) => {
449+
let n = p.elements_patterns.len();
450+
let group = size_groups.entry(n).or_default();
451+
452+
group.filter.add(idx);
453+
group.patterns.push(*pattern);
454+
}
455+
Some(semantic::Pattern::Otherwise(..)) | None => {
456+
wildcard_filter.add(idx);
457+
for group in size_groups.values_mut() {
458+
group.filter.add(idx);
459+
group.patterns.push(None);
460+
}
461+
// Patterns after the wildcard pattern are unreachable, so we can break.
462+
break;
463+
}
464+
Some(pattern) => {
465+
// This should not be reachable without getting a semantic error.
466+
return Some(graph.report_with_missing_node(
467+
pattern.stable_ptr().untyped(),
468+
LoweringDiagnosticKind::UnexpectedError,
469+
));
470+
}
471+
}
472+
}
473+
474+
// Build the chain from back to front, since we need the fallback node before we can create a
475+
// node. The final fallback is the wildcard-only callback.
476+
// Note: `"_"` is used as the pattern for the non-exhaustive diagnostic, as there is no `[..]`
477+
// pattern.
478+
let mut failure_node = build_node_callback(graph, wildcard_filter, "_".into());
479+
480+
for (size, group) in size_groups.into_iter().rev() {
481+
let types = vec![wrap_in_snapshots(ctx.db, elem_ty, 1); size];
482+
let inner_vars = types
483+
.iter()
484+
.map(|ty| graph.new_var(wrapping_info.wrap(ctx.db, *ty), location))
485+
.collect_vec();
486+
487+
// Build the success path: process element patterns within this size group.
488+
let group_filter = group.filter;
489+
let group_patterns: Vec<PatternOption<'_, 'db>> = group.patterns;
490+
let success = create_node_for_tuple_inner(
491+
CreateNodeParams {
492+
ctx,
493+
graph,
494+
patterns: &group_patterns,
495+
build_node_callback: &mut |graph, pattern_indices, path| {
496+
build_node_callback(
497+
graph,
498+
pattern_indices.lift(&group_filter),
499+
format!("[{path}]"),
500+
)
501+
},
502+
location,
503+
},
504+
&inner_vars,
505+
&types,
506+
0,
507+
None,
508+
);
509+
510+
failure_node = graph.add_node(FlowControlNode::SliceDestructure(SliceDestructure {
511+
input: snapshot_array_var,
512+
element_ty: elem_ty,
513+
outputs: inner_vars,
514+
success,
515+
failure: failure_node,
516+
}));
517+
}
518+
519+
// Wrap in a Deconstruct to extract @Array<T> from Span<T> once.
520+
let chain = graph.add_node(FlowControlNode::Deconstruct(Deconstruct {
521+
input: input_var,
522+
outputs: vec![snapshot_array_var],
523+
next: failure_node,
524+
}));
525+
526+
Some(chain)
527+
}
528+
529+
/// Information accumulated for a single array-size group while lowering a `Span<T>` match.
530+
///
531+
/// When matching a `Span<T>` against multiple fixed-size array patterns (e.g. `[a, b]`,
532+
/// `[a, b, c]`), the patterns are partitioned by their length. Each distinct length gets its
533+
/// own `SizeGroupInfo`.
534+
#[derive(Default)]
535+
struct SizeGroupInfo<'a, 'db> {
536+
/// The indices (into the original list of match arms) of the patterns that belong to this
537+
/// size group — including any trailing wildcard/`_` patterns, which apply to every group.
538+
filter: FilteredPatterns,
539+
/// The per-arm patterns to dispatch on once the span has been destructured into a
540+
/// fixed-size array of this length. Wildcards appear as `None`.
541+
///
542+
/// Note: unlike [VariantInfo], which stores the inner pattern of each `EnumVariant` arm,
543+
/// here we store the whole `FixedSizeArray` pattern. The next stage —
544+
/// [create_node_for_tuple_inner] — walks elements by index and extracts
545+
/// `elements_patterns[item_idx]` itself (see the `FixedSizeArray` arm in that function), so
546+
/// pre-unpacking would just force us to duplicate or undo that logic. Enum variants have a
547+
/// single inner pattern and feed into [create_node_for_patterns], which wants it already
548+
/// unwrapped, hence the asymmetry.
549+
patterns: Vec<PatternOption<'a, 'db>>,
550+
}
551+
393552
/// Helper function for [create_node_for_tuple].
394553
///
395554
/// `item_idx` is the index of the current member that is being processed in the tuple.
@@ -444,6 +603,13 @@ fn create_node_for_tuple_inner<'db>(
444603
patterns_on_current_item.push(Some(inner_pattern))
445604
}
446605
}
606+
Some(semantic::Pattern::FixedSizeArray(semantic::PatternFixedSizeArray {
607+
elements_patterns,
608+
..
609+
})) if current_member.is_none() => {
610+
patterns_on_current_item
611+
.push(Some(get_pattern(ctx, elements_patterns[item_idx]).clone()));
612+
}
447613
Some(
448614
pattern @ (semantic::Pattern::StringLiteral(..)
449615
| semantic::Pattern::EnumVariant(..)

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

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

227+
/// Destructures a `@Array<T>` into a fixed-size array `[T; N]` via `TryInto<Span<T>, @Box<[T;
228+
/// N]>>`.
229+
///
230+
/// On success, the array has exactly `N` elements and the output variables are bound to them.
231+
/// On failure (wrong number of elements), execution continues to the `failure` node.
232+
pub struct SliceDestructure<'db> {
233+
/// The input `@Array<T>` variable (already extracted from the Span).
234+
pub input: FlowControlVar,
235+
/// The element type `T`. The array size `N` is `outputs.len()`.
236+
pub element_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+
245+
impl<'db> std::fmt::Debug for SliceDestructure<'db> {
246+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247+
// Ignore the element type for the debug output, it requires the db for formatting and is
248+
// not interesting enough.
249+
let SliceDestructure { input, element_ty: _, outputs, success, failure } = self;
250+
write!(
251+
f,
252+
"SliceDestructure {{ input: {:?}, outputs: {:?}, success: {:?}, failure: {:?}}}",
253+
input, outputs, success, failure,
254+
)
255+
}
256+
}
227257
/// An arm (final node) that returns a tuple of bound variables for the let-else success arm.
228258
///
229259
/// See [crate::lower::lower_let_else::lower_let_else] for more details.
@@ -257,6 +287,9 @@ pub enum FlowControlNode<'db> {
257287
Upcast(Upcast),
258288
/// Downcasts a value to a smaller type.
259289
Downcast(Downcast),
290+
/// Unpacks an `@Array<T>` (already extracted from a `Span<T>`) into a fixed-size array
291+
/// `[T; N]`.
292+
SliceDestructure(SliceDestructure<'db>),
260293
/// An arm (final node) that returns a tuple of bound variables for the let-else success arm.
261294
LetElseSuccess(LetElseSuccess<'db>),
262295
/// An arm (final node) that returns a unit value - `()`.
@@ -285,6 +318,7 @@ impl<'db> FlowControlNode<'db> {
285318
FlowControlNode::BindVar(node) => Some(node.input),
286319
FlowControlNode::Upcast(node) => Some(node.input),
287320
FlowControlNode::Downcast(node) => Some(node.input),
321+
FlowControlNode::SliceDestructure(node) => Some(node.input),
288322
FlowControlNode::LetElseSuccess(..) => None,
289323
FlowControlNode::UnitResult => None,
290324
FlowControlNode::Missing(_) => None,
@@ -306,6 +340,7 @@ impl<'db> Debug for FlowControlNode<'db> {
306340
FlowControlNode::BindVar(node) => node.fmt(f),
307341
FlowControlNode::Upcast(node) => node.fmt(f),
308342
FlowControlNode::Downcast(node) => node.fmt(f),
343+
FlowControlNode::SliceDestructure(node) => node.fmt(f),
309344
FlowControlNode::LetElseSuccess(node) => node.fmt(f),
310345
FlowControlNode::UnitResult => write!(f, "UnitResult"),
311346
FlowControlNode::Missing(_) => write!(f, "Missing"),

0 commit comments

Comments
 (0)