diff --git a/compiler/ast/src/expressions/mod.rs b/compiler/ast/src/expressions/mod.rs index 7dc277a7287..ac5711e7ed7 100644 --- a/compiler/ast/src/expressions/mod.rs +++ b/compiler/ast/src/expressions/mod.rs @@ -53,6 +53,9 @@ pub use intrinsic::*; mod repeat; pub use repeat::*; +mod slice; +pub use slice::*; + mod ternary; pub use ternary::*; @@ -106,6 +109,8 @@ pub enum Expression { MemberAccess(Box), /// An array expression constructed from one repeated element, e.g., `[1u32; 5]`. Repeat(Box), + /// An array expression constructed from a slice of another array, e.g., `arr[1..4]`. + Slice(Box), /// A ternary conditional expression `cond ? if_expr : else_expr`. Ternary(Box), /// A tuple expression e.g., `(foo, 42, true)`. @@ -142,6 +147,7 @@ impl Node for Expression { Locator(n) => n.span(), MemberAccess(n) => n.span(), Repeat(n) => n.span(), + Slice(n) => n.span(), Ternary(n) => n.span(), Tuple(n) => n.span(), TupleAccess(n) => n.span(), @@ -167,6 +173,7 @@ impl Node for Expression { Locator(n) => n.set_span(span), MemberAccess(n) => n.set_span(span), Repeat(n) => n.set_span(span), + Slice(n) => n.set_span(span), Ternary(n) => n.set_span(span), Tuple(n) => n.set_span(span), TupleAccess(n) => n.set_span(span), @@ -190,6 +197,7 @@ impl Node for Expression { Locator(n) => n.id(), MemberAccess(n) => n.id(), Repeat(n) => n.id(), + Slice(n) => n.id(), Err(n) => n.id(), Intrinsic(n) => n.id(), Ternary(n) => n.id(), @@ -215,6 +223,7 @@ impl Node for Expression { Locator(n) => n.set_id(id), MemberAccess(n) => n.set_id(id), Repeat(n) => n.set_id(id), + Slice(n) => n.set_id(id), Err(n) => n.set_id(id), Intrinsic(n) => n.set_id(id), Ternary(n) => n.set_id(id), @@ -244,6 +253,7 @@ impl fmt::Display for Expression { Locator(n) => n.fmt(f), MemberAccess(n) => n.fmt(f), Repeat(n) => n.fmt(f), + Slice(n) => n.fmt(f), Ternary(n) => n.fmt(f), Tuple(n) => n.fmt(f), TupleAccess(n) => n.fmt(f), @@ -268,8 +278,8 @@ impl Expression { Cast(_) => 12, Ternary(_) => 0, Array(_) | ArrayAccess(_) | Async(_) | Call(_) | Composite(_) | Err(_) | Intrinsic(_) | Path(_) - | Literal(_) | Locator(_) | MemberAccess(_) | Repeat(_) | Tuple(_) | TupleAccess(_) | Unary(_) - | Unit(_) => 20, + | Literal(_) | Locator(_) | MemberAccess(_) | Repeat(_) | Slice(_) | Tuple(_) | TupleAccess(_) + | Unary(_) | Unit(_) => 20, } } @@ -354,6 +364,11 @@ impl Expression { Expression::ArrayAccess(expr) => expr.array.is_pure(get_type) && expr.index.is_pure(get_type), Expression::MemberAccess(expr) => expr.inner.is_pure(get_type), Expression::Repeat(expr) => expr.expr.is_pure(get_type) && expr.count.is_pure(get_type), + Expression::Slice(expr) => { + expr.array.is_pure(get_type) + && expr.start.as_ref().is_none_or(|s| s.is_pure(get_type)) + && expr.end.as_ref().is_none_or(|(_, e)| e.is_pure(get_type)) + } Expression::TupleAccess(expr) => expr.tuple.is_pure(get_type), Expression::Array(expr) => expr.elements.iter().all(|e| e.is_pure(get_type)), Expression::Composite(expr) => { diff --git a/compiler/ast/src/expressions/slice.rs b/compiler/ast/src/expressions/slice.rs new file mode 100644 index 00000000000..ce01184694d --- /dev/null +++ b/compiler/ast/src/expressions/slice.rs @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use super::*; + +/// An array constructed from slicing another. +/// +/// E.g., `arr[2..5]`, `arr[0..=3]`, `arr[1..]`, `arr[..4]` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SliceExpression { + /// The array being sliced. + pub array: Expression, + /// The optional starting index. + pub start: Option, + /// The optional ending index and whether it's inclusive. + pub end: Option<(bool, Expression)>, + /// The span. + pub span: Span, + /// The ID of the node. + pub id: NodeID, +} + +impl fmt::Display for SliceExpression { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Format the start expression. + let start = match &self.start { + Some(expr) => expr.to_string(), + None => "".to_string(), + }; + // Format the end expression. + let end = match &self.end { + Some((true, expr)) => format!("={expr}"), + Some((false, expr)) => expr.to_string(), + None => "".to_string(), + }; + + write!(f, "{}[{start}..{end}]", self.array) + } +} + +impl From for Expression { + fn from(value: SliceExpression) -> Self { + Expression::Slice(Box::new(value)) + } +} + +crate::simple_node_impl!(SliceExpression); diff --git a/compiler/ast/src/interpreter_value/evaluate.rs b/compiler/ast/src/interpreter_value/evaluate.rs index ca106b26e42..cce0038e64e 100644 --- a/compiler/ast/src/interpreter_value/evaluate.rs +++ b/compiler/ast/src/interpreter_value/evaluate.rs @@ -404,6 +404,21 @@ pub fn evaluate_binary( _ => {} } + // Handle array concatenation for Add operation + if matches!(op, BinaryOperation::Add) + && let ( + ValueVariants::Svm(SvmValueParam::Plaintext(Plaintext::Array(x, _))), + ValueVariants::Svm(SvmValueParam::Plaintext(Plaintext::Array(y, _))), + ) = (&lhs.contents, &rhs.contents) + { + let mut elements = x.clone(); + elements.extend(y.clone()); + return Ok(Value::from(ValueVariants::Svm(SvmValueParam::Plaintext(Plaintext::Array( + elements, + Default::default(), + ))))); + } + let ValueVariants::Svm(SvmValueParam::Plaintext(Plaintext::Literal(lhs, ..))) = &lhs.contents else { halt2!(span, "Type error"); }; diff --git a/compiler/ast/src/interpreter_value/value.rs b/compiler/ast/src/interpreter_value/value.rs index 72e91d3409e..abb59cf590b 100644 --- a/compiler/ast/src/interpreter_value/value.rs +++ b/compiler/ast/src/interpreter_value/value.rs @@ -601,6 +601,25 @@ impl Value { Some(array[i].clone().into()) } + pub fn array_slice(&self, start: usize, end_exclusive: Option) -> Option { + let plaintext: &SvmPlaintext = self.try_as_ref()?; + let Plaintext::Array(array, ..) = plaintext else { + return None; + }; + + let range = match end_exclusive { + Some(end) if end <= start || end > array.len() => return None, + Some(end) => start..end, + None if start > array.len() => return None, + None => start..array.len(), + }; + + Some(Value::from(ValueVariants::Svm(SvmValue::Plaintext(SvmPlaintext::Array( + array[range].to_vec(), + Default::default(), + ))))) + } + pub fn array_index_set(&mut self, i: usize, value: Self) -> Option<()> { let plaintext_rhs: SvmPlaintext = value.try_into().ok()?; diff --git a/compiler/ast/src/passes/consumer.rs b/compiler/ast/src/passes/consumer.rs index c28e5d61662..7d09195a70f 100644 --- a/compiler/ast/src/passes/consumer.rs +++ b/compiler/ast/src/passes/consumer.rs @@ -38,6 +38,7 @@ pub trait ExpressionConsumer { Expression::Locator(locator) => self.consume_locator(locator), Expression::MemberAccess(access) => self.consume_member_access(*access), Expression::Repeat(repeat) => self.consume_repeat(*repeat), + Expression::Slice(slice) => self.consume_slice(*slice), Expression::Ternary(ternary) => self.consume_ternary(*ternary), Expression::Tuple(tuple) => self.consume_tuple(tuple), Expression::TupleAccess(access) => self.consume_tuple_access(*access), @@ -77,6 +78,8 @@ pub trait ExpressionConsumer { fn consume_repeat(&mut self, _input: RepeatExpression) -> Self::Output; + fn consume_slice(&mut self, _input: SliceExpression) -> Self::Output; + fn consume_intrinsic(&mut self, _input: IntrinsicExpression) -> Self::Output; fn consume_ternary(&mut self, _input: TernaryExpression) -> Self::Output; diff --git a/compiler/ast/src/passes/reconstructor.rs b/compiler/ast/src/passes/reconstructor.rs index f4925a4883e..7b011e7aada 100644 --- a/compiler/ast/src/passes/reconstructor.rs +++ b/compiler/ast/src/passes/reconstructor.rs @@ -135,6 +135,7 @@ pub trait AstReconstructor { Expression::Locator(locator) => self.reconstruct_locator(locator, additional), Expression::MemberAccess(access) => self.reconstruct_member_access(*access, additional), Expression::Repeat(repeat) => self.reconstruct_repeat(*repeat, additional), + Expression::Slice(slice) => self.reconstruct_slice(*slice, additional), Expression::Ternary(ternary) => self.reconstruct_ternary(*ternary, additional), Expression::Tuple(tuple) => self.reconstruct_tuple(tuple, additional), Expression::TupleAccess(access) => self.reconstruct_tuple_access(*access, additional), @@ -195,6 +196,25 @@ pub trait AstReconstructor { ) } + fn reconstruct_slice( + &mut self, + input: SliceExpression, + _additional: &Self::AdditionalInput, + ) -> (Expression, Self::AdditionalOutput) { + ( + SliceExpression { + array: self.reconstruct_expression(input.array, &Default::default()).0, + start: input.start.map(|start| self.reconstruct_expression(start, &Default::default()).0), + end: input + .end + .map(|(inclusive, end)| (inclusive, self.reconstruct_expression(end, &Default::default()).0)), + ..input + } + .into(), + Default::default(), + ) + } + fn reconstruct_intrinsic( &mut self, input: IntrinsicExpression, diff --git a/compiler/ast/src/passes/visitor.rs b/compiler/ast/src/passes/visitor.rs index 3fc21b6715e..b7df8634bdf 100644 --- a/compiler/ast/src/passes/visitor.rs +++ b/compiler/ast/src/passes/visitor.rs @@ -120,6 +120,7 @@ pub trait AstVisitor { Expression::Locator(locator) => self.visit_locator(locator, additional), Expression::MemberAccess(access) => self.visit_member_access(access, additional), Expression::Repeat(repeat) => self.visit_repeat(repeat, additional), + Expression::Slice(slice) => self.visit_slice(slice, additional), Expression::Ternary(ternary) => self.visit_ternary(ternary, additional), Expression::Tuple(tuple) => self.visit_tuple(tuple, additional), Expression::TupleAccess(access) => self.visit_tuple_access(access, additional), @@ -223,6 +224,17 @@ pub trait AstVisitor { Default::default() } + fn visit_slice(&mut self, input: &SliceExpression, _additional: &Self::AdditionalInput) -> Self::Output { + self.visit_expression(&input.array, &Default::default()); + if let Some(start) = input.start.as_ref() { + self.visit_expression(start, &Default::default()); + } + if let Some((_, end)) = input.end.as_ref() { + self.visit_expression(end, &Default::default()); + } + Default::default() + } + fn visit_ternary(&mut self, input: &TernaryExpression, _additional: &Self::AdditionalInput) -> Self::Output { self.visit_expression(&input.condition, &Default::default()); self.visit_expression(&input.if_true, &Default::default()); diff --git a/compiler/parser-lossless/src/grammar.lalrpop b/compiler/parser-lossless/src/grammar.lalrpop index 342b77d273d..b6082709fec 100644 --- a/compiler/parser-lossless/src/grammar.lalrpop +++ b/compiler/parser-lossless/src/grammar.lalrpop @@ -369,6 +369,28 @@ Expr1: SyntaxNode<'a> = { > > > => { SyntaxNode::new(ExpressionKind::ArrayAccess, [x, l, index, r]) }, + // Slice with both bounds: arr[start..end] or arr[start..=end] + > > > > => { + SyntaxNode::new(ExpressionKind::SliceBoth, [x, l, start, op, end, r]) + }, + > > > > => { + SyntaxNode::new(ExpressionKind::SliceBoth, [x, l, start, op, end, r]) + }, + // Slice with only start: arr[start..] + > > > > => { + SyntaxNode::new(ExpressionKind::SliceFirst, [x, l, start, op, r]) + }, + // Slice with only end: arr[..end] or arr[..=end] + > > > > => { + SyntaxNode::new(ExpressionKind::SliceLast, [x, l, op, end, r]) + }, + > > > > => { + SyntaxNode::new(ExpressionKind::SliceLast, [x, l, op, end, r]) + }, + // Slice with neither: arr[..] (slice entire array) + > > > > => { + SyntaxNode::new(ExpressionKind::SliceNone, [x, l, op, r]) + }, // Member access. > > > => { SyntaxNode::new(ExpressionKind::MemberAccess, [x, d, i]) @@ -906,6 +928,7 @@ extern { Comma => LalrToken { token: Token::Comma, .. }, Dot => LalrToken { token: Token::Dot, .. }, DotDot => LalrToken { token: Token::DotDot, .. }, + DotDotEq => LalrToken { token: Token::DotDotEq, .. }, Semicolon => LalrToken { token: Token::Semicolon, .. }, Colon => LalrToken { token: Token::Colon, .. }, DoubleColon => LalrToken{ token: Token::DoubleColon, .. }, diff --git a/compiler/parser-lossless/src/lib.rs b/compiler/parser-lossless/src/lib.rs index b1aa7104020..f58b0f4b1c5 100644 --- a/compiler/parser-lossless/src/lib.rs +++ b/compiler/parser-lossless/src/lib.rs @@ -162,6 +162,10 @@ pub enum ExpressionKind { MethodCall, Parenthesized, Repeat, + SliceBoth, + SliceFirst, + SliceLast, + SliceNone, Intrinsic, SpecialAccess, // TODO: fold into Intrinsic Struct, diff --git a/compiler/parser-lossless/src/tokens.rs b/compiler/parser-lossless/src/tokens.rs index 615ddd65e25..0bc1539a67f 100644 --- a/compiler/parser-lossless/src/tokens.rs +++ b/compiler/parser-lossless/src/tokens.rs @@ -217,6 +217,8 @@ pub enum Token { Dot, #[token("..")] DotDot, + #[token("..=")] + DotDotEq, #[token(";")] Semicolon, #[token(":")] @@ -414,6 +416,7 @@ impl Token { "Comma" => "','", "Dot" => "'.'", "DotDot" => "'..'", + "DotDotEq" => "'..='", "Semicolon" => "';'", "Colon" => "':'", "DoubleColon" => "'::'", diff --git a/compiler/parser/src/conversions.rs b/compiler/parser/src/conversions.rs index ac0cfd70645..1189d0beb96 100644 --- a/compiler/parser/src/conversions.rs +++ b/compiler/parser/src/conversions.rs @@ -448,6 +448,60 @@ impl<'a> ConversionContext<'a> { leo_ast::ArrayAccess { array, index, span, id }.into() } + ExpressionKind::SliceBoth => { + // arr[start..end] or arr[start..=end] + let [array, _left, start, op, end, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + let array = self.to_expression(array)?; + let start = Some(self.to_expression(start)?); + let inclusive = op.text == "..="; + let end_expr = self.to_expression(end)?; + let end = Some((inclusive, end_expr)); + + leo_ast::SliceExpression { array, start, end, span, id }.into() + } + + ExpressionKind::SliceFirst => { + // arr[start..] + let [array, _left, start, _op, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + let array = self.to_expression(array)?; + let start = Some(self.to_expression(start)?); + let end = None; + + leo_ast::SliceExpression { array, start, end, span, id }.into() + } + + ExpressionKind::SliceLast => { + // arr[..end] or arr[..=end] + let [array, _left, op, end, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + let array = self.to_expression(array)?; + let start = None; + let inclusive = op.text == "..="; + let end_expr = self.to_expression(end)?; + let end = Some((inclusive, end_expr)); + + leo_ast::SliceExpression { array, start, end, span, id }.into() + } + + ExpressionKind::SliceNone => { + // arr[..] + let [array, _left, _op, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + let array = self.to_expression(array)?; + + leo_ast::SliceExpression { array, start: None, end: None, span, id }.into() + } + ExpressionKind::Intrinsic => { let name = Symbol::intern(node.children[0].text); diff --git a/compiler/passes/src/code_generation/expression.rs b/compiler/passes/src/code_generation/expression.rs index 1d93a32743a..36820009302 100644 --- a/compiler/passes/src/code_generation/expression.rs +++ b/compiler/passes/src/code_generation/expression.rs @@ -40,6 +40,7 @@ use leo_ast::{ Path, ProgramId, RepeatExpression, + SliceExpression, TernaryExpression, TupleExpression, Type, @@ -80,6 +81,7 @@ impl CodeGeneratingVisitor<'_> { Expression::Cast(expr) => some_expr(self.visit_cast(expr)), Expression::Composite(expr) => some_expr(self.visit_composite_init(expr)), Expression::Repeat(expr) => some_expr(self.visit_repeat(expr)), + Expression::Slice(expr) => some_expr(self.visit_slice(expr)), Expression::Ternary(expr) => some_expr(self.visit_ternary(expr)), Expression::Tuple(expr) => some_expr(self.visit_tuple(expr)), Expression::Unary(expr) => some_expr(self.visit_unary(expr)), @@ -189,6 +191,21 @@ impl CodeGeneratingVisitor<'_> { } fn visit_binary(&mut self, input: &BinaryExpression) -> (AleoExpr, Vec) { + // Array concatenation should be resolved during const propagation. + // If we reach code generation with an Add on arrays, something went wrong. + if matches!(input.op, BinaryOperation::Add) { + let left_type = self.state.type_table.get(&input.left.id()); + let right_type = self.state.type_table.get(&input.right.id()); + assert!( + !matches!(left_type, Some(Type::Array(_))), + "Array concatenation should be resolved before code generation" + ); + assert!( + !matches!(right_type, Some(Type::Array(_))), + "Array concatenation should be resolved before code generation" + ); + } + let (left, left_instructions) = self.visit_expression(&input.left); let (right, right_instructions) = self.visit_expression(&input.right); let left = left.expect("Trying to operate on an empty expression"); @@ -428,6 +445,41 @@ impl CodeGeneratingVisitor<'_> { (AleoExpr::Reg(dest_reg), operand_instructions) } + fn visit_slice(&mut self, input: &SliceExpression) -> (AleoExpr, Vec) { + let (array_operand, mut operand_instructions) = self.visit_expression(&input.array); + let array_operand = array_operand.expect("Trying to slice an empty expression"); + + // Get the start index - should be a literal by now (resolved during const propagation). + let start = input.start.as_ref().and_then(|e| e.as_u32()).unwrap_or(0) as usize; + + // Get the result array type. The type checker has already computed the correct length. + let Some(Type::Array(result_array_type)) = self.state.type_table.get(&input.id) else { + panic!("All types should be known at this phase of compilation"); + }; + let result_len = + result_array_type.length.as_u32().expect("slice length should be known at this point") as usize; + + // Compute the end index from start + result_len. + let end = start + result_len; + + // Construct the expression operands by indexing into the array. + let expression_operands = (start..end) + .map(|i| AleoExpr::ArrayAccess(Box::new(array_operand.clone()), Box::new(AleoExpr::U32(i as u32)))) + .collect::>(); + + // Construct the destination register. + let dest_reg = self.next_register(); + + let array_type = Self::visit_type(&Type::Array(result_array_type.clone())); + + let array_instruction = AleoStmt::Cast(AleoExpr::Tuple(expression_operands), dest_reg.clone(), array_type); + + // Concatenate the instructions. + operand_instructions.push(array_instruction); + + (AleoExpr::Reg(dest_reg), operand_instructions) + } + fn visit_intrinsic(&mut self, input: &IntrinsicExpression) -> (Option, Vec) { let mut stmts = vec![]; diff --git a/compiler/passes/src/common/replacer/mod.rs b/compiler/passes/src/common/replacer/mod.rs index 428c97f7117..b7ef10361dd 100644 --- a/compiler/passes/src/common/replacer/mod.rs +++ b/compiler/passes/src/common/replacer/mod.rs @@ -81,6 +81,7 @@ where Expression::Locator(locator) => self.reconstruct_locator(locator, &()), Expression::MemberAccess(access) => self.reconstruct_member_access(*access, &()), Expression::Repeat(repeat) => self.reconstruct_repeat(*repeat, &()), + Expression::Slice(slice) => self.reconstruct_slice(*slice, &()), Expression::Ternary(ternary) => self.reconstruct_ternary(*ternary, &()), Expression::Tuple(tuple) => self.reconstruct_tuple(tuple, &()), Expression::TupleAccess(access) => self.reconstruct_tuple_access(*access, &()), diff --git a/compiler/passes/src/common_subexpression_elimination/visitor.rs b/compiler/passes/src/common_subexpression_elimination/visitor.rs index d8b8a5cc2fc..4a6c450b1b6 100644 --- a/compiler/passes/src/common_subexpression_elimination/visitor.rs +++ b/compiler/passes/src/common_subexpression_elimination/visitor.rs @@ -116,6 +116,7 @@ impl CommonSubexpressionEliminatingVisitor<'_> { | Expression::Locator(_) | Expression::MemberAccess(_) | Expression::Repeat(_) + | Expression::Slice(_) | Expression::Composite(_) | Expression::Ternary(_) | Expression::Tuple(_) @@ -180,6 +181,16 @@ impl CommonSubexpressionEliminatingVisitor<'_> { let count = self.try_atom(&mut repeat_expression.count)?; Expr::Repeat { value, count } } + Expression::Slice(slice_expression) => { + self.try_atom(&mut slice_expression.array)?; + if let Some(start) = &mut slice_expression.start { + self.try_atom(start)?; + } + if let Some((_, end)) = &mut slice_expression.end { + self.try_atom(end)?; + } + return Some((expression, false)); + } Expression::Ternary(ternary_expression) => { let condition = self.try_atom(&mut ternary_expression.condition)?; let if_true = self.try_atom(&mut ternary_expression.if_true)?; diff --git a/compiler/passes/src/const_propagation/ast.rs b/compiler/passes/src/const_propagation/ast.rs index 1e49be71a13..b51770ac160 100644 --- a/compiler/passes/src/const_propagation/ast.rs +++ b/compiler/passes/src/const_propagation/ast.rs @@ -65,6 +65,7 @@ impl AstReconstructor for ConstPropagationVisitor<'_> { Expression::Locator(locator) => self.reconstruct_locator(locator, &()), Expression::MemberAccess(access) => self.reconstruct_member_access(*access, &()), Expression::Repeat(repeat) => self.reconstruct_repeat(*repeat, &()), + Expression::Slice(slice) => self.reconstruct_slice(*slice, &()), Expression::Ternary(ternary) => self.reconstruct_ternary(*ternary, &()), Expression::Tuple(tuple) => self.reconstruct_tuple(tuple, &()), Expression::TupleAccess(access) => self.reconstruct_tuple_access(*access, &()), @@ -313,21 +314,85 @@ impl AstReconstructor for ConstPropagationVisitor<'_> { let (left, lhs_opt_value) = self.reconstruct_expression(input.left, &()); let (right, rhs_opt_value) = self.reconstruct_expression(input.right, &()); - if let (Some(lhs_value), Some(rhs_value)) = (lhs_opt_value, rhs_opt_value) { + if let (Some(lhs_value), Some(rhs_value)) = (&lhs_opt_value, &rhs_opt_value) { // We were able to evaluate both operands, so we can evaluate this expression. match interpreter_value::evaluate_binary( span, input.op, - &lhs_value, - &rhs_value, + lhs_value, + rhs_value, &self.state.type_table.get(&input_id), ) { Ok(new_value) => { let new_expr = self.value_to_expression(&new_value, span, input_id).expect(VALUE_ERROR); return (new_expr, Some(new_value)); } - Err(err) => self - .emit_err(StaticAnalyzerError::compile_time_binary_op(lhs_value, rhs_value, input.op, err, span)), + Err(err) => self.emit_err(StaticAnalyzerError::compile_time_binary_op( + lhs_value.clone(), + rhs_value.clone(), + input.op, + err, + span, + )), + } + } + + // Handle array concatenation when values aren't known but types are arrays + if matches!(input.op, BinaryOperation::Add) { + let left_type = self.state.type_table.get(&left.id()); + let right_type = self.state.type_table.get(&right.id()); + + if let (Some(Type::Array(left_arr)), Some(Type::Array(right_arr))) = (left_type, right_type) { + // Get array lengths - either from the expression itself (if ArrayExpression) + // or from the type (if literal length) + let left_len = match &left { + Expression::Array(arr) => Some(arr.elements.len() as u32), + _ => left_arr.length.as_u32(), + }; + let right_len = match &right { + Expression::Array(arr) => Some(arr.elements.len() as u32), + _ => right_arr.length.as_u32(), + }; + + if let (Some(left_len), Some(right_len)) = (left_len, right_len) { + // Expand a + b to [a[0], a[1], ..., b[0], b[1], ...] + let mut elements = Vec::with_capacity((left_len + right_len) as usize); + + // Helper to add elements from an array operand + let mut add_elements = |arr: &Expression, arr_type: &ArrayType, len: u32| { + // If the operand is already an ArrayExpression, use its elements directly + if let Expression::Array(array_expr) = arr { + for elem in &array_expr.elements { + elements.push(elem.clone()); + } + } else { + // Otherwise, create array accesses + for i in 0..len { + let index = Expression::Literal(Literal::integer( + IntegerType::U32, + i.to_string(), + span, + self.state.node_builder.next_id(), + )); + let access = ArrayAccess { + array: arr.clone(), + index, + span, + id: self.state.node_builder.next_id(), + }; + // Register the element type in the type table + self.state.type_table.insert(access.id, arr_type.element_type().clone()); + elements.push(Expression::ArrayAccess(Box::new(access))); + } + } + }; + + add_elements(&left, &left_arr, left_len); + add_elements(&right, &right_arr, right_len); + + let array_expr = ArrayExpression { elements, span, id: input_id }; + return (Expression::Array(array_expr), None); + } } } diff --git a/compiler/passes/src/monomorphization/ast.rs b/compiler/passes/src/monomorphization/ast.rs index 08b1bde332c..ffdb5984679 100644 --- a/compiler/passes/src/monomorphization/ast.rs +++ b/compiler/passes/src/monomorphization/ast.rs @@ -103,6 +103,7 @@ impl AstReconstructor for MonomorphizationVisitor<'_> { Expression::Locator(locator) => self.reconstruct_locator(locator, &()), Expression::MemberAccess(access) => self.reconstruct_member_access(*access, &()), Expression::Repeat(repeat) => self.reconstruct_repeat(*repeat, &()), + Expression::Slice(slice) => self.reconstruct_slice(*slice, &()), Expression::Ternary(ternary) => self.reconstruct_ternary(*ternary, &()), Expression::Tuple(tuple) => self.reconstruct_tuple(tuple, &()), Expression::TupleAccess(access) => self.reconstruct_tuple_access(*access, &()), diff --git a/compiler/passes/src/option_lowering/ast.rs b/compiler/passes/src/option_lowering/ast.rs index cf6f7750786..c52bd13bdf4 100644 --- a/compiler/passes/src/option_lowering/ast.rs +++ b/compiler/passes/src/option_lowering/ast.rs @@ -100,6 +100,7 @@ impl leo_ast::AstReconstructor for OptionLoweringVisitor<'_> { Expression::Locator(e) => self.reconstruct_locator(e, additional), Expression::MemberAccess(e) => self.reconstruct_member_access(*e, additional), Expression::Repeat(e) => self.reconstruct_repeat(*e, additional), + Expression::Slice(e) => self.reconstruct_slice(*e, additional), Expression::Ternary(e) => self.reconstruct_ternary(*e, additional), Expression::Tuple(e) => self.reconstruct_tuple(e, additional), Expression::TupleAccess(e) => self.reconstruct_tuple_access(*e, additional), diff --git a/compiler/passes/src/static_analysis/future_checker.rs b/compiler/passes/src/static_analysis/future_checker.rs index 577362be962..089056985a1 100644 --- a/compiler/passes/src/static_analysis/future_checker.rs +++ b/compiler/passes/src/static_analysis/future_checker.rs @@ -103,6 +103,7 @@ impl AstVisitor for FutureChecker<'_> { Expression::Locator(locator) => self.visit_locator(locator, &Position::Misc), Expression::MemberAccess(access) => self.visit_member_access(access, &Position::Misc), Expression::Repeat(repeat) => self.visit_repeat(repeat, &Position::Misc), + Expression::Slice(slice) => self.visit_slice(slice, &Position::Misc), Expression::Ternary(ternary) => self.visit_ternary(ternary, &Position::Misc), Expression::Tuple(tuple) => self.visit_tuple(tuple, additional), Expression::TupleAccess(access) => self.visit_tuple_access(access, &Position::Misc), diff --git a/compiler/passes/src/static_single_assignment/expression.rs b/compiler/passes/src/static_single_assignment/expression.rs index 21963aab321..8792ce47ecc 100644 --- a/compiler/passes/src/static_single_assignment/expression.rs +++ b/compiler/passes/src/static_single_assignment/expression.rs @@ -34,6 +34,7 @@ use leo_ast::{ MemberAccess, Path, RepeatExpression, + SliceExpression, Statement, TernaryExpression, TupleAccess, @@ -234,6 +235,13 @@ impl ExpressionConsumer for SsaFormingVisitor<'_> { (RepeatExpression { expr, ..input }.into(), statements) } + fn consume_slice(&mut self, input: SliceExpression) -> Self::Output { + let (array, statements) = self.consume_expression_and_define(input.array); + + // By now, the start and end should be literals. So we just ignore them. There is no need to SSA them. + (SliceExpression { array, ..input }.into(), statements) + } + /// Consumes a ternary expression, accumulating any statements that are generated. fn consume_ternary(&mut self, input: TernaryExpression) -> Self::Output { // Reconstruct the condition of the ternary expression. diff --git a/compiler/passes/src/type_checking/ast.rs b/compiler/passes/src/type_checking/ast.rs index de796e42b8e..a24dbe7a5d5 100644 --- a/compiler/passes/src/type_checking/ast.rs +++ b/compiler/passes/src/type_checking/ast.rs @@ -377,6 +377,7 @@ impl AstVisitor for TypeCheckingVisitor<'_> { Expression::Locator(locator) => self.visit_locator(locator, additional), Expression::MemberAccess(access) => self.visit_member_access_general(access, false, additional), Expression::Repeat(repeat) => self.visit_repeat(repeat, additional), + Expression::Slice(slice) => self.visit_slice(slice, additional), Expression::Ternary(ternary) => self.visit_ternary(ternary, additional), Expression::Tuple(tuple) => self.visit_tuple(tuple, additional), Expression::TupleAccess(access) => self.visit_tuple_access_general(access, false, additional), @@ -497,6 +498,87 @@ impl AstVisitor for TypeCheckingVisitor<'_> { type_ } + fn visit_slice(&mut self, input: &SliceExpression, additional: &Self::AdditionalInput) -> Self::Output { + // Type check the array expression. + let array_type = self.visit_expression(&input.array, &None); + + // Make sure the array type is actually an array. + let element_type = match &array_type { + Type::Array(array_ty) => array_ty.element_type().clone(), + Type::Err => return Type::Err, + _ => { + self.emit_err(TypeCheckerError::expected_array_type(array_type.clone(), input.array.span())); + return Type::Err; + } + }; + + // Type check the start expression if present. + if let Some(start) = &input.start { + self.visit_expression_infer_default_u32(start); + } + + // Type check the end expression if present. + if let Some((_, end)) = &input.end { + self.visit_expression_infer_default_u32(end); + } + + // Get the start index (default 0). + let start = input.start.as_ref().and_then(|e| e.as_u32()).unwrap_or(0); + + // Get the original array length if known. + let array_len = if let Type::Array(array_ty) = &array_type { array_ty.length.as_u32() } else { None }; + + // Compute the slice length. + let slice_len = match (&input.end, array_len) { + (Some((inclusive, end_expr)), _) => { + if let Some(end) = end_expr.as_u32() { + let end = if *inclusive { end + 1 } else { end }; + if end < start { + self.emit_err(TypeCheckerError::slice_range_invalid(start, end, input.span())); + return Type::Err; + } + Some(end - start) + } else { + None + } + } + (None, Some(arr_len)) => { + // Slice to end of array. + if start > arr_len { + self.emit_err(TypeCheckerError::slice_range_invalid(start, arr_len, input.span())); + return Type::Err; + } + Some(arr_len - start) + } + (None, None) => None, + }; + + // If slice length is known, check bounds. + if let (Some(slice_len), Some(arr_len)) = (slice_len, array_len) + && start + slice_len > arr_len + { + self.emit_err(TypeCheckerError::slice_out_of_bounds(start, start + slice_len, arr_len, input.span())); + } + + // Build the result type. + let result_type = if let Some(len) = slice_len { + let len_expr = Expression::Literal(Literal::integer( + IntegerType::U32, + len.to_string(), + input.span(), + self.state.node_builder.next_id(), + )); + Type::Array(ArrayType::new(element_type, len_expr)) + } else { + // If we can't determine the length, emit an error. + self.emit_err(TypeCheckerError::slice_length_unknown(input.span())); + return Type::Err; + }; + + self.maybe_assert_type(&result_type, additional, input.span()); + result_type + } + fn visit_intrinsic(&mut self, input: &IntrinsicExpression, expected: &Self::AdditionalInput) -> Self::Output { // Check core struct name and function. let Some(intrinsic) = self.get_intrinsic(input) else { @@ -648,12 +730,39 @@ impl AstVisitor for TypeCheckingVisitor<'_> { result_t } BinaryOperation::Add => { - let operand_expected = self.unwrap_optional_type(destination); + let unwrapped_dest = self.unwrap_optional_type(destination); + + // For array concatenation, don't pass destination type as expected (operands have different type than result) + // First, probe if operands are arrays by visiting without expected type + let is_array_dest = matches!(unwrapped_dest, Some(Type::Array(_))); + let operand_expected = if is_array_dest { None } else { unwrapped_dest.clone() }; - // The expected type for both `left` and `right` is the unwrapped type let mut t1 = self.visit_expression(&input.left, &operand_expected); let mut t2 = self.visit_expression(&input.right, &operand_expected); + // Handle array concatenation + if let (Type::Array(arr1), Type::Array(arr2)) = (&t1, &t2) { + if arr1.element_type() != arr2.element_type() { + self.emit_err(TypeCheckerError::array_concat_element_mismatch( + arr1.element_type(), + arr2.element_type(), + input.span(), + )); + return Type::Err; + } + // Result type: [ElementType; len1 + len2] + let result_length = Expression::Binary(Box::new(BinaryExpression { + left: (*arr1.length).clone(), + op: BinaryOperation::Add, + right: (*arr2.length).clone(), + id: self.state.node_builder.next_id(), + span: Default::default(), + })); + let result_t = Type::Array(ArrayType::new(arr1.element_type().clone(), result_length)); + self.maybe_assert_type(&result_t, destination, input.span()); + return result_t; + } + // Infer `Numeric` types if possible infer_numeric_types(self, &mut t1, &mut t2); @@ -662,7 +771,7 @@ impl AstVisitor for TypeCheckingVisitor<'_> { if !matches!(type_, Type::Err | Type::Field | Type::Group | Type::Scalar | Type::Integer(_)) { self.emit_err(TypeCheckerError::type_should_be2( type_, - "a field, group, scalar, or integer", + "a field, group, scalar, integer, or array", span, )); } diff --git a/errors/src/errors/type_checker/type_checker_error.rs b/errors/src/errors/type_checker/type_checker_error.rs index 5a355594d0f..4641cbb7e6a 100644 --- a/errors/src/errors/type_checker/type_checker_error.rs +++ b/errors/src/errors/type_checker/type_checker_error.rs @@ -1384,4 +1384,54 @@ create_messages!( help: None, } + /// For when slicing is applied to a non-array type. + @formatted + expected_array_type { + args: (found: impl Display), + msg: format!( + "Expected an array type, found `{found}`.", + ), + help: Some("Slice expressions can only be applied to arrays.".to_string()), + } + + /// For when a slice range is invalid (e.g., start > end). + @formatted + slice_range_invalid { + args: (start: impl Display, end: impl Display), + msg: format!( + "Invalid slice range: start index `{start}` is greater than end index `{end}`.", + ), + help: None, + } + + /// For when a slice is out of bounds. + @formatted + slice_out_of_bounds { + args: (start: impl Display, end: impl Display, length: impl Display), + msg: format!( + "Slice range [{start}..{end}] is out of bounds for array of length `{length}`.", + ), + help: None, + } + + /// For when a slice length cannot be determined. + @formatted + slice_length_unknown { + args: (), + msg: format!( + "Cannot determine the length of the slice. Slice bounds must be compile-time constants.", + ), + help: Some("Use literal values or const expressions for slice bounds.".to_string()), + } + + /// For when array concatenation element types do not match. + @formatted + array_concat_element_mismatch { + args: (left: impl Display, right: impl Display), + msg: format!( + "Cannot concatenate arrays with different element types: `{left}` and `{right}`.", + ), + help: Some("Both arrays must have the same element type for concatenation.".to_string()), + } + ); diff --git a/interpreter/src/cursor.rs b/interpreter/src/cursor.rs index f84c5067eb1..e5de996dda6 100644 --- a/interpreter/src/cursor.rs +++ b/interpreter/src/cursor.rs @@ -447,6 +447,7 @@ impl Cursor { | Expression::Literal(..) | Expression::Locator(..) | Expression::Repeat(..) + | Expression::Slice(..) | Expression::Composite(..) | Expression::Ternary(..) | Expression::Tuple(..) @@ -1008,6 +1009,44 @@ impl Cursor { count_resolved.as_u32().expect_tc(repeat.span())? as usize, ))) } + Expression::Slice(slice) if step == 0 => { + // Push expressions for start, end, and array + if let Some(start) = &slice.start { + push!()(start, &None); + } + if let Some((_, end)) = &slice.end { + push!()(end, &None); + } + push!()(&slice.array, &None); + None + } + Expression::Slice(slice) if step == 1 => { + let array = self.pop_value()?; + let end = if slice.end.is_some() { + let end_val = self.pop_value()?; + let end_resolved = + end_val.resolve_if_unsuffixed(&Some(Type::Integer(leo_ast::IntegerType::U32)), slice.span())?; + let end_u32 = end_resolved.as_u32().expect_tc(slice.span())?; + if slice.end.as_ref().unwrap().0 { + // Inclusive: add 1 + Some(end_u32 + 1) + } else { + Some(end_u32) + } + } else { + None + }; + let start = if slice.start.is_some() { + let start_val = self.pop_value()?; + let start_resolved = start_val + .resolve_if_unsuffixed(&Some(Type::Integer(leo_ast::IntegerType::U32)), slice.span())?; + start_resolved.as_u32().expect_tc(slice.span())? as usize + } else { + 0 + }; + let end = end.map(|e| e as usize); + Some(array.array_slice(start, end).expect_tc(slice.span())?) + } Expression::Intrinsic(intr) if step == 0 => { let intrinsic = if intr.name == Symbol::intern("__unresolved_get") { Intrinsic::MappingGet diff --git a/tests/expectations/compiler/array/array_concat.out b/tests/expectations/compiler/array/array_concat.out new file mode 100644 index 00000000000..a329f6cf5ba --- /dev/null +++ b/tests/expectations/compiler/array/array_concat.out @@ -0,0 +1,19 @@ +program test.aleo; + +function foo: + input r0 as [u32; 2u32].private; + input r1 as [u32; 2u32].private; + cast r0[0u32] r0[1u32] r1[0u32] r1[1u32] into r2 as [u32; 4u32]; + output r2 as [u32; 4u32].private; + +function flatten: + input r0 as [[boolean; 2u32]; 2u32].private; + cast r0[0u32][0u32] r0[0u32][1u32] r0[1u32][0u32] r0[1u32][1u32] into r1 as [boolean; 4u32]; + assert.eq r1 r1; + output r1 as [boolean; 4u32].private; + +function concat_different_sizes: + input r0 as [u8; 3u32].private; + input r1 as [u8; 2u32].private; + cast r0[0u32] r0[1u32] r0[2u32] r1[0u32] r1[1u32] into r2 as [u8; 5u32]; + output r2 as [u8; 5u32].private; diff --git a/tests/expectations/compiler/array/array_concat_chained.out b/tests/expectations/compiler/array/array_concat_chained.out new file mode 100644 index 00000000000..cc70e18c6a0 --- /dev/null +++ b/tests/expectations/compiler/array/array_concat_chained.out @@ -0,0 +1,8 @@ +program test.aleo; + +function foo: + input r0 as [u32; 2u32].private; + input r1 as [u32; 2u32].private; + input r2 as [u32; 2u32].private; + cast r0[0u32] r0[1u32] r1[0u32] r1[1u32] r2[0u32] r2[1u32] into r3 as [u32; 6u32]; + output r3 as [u32; 6u32].private; diff --git a/tests/expectations/compiler/array/array_concat_type_mismatch_fail.out b/tests/expectations/compiler/array/array_concat_type_mismatch_fail.out new file mode 100644 index 00000000000..83f1b9c0966 --- /dev/null +++ b/tests/expectations/compiler/array/array_concat_type_mismatch_fail.out @@ -0,0 +1,7 @@ +Error [ETYC0372177]: Cannot concatenate arrays with different element types: `u32` and `u8`. + --> compiler-test:5:16 + | + 5 | return a + b; + | ^^^^^ + | + = Both arrays must have the same element type for concatenation. diff --git a/tests/expectations/compiler/array/array_concat_with_slice.out b/tests/expectations/compiler/array/array_concat_with_slice.out new file mode 100644 index 00000000000..fdc990ac495 --- /dev/null +++ b/tests/expectations/compiler/array/array_concat_with_slice.out @@ -0,0 +1,11 @@ +program test.aleo; + +function foo: + input r0 as [u32; 4u32].private; + input r1 as [u32; 4u32].private; + cast r0[0u32] r0[1u32] into r2 as [u32; 2u32]; + cast r0[0u32] r0[1u32] into r3 as [u32; 2u32]; + cast r1[2u32] r1[3u32] into r4 as [u32; 2u32]; + cast r1[2u32] r1[3u32] into r5 as [u32; 2u32]; + cast r2[0u32] r3[1u32] r4[0u32] r5[1u32] into r6 as [u32; 4u32]; + output r6 as [u32; 4u32].private; diff --git a/tests/expectations/compiler/operations/add_type_fail.out b/tests/expectations/compiler/operations/add_type_fail.out index df7bf309170..557555ab438 100644 --- a/tests/expectations/compiler/operations/add_type_fail.out +++ b/tests/expectations/compiler/operations/add_type_fail.out @@ -1,9 +1,9 @@ -Error [ETYC0372117]: Expected a field, group, scalar, or integer but type `bool` was found. +Error [ETYC0372117]: Expected a field, group, scalar, integer, or array but type `bool` was found. --> compiler-test:4:17 | 4 | return (true + false) as u8; | ^^^^ -Error [ETYC0372117]: Expected a field, group, scalar, or integer but type `bool` was found. +Error [ETYC0372117]: Expected a field, group, scalar, integer, or array but type `bool` was found. --> compiler-test:4:24 | 4 | return (true + false) as u8; diff --git a/tests/expectations/compiler/slice/slice_basic.out b/tests/expectations/compiler/slice/slice_basic.out new file mode 100644 index 00000000000..edb58815c87 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_basic.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[2u32] r0[3u32] r0[4u32] into r1 as [u32; 3u32]; + output r1 as [u32; 3u32].private; diff --git a/tests/expectations/compiler/slice/slice_from_start.out b/tests/expectations/compiler/slice/slice_from_start.out new file mode 100644 index 00000000000..bd62782c149 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_from_start.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[0u32] r0[1u32] r0[2u32] r0[3u32] into r1 as [u32; 4u32]; + output r1 as [u32; 4u32].private; diff --git a/tests/expectations/compiler/slice/slice_full.out b/tests/expectations/compiler/slice/slice_full.out new file mode 100644 index 00000000000..1f96da9d022 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_full.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 4u32].private; + cast r0[0u32] r0[1u32] r0[2u32] r0[3u32] into r1 as [u32; 4u32]; + output r1 as [u32; 4u32].private; diff --git a/tests/expectations/compiler/slice/slice_inclusive.out b/tests/expectations/compiler/slice/slice_inclusive.out new file mode 100644 index 00000000000..46a02936a53 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_inclusive.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[2u32] r0[3u32] r0[4u32] r0[5u32] into r1 as [u32; 4u32]; + output r1 as [u32; 4u32].private; diff --git a/tests/expectations/compiler/slice/slice_invalid_range_fail.out b/tests/expectations/compiler/slice/slice_invalid_range_fail.out new file mode 100644 index 00000000000..389076e877c --- /dev/null +++ b/tests/expectations/compiler/slice/slice_invalid_range_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372174]: Invalid slice range: start index `5` is greater than end index `3`. + --> compiler-test:5:16 + | + 5 | return a[5..3]; + | ^^^^^^^ diff --git a/tests/expectations/compiler/slice/slice_out_of_bounds_fail.out b/tests/expectations/compiler/slice/slice_out_of_bounds_fail.out new file mode 100644 index 00000000000..c7926e144e1 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_out_of_bounds_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372175]: Slice range [5..10] is out of bounds for array of length `8`. + --> compiler-test:5:16 + | + 5 | return a[5..10]; + | ^^^^^^^^ diff --git a/tests/expectations/compiler/slice/slice_to_end.out b/tests/expectations/compiler/slice/slice_to_end.out new file mode 100644 index 00000000000..49e2285a791 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_to_end.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[3u32] r0[4u32] r0[5u32] r0[6u32] r0[7u32] into r1 as [u32; 5u32]; + output r1 as [u32; 5u32].private; diff --git a/tests/expectations/compiler/slice/slice_with_variables.out b/tests/expectations/compiler/slice/slice_with_variables.out new file mode 100644 index 00000000000..1f41f1c4269 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_with_variables.out @@ -0,0 +1,7 @@ +Error [ETYC0372176]: Cannot determine the length of the slice. Slice bounds must be compile-time constants. + --> compiler-test:8:16 + | + 8 | return a[START..END]; + | ^^^^^^^^^^^^^ + | + = Use literal values or const expressions for slice bounds. diff --git a/tests/expectations/compiler/statements/all_loops_fail.out b/tests/expectations/compiler/statements/all_loops_fail.out index 721e1247a85..9def44c4cd4 100644 --- a/tests/expectations/compiler/statements/all_loops_fail.out +++ b/tests/expectations/compiler/statements/all_loops_fail.out @@ -1,5 +1,5 @@ -Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', 'true', 'false', 'async', 'block', 'network', 'self' -- found '=' - --> compiler-test:15:30 +Error [EPAR0370005]: expected '&&', '||', '&', '|', '^', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '**', '%', '<<', '>>', '[', '.', '..', '?', 'as' -- found '..=' + --> compiler-test:15:28 | 15 | for a: u32 in 10u32..=0u32 { - | ^ + | ^^^ diff --git a/tests/expectations/compiler/type_inference/unsuffixed_literals_fail.out b/tests/expectations/compiler/type_inference/unsuffixed_literals_fail.out index e86bc0459a2..b5b2cc5212b 100644 --- a/tests/expectations/compiler/type_inference/unsuffixed_literals_fail.out +++ b/tests/expectations/compiler/type_inference/unsuffixed_literals_fail.out @@ -75,7 +75,7 @@ Error [ETYC0372004]: Could not determine the type of `1` | ^ | = Consider using explicit type annotations. -Error [ETYC0372117]: Expected a field, group, scalar, or integer but type `bool` was found. +Error [ETYC0372117]: Expected a field, group, scalar, integer, or array but type `bool` was found. --> compiler-test:13:22 | 13 | let c0 = 1 + true; diff --git a/tests/expectations/parser-statement/statement/expression_fail.out b/tests/expectations/parser-statement/statement/expression_fail.out index e115aeb6f80..fbc6cac515d 100644 --- a/tests/expectations/parser-statement/statement/expression_fail.out +++ b/tests/expectations/parser-statement/statement/expression_fail.out @@ -19,7 +19,7 @@ Error [EPAR0370005]: expected an identifier, a program id, an address literal, a | 1 | (x,y|; | ^ -Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', 'true', 'false', 'async', 'block', 'network', 'self' -- found '}' +Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', '..', '..=', 'true', 'false', 'async', 'block', 'network', 'self' -- found '}' --> test_4:1:3 | 1 | x[}; diff --git a/tests/expectations/parser-statement/unreachable/eat_int.out b/tests/expectations/parser-statement/unreachable/eat_int.out index 2418346df2d..c135e5005df 100644 --- a/tests/expectations/parser-statement/unreachable/eat_int.out +++ b/tests/expectations/parser-statement/unreachable/eat_int.out @@ -133,7 +133,7 @@ Error [EPAR0370003]: unexpected EOF | 1 | x.0_> | ^ -Error [EPAR0370005]: expected '=', '&&', '||', '&&=', '||=', '&', '&=', '|', '|=', '^', '&=', '==', '!=', '<', '<=', '>', '>=', '+', '+=', '-', '-=', '*', '*=', '/', '/=', '**', '**=', '%', '%=', '<<', '<<=', '>>', '>>=', '[', '.', ';', '?', 'as' -- found '..' +Error [EPAR0370005]: expected '=', '&&=', '||=', '&=', '|=', '&=', '+=', '-=', '*=', '/=', '**=', '%=', '<<=', '>>=', ';' -- found '..' --> test_30:1:5 | 1 | x.0_.. diff --git a/tests/expectations/parser-statement/unreachable/math_op_fail.out b/tests/expectations/parser-statement/unreachable/math_op_fail.out index 3b682687524..350ae446b6a 100644 --- a/tests/expectations/parser-statement/unreachable/math_op_fail.out +++ b/tests/expectations/parser-statement/unreachable/math_op_fail.out @@ -13,7 +13,7 @@ Error [EPAR0370005]: expected ';' -- found ',' | 1 | let x = a , b; | ^ -Error [EPAR0370005]: expected ']' -- found ';' +Error [EPAR0370005]: expected ']', '..', '..=' -- found ';' --> test_3:1:14 | 1 | let x = a [ b; @@ -68,7 +68,7 @@ Error [EPAR0370005]: expected '&&', '||', '&', '|', '^', '==', '!=', '<', '<=', | 1 | let x = a ! b; | ^ -Error [EPAR0370005]: expected '&&', '||', '&', '|', '^', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '**', '%', '<<', '>>', '[', '.', ';', '?', 'as' -- found '..' +Error [EPAR0370005]: expected ';' -- found '..' --> test_14:1:11 | 1 | let x = a .. b; @@ -193,7 +193,7 @@ Error [EPAR0370005]: expected '=', '&&=', '||=', '&=', '|=', '&=', '+=', '-=', ' | 1 | x,=b; // 43 | ^ -Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', 'true', 'false', 'async', 'block', 'network', 'self' -- found '=' +Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', '..', '..=', 'true', 'false', 'async', 'block', 'network', 'self' -- found '=' --> test_40:1:3 | 1 | x[=b; @@ -253,8 +253,8 @@ Error [EPAR0370005]: expected an identifier, a program id, an address literal, a | 1 | x<==b; | ^ -Error [EPAR0370005]: expected '=', '&&', '||', '&&=', '||=', '&', '&=', '|', '|=', '^', '&=', '==', '!=', '<', '<=', '>', '>=', '+', '+=', '-', '-=', '*', '*=', '/', '/=', '**', '**=', '%', '%=', '<<', '<<=', '>>', '>>=', '[', '.', ';', '?', 'as' -- found '..' +Error [EPAR0370005]: expected '=', '&&=', '||=', '&=', '|=', '&=', '+=', '-=', '*=', '/=', '**=', '%=', '<<=', '>>=', ';' -- found '..=' --> test_52:1:2 | 1 | x..=b; - | ^^ + | ^^^ diff --git a/tests/expectations/parser-statement/unreachable/postfix_fail.out b/tests/expectations/parser-statement/unreachable/postfix_fail.out index a0cc6e2fba2..6f767f34cbd 100644 --- a/tests/expectations/parser-statement/unreachable/postfix_fail.out +++ b/tests/expectations/parser-statement/unreachable/postfix_fail.out @@ -13,7 +13,7 @@ Error [EPAR0370005]: expected ';' -- found ',' | 1 | let x = a,; | ^ -Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', 'true', 'false', 'async', 'block', 'network', 'self' -- found ';' +Error [EPAR0370005]: expected an identifier, a program id, an address literal, an integer literal, a static string, '!', '-', '(', '[', '..', '..=', 'true', 'false', 'async', 'block', 'network', 'self' -- found ';' --> test_3:1:11 | 1 | let x = a[; @@ -93,7 +93,7 @@ Error [EPAR0370005]: expected an identifier, a program id, an address literal, a | 1 | let x = a>; | ^ -Error [EPAR0370005]: expected '&&', '||', '&', '|', '^', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '**', '%', '<<', '>>', '[', '.', ';', '?', 'as' -- found '..' +Error [EPAR0370005]: expected ';' -- found '..' --> test_19:1:10 | 1 | let x = a..; diff --git a/tests/tests/compiler/array/array_concat.leo b/tests/tests/compiler/array/array_concat.leo new file mode 100644 index 00000000000..93ac218b99c --- /dev/null +++ b/tests/tests/compiler/array/array_concat.leo @@ -0,0 +1,20 @@ + +program test.aleo { + // Basic array concatenation + transition foo(a: [u32; 2], b: [u32; 2]) -> [u32; 4] { + return a + b; + } + + // Nested array concatenation (flatten) + transition flatten(a: [[bool; 2]; 2]) -> [bool; 4] { + let b: [bool; 4] = a[0] + a[1]; + let c: [bool; 4] = [a[0][0], a[0][1], a[1][0], a[1][1]]; + assert_eq(b, c); + return b; + } + + // Different sized arrays + transition concat_different_sizes(a: [u8; 3], b: [u8; 2]) -> [u8; 5] { + return a + b; + } +} diff --git a/tests/tests/compiler/array/array_concat_chained.leo b/tests/tests/compiler/array/array_concat_chained.leo new file mode 100644 index 00000000000..df24b23996e --- /dev/null +++ b/tests/tests/compiler/array/array_concat_chained.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Chained array concatenation: a + b + c + transition foo(a: [u32; 2], b: [u32; 2], c: [u32; 2]) -> [u32; 6] { + return a + b + c; + } +} diff --git a/tests/tests/compiler/array/array_concat_type_mismatch_fail.leo b/tests/tests/compiler/array/array_concat_type_mismatch_fail.leo new file mode 100644 index 00000000000..85480c0cbba --- /dev/null +++ b/tests/tests/compiler/array/array_concat_type_mismatch_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // This should fail: element types don't match + transition foo(a: [u32; 2], b: [u8; 2]) -> [u32; 4] { + return a + b; + } +} diff --git a/tests/tests/compiler/array/array_concat_with_slice.leo b/tests/tests/compiler/array/array_concat_with_slice.leo new file mode 100644 index 00000000000..a5ec88a5ebd --- /dev/null +++ b/tests/tests/compiler/array/array_concat_with_slice.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Concatenate sliced arrays + transition foo(a: [u32; 4], b: [u32; 4]) -> [u32; 4] { + return a[0..2] + b[2..4]; + } +} diff --git a/tests/tests/compiler/slice/slice_basic.leo b/tests/tests/compiler/slice/slice_basic.leo new file mode 100644 index 00000000000..fb10db77814 --- /dev/null +++ b/tests/tests/compiler/slice/slice_basic.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Basic slice with both bounds + transition foo(a: [u32; 8]) -> [u32; 3] { + return a[2..5]; + } +} diff --git a/tests/tests/compiler/slice/slice_from_start.leo b/tests/tests/compiler/slice/slice_from_start.leo new file mode 100644 index 00000000000..0bd222d595e --- /dev/null +++ b/tests/tests/compiler/slice/slice_from_start.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice from start to index + transition foo(a: [u32; 8]) -> [u32; 4] { + return a[..4]; + } +} diff --git a/tests/tests/compiler/slice/slice_full.leo b/tests/tests/compiler/slice/slice_full.leo new file mode 100644 index 00000000000..c08dd3591cd --- /dev/null +++ b/tests/tests/compiler/slice/slice_full.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Full slice (copy entire array) + transition foo(a: [u32; 4]) -> [u32; 4] { + return a[..]; + } +} diff --git a/tests/tests/compiler/slice/slice_inclusive.leo b/tests/tests/compiler/slice/slice_inclusive.leo new file mode 100644 index 00000000000..87be9590b7c --- /dev/null +++ b/tests/tests/compiler/slice/slice_inclusive.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice with inclusive end bound + transition foo(a: [u32; 8]) -> [u32; 4] { + return a[2..=5]; + } +} diff --git a/tests/tests/compiler/slice/slice_invalid_range_fail.leo b/tests/tests/compiler/slice/slice_invalid_range_fail.leo new file mode 100644 index 00000000000..c3725c70bea --- /dev/null +++ b/tests/tests/compiler/slice/slice_invalid_range_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Invalid range - start > end - should fail + transition foo(a: [u32; 8]) -> [u32; 2] { + return a[5..3]; + } +} diff --git a/tests/tests/compiler/slice/slice_out_of_bounds_fail.leo b/tests/tests/compiler/slice/slice_out_of_bounds_fail.leo new file mode 100644 index 00000000000..ae24bb84704 --- /dev/null +++ b/tests/tests/compiler/slice/slice_out_of_bounds_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice out of bounds - should fail + transition foo(a: [u32; 8]) -> [u32; 5] { + return a[5..10]; + } +} diff --git a/tests/tests/compiler/slice/slice_to_end.leo b/tests/tests/compiler/slice/slice_to_end.leo new file mode 100644 index 00000000000..0e02cf6fed1 --- /dev/null +++ b/tests/tests/compiler/slice/slice_to_end.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice from index to end + transition foo(a: [u32; 8]) -> [u32; 5] { + return a[3..]; + } +} diff --git a/tests/tests/compiler/slice/slice_with_variables.leo b/tests/tests/compiler/slice/slice_with_variables.leo new file mode 100644 index 00000000000..2be150843cc --- /dev/null +++ b/tests/tests/compiler/slice/slice_with_variables.leo @@ -0,0 +1,10 @@ + +program test.aleo { + const START: u32 = 1u32; + const END: u32 = 4u32; + + // Slice with const variables + transition foo(a: [u32; 8]) -> [u32; 3] { + return a[START..END]; + } +}