Skip to content

Commit 2621846

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 dad17c0 commit 2621846

7 files changed

Lines changed: 468 additions & 21 deletions

File tree

corelib/src/test/language_features/match_test.cairo

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ 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] => {
23+
assert_eq!(a, @10);
24+
assert_eq!(b, @20);
25+
assert_eq!(c, @30);
26+
},
27+
_ => panic!("Expected 3 elements, but got a different pattern"),
28+
}
29+
}
30+
1531
#[test]
1632
fn test_match_extern_multilevel() {
1733
if true {

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

Lines changed: 184 additions & 19 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.
@@ -150,21 +158,6 @@ pub fn create_node_for_patterns<'db>(
150158
create_node_for_enum(params, input_var, concrete_enum_id, wrapping_info)
151159
}
152160
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-
}
168161
create_node_for_struct(params, input_var, concrete_struct_id, wrapping_info)
169162
}
170163
TypeLongId::Tuple(types) => create_node_for_tuple(params, input_var, &types, wrapping_info),
@@ -354,6 +347,19 @@ fn create_node_for_struct<'db>(
354347
) -> NodeId {
355348
let CreateNodeParams { ctx, graph, patterns, build_node_callback, location } = params;
356349

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+
357363
let members = match ctx.db.concrete_struct_members(concrete_struct_id) {
358364
Ok(members) => members,
359365
Err(diag_added) => return graph.add_node(FlowControlNode::Missing(diag_added)),
@@ -390,6 +396,158 @@ fn create_node_for_struct<'db>(
390396
}))
391397
}
392398

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+
// Track accumulated wildcards so newly created size groups inherit earlier wildcards.
450+
let mut accumulated_wildcards: Vec<usize> = Vec::new();
451+
452+
for (idx, pattern) in patterns.iter().enumerate() {
453+
match pattern {
454+
Some(semantic::Pattern::FixedSizeArray(p)) => {
455+
let n = p.elements_patterns.len();
456+
let is_new = !size_groups.contains_key(&n);
457+
if is_new {
458+
size_groups.insert(n, SizeGroupInfo::default());
459+
}
460+
let group = size_groups.get_mut(&n).unwrap();
461+
if is_new {
462+
// Seed new size group with all previously accumulated wildcards.
463+
for &wc_idx in &accumulated_wildcards {
464+
group.filter.add(wc_idx);
465+
group.patterns.push(None);
466+
}
467+
}
468+
group.filter.add(idx);
469+
group.patterns.push(*pattern);
470+
}
471+
Some(semantic::Pattern::Otherwise(..)) | None => {
472+
wildcard_filter.add(idx);
473+
accumulated_wildcards.push(idx);
474+
for group in size_groups.values_mut() {
475+
group.filter.add(idx);
476+
group.patterns.push(None);
477+
}
478+
}
479+
_ => unreachable!("Non-FixedSizeArray/Otherwise pattern in slice destructure chain"),
480+
}
481+
}
482+
483+
let sizes: Vec<usize> = size_groups.keys().copied().collect();
484+
485+
// Build the chain from back to front. The final fallback is the wildcard-only callback.
486+
let mut failure_node = build_node_callback(graph, wildcard_filter, "[slice_no_match]".into());
487+
488+
for &size in sizes.iter().rev() {
489+
let group = size_groups.swap_remove(&size).unwrap();
490+
let n = size;
491+
let types = vec![wrap_in_snapshots(ctx.db, elem_ty, 1); n];
492+
let inner_vars = types
493+
.iter()
494+
.map(|ty| graph.new_var(wrapping_info.wrap(ctx.db, *ty), location))
495+
.collect_vec();
496+
497+
// Build the success path: process element patterns within this size group.
498+
let group_filter = group.filter;
499+
let group_patterns: Vec<PatternOption<'_, 'db>> = group.patterns;
500+
let success = create_node_for_tuple_inner(
501+
CreateNodeParams {
502+
ctx,
503+
graph,
504+
patterns: &group_patterns,
505+
build_node_callback: &mut |graph, pattern_indices, path| {
506+
build_node_callback(
507+
graph,
508+
pattern_indices.lift(&group_filter),
509+
format!("[{path}]"),
510+
)
511+
},
512+
location,
513+
},
514+
&inner_vars,
515+
&types,
516+
0,
517+
None,
518+
);
519+
520+
let fixed_array_ty = TypeLongId::FixedSizeArray {
521+
type_id: elem_ty,
522+
size: ConstValue::Int(n.into(), get_usize_ty(ctx.db)).intern(ctx.db),
523+
}
524+
.intern(ctx.db);
525+
526+
failure_node = graph.add_node(FlowControlNode::SliceDestructure(SliceDestructure {
527+
input: snapshot_array_var,
528+
fixed_array_ty,
529+
outputs: inner_vars,
530+
success,
531+
failure: failure_node,
532+
}));
533+
}
534+
535+
// Wrap in a Deconstruct to extract @Array<T> from Span<T> once.
536+
let chain = graph.add_node(FlowControlNode::Deconstruct(Deconstruct {
537+
input: input_var,
538+
outputs: vec![snapshot_array_var],
539+
next: failure_node,
540+
}));
541+
542+
Some(chain)
543+
}
544+
545+
#[derive(Default)]
546+
struct SizeGroupInfo<'a, 'db> {
547+
filter: FilteredPatterns,
548+
patterns: Vec<PatternOption<'a, 'db>>,
549+
}
550+
393551
/// Helper function for [create_node_for_tuple].
394552
///
395553
/// `item_idx` is the index of the current member that is being processed in the tuple.
@@ -444,6 +602,13 @@ fn create_node_for_tuple_inner<'db>(
444602
patterns_on_current_item.push(Some(inner_pattern))
445603
}
446604
}
605+
Some(semantic::Pattern::FixedSizeArray(semantic::PatternFixedSizeArray {
606+
elements_patterns,
607+
..
608+
})) if current_member.is_none() => {
609+
patterns_on_current_item
610+
.push(Some(get_pattern(ctx, elements_patterns[item_idx]).clone()));
611+
}
447612
Some(
448613
pattern @ (semantic::Pattern::StringLiteral(..)
449614
| semantic::Pattern::EnumVariant(..)

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,34 @@ 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+
pub struct SliceDestructure<'db> {
232+
/// The input `@Array<T>` variable (already extracted from the Span).
233+
pub input: FlowControlVar,
234+
/// The fixed-size array type `[T; N]`.
235+
pub fixed_array_ty: semantic::TypeId<'db>,
236+
/// The output element variables (if the slice has the right size).
237+
pub outputs: Vec<FlowControlVar>,
238+
/// The next node if the slice has the right number of elements.
239+
pub success: NodeId,
240+
/// The next node if the slice doesn't have the right number of elements.
241+
pub failure: NodeId,
242+
}
243+
impl Debug for SliceDestructure<'_> {
244+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245+
f.debug_struct("SliceDestructure")
246+
.field("input", &self.input)
247+
.field("n_elements", &self.outputs.len())
248+
.field("outputs", &self.outputs)
249+
.field("success", &self.success)
250+
.field("failure", &self.failure)
251+
.finish()
252+
}
253+
}
254+
227255
/// An arm (final node) that returns a tuple of bound variables for the let-else success arm.
228256
///
229257
/// See [crate::lower::lower_let_else::lower_let_else] for more details.
@@ -257,6 +285,8 @@ pub enum FlowControlNode<'db> {
257285
Upcast(Upcast),
258286
/// Downcasts a value to a smaller type.
259287
Downcast(Downcast),
288+
/// Unpacks a `Span<T>` into a fixed-size array `[T; N]`.
289+
SliceDestructure(SliceDestructure<'db>),
260290
/// An arm (final node) that returns a tuple of bound variables for the let-else success arm.
261291
LetElseSuccess(LetElseSuccess<'db>),
262292
/// An arm (final node) that returns a unit value - `()`.
@@ -285,6 +315,7 @@ impl<'db> FlowControlNode<'db> {
285315
FlowControlNode::BindVar(node) => Some(node.input),
286316
FlowControlNode::Upcast(node) => Some(node.input),
287317
FlowControlNode::Downcast(node) => Some(node.input),
318+
FlowControlNode::SliceDestructure(node) => Some(node.input),
288319
FlowControlNode::LetElseSuccess(..) => None,
289320
FlowControlNode::UnitResult => None,
290321
FlowControlNode::Missing(_) => None,
@@ -306,6 +337,7 @@ impl<'db> Debug for FlowControlNode<'db> {
306337
FlowControlNode::BindVar(node) => node.fmt(f),
307338
FlowControlNode::Upcast(node) => node.fmt(f),
308339
FlowControlNode::Downcast(node) => node.fmt(f),
340+
FlowControlNode::SliceDestructure(node) => node.fmt(f),
309341
FlowControlNode::LetElseSuccess(node) => node.fmt(f),
310342
FlowControlNode::UnitResult => write!(f, "UnitResult"),
311343
FlowControlNode::Missing(_) => write!(f, "Missing"),

0 commit comments

Comments
 (0)