Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions front/hir/tests/named_proto_boundary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use hir::TypedProgram;
use lexer::Lexer;
use parser::parse_syntax_with_spans;

fn syntax(source: &str) -> Vec<parser::ast::ASTNode> {
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<Buffer>) -> i32 { return 7; } } fun main() {}";
TypedProgram::lower(syntax(source)).unwrap();
}
4 changes: 4 additions & 0 deletions front/parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// The target name includes generic arguments, e.g. Buffer<i32>.
pub target: String,
pub methods: Vec<FunctionNode>,
pub visibility: Visibility,
}

#[derive(Debug, Clone)]
Expand Down
2 changes: 2 additions & 0 deletions front/parser/src/ast/cloning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
7 changes: 6 additions & 1 deletion front/parser/src/ast_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
],
)
}
}
Expand Down
45 changes: 41 additions & 4 deletions front/parser/src/parser/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +74,13 @@ pub fn parse_generic_param_names(

pub fn parse_parameters(
tokens: &mut Peekable<Iter<Token>>,
) -> Result<Vec<ParameterNode>, ParseError> {
parse_parameters_with_receiver(tokens, None)
}

fn parse_parameters_with_receiver(
tokens: &mut Peekable<Iter<Token>>,
receiver_type: Option<&WaveType>,
) -> Result<Vec<ParameterNode>, ParseError> {
let anchor = tokens.peek().copied();
let context = "function parameters";
Expand All @@ -90,9 +99,21 @@ 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 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);
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();
Expand Down Expand Up @@ -130,6 +151,22 @@ pub fn parse_function(tokens: &mut Peekable<Iter<Token>>) -> Result<ASTNode, Par
pub fn parse_function_with_export(
tokens: &mut Peekable<Iter<Token>>,
export: Option<ExportAttribute>,
) -> Result<ASTNode, ParseError> {
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<Iter<Token>>,
receiver_type: WaveType,
) -> Result<ASTNode, ParseError> {
parse_function_with_export_and_receiver(tokens, None, Some(receiver_type))
}

fn parse_function_with_export_and_receiver(
tokens: &mut Peekable<Iter<Token>>,
export: Option<ExportAttribute>,
receiver_type: Option<WaveType>,
) -> Result<ASTNode, ParseError> {
let anchor = tokens.peek().copied();
let invalid = |token: Option<&Token>| {
Expand Down Expand Up @@ -175,7 +212,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;
Expand Down
52 changes: 46 additions & 6 deletions front/parser/src/parser/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,25 +103,63 @@ pub fn parse_import(tokens: &mut Peekable<Iter<Token>>) -> Result<ASTNode, Parse

pub fn parse_proto(tokens: &mut Peekable<Iter<Token>>) -> Result<ASTNode, ParseError> {
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);
let opener = if name.is_some() { "'{'" } else { "'{' or ':'" };
crate::expr::expect_token(tokens, anchor, TokenType::Lbrace, opener, 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 parsed = if name.is_some() {
parse_function_with_receiver(tokens, WaveType::Struct(target.clone()))?
} else {
parse_function(tokens)?
};
let ASTNode::Function(mut method) = parsed else {
unreachable!("function parser returns a function node");
};
if method.return_type.is_none() {
method.return_type = Some(WaveType::Void);
}
methods.push(method);
},
None | Some(TokenType::Eof) if name.is_some() => {
return Err(ParseError::expected_at(
tokens.peek().copied(),
anchor,
"'}'",
context,
));
},
_ => {
return Err(ParseError::expected_at(
tokens.peek().copied(),
Expand All @@ -130,7 +170,7 @@ pub fn parse_proto(tokens: &mut Peekable<Iter<Token>>) -> Result<ASTNode, ParseE
},
}
}
Ok(ASTNode::ProtoImpl(ProtoImplNode { target: target_struct, methods }))
Ok(ASTNode::ProtoImpl(ProtoImplNode { name, target, methods, visibility: Visibility::Private }))
}

pub fn parse_struct(tokens: &mut Peekable<Iter<Token>>) -> Result<ASTNode, ParseError> {
Expand Down
16 changes: 16 additions & 0 deletions front/parser/src/parser/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result<Vec<ASTNode>, 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)?)
Expand Down Expand Up @@ -398,6 +402,7 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result<Vec<ASTNode>, ParseError> {
"pub import",
"pub export(c) fun",
"pub struct",
"pub proto",
"pub enum",
"pub variant",
"pub type",
Expand Down Expand Up @@ -434,6 +439,17 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result<Vec<ASTNode>, 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,
Expand Down
8 changes: 8 additions & 0 deletions front/parser/src/verification/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
90 changes: 90 additions & 0 deletions front/parser/tests/proto_contracts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! 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<ASTNode> {
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 render(self) { return; } }
proto Buffer { fun clear(self: ptr<Buffer>) {} }";
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<i32> {
fun render(self) {}
fun copy(self: ptr<Buffer<i32>>) {}
}",
);
let ASTNode::ProtoImpl(contract) = nodes[0].unspanned() else { panic!("proto") };
assert_eq!(contract.name.as_deref(), Some("Printable"));
assert_eq!(contract.target, "Buffer<i32>");
assert_eq!(contract.visibility, Visibility::Public);
assert_eq!(contract.methods.len(), 2);
assert_eq!(
contract.methods[0].parameters[0].param_type,
WaveType::Struct("Buffer<i32>".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", "'{'"),
("proto Printable: Buffer {", "'}'"),
("proto Printable: Buffer { fun render(self) }", "function"),
("pub proto Buffer {}", "only named proto"),
] {
failure(source, expected);
}
}

#[test]
fn incomplete_inherent_proto_preserves_the_existing_diagnostic() {
failure("proto Buffer {", "method declaration or '}'");
}
Loading