writeonce/crates/rt/src/parser.rs

1195 lines
46 KiB
Rust

//! Recursive-descent parser for `.wo` source.
//!
//! Stage 2 scope:
//! * `type Name { ... }` declarations with fields and `service rest` blocks
//! * graceful skip of constructs we don't yet execute: `policy`, `on <event>`,
//! computed field defaults, `fn`, `main`, `##ui`/`##app`/`##sql`/`##doc`/
//! `##graph`/`##policy`/`##service`/`##logic`/`##logic` blocks.
//!
//! "Skip" means: consume until the matching close brace / next top-level start,
//! so the parser survives and later phases can do nothing.
use crate::ast::*;
use crate::lexer::tokenize;
use crate::token::{Kind, Token};
use anyhow::{bail, Context, Result};
pub fn parse(src: &str) -> Result<Schema> {
let toks = tokenize(src).context("tokenize")?;
Parser::new(toks).parse_schema()
}
struct Parser {
toks: Vec<Token>,
pos: usize,
}
impl Parser {
fn new(toks: Vec<Token>) -> Self { Self { toks, pos: 0 } }
// --- primitives ---
fn peek(&self) -> &Kind { &self.toks[self.pos.min(self.toks.len() - 1)].kind }
fn peek_line(&self) -> u32 { self.toks[self.pos.min(self.toks.len() - 1)].line }
fn advance(&mut self) -> &Token {
let t = &self.toks[self.pos];
if !matches!(t.kind, Kind::End) { self.pos += 1; }
&self.toks[self.pos.saturating_sub(1)]
}
fn skip_newlines(&mut self) {
while matches!(self.peek(), Kind::Newline) { self.advance(); }
}
fn accept(&mut self, want: &Kind) -> bool {
if std::mem::discriminant(self.peek()) == std::mem::discriminant(want) {
self.advance();
true
} else { false }
}
fn expect(&mut self, want: &Kind, what: &str) -> Result<&Token> {
if std::mem::discriminant(self.peek()) == std::mem::discriminant(want) {
Ok(self.advance())
} else {
bail!("line {}: expected {what}, got {}", self.peek_line(), self.peek())
}
}
fn expect_ident(&mut self, what: &str) -> Result<String> {
match self.peek().clone() {
Kind::Ident(s) => { self.advance(); Ok(s) }
k => bail!("line {}: expected {what}, got {k}", self.peek_line()),
}
}
fn at_end(&self) -> bool { matches!(self.peek(), Kind::End) }
// --- top-level ---
fn parse_schema(&mut self) -> Result<Schema> {
let mut sch = Schema::default();
loop {
self.skip_newlines();
if self.at_end() { break; }
match self.peek() {
Kind::KwType | Kind::KwClass =>
sch.types.push(self.parse_type(TableCfg::default())?),
// Type-level annotation: `@table(...)` configures the
// declaration that follows; unknown names skip silently
// (the field-annotation precedent).
Kind::At => {
if let Some(table) = self.parse_type_annotations()? {
self.skip_newlines();
if !matches!(self.peek(), Kind::KwType | Kind::KwClass) {
bail!("line {}: expected `type` or `class` after @table, got {}",
self.peek_line(), self.peek());
}
sch.types.push(self.parse_type(table)?);
}
}
// Skip constructs we don't execute yet.
Kind::HashHash(_)
| Kind::KwFn
| Kind::KwMain
| Kind::KwOn
| Kind::KwPolicy
| Kind::KwTest
| Kind::KwLet // stray `let` at top level (in `main { ... }` probably)
| Kind::Hash(_) // ##sql #table etc.
=> self.skip_top_level_chunk()?,
_ => self.skip_top_level_chunk()?,
}
}
Ok(sch)
}
/// Parse the type-level annotation list ahead of a `type`/`class`.
/// Returns `Some(cfg)` when a recognised `@table` was consumed, `None`
/// when the annotation was unknown and skipped (caller resumes the loop).
fn parse_type_annotations(&mut self) -> Result<Option<TableCfg>> {
self.expect(&Kind::At, "'@'")?;
let name = self.expect_ident("annotation name")?;
if name != "table" {
// Unknown type-level annotation: consume an optional (...) block
// and let the schema loop decide what the next token means.
if matches!(self.peek(), Kind::LParen) {
let mut depth = 1i32;
self.advance();
while depth > 0 && !self.at_end() {
match self.peek() {
Kind::LParen => { depth += 1; self.advance(); }
Kind::RParen => { depth -= 1; self.advance(); }
_ => { self.advance(); }
}
}
}
return Ok(None);
}
let mut cfg = TableCfg::default();
if !self.accept(&Kind::LParen) {
return Ok(Some(cfg)); // bare `@table` — legal no-op
}
loop {
self.skip_newlines();
if self.accept(&Kind::RParen) { break; }
let key = self.expect_ident("@table argument")?;
self.expect(&Kind::Colon, "':'")?;
match key.as_str() {
"name" => {
if cfg.name.is_some() {
bail!("line {}: @table(name: ...) given twice", self.peek_line());
}
match self.peek().clone() {
Kind::Str(s) => { self.advance(); cfg.name = Some(s); }
other => bail!("line {}: @table name must be a string, got {other}",
self.peek_line()),
}
}
"index" => {
self.expect(&Kind::LBracket, "'['")?;
let mut cols = Vec::new();
loop {
cols.push(self.expect_ident("index column")?);
if !self.accept(&Kind::Comma) { break; }
}
self.expect(&Kind::RBracket, "']'")?;
if cols.is_empty() {
bail!("line {}: @table index needs at least one column", self.peek_line());
}
cfg.indexes.push(cols);
}
// `shard_key`/`retention` are reserved for later phases —
// reject loudly rather than silently ignoring (no silent
// passthrough on surface we own).
other => bail!(
"line {}: unknown @table argument `{other}` \
(supported: name, index)", self.peek_line()),
}
self.skip_newlines();
if !self.accept(&Kind::Comma) {
self.skip_newlines();
self.expect(&Kind::RParen, "')' or ','")?;
break;
}
}
Ok(Some(cfg))
}
/// Walk forward until we reach the start of the next top-level construct
/// (another `type`, `##…`, or EOF), balancing braces in between.
fn skip_top_level_chunk(&mut self) -> Result<()> {
// Always consume at least one token so we don't loop forever.
let mut depth = 0i32;
let start = self.pos;
loop {
match self.peek() {
Kind::End => break,
Kind::LBrace => { depth += 1; self.advance(); }
Kind::RBrace => { depth -= 1; self.advance(); if depth <= 0 { break; } }
Kind::LBracket => { depth += 1; self.advance(); }
Kind::RBracket => { depth -= 1; self.advance(); }
Kind::LParen => { depth += 1; self.advance(); }
Kind::RParen => { depth -= 1; self.advance(); }
Kind::KwType if depth == 0 && self.pos > start => break,
Kind::KwClass if depth == 0 && self.pos > start => break,
Kind::HashHash(_) if depth == 0 && self.pos > start => break,
_ => { self.advance(); }
}
}
Ok(())
}
// --- type declaration ---
fn parse_type(&mut self, table: TableCfg) -> Result<TypeDecl> {
// `class` is the behavior-bearing sibling of `type` — identical field
// grammar plus `fn` methods (plan 13a). Storage/REST are class-blind.
let is_class = matches!(self.peek(), Kind::KwClass);
if is_class {
self.advance();
} else {
self.expect(&Kind::KwType, "`type` or `class`")?;
}
let name = self.expect_ident("type name")?;
// Link types have a different header: `type Purchase link Customer -> Product { ... }`.
// For Stage 2 we don't bind link-type behaviour, so parse-and-discard the body.
let is_link = matches!(self.peek(), Kind::KwLink);
if is_link {
// consume link A -> B
while !matches!(self.peek(), Kind::LBrace | Kind::End) {
self.advance();
}
}
self.expect(&Kind::LBrace, "'{'")?;
let mut decl = TypeDecl {
name, fields: Vec::new(), services: Vec::new(), is_class,
methods: Vec::new(), table,
};
loop {
self.skip_newlines();
match self.peek() {
Kind::RBrace => { self.advance(); break; }
Kind::End => bail!("unexpected end of input inside type body"),
Kind::KwPolicy => self.skip_block_line()?, // policy <read|write|...> ...
Kind::KwOn => self.skip_on_block()?, // on update when ... do ...
Kind::KwFn if is_class => {
// 13b: class methods parse for real — signature + DML body.
decl.methods.push(self.parse_method()?);
}
Kind::KwFn => self.skip_block_line()?, // fn inside a plain `type`:
// parse-and-discard (13a);
// brace depth keeps the body's
// `}` from closing the type
Kind::KwService => decl.services.push(self.parse_service()?),
Kind::Ident(_) => {
if is_link {
// Stage 2: absorb link-type bodies without interpreting them.
self.skip_block_line()?;
} else {
// Distinguish a field from a trailing junk line. Fields look like
// `ident : type ...`. Anything else → skip.
if self.looks_like_field() {
decl.fields.push(self.parse_field()?);
} else {
self.skip_block_line()?;
}
}
}
_ => self.skip_block_line()?,
}
}
Ok(decl)
}
fn looks_like_field(&self) -> bool {
// Lookahead: Ident followed by Colon (possibly after a hyphenated ident).
let mut i = self.pos;
match self.toks.get(i).map(|t| &t.kind) {
Some(Kind::Ident(_)) => {}
_ => return false,
}
i += 1;
matches!(self.toks.get(i).map(|t| &t.kind), Some(Kind::Colon))
}
/// Skip tokens until the end of the current logical line (up to Newline or
/// the outer RBrace). Used for policies, triggers, etc., whose full grammar
/// is out of Stage 2 scope.
fn skip_block_line(&mut self) -> Result<()> {
let mut depth = 0i32;
loop {
match self.peek() {
Kind::End => break,
Kind::Newline if depth == 0 => { self.advance(); break; }
Kind::RBrace if depth == 0 => break, // stop before the outer `}` — caller handles it
Kind::LBrace | Kind::LBracket | Kind::LParen => { depth += 1; self.advance(); }
Kind::RBrace | Kind::RBracket | Kind::RParen => { depth -= 1; self.advance(); }
_ => { self.advance(); }
}
}
Ok(())
}
/// `on <event> [when ...] do <action>` possibly spanning many lines. Skip
/// until the next top-level keyword inside the type body. Trigger bodies
/// commonly contain `{ k: v, ... }` object literals and `( ... )` calls,
/// so we track brace depth — RBrace only terminates when we're at the
/// outermost level of the `on` block.
fn skip_on_block(&mut self) -> Result<()> {
self.advance(); // consume `on`
let mut depth = 0i32;
loop {
match self.peek() {
Kind::End => break,
Kind::RBrace if depth == 0 => break, // closes the surrounding type body
Kind::LBrace | Kind::LBracket | Kind::LParen => {
depth += 1;
self.advance();
}
Kind::RBrace | Kind::RBracket | Kind::RParen => {
depth -= 1;
self.advance();
}
Kind::Newline if depth == 0 => {
// At depth 0, a newline may end the `on` block if the next
// meaningful token starts a new type-body item.
while matches!(self.peek(), Kind::Newline) { self.advance(); }
if matches!(self.peek(),
Kind::RBrace | Kind::KwPolicy | Kind::KwService
| Kind::KwOn | Kind::KwFn | Kind::End
) { return Ok(()); }
if matches!(self.peek(), Kind::Ident(_)) && self.looks_like_field() {
return Ok(());
}
}
_ => { self.advance(); }
}
}
Ok(())
}
// --- field parsing ---
fn parse_field(&mut self) -> Result<Field> {
let name = self.expect_ident("field name")?;
self.expect(&Kind::Colon, "':'")?;
let (ty, is_relation) = self.parse_field_ty()?;
let mut nullable = false;
if matches!(self.peek(), Kind::Question) {
self.advance();
nullable = true;
}
let mut unique = false;
let mut default = None;
loop {
match self.peek() {
Kind::At => {
self.advance();
let name = self.expect_ident("annotation name")?;
// Consume optional (...) argument block without interpreting it.
if matches!(self.peek(), Kind::LParen) {
let mut depth = 1i32;
self.advance();
while depth > 0 {
match self.peek() {
Kind::End => break,
Kind::LParen => { depth += 1; self.advance(); }
Kind::RParen => { depth -= 1; self.advance(); }
_ => { self.advance(); }
}
}
}
if name == "unique" { unique = true; }
}
Kind::Eq => {
self.advance();
default = Some(self.parse_default_expr()?);
}
Kind::Newline | Kind::RBrace | Kind::End => break,
_ => {
// Skip any stray tokens until end-of-line — resilient to unhandled
// annotation forms like `@check(between 1 and 5)`.
self.advance();
}
}
}
Ok(Field { name, ty, nullable, unique, default, is_relation })
}
/// Returns (type, is_relation) — `ref`/`multi`/`backlink` are relations
/// and don't carry a stored scalar column in this type.
fn parse_field_ty(&mut self) -> Result<(FieldTy, bool)> {
match self.peek() {
Kind::KwRef => {
self.advance();
let target = self.expect_ident("ref target type")?;
Ok((FieldTy::Ref(target), true))
}
Kind::KwMulti => {
self.advance();
let target = self.expect_ident("multi target type")?;
// Either `@edge(:TAG)` or `via LinkType` or nothing (= `@edge(:target_upper)`).
match self.peek() {
Kind::At => {
self.advance();
let ann = self.expect_ident("annotation name")?;
if ann != "edge" {
bail!("line {}: expected @edge, got @{ann}", self.peek_line());
}
self.expect(&Kind::LParen, "'('")?;
self.expect(&Kind::Colon, "':'")?;
let tag = self.expect_ident("edge tag")?;
self.expect(&Kind::RParen, "')'")?;
Ok((FieldTy::MultiEdge { target, tag: Some(tag) }, true))
}
Kind::KwVia => {
self.advance();
let link = self.expect_ident("link type name")?;
Ok((FieldTy::MultiVia { target, link }, true))
}
_ => Ok((FieldTy::MultiEdge { target, tag: None }, true))
}
}
Kind::KwBacklink => {
self.advance();
let target = self.expect_ident("backlink target type")?;
self.expect(&Kind::Dot, "'.'")?;
let field = self.expect_ident("backlink field")?;
Ok((FieldTy::Backlink { target, field }, true))
}
Kind::LBracket => {
self.advance();
let (inner, _) = self.parse_field_ty()?;
self.expect(&Kind::RBracket, "']'")?;
Ok((FieldTy::Array(Box::new(inner)), false))
}
Kind::LBrace => {
self.advance();
let mut fields = Vec::new();
loop {
self.skip_newlines();
if matches!(self.peek(), Kind::RBrace) { self.advance(); break; }
if matches!(self.peek(), Kind::End) { bail!("unexpected end in struct type"); }
fields.push(self.parse_field()?);
// Field end can be comma or just newline
self.accept(&Kind::Comma);
}
Ok((FieldTy::Struct(fields), false))
}
Kind::Ident(_) => {
let first = self.expect_ident("type name")?;
// Try tagged union: IDENT | IDENT | IDENT
if matches!(self.peek(), Kind::Pipe) {
let mut variants = vec![first];
while matches!(self.peek(), Kind::Pipe) {
self.advance();
let next = self.expect_ident("union variant")?;
variants.push(next);
}
Ok((FieldTy::Union(variants), false))
} else {
Ok((FieldTy::Scalar(first), false))
}
}
other => bail!("line {}: expected type, got {other}", self.peek_line()),
}
}
fn parse_default_expr(&mut self) -> Result<DefaultExpr> {
// Fast path: a single literal followed by newline / `}` / `,`.
if let Some(lit) = self.try_standalone_literal() {
return Ok(lit);
}
// `now` or `now()` — recognise before falling into opaque slurp.
if matches!(self.peek(), Kind::Ident(ref s) if s == "now") {
self.advance();
if matches!(self.peek(), Kind::LParen) {
self.advance();
if matches!(self.peek(), Kind::RParen) { self.advance(); }
}
// Only honour if the expression ends here; otherwise fall through
// to the opaque path — `now + 5` etc. becomes opaque.
if matches!(self.peek(), Kind::Newline | Kind::Comma | Kind::RBrace | Kind::End) {
return Ok(DefaultExpr::Now);
}
}
// Otherwise slurp a balanced expression until newline at depth 0.
// This handles `now()`, `count(...)`, `words(meta.body_md)`, `self.xxx`,
// and anything else we haven't modelled explicitly. Stage 2 treats the
// result as opaque for execution, but the parser survives intact.
let mut buf = String::new();
let mut depth = 0i32;
loop {
match self.peek() {
Kind::End => break,
Kind::Newline | Kind::Comma if depth == 0 => break,
Kind::RBrace if depth == 0 => break,
Kind::LBrace | Kind::LBracket | Kind::LParen => {
depth += 1;
let t = self.advance();
buf.push_str(&format!("{} ", t.kind));
}
Kind::RBrace | Kind::RBracket | Kind::RParen => {
depth -= 1;
let t = self.advance();
buf.push_str(&format!("{} ", t.kind));
}
_ => {
let t = self.advance();
buf.push_str(&format!("{} ", t.kind));
}
}
}
let trimmed = buf.trim().to_string();
// Friendly recognition of common forms.
if trimmed.starts_with("now ") || trimmed == "now" {
return Ok(DefaultExpr::Now);
}
Ok(DefaultExpr::Opaque(trimmed))
}
/// If the next token is a self-contained literal (not followed by more
/// expression tokens), consume and return it. Otherwise leave the cursor
/// alone and return None so the opaque path takes over.
fn try_standalone_literal(&mut self) -> Option<DefaultExpr> {
// Peek one ahead to decide if the literal is the whole expression.
let follower_ends_expr = match self.toks.get(self.pos + 1).map(|t| &t.kind) {
Some(Kind::Newline) | Some(Kind::End) | Some(Kind::RBrace) | Some(Kind::Comma) => true,
_ => false,
};
if !follower_ends_expr { return None; }
match self.peek().clone() {
Kind::Str(s) => { self.advance(); Some(DefaultExpr::Str(s)) }
Kind::Int(n) => { self.advance(); Some(DefaultExpr::Int(n)) }
Kind::KwTrue => { self.advance(); Some(DefaultExpr::Bool(true)) }
Kind::KwFalse => { self.advance(); Some(DefaultExpr::Bool(false)) }
Kind::KwNull => { self.advance(); Some(DefaultExpr::Null) }
Kind::Ident(s) => { self.advance(); Some(DefaultExpr::Enum(s)) }
_ => None,
}
}
// --- service ---
fn parse_service(&mut self) -> Result<ServiceDecl> {
self.expect(&Kind::KwService, "`service`")?;
let kind = match self.peek() {
Kind::KwRest => { self.advance(); ServiceKind::Rest }
Kind::KwGraphql => { self.advance(); ServiceKind::Graphql }
Kind::KwNative => { self.advance(); ServiceKind::Native }
Kind::Ident(s) if s == "rest" => { self.advance(); ServiceKind::Rest }
Kind::Ident(s) if s == "graphql" => { self.advance(); ServiceKind::Graphql }
Kind::Ident(s) if s == "native" => { self.advance(); ServiceKind::Native }
other => bail!("line {}: expected `rest`/`graphql`/`native`, got {other}", self.peek_line()),
};
let path = match self.peek().clone() {
Kind::Str(s) => { self.advance(); s }
other => bail!("line {}: expected service path string, got {other}", self.peek_line()),
};
self.skip_newlines();
self.expect(&Kind::KwExpose, "`expose`")?;
let mut expose = Vec::new();
loop {
let name = self.expect_ident("operation name")?;
expose.push(Operation::from_ident(&name));
if !self.accept(&Kind::Comma) { break; }
}
Ok(ServiceDecl { kind, path, expose })
}
// --- class methods (plan 13b) ---
/// `fn name([p: T, ...]) [-> Ret] [in txn [snapshot|serializable]] { body }`
fn parse_method(&mut self) -> Result<MethodDecl> {
self.expect(&Kind::KwFn, "`fn`")?;
let name = self.expect_ident("method name")?;
self.expect(&Kind::LParen, "'('")?;
let mut params = Vec::new();
self.skip_newlines();
while !matches!(self.peek(), Kind::RParen) {
let pname = self.expect_ident("parameter name")?;
self.expect(&Kind::Colon, "':'")?;
let pty = self.expect_ident("parameter type")?;
params.push((pname, pty));
self.skip_newlines();
if !self.accept(&Kind::Comma) { break; }
self.skip_newlines();
}
self.expect(&Kind::RParen, "')'")?;
let mut ret = None;
if self.accept(&Kind::Arrow) {
// `-> Money` or `-> [Price]` — diagnostic-only in Stage 2.
if self.accept(&Kind::LBracket) {
let inner = self.expect_ident("return type")?;
self.expect(&Kind::RBracket, "']'")?;
ret = Some(format!("[{inner}]"));
} else {
ret = Some(self.expect_ident("return type")?);
}
}
let mut txn = TxnMode::None;
if self.accept(&Kind::KwIn) {
self.expect(&Kind::KwTxn, "`txn`")?;
txn = match self.peek() {
Kind::KwSnapshot => { self.advance(); TxnMode::Snapshot }
Kind::KwSerializable => { self.advance(); TxnMode::Serializable }
_ => TxnMode::Txn,
};
}
self.skip_newlines();
self.expect(&Kind::LBrace, "'{' to open method body")?;
let body = self.parse_stmt_block()?;
Ok(MethodDecl { name, params, ret, txn, body })
}
/// Statements until the matching `}` (consumed).
fn parse_stmt_block(&mut self) -> Result<Vec<Stmt>> {
let mut stmts = Vec::new();
loop {
self.skip_newlines();
while self.accept(&Kind::Semicolon) { self.skip_newlines(); }
match self.peek() {
Kind::RBrace => { self.advance(); return Ok(stmts); }
Kind::End => bail!("unexpected end of input inside method body"),
_ => stmts.push(self.parse_stmt()?),
}
}
}
fn parse_stmt(&mut self) -> Result<Stmt> {
let stmt = match self.peek() {
Kind::KwLet => {
self.advance();
let name = self.expect_ident("binding name")?;
self.expect(&Kind::Eq, "'='")?;
let expr = self.parse_expr()?;
Stmt::Let { name, expr }
}
// Schema-layer DML `insert` is lowercase and deliberately NOT a
// lexer keyword (only SQL-layer `INSERT` is) — same rule that
// keeps `subscribe`/`me`/`self` usable as plain identifiers.
Kind::Ident(s) if s == "insert" => {
self.advance();
let ty = self.expect_ident("type name after `insert`")?;
self.expect(&Kind::LBrace, "'{'")?;
let mut fields = Vec::new();
loop {
self.skip_newlines();
if self.accept(&Kind::RBrace) { break; }
let fname = self.expect_ident("field name")?;
self.expect(&Kind::Colon, "':'")?;
let expr = self.parse_expr()?;
fields.push((fname, expr));
self.skip_newlines();
self.accept(&Kind::Comma);
}
Stmt::Insert { ty, fields }
}
Kind::KwReturn => {
self.advance();
let expr = if matches!(self.peek(),
Kind::Semicolon | Kind::Newline | Kind::RBrace | Kind::End)
{ None } else { Some(self.parse_expr()?) };
Stmt::Return { expr }
}
Kind::KwAssert => {
self.advance();
let cond = self.parse_expr()?;
let mut msg = None;
if self.accept(&Kind::KwOtherwise) {
self.expect(&Kind::KwAbort, "`abort`")?;
if let Kind::Str(s) = self.peek().clone() {
self.advance();
msg = Some(s);
}
}
Stmt::Assert { cond, msg }
}
Kind::KwIf => {
self.advance();
let cond = self.parse_expr()?;
self.skip_newlines();
self.expect(&Kind::LBrace, "'{' after if condition")?;
let then = self.parse_stmt_block()?;
let mut otherwise = Vec::new();
// `else` may sit on the next line.
let mark = self.pos;
self.skip_newlines();
if self.accept(&Kind::KwElse) {
self.skip_newlines();
if matches!(self.peek(), Kind::KwIf) {
otherwise.push(self.parse_stmt()?); // else-if chain
} else {
self.expect(&Kind::LBrace, "'{' after else")?;
otherwise = self.parse_stmt_block()?;
}
} else {
self.pos = mark; // no else — restore consumed newlines
}
return Ok(Stmt::If { cond, then, otherwise });
}
other => bail!(
"line {}: unsupported statement in method body: {other} \
(13b executes `let`/`insert`/`return`/`assert`/`if`)",
self.peek_line()
),
};
// Statement terminator: `;`, newline, or the closing `}`.
match self.peek() {
Kind::Semicolon | Kind::Newline => { self.advance(); }
Kind::RBrace | Kind::End => {}
other => bail!("line {}: expected end of statement, got {other}", self.peek_line()),
}
Ok(stmt)
}
// --- expressions (precedence climbing: or < and < cmp < add < mul < unary < postfix) ---
fn parse_expr(&mut self) -> Result<Expr> {
self.parse_or()
}
fn parse_or(&mut self) -> Result<Expr> {
let mut lhs = self.parse_and()?;
while self.accept(&Kind::KwOr) {
let rhs = self.parse_and()?;
lhs = Expr::Binary(BinOp::Or, Box::new(lhs), Box::new(rhs));
}
Ok(lhs)
}
fn parse_and(&mut self) -> Result<Expr> {
let mut lhs = self.parse_cmp()?;
while self.accept(&Kind::KwAnd) {
let rhs = self.parse_cmp()?;
lhs = Expr::Binary(BinOp::And, Box::new(lhs), Box::new(rhs));
}
Ok(lhs)
}
fn parse_cmp(&mut self) -> Result<Expr> {
let lhs = self.parse_add()?;
let op = match self.peek() {
Kind::EqEq => BinOp::Eq,
Kind::NotEq => BinOp::Ne,
Kind::Lt => BinOp::Lt,
Kind::LtEq => BinOp::Le,
Kind::Gt => BinOp::Gt,
Kind::GtEq => BinOp::Ge,
_ => return Ok(lhs),
};
self.advance();
let rhs = self.parse_add()?;
Ok(Expr::Binary(op, Box::new(lhs), Box::new(rhs)))
}
fn parse_add(&mut self) -> Result<Expr> {
let mut lhs = self.parse_mul()?;
loop {
let op = match self.peek() {
Kind::Plus => BinOp::Add,
Kind::Dash => BinOp::Sub,
_ => return Ok(lhs),
};
self.advance();
let rhs = self.parse_mul()?;
lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
}
}
fn parse_mul(&mut self) -> Result<Expr> {
let mut lhs = self.parse_unary()?;
loop {
let op = match self.peek() {
Kind::Star => BinOp::Mul,
Kind::Slash => BinOp::Div,
Kind::Percent => BinOp::Mod,
_ => return Ok(lhs),
};
self.advance();
let rhs = self.parse_unary()?;
lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
}
}
fn parse_unary(&mut self) -> Result<Expr> {
match self.peek() {
Kind::Dash => { self.advance(); Ok(Expr::Unary(UnOp::Neg, Box::new(self.parse_unary()?))) }
Kind::KwNot => { self.advance(); Ok(Expr::Unary(UnOp::Not, Box::new(self.parse_unary()?))) }
_ => self.parse_postfix(),
}
}
/// Primary followed by `.field` chains.
fn parse_postfix(&mut self) -> Result<Expr> {
let mut e = self.parse_primary()?;
while self.accept(&Kind::Dot) {
let field = self.expect_ident("field name after '.'")?;
e = Expr::Field(Box::new(e), field);
}
Ok(e)
}
fn parse_primary(&mut self) -> Result<Expr> {
match self.peek().clone() {
Kind::Int(n) => { self.advance(); Ok(Expr::Int(n)) }
Kind::Str(s) => { self.advance(); Ok(Expr::Str(s)) }
Kind::KwTrue => { self.advance(); Ok(Expr::Bool(true)) }
Kind::KwFalse => { self.advance(); Ok(Expr::Bool(false)) }
Kind::KwNull => { self.advance(); Ok(Expr::Null) }
Kind::LParen => {
self.advance();
let e = self.parse_expr()?;
self.expect(&Kind::RParen, "')'")?;
Ok(e)
}
Kind::Ident(name) => {
self.advance();
// `select Type{ ... }` — schema-layer select expression.
// Lowercase `select` is an ident (only SQL `SELECT` is a
// keyword); the two-token shape Ident + LBrace disambiguates
// it from a variable named `select`.
if name == "select"
&& matches!(self.peek(), Kind::Ident(_))
&& matches!(self.toks.get(self.pos + 1).map(|t| &t.kind), Some(Kind::LBrace))
{
return self.parse_select_expr();
}
// `name(args)` — call form.
if self.accept(&Kind::LParen) {
let mut args = Vec::new();
if !matches!(self.peek(), Kind::RParen) {
loop {
args.push(self.parse_expr()?);
if !self.accept(&Kind::Comma) { break; }
}
}
self.expect(&Kind::RParen, "')'")?;
Ok(Expr::Call(name, args))
} else {
Ok(Expr::Ident(name))
}
}
other => bail!("line {}: expected expression, got {other}", self.peek_line()),
}
}
/// `select Type{ entries }` — per § Brace Disambiguation: an entry with a
/// comparison operator is a predicate; a bare identifier is a projection.
/// (`select` and the type name are already consumed up to the ident.)
fn parse_select_expr(&mut self) -> Result<Expr> {
let ty = self.expect_ident("type name after `select`")?;
self.expect(&Kind::LBrace, "'{'")?;
let mut predicates = Vec::new();
let mut projection = Vec::new();
loop {
self.skip_newlines();
if self.accept(&Kind::RBrace) { break; }
let field = self.expect_ident("field name")?;
let op = match self.peek() {
Kind::EqEq => Some(BinOp::Eq),
Kind::NotEq => Some(BinOp::Ne),
Kind::Lt => Some(BinOp::Lt),
Kind::LtEq => Some(BinOp::Le),
Kind::Gt => Some(BinOp::Gt),
Kind::GtEq => Some(BinOp::Ge),
_ => None,
};
match op {
Some(op) => {
self.advance();
// RHS is an additive expression — comparisons don't chain.
let rhs = self.parse_add()?;
predicates.push((field, op, Box::new(rhs)));
}
None => projection.push(field),
}
self.skip_newlines();
self.accept(&Kind::Comma);
}
Ok(Expr::Select { ty, predicates, projection })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_simple_type() {
let src = r#"
type Article {
id: Id
title: Text
service rest "/api/articles" expose list, get, create
}
"#;
let sch = parse(src).unwrap();
assert_eq!(sch.types.len(), 1);
let t = &sch.types[0];
assert_eq!(t.name, "Article");
assert_eq!(t.fields.len(), 2);
assert_eq!(t.fields[0].name, "id");
assert_eq!(t.fields[1].name, "title");
assert_eq!(t.services.len(), 1);
assert_eq!(t.services[0].path, "/api/articles");
assert_eq!(t.services[0].expose, vec![
Operation::List, Operation::Get, Operation::Create,
]);
}
#[test]
fn parses_nullable_and_unique_and_default() {
let src = r#"
type User {
email: Email @unique
name: Text
avatar: Url?
joined: Timestamp = now()
admin: Bool = false
}
"#;
let sch = parse(src).unwrap();
let t = &sch.types[0];
assert!(t.fields[0].unique);
assert!(t.fields[2].nullable);
assert!(matches!(t.fields[3].default, Some(DefaultExpr::Now)));
assert!(matches!(t.fields[4].default, Some(DefaultExpr::Bool(false))));
}
#[test]
fn parses_relations() {
let src = r#"
type Article {
author: ref User
tags: multi Tag @edge(:TAGGED_AS)
related: multi Article @edge(:RELATED_TO)
}
"#;
let sch = parse(src).unwrap();
let t = &sch.types[0];
assert!(matches!(t.fields[0].ty, FieldTy::Ref(ref n) if n == "User"));
assert!(matches!(t.fields[1].ty, FieldTy::MultiEdge { ref target, .. } if target == "Tag"));
}
#[test]
fn skips_policy_and_on_trigger() {
let src = r#"
type Article {
id: Id
title: Text
policy read anyone
policy write for role Admin
on update when old.published == false and new.published == true
do set self.published_at = now()
service rest "/api/articles" expose list, get
}
"#;
let sch = parse(src).unwrap();
let t = &sch.types[0];
assert_eq!(t.name, "Article");
assert_eq!(t.fields.len(), 2);
assert_eq!(t.services.len(), 1);
}
#[test]
fn parses_inline_struct() {
let src = r#"
type Product {
meta: {
title: Text
tags: [Text]
}
}
"#;
let sch = parse(src).unwrap();
let t = &sch.types[0];
if let FieldTy::Struct(fs) = &t.fields[0].ty {
assert_eq!(fs.len(), 2);
assert!(matches!(fs[1].ty, FieldTy::Array(_)));
} else { panic!("expected struct") }
}
#[test]
fn parses_union() {
let src = r#"
type Order { status: Pending | Paid | Shipped }
"#;
let sch = parse(src).unwrap();
if let FieldTy::Union(v) = &sch.types[0].fields[0].ty {
assert_eq!(v, &vec!["Pending".to_string(), "Paid".into(), "Shipped".into()]);
} else { panic!("expected union") }
}
#[test]
fn article_debug() {
let src = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"), "/../../docs/examples/blog/types/article.wo"
)).unwrap();
let sch = parse(&src).unwrap();
eprintln!("types: {}", sch.types.len());
for t in &sch.types {
eprintln!(" type {} — {} fields, {} services", t.name, t.fields.len(), t.services.len());
for f in &t.fields { eprintln!(" field: {}", f.name); }
for svc in &t.services { eprintln!(" service {:?} {} expose {:?}", svc.kind, svc.path, svc.expose); }
}
assert!(sch.types.iter().any(|t| t.name == "Article" && !t.services.is_empty()),
"Article should have a service");
}
#[test]
fn parses_class_with_methods() {
let src = r#"
class Product {
id: Id
sku: SKU @unique
name: Text
prices: multi Price
fn current_price() -> Money in txn {
return latest(self.prices).amount;
}
fn set_price(amount: Money) in txn {
insert Price { product: self.id, amount: amount };
}
service rest "/api/products"
expose list, get, create, update, delete, subscribe
}
"#;
let sch = parse(src).unwrap();
assert_eq!(sch.types.len(), 1);
let t = &sch.types[0];
assert!(t.is_class);
assert_eq!(t.name, "Product");
assert_eq!(t.fields.len(), 4);
assert!(matches!(t.fields[3].ty, FieldTy::MultiEdge { ref target, .. } if target == "Price"));
assert_eq!(t.services.len(), 1);
assert_eq!(t.services[0].path, "/api/products");
assert_eq!(t.services[0].expose.len(), 6);
// 13b: methods parse into real AST.
assert_eq!(t.methods.len(), 2);
let cp = &t.methods[0];
assert_eq!(cp.name, "current_price");
assert!(cp.params.is_empty());
assert_eq!(cp.ret.as_deref(), Some("Money"));
assert_eq!(cp.txn, TxnMode::Txn);
assert_eq!(cp.body.len(), 1);
assert!(matches!(cp.body[0], Stmt::Return { expr: Some(_) }));
let sp = &t.methods[1];
assert_eq!(sp.name, "set_price");
assert_eq!(sp.params, vec![("amount".to_string(), "Money".to_string())]);
assert_eq!(sp.txn, TxnMode::Txn);
assert!(matches!(&sp.body[0],
Stmt::Insert { ty, fields } if ty == "Price" && fields.len() == 2));
}
#[test]
fn parses_table_annotation() {
let src = r#"
@table(name: "prices", index: [product, at], index: [sku])
class Price {
id: Id
product: ref Product
amount: Money
at: Timestamp = now()
sku: SKU
}
@table
type Note { id: Id }
type Plain { id: Id }
"#;
let sch = parse(src).unwrap();
assert_eq!(sch.types.len(), 3);
let p = &sch.types[0];
assert_eq!(p.table.name.as_deref(), Some("prices"));
assert_eq!(p.table.indexes, vec![
vec!["product".to_string(), "at".to_string()],
vec!["sku".to_string()],
]);
// bare @table = legal no-op config
let n = &sch.types[1];
assert!(n.table.name.is_none() && n.table.indexes.is_empty());
assert!(sch.types[2].table.indexes.is_empty());
}
#[test]
fn table_annotation_error_cases() {
// Reserved-for-later keys error loudly — no silent passthrough.
let err = parse("@table(shard_key: sku)\ntype T { id: Id }").unwrap_err().to_string();
assert!(err.contains("unknown @table argument `shard_key`"), "{err}");
// @table must be followed by a type/class.
assert!(parse("@table(name: \"x\")\nfn stray() {}").is_err());
// Unknown annotation NAMES skip silently (field-annotation precedent).
let sch = parse("@experimental(anything, at: all)\ntype T { id: Id }").unwrap();
assert_eq!(sch.types.len(), 1);
assert_eq!(sch.types[0].name, "T");
}
#[test]
fn parses_select_expression() {
let src = r#"
class Product {
id: Id
fn history() -> [Price] in txn {
return select Price{ product == self.id, amount, at };
}
}
"#;
let m = &parse(src).unwrap().types[0].methods[0];
let Stmt::Return { expr: Some(Expr::Select { ty, predicates, projection }) } = &m.body[0]
else { panic!("expected return select, got {:?}", m.body[0]) };
assert_eq!(ty, "Price");
assert_eq!(predicates.len(), 1);
assert_eq!(predicates[0].0, "product");
assert!(matches!(predicates[0].1, BinOp::Eq));
assert!(matches!(&*predicates[0].2, Expr::Field(b, f) if f == "id"
&& matches!(&**b, Expr::Ident(s) if s == "self")));
assert_eq!(projection, &vec!["amount".to_string(), "at".to_string()]);
}
#[test]
fn method_body_expression_ast() {
let src = r#"
class Price {
id: Id
amount: Money
fn discounted(pct: Int) -> Money {
return self.amount * (100 - pct) / 100;
}
}
"#;
let sch = parse(src).unwrap();
let m = &sch.types[0].methods[0];
assert_eq!(m.txn, TxnMode::None);
let Stmt::Return { expr: Some(e) } = &m.body[0] else { panic!("expected return") };
// ((self.amount * (100 - pct)) / 100) — mul level is left-associative.
let Expr::Binary(BinOp::Div, lhs, rhs) = e else { panic!("expected /: {e:?}") };
assert!(matches!(**rhs, Expr::Int(100)));
let Expr::Binary(BinOp::Mul, base, paren) = &**lhs else { panic!("expected *") };
assert!(matches!(&**base, Expr::Field(b, f) if f == "amount"
&& matches!(&**b, Expr::Ident(s) if s == "self")));
assert!(matches!(&**paren, Expr::Binary(BinOp::Sub, _, _)));
}
#[test]
fn class_method_braces_do_not_truncate_body() {
// The method body's `}` and nested `{ ... }` literals must not be
// mistaken for the class's closing brace — fields AFTER the methods
// must still parse, and a following declaration must be seen.
let src = r#"
class Price {
id: Id
fn discounted(pct: Int) -> Money {
if pct > 0 {
return self.amount * (100 - pct) / 100;
}
return self.amount;
}
amount: Money
currency: Text = "EUR"
}
type Audit { id: Id }
"#;
let sch = parse(src).unwrap();
assert_eq!(sch.types.len(), 2);
let p = &sch.types[0];
assert!(p.is_class);
assert_eq!(p.fields.len(), 3, "fields after the method must parse");
assert_eq!(p.fields[1].name, "amount");
assert!(!sch.types[1].is_class);
assert_eq!(sch.types[1].name, "Audit");
}
}