From 728c3ba8e52c622079c7b32d2ef07a980d4a5c9e Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:18:54 +0200 Subject: [PATCH 01/12] parser: preserve named proto contract syntax and source metadata Parse the named contract form without changing inherent proto attachments Keep target arguments, visibility and receiver types in the syntax tree with focused diagnostic tests Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- front/parser/src/ast.rs | 4 ++ front/parser/src/ast/cloning.rs | 2 + front/parser/src/ast_output.rs | 7 ++- front/parser/src/parser/functions.rs | 41 +++++++++++++-- front/parser/src/parser/items.rs | 61 ++++++++++++++++++--- front/parser/src/parser/parse.rs | 16 ++++++ front/parser/tests/proto_contracts.rs | 76 +++++++++++++++++++++++++++ 7 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 front/parser/tests/proto_contracts.rs diff --git a/front/parser/src/ast.rs b/front/parser/src/ast.rs index 60814b81..82636a49 100644 --- a/front/parser/src/ast.rs +++ b/front/parser/src/ast.rs @@ -135,8 +135,12 @@ pub struct StructNode { #[derive(Debug, Clone)] pub struct ProtoImplNode { + /// Named contract, or None for the existing inherent proto form. + pub name: Option, + /// The target name includes generic arguments, e.g. Buffer. pub target: String, pub methods: Vec, + pub visibility: Visibility, } #[derive(Debug, Clone)] diff --git a/front/parser/src/ast/cloning.rs b/front/parser/src/ast/cloning.rs index 4d1cde5b..f92dd928 100644 --- a/front/parser/src/ast/cloning.rs +++ b/front/parser/src/ast/cloning.rs @@ -134,8 +134,10 @@ impl Clone for ASTNode { visibility: n.visibility, }), ASTNode::ProtoImpl(n) => ASTNode::ProtoImpl(ProtoImplNode { + name: n.name.clone(), target: n.target.clone(), methods: n.methods.iter().map(|n| function(n, &mut nodes)).collect(), + visibility: n.visibility, }), ASTNode::Statement(n) => ASTNode::Statement(statement(n, &mut nodes)), ASTNode::ExternFunction(n) => ASTNode::ExternFunction(n.clone()), diff --git a/front/parser/src/ast_output.rs b/front/parser/src/ast_output.rs index 2eb955b2..5c194a0a 100644 --- a/front/parser/src/ast_output.rs +++ b/front/parser/src/ast_output.rs @@ -323,7 +323,12 @@ impl Data for ProtoImplNode { fn data(&self, cx: &Context) -> Value { record( "proto_impl", - vec![("target", self.target.data(cx)), ("methods", self.methods.data(cx))], + vec![ + ("name", self.name.data(cx)), + ("target", self.target.data(cx)), + ("methods", self.methods.data(cx)), + ("visibility", self.visibility.data(cx)), + ], ) } } diff --git a/front/parser/src/parser/functions.rs b/front/parser/src/parser/functions.rs index 590bb27e..9a0d8cb3 100644 --- a/front/parser/src/parser/functions.rs +++ b/front/parser/src/parser/functions.rs @@ -16,7 +16,9 @@ //! within generic parameter lists are rejected here; program-wide symbol and //! body type checks remain the semantic verifier's responsibility. -use crate::ast::{ASTNode, ExportAttribute, Expression, FunctionNode, ParameterNode, Visibility}; +use crate::ast::{ + ASTNode, ExportAttribute, Expression, FunctionNode, ParameterNode, Visibility, WaveType, +}; use crate::parser::decl::parse_ffi_header; use crate::parser::ParseError; use lexer::token::TokenType; @@ -72,6 +74,13 @@ pub fn parse_generic_param_names( pub fn parse_parameters( tokens: &mut Peekable>, +) -> Result, ParseError> { + parse_parameters_with_receiver(tokens, None) +} + +fn parse_parameters_with_receiver( + tokens: &mut Peekable>, + receiver_type: Option<&WaveType>, ) -> Result, ParseError> { let anchor = tokens.peek().copied(); let context = "function parameters"; @@ -90,9 +99,17 @@ pub fn parse_parameters( return Err(ParseError::syntax_at(at, format!("duplicate parameter '{name}'")) .with_context(context)); } - crate::expr::expect_token(tokens, anchor, TokenType::Colon, "':'", context)?; skip_ws(tokens); - let param_type = crate::types::parse_type_checked(tokens, "parameter type")?; + let param_type = if name == "self" + && matches!(tokens.peek().map(|t| &t.token_type), Some(TokenType::Comma | TokenType::Rparen)) + && receiver_type.is_some() + { + receiver_type.unwrap().clone() + } else { + crate::expr::expect_token(tokens, anchor, TokenType::Colon, "':'", context)?; + skip_ws(tokens); + crate::types::parse_type_checked(tokens, "parameter type")? + }; skip_ws(tokens); let initial_value = if tokens.peek().is_some_and(|t| t.token_type == TokenType::Equal) { tokens.next(); @@ -130,6 +147,22 @@ pub fn parse_function(tokens: &mut Peekable>) -> Result>, export: Option, +) -> Result { + parse_function_with_export_and_receiver(tokens, export, None) +} + +/// A named proto may use an untyped `self` receiver, whose type is its target. +pub fn parse_function_with_receiver( + tokens: &mut Peekable>, + receiver_type: WaveType, +) -> Result { + parse_function_with_export_and_receiver(tokens, None, Some(receiver_type)) +} + +fn parse_function_with_export_and_receiver( + tokens: &mut Peekable>, + export: Option, + receiver_type: Option, ) -> Result { let anchor = tokens.peek().copied(); let invalid = |token: Option<&Token>| { @@ -175,7 +208,7 @@ pub fn parse_function_with_export( } tokens.next(); // consume '(' - let parameters = parse_parameters(tokens)?; + let parameters = parse_parameters_with_receiver(tokens, receiver_type.as_ref())?; skip_ws(tokens); let mut return_type_span = None; diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index ec8c905b..6a80bf6e 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -19,7 +19,9 @@ use crate::ast::{ ASTNode, ImportNode, ProtoImplNode, StatementNode, StructNode, Visibility, WaveType, }; -use crate::parser::functions::{parse_function, parse_generic_param_names}; +use crate::parser::functions::{ + parse_function, parse_function_with_receiver, parse_generic_param_names, +}; use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; @@ -101,18 +103,52 @@ pub fn parse_import(tokens: &mut Peekable>) -> Result>) -> Result { let anchor = tokens.peek().copied(); - let context = "proto method attachment"; - let target_struct = crate::expr::identifier(tokens, anchor, context)?; - crate::expr::expect_token(tokens, anchor, TokenType::Lbrace, "'{'", context)?; + let context = "proto declaration"; + skip_ws(tokens); + let first = crate::expr::identifier(tokens, anchor, "proto name")?; + skip_ws(tokens); + + // The colon distinguishes a named contract from the existing inherent + // method-attachment syntax, without changing how inherent targets lower. + let (name, target) = if tokens.peek().is_some_and(|t| t.token_type == TokenType::Colon) { + tokens.next(); + skip_ws(tokens); + let target_anchor = tokens.peek().copied(); + let target_type = crate::types::parse_type_checked(tokens, "proto target type")?; + let WaveType::Struct(target) = target_type else { + return Err(ParseError::expected_at( + target_anchor, + anchor, + "named target type", + "proto target type", + )); + }; + (Some(first), target) + } else { + (None, first) + }; + skip_ws(tokens); + crate::expr::expect_token( + tokens, + anchor, + TokenType::Lbrace, + "'{' or ':'", + context, + )?; let mut methods = Vec::new(); loop { + skip_ws(tokens); match tokens.peek().map(|token| &token.token_type) { Some(TokenType::Rbrace) => { tokens.next(); break; }, Some(TokenType::Fun | TokenType::Async) => { - let ASTNode::Function(mut method) = parse_function(tokens)? else { + let ASTNode::Function(mut method) = (if name.is_some() { + parse_function_with_receiver(tokens, WaveType::Struct(target.clone()))? + } else { + parse_function(tokens)? + }) else { unreachable!("function parser returns a function node"); }; if method.return_type.is_none() { @@ -120,6 +156,14 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Result { + return Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + "'}'", + context, + )); + }, _ => { return Err(ParseError::expected_at( tokens.peek().copied(), @@ -130,7 +174,12 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Result>) -> Result { diff --git a/front/parser/src/parser/parse.rs b/front/parser/src/parser/parse.rs index 64ffe6c7..f4a405c6 100644 --- a/front/parser/src/parser/parse.rs +++ b/front/parser/src/parser/parse.rs @@ -364,6 +364,10 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result, ParseError> { iter.next(); Some(parse_struct(&mut iter)?) }, + Some(TokenType::Proto) => { + iter.next(); + Some(parse_proto(&mut iter)?) + }, Some(TokenType::Type) => { iter.next(); Some(parse_type_alias(&mut iter)?) @@ -398,6 +402,7 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result, ParseError> { "pub import", "pub export(c) fun", "pub struct", + "pub proto", "pub enum", "pub variant", "pub type", @@ -434,6 +439,17 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result, ParseError> { function.visibility = Visibility::Public; }, ASTNode::Struct(structure) => structure.visibility = Visibility::Public, + ASTNode::ProtoImpl(implementation) => { + if implementation.name.is_none() { + return Err(ParseError::syntax_at( + Some(&anchor), + "only named proto contracts may be public", + ) + .with_context("public declaration") + .with_help("use `proto Target { ... }` for inherent methods")); + } + implementation.visibility = Visibility::Public; + }, ASTNode::TypeAlias(alias) => alias.visibility = Visibility::Public, ASTNode::Enum(enumeration) => enumeration.visibility = Visibility::Public, ASTNode::Variant(variant) => variant.visibility = Visibility::Public, diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs new file mode 100644 index 00000000..4719e816 --- /dev/null +++ b/front/parser/tests/proto_contracts.rs @@ -0,0 +1,76 @@ +//! Named proto syntax is parsed without changing inherent method attachments. +use lexer::Lexer; +use parser::{ast::{ASTNode, Visibility, WaveType}, parse_syntax_with_spans}; + +fn parse(source: &str) -> Vec { + let tokens = Lexer::new_with_file(source, "proto.wave").tokenize().unwrap(); + parse_syntax_with_spans(&tokens).unwrap() +} + +fn failure(source: &str, expected: &str) { + let tokens = Lexer::new_with_file(source, "proto.wave").tokenize().unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert!( + error.expected().iter().any(|item| item.contains(expected)) + || error.message().contains(expected), + "{source}: {error:?}" + ); + assert!(error.span().is_some(), "{source}: missing source span"); +} + +#[test] +fn named_and_inherent_proto_forms_remain_distinct() { + let source = "proto Printable : Buffer { fun print(self) { return; } } + proto Buffer { fun clear(self: ptr) {} }"; + let nodes = parse(source); + assert_eq!(nodes.len(), 2); + let ASTNode::ProtoImpl(named) = nodes[0].unspanned() else { panic!("named proto") }; + assert_eq!(named.name.as_deref(), Some("Printable")); + assert_eq!(named.target, "Buffer"); + assert_eq!(named.visibility, Visibility::Private); + assert_eq!(named.methods.len(), 1); + assert_eq!(named.methods[0].parameters[0].name, "self"); + assert_eq!(named.methods[0].parameters[0].param_type, WaveType::Struct("Buffer".into())); + assert!(!named.methods[0].body.is_empty()); + + let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { panic!("inherent proto") }; + assert_eq!(inherent.name, None); + assert_eq!(inherent.target, "Buffer"); + assert_eq!(inherent.methods[0].parameters[0].param_type, + WaveType::Pointer(Box::new(WaveType::Struct("Buffer".into())))); + assert!(nodes[0].clone().span().is_some()); +} + +#[test] +fn public_named_contract_preserves_target_generic_arguments_and_visibility() { + let nodes = parse("pub proto Printable: Buffer { + fun print(self) {} + fun copy(self: ptr>) {} + }"); + let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { panic!("proto") }; + assert_eq!(contract.name.as_deref(), Some("Printable")); + assert_eq!(contract.target, "Buffer"); + assert_eq!(contract.visibility, Visibility::Public); + assert_eq!(contract.methods.len(), 2); + assert_eq!(contract.methods[0].parameters[0].param_type, + WaveType::Struct("Buffer".into())); + let cloned = nodes[0].clone(); + let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { panic!("clone") }; + assert_eq!(copy.name, contract.name); + assert_eq!(copy.visibility, Visibility::Public); +} + +#[test] +fn malformed_named_proto_syntax_has_located_diagnostics() { + for (source, expected) in [ + ("proto : Buffer {}", "identifier"), + ("proto Printable Buffer {}", "'{' or ':'"), + ("proto Printable: {}", "type"), + ("proto Printable: Buffer", "'{' or ':'"), + ("proto Printable: Buffer {", "'}'"), + ("proto Printable: Buffer { fun print(self) }", "function"), + ("pub proto Buffer {}", "only named proto"), + ] { + failure(source, expected); + } +} From e94f788355c8a8bdb509c3131060950ff092a969 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:21:15 +0200 Subject: [PATCH 02/12] test(parser): tighten named proto diagnostics and receiver parsing Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- front/parser/src/parser/functions.rs | 14 +++++++++----- front/parser/src/parser/items.rs | 14 +++++--------- front/parser/tests/proto_contracts.rs | 24 +++++++++++++++++------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/front/parser/src/parser/functions.rs b/front/parser/src/parser/functions.rs index 9a0d8cb3..40d61014 100644 --- a/front/parser/src/parser/functions.rs +++ b/front/parser/src/parser/functions.rs @@ -100,11 +100,15 @@ fn parse_parameters_with_receiver( .with_context(context)); } skip_ws(tokens); - let param_type = if name == "self" - && matches!(tokens.peek().map(|t| &t.token_type), Some(TokenType::Comma | TokenType::Rparen)) - && receiver_type.is_some() - { - receiver_type.unwrap().clone() + let receiver = receiver_type.filter(|_| { + name == "self" + && matches!( + tokens.peek().map(|t| &t.token_type), + Some(TokenType::Comma | TokenType::Rparen) + ) + }); + let param_type = if let Some(receiver) = receiver { + receiver.clone() } else { crate::expr::expect_token(tokens, anchor, TokenType::Colon, "':'", context)?; skip_ws(tokens); diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index 6a80bf6e..81fd5e5e 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -128,13 +128,8 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Result>) -> Result { - let ASTNode::Function(mut method) = (if name.is_some() { + let parsed = if name.is_some() { parse_function_with_receiver(tokens, WaveType::Struct(target.clone()))? } else { parse_function(tokens)? - }) else { + }; + let ASTNode::Function(mut method) = parsed else { unreachable!("function parser returns a function node"); }; if method.return_type.is_none() { diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index 4719e816..385aacb0 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -1,6 +1,9 @@ //! Named proto syntax is parsed without changing inherent method attachments. use lexer::Lexer; -use parser::{ast::{ASTNode, Visibility, WaveType}, parse_syntax_with_spans}; +use parser::{ + ast::{ASTNode, Visibility, WaveType}, + parse_syntax_with_spans, +}; fn parse(source: &str) -> Vec { let tokens = Lexer::new_with_file(source, "proto.wave").tokenize().unwrap(); @@ -30,14 +33,19 @@ fn named_and_inherent_proto_forms_remain_distinct() { assert_eq!(named.visibility, Visibility::Private); assert_eq!(named.methods.len(), 1); assert_eq!(named.methods[0].parameters[0].name, "self"); - assert_eq!(named.methods[0].parameters[0].param_type, WaveType::Struct("Buffer".into())); + assert_eq!( + named.methods[0].parameters[0].param_type, + WaveType::Struct("Buffer".into()) + ); assert!(!named.methods[0].body.is_empty()); let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { panic!("inherent proto") }; assert_eq!(inherent.name, None); assert_eq!(inherent.target, "Buffer"); - assert_eq!(inherent.methods[0].parameters[0].param_type, - WaveType::Pointer(Box::new(WaveType::Struct("Buffer".into())))); + assert_eq!( + inherent.methods[0].parameters[0].param_type, + WaveType::Pointer(Box::new(WaveType::Struct("Buffer".into()))) + ); assert!(nodes[0].clone().span().is_some()); } @@ -52,8 +60,10 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { assert_eq!(contract.target, "Buffer"); assert_eq!(contract.visibility, Visibility::Public); assert_eq!(contract.methods.len(), 2); - assert_eq!(contract.methods[0].parameters[0].param_type, - WaveType::Struct("Buffer".into())); + assert_eq!( + contract.methods[0].parameters[0].param_type, + WaveType::Struct("Buffer".into()) + ); let cloned = nodes[0].clone(); let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { panic!("clone") }; assert_eq!(copy.name, contract.name); @@ -66,7 +76,7 @@ fn malformed_named_proto_syntax_has_located_diagnostics() { ("proto : Buffer {}", "identifier"), ("proto Printable Buffer {}", "'{' or ':'"), ("proto Printable: {}", "type"), - ("proto Printable: Buffer", "'{' or ':'"), + ("proto Printable: Buffer", "'{'"), ("proto Printable: Buffer {", "'}'"), ("proto Printable: Buffer { fun print(self) }", "function"), ("pub proto Buffer {}", "only named proto"), From bf77a7a42159c605d4679e70b9fb5b194c1af47b Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:19:02 +0200 Subject: [PATCH 03/12] test(parser): use legal method identifiers in named proto cases --- front/parser/tests/proto_contracts.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index 385aacb0..78d01184 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -23,7 +23,7 @@ fn failure(source: &str, expected: &str) { #[test] fn named_and_inherent_proto_forms_remain_distinct() { - let source = "proto Printable : Buffer { fun print(self) { return; } } + let source = "proto Printable : Buffer { fun render(self) { return; } } proto Buffer { fun clear(self: ptr) {} }"; let nodes = parse(source); assert_eq!(nodes.len(), 2); @@ -52,7 +52,7 @@ fn named_and_inherent_proto_forms_remain_distinct() { #[test] fn public_named_contract_preserves_target_generic_arguments_and_visibility() { let nodes = parse("pub proto Printable: Buffer { - fun print(self) {} + fun render(self) {} fun copy(self: ptr>) {} }"); let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { panic!("proto") }; @@ -78,7 +78,7 @@ fn malformed_named_proto_syntax_has_located_diagnostics() { ("proto Printable: {}", "type"), ("proto Printable: Buffer", "'{'"), ("proto Printable: Buffer {", "'}'"), - ("proto Printable: Buffer { fun print(self) }", "function"), + ("proto Printable: Buffer { fun render(self) }", "function"), ("pub proto Buffer {}", "only named proto"), ] { failure(source, expected); From 763e09cdb0e3a63466e61e7b5058efcbbaafa710 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:19:19 +0200 Subject: [PATCH 04/12] fix(semantic): reject named proto contracts until lowering is supported --- front/parser/src/verification/program.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/front/parser/src/verification/program.rs b/front/parser/src/verification/program.rs index 1353c34e..c462714e 100644 --- a/front/parser/src/verification/program.rs +++ b/front/parser/src/verification/program.rs @@ -186,6 +186,14 @@ impl ProgramTypes { } }, ASTNode::ProtoImpl(implementation) => { + if let Some(name) = &implementation.name { + return Err(failure( + format!( + "named proto contract `{name}` is not supported by semantic lowering yet" + ), + Some(top_level_span_hint(node)), + )); + } for method in &implementation.methods { let signature = function_type(method); insert_unique_method( From cd89a3c2888f613455122a331d921d24485602b3 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:19:39 +0200 Subject: [PATCH 05/12] test(hir): guard named proto semantic boundary and preserve inherent methods --- front/hir/tests/named_proto_boundary.rs | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 front/hir/tests/named_proto_boundary.rs diff --git a/front/hir/tests/named_proto_boundary.rs b/front/hir/tests/named_proto_boundary.rs new file mode 100644 index 00000000..c24f2e22 --- /dev/null +++ b/front/hir/tests/named_proto_boundary.rs @@ -0,0 +1,31 @@ +use hir::TypedProgram; +use lexer::Lexer; +use parser::parse_syntax_with_spans; + +fn syntax(source: &str) -> Vec { + let tokens = Lexer::new_with_file(source, "named-proto.wave").tokenize().unwrap(); + parse_syntax_with_spans(&tokens).unwrap() +} + +#[test] +fn named_contracts_are_rejected_at_the_semantic_boundary_with_a_span() { + for source in [ + "struct Buffer {} proto MissingContract: Buffer { fun value(self) -> i32 { return 7; } } fun main() {}", + "struct Buffer {} proto First: Buffer { fun value(self) -> i32 { return 7; } } proto Second: Buffer { fun value(self) -> i32 { return 8; } } fun main() {}", + ] { + let error = TypedProgram::lower(syntax(source)).unwrap_err(); + assert!( + error.diagnostic().message.contains("named proto contract"), + "{}", + error.diagnostic().message + ); + assert!(error.diagnostic().span.is_some(), "missing source span"); + } +} + +#[test] +fn inherent_proto_methods_still_lower_normally() { + let source = + "struct Buffer {} proto Buffer { fun value(self: ptr) -> i32 { return 7; } } fun main() {}"; + TypedProgram::lower(syntax(source)).unwrap(); +} From f158cbb2f7e9644df77fb959d5542667ccf52a1c Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:27:41 +0200 Subject: [PATCH 06/12] style(parser): expand let-else test assertions --- front/parser/tests/proto_contracts.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index 78d01184..9c167b5b 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -27,7 +27,11 @@ fn named_and_inherent_proto_forms_remain_distinct() { proto Buffer { fun clear(self: ptr) {} }"; let nodes = parse(source); assert_eq!(nodes.len(), 2); - let ASTNode::ProtoImpl(named) = nodes[0].unspanned() else { panic!("named proto") }; + let ASTNode::ProtoImpl(named) = nodes[0].unspanned() else { + + panic!("named proto") + + }; assert_eq!(named.name.as_deref(), Some("Printable")); assert_eq!(named.target, "Buffer"); assert_eq!(named.visibility, Visibility::Private); @@ -39,7 +43,13 @@ fn named_and_inherent_proto_forms_remain_distinct() { ); assert!(!named.methods[0].body.is_empty()); - let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { panic!("inherent proto") }; + let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { + + + panic!("inherent proto") + + + }; assert_eq!(inherent.name, None); assert_eq!(inherent.target, "Buffer"); assert_eq!( @@ -55,7 +65,11 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { fun render(self) {} fun copy(self: ptr>) {} }"); - let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { panic!("proto") }; + let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { + + panic!("proto") + + }; assert_eq!(contract.name.as_deref(), Some("Printable")); assert_eq!(contract.target, "Buffer"); assert_eq!(contract.visibility, Visibility::Public); @@ -65,7 +79,11 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { WaveType::Struct("Buffer".into()) ); let cloned = nodes[0].clone(); - let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { panic!("clone") }; + let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { + + panic!("clone") + + }; assert_eq!(copy.name, contract.name); assert_eq!(copy.visibility, Visibility::Public); } From 55edcb9245ce0826afdd77ba307cea948b6a9b07 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:27:58 +0200 Subject: [PATCH 07/12] style(parser): normalize let-else block spacing --- front/parser/tests/proto_contracts.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index 9c167b5b..e174aefb 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -28,9 +28,7 @@ fn named_and_inherent_proto_forms_remain_distinct() { let nodes = parse(source); assert_eq!(nodes.len(), 2); let ASTNode::ProtoImpl(named) = nodes[0].unspanned() else { - panic!("named proto") - }; assert_eq!(named.name.as_deref(), Some("Printable")); assert_eq!(named.target, "Buffer"); @@ -44,11 +42,7 @@ fn named_and_inherent_proto_forms_remain_distinct() { assert!(!named.methods[0].body.is_empty()); let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { - - panic!("inherent proto") - - }; assert_eq!(inherent.name, None); assert_eq!(inherent.target, "Buffer"); @@ -66,9 +60,7 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { fun copy(self: ptr>) {} }"); let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { - panic!("proto") - }; assert_eq!(contract.name.as_deref(), Some("Printable")); assert_eq!(contract.target, "Buffer"); @@ -80,9 +72,7 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { ); let cloned = nodes[0].clone(); let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { - panic!("clone") - }; assert_eq!(copy.name, contract.name); assert_eq!(copy.visibility, Visibility::Public); From c7d18af81bd4e9dc37a5440c12e8144776d6c863 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:41:38 +0200 Subject: [PATCH 08/12] style(parser): format named proto parser and regression tests --- front/parser/src/parser/items.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index 81fd5e5e..9d4d86ea 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -194,7 +194,9 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Result { - let ASTNode::Function(mut method) = parse_function(tokens)? else { unreachable!() }; + let ASTNode::Function(mut method) = parse_function(tokens)? else { + unreachable!() + }; if method.return_type.is_none() { method.return_type = Some(WaveType::Void); } From d12b1a270ee2a8dfe3f881fefc7226ca6487a637 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:41:41 +0200 Subject: [PATCH 09/12] style(parser): format named proto parser and regression tests --- front/parser/tests/proto_contracts.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index e174aefb..2dd01758 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -55,10 +55,12 @@ fn named_and_inherent_proto_forms_remain_distinct() { #[test] fn public_named_contract_preserves_target_generic_arguments_and_visibility() { - let nodes = parse("pub proto Printable: Buffer { + let nodes = parse( + "pub proto Printable: Buffer { fun render(self) {} fun copy(self: ptr>) {} - }"); + }", + ); let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { panic!("proto") }; From b5acb6602b39651199b2abf627994291f2c39dfc Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:47:23 +0200 Subject: [PATCH 10/12] style(parser): align proto node initialization with rustfmt width --- front/parser/src/parser/items.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index 9d4d86ea..8f859a5f 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -170,12 +170,7 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Result>) -> Result { From 48ae0be3bdcd5240791162887e7842c6dde49deb Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:41:31 +0200 Subject: [PATCH 11/12] style(parser): apply pinned rustfmt --- front/parser/src/parser/items.rs | 4 +--- front/parser/tests/proto_contracts.rs | 21 +++++---------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index 8f859a5f..27b3faf2 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -189,9 +189,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Result { - let ASTNode::Function(mut method) = parse_function(tokens)? else { - unreachable!() - }; + let ASTNode::Function(mut method) = parse_function(tokens)? else { unreachable!() }; if method.return_type.is_none() { method.return_type = Some(WaveType::Void); } diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index 2dd01758..8a5036d1 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -27,23 +27,16 @@ fn named_and_inherent_proto_forms_remain_distinct() { proto Buffer { fun clear(self: ptr) {} }"; let nodes = parse(source); assert_eq!(nodes.len(), 2); - let ASTNode::ProtoImpl(named) = nodes[0].unspanned() else { - panic!("named proto") - }; + let ASTNode::ProtoImpl(named) = nodes[0].unspanned() else { panic!("named proto") }; assert_eq!(named.name.as_deref(), Some("Printable")); assert_eq!(named.target, "Buffer"); assert_eq!(named.visibility, Visibility::Private); assert_eq!(named.methods.len(), 1); assert_eq!(named.methods[0].parameters[0].name, "self"); - assert_eq!( - named.methods[0].parameters[0].param_type, - WaveType::Struct("Buffer".into()) - ); + assert_eq!(named.methods[0].parameters[0].param_type, WaveType::Struct("Buffer".into())); assert!(!named.methods[0].body.is_empty()); - let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { - panic!("inherent proto") - }; + let ASTNode::ProtoImpl(inherent) = nodes[1].unspanned() else { panic!("inherent proto") }; assert_eq!(inherent.name, None); assert_eq!(inherent.target, "Buffer"); assert_eq!( @@ -61,9 +54,7 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { fun copy(self: ptr>) {} }", ); - let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { - panic!("proto") - }; + let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { panic!("proto") }; assert_eq!(contract.name.as_deref(), Some("Printable")); assert_eq!(contract.target, "Buffer"); assert_eq!(contract.visibility, Visibility::Public); @@ -73,9 +64,7 @@ fn public_named_contract_preserves_target_generic_arguments_and_visibility() { WaveType::Struct("Buffer".into()) ); let cloned = nodes[0].clone(); - let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { - panic!("clone") - }; + let ASTNode::ProtoImpl(copy) = cloned.unspanned() else { panic!("clone") }; assert_eq!(copy.name, contract.name); assert_eq!(copy.visibility, Visibility::Public); } From 3a4ae11e79b93ac966624093be2734e99959ffef Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:55:42 +0200 Subject: [PATCH 12/12] fix(parser): preserve incomplete proto diagnostic --- front/parser/src/parser/items.rs | 2 +- front/parser/tests/proto_contracts.rs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index 27b3faf2..edcfa692 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -152,7 +152,7 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Result { + None | Some(TokenType::Eof) if name.is_some() => { return Err(ParseError::expected_at( tokens.peek().copied(), anchor, diff --git a/front/parser/tests/proto_contracts.rs b/front/parser/tests/proto_contracts.rs index 8a5036d1..6e97af81 100644 --- a/front/parser/tests/proto_contracts.rs +++ b/front/parser/tests/proto_contracts.rs @@ -83,3 +83,8 @@ fn malformed_named_proto_syntax_has_located_diagnostics() { failure(source, expected); } } + +#[test] +fn incomplete_inherent_proto_preserves_the_existing_diagnostic() { + failure("proto Buffer {", "method declaration or '}'"); +}