From 31d681f4d0f354031cd221b04a08417030a470f7 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 27 Aug 2026 20:48:46 +0300 Subject: [PATCH 1/7] preserve complete spans for composed expressions Build infix and postfix spans from parser-pair extents instead of normalized child AST spans. This retains grouping delimiters and prevents syntactically incomplete source ranges. Cover casts, field and index access, suffix operations, chained postfix expressions, and grouped infix operands while keeping identifier-name spans exact. --- silverscript-lang/src/ast/mod.rs | 16 +++++------ silverscript-lang/tests/ast_spans_tests.rs | 33 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/silverscript-lang/src/ast/mod.rs b/silverscript-lang/src/ast/mod.rs index f0be7d60..0952af0d 100644 --- a/silverscript-lang/src/ast/mod.rs +++ b/silverscript-lang/src/ast/mod.rs @@ -2159,15 +2159,16 @@ fn parse_unary<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { let mut inner = pair.into_inner(); let primary = inner.next().ok_or_else(|| CompilerError::Unsupported("missing primary in postfix".to_string()))?; + let mut source_span = Span::from(primary.as_span()); let mut expr = parse_primary(primary)?; for postfix in inner { let postfix_span = Span::from(postfix.as_span()); + let span = source_span.join(&postfix_span); match postfix.as_rule() { Rule::split_call => { let mut split_inner = postfix.into_inner(); let index_expr = split_inner.next().ok_or_else(|| CompilerError::Unsupported("missing split index".to_string()))?; let index = Box::new(parse_expression(index_expr)?); - let span = expr.span.join(&postfix_span); expr = Expr::new(ExprKind::Split { source: Box::new(expr), index, part: SplitPart::Left, span: postfix_span }, span); } Rule::slice_call => { @@ -2176,7 +2177,6 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { let end_expr = slice_inner.next().ok_or_else(|| CompilerError::Unsupported("missing slice end".to_string()))?; let start = Box::new(parse_expression(start_expr)?); let end = Box::new(parse_expression(end_expr)?); - let span = expr.span.join(&postfix_span); expr = Expr::new(ExprKind::Slice { source: Box::new(expr), start, end, span: postfix_span }, span); } Rule::append_call => { @@ -2189,14 +2189,12 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { CompilerError::Unsupported("append requires at least one expression".to_string()).with_span(&postfix_span) ); } - let span = expr.span.join(&postfix_span); expr = Expr::new(ExprKind::Append { source: Box::new(expr), args, span: postfix_span }, span); } Rule::tuple_index => { let mut index_inner = postfix.into_inner(); let index_pair = index_inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple index".to_string()))?; let index_expr = parse_expression(index_pair)?; - let span = expr.span.join(&postfix_span); if matches!(&expr.kind, ExprKind::Split { .. }) { return Err(CompilerError::Unsupported("split() results must be accessed with .0 or .1".to_string()) .with_span(&postfix_span)); @@ -2208,7 +2206,6 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { ".length" => UnarySuffixKind::Length, other => return Err(CompilerError::Unsupported(format!("unknown unary suffix '{other}'"))), }; - let span = expr.span.join(&postfix_span); expr = Expr::new(ExprKind::UnarySuffix { source: Box::new(expr), kind, span: postfix_span }, span); } Rule::tuple_field_access => { @@ -2216,7 +2213,6 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { let index = raw .parse::() .map_err(|_| CompilerError::Unsupported(format!("invalid tuple field index '{raw}'")).with_span(&postfix_span))?; - let span = expr.span.join(&postfix_span); if let ExprKind::Split { source, index: split_index, span: split_span, .. } = &expr.kind { let part = match index { 0 => SplitPart::Left, @@ -2246,7 +2242,6 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { let field_pair = postfix.into_inner().next().ok_or_else(|| CompilerError::Unsupported("missing field access name".to_string()))?; let Identifier { name: field, span: field_span } = parse_identifier(field_pair)?; - let span = expr.span.join(&postfix_span); expr = Expr::new(ExprKind::FieldAccess { source: Box::new(expr), field, field_span }, span); } Rule::as_cast => { @@ -2254,13 +2249,13 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { let type_pair = cast_inner.next().ok_or_else(|| CompilerError::Unsupported("missing type after 'as'".to_string()))?; let type_span = Span::from(type_pair.as_span()); let type_ref = parse_type_name_pair(type_pair)?; - let span = expr.span.join(&postfix_span); expr = Expr::new(ExprKind::Call { name: as_cast_call_name(&type_ref), args: vec![expr], name_span: type_span }, span); } _ => { return Err(CompilerError::Unsupported("postfix operators are not supported".to_string())); } } + source_span = span; } Ok(expr) } @@ -2710,14 +2705,17 @@ where { let mut inner = pair.into_inner(); let first = inner.next().ok_or_else(|| CompilerError::Unsupported("missing infix operand".to_string()))?; + let mut source_span = Span::from(first.as_span()); let mut expr = parse_operand(first)?; while let Some(op_pair) = inner.next() { let rhs = inner.next().ok_or_else(|| CompilerError::Unsupported("missing infix rhs".to_string()))?; let op = map_op(op_pair)?; + let rhs_span = Span::from(rhs.as_span()); let rhs_expr = parse_operand(rhs)?; - let span = expr.span.join(&rhs_expr.span); + let span = source_span.join(&rhs_span); expr = Expr::new(ExprKind::Binary { op, left: Box::new(expr), right: Box::new(rhs_expr) }, span); + source_span = span; } Ok(expr) diff --git a/silverscript-lang/tests/ast_spans_tests.rs b/silverscript-lang/tests/ast_spans_tests.rs index df99d89f..c7bed402 100644 --- a/silverscript-lang/tests/ast_spans_tests.rs +++ b/silverscript-lang/tests/ast_spans_tests.rs @@ -1,5 +1,5 @@ use silverscript_lang::ast::visit::{AstVisitorMut, NameKind, visit_function_mut}; -use silverscript_lang::ast::{ExprKind, Statement, parse_contract_ast, parse_function_ast}; +use silverscript_lang::ast::{ExprKind, Statement, parse_contract_ast, parse_expression_ast, parse_function_ast}; use silverscript_lang::span::Span; fn assert_span_text(source: &str, actual: &str, expected: &str) { @@ -63,6 +63,37 @@ fn parses_standalone_functions_and_visits_name_spans() { ); } +#[test] +fn composed_expression_spans_remain_syntactically_complete() { + let sources = [ + "(signed(next_status) + signed(increment)) as byte", + "(value).field", + "(values)[index]", + "(values)[index].length", + "(value).length", + "(left + right) * factor", + "left + (right * factor)", + ]; + + for source in sources { + let expr = parse_expression_ast(source).unwrap_or_else(|err| panic!("`{source}` should parse: {err}")); + assert_eq!(expr.span.as_str(), source, "composite expression span should be valid and complete"); + } +} + +#[test] +fn redundant_parentheses_do_not_widen_identifier_name_spans() { + let mut expr = parse_expression_ast("((value))").expect("parenthesized identifier should parse"); + let mut collector = NameOccurrenceCollector::default(); + collector.visit_expr(&mut expr); + + assert_eq!(expr.span.as_str(), "value"); + assert_eq!( + collector.occurrences, + vec![NameOccurrence { name: "value".to_string(), kind: NameKind::IdentifierExpr, source: "value".to_string() }] + ); +} + #[test] fn populates_contract_function_and_statement_spans() { let source = r#" From eeef3596311ef87322476a28baf823496b1dfcf2 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 27 Aug 2026 21:48:24 +0300 Subject: [PATCH 2/7] expose exact spans for authored type references Add exact target-struct and typed-array type spans to the public AST for source transformations. Capture the spans during parsing and preserve them through compiler rewrites. Include the new spans in mutable visitor traversal and cover exact source ranges plus span mutation in regression tests. --- debugger/session/src/session.rs | 8 ++- silverscript-lang/src/ast/mod.rs | 24 +++++--- silverscript-lang/src/ast/visit.rs | 9 ++- .../src/compiler/array_append.rs | 12 ++-- .../src/compiler/compile/const_eval.rs | 4 +- .../src/compiler/compile/expression.rs | 2 +- .../src/compiler/debug_recording.rs | 11 ++-- .../src/compiler/inline_functions.rs | 10 ++-- silverscript-lang/src/compiler/locals.rs | 13 +++-- .../src/compiler/read_input_state.rs | 9 ++- .../src/compiler/structs/declaration.rs | 1 + .../src/compiler/structs/scalar_expr.rs | 3 +- .../src/compiler/structs/statement.rs | 5 +- silverscript-lang/src/compiler/ternary.rs | 21 +++++-- silverscript-lang/src/compiler/type_check.rs | 2 +- .../src/compiler/validate_output_state.rs | 6 +- silverscript-lang/tests/ast_spans_tests.rs | 55 +++++++++++++++++++ silverscript-lang/tests/parser_tests.rs | 10 ++-- 18 files changed, 152 insertions(+), 53 deletions(-) diff --git a/debugger/session/src/session.rs b/debugger/session/src/session.rs index 5337fdbe..e97d9199 100644 --- a/debugger/session/src/session.rs +++ b/debugger/session/src/session.rs @@ -2178,8 +2178,12 @@ where }, span, )), - ExprKind::Array { type_ref, values } => Ok(Expr::new( - ExprKind::Array { type_ref: type_ref.clone(), values: values.iter().map(&mut *map_child).collect::, _>>()? }, + ExprKind::Array { type_ref, values, type_span } => Ok(Expr::new( + ExprKind::Array { + type_ref: type_ref.clone(), + values: values.iter().map(&mut *map_child).collect::, _>>()?, + type_span: *type_span, + }, span, )), ExprKind::StructLiteral { name, fields, name_span } => Ok(Expr::new( diff --git a/silverscript-lang/src/ast/mod.rs b/silverscript-lang/src/ast/mod.rs index 0952af0d..16423a6a 100644 --- a/silverscript-lang/src/ast/mod.rs +++ b/silverscript-lang/src/ast/mod.rs @@ -444,6 +444,8 @@ pub enum Statement<'i> { #[serde(skip_deserializing)] span: Span<'i>, #[serde(skip_deserializing)] + target_struct_span: Span<'i>, + #[serde(skip_deserializing)] name_span: Span<'i>, }, StructDestructure { @@ -452,6 +454,8 @@ pub enum Statement<'i> { expr: Expr<'i>, #[serde(skip_deserializing)] span: Span<'i>, + #[serde(skip_deserializing)] + struct_name_span: Span<'i>, }, Assign { name: String, @@ -621,7 +625,7 @@ impl<'i> Expr<'i> { if matches!(type_ref.array_dims.last(), Some(ArrayDim::Inferred)) { *type_ref.array_dims.last_mut().unwrap() = ArrayDim::Fixed(values.len()); } - Self::new(ExprKind::Array { type_ref, values }, Span::default()) + Self::new(ExprKind::Array { type_ref, values, type_span: Span::default() }, Span::default()) } pub fn inferred_array(values: Vec>) -> Option { @@ -726,6 +730,8 @@ pub enum ExprKind<'i> { Array { type_ref: TypeRef, values: Vec>, + #[serde(skip_deserializing)] + type_span: Span<'i>, }, Call { name: String, @@ -1169,7 +1175,7 @@ fn format_expr_with_prec(expr: &Expr<'_>, parent_prec: u8, right_child: bool) -> ExprKind::String(value) => format_string_literal(value), ExprKind::DateLiteral(value) => format!("temporal({value})"), ExprKind::Identifier(value) => value.clone(), - ExprKind::Array { type_ref, values } => format_array(type_ref, values), + ExprKind::Array { type_ref, values, .. } => format_array(type_ref, values), ExprKind::Call { name, args, .. } => { if let (Some(type_ref), [source]) = (as_cast_type(name), args.as_slice()) { format!("{} as {}", format_expr_with_prec(source, PREC_POSTFIX, false), type_ref.type_name()) @@ -1837,7 +1843,7 @@ fn parse_statement<'i>(pair: Pair<'i, Rule>) -> Result, CompilerEr let struct_pair = inner .next() .ok_or_else(|| CompilerError::Unsupported("missing destructuring struct name".to_string()).with_span(&span))?; - let Identifier { name: target_struct, .. } = parse_identifier(struct_pair)?; + let Identifier { name: target_struct, span: target_struct_span } = parse_identifier(struct_pair)?; let mut bindings = Vec::new(); while let Some(p) = inner.peek() { if p.as_rule() != Rule::state_typed_binding { @@ -1850,14 +1856,14 @@ fn parse_statement<'i>(pair: Pair<'i, Rule>) -> Result, CompilerEr inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()).with_span(&span))?; let (Identifier { name, span: name_span }, args) = parse_function_call_parts(call_pair).map_err(|err| err.with_span(&span))?; - Ok(Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span }) + Ok(Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span }) } Rule::struct_destructure_assignment => { let mut inner = pair.into_inner(); let struct_pair = inner .next() .ok_or_else(|| CompilerError::Unsupported("missing destructuring struct name".to_string()).with_span(&span))?; - let Identifier { name: struct_name, .. } = parse_identifier(struct_pair)?; + let Identifier { name: struct_name, span: struct_name_span } = parse_identifier(struct_pair)?; let mut bindings = Vec::new(); while let Some(p) = inner.peek() { if p.as_rule() != Rule::state_typed_binding { @@ -1870,7 +1876,7 @@ fn parse_statement<'i>(pair: Pair<'i, Rule>) -> Result, CompilerEr .next() .ok_or_else(|| CompilerError::Unsupported("missing destructuring expression".to_string()).with_span(&span))?; let expr = parse_expression(expr_pair).map_err(|err| err.with_span(&span))?; - Ok(Statement::StructDestructure { struct_name, bindings, expr, span }) + Ok(Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span }) } Rule::call_statement => { let mut inner = pair.into_inner(); @@ -2413,6 +2419,7 @@ fn parse_typed_array<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError let span = Span::from(pair.as_span()); let mut inner = pair.into_inner(); let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing array literal type".to_string()))?; + let type_span = Span::from(type_pair.as_span()); let mut type_ref = parse_type_name_pair(type_pair)?; let mut values = Vec::new(); for expr_pair in inner { @@ -2429,7 +2436,7 @@ fn parse_typed_array<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError } ArrayDim::Fixed(_) => {} } - Ok(Expr::new(ExprKind::Array { type_ref, values }, span)) + Ok(Expr::new(ExprKind::Array { type_ref, values, type_span }, span)) } fn parse_function_call_parts<'i>(pair: Pair<'i, Rule>) -> Result<(Identifier<'i>, Vec>), CompilerError> { @@ -2506,7 +2513,7 @@ fn parse_cast<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { if matches!(type_ref.array_size(), Some(ArrayDim::Inferred)) { type_ref.array_dims[0] = ArrayDim::Fixed(values.len()); } - return Ok(Expr::new(ExprKind::Array { type_ref, values }, span)); + return Ok(Expr::new(ExprKind::Array { type_ref, values, type_span }, span)); } if let Some(expected_len) = cast_type.base.fixed_byte_sequence_len() { if values.len() != expected_len { @@ -2520,6 +2527,7 @@ fn parse_cast<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { ExprKind::Array { type_ref: TypeRef { base: TypeBase::Byte, array_dims: vec![ArrayDim::Fixed(expected_len)] }, values, + type_span: Span::default(), }, byte_span, ); diff --git a/silverscript-lang/src/ast/visit.rs b/silverscript-lang/src/ast/visit.rs index 0120c689..ca96fa6a 100644 --- a/silverscript-lang/src/ast/visit.rs +++ b/silverscript-lang/src/ast/visit.rs @@ -222,9 +222,10 @@ pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, st visitor.visit_expr(arg); } } - Statement::StateFunctionCallAssign { target_struct: _, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct: _, bindings, name, args, span, target_struct_span, name_span } => { visitor.visit_name(name, NameKind::CallTarget, *name_span); visitor.visit_span(span); + visitor.visit_span(target_struct_span); visitor.visit_span(name_span); for binding in bindings { visitor.visit_state_binding(binding); @@ -233,8 +234,9 @@ pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, st visitor.visit_expr(arg); } } - Statement::StructDestructure { bindings, expr, span, .. } => { + Statement::StructDestructure { bindings, expr, span, struct_name_span, .. } => { visitor.visit_span(span); + visitor.visit_span(struct_name_span); for binding in bindings { visitor.visit_state_binding(binding); } @@ -317,7 +319,8 @@ pub fn walk_expr_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, expr: & visitor.visit_span(&mut expr.span); match &mut expr.kind { ExprKind::Identifier(name) => visitor.visit_name(name, NameKind::IdentifierExpr, expr_span), - ExprKind::Array { values: items, .. } => { + ExprKind::Array { values: items, type_span, .. } => { + visitor.visit_span(type_span); for item in items { visitor.visit_expr(item); } diff --git a/silverscript-lang/src/compiler/array_append.rs b/silverscript-lang/src/compiler/array_append.rs index 9880c272..2dce3a32 100644 --- a/silverscript-lang/src/compiler/array_append.rs +++ b/silverscript-lang/src/compiler/array_append.rs @@ -146,7 +146,7 @@ fn lower_statement<'i>( name_span: *name_span, }) } - Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { let lowered_args = args.iter().map(|arg| lower_expr(arg, types, constants, functions)).collect::, _>>()?; for binding in bindings { types.insert(binding.name.clone(), binding.type_ref.clone()); @@ -157,10 +157,11 @@ fn lower_statement<'i>( name: name.clone(), args: lowered_args, span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, }) } - Statement::StructDestructure { struct_name, bindings, expr, span } => { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => { let lowered_expr = lower_expr(expr, types, constants, functions)?; for binding in bindings { types.insert(binding.name.clone(), binding.type_ref.clone()); @@ -170,6 +171,7 @@ fn lower_statement<'i>( bindings: bindings.clone(), expr: lowered_expr, span: *span, + struct_name_span: *struct_name_span, }) } Statement::Assign { name, expr, span, name_span } => Ok(Statement::Assign { @@ -279,6 +281,7 @@ fn lower_expr<'i>( .iter() .map(|arg| lower_expr(arg, types, constants, functions)) .collect::, _>>()?, + type_span: span::Span::default(), }, span::Span::default(), )), @@ -286,10 +289,11 @@ fn lower_expr<'i>( span, )) } - ExprKind::Array { type_ref, values } => Ok(Expr::new( + ExprKind::Array { type_ref, values, type_span } => Ok(Expr::new( ExprKind::Array { type_ref: type_ref.clone(), values: values.iter().map(|value| lower_expr(value, types, constants, functions)).collect::, _>>()?, + type_span: *type_span, }, span, )), @@ -423,7 +427,7 @@ mod tests { let ExprKind::Binary { op: BinaryOp::Add, right, .. } = &expr.kind else { panic!("append should lower to addition"); }; - let ExprKind::Array { type_ref, values } = &right.kind else { + let ExprKind::Array { type_ref, values, .. } = &right.kind else { panic!("append arguments should lower to an array"); }; diff --git a/silverscript-lang/src/compiler/compile/const_eval.rs b/silverscript-lang/src/compiler/compile/const_eval.rs index ffc939ef..402aedc4 100644 --- a/silverscript-lang/src/compiler/compile/const_eval.rs +++ b/silverscript-lang/src/compiler/compile/const_eval.rs @@ -136,12 +136,12 @@ pub(crate) fn resolve_constant_references<'i>( }, span, )), - ExprKind::Array { type_ref, values } => { + ExprKind::Array { type_ref, values, type_span } => { let mut resolved = Vec::with_capacity(values.len()); for value in values { resolved.push(resolve_constant_references(value, constants, visiting)?); } - Ok(Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values: resolved }, span)) + Ok(Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values: resolved, type_span }, span)) } ExprKind::StructLiteral { name, fields, name_span } => { let mut resolved_fields = Vec::with_capacity(fields.len()); diff --git a/silverscript-lang/src/compiler/compile/expression.rs b/silverscript-lang/src/compiler/compile/expression.rs index 795ee44c..4b79bee5 100644 --- a/silverscript-lang/src/compiler/compile/expression.rs +++ b/silverscript-lang/src/compiler/compile/expression.rs @@ -53,7 +53,7 @@ fn compile_expr_with_context<'i>( ExprKind::Int(value) | ExprKind::Temporal(value) => compile_int_expr(ctx, *value, expected_type), ExprKind::Bool(value) => compile_bool_expr(ctx, *value), ExprKind::Byte(byte) => compile_byte_expr(ctx, *byte), - ExprKind::Array { type_ref, values } => compile_array_expr(ctx, values, Some(type_ref)), + ExprKind::Array { type_ref, values, .. } => compile_array_expr(ctx, values, Some(type_ref)), ExprKind::StructLiteral { .. } => compile_state_object_expr(), ExprKind::FieldAccess { .. } => compile_field_access_expr(), ExprKind::String(value) => compile_string_expr(ctx, value), diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index cf6ffa79..1f3f5e09 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -217,9 +217,11 @@ impl<'i> DebugRecorder<'i> { let span = expr.span; let kind = match expr.kind { ExprKind::Identifier(name) => ExprKind::Identifier(self.visible_name(&name)), - ExprKind::Array { type_ref, values } => { - ExprKind::Array { type_ref, values: values.into_iter().map(|value| self.rewrite_debug_expr(value)).collect() } - } + ExprKind::Array { type_ref, values, type_span } => ExprKind::Array { + type_ref, + values: values.into_iter().map(|value| self.rewrite_debug_expr(value)).collect(), + type_span, + }, ExprKind::Call { name, args, name_span } => { ExprKind::Call { name, args: args.into_iter().map(|arg| self.rewrite_debug_expr(arg)).collect(), name_span } } @@ -522,12 +524,13 @@ fn rewrite_debug_expr_with_function<'i>( let kind = match expr.kind { ExprKind::Identifier(name) => ExprKind::Identifier(visible_name(&name)), - ExprKind::Array { type_ref, values } => ExprKind::Array { + ExprKind::Array { type_ref, values, type_span } => ExprKind::Array { type_ref, values: values .into_iter() .map(|value| rewrite_debug_expr_with_function(value, function_name, visible_names_by_function)) .collect(), + type_span, }, ExprKind::Call { name, args, name_span } => ExprKind::Call { name, diff --git a/silverscript-lang/src/compiler/inline_functions.rs b/silverscript-lang/src/compiler/inline_functions.rs index 36c22695..5e9a1c3b 100644 --- a/silverscript-lang/src/compiler/inline_functions.rs +++ b/silverscript-lang/src/compiler/inline_functions.rs @@ -205,7 +205,7 @@ impl<'i, 'd> Inliner<'i, 'd> { ); } } - Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { let (prelude, renamed_args) = self.lower_exprs(args, scope, visited_functions)?; lowered.extend(prelude); let renamed_bindings = bindings @@ -223,11 +223,12 @@ impl<'i, 'd> Inliner<'i, 'd> { name: name.clone(), args: renamed_args, span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, }, ); } - Statement::StructDestructure { struct_name, bindings, expr, span } => { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => { let (prelude, renamed_expr) = self.lower_expr(expr, scope, visited_functions)?; lowered.extend(prelude); let renamed_bindings = bindings @@ -244,6 +245,7 @@ impl<'i, 'd> Inliner<'i, 'd> { bindings: renamed_bindings, expr: renamed_expr, span: *span, + struct_name_span: *struct_name_span, }, ); } @@ -506,9 +508,9 @@ impl<'i, 'd> Inliner<'i, 'd> { ExprKind::String(value) => Ok((Vec::new(), Expr::new(ExprKind::String(value.clone()), span))), ExprKind::DateLiteral(value) => Ok((Vec::new(), Expr::new(ExprKind::DateLiteral(*value), span))), ExprKind::Identifier(name) => Ok((Vec::new(), Expr::new(ExprKind::Identifier(self.rename_name(name, scope)), span))), - ExprKind::Array { type_ref, values } => { + ExprKind::Array { type_ref, values, type_span } => { let (prelude, values) = self.lower_exprs(values, scope, visited_functions)?; - Ok((prelude, Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values }, span))) + Ok((prelude, Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values, type_span: *type_span }, span))) } ExprKind::Call { name, args, name_span } => { let (mut prelude, args) = self.lower_exprs(args, scope, visited_functions)?; diff --git a/silverscript-lang/src/compiler/locals.rs b/silverscript-lang/src/compiler/locals.rs index b7b5ca39..257ae17a 100644 --- a/silverscript-lang/src/compiler/locals.rs +++ b/silverscript-lang/src/compiler/locals.rs @@ -104,7 +104,7 @@ fn lower_statements<'i>( name_span: *name_span, }); } - Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { for binding in bindings { local_aliases.remove(&binding.name); } @@ -114,10 +114,11 @@ fn lower_statements<'i>( name: name.clone(), args: args.iter().map(|arg| substitute_expr(arg, &local_aliases)).collect::, _>>()?, span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, }); } - Statement::StructDestructure { struct_name, bindings, expr, span } => { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => { for binding in bindings { local_aliases.remove(&binding.name); } @@ -126,6 +127,7 @@ fn lower_statements<'i>( bindings: bindings.clone(), expr: substitute_expr(expr, &local_aliases)?, span: *span, + struct_name_span: *struct_name_span, }); } Statement::Assign { name, expr, span, name_span } => { @@ -215,11 +217,11 @@ fn coerce_expr_for_declared_type<'i>(expr: Expr<'i>, type_ref: &TypeRef) -> Resu return Ok(Expr::new(ExprKind::Byte(byte_value), expr.span)); } if let Some(element_type) = type_ref.array_element_type() - && let ExprKind::Array { values, .. } = expr.kind + && let ExprKind::Array { values, type_span, .. } = expr.kind { let values = values.into_iter().map(|value| coerce_expr_for_declared_type(value, &element_type)).collect::, _>>()?; - return Ok(Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values }, expr.span)); + return Ok(Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values, type_span }, expr.span)); } Ok(expr) } @@ -245,10 +247,11 @@ fn substitute_expr<'i>(expr: &Expr<'i>, aliases: &HashMap>) -> }, span, ), - ExprKind::Array { type_ref, values } => Expr::new( + ExprKind::Array { type_ref, values, type_span } => Expr::new( ExprKind::Array { type_ref, values: values.iter().map(|value| substitute_expr(value, aliases)).collect::, _>>()?, + type_span, }, span, ), diff --git a/silverscript-lang/src/compiler/read_input_state.rs b/silverscript-lang/src/compiler/read_input_state.rs index 95d1edcf..11726727 100644 --- a/silverscript-lang/src/compiler/read_input_state.rs +++ b/silverscript-lang/src/compiler/read_input_state.rs @@ -156,21 +156,23 @@ fn lower_statement<'i>(statement: &Statement<'i>, context: &mut LoweringContext) span: *span, name_span: *name_span, }, - Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { Statement::StateFunctionCallAssign { target_struct: target_struct.clone(), bindings: bindings.clone(), name: name.clone(), args: lower_call_args(args, &mut prefix, context), span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, } } - Statement::StructDestructure { struct_name, bindings, expr, span } => Statement::StructDestructure { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => Statement::StructDestructure { struct_name: struct_name.clone(), bindings: bindings.clone(), expr: lower_expr(expr, &mut prefix, context), span: *span, + struct_name_span: *struct_name_span, }, Statement::Assign { name, expr, span, name_span } => { Statement::Assign { name: name.clone(), expr: lower_expr(expr, &mut prefix, context), span: *span, name_span: *name_span } @@ -255,9 +257,10 @@ fn lower_call_arg<'i>(arg: &Expr<'i>, prefix: &mut Vec>, context: fn lower_expr<'i>(expr: &Expr<'i>, prefix: &mut Vec>, context: &mut LoweringContext) -> Expr<'i> { let kind = match &expr.kind { - ExprKind::Array { type_ref, values } => ExprKind::Array { + ExprKind::Array { type_ref, values, type_span } => ExprKind::Array { type_ref: type_ref.clone(), values: values.iter().map(|value| lower_expr(value, prefix, context)).collect(), + type_span: *type_span, }, ExprKind::Call { name, args, name_span } => { ExprKind::Call { name: name.clone(), args: lower_call_args(args, prefix, context), name_span: *name_span } diff --git a/silverscript-lang/src/compiler/structs/declaration.rs b/silverscript-lang/src/compiler/structs/declaration.rs index 246c724f..8259b8d8 100644 --- a/silverscript-lang/src/compiler/structs/declaration.rs +++ b/silverscript-lang/src/compiler/structs/declaration.rs @@ -37,6 +37,7 @@ pub(super) fn lower_variable_definition<'i>( name: builtin_name.clone(), args: args.iter().map(|arg| lower_scalar_expr(arg, scope, lowerer)).collect::, _>>()?, span: *span, + target_struct_span: *type_span, name_span: *name_span, }]); } diff --git a/silverscript-lang/src/compiler/structs/scalar_expr.rs b/silverscript-lang/src/compiler/structs/scalar_expr.rs index 816d455d..46994b9b 100644 --- a/silverscript-lang/src/compiler/structs/scalar_expr.rs +++ b/silverscript-lang/src/compiler/structs/scalar_expr.rs @@ -125,10 +125,11 @@ pub(super) fn lower_scalar_expr<'i>( }, span, )), - ExprKind::Array { type_ref, values } => Ok(Expr::new( + ExprKind::Array { type_ref, values, type_span } => Ok(Expr::new( ExprKind::Array { type_ref: type_ref.clone(), values: values.iter().map(|value| lower_scalar_expr(value, scope, lowerer)).collect::, _>>()?, + type_span: *type_span, }, span, )), diff --git a/silverscript-lang/src/compiler/structs/statement.rs b/silverscript-lang/src/compiler/structs/statement.rs index 8c204a42..3ba13e80 100644 --- a/silverscript-lang/src/compiler/structs/statement.rs +++ b/silverscript-lang/src/compiler/structs/statement.rs @@ -68,7 +68,7 @@ pub(super) fn lower_statements<'i>( name_span: *name_span, }); } - Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { for binding in bindings { scope.declare(binding.name.clone(), binding.type_ref.clone()); } @@ -78,10 +78,11 @@ pub(super) fn lower_statements<'i>( name: name.clone(), args: args.iter().map(|arg| lower_scalar_expr(arg, scope, lowerer)).collect::, _>>()?, span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, }); } - Statement::StructDestructure { struct_name, bindings, expr, span } => { + Statement::StructDestructure { struct_name, bindings, expr, span, .. } => { for binding in bindings { scope.declare(binding.name.clone(), binding.type_ref.clone()); } diff --git a/silverscript-lang/src/compiler/ternary.rs b/silverscript-lang/src/compiler/ternary.rs index 51e2b98d..56d97fdd 100644 --- a/silverscript-lang/src/compiler/ternary.rs +++ b/silverscript-lang/src/compiler/ternary.rs @@ -131,7 +131,7 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { name_span: *name_span, }); } - Statement::StateFunctionCallAssign { bindings, name, args, span, name_span, target_struct } => { + Statement::StateFunctionCallAssign { bindings, name, args, span, target_struct_span, name_span, target_struct } => { let (prelude, args) = self.lower_exprs(args, types)?; lowered.extend(prelude); for binding in bindings { @@ -142,11 +142,12 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { name: name.clone(), args, span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, target_struct: target_struct.clone(), }); } - Statement::StructDestructure { struct_name, bindings, expr, span } => { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => { let (prelude, expr) = self.lower_expr(expr, None, types)?; lowered.extend(prelude); for binding in bindings { @@ -157,6 +158,7 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { bindings: bindings.clone(), expr, span: *span, + struct_name_span: *struct_name_span, }); } Statement::FunctionCall { name, args, span, name_span } => { @@ -336,7 +338,7 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { Ok((prelude, Expr::new(ExprKind::Identifier(result_name), span))) } - ExprKind::Array { type_ref, values } => { + ExprKind::Array { type_ref, values, type_span } => { let element_type = expected.or(Some(type_ref)).and_then(TypeRef::array_element_type); let mut prelude = Vec::new(); let mut lowered_values = Vec::with_capacity(values.len()); @@ -345,7 +347,10 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { prelude.extend(more_prelude); lowered_values.push(value); } - Ok((prelude, Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values: lowered_values }, span))) + Ok(( + prelude, + Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values: lowered_values, type_span: *type_span }, span), + )) } ExprKind::Call { name, args, name_span } => { let (prelude, args) = self.lower_exprs(args, types)?; @@ -482,7 +487,7 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { } }; let values = (0..len).map(|_| self.default_expr(&element_type, span)).collect::, CompilerError>>()?; - return Ok(Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values }, span)); + return Ok(Expr::new(ExprKind::Array { type_ref: type_ref.clone(), values, type_span: span::Span::default() }, span)); } let kind = match &type_ref.base { @@ -496,7 +501,11 @@ impl<'a, 'i> TernaryLowerer<'a, 'i> { .base .fixed_byte_sequence_len() .ok_or_else(|| CompilerError::Unsupported(format!("cannot create default {}", type_ref.type_name())))?; - ExprKind::Array { type_ref: type_ref.clone(), values: (0..len).map(|_| Expr::new(ExprKind::Byte(0), span)).collect() } + ExprKind::Array { + type_ref: type_ref.clone(), + values: (0..len).map(|_| Expr::new(ExprKind::Byte(0), span)).collect(), + type_span: span::Span::default(), + } } TypeBase::Custom(name) => { let item = self diff --git a/silverscript-lang/src/compiler/type_check.rs b/silverscript-lang/src/compiler/type_check.rs index 3707c0ed..90101b1e 100644 --- a/silverscript-lang/src/compiler/type_check.rs +++ b/silverscript-lang/src/compiler/type_check.rs @@ -46,7 +46,7 @@ pub(super) fn check_expr<'i>( ExprKind::Byte(_) => scalar_type(TypeBase::Byte), ExprKind::String(_) => scalar_type(TypeBase::String), ExprKind::Identifier(name) => ctx.types.get(name).cloned().ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?, - ExprKind::Array { type_ref, values } => check_typed_array_literal(values, type_ref, expected, ctx)?, + ExprKind::Array { type_ref, values, .. } => check_typed_array_literal(values, type_ref, expected, ctx)?, ExprKind::Call { name, args, .. } => check_call(name, args, expected, ctx)? .ok_or_else(|| CompilerError::Unsupported(format!("function '{name}' does not return a value")))?, ExprKind::New { name, args, .. } => check_constructor(name, args, ctx)?, diff --git a/silverscript-lang/src/compiler/validate_output_state.rs b/silverscript-lang/src/compiler/validate_output_state.rs index 07230cf6..4c0f3323 100644 --- a/silverscript-lang/src/compiler/validate_output_state.rs +++ b/silverscript-lang/src/compiler/validate_output_state.rs @@ -227,7 +227,7 @@ fn lower_statement<'i>( name_span: *name_span, }]) } - Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { for binding in bindings { scope.vars.insert(binding.name.clone(), binding.type_ref.clone()); } @@ -237,10 +237,11 @@ fn lower_statement<'i>( name: name.clone(), args: args.clone(), span: *span, + target_struct_span: *target_struct_span, name_span: *name_span, }]) } - Statement::StructDestructure { struct_name, bindings, expr, span } => { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => { for binding in bindings { scope.vars.insert(binding.name.clone(), binding.type_ref.clone()); } @@ -249,6 +250,7 @@ fn lower_statement<'i>( bindings: bindings.clone(), expr: expr.clone(), span: *span, + struct_name_span: *struct_name_span, }]) } Statement::Block { body, span } => { diff --git a/silverscript-lang/tests/ast_spans_tests.rs b/silverscript-lang/tests/ast_spans_tests.rs index c7bed402..2f22288e 100644 --- a/silverscript-lang/tests/ast_spans_tests.rs +++ b/silverscript-lang/tests/ast_spans_tests.rs @@ -21,6 +21,8 @@ struct NameOccurrenceCollector { occurrences: Vec, } +struct SpanResetter; + impl<'i> AstVisitorMut<'i> for NameOccurrenceCollector { fn visit_name(&mut self, name: &mut String, kind: NameKind, span: Span<'i>) { self.occurrences.push(NameOccurrence { name: name.clone(), kind, source: span.as_str().to_string() }); @@ -63,6 +65,12 @@ fn parses_standalone_functions_and_visits_name_spans() { ); } +impl<'i> AstVisitorMut<'i> for SpanResetter { + fn visit_span(&mut self, span: &mut Span<'i>) { + *span = Span::default(); + } +} + #[test] fn composed_expression_spans_remain_syntactically_complete() { let sources = [ @@ -94,6 +102,53 @@ fn redundant_parentheses_do_not_widen_identifier_name_spans() { ); } +#[test] +fn exposes_exact_spans_for_struct_types_and_typed_arrays() { + let source = r#"function inspect() { + CounterState {value: int current} = readInputState(0); + CounterState {value: int copy} = current_state; + CounterState[] values = CounterState[] {CounterState {value: 1}}; + }"#; + let mut function = parse_function_ast(source).expect("standalone function should parse"); + + let Statement::StateFunctionCallAssign { target_struct, target_struct_span, .. } = &function.body[0] else { + panic!("expected a state function call assignment"); + }; + assert_eq!(target_struct, "CounterState"); + assert_eq!(target_struct_span.as_str(), "CounterState"); + + let Statement::StructDestructure { struct_name, struct_name_span, .. } = &function.body[1] else { + panic!("expected a struct destructure assignment"); + }; + assert_eq!(struct_name, "CounterState"); + assert_eq!(struct_name_span.as_str(), "CounterState"); + + let Statement::VariableDefinition { expr: Some(expr), .. } = &function.body[2] else { + panic!("expected a variable definition with an initializer"); + }; + let ExprKind::Array { type_span, .. } = &expr.kind else { + panic!("expected a typed array expression"); + }; + assert_eq!(type_span.as_str(), "CounterState[]"); + + visit_function_mut(&mut SpanResetter, &mut function); + let Statement::StateFunctionCallAssign { target_struct_span, .. } = &function.body[0] else { + unreachable!("statement shape was already checked"); + }; + let Statement::StructDestructure { struct_name_span, .. } = &function.body[1] else { + unreachable!("statement shape was already checked"); + }; + let Statement::VariableDefinition { expr: Some(expr), .. } = &function.body[2] else { + unreachable!("statement shape was already checked"); + }; + let ExprKind::Array { type_span, .. } = &expr.kind else { + unreachable!("expression shape was already checked"); + }; + assert!(target_struct_span.as_str().is_empty()); + assert!(struct_name_span.as_str().is_empty()); + assert!(type_span.as_str().is_empty()); +} + #[test] fn populates_contract_function_and_statement_spans() { let source = r#" diff --git a/silverscript-lang/tests/parser_tests.rs b/silverscript-lang/tests/parser_tests.rs index dfabbb45..04ba6ac5 100644 --- a/silverscript-lang/tests/parser_tests.rs +++ b/silverscript-lang/tests/parser_tests.rs @@ -99,7 +99,7 @@ fn scalar_byte_cast_remains_scalar_in_the_ast() { #[test] fn try_from_expr_vec_infers_a_fixed_array_type() { let expr = Expr::try_from(vec![Expr::int(1), Expr::int(2)]).expect("homogeneous array type should be inferred"); - let ExprKind::Array { type_ref, values } = expr.kind else { + let ExprKind::Array { type_ref, values, .. } = expr.kind else { panic!("expected an array expression"); }; @@ -135,7 +135,7 @@ fn array_constructor_resolves_inferred_dimension_to_fixed() { #[test] fn try_from_nested_byte_vec_requires_equal_nonempty_elements() { let expr = Expr::try_from(vec![vec![1u8, 2], vec![3, 4]]).expect("uniform nested byte array should be inferred"); - let ExprKind::Array { type_ref, values } = expr.kind else { + let ExprKind::Array { type_ref, values, .. } = expr.kind else { panic!("expected an array expression"); }; @@ -253,7 +253,7 @@ fn typed_array_literal_stores_its_declared_type_on_the_array_expr() { let Statement::VariableDefinition { expr: Some(expr), .. } = &contract.functions[0].body[0] else { panic!("expected a variable definition with an initializer"); }; - let ExprKind::Array { type_ref, values } = &expr.kind else { + let ExprKind::Array { type_ref, values, .. } = &expr.kind else { panic!("expected a typed array expression"); }; @@ -311,7 +311,7 @@ fn byte_array_hex_cast_becomes_a_typed_array_expr() { let Statement::VariableDefinition { expr: Some(expr), .. } = &contract.functions[0].body[0] else { panic!("expected a variable definition with an initializer"); }; - let ExprKind::Array { type_ref, values } = &expr.kind else { + let ExprKind::Array { type_ref, values, .. } = &expr.kind else { panic!("expected the hex cast to lower directly to a typed array expression"); }; @@ -333,7 +333,7 @@ fn fixed_byte_sequence_hex_casts_contain_fixed_byte_array_exprs() { panic!("expected a scalar cast call"); }; assert_eq!(name, type_name); - let [Expr { kind: ExprKind::Array { type_ref, values }, .. }] = args.as_slice() else { + let [Expr { kind: ExprKind::Array { type_ref, values, .. }, .. }] = args.as_slice() else { panic!("expected a single typed byte-array argument"); }; assert_eq!(type_ref, &parse_type_ref(&format!("byte[{size}]")).unwrap()); From 345e74f62b494531079d2686bf51e75fb6541526 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 27 Aug 2026 22:35:26 +0300 Subject: [PATCH 3/7] add standalone statement ast parsing Expose a parser entry point for exactly one Sil statement without wrapping it in a synthetic function. Returned nodes retain spans into the caller's original source. Cover required terminators, surrounding comments, supported statement shapes, and rejection of incomplete or multiple statements. --- silverscript-lang/src/ast/mod.rs | 10 +++++- silverscript-lang/src/parser.rs | 5 +++ silverscript-lang/src/silverscript.pest | 1 + silverscript-lang/tests/ast_spans_tests.rs | 40 +++++++++++++++++++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/silverscript-lang/src/ast/mod.rs b/silverscript-lang/src/ast/mod.rs index 16423a6a..a378db28 100644 --- a/silverscript-lang/src/ast/mod.rs +++ b/silverscript-lang/src/ast/mod.rs @@ -8,7 +8,7 @@ use crate::checked_arithmetic::{checked_mul, checked_pow}; use crate::errors::CompilerError; use crate::parser::{ Rule, parse_expression as parse_expression_rule, parse_function as parse_function_rule, parse_source_file, - parse_type_name as parse_type_name_rule, + parse_statement as parse_statement_rule, parse_type_name as parse_type_name_rule, }; pub use crate::span::{Span, SpanUtils}; @@ -1495,6 +1495,14 @@ pub fn parse_function_ast<'i>(source: &'i str) -> Result, Compil parse_function_definition(function_pair) } +/// Parses exactly one standalone statement. +pub fn parse_statement_ast<'i>(source: &'i str) -> Result, CompilerError> { + let mut pairs = parse_statement_rule(source)?; + let source_pair = pairs.next().ok_or_else(|| CompilerError::Unsupported("empty statement source".to_string()))?; + let statement_pair = source_pair.into_inner().next().ok_or_else(|| CompilerError::Unsupported("no statement".to_string()))?; + parse_statement(statement_pair) +} + pub fn parse_expression_ast<'i>(source: &'i str) -> Result, CompilerError> { let mut pairs = parse_expression_rule(source)?; let expr_pair = pairs.next().ok_or_else(|| CompilerError::Unsupported("empty expression".to_string()))?; diff --git a/silverscript-lang/src/parser.rs b/silverscript-lang/src/parser.rs index 007d3eb5..3508d8ab 100644 --- a/silverscript-lang/src/parser.rs +++ b/silverscript-lang/src/parser.rs @@ -18,6 +18,11 @@ pub fn parse_function(input: &str) -> Result, ParseDiagnostic> { SilverScriptParser::parse(Rule::function_source, input).map_err(|err| crate::diagnostic::interpret_parse_error(input, &err)) } +pub fn parse_statement(input: &str) -> Result, ParseDiagnostic> { + pest::set_error_detail(true); + SilverScriptParser::parse(Rule::statement_source, input).map_err(|err| crate::diagnostic::interpret_parse_error(input, &err)) +} + pub fn parse_expression(input: &str) -> Result, ParseDiagnostic> { pest::set_error_detail(true); SilverScriptParser::parse(Rule::expression, input).map_err(|err| crate::diagnostic::interpret_parse_error(input, &err)) diff --git a/silverscript-lang/src/silverscript.pest b/silverscript-lang/src/silverscript.pest index f4b63cc2..d4283316 100644 --- a/silverscript-lang/src/silverscript.pest +++ b/silverscript-lang/src/silverscript.pest @@ -1,5 +1,6 @@ source_file = { SOI ~ pragma_directive? ~ contract_definition ~ EOI } function_source = { SOI ~ function_definition ~ EOI } +statement_source = { SOI ~ statement ~ EOI } pragma_directive = { "pragma" ~ pragma_name ~ pragma_value ~ ";" } pragma_name = { "silverscript" } diff --git a/silverscript-lang/tests/ast_spans_tests.rs b/silverscript-lang/tests/ast_spans_tests.rs index 2f22288e..0e09834a 100644 --- a/silverscript-lang/tests/ast_spans_tests.rs +++ b/silverscript-lang/tests/ast_spans_tests.rs @@ -1,5 +1,5 @@ use silverscript_lang::ast::visit::{AstVisitorMut, NameKind, visit_function_mut}; -use silverscript_lang::ast::{ExprKind, Statement, parse_contract_ast, parse_expression_ast, parse_function_ast}; +use silverscript_lang::ast::{ExprKind, Statement, parse_contract_ast, parse_expression_ast, parse_function_ast, parse_statement_ast}; use silverscript_lang::span::Span; fn assert_span_text(source: &str, actual: &str, expected: &str) { @@ -65,6 +65,44 @@ fn parses_standalone_functions_and_visits_name_spans() { ); } +#[test] +fn parses_standalone_statements_with_original_source_spans() { + let source = " /* before */ value = (signed(value) + signed(step)) as byte; // after\n"; + let statement = parse_statement_ast(source).expect("standalone statement should parse"); + + let Statement::Assign { name, expr, span, name_span } = statement else { + panic!("expected an assignment"); + }; + assert_eq!(name, "value"); + assert_eq!(span.as_str(), "value = (signed(value) + signed(step)) as byte;"); + assert_eq!(name_span.as_str(), "value"); + assert_eq!(expr.span.as_str(), "(signed(value) + signed(step)) as byte"); + assert_eq!(span.get_input(), source); + assert_eq!(name_span.get_input(), source); + assert_eq!(expr.span.get_input(), source); +} + +#[test] +fn parses_supported_standalone_statement_shapes() { + let sources = [ + "int value = 1;", + "CounterState { value: int current } = readInputState(0);", + "{ int value = 1; value = 2; }", + "if (ready) { value = 1; } else value = 2;", + ]; + + for source in sources { + parse_statement_ast(source).unwrap_or_else(|err| panic!("`{source}` should parse as one statement: {err}")); + } +} + +#[test] +fn rejects_incomplete_or_multiple_standalone_statements() { + for source in ["", "value = 1", "value = 1; other = 2;", "value + 1"] { + assert!(parse_statement_ast(source).is_err(), "`{source}` must not parse as exactly one statement"); + } +} + impl<'i> AstVisitorMut<'i> for SpanResetter { fn visit_span(&mut self, span: &mut Span<'i>) { *span = Span::default(); From 44c8aa996b0e7b734dd0bcf4127060ea4082065e Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Fri, 28 Aug 2026 02:05:12 +0300 Subject: [PATCH 4/7] visit authored types through the mutable ast visitor Add a uniform visit_type callback across declarations, bindings, typed arrays, and struct-oriented syntax. String-backed struct references are exposed as temporary scalar custom TypeRefs, while their source spans remain anchored at the authored base name. Complete contract traversal through structs and classify struct declaration names. Cover all type-bearing AST shapes, leading trivia, span mutation order, and nested expression traversal. --- silverscript-lang/src/ast/visit.rs | 75 ++++++++++- silverscript-lang/tests/ast_spans_tests.rs | 143 ++++++++++++++++++++- 2 files changed, 210 insertions(+), 8 deletions(-) diff --git a/silverscript-lang/src/ast/visit.rs b/silverscript-lang/src/ast/visit.rs index ca96fa6a..4b73f6bc 100644 --- a/silverscript-lang/src/ast/visit.rs +++ b/silverscript-lang/src/ast/visit.rs @@ -1,12 +1,23 @@ use super::{ ConstantAst, ContractAst, ContractFieldAst, Expr, ExprKind, FunctionAst, FunctionAttributeArgAst, FunctionAttributeAst, ParamAst, - Statement, StructBindingAst, + Statement, StructAst, StructBindingAst, StructFieldAst, TypeBase, TypeRef, }; use crate::span::Span; +fn visit_type_ref<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, type_ref: &TypeRef, span: Span<'i>) { + visitor.visit_type(type_ref, span); +} + +fn visit_struct_type_name<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, name: &str, span: Span<'i>) { + let type_ref = TypeRef { base: TypeBase::Custom(name.to_string()), array_dims: Vec::new() }; + visitor.visit_type(&type_ref, span); +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NameKind { Contract, + Struct, + StructField, ContractField, Constant, Function, @@ -25,6 +36,14 @@ pub enum NameKind { pub trait AstVisitorMut<'i> { /// Visits a classified name together with its exact source span. fn visit_name(&mut self, _name: &mut String, _kind: NameKind, _span: Span<'i>) {} + /// Visits a classified type. For parsed ASTs, its base type name starts at + /// `span.start()`. + fn visit_type(&mut self, _type_ref: &TypeRef, _span: Span<'i>) {} + /// Visits a stored source span without classifying what it belongs to. + /// + /// The span is mutable so visitors can relocate or reset every span reached + /// by the walker, for example after embedding a parsed fragment at a source + /// offset. Synthetic AST nodes may carry an empty `Span::default()` value. fn visit_span(&mut self, _span: &mut Span<'i>) {} fn visit_contract(&mut self, contract: &mut ContractAst<'i>) { @@ -35,6 +54,14 @@ pub trait AstVisitorMut<'i> { walk_contract_field_mut(self, field); } + fn visit_struct(&mut self, item: &mut StructAst<'i>) { + walk_struct_mut(self, item); + } + + fn visit_struct_field(&mut self, field: &mut StructFieldAst<'i>) { + walk_struct_field_mut(self, field); + } + fn visit_constant(&mut self, constant: &mut ConstantAst<'i>) { walk_constant_mut(self, constant); } @@ -83,6 +110,9 @@ pub fn walk_contract_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, con for param in &mut contract.params { visitor.visit_param(param); } + for item in &mut contract.structs { + visitor.visit_struct(item); + } for field in &mut contract.fields { visitor.visit_contract_field(field); } @@ -94,7 +124,25 @@ pub fn walk_contract_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, con } } +pub fn walk_struct_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, item: &mut StructAst<'i>) { + visitor.visit_name(&mut item.name, NameKind::Struct, item.name_span); + visitor.visit_span(&mut item.span); + visitor.visit_span(&mut item.name_span); + for field in &mut item.fields { + visitor.visit_struct_field(field); + } +} + +pub fn walk_struct_field_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, field: &mut StructFieldAst<'i>) { + visit_type_ref(visitor, &field.type_ref, field.type_span); + visitor.visit_name(&mut field.name, NameKind::StructField, field.name_span); + visitor.visit_span(&mut field.span); + visitor.visit_span(&mut field.type_span); + visitor.visit_span(&mut field.name_span); +} + pub fn walk_contract_field_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, field: &mut ContractFieldAst<'i>) { + visit_type_ref(visitor, &field.type_ref, field.type_span); visitor.visit_name(&mut field.name, NameKind::ContractField, field.name_span); visitor.visit_span(&mut field.span); visitor.visit_span(&mut field.type_span); @@ -103,6 +151,7 @@ pub fn walk_contract_field_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut } pub fn walk_constant_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, constant: &mut ConstantAst<'i>) { + visit_type_ref(visitor, &constant.type_ref, constant.type_span); visitor.visit_name(&mut constant.name, NameKind::Constant, constant.name_span); visitor.visit_span(&mut constant.span); visitor.visit_span(&mut constant.type_span); @@ -115,6 +164,9 @@ pub fn walk_function_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, fun visitor.visit_span(&mut function.span); visitor.visit_span(&mut function.name_span); visitor.visit_span(&mut function.body_span); + for (type_ref, span) in function.return_types.iter().zip(function.return_type_spans.iter().copied()) { + visit_type_ref(visitor, type_ref, span); + } for span in &mut function.return_type_spans { visitor.visit_span(span); } @@ -154,6 +206,7 @@ pub fn walk_param_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, param: } fn walk_param_with_kind_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, param: &mut ParamAst<'i>, kind: NameKind) { + visit_type_ref(visitor, ¶m.type_ref, param.type_span); visitor.visit_name(&mut param.name, kind, param.name_span); visitor.visit_span(&mut param.span); visitor.visit_span(&mut param.type_span); @@ -161,6 +214,7 @@ fn walk_param_with_kind_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, } pub fn walk_state_binding_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, binding: &mut StructBindingAst<'i>) { + visit_type_ref(visitor, &binding.type_ref, binding.type_span); visitor.visit_name(&mut binding.field_name, NameKind::StateField, binding.field_span); visitor.visit_name(&mut binding.name, NameKind::StateBinding, binding.name_span); visitor.visit_span(&mut binding.span); @@ -171,7 +225,8 @@ pub fn walk_state_binding_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, statement: &mut Statement<'i>) { match statement { - Statement::VariableDefinition { name, expr, span, type_span, modifier_spans, name_span, .. } => { + Statement::VariableDefinition { type_ref, name, expr, span, type_span, modifier_spans, name_span, .. } => { + visit_type_ref(visitor, type_ref, *type_span); visitor.visit_name(name, NameKind::LocalBinding, *name_span); visitor.visit_span(span); visitor.visit_span(type_span); @@ -185,7 +240,9 @@ pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, st } Statement::TupleAssignment { left_name, + left_type_ref, right_name, + right_type_ref, expr, span, left_type_span, @@ -194,6 +251,8 @@ pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, st right_name_span, .. } => { + visit_type_ref(visitor, left_type_ref, *left_type_span); + visit_type_ref(visitor, right_type_ref, *right_type_span); visitor.visit_name(left_name, NameKind::LocalBinding, *left_name_span); visitor.visit_name(right_name, NameKind::LocalBinding, *right_name_span); visitor.visit_span(span); @@ -222,7 +281,8 @@ pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, st visitor.visit_expr(arg); } } - Statement::StateFunctionCallAssign { target_struct: _, bindings, name, args, span, target_struct_span, name_span } => { + Statement::StateFunctionCallAssign { target_struct, bindings, name, args, span, target_struct_span, name_span } => { + visit_struct_type_name(visitor, target_struct, *target_struct_span); visitor.visit_name(name, NameKind::CallTarget, *name_span); visitor.visit_span(span); visitor.visit_span(target_struct_span); @@ -234,7 +294,8 @@ pub fn walk_statement_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, st visitor.visit_expr(arg); } } - Statement::StructDestructure { bindings, expr, span, struct_name_span, .. } => { + Statement::StructDestructure { struct_name, bindings, expr, span, struct_name_span } => { + visit_struct_type_name(visitor, struct_name, *struct_name_span); visitor.visit_span(span); visitor.visit_span(struct_name_span); for binding in bindings { @@ -319,7 +380,8 @@ pub fn walk_expr_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, expr: & visitor.visit_span(&mut expr.span); match &mut expr.kind { ExprKind::Identifier(name) => visitor.visit_name(name, NameKind::IdentifierExpr, expr_span), - ExprKind::Array { values: items, type_span, .. } => { + ExprKind::Array { type_ref, values: items, type_span } => { + visit_type_ref(visitor, type_ref, *type_span); visitor.visit_span(type_span); for item in items { visitor.visit_expr(item); @@ -374,7 +436,8 @@ pub fn walk_expr_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, expr: & visitor.visit_span(field_span); visitor.visit_expr(index); } - ExprKind::StructLiteral { fields, name_span, .. } => { + ExprKind::StructLiteral { name, fields, name_span } => { + visit_struct_type_name(visitor, name, *name_span); visitor.visit_span(name_span); for field in fields { visitor.visit_name(&mut field.name, NameKind::StateField, field.name_span); diff --git a/silverscript-lang/tests/ast_spans_tests.rs b/silverscript-lang/tests/ast_spans_tests.rs index 0e09834a..0a670064 100644 --- a/silverscript-lang/tests/ast_spans_tests.rs +++ b/silverscript-lang/tests/ast_spans_tests.rs @@ -1,5 +1,7 @@ -use silverscript_lang::ast::visit::{AstVisitorMut, NameKind, visit_function_mut}; -use silverscript_lang::ast::{ExprKind, Statement, parse_contract_ast, parse_expression_ast, parse_function_ast, parse_statement_ast}; +use silverscript_lang::ast::visit::{AstVisitorMut, NameKind, visit_contract_mut, visit_function_mut}; +use silverscript_lang::ast::{ + ExprKind, Statement, TypeBase, parse_contract_ast, parse_expression_ast, parse_function_ast, parse_statement_ast, +}; use silverscript_lang::span::Span; fn assert_span_text(source: &str, actual: &str, expected: &str) { @@ -23,6 +25,18 @@ struct NameOccurrenceCollector { struct SpanResetter; +#[derive(Debug, PartialEq, Eq)] +struct TypeOccurrence { + type_name: String, + base_name: String, + span_prefix: String, +} + +#[derive(Default)] +struct TypeOccurrenceCollector { + occurrences: Vec, +} + impl<'i> AstVisitorMut<'i> for NameOccurrenceCollector { fn visit_name(&mut self, name: &mut String, kind: NameKind, span: Span<'i>) { self.occurrences.push(NameOccurrence { name: name.clone(), kind, source: span.as_str().to_string() }); @@ -109,6 +123,24 @@ impl<'i> AstVisitorMut<'i> for SpanResetter { } } +impl<'i> AstVisitorMut<'i> for TypeOccurrenceCollector { + fn visit_type(&mut self, type_ref: &silverscript_lang::ast::TypeRef, span: Span<'i>) { + let TypeBase::Custom(base_name) = &type_ref.base else { + panic!("fixture only contains custom type sites"); + }; + let span_prefix = span + .get_input() + .get(span.start()..span.start() + base_name.len()) + .expect("custom type name should fit within the source") + .to_string(); + self.occurrences.push(TypeOccurrence { type_name: type_ref.type_name(), base_name: base_name.to_string(), span_prefix }); + } + + fn visit_span(&mut self, span: &mut Span<'i>) { + *span = Span::default(); + } +} + #[test] fn composed_expression_spans_remain_syntactically_complete() { let sources = [ @@ -187,6 +219,113 @@ fn exposes_exact_spans_for_struct_types_and_typed_arrays() { assert!(type_span.as_str().is_empty()); } +#[test] +fn type_spans_start_at_the_base_type_after_leading_trivia() { + let source = r#"function inspect( + /* before scalar */ ScalarType /* before name */ scalar, + /* before array */ ArrayType /* before suffix */ [] /* before name */ values + ) {}"#; + let function = parse_function_ast(source).expect("type-span fixture should parse"); + + for param in &function.params { + let TypeBase::Custom(name) = ¶m.type_ref.base else { + panic!("fixture should use custom types"); + }; + let start = param.type_span.start(); + let authored_name = source.get(start..start + name.len()).expect("type name should fit within its source"); + + assert_eq!(authored_name, name); + } +} + +#[test] +fn contract_traversal_classifies_struct_names() { + let source = "contract Container() { struct Item { int value; } }"; + let mut contract = parse_contract_ast(source).expect("struct-name fixture should parse"); + let mut collector = NameOccurrenceCollector::default(); + + visit_contract_mut(&mut collector, &mut contract); + + assert_eq!( + collector.occurrences, + [("Container", NameKind::Contract), ("Item", NameKind::Struct), ("value", NameKind::StructField),] + .into_iter() + .map(|(name, kind)| NameOccurrence { name: name.to_string(), kind, source: name.to_string() }) + .collect::>() + ); +} + +#[test] +fn visits_every_classified_type_site_before_mutating_its_span() { + let source = r#"contract Inspect(/* leading */ ContractParam /* separator */ ctor) { + struct Wrapper { + StructFieldType field; + } + + ContractFieldType stored = ContractFieldCtor { count: 0 }; + ConstantType constant initial = ConstantCtor { count: 0 }; + + function inspect(FunctionParam param) : ReturnType { + NestedResult nested = wrap( + flag + ? OuterCtor { items: NestedArray[]{ InnerCtor { count: 1 } } } + : FallbackCtor { count: 0 } + ); + VariableType local = VariableCtor { count: 1 }; + TupleLeft left, TupleRight right = pair(); + (CallBinding output) = identity(local); + StateOwner { count: StateBinding from_call } = readInputState(0); + DestructureOwner { count: DestructureBinding copy } = local; + DeclaredArray[] values = LiteralArray[]{ ArrayElementCtor { count: 1 } }; + return(local); + } + }"#; + let mut contract = parse_contract_ast(source).expect("type-site fixture should parse"); + let mut collector = TypeOccurrenceCollector::default(); + + visit_contract_mut(&mut collector, &mut contract); + + let expected = [ + ("ContractParam", "ContractParam", "ContractParam"), + ("StructFieldType", "StructFieldType", "StructFieldType"), + ("ContractFieldType", "ContractFieldType", "ContractFieldType"), + ("ContractFieldCtor", "ContractFieldCtor", "ContractFieldCtor"), + ("ConstantType", "ConstantType", "ConstantType"), + ("ConstantCtor", "ConstantCtor", "ConstantCtor"), + ("ReturnType", "ReturnType", "ReturnType"), + ("FunctionParam", "FunctionParam", "FunctionParam"), + ("NestedResult", "NestedResult", "NestedResult"), + ("OuterCtor", "OuterCtor", "OuterCtor"), + ("NestedArray[]", "NestedArray", "NestedArray"), + ("InnerCtor", "InnerCtor", "InnerCtor"), + ("FallbackCtor", "FallbackCtor", "FallbackCtor"), + ("VariableType", "VariableType", "VariableType"), + ("VariableCtor", "VariableCtor", "VariableCtor"), + ("TupleLeft", "TupleLeft", "TupleLeft"), + ("TupleRight", "TupleRight", "TupleRight"), + ("CallBinding", "CallBinding", "CallBinding"), + ("StateOwner", "StateOwner", "StateOwner"), + ("StateBinding", "StateBinding", "StateBinding"), + ("DestructureOwner", "DestructureOwner", "DestructureOwner"), + ("DestructureBinding", "DestructureBinding", "DestructureBinding"), + ("DeclaredArray[]", "DeclaredArray", "DeclaredArray"), + ("LiteralArray[]", "LiteralArray", "LiteralArray"), + ("ArrayElementCtor", "ArrayElementCtor", "ArrayElementCtor"), + ]; + assert_eq!( + collector.occurrences, + expected + .into_iter() + .map(|(type_name, base_name, span_prefix)| TypeOccurrence { + type_name: type_name.to_string(), + base_name: base_name.to_string(), + span_prefix: span_prefix.to_string(), + }) + .collect::>() + ); + assert!(contract.structs[0].fields[0].type_span.as_str().is_empty(), "contract traversal must visit struct-field spans"); +} + #[test] fn populates_contract_function_and_statement_spans() { let source = r#" From 6508d188190a99ed2e179a775dc45b0143ac0da4 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Fri, 28 Aug 2026 02:33:30 +0300 Subject: [PATCH 5/7] visit ast elements without source spans Drive return-type and attribute-path traversal from semantic vectors, supplying synthetic spans when deserialized AST metadata is absent. Cover both callbacks with a serialized AST regression. --- silverscript-lang/src/ast/visit.rs | 8 +++-- silverscript-lang/tests/ast_spans_tests.rs | 40 +++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/silverscript-lang/src/ast/visit.rs b/silverscript-lang/src/ast/visit.rs index 4b73f6bc..31d52b6e 100644 --- a/silverscript-lang/src/ast/visit.rs +++ b/silverscript-lang/src/ast/visit.rs @@ -164,7 +164,9 @@ pub fn walk_function_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, fun visitor.visit_span(&mut function.span); visitor.visit_span(&mut function.name_span); visitor.visit_span(&mut function.body_span); - for (type_ref, span) in function.return_types.iter().zip(function.return_type_spans.iter().copied()) { + for (type_ref, span) in + function.return_types.iter().zip(function.return_type_spans.iter().copied().chain(std::iter::repeat(Span::default()))) + { visit_type_ref(visitor, type_ref, span); } for span in &mut function.return_type_spans { @@ -183,7 +185,9 @@ pub fn walk_function_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, fun pub fn walk_function_attribute_mut<'i, V: AstVisitorMut<'i> + ?Sized>(visitor: &mut V, attribute: &mut FunctionAttributeAst<'i>) { visitor.visit_span(&mut attribute.span); - for (segment, span) in attribute.path.iter_mut().zip(attribute.path_spans.iter().copied()) { + for (segment, span) in + attribute.path.iter_mut().zip(attribute.path_spans.iter().copied().chain(std::iter::repeat(Span::default()))) + { visitor.visit_name(segment, NameKind::AttributePathSegment, span); } for span in &mut attribute.path_spans { diff --git a/silverscript-lang/tests/ast_spans_tests.rs b/silverscript-lang/tests/ast_spans_tests.rs index 0a670064..19d10637 100644 --- a/silverscript-lang/tests/ast_spans_tests.rs +++ b/silverscript-lang/tests/ast_spans_tests.rs @@ -1,6 +1,6 @@ use silverscript_lang::ast::visit::{AstVisitorMut, NameKind, visit_contract_mut, visit_function_mut}; use silverscript_lang::ast::{ - ExprKind, Statement, TypeBase, parse_contract_ast, parse_expression_ast, parse_function_ast, parse_statement_ast, + ExprKind, FunctionAst, Statement, TypeBase, parse_contract_ast, parse_expression_ast, parse_function_ast, parse_statement_ast, }; use silverscript_lang::span::Span; @@ -37,6 +37,12 @@ struct TypeOccurrenceCollector { occurrences: Vec, } +#[derive(Default)] +struct SyntheticMetadataCollector { + type_names: Vec<(String, bool)>, + attribute_path_segments: Vec<(String, bool)>, +} + impl<'i> AstVisitorMut<'i> for NameOccurrenceCollector { fn visit_name(&mut self, name: &mut String, kind: NameKind, span: Span<'i>) { self.occurrences.push(NameOccurrence { name: name.clone(), kind, source: span.as_str().to_string() }); @@ -141,6 +147,38 @@ impl<'i> AstVisitorMut<'i> for TypeOccurrenceCollector { } } +impl<'i> AstVisitorMut<'i> for SyntheticMetadataCollector { + fn visit_name(&mut self, name: &mut String, kind: NameKind, span: Span<'i>) { + if kind == NameKind::AttributePathSegment { + self.attribute_path_segments.push((name.clone(), span == Span::default())); + } + } + + fn visit_type(&mut self, type_ref: &silverscript_lang::ast::TypeRef, span: Span<'i>) { + self.type_names.push((type_ref.type_name(), span == Span::default())); + } +} + +#[test] +fn visits_deserialized_function_metadata_without_source_spans() { + let source = "#[covenant.singleton] function inspect() : (LeftResult, RightResult) {}"; + let function = parse_function_ast(source).expect("function metadata fixture should parse"); + let serialized = serde_json::to_string(&function).expect("function AST should serialize"); + let mut function: FunctionAst<'_> = serde_json::from_str(&serialized).expect("function AST should deserialize"); + let mut collector = SyntheticMetadataCollector::default(); + + visit_function_mut(&mut collector, &mut function); + + assert_eq!( + collector.type_names, + [("LeftResult", true), ("RightResult", true)].map(|(name, synthetic)| (name.to_string(), synthetic)) + ); + assert_eq!( + collector.attribute_path_segments, + [("covenant", true), ("singleton", true)].map(|(name, synthetic)| (name.to_string(), synthetic)) + ); +} + #[test] fn composed_expression_spans_remain_syntactically_complete() { let sources = [ From 77c9837f3d160af5ce21803a7a3e41defb78309c Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Fri, 28 Aug 2026 14:32:43 +0300 Subject: [PATCH 6/7] fix ast source formatting --- silverscript-lang/src/ast/mod.rs | 26 ++++- silverscript-lang/tests/ast_format_tests.rs | 100 +++++++++++++++++++- 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/silverscript-lang/src/ast/mod.rs b/silverscript-lang/src/ast/mod.rs index a378db28..cf74ae03 100644 --- a/silverscript-lang/src/ast/mod.rs +++ b/silverscript-lang/src/ast/mod.rs @@ -966,6 +966,10 @@ impl SourceFormatter { } fn write_function(&mut self, function: &FunctionAst<'_>) { + for attribute in &function.attributes { + self.line(&format_function_attribute(attribute)); + } + let mut signature = String::new(); if function.entrypoint { signature.push_str("entry "); @@ -1114,10 +1118,12 @@ impl SourceFormatter { } fn ordered_contract_items<'a, 'i>(contract: &'a ContractAst<'i>) -> Vec> { - let has_real_spans = contract.structs.iter().any(|item| !item.span.is_empty()) - || contract.fields.iter().any(|field| !field.span.is_empty()) - || contract.constants.iter().any(|constant| !constant.span.is_empty()) - || contract.functions.iter().any(|function| !function.span.is_empty()); + // Require all items to be spanned: sorting a mixed AST would move synthetic + // zero-span items before parsed items and could change semantic vector order. + let all_spanned = contract.structs.iter().all(|item| !item.span.is_empty()) + && contract.fields.iter().all(|field| !field.span.is_empty()) + && contract.constants.iter().all(|constant| !constant.span.is_empty()) + && contract.functions.iter().all(|function| !function.span.is_empty()); let mut items = Vec::with_capacity(contract.structs.len() + contract.fields.len() + contract.constants.len() + contract.functions.len()); @@ -1134,13 +1140,23 @@ fn ordered_contract_items<'a, 'i>(contract: &'a ContractAst<'i>) -> Vec) -> String { + let path = attribute.path.join("."); + if attribute.args.is_empty() { + return format!("#[{path}]"); + } + + let args = attribute.args.iter().map(|arg| format!("{} = {}", arg.name, format_expr(&arg.expr))).collect::>().join(", "); + format!("#[{path}({args})]") +} + fn format_params(params: &[ParamAst<'_>]) -> String { params.iter().map(|param| format!("{} {}", param.type_ref.type_name(), param.name)).collect::>().join(", ") } diff --git a/silverscript-lang/tests/ast_format_tests.rs b/silverscript-lang/tests/ast_format_tests.rs index fc76c58a..941f44c8 100644 --- a/silverscript-lang/tests/ast_format_tests.rs +++ b/silverscript-lang/tests/ast_format_tests.rs @@ -1,5 +1,5 @@ -use silverscript_lang::ast::{format_contract_ast, parse_contract_ast}; -use silverscript_lang::compiler::{CompileOptions, compile_contract}; +use silverscript_lang::ast::{Expr, Span, format_contract_ast, parse_contract_ast}; +use silverscript_lang::compiler::{CompileOptions, compile_contract, compile_contract_ast}; fn assert_compiled_formatted_contract_preserves_ast(source: &str, options: CompileOptions) { let ast = parse_contract_ast(source).expect("parse succeeds"); @@ -173,3 +173,99 @@ fn compiled_formatted_contract_preserves_exact_ast_with_state_and_return() { CompileOptions { allow_entrypoint_return: true, ..CompileOptions::default() }, ); } + +#[test] +fn formats_function_attributes_and_preserves_compilation() { + let source = r#"contract Decls(int max_outs) { + #[covenant(binding = auth, from = 1, to = max_outs, mode = verification)] + function spend(int amount) { + require(amount >= 0); + } +} +"#; + + let ast = parse_contract_ast(source).expect("parse succeeds"); + let formatted = format_contract_ast(&ast); + let reparsed = parse_contract_ast(&formatted).expect("formatted attributes parse"); + + assert!(formatted.contains("#[covenant(binding = auth, from = 1, to = max_outs, mode = verification)]")); + assert_eq!( + serde_json::to_value(&reparsed).expect("serialize reparsed ast"), + serde_json::to_value(&ast).expect("serialize original ast") + ); + + let args = [Expr::int(3)]; + let from_source = compile_contract(source, &args, CompileOptions::default()).expect("source compiles"); + let direct = compile_contract_ast(&ast, &args, CompileOptions::default()).expect("direct AST compiles"); + let round_tripped = + compile_contract_ast(&reparsed, &args, CompileOptions::default()).expect("formatted and reparsed AST compiles"); + assert_eq!(from_source.bytecode, direct.bytecode); + assert_eq!(from_source.abi, direct.abi); + assert_eq!(from_source.cov_decl_to_abi, direct.cov_decl_to_abi); + assert_eq!(from_source.delegate_entry_abi, direct.delegate_entry_abi); + assert_eq!(from_source.state_layout, direct.state_layout); + assert_eq!(direct.bytecode, round_tripped.bytecode); + assert_eq!(direct.abi, round_tripped.abi); + assert_eq!(direct.cov_decl_to_abi, round_tripped.cov_decl_to_abi); + assert_eq!(direct.delegate_entry_abi, round_tripped.delegate_entry_abi); + assert_eq!(direct.state_layout, round_tripped.state_layout); +} + +#[test] +fn formats_no_arg_and_qualified_function_attributes() { + let source = r#"contract Attributes() { + #[covenant.delegate] + function delegate() { + require(true); + } + + #[covenant.allow(rule = manual_entrypoint_in_leader_contract)] + entry recover() { + require(true); + } +} +"#; + + let ast = parse_contract_ast(source).expect("parse succeeds"); + let formatted = format_contract_ast(&ast); + let reparsed = parse_contract_ast(&formatted).expect("formatted attributes parse"); + + assert!(formatted.contains("#[covenant.delegate]\n")); + assert!(formatted.contains("#[covenant.allow(rule = manual_entrypoint_in_leader_contract)]\n")); + assert_eq!( + serde_json::to_value(&reparsed).expect("serialize reparsed ast"), + serde_json::to_value(&ast).expect("serialize original ast") + ); +} + +#[test] +fn synthetic_items_preserve_ast_vector_order_after_formatting() { + let source = r#"contract Generated() { + int first = 1; + + entry spend() { + require(first == 1); + } +} +"#; + + let mut ast = parse_contract_ast(source).expect("parse succeeds"); + let mut synthetic_field = ast.fields[0].clone(); + synthetic_field.name = "second".to_string(); + synthetic_field.expr = Expr::int(2); + synthetic_field.span = Span::default(); + synthetic_field.type_span = Span::default(); + synthetic_field.name_span = Span::default(); + ast.fields.push(synthetic_field); + + let formatted = format_contract_ast(&ast); + let reparsed = parse_contract_ast(&formatted).expect("formatted mixed-source AST parses"); + assert_eq!(reparsed.fields.iter().map(|field| field.name.as_str()).collect::>(), vec!["first", "second"]); + + let direct = compile_contract_ast(&ast, &[], CompileOptions::default()).expect("direct mixed-source AST compiles"); + let round_tripped = + compile_contract_ast(&reparsed, &[], CompileOptions::default()).expect("formatted and reparsed mixed-source AST compiles"); + assert_eq!(direct.bytecode, round_tripped.bytecode); + assert_eq!(direct.abi, round_tripped.abi); + assert_eq!(direct.state_layout, round_tripped.state_layout); +} From 16c381e34f91bf87e236200e6d34199e2fa71cef Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Sat, 29 Aug 2026 10:20:12 +0300 Subject: [PATCH 7/7] Fix build errors --- debugger/cli/src/main.rs | 2 +- silverscript-lang/tests/ast_format_tests.rs | 25 +++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/debugger/cli/src/main.rs b/debugger/cli/src/main.rs index d5e38cc7..e90cf431 100644 --- a/debugger/cli/src/main.rs +++ b/debugger/cli/src/main.rs @@ -125,7 +125,7 @@ fn expr_to_artifact_value(expr: &Expr<'_>) -> Result { ExprKind::Bool(value) => Ok(ArtifactValue::Bool(*value)), ExprKind::Byte(value) => Ok(ArtifactValue::Byte(*value)), ExprKind::String(value) => Ok(ArtifactValue::Text(value.clone())), - ExprKind::Array { type_ref, values } if matches!(type_ref.base, TypeBase::Byte) && type_ref.array_dims.len() == 1 => { + ExprKind::Array { type_ref, values, .. } if matches!(type_ref.base, TypeBase::Byte) && type_ref.array_dims.len() == 1 => { let bytes = values .iter() .map(|value| match value.kind { diff --git a/silverscript-lang/tests/ast_format_tests.rs b/silverscript-lang/tests/ast_format_tests.rs index 941f44c8..9776135b 100644 --- a/silverscript-lang/tests/ast_format_tests.rs +++ b/silverscript-lang/tests/ast_format_tests.rs @@ -1,5 +1,5 @@ use silverscript_lang::ast::{Expr, Span, format_contract_ast, parse_contract_ast}; -use silverscript_lang::compiler::{CompileOptions, compile_contract, compile_contract_ast}; +use silverscript_lang::compiler::{CompileOptions, compile_contract, compile_contract_ast, sil_abi_artifact_from_compiled}; fn assert_compiled_formatted_contract_preserves_ast(source: &str, options: CompileOptions) { let ast = parse_contract_ast(source).expect("parse succeeds"); @@ -199,16 +199,12 @@ fn formats_function_attributes_and_preserves_compilation() { let direct = compile_contract_ast(&ast, &args, CompileOptions::default()).expect("direct AST compiles"); let round_tripped = compile_contract_ast(&reparsed, &args, CompileOptions::default()).expect("formatted and reparsed AST compiles"); - assert_eq!(from_source.bytecode, direct.bytecode); - assert_eq!(from_source.abi, direct.abi); - assert_eq!(from_source.cov_decl_to_abi, direct.cov_decl_to_abi); - assert_eq!(from_source.delegate_entry_abi, direct.delegate_entry_abi); - assert_eq!(from_source.state_layout, direct.state_layout); - assert_eq!(direct.bytecode, round_tripped.bytecode); - assert_eq!(direct.abi, round_tripped.abi); - assert_eq!(direct.cov_decl_to_abi, round_tripped.cov_decl_to_abi); - assert_eq!(direct.delegate_entry_abi, round_tripped.delegate_entry_abi); - assert_eq!(direct.state_layout, round_tripped.state_layout); + let from_source_artifact = sil_abi_artifact_from_compiled(&from_source, &args).expect("source artifact builds"); + let direct_artifact = sil_abi_artifact_from_compiled(&direct, &args).expect("direct AST artifact builds"); + let round_tripped_artifact = + sil_abi_artifact_from_compiled(&round_tripped, &args).expect("formatted and reparsed AST artifact builds"); + assert_eq!(from_source_artifact, direct_artifact); + assert_eq!(direct_artifact, round_tripped_artifact); } #[test] @@ -265,7 +261,8 @@ fn synthetic_items_preserve_ast_vector_order_after_formatting() { let direct = compile_contract_ast(&ast, &[], CompileOptions::default()).expect("direct mixed-source AST compiles"); let round_tripped = compile_contract_ast(&reparsed, &[], CompileOptions::default()).expect("formatted and reparsed mixed-source AST compiles"); - assert_eq!(direct.bytecode, round_tripped.bytecode); - assert_eq!(direct.abi, round_tripped.abi); - assert_eq!(direct.state_layout, round_tripped.state_layout); + let direct_artifact = sil_abi_artifact_from_compiled(&direct, &[]).expect("direct mixed-source AST artifact builds"); + let round_tripped_artifact = + sil_abi_artifact_from_compiled(&round_tripped, &[]).expect("formatted and reparsed mixed-source AST artifact builds"); + assert_eq!(direct_artifact, round_tripped_artifact); }