diff --git a/crates/ast/src/const_eval/CLAUDE.md b/crates/ast/src/const_eval/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/ast/src/const_eval/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/ast/src/const_eval/evaluate.rs b/crates/ast/src/const_eval/evaluate.rs index 109b452b6c8..d65ec32c2bf 100644 --- a/crates/ast/src/const_eval/evaluate.rs +++ b/crates/ast/src/const_eval/evaluate.rs @@ -407,6 +407,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/crates/ast/src/const_eval/value.rs b/crates/ast/src/const_eval/value.rs index be3d9a067fd..9017ceea96f 100644 --- a/crates/ast/src/const_eval/value.rs +++ b/crates/ast/src/const_eval/value.rs @@ -610,6 +610,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/crates/ast/src/expressions/CLAUDE.md b/crates/ast/src/expressions/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/ast/src/expressions/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/ast/src/expressions/mod.rs b/crates/ast/src/expressions/mod.rs index f3b727268cb..4fd4c1604e3 100644 --- a/crates/ast/src/expressions/mod.rs +++ b/crates/ast/src/expressions/mod.rs @@ -71,6 +71,9 @@ pub use unit::*; mod literal; pub use literal::*; +mod slice; +pub use slice::*; + /// Expression that evaluates to a value. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Expression { @@ -109,6 +112,8 @@ pub enum Expression { TupleAccess(Box), /// An unary expression. Unary(Box), + /// An array slice expression e.g., `arr[2..5]`. + Slice(Box), /// A unit expression e.g. `()` Unit(UnitExpression), } @@ -139,6 +144,7 @@ impl Node for Expression { Ternary(n) => n.span(), Tuple(n) => n.span(), TupleAccess(n) => n.span(), + Slice(n) => n.span(), Unary(n) => n.span(), Unit(n) => n.span(), } @@ -163,6 +169,7 @@ impl Node for Expression { Ternary(n) => n.set_span(span), Tuple(n) => n.set_span(span), TupleAccess(n) => n.set_span(span), + Slice(n) => n.set_span(span), Unary(n) => n.set_span(span), Unit(n) => n.set_span(span), } @@ -187,6 +194,7 @@ impl Node for Expression { Ternary(n) => n.id(), Tuple(n) => n.id(), TupleAccess(n) => n.id(), + Slice(n) => n.id(), Unary(n) => n.id(), Unit(n) => n.id(), } @@ -211,6 +219,7 @@ impl Node for Expression { Ternary(n) => n.set_id(id), Tuple(n) => n.set_id(id), TupleAccess(n) => n.set_id(id), + Slice(n) => n.set_id(id), Unary(n) => n.set_id(id), Unit(n) => n.set_id(id), } @@ -237,6 +246,7 @@ impl fmt::Display for Expression { Ternary(n) => n.fmt(f), Tuple(n) => n.fmt(f), TupleAccess(n) => n.fmt(f), + Slice(n) => n.fmt(f), Unary(n) => n.fmt(f), Unit(n) => n.fmt(f), } @@ -258,7 +268,9 @@ impl Expression { Cast(_) => 12, Ternary(_) => 0, Array(_) | ArrayAccess(_) | Async(_) | Call(_) | Composite(_) | Err(_) | Intrinsic(_) | Path(_) - | Literal(_) | MemberAccess(_) | Repeat(_) | Tuple(_) | TupleAccess(_) | Unary(_) | Unit(_) => 20, + | Literal(_) | MemberAccess(_) | Repeat(_) | Slice(_) | Tuple(_) | TupleAccess(_) | Unary(_) | Unit(_) => { + 20 + } } } @@ -341,6 +353,11 @@ impl Expression { // Recurse Expression::ArrayAccess(expr) => expr.array.is_pure(get_type) && expr.index.is_pure(get_type), + Expression::Slice(expr) => { + expr.array.is_pure(get_type) + && expr.start.as_ref().is_none_or(|e| e.is_pure(get_type)) + && expr.end.as_ref().is_none_or(|(_, e)| e.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::TupleAccess(expr) => expr.tuple.is_pure(get_type), diff --git a/crates/ast/src/expressions/slice.rs b/crates/ast/src/expressions/slice.rs new file mode 100644 index 00000000000..72b5ffa3dbb --- /dev/null +++ b/crates/ast/src/expressions/slice.rs @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2026 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/crates/ast/src/passes/consumer.rs b/crates/ast/src/passes/consumer.rs index 49f9fab651b..da0edb6e42d 100644 --- a/crates/ast/src/passes/consumer.rs +++ b/crates/ast/src/passes/consumer.rs @@ -40,12 +40,15 @@ pub trait ExpressionConsumer { Expression::Ternary(ternary) => self.consume_ternary(*ternary), Expression::Tuple(tuple) => self.consume_tuple(tuple), Expression::TupleAccess(access) => self.consume_tuple_access(*access), + Expression::Slice(slice) => self.consume_slice(*slice), Expression::Unary(unary) => self.consume_unary(*unary), Expression::Unit(unit) => self.consume_unit(unit), Expression::Intrinsic(intrinsic) => self.consume_intrinsic(*intrinsic), } } + fn consume_slice(&mut self, _input: SliceExpression) -> Self::Output; + fn consume_array_access(&mut self, _input: ArrayAccess) -> Self::Output; fn consume_member_access(&mut self, _input: MemberAccess) -> Self::Output; diff --git a/crates/ast/src/passes/reconstructor.rs b/crates/ast/src/passes/reconstructor.rs index 173c64570d9..7952fd337e9 100644 --- a/crates/ast/src/passes/reconstructor.rs +++ b/crates/ast/src/passes/reconstructor.rs @@ -136,12 +136,30 @@ pub trait AstReconstructor { 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), + Expression::Slice(slice) => self.reconstruct_slice(*slice, additional), Expression::Unary(unary) => self.reconstruct_unary(*unary, additional), Expression::Unit(unit) => self.reconstruct_unit(unit, additional), Expression::Intrinsic(intr) => self.reconstruct_intrinsic(*intr, additional), } } + 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(|e| self.reconstruct_expression(e, &Default::default()).0), + end: input.end.map(|(inc, e)| (inc, self.reconstruct_expression(e, &Default::default()).0)), + ..input + } + .into(), + Default::default(), + ) + } + fn reconstruct_array_access( &mut self, input: ArrayAccess, diff --git a/crates/ast/src/passes/visitor.rs b/crates/ast/src/passes/visitor.rs index 66c8660bf9c..f48ec00252f 100644 --- a/crates/ast/src/passes/visitor.rs +++ b/crates/ast/src/passes/visitor.rs @@ -122,12 +122,24 @@ pub trait AstVisitor { 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), + Expression::Slice(slice) => self.visit_slice(slice, additional), Expression::Unary(unary) => self.visit_unary(unary, additional), Expression::Unit(unit) => self.visit_unit(unit, additional), Expression::Intrinsic(intr) => self.visit_intrinsic(intr, additional), } } + 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 { + self.visit_expression(start, &Default::default()); + } + if let Some((_, end)) = &input.end { + self.visit_expression(end, &Default::default()); + } + Default::default() + } + fn visit_array_access(&mut self, input: &ArrayAccess, _additional: &Self::AdditionalInput) -> Self::Output { self.visit_expression(&input.array, &Default::default()); self.visit_expression(&input.index, &Default::default()); diff --git a/crates/errors/src/errors/compiler/compiler_errors.rs b/crates/errors/src/errors/compiler/compiler_errors.rs index 6ae4254a081..66b665f39fc 100644 --- a/crates/errors/src/errors/compiler/compiler_errors.rs +++ b/crates/errors/src/errors/compiler/compiler_errors.rs @@ -132,4 +132,11 @@ create_messages!( msg: "This repeat count could not be determined at compile time.".to_string(), help: None, } + + @formatted + slice_bounds_not_evaluated { + args: (), + msg: "The bounds of this slice expression could not be determined at compile time.".to_string(), + help: None, + } ); diff --git a/crates/errors/src/errors/type_checker/type_checker_error.rs b/crates/errors/src/errors/type_checker/type_checker_error.rs index 5772b66e457..6189f86e0f9 100644 --- a/crates/errors/src/errors/type_checker/type_checker_error.rs +++ b/crates/errors/src/errors/type_checker/type_checker_error.rs @@ -1424,4 +1424,25 @@ create_messages!( msg: format!("`@no_inline` is not allowed on `final fn` functions because they must always be inlined."), help: None, } + + @formatted + slice_range_invalid { + args: (start: impl Display, end: impl Display), + msg: format!("Slice range `{start}..{end}` is invalid: end must be greater than start."), + help: None, + } + + @formatted + slice_out_of_bounds { + args: (start: impl Display, end: impl Display, len: impl Display), + msg: format!("Slice range `{start}..{end}` is out of bounds for an array of length `{len}`."), + help: None, + } + + @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: None, + } ); diff --git a/crates/fmt/src/CLAUDE.md b/crates/fmt/src/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/fmt/src/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/fmt/src/format.rs b/crates/fmt/src/format.rs index ba9f2562293..9bee272a51d 100644 --- a/crates/fmt/src/format.rs +++ b/crates/fmt/src/format.rs @@ -75,7 +75,7 @@ pub fn format_node(node: &SyntaxNode, out: &mut Output) { TERNARY_EXPR => format_ternary(node, out), FIELD_EXPR => format_field_expr(node, out), TUPLE_ACCESS_EXPR => format_tuple_access(node, out), - INDEX_EXPR => format_index_expr(node, out), + INDEX_EXPR | SLICE_EXPR => format_index_expr(node, out), CAST_EXPR => format_cast(node, out), ARRAY_EXPR => format_array_expr(node, out), REPEAT_EXPR => format_repeat_expr(node, out), diff --git a/crates/fmt/tests/source/expr_slice.leo b/crates/fmt/tests/source/expr_slice.leo new file mode 100644 index 00000000000..e6f746e9a8e --- /dev/null +++ b/crates/fmt/tests/source/expr_slice.leo @@ -0,0 +1,11 @@ +program test.aleo + { + fn main( arr : [u32; 8] ){ + + let s1 = arr [ 2 .. 5 ] ; + let s2 = arr [ .. 4 ] ; + let s3 = arr [ 3 .. ] ; + let s4 = arr [ 1 ..= 6 ] ; + + } +} diff --git a/crates/fmt/tests/target/expr_slice.leo b/crates/fmt/tests/target/expr_slice.leo new file mode 100644 index 00000000000..9030b58ba28 --- /dev/null +++ b/crates/fmt/tests/target/expr_slice.leo @@ -0,0 +1,8 @@ +program test.aleo { + fn main(arr: [u32; 8]) { + let s1 = arr[2..5]; + let s2 = arr[..4]; + let s3 = arr[3..]; + let s4 = arr[1..=6]; + } +} diff --git a/crates/parser-rowan/src/parser/CLAUDE.md b/crates/parser-rowan/src/parser/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/parser-rowan/src/parser/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/parser-rowan/src/parser/expressions.rs b/crates/parser-rowan/src/parser/expressions.rs index 43caac85d4c..626a37b9d6e 100644 --- a/crates/parser-rowan/src/parser/expressions.rs +++ b/crates/parser-rowan/src/parser/expressions.rs @@ -346,12 +346,36 @@ impl Parser<'_, '_> { let m = lhs.precede(self); self.bump_any(); // [ + // Check for slice with no start: `arr[..end]` or `arr[..]` + if matches!(self.current(), DOT_DOT | DOT_DOT_EQ) { + self.bump_any(); // consume `..` or `..=` + // Parse optional end expression. + if !matches!(self.current(), R_BRACKET | EOF) { + self.parse_expr(); + } + self.expect(R_BRACKET); + return Some(m.complete(self, SLICE_EXPR)); + } + + // Parse first expression (start index or regular index). if self.parse_expr().is_none() { self.error("expected index expression"); + self.expect(R_BRACKET); + return Some(m.complete(self, INDEX_EXPR)); } - self.expect(R_BRACKET); + // Check for slice with start: `arr[start..end]` or `arr[start..]` + if matches!(self.current(), DOT_DOT | DOT_DOT_EQ) { + self.bump_any(); // consume `..` or `..=` + // Parse optional end expression. + if !matches!(self.current(), R_BRACKET | EOF) { + self.parse_expr(); + } + self.expect(R_BRACKET); + return Some(m.complete(self, SLICE_EXPR)); + } + self.expect(R_BRACKET); Some(m.complete(self, INDEX_EXPR)) } diff --git a/crates/parser-rowan/src/syntax_kind.rs b/crates/parser-rowan/src/syntax_kind.rs index d507081e282..d6fde273a1f 100644 --- a/crates/parser-rowan/src/syntax_kind.rs +++ b/crates/parser-rowan/src/syntax_kind.rs @@ -461,6 +461,8 @@ pub enum SyntaxKind { FIELD_EXPR, /// Array/tuple index: `a[0]` INDEX_EXPR, + /// Array slice expression: `a[1..3]`, `a[..2]`, `a[1..]`, `a[..]` + SLICE_EXPR, /// Cast expression: `a as u32` CAST_EXPR, /// Ternary expression: `a ? b : c` @@ -680,6 +682,7 @@ impl SyntaxKind { | FIELD_EXPR | TUPLE_ACCESS_EXPR | INDEX_EXPR + | SLICE_EXPR | CAST_EXPR | TERNARY_EXPR | ARRAY_EXPR @@ -1113,6 +1116,7 @@ const SYNTAX_KIND_TABLE: &[SyntaxKind] = &[ METHOD_CALL_EXPR, FIELD_EXPR, INDEX_EXPR, + SLICE_EXPR, CAST_EXPR, TERNARY_EXPR, ARRAY_EXPR, diff --git a/crates/parser/src/rowan.rs b/crates/parser/src/rowan.rs index ce5b659c687..55fefcfc5b5 100644 --- a/crates/parser/src/rowan.rs +++ b/crates/parser/src/rowan.rs @@ -502,6 +502,7 @@ impl<'a> ConversionContext<'a> { FIELD_EXPR => self.field_expr_to_expression(node)?, TUPLE_ACCESS_EXPR => self.tuple_access_expr_to_expression(node)?, INDEX_EXPR => self.index_expr_to_expression(node)?, + SLICE_EXPR => self.slice_expr_to_expression(node)?, CAST_EXPR => self.cast_expr_to_expression(node)?, TERNARY_EXPR => self.ternary_expr_to_expression(node)?, ARRAY_EXPR => self.array_expr_to_expression(node)?, @@ -966,6 +967,54 @@ impl<'a> ConversionContext<'a> { Ok(leo_ast::ArrayAccess { array, index, span, id }.into()) } + /// Convert a SLICE_EXPR node to a SliceExpression. + /// + /// The CST structure is: + /// - `array_expr L_BRACKET (start_expr)? (DOT_DOT|DOT_DOT_EQ) (end_expr)? R_BRACKET` + fn slice_expr_to_expression(&self, node: &SyntaxNode) -> Result { + debug_assert_eq!(node.kind(), SLICE_EXPR); + let span = self.content_span(node); + let id = self.builder.next_id(); + + // Walk children to determine structure: array, optional start, range op, optional end. + let mut array_node: Option = None; + let mut start_node: Option = None; + let mut end_node: Option = None; + let mut inclusive = false; + let mut saw_range = false; + + for child in node.children_with_tokens() { + match child { + SyntaxElement::Node(n) if n.kind().is_expression() => { + if array_node.is_none() { + array_node = Some(n); + } else if !saw_range { + start_node = Some(n); + } else { + end_node = Some(n); + } + } + SyntaxElement::Token(t) if matches!(t.kind(), DOT_DOT | DOT_DOT_EQ) => { + inclusive = t.kind() == DOT_DOT_EQ; + saw_range = true; + } + _ => {} + } + } + + let array = match array_node { + Some(n) => self.to_expression(&n)?, + None => { + self.emit_unexpected_str("array in slice expression", node.text(), span); + return Ok(self.error_expression(span)); + } + }; + let start = start_node.map(|n| self.to_expression(&n)).transpose()?; + let end = end_node.map(|n| self.to_expression(&n)).transpose()?.map(|e| (inclusive, e)); + + Ok(leo_ast::SliceExpression { array, start, end, span, id }.into()) + } + /// Convert a CAST_EXPR node to a CastExpression. fn cast_expr_to_expression(&self, node: &SyntaxNode) -> Result { debug_assert_eq!(node.kind(), CAST_EXPR); diff --git a/crates/passes/src/code_generation/CLAUDE.md b/crates/passes/src/code_generation/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/passes/src/code_generation/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/passes/src/code_generation/expression.rs b/crates/passes/src/code_generation/expression.rs index 02560bef6bc..b7dd4a96c2b 100644 --- a/crates/passes/src/code_generation/expression.rs +++ b/crates/passes/src/code_generation/expression.rs @@ -77,6 +77,9 @@ 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(_) => { + unreachable!("Slice expressions must be eliminated by const propagation before code generation.") + } 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)), @@ -188,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"); diff --git a/crates/passes/src/common/replacer/mod.rs b/crates/passes/src/common/replacer/mod.rs index dd73bd1dc9b..6e63205559a 100644 --- a/crates/passes/src/common/replacer/mod.rs +++ b/crates/passes/src/common/replacer/mod.rs @@ -80,6 +80,7 @@ where Expression::Literal(value) => self.reconstruct_literal(value, &()), 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/crates/passes/src/common_subexpression_elimination/visitor.rs b/crates/passes/src/common_subexpression_elimination/visitor.rs index 56aa439729a..0d1d5be7262 100644 --- a/crates/passes/src/common_subexpression_elimination/visitor.rs +++ b/crates/passes/src/common_subexpression_elimination/visitor.rs @@ -108,6 +108,7 @@ impl CommonSubexpressionEliminatingVisitor<'_> { | Expression::Err(_) | Expression::MemberAccess(_) | Expression::Repeat(_) + | Expression::Slice(_) | Expression::Composite(_) | Expression::Ternary(_) | Expression::Tuple(_) @@ -217,6 +218,17 @@ impl CommonSubexpressionEliminatingVisitor<'_> { return Some((expression, false)); } + 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::Tuple(tuple_expression) => { // Tuple expressions only exist in return statements at this point in // compilation, so we need only visit each member. diff --git a/crates/passes/src/const_prop_unroll_and_morphing.rs b/crates/passes/src/const_prop_unroll_and_morphing.rs index 80425a52693..38ecb603d9c 100644 --- a/crates/passes/src/const_prop_unroll_and_morphing.rs +++ b/crates/passes/src/const_prop_unroll_and_morphing.rs @@ -82,6 +82,10 @@ impl Pass for ConstPropUnrollAndMorphing { return Err(CompilerError::repeat_count_not_evaluated(not_evaluated_span).into()); } + if let Some(not_evaluated_span) = const_prop_output.slice_bounds_not_evaluated { + return Err(CompilerError::slice_bounds_not_evaluated(not_evaluated_span).into()); + } + if let Some(not_evaluated_span) = const_prop_output.array_length_not_evaluated { return Err(CompilerError::array_length_not_evaluated(not_evaluated_span).into()); } diff --git a/crates/passes/src/const_propagation/CLAUDE.md b/crates/passes/src/const_propagation/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/passes/src/const_propagation/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/passes/src/const_propagation/ast.rs b/crates/passes/src/const_propagation/ast.rs index 86bba77b857..98c7b68482f 100644 --- a/crates/passes/src/const_propagation/ast.rs +++ b/crates/passes/src/const_propagation/ast.rs @@ -63,6 +63,7 @@ impl AstReconstructor for ConstPropagationVisitor<'_> { Expression::Literal(value) => self.reconstruct_literal(value, &()), 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, &()), @@ -262,6 +263,77 @@ impl AstReconstructor for ConstPropagationVisitor<'_> { } } + fn reconstruct_slice( + &mut self, + input: leo_ast::SliceExpression, + _additional: &(), + ) -> (Expression, Self::AdditionalOutput) { + let span = input.span(); + let id = input.id(); + let array_id = input.array.id(); + let (array, array_opt) = self.reconstruct_expression(input.array, &()); + let start_expr = input.start.map(|e| self.reconstruct_expression(e, &())); + let end_expr = input.end.map(|(incl, e)| (incl, self.reconstruct_expression(e, &()))); + + let ty = self.state.type_table.get(&array_id); + let Some(Type::Array(arr_ty)) = &ty else { + panic!("Type checking guaranteed that this is an array."); + }; + + let start_val = match &start_expr { + Some((expr, _)) => expr.as_u32(), + None => Some(0), + }; + let end_val = match &end_expr { + Some((inclusive, (expr, _))) => { + expr.as_u32().and_then(|v| if *inclusive { v.checked_add(1) } else { Some(v) }) + } + None => arr_ty.length.as_u32(), + }; + + if let (Some(start), Some(end)) = (start_val, end_val) + && end >= start + { + // If the array value is known, extract the sub-array directly. + if let Some(array_value) = array_opt { + let slice_values: Vec<_> = (start..end) + .map(|i| array_value.array_index(i as usize).expect("Type checking verified bounds.").clone()) + .collect(); + let value = Value::make_array(slice_values.into_iter()); + let expr = self.value_to_expression(&value, span, id).expect(VALUE_ERROR); + return (expr, Some(value)); + } + + // Otherwise expand arr[start..end] into [arr[start], ..., arr[end-1]]. + let elem_type = arr_ty.element_type().clone(); + let mut elements = Vec::with_capacity((end - start) as usize); + for i in start..end { + let index = Expression::Literal(Literal::integer( + IntegerType::U32, + i.to_string(), + span, + self.state.node_builder.next_id(), + )); + let access = ArrayAccess { array: array.clone(), index, span, id: self.state.node_builder.next_id() }; + self.state.type_table.insert(access.id, elem_type.clone()); + elements.push(Expression::ArrayAccess(Box::new(access))); + } + + let array_expr = ArrayExpression { elements, span, id }; + return (Expression::Array(array_expr), None); + } + + self.slice_bounds_not_evaluated = Some(span); + let reconstructed = SliceExpression { + array, + start: start_expr.map(|(e, _)| e), + end: end_expr.map(|(incl, (e, _))| (incl, e)), + span, + id, + }; + (reconstructed.into(), None) + } + fn reconstruct_tuple_access( &mut self, input: TupleAccess, @@ -309,21 +381,78 @@ 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 const_eval::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) { + 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) { + let mut elements = Vec::with_capacity((left_len + right_len) as usize); + + let mut add_elements = |arr: &Expression, arr_type: &ArrayType, len: u32| { + if let Expression::Array(array_expr) = arr { + for elem in &array_expr.elements { + elements.push(elem.clone()); + } + } else { + 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(), + }; + 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/crates/passes/src/const_propagation/mod.rs b/crates/passes/src/const_propagation/mod.rs index 53802d02c34..ac929c5c59b 100644 --- a/crates/passes/src/const_propagation/mod.rs +++ b/crates/passes/src/const_propagation/mod.rs @@ -38,6 +38,8 @@ pub struct ConstPropagationOutput { pub array_length_not_evaluated: Option, /// A repeat expression count which was not able to be evaluated. pub repeat_count_not_evaluated: Option, + /// A slice bound which was not able to be evaluated. + pub slice_bounds_not_evaluated: Option, } /// A pass to perform const propagation and folding. @@ -74,6 +76,7 @@ impl Pass for ConstPropagation { array_index_not_evaluated: None, array_length_not_evaluated: None, repeat_count_not_evaluated: None, + slice_bounds_not_evaluated: None, }; ast.ast = visitor.reconstruct_program(ast.ast); visitor.state.handler.last_err()?; @@ -84,6 +87,7 @@ impl Pass for ConstPropagation { array_index_not_evaluated: visitor.array_index_not_evaluated, array_length_not_evaluated: visitor.array_length_not_evaluated, repeat_count_not_evaluated: visitor.repeat_count_not_evaluated, + slice_bounds_not_evaluated: visitor.slice_bounds_not_evaluated, }) } } @@ -99,6 +103,7 @@ impl<'a> ConstPropagationVisitor<'a> { array_index_not_evaluated: None, array_length_not_evaluated: None, repeat_count_not_evaluated: None, + slice_bounds_not_evaluated: None, } } } diff --git a/crates/passes/src/const_propagation/visitor.rs b/crates/passes/src/const_propagation/visitor.rs index e2496a7dad0..5ab83ddd4cb 100644 --- a/crates/passes/src/const_propagation/visitor.rs +++ b/crates/passes/src/const_propagation/visitor.rs @@ -36,6 +36,8 @@ pub struct ConstPropagationVisitor<'a> { pub array_length_not_evaluated: Option, /// A repeat expression count which was not able to be evaluated. pub repeat_count_not_evaluated: Option, + /// A slice bound which was not able to be evaluated. + pub slice_bounds_not_evaluated: Option, } impl ConstPropagationVisitor<'_> { diff --git a/crates/passes/src/monomorphization/ast.rs b/crates/passes/src/monomorphization/ast.rs index 63714a79aa1..ca4ec2f1c47 100644 --- a/crates/passes/src/monomorphization/ast.rs +++ b/crates/passes/src/monomorphization/ast.rs @@ -100,6 +100,7 @@ impl AstReconstructor for MonomorphizationVisitor<'_> { Expression::Literal(value) => self.reconstruct_literal(value, &()), 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/crates/passes/src/option_lowering/ast.rs b/crates/passes/src/option_lowering/ast.rs index 8e221e0dc0b..d85a65a5d6c 100644 --- a/crates/passes/src/option_lowering/ast.rs +++ b/crates/passes/src/option_lowering/ast.rs @@ -99,6 +99,7 @@ impl leo_ast::AstReconstructor for OptionLoweringVisitor<'_> { Expression::Literal(e) => self.reconstruct_literal(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/crates/passes/src/static_analysis/future_checker.rs b/crates/passes/src/static_analysis/future_checker.rs index 85aad2dfb6b..35398d39978 100644 --- a/crates/passes/src/static_analysis/future_checker.rs +++ b/crates/passes/src/static_analysis/future_checker.rs @@ -102,6 +102,7 @@ impl AstVisitor for FutureChecker<'_> { Expression::Literal(literal) => self.visit_literal(literal, &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/crates/passes/src/static_single_assignment/expression.rs b/crates/passes/src/static_single_assignment/expression.rs index 36d580b41f3..cd3757d02d3 100644 --- a/crates/passes/src/static_single_assignment/expression.rs +++ b/crates/passes/src/static_single_assignment/expression.rs @@ -32,6 +32,7 @@ use leo_ast::{ MemberAccess, Path, RepeatExpression, + SliceExpression, Statement, TernaryExpression, TupleAccess, @@ -232,6 +233,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/crates/passes/src/type_checking/CLAUDE.md b/crates/passes/src/type_checking/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/crates/passes/src/type_checking/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/passes/src/type_checking/ast.rs b/crates/passes/src/type_checking/ast.rs index 5f5284b1e12..9ede711daae 100644 --- a/crates/passes/src/type_checking/ast.rs +++ b/crates/passes/src/type_checking/ast.rs @@ -386,6 +386,7 @@ impl AstVisitor for TypeCheckingVisitor<'_> { Expression::Literal(literal) => self.visit_literal(literal, 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), @@ -506,6 +507,98 @@ 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::type_should_be2(array_type.clone(), "an array", input.array.span())); + return Type::Err; + } + }; + + // Type check the start expression if present. + if let Some(start) = &input.start { + let ty = self.visit_expression_infer_default_u32(start); + self.assert_unsigned_type(&ty, start.span()); + } + + // Type check the end expression if present. + if let Some((_, end)) = &input.end { + let ty = self.visit_expression_infer_default_u32(end); + self.assert_unsigned_type(&ty, end.span()); + } + + // Get the start index (default 0). If start is present but not resolvable, defer. + let start = match &input.start { + Some(e) => match e.as_u32() { + Some(v) => v, + None => return Type::Err, + }, + None => 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(raw_end) = end_expr.as_u32() { + let Some(end) = (if *inclusive { raw_end.checked_add(1) } else { Some(raw_end) }) else { + self.emit_err(TypeCheckerError::slice_range_invalid(raw_end, raw_end, input.span())); + return Type::Err; + }; + if end <= start { + self.emit_err(TypeCheckerError::slice_range_invalid(start, raw_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_out_of_bounds(start, arr_len, 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 { + // Bounds not yet resolvable (e.g. const variables not yet propagated). + // Return Type::Err silently; the fixed-point loop will retry after const prop. + 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 { @@ -674,21 +767,47 @@ 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). + 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); - // Infer `Numeric` types if possible + // 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); - // Now sanity check everything + // Now sanity check everything. let assert_add_type = |type_: &Type, span: Span| { 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/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_literal.out b/tests/expectations/compiler/array/array_concat_literal.out new file mode 100644 index 00000000000..37350dd15d6 --- /dev/null +++ b/tests/expectations/compiler/array/array_concat_literal.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 2u32].private; + cast 1u32 2u32 r0[0u32] r0[1u32] into r1 as [u32; 4u32]; + output r1 as [u32; 4u32].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..ab55d3b5a01 --- /dev/null +++ b/tests/expectations/compiler/array/array_concat_type_mismatch_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372178]: Cannot concatenate arrays with different element types `u32` and `u8`. + --> compiler-test:5:16 + | + 5 | return a + b; + | ^^^^^ 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..4cf6e6ce428 --- /dev/null +++ b/tests/expectations/compiler/array/array_concat_with_slice.out @@ -0,0 +1,7 @@ +program test.aleo; + +function foo: + input r0 as [u32; 4u32].private; + input r1 as [u32; 4u32].private; + cast r0[0u32] r0[1u32] r1[2u32] r1[3u32] into r2 as [u32; 4u32]; + output r2 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_bool_array.out b/tests/expectations/compiler/slice/slice_bool_array.out new file mode 100644 index 00000000000..e59f3c5ae7b --- /dev/null +++ b/tests/expectations/compiler/slice/slice_bool_array.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [boolean; 4u32].private; + cast r0[1u32] r0[2u32] into r1 as [boolean; 2u32]; + output r1 as [boolean; 2u32].private; diff --git a/tests/expectations/compiler/slice/slice_empty_fail.out b/tests/expectations/compiler/slice/slice_empty_fail.out new file mode 100644 index 00000000000..00ca9227e55 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_empty_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372176]: Slice range `5..5` is invalid: end must be greater than start. + --> compiler-test:5:16 + | + 5 | return a[5..5]; + | ^^^^^^^ 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_inclusive_invalid_range_fail.out b/tests/expectations/compiler/slice/slice_inclusive_invalid_range_fail.out new file mode 100644 index 00000000000..1ea5f2ffc00 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_inclusive_invalid_range_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372176]: Slice range `5..2` is invalid: end must be greater than start. + --> compiler-test:5:16 + | + 5 | return a[5..=2]; + | ^^^^^^^^ diff --git a/tests/expectations/compiler/slice/slice_inclusive_no_start.out b/tests/expectations/compiler/slice/slice_inclusive_no_start.out new file mode 100644 index 00000000000..bd62782c149 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_inclusive_no_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_inclusive_oob_fail.out b/tests/expectations/compiler/slice/slice_inclusive_oob_fail.out new file mode 100644 index 00000000000..4c5fb0101c6 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_inclusive_oob_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372177]: Slice range `0..9` is out of bounds for an array of length `8`. + --> compiler-test:5:16 + | + 5 | return a[0..=8]; + | ^^^^^^^^ 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..b720230ceeb --- /dev/null +++ b/tests/expectations/compiler/slice/slice_invalid_range_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372176]: Slice range `5..3` is invalid: end must be greater than start. + --> compiler-test:5:16 + | + 5 | return a[5..3]; + | ^^^^^^^ diff --git a/tests/expectations/compiler/slice/slice_length_one.out b/tests/expectations/compiler/slice/slice_length_one.out new file mode 100644 index 00000000000..71b8e6ec6a4 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_length_one.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[3u32] into r1 as [u32; 1u32]; + output r1 as [u32; 1u32].private; diff --git a/tests/expectations/compiler/slice/slice_negative_bounds_fail.out b/tests/expectations/compiler/slice/slice_negative_bounds_fail.out new file mode 100644 index 00000000000..7484c652f43 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_negative_bounds_fail.out @@ -0,0 +1,10 @@ +Error [ETYC0372117]: Expected an unsigned integer but type `i32` was found. + --> compiler-test:4:18 + | + 4 | return a[-1i32..3i32]; + | ^^^^^ +Error [ETYC0372117]: Expected an unsigned integer but type `i32` was found. + --> compiler-test:4:25 + | + 4 | return a[-1i32..3i32]; + | ^^^^ diff --git a/tests/expectations/compiler/slice/slice_nested.out b/tests/expectations/compiler/slice/slice_nested.out new file mode 100644 index 00000000000..dc7c85ca01f --- /dev/null +++ b/tests/expectations/compiler/slice/slice_nested.out @@ -0,0 +1,7 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[1u32] r0[2u32] r0[3u32] r0[4u32] into r1 as [u32; 4u32]; + cast r1[0u32] r1[1u32] into r2 as [u32; 2u32]; + output r2 as [u32; 2u32].private; diff --git a/tests/expectations/compiler/slice/slice_nested_array.out b/tests/expectations/compiler/slice/slice_nested_array.out new file mode 100644 index 00000000000..013ce027b4e --- /dev/null +++ b/tests/expectations/compiler/slice/slice_nested_array.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [[u32; 4u32]; 4u32].private; + cast r0[1u32] r0[2u32] into r1 as [[u32; 4u32]; 2u32]; + output r1 as [[u32; 4u32]; 2u32].private; diff --git a/tests/expectations/compiler/slice/slice_non_const_fail.out b/tests/expectations/compiler/slice/slice_non_const_fail.out new file mode 100644 index 00000000000..03b6cf33cef --- /dev/null +++ b/tests/expectations/compiler/slice/slice_non_const_fail.out @@ -0,0 +1,5 @@ +Error [ECMP0376015]: The bounds of this slice expression could not be determined at compile time. + --> compiler-test:5:16 + | + 5 | return a[start..end]; + | ^^^^^^^^^^^^^ 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..2a3ead63b40 --- /dev/null +++ b/tests/expectations/compiler/slice/slice_out_of_bounds_fail.out @@ -0,0 +1,5 @@ +Error [ETYC0372177]: Slice range `5..10` is out of bounds for an 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..98021637bdd --- /dev/null +++ b/tests/expectations/compiler/slice/slice_with_variables.out @@ -0,0 +1,6 @@ +program test.aleo; + +function foo: + input r0 as [u32; 8u32].private; + cast r0[1u32] r0[2u32] r0[3u32] into r1 as [u32; 3u32]; + output r1 as [u32; 3u32].private; diff --git a/tests/expectations/compiler/statements/CLAUDE.md b/tests/expectations/compiler/statements/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/tests/expectations/compiler/statements/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file 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/CLAUDE.md b/tests/expectations/parser-statement/statement/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/tests/expectations/parser-statement/statement/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/tests/expectations/parser-statement/unreachable/CLAUDE.md b/tests/expectations/parser-statement/unreachable/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/tests/expectations/parser-statement/unreachable/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/tests/tests/compiler/array/array_concat.leo b/tests/tests/compiler/array/array_concat.leo new file mode 100644 index 00000000000..2e794e5fa60 --- /dev/null +++ b/tests/tests/compiler/array/array_concat.leo @@ -0,0 +1,20 @@ + +program test.aleo { + // Basic array concatenation + fn foo(a: [u32; 2], b: [u32; 2]) -> [u32; 4] { + return a + b; + } + + // Nested array concatenation (flatten) + fn 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 + fn 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..d929d5a9137 --- /dev/null +++ b/tests/tests/compiler/array/array_concat_chained.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Chained array concatenation: a + b + c + fn 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_literal.leo b/tests/tests/compiler/array/array_concat_literal.leo new file mode 100644 index 00000000000..cece167fa79 --- /dev/null +++ b/tests/tests/compiler/array/array_concat_literal.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Concatenate a literal array with a variable array + fn foo(a: [u32; 2]) -> [u32; 4] { + return [1u32, 2u32] + a; + } +} 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..839d8e39de4 --- /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 + fn 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..f683f061523 --- /dev/null +++ b/tests/tests/compiler/array/array_concat_with_slice.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Concatenate sliced arrays + fn foo(a: [u32; 4], b: [u32; 4]) -> [u32; 4] { + return a[0..2] + b[2..4]; + } +} diff --git a/tests/tests/compiler/slice/CLAUDE.md b/tests/tests/compiler/slice/CLAUDE.md new file mode 100644 index 00000000000..59ab83fcc31 --- /dev/null +++ b/tests/tests/compiler/slice/CLAUDE.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/tests/tests/compiler/slice/slice_basic.leo b/tests/tests/compiler/slice/slice_basic.leo new file mode 100644 index 00000000000..d38853c7d3c --- /dev/null +++ b/tests/tests/compiler/slice/slice_basic.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Basic slice with both bounds + fn foo(a: [u32; 8]) -> [u32; 3] { + return a[2..5]; + } +} diff --git a/tests/tests/compiler/slice/slice_bool_array.leo b/tests/tests/compiler/slice/slice_bool_array.leo new file mode 100644 index 00000000000..118257ee89f --- /dev/null +++ b/tests/tests/compiler/slice/slice_bool_array.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice with non-u32 element type + fn foo(a: [bool; 4]) -> [bool; 2] { + return a[1..3]; + } +} diff --git a/tests/tests/compiler/slice/slice_empty_fail.leo b/tests/tests/compiler/slice/slice_empty_fail.leo new file mode 100644 index 00000000000..90bfc129d58 --- /dev/null +++ b/tests/tests/compiler/slice/slice_empty_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Empty slice (start == end) - should fail + fn foo(a: [u32; 8]) -> [u32; 0] { + return a[5..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..b0ddbda90f7 --- /dev/null +++ b/tests/tests/compiler/slice/slice_from_start.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice from start to index + fn 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..0acfd4cf19f --- /dev/null +++ b/tests/tests/compiler/slice/slice_full.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Full slice (copy entire array) + fn 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..1613b1d8dbe --- /dev/null +++ b/tests/tests/compiler/slice/slice_inclusive.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice with inclusive end bound + fn foo(a: [u32; 8]) -> [u32; 4] { + return a[2..=5]; + } +} diff --git a/tests/tests/compiler/slice/slice_inclusive_invalid_range_fail.leo b/tests/tests/compiler/slice/slice_inclusive_invalid_range_fail.leo new file mode 100644 index 00000000000..1d6a94aac76 --- /dev/null +++ b/tests/tests/compiler/slice/slice_inclusive_invalid_range_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Inclusive range with start > end - should fail + fn foo(a: [u32; 8]) -> [u32; 2] { + return a[5..=2]; + } +} diff --git a/tests/tests/compiler/slice/slice_inclusive_no_start.leo b/tests/tests/compiler/slice/slice_inclusive_no_start.leo new file mode 100644 index 00000000000..ba6b193136d --- /dev/null +++ b/tests/tests/compiler/slice/slice_inclusive_no_start.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Inclusive range with no start bound + fn foo(a: [u32; 8]) -> [u32; 4] { + return a[..=3]; + } +} diff --git a/tests/tests/compiler/slice/slice_inclusive_oob_fail.leo b/tests/tests/compiler/slice/slice_inclusive_oob_fail.leo new file mode 100644 index 00000000000..45b525e3266 --- /dev/null +++ b/tests/tests/compiler/slice/slice_inclusive_oob_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Inclusive range out of bounds - a[0..=8] on [u32; 8] means index 8 which is OOB + fn foo(a: [u32; 8]) -> [u32; 9] { + return a[0..=8]; + } +} 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..d1bd0e69fec --- /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 + fn foo(a: [u32; 8]) -> [u32; 2] { + return a[5..3]; + } +} diff --git a/tests/tests/compiler/slice/slice_length_one.leo b/tests/tests/compiler/slice/slice_length_one.leo new file mode 100644 index 00000000000..cc835935b2a --- /dev/null +++ b/tests/tests/compiler/slice/slice_length_one.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Minimum non-empty slice (length 1) + fn foo(a: [u32; 8]) -> [u32; 1] { + return a[3..4]; + } +} diff --git a/tests/tests/compiler/slice/slice_negative_bounds_fail.leo b/tests/tests/compiler/slice/slice_negative_bounds_fail.leo new file mode 100644 index 00000000000..43c4563d880 --- /dev/null +++ b/tests/tests/compiler/slice/slice_negative_bounds_fail.leo @@ -0,0 +1,6 @@ +program test.aleo { + // Slice bounds must be unsigned integers; signed integers are rejected. + fn foo(a: [u32; 8]) -> [u32; 3] { + return a[-1i32..3i32]; + } +} diff --git a/tests/tests/compiler/slice/slice_nested.leo b/tests/tests/compiler/slice/slice_nested.leo new file mode 100644 index 00000000000..98eab9d21b2 --- /dev/null +++ b/tests/tests/compiler/slice/slice_nested.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Nested slicing: slice the result of a slice + fn foo(a: [u32; 8]) -> [u32; 2] { + return a[1..5][0..2]; + } +} diff --git a/tests/tests/compiler/slice/slice_nested_array.leo b/tests/tests/compiler/slice/slice_nested_array.leo new file mode 100644 index 00000000000..bcc748da776 --- /dev/null +++ b/tests/tests/compiler/slice/slice_nested_array.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice of a nested array + fn foo(a: [[u32; 4]; 4]) -> [[u32; 4]; 2] { + return a[1..3]; + } +} diff --git a/tests/tests/compiler/slice/slice_non_const_fail.leo b/tests/tests/compiler/slice/slice_non_const_fail.leo new file mode 100644 index 00000000000..51a4e90b7de --- /dev/null +++ b/tests/tests/compiler/slice/slice_non_const_fail.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Non-const variable slice bounds - should fail (bounds must be compile-time constants) + fn foo(a: [u32; 8], start: u32, end: u32) -> [u32; 3] { + return a[start..end]; + } +} 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..b8acacf014d --- /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 + fn 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..b2bdf4b8a48 --- /dev/null +++ b/tests/tests/compiler/slice/slice_to_end.leo @@ -0,0 +1,7 @@ + +program test.aleo { + // Slice from index to end + fn 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..4c60ebf0727 --- /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 + fn foo(a: [u32; 8]) -> [u32; 3] { + return a[START..END]; + } +}