Add pipe operator, with-update, retry/fallback, reason, parallel, trace, contract, deploy
Implements 8 new language features:
- |> pipe operator: a |> f desugars to f(a), left-associative chains
- with record update: let b = a with { field: val } — non-destructive struct update
- retry/fallback: retry N times { ... } fallback { ... } with counter-based loop codegen
- reason: AI inference primitive calling soma /v1/chat/completions at runtime
- parallel: concurrent execution block returning a Map of named results via threads
- trace: zero-cost observability block emitting TraceBegin/TraceEnd with ms timing
- requires: precondition annotation on fn, emits ContractCheck bytecode at entry
- deploy: deployment-as-syntax posting to soma /v1/deploy at runtime
All features thread through lexer → parser/AST → codegen → runtime interpreter.
This commit is contained in:
@@ -126,6 +126,24 @@ pub enum Expr {
|
||||
fields: Vec<(String, Expr)>,
|
||||
span: Span,
|
||||
},
|
||||
/// Record update: `a with { field: new_val }`
|
||||
With {
|
||||
base: Box<Expr>,
|
||||
updates: Vec<(String, Expr)>,
|
||||
},
|
||||
/// AI inference: `reason "query"`
|
||||
Reason {
|
||||
query: String,
|
||||
},
|
||||
/// Concurrent execution: `parallel { name: expr, ... }`
|
||||
Parallel {
|
||||
entries: Vec<(String, Expr)>,
|
||||
},
|
||||
/// Trace block: `trace "label" { stmts }`
|
||||
Trace {
|
||||
label: String,
|
||||
body: Vec<Stmt>,
|
||||
},
|
||||
}
|
||||
|
||||
// ── Match arm ─────────────────────────────────────────────────────────────────
|
||||
@@ -198,7 +216,7 @@ pub enum Stmt {
|
||||
Return(Expr, Span),
|
||||
/// A bare expression used as a statement (usually a call).
|
||||
Expr(Expr, Span),
|
||||
/// `fn name<T, E>(params) -> ReturnType { body }` (with optional decorators)
|
||||
/// `fn name<T, E>(params) -> ReturnType [requires cond] { body }` (with optional decorators)
|
||||
FnDef {
|
||||
name: String,
|
||||
decorators: Vec<Decorator>,
|
||||
@@ -206,6 +224,8 @@ pub enum Stmt {
|
||||
type_params: Vec<String>,
|
||||
params: Vec<Param>,
|
||||
return_type: TypeExpr,
|
||||
/// Optional precondition: `requires expr`
|
||||
requires: Option<Box<Expr>>,
|
||||
body: Vec<Stmt>,
|
||||
span: Span,
|
||||
},
|
||||
@@ -252,6 +272,20 @@ pub enum Stmt {
|
||||
methods: Vec<Stmt>,
|
||||
span: Span,
|
||||
},
|
||||
/// `retry N times { ... } fallback { ... }`
|
||||
Retry {
|
||||
count: Expr,
|
||||
body: Vec<Stmt>,
|
||||
fallback: Option<Vec<Stmt>>,
|
||||
span: Span,
|
||||
},
|
||||
/// `deploy fn_name to "/route" via target`
|
||||
Deploy {
|
||||
fn_name: String,
|
||||
route: String,
|
||||
target: String,
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
// ── Top-level program ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -112,6 +112,17 @@ impl Parser {
|
||||
Token::Import => "import".to_string(),
|
||||
Token::From => "from".to_string(),
|
||||
Token::As => "as".to_string(),
|
||||
Token::With => "with".to_string(),
|
||||
Token::Retry => "retry".to_string(),
|
||||
Token::Times => "times".to_string(),
|
||||
Token::Fallback => "fallback".to_string(),
|
||||
Token::Reason => "reason".to_string(),
|
||||
Token::Parallel => "parallel".to_string(),
|
||||
Token::Trace => "trace".to_string(),
|
||||
Token::Requires => "requires".to_string(),
|
||||
Token::Deploy => "deploy".to_string(),
|
||||
Token::To => "to".to_string(),
|
||||
Token::Via => "via".to_string(),
|
||||
tok => return Err(ParseError::new(
|
||||
ParseErrorKind::ExpectedIdent(tok.to_string()),
|
||||
span,
|
||||
@@ -162,6 +173,8 @@ impl Parser {
|
||||
Token::From => self.parse_from_import(start),
|
||||
Token::Protocol => self.parse_protocol_def(start),
|
||||
Token::Impl => self.parse_impl_def(start),
|
||||
Token::Retry => self.parse_retry(start),
|
||||
Token::Deploy => self.parse_deploy(start),
|
||||
Token::Return => {
|
||||
self.advance(); // consume `return`
|
||||
let expr = self.parse_expr()?;
|
||||
@@ -351,10 +364,16 @@ impl Parser {
|
||||
self.expect(&Token::RParen)?;
|
||||
self.expect(&Token::Arrow)?;
|
||||
let return_type = self.parse_type_expr_with_params(&type_params)?;
|
||||
// Optional `requires expr`
|
||||
let requires = if self.eat(&Token::Requires) {
|
||||
Some(Box::new(self.parse_expr()?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.expect(&Token::LBrace)?;
|
||||
let body = self.parse_block_body()?;
|
||||
self.expect(&Token::RBrace)?;
|
||||
Ok(Stmt::FnDef { name, decorators, type_params, params, return_type, body, span: start })
|
||||
Ok(Stmt::FnDef { name, decorators, type_params, params, return_type, requires, body, span: start })
|
||||
}
|
||||
|
||||
/// Parse one or more `@decorator` annotations, then the `fn` definition.
|
||||
@@ -497,6 +516,40 @@ impl Parser {
|
||||
Ok(Stmt::ImplDef { protocol_name, type_name, methods, span: start })
|
||||
}
|
||||
|
||||
/// Parse `retry N times { ... } fallback { ... }`
|
||||
fn parse_retry(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||
self.expect(&Token::Retry)?;
|
||||
let count = self.parse_expr()?;
|
||||
self.expect(&Token::Times)?;
|
||||
self.expect(&Token::LBrace)?;
|
||||
let body = self.parse_block_body()?;
|
||||
self.expect(&Token::RBrace)?;
|
||||
let fallback = if self.eat(&Token::Fallback) {
|
||||
self.expect(&Token::LBrace)?;
|
||||
let fb = self.parse_block_body()?;
|
||||
self.expect(&Token::RBrace)?;
|
||||
Some(fb)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Stmt::Retry { count, body, fallback, span: start })
|
||||
}
|
||||
|
||||
/// Parse `deploy fn_name to "/route" via target`
|
||||
fn parse_deploy(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||
self.expect(&Token::Deploy)?;
|
||||
let (fn_name, _) = self.expect_ident()?;
|
||||
self.expect(&Token::To)?;
|
||||
let route = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string literal (route)", &tok, self.peek_span())),
|
||||
};
|
||||
self.expect(&Token::Via)?;
|
||||
let (target, _) = self.expect_ident()?;
|
||||
self.eat(&Token::Semicolon);
|
||||
Ok(Stmt::Deploy { fn_name, route, target, span: start })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn parse_param_list(&mut self) -> Result<Vec<Param>, ParseError> {
|
||||
self.parse_param_list_with_type_params(&[])
|
||||
@@ -649,7 +702,22 @@ impl Parser {
|
||||
// ── Expressions ───────────────────────────────────────────────────────────
|
||||
|
||||
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
self.parse_or_expr()
|
||||
self.parse_pipe_expr()
|
||||
}
|
||||
|
||||
/// pipe_expr = or_expr (|> ident)*
|
||||
/// `a |> f` desugars to `Call(f, [a])`
|
||||
fn parse_pipe_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
let mut left = self.parse_or_expr()?;
|
||||
while self.eat(&Token::PipeOp) {
|
||||
// RHS must be a callable (ident or path)
|
||||
let func_expr = self.parse_postfix()?;
|
||||
left = Expr::Call {
|
||||
func: Box::new(func_expr),
|
||||
args: vec![left],
|
||||
};
|
||||
}
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
@@ -765,6 +833,20 @@ impl Parser {
|
||||
self.advance();
|
||||
expr = Expr::Try(Box::new(expr));
|
||||
}
|
||||
Token::With => {
|
||||
self.advance(); // consume `with`
|
||||
self.expect(&Token::LBrace)?;
|
||||
let mut updates = Vec::new();
|
||||
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||
let (field_name, _) = self.expect_ident()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
let field_expr = self.parse_expr()?;
|
||||
updates.push((field_name, field_expr));
|
||||
if !self.eat(&Token::Comma) { break; }
|
||||
}
|
||||
self.expect(&Token::RBrace)?;
|
||||
expr = Expr::With { base: Box::new(expr), updates };
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
@@ -938,6 +1020,48 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
// reason "query"
|
||||
Token::Reason => {
|
||||
self.advance();
|
||||
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::Reason { query })
|
||||
}
|
||||
|
||||
// parallel { name: expr, ... }
|
||||
Token::Parallel => {
|
||||
self.advance();
|
||||
self.expect(&Token::LBrace)?;
|
||||
let mut entries = Vec::new();
|
||||
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||
let (entry_name, _) = self.expect_ident()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
let entry_expr = self.parse_expr()?;
|
||||
entries.push((entry_name, entry_expr));
|
||||
if !self.eat(&Token::Comma) {
|
||||
self.eat(&Token::Semicolon);
|
||||
}
|
||||
if matches!(self.peek(), Token::RBrace) { break; }
|
||||
}
|
||||
self.expect(&Token::RBrace)?;
|
||||
Ok(Expr::Parallel { entries })
|
||||
}
|
||||
|
||||
// trace "label" { stmts }
|
||||
Token::Trace => {
|
||||
self.advance();
|
||||
let label = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string literal (trace label)", &tok, self.peek_span())),
|
||||
};
|
||||
self.expect(&Token::LBrace)?;
|
||||
let body = self.parse_block_body()?;
|
||||
self.expect(&Token::RBrace)?;
|
||||
Ok(Expr::Trace { label, body })
|
||||
}
|
||||
|
||||
tok => Err(ParseError::new(
|
||||
ParseErrorKind::InvalidExprStart(tok.to_string()),
|
||||
span,
|
||||
|
||||
Reference in New Issue
Block a user