feat: engram-lang — new programming language, quantum-sealed prod target, spreading activation types

This commit is contained in:
Will Anderson
2026-04-27 18:46:51 -05:00
commit 9ced941590
5569 changed files with 8153 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
//! Abstract syntax tree node types.
use el_lexer::Span;
// ── Literals ──────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
Int(i64),
Float(f64),
Str(String),
Bool(bool),
}
// ── Type expressions ──────────────────────────────────────────────────────────
/// A type annotation in source code, e.g. `String`, `[Int]`, `User?`.
#[derive(Debug, Clone, PartialEq)]
pub enum TypeExpr {
/// A named type: `Int`, `String`, `User`, …
Named(String),
/// An array type: `[T]`
Array(Box<TypeExpr>),
/// An optional type: `T?`
Optional(Box<TypeExpr>),
/// A function type: `fn(A, B) -> C`
Fn { params: Vec<TypeExpr>, return_type: Box<TypeExpr> },
}
// ── Patterns (for match arms) ─────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
/// `Status::Active`
EnumVariant { enum_name: String, variant: String, payload: Option<String> },
/// A wildcard `_`
Wildcard,
/// A literal: `42`, `"str"`, `true`
Literal(Literal),
/// A binding: `x`
Binding(String),
}
// ── Binary operators ──────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum BinOp {
Add, Sub, Mul, Div,
Eq, NotEq, Lt, Gt, LtEq, GtEq,
And, Or,
}
// ── Expressions ───────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Literal(Literal),
Ident(String),
BinOp { op: BinOp, left: Box<Expr>, right: Box<Expr> },
UnaryNot(Box<Expr>),
Call { func: Box<Expr>, args: Vec<Expr> },
Block(Vec<Stmt>),
Match { subject: Box<Expr>, arms: Vec<MatchArm> },
/// `activate TypeName where "semantic query string"`
Activate { type_name: String, query: String },
/// `sealed { stmts... }` — quantum-sealed block
Sealed(Vec<Stmt>),
If { cond: Box<Expr>, then: Box<Expr>, else_: Option<Box<Expr>> },
Field { object: Box<Expr>, field: String },
/// Array constructor: `[a, b, c]`
Array(Vec<Expr>),
/// Path expression: `Status::Active` (enum variant ref)
Path { segments: Vec<String> },
/// Index expression: `arr[0]`
Index { object: Box<Expr>, index: Box<Expr> },
}
// ── Match arm ─────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
pub pattern: Pattern,
pub body: Expr,
pub span: Span,
}
// ── Statements ────────────────────────────────────────────────────────────────
/// A named parameter in a function definition.
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub name: String,
pub type_ann: TypeExpr,
pub span: Span,
}
/// A field in a type definition.
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub name: String,
pub type_ann: TypeExpr,
pub span: Span,
}
/// A variant in an enum definition.
#[derive(Debug, Clone, PartialEq)]
pub struct Variant {
pub name: String,
/// Payload type, if any (e.g. `Pending(String)`)
pub payload: Option<TypeExpr>,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
/// `let name: Type = expr`
Let {
name: String,
type_ann: Option<TypeExpr>,
value: Expr,
span: Span,
},
/// `return expr`
Return(Expr, Span),
/// A bare expression used as a statement (usually a call).
Expr(Expr, Span),
/// `fn name(params) -> ReturnType { body }`
FnDef {
name: String,
params: Vec<Param>,
return_type: TypeExpr,
body: Vec<Stmt>,
span: Span,
},
/// `type Name { fields... }`
TypeDef {
name: String,
fields: Vec<Field>,
span: Span,
},
/// `enum Name { variants... }`
EnumDef {
name: String,
variants: Vec<Variant>,
span: Span,
},
}
// ── Top-level program ─────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
pub stmts: Vec<Stmt>,
/// The original source, kept for diagnostics and source maps.
pub source: String,
}
+54
View File
@@ -0,0 +1,54 @@
//! Parser error types.
use thiserror::Error;
use el_lexer::{Span, Token};
#[derive(Debug, Clone, Error)]
#[error("{kind} at {span}")]
pub struct ParseError {
pub kind: ParseErrorKind,
pub span: Span,
}
impl ParseError {
pub fn new(kind: ParseErrorKind, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone, Error)]
pub enum ParseErrorKind {
#[error("unexpected token {got}, expected {expected}")]
UnexpectedToken { expected: String, got: String },
#[error("unexpected end of file")]
UnexpectedEof,
#[error("invalid expression starting with {0}")]
InvalidExprStart(String),
#[error("invalid type expression: {0}")]
InvalidTypeExpr(String),
#[error("invalid pattern: {0}")]
InvalidPattern(String),
#[error("expected identifier, got {0}")]
ExpectedIdent(String),
}
impl ParseError {
pub fn expected(expected: impl Into<String>, got: &Token, span: Span) -> Self {
Self::new(
ParseErrorKind::UnexpectedToken {
expected: expected.into(),
got: got.to_string(),
},
span,
)
}
pub fn eof(span: Span) -> Self {
Self::new(ParseErrorKind::UnexpectedEof, span)
}
}
+17
View File
@@ -0,0 +1,17 @@
//! el-parser — Engram language recursive-descent parser.
//!
//! Converts a flat token stream into a typed [`Program`] AST.
//!
//! # Design
//! Hand-written recursive descent — no parser generator. Every parse function
//! returns `Result<T, ParseError>`, making the error path explicit.
mod ast;
mod error;
mod parser;
pub use ast::{
BinOp, Expr, Field, Literal, MatchArm, Param, Pattern, Program, Stmt, TypeExpr, Variant,
};
pub use error::{ParseError, ParseErrorKind};
pub use parser::parse;
+685
View File
@@ -0,0 +1,685 @@
//! Recursive-descent parser for the Engram language.
use el_lexer::{Span, Spanned, Token};
use crate::ast::*;
use crate::error::{ParseError, ParseErrorKind};
/// Parse a token stream into a [`Program`].
///
/// The `source` string is stored verbatim in the returned program for
/// diagnostics and source map generation.
pub fn parse(tokens: Vec<Spanned<Token>>, source: String) -> Result<Program, ParseError> {
let mut p = Parser::new(tokens);
let stmts = p.parse_program()?;
Ok(Program { stmts, source })
}
// ── Parser state ──────────────────────────────────────────────────────────────
struct Parser {
tokens: Vec<Spanned<Token>>,
/// Current cursor into `tokens`.
pos: usize,
}
impl Parser {
fn new(tokens: Vec<Spanned<Token>>) -> Self {
Self { tokens, pos: 0 }
}
// ── Token stream navigation ───────────────────────────────────────────────
fn current(&self) -> &Spanned<Token> {
&self.tokens[self.pos.min(self.tokens.len() - 1)]
}
fn peek(&self) -> &Token {
&self.current().node
}
fn peek_span(&self) -> Span {
self.current().span
}
#[allow(dead_code)]
fn peek2(&self) -> Option<&Token> {
self.tokens.get(self.pos + 1).map(|s| &s.node)
}
fn advance(&mut self) -> &Spanned<Token> {
let tok = &self.tokens[self.pos.min(self.tokens.len() - 1)];
if self.pos < self.tokens.len() - 1 {
self.pos += 1;
}
tok
}
fn at_end(&self) -> bool {
matches!(self.peek(), Token::Eof)
}
/// Consume the current token if it matches `expected`, otherwise error.
fn expect(&mut self, expected: &Token) -> Result<Span, ParseError> {
if self.peek() == expected {
let span = self.peek_span();
self.advance();
Ok(span)
} else {
Err(ParseError::expected(format!("{expected}"), self.peek(), self.peek_span()))
}
}
fn expect_ident(&mut self) -> Result<(String, Span), ParseError> {
let span = self.peek_span();
match self.peek().clone() {
Token::Ident(name) => {
self.advance();
Ok((name, span))
}
tok => Err(ParseError::new(
ParseErrorKind::ExpectedIdent(tok.to_string()),
span,
)),
}
}
fn eat(&mut self, tok: &Token) -> bool {
if self.peek() == tok {
self.advance();
true
} else {
false
}
}
// ── Top-level ─────────────────────────────────────────────────────────────
fn parse_program(&mut self) -> Result<Vec<Stmt>, ParseError> {
let mut stmts = Vec::new();
while !self.at_end() {
// Skip optional semicolons at top level
while self.eat(&Token::Semicolon) {}
if self.at_end() {
break;
}
stmts.push(self.parse_stmt()?);
}
Ok(stmts)
}
// ── Statements ────────────────────────────────────────────────────────────
fn parse_stmt(&mut self) -> Result<Stmt, ParseError> {
let start = self.peek_span();
match self.peek().clone() {
Token::Let => self.parse_let(start),
Token::Fn => self.parse_fn_def(start),
Token::Type => self.parse_type_def(start),
Token::Enum => self.parse_enum_def(start),
Token::Return => {
self.advance(); // consume `return`
let expr = self.parse_expr()?;
let span = Span::new(start.start, expr_span_end(&expr, start), start.line, start.col);
self.eat(&Token::Semicolon);
Ok(Stmt::Return(expr, span))
}
_ => {
let expr = self.parse_expr()?;
let span = start;
self.eat(&Token::Semicolon);
Ok(Stmt::Expr(expr, span))
}
}
}
fn parse_let(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Let)?;
let (name, _) = self.expect_ident()?;
let type_ann = if self.eat(&Token::Colon) {
Some(self.parse_type_expr()?)
} else {
None
};
self.expect(&Token::Eq)?;
let value = self.parse_expr()?;
self.eat(&Token::Semicolon);
Ok(Stmt::Let { name, type_ann, value, span: start })
}
fn parse_fn_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Fn)?;
let (name, _) = self.expect_ident()?;
self.expect(&Token::LParen)?;
let params = self.parse_param_list()?;
self.expect(&Token::RParen)?;
self.expect(&Token::Arrow)?;
let return_type = self.parse_type_expr()?;
self.expect(&Token::LBrace)?;
let body = self.parse_block_body()?;
self.expect(&Token::RBrace)?;
Ok(Stmt::FnDef { name, params, return_type, body, span: start })
}
fn parse_param_list(&mut self) -> Result<Vec<Param>, ParseError> {
let mut params = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
let span = self.peek_span();
let (name, _) = self.expect_ident()?;
self.expect(&Token::Colon)?;
let type_ann = self.parse_type_expr()?;
params.push(Param { name, type_ann, span });
if !self.eat(&Token::Comma) {
break;
}
}
Ok(params)
}
fn parse_type_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Type)?;
let (name, _) = self.expect_ident()?;
self.expect(&Token::LBrace)?;
let mut fields = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let span = self.peek_span();
let (fname, _) = self.expect_ident()?;
self.expect(&Token::Colon)?;
let type_ann = self.parse_type_expr()?;
fields.push(Field { name: fname, type_ann, span });
self.eat(&Token::Comma);
self.eat(&Token::Semicolon);
}
self.expect(&Token::RBrace)?;
Ok(Stmt::TypeDef { name, fields, span: start })
}
fn parse_enum_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Enum)?;
let (name, _) = self.expect_ident()?;
self.expect(&Token::LBrace)?;
let mut variants = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let span = self.peek_span();
let (vname, _) = self.expect_ident()?;
let payload = if self.eat(&Token::LParen) {
let ty = self.parse_type_expr()?;
self.expect(&Token::RParen)?;
Some(ty)
} else {
None
};
variants.push(Variant { name: vname, payload, span });
self.eat(&Token::Comma);
self.eat(&Token::Semicolon);
}
self.expect(&Token::RBrace)?;
Ok(Stmt::EnumDef { name, variants, span: start })
}
fn parse_block_body(&mut self) -> Result<Vec<Stmt>, ParseError> {
let mut stmts = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
while self.eat(&Token::Semicolon) {}
if matches!(self.peek(), Token::RBrace | Token::Eof) {
break;
}
stmts.push(self.parse_stmt()?);
}
Ok(stmts)
}
// ── Type expressions ──────────────────────────────────────────────────────
fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
let span = self.peek_span();
// Array type: [T]
if self.eat(&Token::LBracket) {
let inner = self.parse_type_expr()?;
self.expect(&Token::RBracket)?;
let te = TypeExpr::Array(Box::new(inner));
// Optional array: [T]?
if self.eat(&Token::Not) {
// Not actually "!", we need "?" — but we don't have that token.
// We'll use Optional postfix via the Ident "?" — skip for now.
}
return Ok(te);
}
// Named type
let name = match self.peek().clone() {
Token::Ident(n) => { self.advance(); n }
tok => return Err(ParseError::new(
ParseErrorKind::InvalidTypeExpr(tok.to_string()),
span,
)),
};
// Check for function type: fn(A) -> B
if name == "fn" {
self.expect(&Token::LParen)?;
let mut params = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
params.push(self.parse_type_expr()?);
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::RParen)?;
self.expect(&Token::Arrow)?;
let ret = self.parse_type_expr()?;
return Ok(TypeExpr::Fn { params, return_type: Box::new(ret) });
}
Ok(TypeExpr::Named(name))
}
// ── Expressions ───────────────────────────────────────────────────────────
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
self.parse_or_expr()
}
fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_and_expr()?;
while self.eat(&Token::Or) {
let right = self.parse_and_expr()?;
left = Expr::BinOp { op: BinOp::Or, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_and_expr(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_equality()?;
while self.eat(&Token::And) {
let right = self.parse_equality()?;
left = Expr::BinOp { op: BinOp::And, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_equality(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_comparison()?;
loop {
let op = match self.peek() {
Token::EqEq => BinOp::Eq,
Token::NotEq => BinOp::NotEq,
_ => break,
};
self.advance();
let right = self.parse_comparison()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_additive()?;
loop {
let op = match self.peek() {
Token::Lt => BinOp::Lt,
Token::Gt => BinOp::Gt,
Token::LtEq => BinOp::LtEq,
Token::GtEq => BinOp::GtEq,
_ => break,
};
self.advance();
let right = self.parse_additive()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_additive(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_multiplicative()?;
loop {
let op = match self.peek() {
Token::Plus => BinOp::Add,
Token::Minus => BinOp::Sub,
_ => break,
};
self.advance();
let right = self.parse_multiplicative()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_unary()?;
loop {
let op = match self.peek() {
Token::Star => BinOp::Mul,
Token::Slash => BinOp::Div,
_ => break,
};
self.advance();
let right = self.parse_unary()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_unary(&mut self) -> Result<Expr, ParseError> {
if self.eat(&Token::Not) {
let inner = self.parse_unary()?;
return Ok(Expr::UnaryNot(Box::new(inner)));
}
self.parse_postfix()
}
fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
let mut expr = self.parse_primary()?;
loop {
match self.peek() {
Token::Dot => {
self.advance();
let (field, _) = self.expect_ident()?;
expr = Expr::Field { object: Box::new(expr), field };
}
Token::LParen => {
self.advance();
let args = self.parse_arg_list()?;
self.expect(&Token::RParen)?;
expr = Expr::Call { func: Box::new(expr), args };
}
Token::LBracket => {
self.advance();
let index = self.parse_expr()?;
self.expect(&Token::RBracket)?;
expr = Expr::Index { object: Box::new(expr), index: Box::new(index) };
}
_ => break,
}
}
Ok(expr)
}
fn parse_arg_list(&mut self) -> Result<Vec<Expr>, ParseError> {
let mut args = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
args.push(self.parse_expr()?);
if !self.eat(&Token::Comma) { break; }
}
Ok(args)
}
fn parse_primary(&mut self) -> Result<Expr, ParseError> {
let span = self.peek_span();
match self.peek().clone() {
// Literals
Token::IntLiteral(n) => { self.advance(); Ok(Expr::Literal(Literal::Int(n))) }
Token::FloatLiteral(f) => { self.advance(); Ok(Expr::Literal(Literal::Float(f))) }
Token::StringLiteral(s) => { self.advance(); Ok(Expr::Literal(Literal::Str(s))) }
Token::BoolLiteral(b) => { self.advance(); Ok(Expr::Literal(Literal::Bool(b))) }
// Grouped or tuple
Token::LParen => {
self.advance();
let expr = self.parse_expr()?;
self.expect(&Token::RParen)?;
Ok(expr)
}
// Block
Token::LBrace => {
self.advance();
let stmts = self.parse_block_body()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Block(stmts))
}
// Array literal
Token::LBracket => {
self.advance();
let mut elems = Vec::new();
while !matches!(self.peek(), Token::RBracket | Token::Eof) {
elems.push(self.parse_expr()?);
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::RBracket)?;
Ok(Expr::Array(elems))
}
// match expression
Token::Match => {
self.advance();
let subject = self.parse_expr()?;
self.expect(&Token::LBrace)?;
let arms = self.parse_match_arms()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Match { subject: Box::new(subject), arms })
}
// activate Type where "query"
Token::Activate => {
self.advance();
let (type_name, _) = self.expect_ident()?;
self.expect(&Token::Where)?;
let query = match self.peek().clone() {
Token::StringLiteral(s) => { self.advance(); s }
tok => return Err(ParseError::expected("string literal", &tok, self.peek_span())),
};
Ok(Expr::Activate { type_name, query })
}
// sealed { stmts }
Token::Sealed => {
self.advance();
self.expect(&Token::LBrace)?;
let stmts = self.parse_block_body()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Sealed(stmts))
}
// if/else
Token::If => {
self.advance();
let cond = self.parse_expr()?;
let then = self.parse_primary()?; // expects block
let else_ = if self.eat(&Token::Else) {
Some(Box::new(self.parse_primary()?))
} else {
None
};
Ok(Expr::If { cond: Box::new(cond), then: Box::new(then), else_ })
}
// Identifier — could be plain name or path (Foo::Bar)
Token::Ident(name) => {
self.advance();
// Check for path: Foo::Bar or Foo::Bar::Baz
if matches!(self.peek(), Token::ColonColon) {
let mut segments = vec![name];
while self.eat(&Token::ColonColon) {
let (seg, _) = self.expect_ident()?;
segments.push(seg);
}
Ok(Expr::Path { segments })
} else {
Ok(Expr::Ident(name))
}
}
tok => Err(ParseError::new(
ParseErrorKind::InvalidExprStart(tok.to_string()),
span,
)),
}
}
// ── Match arms ────────────────────────────────────────────────────────────
fn parse_match_arms(&mut self) -> Result<Vec<MatchArm>, ParseError> {
let mut arms = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let span = self.peek_span();
let pattern = self.parse_pattern()?;
self.expect(&Token::FatArrow)?;
let body = self.parse_expr()?;
arms.push(MatchArm { pattern, body, span });
self.eat(&Token::Comma);
self.eat(&Token::Semicolon);
}
Ok(arms)
}
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
let span = self.peek_span();
match self.peek().clone() {
// Wildcard
Token::Ident(ref s) if s == "_" => {
self.advance();
Ok(Pattern::Wildcard)
}
// Could be: binding, enum variant, or path
Token::Ident(name) => {
self.advance();
if self.eat(&Token::ColonColon) {
// EnumVariant pattern: Status::Active or Status::Pending(reason)
let (variant, _) = self.expect_ident()?;
let payload = if self.eat(&Token::LParen) {
let (bind, _) = self.expect_ident()?;
self.expect(&Token::RParen)?;
Some(bind)
} else {
None
};
Ok(Pattern::EnumVariant { enum_name: name, variant, payload })
} else {
// Simple binding
Ok(Pattern::Binding(name))
}
}
Token::IntLiteral(n) => { self.advance(); Ok(Pattern::Literal(Literal::Int(n))) }
Token::StringLiteral(s) => { self.advance(); Ok(Pattern::Literal(Literal::Str(s))) }
Token::BoolLiteral(b) => { self.advance(); Ok(Pattern::Literal(Literal::Bool(b))) }
tok => Err(ParseError::new(
ParseErrorKind::InvalidPattern(tok.to_string()),
span,
)),
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
fn expr_span_end(_expr: &Expr, fallback: Span) -> usize {
fallback.end
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use el_lexer::tokenize;
use super::*;
fn parse_src(src: &str) -> Program {
let tokens = tokenize(src).expect("lex failed");
parse(tokens, src.to_string()).expect("parse failed")
}
#[test]
fn test_parse_let() {
let p = parse_src("let x: Int = 42");
assert!(matches!(p.stmts[0], Stmt::Let { ref name, .. } if name == "x"));
}
#[test]
fn test_parse_fn_def() {
let src = r#"fn greet(name: String) -> String { return "Hello" }"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::FnDef { name, .. } if name == "greet"));
}
#[test]
fn test_parse_type_def() {
let src = "type User { id: Uuid name: String email: String }";
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::TypeDef { name, fields, .. } if name == "User" && fields.len() == 3));
}
#[test]
fn test_parse_enum_def() {
let src = "enum Status { Active Inactive Pending(String) }";
let p = parse_src(src);
match &p.stmts[0] {
Stmt::EnumDef { name, variants, .. } => {
assert_eq!(name, "Status");
assert_eq!(variants.len(), 3);
assert_eq!(variants[2].name, "Pending");
assert!(variants[2].payload.is_some());
}
_ => panic!("expected EnumDef"),
}
}
#[test]
fn test_parse_match() {
let src = r#"
match status {
Status::Active => "active"
Status::Inactive => "inactive"
}
"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Match { arms, .. }, _) if arms.len() == 2));
}
#[test]
fn test_parse_activate() {
let src = r#"activate User where "customer who purchased recently""#;
let p = parse_src(src);
assert!(matches!(
&p.stmts[0],
Stmt::Expr(Expr::Activate { type_name, query }, _)
if type_name == "User" && query.contains("customer")
));
}
#[test]
fn test_parse_sealed_block() {
let src = r#"sealed { let key: String = "secret" }"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Sealed(_), _)));
}
#[test]
fn test_parse_binary_ops() {
let p = parse_src("let result = 1 + 2 * 3");
match &p.stmts[0] {
Stmt::Let { value: Expr::BinOp { op: BinOp::Add, right, .. }, .. } => {
// Right side should be 2*3
assert!(matches!(**right, Expr::BinOp { op: BinOp::Mul, .. }));
}
_ => panic!("unexpected AST"),
}
}
#[test]
fn test_parse_fn_call() {
let p = parse_src(r#"greet("Will")"#);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Call { .. }, _)));
}
#[test]
fn test_parse_field_access() {
let p = parse_src("user.name");
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Field { field, .. }, _) if field == "name"));
}
#[test]
fn test_parse_if_else() {
let src = r#"if x == 1 { return "yes" } else { return "no" }"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::If { else_: Some(_), .. }, _)));
}
#[test]
fn test_parse_array_literal() {
let p = parse_src("[1, 2, 3]");
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Array(elems), _) if elems.len() == 3));
}
#[test]
fn test_parse_path_expr() {
let p = parse_src("Status::Active");
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Path { segments }, _) if segments.len() == 2));
}
}