use crate::ast::*;
use crate::tokens::{is_declaration_keyword, Token, TokenType, Trivia, TriviaKind};
const fn is_comment_token(tt: &TokenType) -> bool {
matches!(
tt,
TokenType::LineComment
| TokenType::BlockComment
| TokenType::DocLineComment
| TokenType::DocBlockComment
| TokenType::InnerDocLineComment
| TokenType::InnerDocBlockComment
)
}
const fn token_to_trivia_kind(tt: &TokenType) -> Option<TriviaKind> {
match tt {
TokenType::LineComment => Some(TriviaKind::Line),
TokenType::BlockComment => Some(TriviaKind::Block),
TokenType::DocLineComment => Some(TriviaKind::DocLine),
TokenType::DocBlockComment => Some(TriviaKind::DocBlock),
TokenType::InnerDocLineComment => Some(TriviaKind::InnerDocLine),
TokenType::InnerDocBlockComment => Some(TriviaKind::InnerDocBlock),
_ => None,
}
}
fn attach_trivia_to_decl(decl: &mut Declaration, leading: Vec<Trivia>, trailing: Vec<Trivia>) {
match decl {
Declaration::Import(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Persona(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Context(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Anchor(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Memory(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Tool(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Type(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Flow(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Intent(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Run(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Epistemic(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Let(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::LambdaData(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Agent(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Shield(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Pix(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Psyche(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Corpus(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Dataspace(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Ots(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Mandate(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Compute(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Daemon(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::AxonStore(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::AxonEndpoint(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Resource(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Fabric(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Manifest(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Observe(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Reconcile(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Lease(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Ensemble(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Session(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Topology(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Immune(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Reflex(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Heal(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Component(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::View(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Channel(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
Declaration::Generic(n) => {
n.leading_trivia = leading;
n.trailing_trivia = trailing;
}
}
}
pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
#[derive(Debug, Clone)]
pub struct SourceSnippet {
pub source: String,
pub line: u32,
pub column: u32,
pub filename: String,
pub context_before: usize,
pub context_after: usize,
}
impl SourceSnippet {
pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
Self {
source,
line,
column,
filename,
context_before: SOURCE_CONTEXT_LINES_BEFORE,
context_after: SOURCE_CONTEXT_LINES_AFTER,
}
}
#[must_use]
pub fn render(&self) -> String {
if self.source.is_empty() || self.line < 1 {
return String::new();
}
let raw: Vec<&str> = self.source.split('\n').collect();
let lines: Vec<&str> = if raw.last() == Some(&"") {
raw[..raw.len() - 1].to_vec()
} else {
raw
};
if lines.is_empty() || self.line as usize > lines.len() {
return String::new();
}
let line_idx = self.line as usize;
let start = line_idx.saturating_sub(self.context_before).max(1);
let end = (line_idx + self.context_after).min(lines.len());
let gutter = end.to_string().len();
let empty_gutter = " ".repeat(gutter);
let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
out.push(format!(
"{empty_gutter} --> {}:{}:{}",
self.filename, self.line, self.column
));
out.push(format!("{empty_gutter} |"));
for n in start..=end {
let line_text = lines[n - 1];
out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
if n == line_idx {
let line_len = line_text.chars().count();
let col = (self.column as usize).clamp(1, line_len + 1);
out.push(format!(
"{empty_gutter} | {pad}^",
pad = " ".repeat(col - 1)
));
}
}
out.join("\n")
}
}
#[derive(Debug, Clone, Default)]
pub struct ParseError {
pub message: String,
pub line: u32,
pub column: u32,
pub source_snippet: Option<SourceSnippet>,
}
impl ParseError {
#[must_use]
pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
if self.line >= 1 {
self.source_snippet = Some(SourceSnippet::new(
source.to_string(),
self.line,
self.column,
filename.to_string(),
));
}
self
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
if let Some(snippet) = &self.source_snippet {
let block = snippet.render();
if !block.is_empty() {
write!(f, "\n{block}")?;
}
}
Ok(())
}
}
impl std::error::Error for ParseError {}
#[derive(Debug)]
pub struct ParseResult {
pub program: Program,
pub errors: Vec<ParseError>,
}
impl ParseResult {
#[inline]
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
#[inline]
#[must_use]
pub fn is_clean(&self) -> bool {
self.errors.is_empty()
}
}
#[inline]
const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
matches!(
tt,
TokenType::Import
| TokenType::Persona
| TokenType::Context
| TokenType::Anchor
| TokenType::Memory
| TokenType::Tool
| TokenType::Type
| TokenType::Flow
| TokenType::Intent
| TokenType::Run
| TokenType::Let
| TokenType::Know
| TokenType::Believe
| TokenType::Speculate
| TokenType::Doubt
| TokenType::Lambda
| TokenType::Agent
| TokenType::Shield
| TokenType::Pix
| TokenType::Psyche
| TokenType::Corpus
| TokenType::Dataspace
| TokenType::Ots
| TokenType::Mandate
| TokenType::Compute
| TokenType::Daemon
| TokenType::AxonStore
| TokenType::AxonEndpoint
| TokenType::Resource
| TokenType::Fabric
| TokenType::Manifest
| TokenType::Observe
| TokenType::Reconcile
| TokenType::Lease
| TokenType::Ensemble
| TokenType::Session
| TokenType::Topology
| TokenType::Immune
| TokenType::Reflex
| TokenType::Heal
| TokenType::Component
| TokenType::View
| TokenType::Channel
| TokenType::Ingest
| TokenType::Persist
| TokenType::Retrieve
| TokenType::Mutate
| TokenType::Purge
| TokenType::Transact
| TokenType::Mcp
)
}
pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
&["axon", "openai", "kimi", "glm", "anthropic"];
pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
pub const AXONENDPOINT_METHOD_VALUES: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH"];
#[inline]
fn axonendpoint_is_valid_transport(s: &str) -> bool {
AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
}
#[inline]
fn axonendpoint_is_valid_method(s: &str) -> bool {
AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
}
#[inline]
fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
}
pub fn is_valid_capability_slug(slug: &str) -> bool {
if slug.is_empty() {
return false;
}
for segment in slug.split('.') {
if !is_valid_slug_segment(segment) {
return false;
}
}
true
}
fn is_valid_slug_segment(seg: &str) -> bool {
let mut chars = seg.chars();
let first = match chars.next() {
Some(c) => c,
None => return false,
};
if !first.is_ascii_lowercase() {
return false;
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
#[cfg(test)]
mod capability_slug_tests {
use super::is_valid_capability_slug;
#[test]
fn accepts_canonical_examples() {
assert!(is_valid_capability_slug("admin"));
assert!(is_valid_capability_slug("legal.read"));
assert!(is_valid_capability_slug("hipaa.phi.read"));
assert!(is_valid_capability_slug("bank.officer.senior"));
assert!(is_valid_capability_slug("a"));
assert!(is_valid_capability_slug("a_b"));
assert!(is_valid_capability_slug("a1"));
assert!(is_valid_capability_slug("a.b1_c"));
}
#[test]
fn rejects_empty_string() {
assert!(!is_valid_capability_slug(""));
}
#[test]
fn rejects_uppercase() {
assert!(!is_valid_capability_slug("Admin"));
assert!(!is_valid_capability_slug("admin.READ"));
}
#[test]
fn rejects_digit_first() {
assert!(!is_valid_capability_slug("1admin"));
assert!(!is_valid_capability_slug("admin.1read"));
}
#[test]
fn rejects_hyphen() {
assert!(!is_valid_capability_slug("bank-officer"));
}
#[test]
fn rejects_empty_segments() {
assert!(!is_valid_capability_slug("bank..a"));
assert!(!is_valid_capability_slug(".admin"));
assert!(!is_valid_capability_slug("admin."));
}
#[test]
fn rejects_special_chars() {
assert!(!is_valid_capability_slug("admin@read"));
assert!(!is_valid_capability_slug("admin/read"));
assert!(!is_valid_capability_slug("admin read"));
}
}
pub struct Parser {
tokens: Vec<Token>,
pos: usize,
leading_trivia: Vec<Vec<Trivia>>,
trailing_trivia: Vec<Vec<Trivia>>,
last_let_value_kind: String,
loop_depth: u32,
source: Option<String>,
filename: String,
}
impl Parser {
pub fn new(raw_tokens: Vec<Token>) -> Self {
let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
let mut pending_leading: Vec<Trivia> = Vec::new();
let mut last_effective_line: i64 = -1;
for tok in raw_tokens {
if is_comment_token(&tok.ttype) {
let kind = token_to_trivia_kind(&tok.ttype)
.expect("comment token must map to a trivia kind");
let triv = Trivia {
kind,
text: tok.value,
line: tok.line,
column: tok.column,
};
if !effective.is_empty() && (tok.line as i64) == last_effective_line {
trailing.last_mut().unwrap().push(triv);
} else {
pending_leading.push(triv);
}
} else {
last_effective_line = tok.line as i64;
effective.push(tok);
leading.push(std::mem::take(&mut pending_leading));
trailing.push(Vec::new());
}
}
Parser {
tokens: effective,
pos: 0,
leading_trivia: leading,
trailing_trivia: trailing,
last_let_value_kind: "literal".to_string(),
loop_depth: 0,
source: None,
filename: "<source>".to_string(),
}
}
#[must_use]
pub fn with_source(mut self, source: &str, filename: &str) -> Self {
self.source = Some(source.to_string());
self.filename = filename.to_string();
self
}
pub fn parse(&mut self) -> Result<Program, ParseError> {
let mut program = Program {
declarations: Vec::new(),
declaration_trivia: Vec::new(),
loc: Loc { line: 1, column: 1 },
};
while !self.check(TokenType::Eof) {
let start_pos = self.pos;
let mut decl = match self.parse_declaration() {
Ok(d) => d,
Err(e) => return Err(self.attach_source_to_error(e)),
};
let end_pos = self.pos.saturating_sub(1);
let leading = self
.leading_trivia
.get(start_pos)
.cloned()
.unwrap_or_default();
let trailing = self
.trailing_trivia
.get(end_pos)
.cloned()
.unwrap_or_default();
attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
program.declarations.push(decl);
program
.declaration_trivia
.push(DeclarationTrivia { leading, trailing });
}
Ok(program)
}
pub fn parse_with_recovery(&mut self) -> ParseResult {
let mut program = Program {
declarations: Vec::new(),
declaration_trivia: Vec::new(),
loc: Loc { line: 1, column: 1 },
};
let mut errors: Vec<ParseError> = Vec::new();
while !self.check(TokenType::Eof) {
let start_pos = self.pos;
match self.parse_declaration() {
Ok(mut decl) => {
let end_pos = self.pos.saturating_sub(1);
let leading = self
.leading_trivia
.get(start_pos)
.cloned()
.unwrap_or_default();
let trailing = self
.trailing_trivia
.get(end_pos)
.cloned()
.unwrap_or_default();
attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
program.declarations.push(decl);
program
.declaration_trivia
.push(DeclarationTrivia { leading, trailing });
}
Err(err) => {
errors.push(self.attach_source_to_error(err));
if self.pos == start_pos && !self.check(TokenType::Eof) {
self.advance();
}
self.advance_to_sync_point();
}
}
}
ParseResult { program, errors }
}
fn attach_source_to_error(&self, err: ParseError) -> ParseError {
match &self.source {
Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
_ => err,
}
}
fn advance_to_sync_point(&mut self) {
let mut depth: i32 = 0;
while !self.check(TokenType::Eof) {
let tt = self.current().ttype.clone();
if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
return;
}
if matches!(tt, TokenType::LBrace) {
depth += 1;
} else if matches!(tt, TokenType::RBrace) {
depth -= 1;
}
self.advance();
}
}
fn current(&self) -> &Token {
if self.pos >= self.tokens.len() {
self.tokens.last().unwrap() } else {
&self.tokens[self.pos]
}
}
fn advance(&mut self) -> &Token {
let idx = self.pos;
if self.pos < self.tokens.len() {
self.pos += 1;
}
&self.tokens[idx]
}
fn check(&self, tt: TokenType) -> bool {
self.current().ttype == tt
}
fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
let tok = self.current().clone();
if tok.ttype != expected {
return Err(ParseError {
message: format!(
"Expected {:?}, found {:?}('{}')",
expected, tok.ttype, tok.value
),
line: tok.line,
column: tok.column,
..Default::default()
});
}
self.pos += 1;
Ok(tok)
}
fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Identifier
| TokenType::Bool
| TokenType::StringLit
| TokenType::Integer
| TokenType::Float => {
self.pos += 1;
Ok(tok)
}
_ => {
if !tok.value.is_empty()
&& tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
&& tok.ttype != TokenType::Eof
{
self.pos += 1;
Ok(tok)
} else {
Err(ParseError {
message: format!(
"Expected identifier or keyword value, found {:?}('{}')",
tok.ttype, tok.value
),
line: tok.line,
column: tok.column,
..Default::default()
})
}
}
}
}
fn consume_number(&mut self) -> Result<f64, ParseError> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Float | TokenType::Integer => {
self.pos += 1;
tok.value.parse::<f64>().map_err(|_| ParseError {
message: format!("Invalid number '{}'", tok.value),
line: tok.line,
column: tok.column,
..Default::default()
})
}
_ => Err(ParseError {
message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
line: tok.line,
column: tok.column,
..Default::default()
}),
}
}
fn parse_bool(&mut self) -> Result<bool, ParseError> {
let tok = self.consume(TokenType::Bool)?;
Ok(tok.value == "true")
}
fn loc_of(&self, tok: &Token) -> Loc {
Loc {
line: tok.line,
column: tok.column,
}
}
fn check_comparison(&self) -> bool {
matches!(
self.current().ttype,
TokenType::Lt
| TokenType::Gt
| TokenType::Lte
| TokenType::Gte
| TokenType::Eq
| TokenType::Neq
)
}
fn check_run_modifier(&self) -> bool {
matches!(
self.current().ttype,
TokenType::As
| TokenType::Within
| TokenType::ConstrainedBy
| TokenType::OnFailure
| TokenType::OutputTo
| TokenType::Effort
)
}
fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
self.consume(TokenType::LBracket)?;
let mut items = Vec::new();
items.push(self.consume(TokenType::StringLit)?.value);
while self.check(TokenType::Comma) {
self.advance();
items.push(self.consume(TokenType::StringLit)?.value);
}
self.consume(TokenType::RBracket)?;
Ok(items)
}
fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
let mut names = Vec::new();
names.push(self.consume(TokenType::Identifier)?.value);
while self.check(TokenType::Comma) {
self.advance();
names.push(self.consume(TokenType::Identifier)?.value);
}
Ok(names)
}
fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
self.consume(TokenType::LBracket)?;
let items = self.parse_extended_identifier_list()?;
self.consume(TokenType::RBracket)?;
Ok(items)
}
fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
let mut items = Vec::new();
items.push(self.consume_any_ident_or_kw()?.value);
while self.check(TokenType::Comma) {
self.advance();
items.push(self.consume_any_ident_or_kw()?.value);
}
Ok(items)
}
fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
let mut parts = vec![self.consume_any_ident_or_kw()?.value];
while self.check(TokenType::Dot) {
self.advance();
parts.push(self.consume_any_ident_or_kw()?.value);
}
Ok(parts.join("."))
}
fn parse_expression_string(&mut self) -> Result<String, ParseError> {
if self.check(TokenType::LBracket) {
let items = self.parse_bracketed_dot_identifiers()?;
return Ok(format!("[{}]", items.join(", ")));
}
self.parse_dotted_identifier()
}
fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
self.consume(TokenType::LBracket)?;
let mut items = vec![self.parse_dotted_identifier()?];
while self.check(TokenType::Comma) {
self.advance();
items.push(self.parse_dotted_identifier()?);
}
self.consume(TokenType::RBracket)?;
Ok(items)
}
fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
let mut args = Vec::new();
while !self.check(TokenType::RParen) {
let tok = self.current().clone();
match tok.ttype {
TokenType::StringLit | TokenType::Integer | TokenType::Float => {
self.advance();
args.push(tok.value);
}
TokenType::Identifier => {
self.advance();
let mut val = tok.value;
if self.check(TokenType::Dot) {
self.advance();
val.push('.');
val.push_str(&self.consume_any_ident_or_kw()?.value);
}
args.push(val);
}
_ => {
self.advance();
let key = tok.value;
if self.check(TokenType::Colon) {
self.advance();
let v = self.advance().value.clone();
args.push(format!("{key}:{v}"));
} else {
args.push(key);
}
}
}
if self.check(TokenType::Comma) {
self.advance();
}
}
Ok(args)
}
fn skip_value(&mut self) {
match self.current().ttype {
TokenType::LBracket => {
self.advance();
let mut depth = 1u32;
while depth > 0 && !self.check(TokenType::Eof) {
if self.check(TokenType::LBracket) {
depth += 1;
} else if self.check(TokenType::RBracket) {
depth -= 1;
}
self.advance();
}
}
TokenType::LBrace => {
self.advance();
let mut depth = 1u32;
while depth > 0 && !self.check(TokenType::Eof) {
if self.check(TokenType::LBrace) {
depth += 1;
} else if self.check(TokenType::RBrace) {
depth -= 1;
}
self.advance();
}
}
TokenType::Lt => {
self.advance();
let mut depth = 1u32;
while depth > 0 && !self.check(TokenType::Eof) {
if self.check(TokenType::Lt) {
depth += 1;
} else if self.check(TokenType::Gt) {
depth -= 1;
}
self.advance();
}
}
_ => {
self.advance();
while self.check(TokenType::Dot) {
self.advance();
self.advance();
}
}
}
}
fn skip_braced_block(&mut self) -> Result<(), ParseError> {
self.consume(TokenType::LBrace)?;
let mut depth = 1u32;
while depth > 0 {
if self.check(TokenType::Eof) {
let tok = self.current();
return Err(ParseError {
message: "Unterminated block — expected '}'".to_string(),
line: tok.line,
column: tok.column,
..Default::default()
});
}
if self.check(TokenType::LBrace) {
depth += 1;
} else if self.check(TokenType::RBrace) {
depth -= 1;
}
self.advance();
}
Ok(())
}
fn at_declaration_start(&self) -> bool {
is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
}
fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Import => self.parse_import().map(Declaration::Import),
TokenType::Persona => self.parse_persona().map(Declaration::Persona),
TokenType::Context => self.parse_context().map(Declaration::Context),
TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
TokenType::Memory => self.parse_memory().map(Declaration::Memory),
TokenType::Tool => self.parse_tool().map(Declaration::Tool),
TokenType::Type => self.parse_type_def().map(Declaration::Type),
TokenType::Flow => self.parse_flow().map(Declaration::Flow),
TokenType::Intent => self.parse_intent().map(Declaration::Intent),
TokenType::Run => self.parse_run().map(Declaration::Run),
TokenType::Let => self.parse_let().map(Declaration::Let),
TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
self.parse_epistemic_block().map(Declaration::Epistemic)
}
TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
TokenType::Agent => self.parse_agent().map(Declaration::Agent),
TokenType::Shield => self.parse_shield().map(Declaration::Shield),
TokenType::Pix => self.parse_pix().map(Declaration::Pix),
TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
TokenType::Ots => self.parse_ots().map(Declaration::Ots),
TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
TokenType::Compute => self.parse_compute().map(Declaration::Compute),
TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
TokenType::Resource => self.parse_resource().map(Declaration::Resource),
TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
TokenType::Observe => self.parse_observe().map(Declaration::Observe),
TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
TokenType::Lease => self.parse_lease().map(Declaration::Lease),
TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
TokenType::Session => self.parse_session_definition().map(Declaration::Session),
TokenType::Topology => self.parse_topology().map(Declaration::Topology),
TokenType::Immune => self.parse_immune().map(Declaration::Immune),
TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
TokenType::Heal => self.parse_heal().map(Declaration::Heal),
TokenType::Component => self.parse_component().map(Declaration::Component),
TokenType::View => self.parse_view().map(Declaration::View),
TokenType::Channel => self.parse_channel().map(Declaration::Channel),
TokenType::Ingest
| TokenType::Persist
| TokenType::Retrieve
| TokenType::Mutate
| TokenType::Purge
| TokenType::Transact => self.parse_generic_declaration(),
TokenType::Mcp => self.parse_generic_declaration(),
_ => {
let hint = crate::smart_suggest::suggest_for(
&tok.value,
crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
);
let base = format!(
"Unexpected token at top level: '{}' — expected declaration \
(persona, context, anchor, flow, run, ...)",
tok.value
);
let message = if hint.is_empty() {
base
} else {
format!("{base}. {hint}")
};
Err(ParseError {
message,
line: tok.line,
column: tok.column,
..Default::default()
})
}
}
}
fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
let tok = self.consume(TokenType::Import)?;
let loc = self.loc_of(&tok);
let mut path_parts = Vec::new();
if self.check(TokenType::At) {
self.advance();
let first = self.consume(TokenType::Identifier)?;
path_parts.push(format!("@{}", first.value));
} else {
let first = self.consume(TokenType::Identifier)?;
path_parts.push(first.value);
}
while self.check(TokenType::Dot) {
self.advance();
if self.check(TokenType::LBrace) {
break;
}
let part = self.consume(TokenType::Identifier)?;
path_parts.push(part.value);
}
let mut names = Vec::new();
if self.check(TokenType::LBrace) {
self.advance();
names = self.parse_identifier_list()?;
self.consume(TokenType::RBrace)?;
}
if self.current().value == "with" {
self.advance();
self.advance(); if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
Ok(ImportNode {
module_path: path_parts,
names,
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
})
}
fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
let tok = self.consume(TokenType::Persona)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LBrace)?;
let mut node = PersonaDefinition {
name,
domain: Vec::new(),
tone: String::new(),
confidence_threshold: None,
cite_sources: None,
refuse_if: Vec::new(),
language: String::new(),
description: String::new(),
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field_name = self.current().value.clone();
self.advance();
self.consume(TokenType::Colon)?;
match field_name.as_str() {
"domain" => node.domain = self.parse_string_list()?,
"tone" => node.tone = self.consume_any_ident_or_kw()?.value,
"confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
"cite_sources" => node.cite_sources = Some(self.parse_bool()?),
"refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
"language" => node.language = self.consume(TokenType::StringLit)?.value,
"description" => node.description = self.consume(TokenType::StringLit)?.value,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
let tok = self.consume(TokenType::Context)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LBrace)?;
let mut node = ContextDefinition {
name,
memory_scope: String::new(),
language: String::new(),
depth: String::new(),
max_tokens: None,
temperature: None,
cite_sources: None,
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field_name = self.current().value.clone();
self.advance();
self.consume(TokenType::Colon)?;
match field_name.as_str() {
"memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
"language" => node.language = self.consume(TokenType::StringLit)?.value,
"depth" => node.depth = self.consume_any_ident_or_kw()?.value,
"max_tokens" => {
node.max_tokens = Some(
self.consume(TokenType::Integer)?
.value
.parse::<i64>()
.unwrap_or(0),
)
}
"temperature" => node.temperature = Some(self.consume_number()?),
"cite_sources" => node.cite_sources = Some(self.parse_bool()?),
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
let tok = self.consume(TokenType::Anchor)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LBrace)?;
let mut node = AnchorConstraint {
name,
require: String::new(),
reject: Vec::new(),
enforce: String::new(),
description: String::new(),
confidence_floor: None,
unknown_response: String::new(),
on_violation: String::new(),
on_violation_target: String::new(),
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field_name = self.current().value.clone();
self.advance();
self.consume(TokenType::Colon)?;
match field_name.as_str() {
"require" => node.require = self.consume_any_ident_or_kw()?.value,
"description" => node.description = self.consume(TokenType::StringLit)?.value,
"reject" => node.reject = self.parse_bracketed_identifiers()?,
"enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
"confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
"unknown_response" => {
node.unknown_response = self.consume(TokenType::StringLit)?.value
}
"on_violation" => {
let action = self.consume_any_ident_or_kw()?.value;
node.on_violation = action.clone();
if action == "raise" || action == "fallback" {
node.on_violation_target = self.consume_any_ident_or_kw()?.value;
}
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
let tok = self.consume(TokenType::Memory)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LBrace)?;
let mut node = MemoryDefinition {
name,
store: String::new(),
backend: String::new(),
retrieval: String::new(),
decay: String::new(),
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field_name = self.current().value.clone();
self.advance();
self.consume(TokenType::Colon)?;
match field_name.as_str() {
"store" => node.store = self.consume_any_ident_or_kw()?.value,
"backend" => node.backend = self.consume_any_ident_or_kw()?.value,
"retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
"decay" => {
if self.check(TokenType::Duration) {
node.decay = self.advance().value.clone();
} else {
node.decay = self.consume_any_ident_or_kw()?.value;
}
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
let tok = self.consume(TokenType::Tool)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LBrace)?;
let mut node = ToolDefinition {
name,
provider: String::new(),
max_results: None,
filter_expr: String::new(),
timeout: String::new(),
runtime: String::new(),
sandbox: None,
effects: None,
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field_name = self.current().value.clone();
self.advance();
self.consume(TokenType::Colon)?;
match field_name.as_str() {
"provider" => node.provider = self.consume_any_ident_or_kw()?.value,
"max_results" => {
node.max_results = Some(
self.consume(TokenType::Integer)?
.value
.parse::<i64>()
.unwrap_or(0),
)
}
"filter" => node.filter_expr = self.parse_filter_expression()?,
"timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
"runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
"sandbox" => node.sandbox = Some(self.parse_bool()?),
"effects" => node.effects = Some(self.parse_effect_row()?),
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
let name = self.consume_any_ident_or_kw()?.value;
if self.check(TokenType::LParen) {
self.advance();
let mut parts = vec![name, "(".to_string()];
while !self.check(TokenType::RParen) {
parts.push(self.advance().value.clone());
}
self.consume(TokenType::RParen)?;
parts.push(")".to_string());
Ok(parts.join(""))
} else {
Ok(name)
}
}
fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
let tok = self.consume(TokenType::Lt)?;
let loc = self.loc_of(&tok);
let mut effects = Vec::new();
let mut epistemic_level = String::new();
while !self.check(TokenType::Gt) {
let name = self.consume_any_ident_or_kw()?.value;
if self.check(TokenType::Colon) {
self.advance();
let level = self.parse_qualifier_value()?;
if name == "epistemic" {
epistemic_level = level;
} else {
effects.push(format!("{name}:{level}"));
}
} else {
effects.push(name);
}
if self.check(TokenType::Comma) {
self.advance();
}
}
self.consume(TokenType::Gt)?;
Ok(EffectRow {
effects,
epistemic_level,
loc,
})
}
fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
let mut buf = self.consume_dotted_slug_segment()?;
loop {
let sep = if self.check(TokenType::Dot) {
'.'
} else if self.check(TokenType::Colon) {
':'
} else {
break;
};
self.advance();
let part = self.consume_dotted_slug_segment()?;
buf.push(sep);
buf.push_str(&part);
}
Ok(buf)
}
fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
let first = self.consume_any_ident_or_kw()?;
let mut buf = first.value.clone();
let mut next_line = first.line;
let mut next_col = first.column + first.value.chars().count() as u32;
loop {
let cur = self.current();
let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
if !is_segment_token {
break;
}
if cur.line != next_line || cur.column != next_col {
break;
}
buf.push_str(&cur.value);
next_col = cur.column + cur.value.chars().count() as u32;
next_line = cur.line;
self.pos += 1;
}
Ok(buf)
}
fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
let tok = self.consume(TokenType::Type)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
let mut node = TypeDefinition {
name,
fields: Vec::new(),
range_constraint: None,
where_clause: None,
compliance: Vec::new(),
loc: loc.clone(),
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
if self.check(TokenType::LParen) {
self.advance();
let min_val = self.consume_number()?;
self.consume(TokenType::DotDot)?;
let max_val = self.consume_number()?;
self.consume(TokenType::RParen)?;
node.range_constraint = Some(RangeConstraint {
min_value: min_val,
max_value: max_val,
loc: loc.clone(),
});
}
if self.check(TokenType::Where) {
self.advance();
let mut expr_parts = Vec::new();
while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
if self.check(TokenType::Eof) {
break;
}
expr_parts.push(self.advance().value.clone());
}
node.where_clause = Some(WhereClause {
expression: expr_parts.join(" "),
loc: loc.clone(),
});
}
if self.check(TokenType::Identifier) && self.current().value == "compliance" {
self.advance();
node.compliance = self.parse_bracketed_identifiers()?;
}
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) {
let field_name = self.consume(TokenType::Identifier)?;
let field_loc = self.loc_of(&field_name);
self.consume(TokenType::Colon)?;
let type_expr = self.parse_type_expr()?;
node.fields.push(TypeField {
name: field_name.value,
type_expr,
loc: field_loc,
});
if self.check(TokenType::Comma) {
self.advance();
}
}
self.consume(TokenType::RBrace)?;
}
Ok(node)
}
fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
let name_tok = self.consume(TokenType::Identifier)?;
let loc = self.loc_of(&name_tok);
let mut generic_param = String::new();
let mut optional = false;
if self.check(TokenType::Lt) {
self.advance();
generic_param = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::Gt)?;
}
if self.check(TokenType::Question) {
self.advance();
optional = true;
}
Ok(TypeExpr {
name: name_tok.value,
generic_param,
optional,
loc,
})
}
fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
let expr = self.parse_type_expr()?;
let mut s = expr.name;
if !expr.generic_param.is_empty() {
s.push('<');
s.push_str(&expr.generic_param);
s.push('>');
}
if expr.optional {
s.push('?');
}
Ok(s)
}
fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
let tok = self.consume(TokenType::Flow)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LParen)?;
let mut parameters = Vec::new();
if !self.check(TokenType::RParen) {
parameters = self.parse_param_list()?;
}
self.consume(TokenType::RParen)?;
let mut return_type = None;
if self.check(TokenType::Arrow) {
self.advance();
return_type = Some(self.parse_type_expr()?);
}
self.consume(TokenType::LBrace)?;
let mut body = Vec::new();
while !self.check(TokenType::RBrace) {
body.push(self.parse_flow_step()?);
}
self.consume(TokenType::RBrace)?;
Ok(FlowDefinition {
name,
parameters,
return_type,
body,
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
})
}
fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
let mut params = Vec::new();
let name = self.consume(TokenType::Identifier)?;
let ploc = self.loc_of(&name);
self.consume(TokenType::Colon)?;
let type_expr = self.parse_type_expr()?;
params.push(Parameter {
name: name.value,
type_expr,
loc: ploc,
});
while self.check(TokenType::Comma) {
self.advance();
let name = self.consume(TokenType::Identifier)?;
let ploc = self.loc_of(&name);
self.consume(TokenType::Colon)?;
let type_expr = self.parse_type_expr()?;
params.push(Parameter {
name: name.value,
type_expr,
loc: ploc,
});
}
Ok(params)
}
fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Step => self.parse_step().map(FlowStep::Step),
TokenType::If => self.parse_if().map(FlowStep::If),
TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
TokenType::Let => self.parse_let().map(FlowStep::Let),
TokenType::Return => self.parse_return().map(FlowStep::Return),
TokenType::Break => self.parse_break().map(FlowStep::Break),
TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
TokenType::Weave => self.parse_weave_step(),
TokenType::Use => self.parse_use_step(),
TokenType::Remember => self.parse_remember_step(),
TokenType::Recall => self.parse_recall_step(),
TokenType::Par => self.parse_block_step("par").map(|l| FlowStep::Par(ParBlock { loc: l })),
TokenType::Hibernate => self.parse_hibernate_step(),
TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
TokenType::Forge => self.parse_block_step("forge").map(|l| FlowStep::Forge(ForgeBlock { loc: l })),
TokenType::Focus => self.parse_flow_step_simple("focus").map(|l| FlowStep::Focus(FocusStep { expression: l.1, loc: l.0 })),
TokenType::Associate => self.parse_associate_step(),
TokenType::Aggregate => self.parse_aggregate_step(),
TokenType::Explore => self.parse_explore_step(),
TokenType::Ingest => self.parse_ingest_step(),
TokenType::Shield => self.parse_apply_step("shield").map(|l| FlowStep::ShieldApply(ShieldApplyStep { shield_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
TokenType::Stream => self.parse_block_step("stream").map(|l| FlowStep::Stream(StreamBlock { loc: l })),
TokenType::Navigate => self.parse_navigate_step(),
TokenType::Drill => self.parse_drill_step(),
TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
TokenType::Corroborate => self.parse_corroborate_step(),
TokenType::Ots => self.parse_apply_step("ots").map(|l| FlowStep::OtsApply(OtsApplyStep { ots_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
TokenType::Mandate => self.parse_apply_step("mandate").map(|l| FlowStep::MandateApply(MandateApplyStep { mandate_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
TokenType::Compute => self.parse_apply_step("compute").map(|l| FlowStep::ComputeApply(ComputeApplyStep { compute_name: l.1, arguments: Vec::new(), output_name: l.3, loc: l.0 })),
TokenType::Listen => self.parse_listen_step(),
TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
TokenType::Emit => self.parse_emit_step(),
TokenType::Publish => self.parse_publish_step(),
TokenType::Discover => self.parse_discover_step(),
TokenType::Persist => self.parse_persist_step(),
TokenType::Retrieve => self.parse_retrieve_step(),
TokenType::Mutate => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Mutate(MutateStep { store_name, where_expr, loc })),
TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
_ => {
let hint = crate::smart_suggest::suggest_for(
&tok.value,
crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
);
let base = format!(
"Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
tok.value
);
let message = if hint.is_empty() {
base
} else {
format!("{base}. {hint}")
};
Err(ParseError {
message,
line: tok.line,
column: tok.column,
..Default::default()
})
}
}
}
fn parse_step(&mut self) -> Result<StepNode, ParseError> {
let tok = self.consume(TokenType::Step)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
let mut persona_ref = String::new();
if self.check(TokenType::Use) {
self.advance();
persona_ref = self.consume_any_ident_or_kw()?.value;
}
self.consume(TokenType::LBrace)?;
let mut node = StepNode {
name,
persona_ref,
given: String::new(),
ask: String::new(),
output_type: String::new(),
confidence_floor: None,
navigate_ref: String::new(),
apply_ref: String::new(),
loc,
};
while !self.check(TokenType::RBrace) {
let inner = self.current().clone();
match inner.ttype {
TokenType::Given => {
self.advance();
self.consume(TokenType::Colon)?;
node.given = self.parse_expression_string()?;
}
TokenType::Ask => {
self.advance();
self.consume(TokenType::Colon)?;
node.ask = self.consume(TokenType::StringLit)?.value;
}
TokenType::Output => {
self.advance();
self.consume(TokenType::Colon)?;
node.output_type = self.parse_output_type_string()?;
}
TokenType::Navigate => {
self.advance();
self.consume(TokenType::Colon)?;
node.navigate_ref = self.parse_dotted_identifier()?;
}
TokenType::Identifier if inner.value == "confidence_floor" => {
self.advance();
self.consume(TokenType::Colon)?;
node.confidence_floor = Some(self.consume_number()?);
}
TokenType::Identifier if inner.value == "apply" => {
self.advance();
self.consume(TokenType::Colon)?;
node.apply_ref = self.consume_any_ident_or_kw()?.value;
}
TokenType::Use
| TokenType::Probe
| TokenType::Reason
| TokenType::Weave
| TokenType::Stream => {
self.skip_flow_step_structural()?;
}
_ => {
return Err(ParseError {
message: format!(
"Unexpected token in step body: '{}' — expected given, ask, use, \
probe, reason, weave, stream, output, confidence_floor, navigate, apply",
inner.value
),
line: inner.line,
column: inner.column,
..Default::default()
});
}
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
self.advance();
while !self.check(TokenType::LBrace)
&& !self.check(TokenType::RBrace)
&& !self.check(TokenType::Eof)
{
let tt = &self.current().ttype;
if matches!(
tt,
TokenType::Step
| TokenType::Given
| TokenType::Ask
| TokenType::Output
| TokenType::Navigate
| TokenType::Use
| TokenType::Probe
| TokenType::Reason
| TokenType::Weave
| TokenType::Stream
| TokenType::If
| TokenType::For
| TokenType::Let
| TokenType::Return
) {
return Ok(());
}
self.advance();
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(())
}
fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
let tok = self.consume(TokenType::Intent)?;
let loc = self.loc_of(&tok);
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LBrace)?;
let mut node = IntentNode {
name,
given: String::new(),
ask: String::new(),
output_type: None,
confidence_floor: None,
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field_name = self.current().value.clone();
self.advance();
self.consume(TokenType::Colon)?;
match field_name.as_str() {
"given" => node.given = self.consume(TokenType::Identifier)?.value,
"ask" => node.ask = self.consume(TokenType::StringLit)?.value,
"output" => node.output_type = Some(self.parse_type_expr()?),
"confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
let tok = self.consume(TokenType::Run)?;
let loc = self.loc_of(&tok);
let flow_name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LParen)?;
let mut arguments = Vec::new();
if !self.check(TokenType::RParen) {
arguments = self.parse_argument_list()?;
}
self.consume(TokenType::RParen)?;
let mut node = RunStatement {
flow_name,
arguments,
persona: String::new(),
context: String::new(),
anchors: Vec::new(),
on_failure: String::new(),
on_failure_params: Vec::new(),
output_to: String::new(),
effort: String::new(),
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while self.check_run_modifier() {
let mod_tok = self.current().clone();
match mod_tok.ttype {
TokenType::As => {
self.advance();
node.persona = self.consume(TokenType::Identifier)?.value;
}
TokenType::Within => {
self.advance();
node.context = self.consume(TokenType::Identifier)?.value;
}
TokenType::ConstrainedBy => {
self.advance();
node.anchors = self.parse_bracketed_identifiers()?;
}
TokenType::OnFailure => {
self.advance();
self.consume(TokenType::Colon)?;
node.on_failure = self.consume_any_ident_or_kw()?.value;
if self.check(TokenType::LParen) {
self.advance();
while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
let key = self.consume_any_ident_or_kw()?.value;
self.consume(TokenType::Colon)?;
let val = self.consume_any_ident_or_kw()?.value;
node.on_failure_params.push((key, val));
if self.check(TokenType::Comma) {
self.advance();
}
}
if self.check(TokenType::RParen) {
self.advance();
}
}
}
TokenType::OutputTo => {
self.advance();
self.consume(TokenType::Colon)?;
node.output_to = self.consume(TokenType::StringLit)?.value;
}
TokenType::Effort => {
self.advance();
self.consume(TokenType::Colon)?;
node.effort = self.consume_any_ident_or_kw()?.value;
}
_ => break,
}
}
Ok(node)
}
fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
let tok = self.current().clone();
let mode = match tok.ttype {
TokenType::Know => "know",
TokenType::Believe => "believe",
TokenType::Speculate => "speculate",
TokenType::Doubt => "doubt",
_ => unreachable!(),
};
self.advance();
let loc = self.loc_of(&tok);
self.consume(TokenType::LBrace)?;
let mut body = Vec::new();
while !self.check(TokenType::RBrace) {
body.push(self.parse_declaration()?);
}
self.consume(TokenType::RBrace)?;
Ok(EpistemicBlock {
mode: mode.to_string(),
body,
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
})
}
fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
let tok = self.consume(TokenType::If)?;
let loc = self.loc_of(&tok);
let mut parts = vec![self.consume_any_ident_or_kw()?.value];
while self.check(TokenType::Dot) {
self.advance();
parts.push(self.consume_any_ident_or_kw()?.value);
}
let condition = parts.join(".");
let mut comparison_op = String::new();
let mut comparison_value = String::new();
if self.check_comparison() {
comparison_op = self.advance().value.clone();
let val_tok = self.current().clone();
if val_tok.ttype == TokenType::StringLit {
comparison_value = val_tok.value;
self.advance();
} else {
comparison_value = self.advance().value.clone();
}
}
let mut conditions = Vec::new();
let mut conjunctor = String::new();
while self.check(TokenType::Or) {
conjunctor = "or".to_string();
self.advance();
let mut cond_parts = vec![self.consume_any_ident_or_kw()?.value];
while self.check(TokenType::Dot) {
self.advance();
cond_parts.push(self.consume_any_ident_or_kw()?.value);
}
let cond_str = cond_parts.join(".");
let mut cond_op = String::new();
let mut cond_val = String::new();
if self.check_comparison() {
cond_op = self.advance().value.clone();
let val_tok = self.current().clone();
if val_tok.ttype == TokenType::StringLit {
cond_val = val_tok.value;
self.advance();
} else {
cond_val = self.advance().value.clone();
}
}
conditions.push((cond_str, cond_op, cond_val));
}
let mut then_body = Vec::new();
let mut else_body = Vec::new();
if self.check(TokenType::Arrow) {
self.advance();
then_body.push(self.parse_flow_step()?);
} else if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) {
then_body.push(self.parse_flow_step()?);
}
self.consume(TokenType::RBrace)?;
}
if self.check(TokenType::Else) {
self.advance();
if self.check(TokenType::Arrow) {
self.advance();
else_body.push(self.parse_flow_step()?);
} else if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) {
else_body.push(self.parse_flow_step()?);
}
self.consume(TokenType::RBrace)?;
}
}
Ok(ConditionalNode {
condition,
comparison_op,
comparison_value,
then_body,
else_body,
conditions,
conjunctor,
loc,
})
}
fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
let tok = self.consume(TokenType::For)?;
let loc = self.loc_of(&tok);
let variable = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::In)?;
let iterable = self.parse_dotted_identifier()?;
self.consume(TokenType::LBrace)?;
self.loop_depth += 1;
let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
let mut body = Vec::new();
while !self.check(TokenType::RBrace) {
body.push(self.parse_flow_step()?);
}
Ok(body)
})();
self.loop_depth -= 1;
let body = body_result?;
self.consume(TokenType::RBrace)?;
Ok(ForInStatement {
variable,
iterable,
body,
loc,
})
}
fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
let tok = self.consume(TokenType::Break)?;
let loc = self.loc_of(&tok);
if self.loop_depth == 0 {
return Err(ParseError {
message: "'break' outside of a for-in loop body".to_string(),
line: tok.line,
column: tok.column,
..Default::default()
});
}
Ok(BreakStatement { loc })
}
fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
let tok = self.consume(TokenType::Continue)?;
let loc = self.loc_of(&tok);
if self.loop_depth == 0 {
return Err(ParseError {
message: "'continue' outside of a for-in loop body".to_string(),
line: tok.line,
column: tok.column,
..Default::default()
});
}
Ok(ContinueStatement { loc })
}
fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
let tok = self.consume(TokenType::Let)?;
let loc = self.loc_of(&tok);
let name = self.consume_any_ident_or_kw()?.value;
self.consume(TokenType::Assign)?;
self.last_let_value_kind = "literal".to_string();
let value = self.parse_let_value_expr()?;
Ok(LetStatement {
identifier: name,
value_expr: value,
value_kind: self.last_let_value_kind.clone(),
loc,
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
})
}
fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
let atom = self.parse_let_atom()?;
if matches!(
self.current().ttype,
TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
) {
let mut parts = vec![atom];
while matches!(
self.current().ttype,
TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
) {
parts.push(self.advance().value.clone());
parts.push(self.parse_let_atom()?);
}
self.last_let_value_kind = "expression".to_string();
return Ok(parts.join(" "));
}
Ok(atom)
}
fn parse_let_atom(&mut self) -> Result<String, ParseError> {
let tok = self.current().clone();
match tok.ttype {
TokenType::StringLit => {
self.last_let_value_kind = "literal".to_string();
self.advance();
Ok(tok.value)
}
TokenType::Integer | TokenType::Float => {
self.last_let_value_kind = "literal".to_string();
self.advance();
Ok(tok.value)
}
TokenType::Bool => {
self.last_let_value_kind = "literal".to_string();
self.advance();
Ok(tok.value)
}
TokenType::Identifier => {
self.last_let_value_kind = "reference".to_string();
self.parse_dotted_identifier()
}
TokenType::LBracket => {
self.last_let_value_kind = "literal".to_string();
self.parse_let_list_literal()
}
_ => {
if self.pos + 1 < self.tokens.len()
&& self.tokens[self.pos + 1].ttype == TokenType::Dot
{
self.last_let_value_kind = "reference".to_string();
return self.parse_dotted_identifier();
}
Err(ParseError {
message: format!(
"Expected value expression, found {:?}('{}')",
tok.ttype, tok.value
),
line: tok.line,
column: tok.column,
..Default::default()
})
}
}
}
fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
self.consume(TokenType::LBracket)?;
let mut items = Vec::new();
if !self.check(TokenType::RBracket) {
items.push(self.parse_let_value_expr()?);
while self.check(TokenType::Comma) {
self.advance();
if self.check(TokenType::RBracket) {
break; }
items.push(self.parse_let_value_expr()?);
}
}
self.consume(TokenType::RBracket)?;
Ok(format!("[{}]", items.join(", ")))
}
fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
let tok = self.consume(TokenType::Return)?;
let loc = self.loc_of(&tok);
let value = self.parse_let_value_expr()?;
Ok(ReturnStatement {
value_expr: value,
loc,
})
}
fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
let tok = self.current().clone();
self.advance(); let target = if self.at_declaration_start()
|| self.check(TokenType::RBrace)
|| self.check(TokenType::Eof)
{
String::new()
} else {
self.consume_any_ident_or_kw()?.value.clone()
};
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok((
Loc {
line: tok.line,
column: tok.column,
},
target,
))
}
fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
let tok = self.current().clone();
self.advance();
while !self.check(TokenType::LBrace)
&& !self.check(TokenType::RBrace)
&& !self.check(TokenType::Eof)
&& !self.at_declaration_start()
{
self.advance();
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(Loc {
line: tok.line,
column: tok.column,
})
}
fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
let tok = self.current().clone();
self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
let mut target = String::new();
let mut output_type = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.value == "on" {
self.advance();
target = self.consume_any_ident_or_kw()?.value.clone();
}
}
if self.check(TokenType::Arrow) {
self.advance();
output_type = self.consume_any_ident_or_kw()?.value.clone();
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok((
Loc {
line: tok.line,
column: tok.column,
},
name,
target,
output_type,
))
}
fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let mut node = WeaveStep {
sources: Vec::new(),
target: String::new(),
format_type: String::new(),
priority: Vec::new(),
style: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
};
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let f = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match f.as_str() {
"sources" => node.sources = self.parse_bracketed_identifiers()?,
"target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
"format" => {
node.format_type = self.consume_any_ident_or_kw()?.value.clone()
}
"priority" => node.priority = self.parse_bracketed_identifiers()?,
"style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
_ => self.skip_value(),
}
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok(FlowStep::Weave(node))
}
fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let tool_name = self.consume_any_ident_or_kw()?.value.clone();
let mut argument = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.value == "on" {
self.advance();
argument = self.consume_any_ident_or_kw()?.value.clone();
}
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(FlowStep::UseTool(UseToolStep {
tool_name,
argument,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let expr = self.consume_any_ident_or_kw()?.value.clone();
let mut mem = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.value == "in" || next.ttype == TokenType::In {
self.advance();
mem = self.consume_any_ident_or_kw()?.value.clone();
}
}
Ok(FlowStep::Remember(RememberStep {
expression: expr,
memory_target: mem,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let query = if self.check(TokenType::StringLit) {
self.consume(TokenType::StringLit)?.value.clone()
} else {
self.consume_any_ident_or_kw()?.value.clone()
};
let mut mem = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.value == "from" || next.ttype == TokenType::From {
self.advance();
mem = self.consume_any_ident_or_kw()?.value.clone();
}
}
Ok(FlowStep::Recall(RecallStep {
query,
memory_source: mem,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let mut event = String::new();
let mut timeout = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
event = self.consume_any_ident_or_kw()?.value.clone();
}
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.ttype == TokenType::Duration {
self.advance();
timeout = next.value.clone();
}
}
Ok(FlowStep::Hibernate(HibernateStep {
event_name: event,
timeout,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let left = self.consume_any_ident_or_kw()?.value.clone();
let mut right = String::new();
let mut using = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
right = self.consume_any_ident_or_kw()?.value.clone();
}
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.value == "using" {
self.advance();
using = self.consume_any_ident_or_kw()?.value.clone();
}
}
Ok(FlowStep::Associate(AssociateStep {
left,
right,
using_field: using,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let target = self.consume_any_ident_or_kw()?.value.clone();
let mut group_by = Vec::new();
let mut alias = String::new();
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let f = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match f.as_str() {
"group_by" => group_by = self.parse_bracketed_identifiers()?,
"alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
_ => self.skip_value(),
}
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok(FlowStep::Aggregate(AggregateStep {
target,
group_by,
alias,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let target = self.consume_any_ident_or_kw()?.value.clone();
let mut limit = None;
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
if self.current().ttype == TokenType::Integer {
limit = self.current().value.parse::<i64>().ok();
self.advance();
}
}
Ok(FlowStep::ExploreStep(ExploreStepNode {
target,
limit,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let source = self.consume_any_ident_or_kw()?.value.clone();
let mut target = String::new();
if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
let next = self.current().clone();
if next.value == "into" || next.ttype == TokenType::Into {
self.advance();
target = self.consume_any_ident_or_kw()?.value.clone();
}
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(FlowStep::Ingest(IngestStep {
source,
target,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let pix_name = self.consume_any_ident_or_kw()?.value.clone();
let mut node = NavigateStep {
pix_name,
corpus_name: String::new(),
query_expr: String::new(),
trail_enabled: false,
output_name: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
};
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let f = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match f.as_str() {
"corpus" => {
node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
}
"query" => {
node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
}
"trail" => {
node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
}
"output" | "as" => {
node.output_name = self.consume_any_ident_or_kw()?.value.clone()
}
_ => self.skip_value(),
}
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok(FlowStep::Navigate(node))
}
fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let pix_name = self.consume_any_ident_or_kw()?.value.clone();
let mut node = DrillStep {
pix_name,
subtree_path: String::new(),
query_expr: String::new(),
output_name: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
};
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let f = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match f.as_str() {
"subtree" | "path" => {
node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
}
"query" => {
node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
}
"output" | "as" => {
node.output_name = self.consume_any_ident_or_kw()?.value.clone()
}
_ => self.skip_value(),
}
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok(FlowStep::Drill(node))
}
fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
let mut output = String::new();
if self.check(TokenType::Arrow) {
self.advance();
output = self.consume_any_ident_or_kw()?.value.clone();
}
Ok(FlowStep::Corroborate(CorroborateStep {
navigate_ref: nav_ref,
output_name: output,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
(self.consume(TokenType::StringLit)?.value.clone(), false)
} else {
(self.consume_any_ident_or_kw()?.value.clone(), true)
};
let mut alias = String::new();
if !self.at_declaration_start()
&& !self.check(TokenType::RBrace)
&& !self.check(TokenType::LBrace)
{
let next = self.current().clone();
if next.value == "as" || next.ttype == TokenType::As {
self.advance();
alias = self.consume_any_ident_or_kw()?.value.clone();
}
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(FlowStep::Listen(ListenStep {
channel,
channel_is_ref,
event_alias: alias,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance();
let store = self.consume_any_ident_or_kw()?.value.clone();
let mut where_expr = String::new();
let mut alias = String::new();
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let f = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match f.as_str() {
"where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
"as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
_ => self.skip_value(),
}
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok(FlowStep::Retrieve(RetrieveStep {
store_name: store,
where_expr,
alias,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_store_where_step(
&mut self,
) -> Result<(Loc, String, String), ParseError> {
let tok = self.current().clone();
self.advance(); let store = if self.at_declaration_start()
|| self.check(TokenType::RBrace)
|| self.check(TokenType::Eof)
{
String::new()
} else {
self.consume_any_ident_or_kw()?.value.clone()
};
let mut where_expr = String::new();
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field.as_str() {
"where" => {
where_expr =
self.consume(TokenType::StringLit)?.value.clone()
}
_ => self.skip_value(),
}
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok((
Loc {
line: tok.line,
column: tok.column,
},
store,
where_expr,
))
}
fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.current().clone();
self.advance(); if self.current().value == "into" && !self.check(TokenType::LBrace) {
self.advance();
}
let store = if self.at_declaration_start()
|| self.check(TokenType::LBrace)
|| self.check(TokenType::RBrace)
|| self.check(TokenType::Eof)
{
String::new()
} else {
self.consume_any_ident_or_kw()?.value.clone()
};
let mut fields: Vec<(String, String)> = Vec::new();
if self.check(TokenType::LBrace) {
self.advance();
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let col = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
let value = if self.check(TokenType::StringLit) {
self.consume(TokenType::StringLit)?.value.clone()
} else if self.check(TokenType::RBrace)
|| self.check(TokenType::Eof)
|| self.check(TokenType::Colon)
{
String::new()
} else {
let v = self.current().clone();
self.advance();
v.value.clone()
};
fields.push((col, value));
}
}
if self.check(TokenType::RBrace) {
self.advance();
}
}
Ok(FlowStep::Persist(PersistStep {
store_name: store,
fields,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
let tok = self.consume(TokenType::Agent)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = AgentDefinition {
name,
goal: String::new(),
tools: Vec::new(),
memory_ref: String::new(),
strategy: String::new(),
on_stuck: String::new(),
shield_ref: String::new(),
max_iterations: None,
max_tokens: None,
max_time: String::new(),
max_cost: None,
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
self.advance();
}
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field = self.current().clone();
let field_name = field.value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
"tools" => node.tools = self.parse_bracketed_identifiers()?,
"memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
"strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
"on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
"max_iterations" => node.max_iterations = self.parse_optional_int(),
"max_tokens" => node.max_tokens = self.parse_optional_int(),
"max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
"max_cost" => node.max_cost = self.parse_optional_float(),
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
let tok = self.consume(TokenType::Shield)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ShieldDefinition {
name,
scan: Vec::new(),
strategy: String::new(),
on_breach: String::new(),
severity: String::new(),
quarantine: String::new(),
max_retries: None,
confidence_threshold: None,
allow_tools: Vec::new(),
deny_tools: Vec::new(),
sandbox: None,
redact: Vec::new(),
log: String::new(),
deflect_message: String::new(),
taint: String::new(),
compliance: Vec::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"scan" => node.scan = self.parse_bracketed_identifiers()?,
"strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
"on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
"severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
"quarantine" => {
node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
}
"max_retries" => node.max_retries = self.parse_optional_int(),
"confidence_threshold" => {
node.confidence_threshold = self.parse_optional_float()
}
"allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
"deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
"sandbox" => {
node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
}
"redact" => node.redact = self.parse_bracketed_identifiers()?,
"log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
"deflect_message" => {
node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
}
"taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
"compliance" => node.compliance = self.parse_bracketed_identifiers()?,
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
let tok = self.consume(TokenType::Pix)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = PixDefinition {
name,
source: String::new(),
depth: None,
branching: None,
model: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
"depth" => node.depth = self.parse_optional_int(),
"branching" => node.branching = self.parse_optional_int(),
"model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
let tok = self.consume(TokenType::Psyche)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = PsycheDefinition {
name,
dimensions: Vec::new(),
manifold_noise: None,
manifold_momentum: None,
safety_constraints: Vec::new(),
quantum_enabled: None,
inference_mode: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
"manifold_noise" => node.manifold_noise = self.parse_optional_float(),
"manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
"safety_constraints" => {
node.safety_constraints = self.parse_bracketed_identifiers()?
}
"quantum_enabled" => {
node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
}
"inference_mode" => {
node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
}
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
let tok = self.consume(TokenType::Corpus)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = CorpusDefinition {
name,
documents: Vec::new(),
mcp_server: String::new(),
mcp_resource_uri: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
if self.check(TokenType::From) {
self.advance();
self.consume(TokenType::Mcp)?;
self.consume(TokenType::LParen)?;
node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
self.consume(TokenType::Comma)?;
node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
self.consume(TokenType::RParen)?;
return Ok(node);
}
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"documents" => node.documents = self.parse_bracketed_identifiers()?,
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
let tok = self.consume(TokenType::Dataspace)?;
let name = self.consume(TokenType::Identifier)?.value;
let node = DataspaceDefinition {
name,
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(node)
}
fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
let tok = self.consume(TokenType::Ots)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = OtsDefinition {
name,
teleology: String::new(),
homotopy_search: String::new(),
loss_function: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
if self.check(TokenType::Lt) {
while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
self.advance();
}
if self.check(TokenType::Gt) {
self.advance();
}
}
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"teleology" => {
node.teleology = self.consume(TokenType::StringLit)?.value.clone()
}
"homotopy_search" => {
node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
}
"loss_function" => {
node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
}
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
let tok = self.consume(TokenType::Mandate)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = MandateDefinition {
name,
constraint: String::new(),
kp: None,
ki: None,
kd: None,
tolerance: None,
max_steps: None,
on_violation: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"constraint" => {
node.constraint = self.consume(TokenType::StringLit)?.value.clone()
}
"kp" | "Kp" => node.kp = self.parse_optional_float(),
"ki" | "Ki" => node.ki = self.parse_optional_float(),
"kd" | "Kd" => node.kd = self.parse_optional_float(),
"tolerance" => node.tolerance = self.parse_optional_float(),
"max_steps" => node.max_steps = self.parse_optional_int(),
"on_violation" => {
node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
}
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
let tok = self.consume(TokenType::Compute)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ComputeDefinition {
name,
shield_ref: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
self.advance();
}
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
let tok = self.consume(TokenType::Daemon)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = DaemonDefinition {
name,
goal: String::new(),
tools: Vec::new(),
memory_ref: String::new(),
strategy: String::new(),
on_stuck: String::new(),
shield_ref: String::new(),
max_tokens: None,
max_time: String::new(),
max_cost: None,
listeners: Vec::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
self.advance();
}
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field = self.current().clone();
let field_name = field.value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
"tools" => node.tools = self.parse_bracketed_identifiers()?,
"memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
"strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
"on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
"max_tokens" => node.max_tokens = self.parse_optional_int(),
"max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
"max_cost" => node.max_cost = self.parse_optional_float(),
_ => self.skip_value(),
}
} else if field.ttype == TokenType::Listen {
let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
(self.consume(TokenType::StringLit)?.value.clone(), false)
} else {
(self.consume_any_ident_or_kw()?.value.clone(), true)
};
let mut alias = String::new();
if !self.at_declaration_start()
&& !self.check(TokenType::RBrace)
&& !self.check(TokenType::LBrace)
{
let next = self.current().clone();
if next.value == "as" || next.ttype == TokenType::As {
self.advance();
alias = self.consume_any_ident_or_kw()?.value.clone();
}
}
let listen_loc = Loc {
line: field.line,
column: field.column,
};
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
node.listeners.push(ListenStep {
channel,
channel_is_ref,
event_alias: alias,
loc: listen_loc,
});
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
let tok = self.consume(TokenType::AxonStore)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = AxonStoreDefinition {
name,
backend: String::new(),
connection: String::new(),
confidence_floor: None,
isolation: String::new(),
on_breach: String::new(),
capability: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field = self.current().clone();
let field_name = field.value.clone();
if field.ttype == TokenType::Schema {
self.advance();
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
"connection" => {
node.connection = self.consume(TokenType::StringLit)?.value.clone()
}
"confidence_floor" => node.confidence_floor = self.parse_optional_float(),
"isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
"on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
"capability" => {
let slug_tok = self.consume(TokenType::StringLit)?.clone();
if !is_valid_capability_slug(&slug_tok.value) {
return Err(ParseError {
message: format!(
"Invalid capability slug '{}' in axonstore '{}' \
`capability:`. Capability slugs must match \
^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
lowercase identifiers starting with a letter. Examples: \
`admin`, `tenant.read`, `hipaa.phi.read`.",
slug_tok.value, node.name
),
line: slug_tok.line,
column: slug_tok.column,
..Default::default()
});
}
node.capability = slug_tok.value.clone();
}
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
let tok = self.consume(TokenType::Resource)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ResourceDefinition {
name,
kind: String::new(),
endpoint: String::new(),
capacity: None,
lifetime: "affine".to_string(),
certainty_floor: None,
shield_ref: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_tok = self.current().clone();
let field_name = field_tok.value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance(); match field_name.as_str() {
"kind" => node.kind = self.consume_any_ident_or_kw()?.value,
"endpoint" => node.endpoint = self.consume(TokenType::StringLit)?.value,
"capacity" => {
node.capacity = self.parse_optional_int();
}
"lifetime" => {
let lt_tok = self.consume_any_ident_or_kw()?;
let lt = lt_tok.value;
if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
return Err(ParseError {
message: format!(
"Invalid lifetime '{lt}' in resource '{}' — \
expected linear | affine | persistent",
node.name
),
line: lt_tok.line,
column: lt_tok.column,
..Default::default()
});
}
node.lifetime = lt;
}
"certainty_floor" => {
node.certainty_floor = self.parse_optional_float();
}
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
let tok = self.consume(TokenType::Fabric)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = FabricDefinition {
name,
provider: String::new(),
region: String::new(),
zones: None,
ephemeral: None,
shield_ref: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance(); match field_name.as_str() {
"provider" => node.provider = self.consume_any_ident_or_kw()?.value,
"region" => node.region = self.consume(TokenType::StringLit)?.value,
"zones" => node.zones = self.parse_optional_int(),
"ephemeral" => {
let b = self.parse_bool()?;
node.ephemeral = Some(b);
}
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
let tok = self.consume(TokenType::Manifest)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ManifestDefinition {
name,
resources: Vec::new(),
fabric_ref: String::new(),
region: String::new(),
zones: None,
compliance: Vec::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"resources" => node.resources = self.parse_bracketed_identifiers()?,
"fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
"region" => node.region = self.consume(TokenType::StringLit)?.value,
"zones" => node.zones = self.parse_optional_int(),
"compliance" => node.compliance = self.parse_bracketed_identifiers()?,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
let tok = self.consume(TokenType::Observe)?;
let name = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::From)?;
let target = self.consume(TokenType::Identifier)?.value;
let mut node = ObserveDefinition {
name,
target,
sources: Vec::new(),
quorum: None,
timeout: String::new(),
on_partition: "fail".to_string(),
certainty_floor: None,
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"sources" => node.sources = self.parse_bracketed_identifiers()?,
"quorum" => node.quorum = self.parse_optional_int(),
"timeout" => {
let t = self.current().clone();
match t.ttype {
TokenType::Duration | TokenType::StringLit => {
self.advance();
node.timeout = t.value;
}
_ => node.timeout = self.consume_any_ident_or_kw()?.value,
}
}
"on_partition" => {
let p_tok = self.consume_any_ident_or_kw()?;
let p = p_tok.value;
if !matches!(p.as_str(), "fail" | "shield_quarantine") {
return Err(ParseError {
message: format!(
"Invalid on_partition '{p}' in observe '{}' — \
expected fail | shield_quarantine",
node.name
),
line: p_tok.line,
column: p_tok.column,
..Default::default()
});
}
node.on_partition = p;
}
"certainty_floor" => node.certainty_floor = self.parse_optional_float(),
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
let tok = self.consume(TokenType::Reconcile)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ReconcileDefinition {
name,
observe_ref: String::new(),
threshold: None,
tolerance: None,
on_drift: "provision".to_string(),
shield_ref: String::new(),
mandate_ref: String::new(),
max_retries: 3,
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
"threshold" => node.threshold = self.parse_optional_float(),
"tolerance" => node.tolerance = self.parse_optional_float(),
"on_drift" => {
let d_tok = self.consume_any_ident_or_kw()?;
let d = d_tok.value;
if !matches!(d.as_str(), "provision" | "alert" | "refine") {
return Err(ParseError {
message: format!(
"Invalid on_drift '{d}' in reconcile '{}' — \
expected provision | alert | refine",
node.name
),
line: d_tok.line,
column: d_tok.column,
..Default::default()
});
}
node.on_drift = d;
}
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
"mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
"max_retries" => {
if let Some(v) = self.parse_optional_int() {
node.max_retries = v;
}
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
let tok = self.consume(TokenType::Lease)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = LeaseDefinition {
name,
resource_ref: String::new(),
duration: String::new(),
acquire: "on_start".to_string(),
on_expire: "anchor_breach".to_string(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
"duration" => {
let t = self.current().clone();
match t.ttype {
TokenType::Duration | TokenType::StringLit => {
self.advance();
node.duration = t.value;
}
_ => node.duration = self.consume_any_ident_or_kw()?.value,
}
}
"acquire" => {
let a_tok = self.consume_any_ident_or_kw()?;
let a = a_tok.value;
if !matches!(a.as_str(), "on_start" | "on_demand") {
return Err(ParseError {
message: format!(
"Invalid acquire '{a}' in lease '{}' — \
expected on_start | on_demand",
node.name
),
line: a_tok.line,
column: a_tok.column,
..Default::default()
});
}
node.acquire = a;
}
"on_expire" => {
let e_tok = self.consume_any_ident_or_kw()?;
let e = e_tok.value;
if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
return Err(ParseError {
message: format!(
"Invalid on_expire '{e}' in lease '{}' — \
expected anchor_breach | release | extend",
node.name
),
line: e_tok.line,
column: e_tok.column,
..Default::default()
});
}
node.on_expire = e;
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
let tok = self.consume(TokenType::Ensemble)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = EnsembleDefinition {
name,
observations: Vec::new(),
quorum: None,
aggregation: "majority".to_string(),
certainty_mode: "min".to_string(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"observations" => node.observations = self.parse_bracketed_identifiers()?,
"quorum" => node.quorum = self.parse_optional_int(),
"aggregation" => {
let a_tok = self.consume_any_ident_or_kw()?;
let a = a_tok.value;
if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
return Err(ParseError {
message: format!(
"Invalid aggregation '{a}' in ensemble '{}' — \
expected majority | weighted | byzantine",
node.name
),
line: a_tok.line,
column: a_tok.column,
..Default::default()
});
}
node.aggregation = a;
}
"certainty_mode" => {
let c_tok = self.consume_any_ident_or_kw()?;
let c = c_tok.value;
if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
return Err(ParseError {
message: format!(
"Invalid certainty_mode '{c}' in ensemble '{}' — \
expected min | weighted | harmonic",
node.name
),
line: c_tok.line,
column: c_tok.column,
..Default::default()
});
}
node.certainty_mode = c;
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
let tok = self.consume(TokenType::Session)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = SessionDefinition {
name,
roles: Vec::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let role_tok = self.consume_any_ident_or_kw()?;
self.consume(TokenType::Colon)?;
let steps = self.parse_session_steps()?;
node.roles.push(SessionRole {
name: role_tok.value,
steps,
loc: Loc {
line: role_tok.line,
column: role_tok.column,
},
});
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
self.consume(TokenType::LBracket)?;
let mut steps = Vec::new();
while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
steps.push(self.parse_session_step()?);
if self.check(TokenType::Comma) {
self.advance();
}
}
self.consume(TokenType::RBracket)?;
Ok(steps)
}
fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Send => {
self.advance();
let msg = self.consume_any_ident_or_kw()?;
Ok(SessionStep {
op: "send".to_string(),
message_type: msg.value,
loc: Loc {
line: tok.line,
column: tok.column,
},
})
}
TokenType::Receive => {
self.advance();
let msg = self.consume_any_ident_or_kw()?;
Ok(SessionStep {
op: "receive".to_string(),
message_type: msg.value,
loc: Loc {
line: tok.line,
column: tok.column,
},
})
}
TokenType::Loop => {
self.advance();
Ok(SessionStep {
op: "loop".to_string(),
message_type: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
})
}
TokenType::End => {
self.advance();
Ok(SessionStep {
op: "end".to_string(),
message_type: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
})
}
_ => Err(ParseError {
message: format!(
"Invalid session step '{}' — expected send | receive | loop | end",
tok.value
),
line: tok.line,
column: tok.column,
..Default::default()
}),
}
}
fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
let tok = self.consume(TokenType::Topology)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = TopologyDefinition {
name,
nodes: Vec::new(),
edges: Vec::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"nodes" => node.nodes = self.parse_bracketed_identifiers()?,
"edges" => node.edges = self.parse_topology_edges()?,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
self.consume(TokenType::LBracket)?;
let mut edges = Vec::new();
while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
edges.push(self.parse_topology_edge()?);
if self.check(TokenType::Comma) {
self.advance();
}
}
self.consume(TokenType::RBracket)?;
Ok(edges)
}
fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
let src_tok = self.consume_any_ident_or_kw()?;
self.consume(TokenType::Arrow)?;
let tgt_tok = self.consume_any_ident_or_kw()?;
self.consume(TokenType::Colon)?;
let sess_tok = self.consume_any_ident_or_kw()?;
Ok(TopologyEdge {
source: src_tok.value,
target: tgt_tok.value,
session_ref: sess_tok.value,
loc: Loc {
line: src_tok.line,
column: src_tok.column,
},
})
}
fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
let tok = self.consume(TokenType::Immune)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ImmuneDefinition {
name,
watch: Vec::new(),
sensitivity: None,
baseline: "learned".to_string(),
window: 100,
scope: String::new(),
tau: String::new(),
decay: "exponential".to_string(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"watch" => node.watch = self.parse_bracketed_identifiers()?,
"sensitivity" => node.sensitivity = self.parse_optional_float(),
"baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
"window" => {
if let Some(v) = self.parse_optional_int() {
node.window = v;
}
}
"scope" => {
let s_tok = self.consume_any_ident_or_kw()?;
let s = s_tok.value;
if !matches!(s.as_str(), "tenant" | "flow" | "global") {
return Err(ParseError {
message: format!(
"Invalid scope '{s}' in immune '{}' — \
expected tenant | flow | global",
node.name
),
line: s_tok.line,
column: s_tok.column,
..Default::default()
});
}
node.scope = s;
}
"tau" => {
let t = self.current().clone();
match t.ttype {
TokenType::Duration | TokenType::StringLit => {
self.advance();
node.tau = t.value;
}
_ => node.tau = self.consume_any_ident_or_kw()?.value,
}
}
"decay" => {
let d_tok = self.consume_any_ident_or_kw()?;
let d = d_tok.value;
if !matches!(d.as_str(), "exponential" | "linear" | "none") {
return Err(ParseError {
message: format!(
"Invalid decay '{d}' in immune '{}' — \
expected exponential | linear | none",
node.name
),
line: d_tok.line,
column: d_tok.column,
..Default::default()
});
}
node.decay = d;
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
let tok = self.consume(TokenType::Reflex)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ReflexDefinition {
name,
trigger: String::new(),
on_level: "doubt".to_string(),
action: String::new(),
scope: String::new(),
sla: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
"on_level" => {
let l_tok = self.consume_any_ident_or_kw()?;
let l = l_tok.value;
if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
return Err(ParseError {
message: format!(
"Invalid on_level '{l}' in reflex '{}' — \
expected know | believe | speculate | doubt",
node.name
),
line: l_tok.line,
column: l_tok.column,
..Default::default()
});
}
node.on_level = l;
}
"action" => {
let a_tok = self.consume_any_ident_or_kw()?;
let a = a_tok.value;
if !matches!(
a.as_str(),
"drop"
| "revoke"
| "emit"
| "redact"
| "quarantine"
| "terminate"
| "alert"
) {
return Err(ParseError {
message: format!(
"Invalid action '{a}' in reflex '{}' — \
expected drop | revoke | emit | redact | \
quarantine | terminate | alert",
node.name
),
line: a_tok.line,
column: a_tok.column,
..Default::default()
});
}
node.action = a;
}
"scope" => {
let s_tok = self.consume_any_ident_or_kw()?;
let s = s_tok.value;
if !matches!(s.as_str(), "tenant" | "flow" | "global") {
return Err(ParseError {
message: format!(
"Invalid scope '{s}' in reflex '{}' — \
expected tenant | flow | global",
node.name
),
line: s_tok.line,
column: s_tok.column,
..Default::default()
});
}
node.scope = s;
}
"sla" => {
let t = self.current().clone();
match t.ttype {
TokenType::Duration | TokenType::StringLit => {
self.advance();
node.sla = t.value;
}
_ => node.sla = self.consume_any_ident_or_kw()?.value,
}
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
let tok = self.consume(TokenType::Heal)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = HealDefinition {
name,
source: String::new(),
on_level: "doubt".to_string(),
mode: "human_in_loop".to_string(),
scope: String::new(),
review_sla: String::new(),
shield_ref: String::new(),
max_patches: 3,
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"source" => node.source = self.consume_any_ident_or_kw()?.value,
"on_level" => {
let l_tok = self.consume_any_ident_or_kw()?;
let l = l_tok.value;
if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
return Err(ParseError {
message: format!(
"Invalid on_level '{l}' in heal '{}' — \
expected know | believe | speculate | doubt",
node.name
),
line: l_tok.line,
column: l_tok.column,
..Default::default()
});
}
node.on_level = l;
}
"mode" => {
let m_tok = self.consume_any_ident_or_kw()?;
let m = m_tok.value;
if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
return Err(ParseError {
message: format!(
"Invalid mode '{m}' in heal '{}' — \
expected audit_only | human_in_loop | adversarial",
node.name
),
line: m_tok.line,
column: m_tok.column,
..Default::default()
});
}
node.mode = m;
}
"scope" => {
let s_tok = self.consume_any_ident_or_kw()?;
let s = s_tok.value;
if !matches!(s.as_str(), "tenant" | "flow" | "global") {
return Err(ParseError {
message: format!(
"Invalid scope '{s}' in heal '{}' — \
expected tenant | flow | global",
node.name
),
line: s_tok.line,
column: s_tok.column,
..Default::default()
});
}
node.scope = s;
}
"review_sla" => {
let t = self.current().clone();
match t.ttype {
TokenType::Duration | TokenType::StringLit => {
self.advance();
node.review_sla = t.value;
}
_ => node.review_sla = self.consume_any_ident_or_kw()?.value,
}
}
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
"max_patches" => {
if let Some(v) = self.parse_optional_int() {
node.max_patches = v;
}
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
let tok = self.consume(TokenType::Component)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ComponentDefinition {
name,
renders: String::new(),
via_shield: String::new(),
on_interact: String::new(),
render_hint: "custom".to_string(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"renders" => node.renders = self.consume_any_ident_or_kw()?.value,
"via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
"on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
"render_hint" => {
let h_tok = self.consume_any_ident_or_kw()?;
let h = h_tok.value;
if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
return Err(ParseError {
message: format!(
"Invalid render_hint '{h}' in component '{}' — \
expected card | list | form | chart | custom",
node.name
),
line: h_tok.line,
column: h_tok.column,
..Default::default()
});
}
node.render_hint = h;
}
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
let tok = self.consume(TokenType::View)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ViewDefinition {
name,
title: String::new(),
components: Vec::new(),
route: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"title" => node.title = self.consume(TokenType::StringLit)?.value,
"components" => node.components = self.parse_bracketed_identifiers()?,
"route" => node.route = self.consume(TokenType::StringLit)?.value,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
let tok = self.consume(TokenType::AxonEndpoint)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = AxonEndpointDefinition {
name,
method: String::new(),
path: String::new(),
body_type: String::new(),
execute_flow: String::new(),
output_type: String::new(),
shield_ref: String::new(),
retries: None,
timeout: String::new(),
compliance: Vec::new(),
transport: "json".to_string(),
keepalive: String::new(),
transport_explicit: false,
implicit_transport: String::new(),
requires_capabilities: Vec::new(),
replay_explicit: false,
replay: false,
transport_dialect: String::new(),
has_algebraic_stream_effect: false,
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_name = self.current().value.clone();
self.advance();
if self.check(TokenType::Colon) {
self.advance();
match field_name.as_str() {
"method" => {
let value_tok = self.consume_any_ident_or_kw()?;
let value_upper = value_tok.value.to_uppercase();
if !axonendpoint_is_valid_method(&value_upper) {
let hint = crate::smart_suggest::suggest_for(
&value_upper,
AXONENDPOINT_METHOD_VALUES,
);
let base = format!(
"Invalid method '{}' in axonendpoint '{}'.",
value_tok.value, node.name
);
let message = if hint.is_empty() {
format!(
"{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
value_tok.value
)
} else {
format!(
"{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
value_tok.value
)
};
return Err(ParseError {
message,
line: value_tok.line,
column: value_tok.column,
..Default::default()
});
}
node.method = value_upper;
}
"path" => node.path = self.consume(TokenType::StringLit)?.value.clone(),
"body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
"execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
"output" => node.output_type = self.consume_any_ident_or_kw()?.value.clone(),
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
"retries" => node.retries = self.parse_optional_int(),
"timeout" => {
let t = self.current().clone();
self.advance();
node.timeout = t.value.clone();
}
"compliance" => node.compliance = self.parse_bracketed_identifiers()?,
"replay" => {
let value_tok = self.consume(TokenType::Bool)?;
node.replay = value_tok.value.eq_ignore_ascii_case("true");
node.replay_explicit = true;
}
"requires" => {
let bracket_tok = self.current().clone();
let items = self.parse_bracketed_dot_identifiers()?;
for slug in &items {
if !is_valid_capability_slug(slug) {
return Err(ParseError {
message: format!(
"Invalid capability slug '{slug}' in axonendpoint '{}' \
`requires:`. Capability slugs must match \
^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
lowercase identifiers starting with a letter. Examples: \
`admin`, `legal.read`, `hipaa.phi.read`.",
node.name
),
line: bracket_tok.line,
column: bracket_tok.column,
..Default::default()
});
}
}
node.requires_capabilities = items;
}
"transport" => {
let value_tok = self.consume_any_ident_or_kw()?;
let value = &value_tok.value;
if !axonendpoint_is_valid_transport(value) {
let hint = crate::smart_suggest::suggest_for(
value,
AXONENDPOINT_TRANSPORT_VALUES,
);
let base = format!(
"Invalid transport '{}' in axonendpoint '{}'.",
value, node.name
);
let message = if hint.is_empty() {
format!("{base} expected json | sse | ndjson, found {value}")
} else {
format!(
"{base} {hint} (expected json | sse | ndjson, found {value})"
)
};
return Err(ParseError {
message,
line: value_tok.line,
column: value_tok.column,
..Default::default()
});
}
node.transport = value.clone();
node.transport_explicit = true;
if self.check(TokenType::LParen) {
if value != "sse" {
let tok = self.current().clone();
return Err(ParseError {
message: format!(
"Dialect parametrization \
`transport: {value}(<dialect>)` is \
only valid for `sse`; got \
`{value}` in axonendpoint '{}'.",
node.name
),
line: tok.line,
column: tok.column,
..Default::default()
});
}
self.advance(); let dialect_tok = self.consume_any_ident_or_kw()?;
let dialect = dialect_tok.value.clone();
if !AXONENDPOINT_TRANSPORT_DIALECTS
.iter()
.any(|&d| d == dialect)
{
let hint = crate::smart_suggest::suggest_for(
&dialect,
AXONENDPOINT_TRANSPORT_DIALECTS,
);
let base = format!(
"Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
node.name
);
let message = if hint.is_empty() {
format!(
"{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
)
} else {
format!(
"{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
)
};
return Err(ParseError {
message,
line: dialect_tok.line,
column: dialect_tok.column,
..Default::default()
});
}
let rparen_tok = self.current().clone();
if !self.check(TokenType::RParen) {
return Err(ParseError {
message: format!(
"Expected `)` after dialect name \
in axonendpoint '{}' \
(transport: sse(<dialect>) grammar).",
node.name
),
line: rparen_tok.line,
column: rparen_tok.column,
..Default::default()
});
}
self.advance(); node.transport_dialect = dialect;
}
}
"keepalive" => {
let value_tok = self.current().clone();
self.advance();
let value = &value_tok.value;
if !axonendpoint_is_valid_keepalive(value) {
let hint = crate::smart_suggest::suggest_for(
value,
AXONENDPOINT_KEEPALIVE_VALUES,
);
let base = format!(
"Invalid keepalive '{}' in axonendpoint '{}'.",
value, node.name
);
let message = if hint.is_empty() {
format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
} else {
format!(
"{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
)
};
return Err(ParseError {
message,
line: value_tok.line,
column: value_tok.column,
..Default::default()
});
}
node.keepalive = value.clone();
}
_ => self.skip_value(),
}
} else if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_optional_int(&mut self) -> Option<i64> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Integer => {
self.advance();
tok.value.parse::<i64>().ok()
}
_ => {
self.advance();
None
}
}
}
fn parse_optional_float(&mut self) -> Option<f64> {
let tok = self.current().clone();
match tok.ttype {
TokenType::Float | TokenType::Integer => {
self.advance();
tok.value.parse::<f64>().ok()
}
_ => {
self.advance();
None
}
}
}
fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
let tok = self.consume(TokenType::Lambda)?;
let name = self.consume(TokenType::Identifier)?;
self.consume(TokenType::LBrace)?;
let mut node = LambdaDataDefinition {
name: name.value.clone(),
ontology: String::new(),
certainty: 1.0,
temporal_frame_start: String::new(),
temporal_frame_end: String::new(),
provenance: String::new(),
derivation: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
while !self.check(TokenType::RBrace) {
let field = self.current().clone();
match field.ttype {
TokenType::Ontology => {
self.advance();
self.consume(TokenType::Colon)?;
node.ontology = self.consume(TokenType::StringLit)?.value.clone();
}
TokenType::Certainty => {
self.advance();
self.consume(TokenType::Colon)?;
let val = self.current().clone();
match val.ttype {
TokenType::Float => {
self.advance();
node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
}
TokenType::Integer => {
self.advance();
node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
}
_ => {
return Err(ParseError {
message: format!(
"Expected number for certainty, got '{}'",
val.value
),
line: val.line,
column: val.column,
..Default::default()
});
}
}
}
TokenType::TemporalFrame => {
self.advance();
self.consume(TokenType::Colon)?;
node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
if self.check(TokenType::StringLit) {
node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
}
}
TokenType::Provenance => {
self.advance();
self.consume(TokenType::Colon)?;
node.provenance = self.consume(TokenType::StringLit)?.value.clone();
}
TokenType::Derivation => {
self.advance();
self.consume(TokenType::Colon)?;
let d = self.current().clone();
self.advance();
node.derivation = d.value.clone();
}
_ => {
self.advance();
if self.check(TokenType::Colon) {
self.advance();
self.skip_value();
}
}
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
let tok = self.consume(TokenType::Lambda)?;
let lambda_name = self.consume(TokenType::Identifier)?;
let on_tok = self.current().clone();
self.advance();
if on_tok.value != "on" {
return Err(ParseError {
message: format!(
"Expected 'on' after lambda data name in flow step, got '{}'",
on_tok.value
),
line: on_tok.line,
column: on_tok.column,
..Default::default()
});
}
let target = self.current().clone();
self.advance();
let mut output_type = String::new();
if self.check(TokenType::Arrow) {
self.advance();
output_type = self.consume(TokenType::Identifier)?.value.clone();
}
Ok(LambdaDataApplyNode {
lambda_data_name: lambda_name.value.clone(),
target: target.value.clone(),
output_type,
loc: Loc {
line: tok.line,
column: tok.column,
},
})
}
fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
let kw_tok = self.current().clone();
self.advance();
let name = if self.current().ttype == TokenType::Identifier {
let n = self.current().value.clone();
self.advance();
n
} else if !self.check(TokenType::LBrace)
&& !self.check(TokenType::LParen)
&& !self.check(TokenType::Eof)
&& self
.current()
.value
.chars()
.all(|c| c.is_alphanumeric() || c == '_')
{
let n = self.current().value.clone();
self.advance();
n
} else {
String::new()
};
if self.check(TokenType::LParen) {
self.advance();
let mut depth = 1u32;
while depth > 0 && !self.check(TokenType::Eof) {
if self.check(TokenType::LParen) {
depth += 1;
} else if self.check(TokenType::RParen) {
depth -= 1;
}
self.advance();
}
}
while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
if self.check(TokenType::Eof) {
break;
}
self.advance();
}
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
Ok(Declaration::Generic(GenericDeclaration {
keyword: kw_tok.value,
name,
loc: Loc {
line: kw_tok.line,
column: kw_tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
}))
}
fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
let tok = self.consume(TokenType::Channel)?;
let name = self.consume(TokenType::Identifier)?.value;
let mut node = ChannelDefinition {
name: name.clone(),
message: String::new(),
qos: "at_least_once".to_string(),
lifetime: "affine".to_string(),
persistence: "ephemeral".to_string(),
shield_ref: String::new(),
loc: Loc {
line: tok.line,
column: tok.column,
},
leading_trivia: Vec::new(),
trailing_trivia: Vec::new(),
};
self.consume(TokenType::LBrace)?;
while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
let field_tok = self.current().clone();
let field_name = field_tok.value.clone();
self.advance();
if !self.check(TokenType::Colon) {
if self.check(TokenType::LBrace) {
self.skip_braced_block()?;
}
continue;
}
self.advance();
match field_name.as_str() {
"message" => node.message = self.parse_channel_message_type()?,
"qos" => {
let q_tok = self.consume_any_ident_or_kw()?;
if !matches!(
q_tok.value.as_str(),
"at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
) {
return Err(ParseError {
message: format!(
"Invalid qos '{}' in channel '{}' — \
expected at_most_once | at_least_once | \
exactly_once | broadcast | queue",
q_tok.value, name
),
line: q_tok.line,
column: q_tok.column,
..Default::default()
});
}
node.qos = q_tok.value;
}
"lifetime" => {
let lt_tok = self.consume_any_ident_or_kw()?;
if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
return Err(ParseError {
message: format!(
"Invalid lifetime '{}' in channel '{}' — \
expected linear | affine | persistent",
lt_tok.value, name
),
line: lt_tok.line,
column: lt_tok.column,
..Default::default()
});
}
node.lifetime = lt_tok.value;
}
"persistence" => {
let p_tok = self.consume_any_ident_or_kw()?;
if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
return Err(ParseError {
message: format!(
"Invalid persistence '{}' in channel '{}' — \
expected ephemeral | persistent_axonstore",
p_tok.value, name
),
line: p_tok.line,
column: p_tok.column,
..Default::default()
});
}
node.persistence = p_tok.value;
}
"shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
_ => self.skip_value(),
}
}
self.consume(TokenType::RBrace)?;
Ok(node)
}
fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
let head = self.consume(TokenType::Identifier)?;
let mut spelling = head.value;
if self.check(TokenType::Lt) {
self.advance();
let inner = self.parse_channel_message_type()?;
self.consume(TokenType::Gt)?;
spelling = format!("{}<{}>", spelling, inner);
}
Ok(spelling)
}
fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.consume(TokenType::Emit)?;
let channel = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::LParen)?;
let value = self.parse_emit_value_ref()?;
self.consume(TokenType::RParen)?;
Ok(FlowStep::Emit(EmitStatement {
channel_ref: channel,
value_ref: value,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
let head = self.consume(TokenType::Identifier)?.value;
let mut parts = vec![head];
while self.check(TokenType::Dot) {
self.advance(); let next_tok = self.current().clone();
let valid = !next_tok.value.is_empty()
&& next_tok.value.as_bytes()[0].is_ascii_alphabetic()
|| next_tok.value.starts_with('_');
if !valid {
return Err(ParseError {
message: format!(
"Expected identifier or keyword after '.' in dotted \
access, found {:?}",
next_tok.value
),
line: next_tok.line,
column: next_tok.column,
..Default::default()
});
}
self.advance();
parts.push(next_tok.value);
}
Ok(parts.join("."))
}
fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.consume(TokenType::Publish)?;
let channel = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::Within)?;
let shield = self.consume(TokenType::Identifier)?.value;
Ok(FlowStep::Publish(PublishStatement {
channel_ref: channel,
shield_ref: shield,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
let tok = self.consume(TokenType::Discover)?;
let cap = self.consume(TokenType::Identifier)?.value;
self.consume(TokenType::As)?;
let alias = self.consume(TokenType::Identifier)?.value;
Ok(FlowStep::Discover(DiscoverStatement {
capability_ref: cap,
alias,
loc: Loc {
line: tok.line,
column: tok.column,
},
}))
}
}
#[cfg(test)]
mod fase13_parser_tests {
use super::*;
use crate::lexer::Lexer;
fn parse(src: &str) -> Result<Program, ParseError> {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
Parser::new(tokens).parse()
}
#[test]
fn channel_full_parses() {
let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
let prog = parse(src).expect("parse");
match &prog.declarations[0] {
Declaration::Channel(c) => {
assert_eq!(c.name, "C");
assert_eq!(c.message, "Order");
assert_eq!(c.qos, "at_least_once");
assert_eq!(c.lifetime, "affine");
assert_eq!(c.persistence, "ephemeral");
assert_eq!(c.shield_ref, "Gate");
}
_ => panic!("expected ChannelDefinition"),
}
}
#[test]
fn channel_defaults_match_paper_d1() {
let prog = parse("channel C { message: Order }").expect("parse");
if let Declaration::Channel(c) = &prog.declarations[0] {
assert_eq!(c.qos, "at_least_once"); assert_eq!(c.lifetime, "affine"); assert_eq!(c.persistence, "ephemeral");
assert_eq!(c.shield_ref, "");
} else {
panic!("expected ChannelDefinition");
}
}
#[test]
fn channel_second_order_message_type_parses() {
let prog = parse("channel C { message: Channel<Order> }").expect("parse");
if let Declaration::Channel(c) = &prog.declarations[0] {
assert_eq!(c.message, "Channel<Order>");
} else {
panic!("expected ChannelDefinition");
}
}
#[test]
fn channel_nested_channel_message_type_parses() {
let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
if let Declaration::Channel(c) = &prog.declarations[0] {
assert_eq!(c.message, "Channel<Channel<Order>>");
} else {
panic!("expected ChannelDefinition");
}
}
#[test]
fn channel_invalid_qos_rejected() {
let err = parse("channel C { message: T qos: bogus }").unwrap_err();
assert!(err.message.contains("Invalid qos"), "got {}", err.message);
}
#[test]
fn channel_invalid_lifetime_rejected() {
let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
assert!(
err.message.contains("Invalid lifetime"),
"got {}",
err.message
);
}
#[test]
fn channel_invalid_persistence_rejected() {
let err = parse("channel C { message: T persistence: forever }").unwrap_err();
assert!(
err.message.contains("Invalid persistence"),
"got {}",
err.message
);
}
#[test]
fn emit_value_parses() {
let src = "flow f() -> Out { emit C(payload) }";
let prog = parse(src).expect("parse");
if let Declaration::Flow(f) = &prog.declarations[0] {
match &f.body[0] {
FlowStep::Emit(e) => {
assert_eq!(e.channel_ref, "C");
assert_eq!(e.value_ref, "payload");
}
other => panic!("expected Emit, got {:?}", other),
}
} else {
panic!("expected Flow");
}
}
#[test]
fn publish_within_shield_parses() {
let src = "flow f() -> Cap { publish C within Gate }";
let prog = parse(src).expect("parse");
if let Declaration::Flow(f) = &prog.declarations[0] {
match &f.body[0] {
FlowStep::Publish(p) => {
assert_eq!(p.channel_ref, "C");
assert_eq!(p.shield_ref, "Gate");
}
other => panic!("expected Publish, got {:?}", other),
}
} else {
panic!("expected Flow");
}
}
#[test]
fn discover_with_alias_parses() {
let src = "flow f() -> Out { discover C as ch }";
let prog = parse(src).expect("parse");
if let Declaration::Flow(f) = &prog.declarations[0] {
match &f.body[0] {
FlowStep::Discover(d) => {
assert_eq!(d.capability_ref, "C");
assert_eq!(d.alias, "ch");
}
other => panic!("expected Discover, got {:?}", other),
}
} else {
panic!("expected Flow");
}
}
#[test]
fn listen_typed_ref_sets_flag_true() {
let src = "daemon D() { goal: \"x\" listen C as ev { } }";
let prog = parse(src).expect("parse");
if let Declaration::Daemon(d) = &prog.declarations[0] {
assert_eq!(d.listeners.len(), 1);
assert_eq!(d.listeners[0].channel, "C");
assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
} else {
panic!("expected Daemon");
}
}
#[test]
fn listen_string_topic_legacy_flag_false() {
let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
let prog = parse(src).expect("parse");
if let Declaration::Daemon(d) = &prog.declarations[0] {
assert_eq!(d.listeners.len(), 1);
assert_eq!(d.listeners[0].channel, "orders");
assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
} else {
panic!("expected Daemon");
}
}
fn extract_first_emit(prog: &Program) -> &EmitStatement {
if let Declaration::Flow(f) = &prog.declarations[0] {
if let FlowStep::Emit(e) = &f.body[0] {
return e;
}
}
panic!("expected emit statement at flow body[0]");
}
#[test]
fn emit_accepts_bare_identifier_value_ref() {
let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
let emit = extract_first_emit(&prog);
assert_eq!(emit.channel_ref, "Hello");
assert_eq!(emit.value_ref, "payload");
}
#[test]
fn emit_accepts_two_segment_dotted_value_ref() {
let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
let emit = extract_first_emit(&prog);
assert_eq!(emit.value_ref, "Build.output");
}
#[test]
fn emit_accepts_three_segment_nested_dotted_value_ref() {
let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
let emit = extract_first_emit(&prog);
assert_eq!(emit.value_ref, "Analyze.result.score");
}
#[test]
fn emit_dotted_with_trailing_dot_fails() {
let result = parse("flow f() -> Out { emit Hello(Build.) }");
assert!(result.is_err(), "expected parse error for trailing dot");
}
}
#[cfg(test)]
mod fase14a_declaration_trivia_tests {
use super::*;
use crate::lexer::Lexer;
use crate::tokens::TriviaKind;
fn parse(src: &str) -> Program {
let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
Parser::new(toks).parse().expect("parse")
}
#[test]
fn no_comments_means_empty_trivia_per_decl() {
let prog = parse("flow F() -> Out { }");
assert_eq!(prog.declarations.len(), 1);
assert_eq!(prog.declaration_trivia.len(), 1);
assert!(prog.declaration_trivia[0].leading.is_empty());
assert!(prog.declaration_trivia[0].trailing.is_empty());
}
#[test]
fn doc_line_comment_attaches_as_leading() {
let prog = parse("/// Documents F\nflow F() -> Out { }");
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading.len(), 1);
assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
assert!(triv.leading[0].is_doc());
assert_eq!(triv.leading[0].text, "/// Documents F");
}
#[test]
fn regular_line_comment_attaches_as_leading() {
let prog = parse("// header\nflow F() -> Out { }");
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading.len(), 1);
assert_eq!(triv.leading[0].kind, TriviaKind::Line);
assert!(!triv.leading[0].is_doc());
}
#[test]
fn block_doc_comment_attaches_as_leading() {
let prog = parse("/** Doc block */\nflow F() -> Out { }");
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
assert!(triv.leading[0].is_doc());
}
#[test]
fn multiple_comments_collected_in_source_order() {
let src = "/// First\n/// Second\nflow F() -> Out { }";
let prog = parse(src);
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading.len(), 2);
assert_eq!(triv.leading[0].text, "/// First");
assert_eq!(triv.leading[1].text, "/// Second");
}
#[test]
fn three_decls_each_get_own_leading() {
let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
let prog = parse(src);
assert_eq!(prog.declarations.len(), 3);
assert_eq!(prog.declaration_trivia.len(), 3);
for (idx, name) in ["A", "B", "C"].iter().enumerate() {
let triv = &prog.declaration_trivia[idx];
assert_eq!(triv.leading.len(), 1);
assert_eq!(triv.leading[0].text, format!("/// for {name}"));
}
}
#[test]
fn trailing_comment_attaches_to_last_token_of_decl() {
let prog = parse("flow F() -> Out { } // tail");
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.trailing.len(), 1);
assert_eq!(triv.trailing[0].text, "// tail");
}
#[test]
fn mixed_doc_and_regular_preserve_order_between_decls() {
let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
let prog = parse(src);
assert_eq!(prog.declarations.len(), 2);
assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
}
#[test]
fn parser_unaffected_by_comments_in_grammar_path() {
let src =
"// before flow\nflow /* between flow and name */ F() -> Out {\n // body comment\n}";
let prog = parse(src);
assert_eq!(prog.declarations.len(), 1);
if let Declaration::Flow(f) = &prog.declarations[0] {
assert_eq!(f.name, "F");
} else {
panic!("expected Flow declaration");
}
}
}
#[cfg(test)]
mod fase14b_per_struct_trivia_tests {
use super::*;
use crate::lexer::Lexer;
use crate::tokens::TriviaKind;
fn parse(src: &str) -> Program {
let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
Parser::new(toks).parse().expect("parse")
}
#[test]
fn flow_definition_carries_leading_trivia_directly() {
let prog = parse("/// documents F\nflow F() -> Out { }");
if let Declaration::Flow(f) = &prog.declarations[0] {
assert_eq!(f.leading_trivia.len(), 1);
assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
assert_eq!(f.leading_trivia[0].text, "/// documents F");
assert!(f.trailing_trivia.is_empty());
} else {
panic!("expected Flow declaration");
}
}
#[test]
fn flow_definition_carries_trailing_trivia_directly() {
let prog = parse("flow F() -> Out { } // tail comment");
if let Declaration::Flow(f) = &prog.declarations[0] {
assert_eq!(f.trailing_trivia.len(), 1);
assert_eq!(f.trailing_trivia[0].text, "// tail comment");
} else {
panic!("expected Flow declaration");
}
}
#[test]
fn channel_definition_carries_trivia_directly() {
let src = concat!(
"/// inbound order events\n",
"channel Orders {\n",
" message: Order\n",
" qos: at_least_once\n",
" lifetime: affine\n",
" persistence: ephemeral\n",
" shield: Broker\n",
"}",
);
let prog = parse(src);
if let Declaration::Channel(ch) = &prog.declarations[0] {
assert_eq!(ch.leading_trivia.len(), 1);
assert!(ch.leading_trivia[0].is_doc());
assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
} else {
panic!("expected Channel declaration");
}
}
#[test]
fn per_struct_fields_match_side_channel() {
let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
let prog = parse(src);
for (idx, decl) in prog.declarations.iter().enumerate() {
let side = &prog.declaration_trivia[idx];
let (per_lead, per_trail) = match decl {
Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
_ => panic!("unexpected variant"),
};
assert_eq!(per_lead.len(), side.leading.len());
assert_eq!(per_trail.len(), side.trailing.len());
for (a, b) in per_lead.iter().zip(side.leading.iter()) {
assert_eq!(a.text, b.text);
assert_eq!(a.kind, b.kind);
}
}
}
#[test]
fn comment_free_program_yields_empty_per_struct_fields() {
let prog = parse("flow F() -> Out { }");
if let Declaration::Flow(f) = &prog.declarations[0] {
assert!(f.leading_trivia.is_empty());
assert!(f.trailing_trivia.is_empty());
} else {
panic!("expected Flow declaration");
}
}
}
#[cfg(test)]
mod fase14c_inner_doc_tests {
use super::*;
use crate::lexer::Lexer;
use crate::tokens::TriviaKind;
fn parse(src: &str) -> Program {
let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
Parser::new(toks).parse().expect("parse")
}
#[test]
fn inner_doc_line_reaches_leading_trivia() {
let src = "//! file-level docs\nflow F() -> Out { }";
let prog = parse(src);
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading.len(), 1);
assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
assert!(triv.leading[0].is_doc());
assert!(triv.leading[0].is_inner_doc());
assert_eq!(triv.leading[0].text, "//! file-level docs");
assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
}
#[test]
fn inner_doc_block_reaches_leading_trivia() {
let src = "/*! module-level docs */\nflow F() -> Out { }";
let prog = parse(src);
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading.len(), 1);
assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
assert!(triv.leading[0].is_inner_doc());
assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
}
#[test]
fn outer_and_inner_doc_can_coexist() {
let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
let prog = parse(src);
let triv = &prog.declaration_trivia[0];
assert_eq!(triv.leading.len(), 2);
assert!(triv.leading[0].is_inner_doc());
assert!(triv.leading[1].is_doc());
assert!(!triv.leading[1].is_inner_doc());
}
#[test]
fn inner_doc_reaches_per_struct_fields() {
let src = "//! intro\nflow F() -> Out { }";
let prog = parse(src);
if let Declaration::Flow(f) = &prog.declarations[0] {
assert_eq!(f.leading_trivia.len(), 1);
assert!(f.leading_trivia[0].is_inner_doc());
} else {
panic!("expected Flow declaration");
}
}
}
#[cfg(test)]
mod fase28_recovery_tests {
use super::*;
use crate::lexer::Lexer;
fn lex(src: &str) -> Vec<Token> {
Lexer::new(src, "<test>").tokenize().expect("lex")
}
fn recover(src: &str) -> ParseResult {
Parser::new(lex(src)).parse_with_recovery()
}
fn strict(src: &str) -> Result<Program, ParseError> {
Parser::new(lex(src)).parse()
}
#[test]
fn strict_parse_unchanged_for_clean_source() {
let src = "intent I {}";
let prog = strict(src).expect("clean parse");
assert_eq!(prog.declarations.len(), 1);
}
#[test]
fn strict_parse_still_raises_on_first_error() {
let src = "flow F() { } not_a_keyword flow G() { }";
let _ = strict(src).expect_err("must error fast in strict mode");
}
#[test]
fn recovery_clean_source_yields_no_errors() {
let src = "flow F() { } flow G() { }";
let pr = recover(src);
assert!(pr.is_clean(), "errors: {:?}", pr.errors);
assert_eq!(pr.program.declarations.len(), 2);
}
#[test]
fn single_unknown_top_level_token_recovers() {
let src = "garbage_token flow F() { } flow G() { }";
let pr = recover(src);
assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
assert_eq!(pr.program.declarations.len(), 2);
}
#[test]
fn error_in_first_decl_does_not_block_second() {
let src = "flow F() { not_a_step nope } flow G() { }";
let pr = recover(src);
assert!(pr.has_errors(), "expected at least one error");
let names: Vec<&str> = pr
.program
.declarations
.iter()
.filter_map(|d| match d {
Declaration::Flow(f) => Some(f.name.as_str()),
_ => None,
})
.collect();
assert!(names.contains(&"G"), "G not found among {names:?}");
}
#[test]
fn malformed_declaration_then_clean_intent_recovers() {
let src = "flow @ () { } intent I {}";
let pr = recover(src);
assert!(pr.has_errors());
let kinds: Vec<&str> = pr
.program
.declarations
.iter()
.map(|d| match d {
Declaration::Intent(_) => "intent",
Declaration::Flow(_) => "flow",
_ => "other",
})
.collect();
assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
}
#[test]
fn recovery_does_not_double_count_a_single_error() {
let src = "flow F() { not_a_step }";
let pr = recover(src);
let outer_ghosts = pr
.errors
.iter()
.filter(|e| e.message.contains("at top level"))
.count();
assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
}
#[test]
fn three_independent_errors_yield_three_entries() {
let src =
"garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
let pr = recover(src);
assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
assert_eq!(pr.program.declarations.len(), 3);
}
#[test]
fn all_errors_no_valid_declarations() {
let src = "foo bar baz qux";
let pr = recover(src);
assert!(pr.has_errors());
assert!(pr.program.declarations.is_empty());
}
#[test]
fn errors_recorded_in_source_order() {
let src = "x flow A() { } y flow B() { } z flow C() { }";
let pr = recover(src);
assert_eq!(pr.errors.len(), 3);
let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
assert!(
lines.windows(2).all(|w| w[0] <= w[1]),
"errors out of order: {lines:?}"
);
}
#[test]
fn sync_to_flow_keyword() {
let src = "garbage flow F() { }";
let pr = recover(src);
assert_eq!(pr.program.declarations.len(), 1);
}
#[test]
fn sync_to_intent_keyword() {
let src = "garbage intent I {}";
let pr = recover(src);
assert_eq!(pr.program.declarations.len(), 1);
}
#[test]
fn sync_to_persona_keyword() {
let src = "garbage persona P { name: \"P\" role: \"R\" }";
let pr = recover(src);
assert!(
pr.program
.declarations
.iter()
.any(|d| matches!(d, Declaration::Persona(_))),
"persona not recovered: decls = {:?}",
pr.program.declarations.len()
);
}
#[test]
fn sync_to_run_keyword() {
let src = "garbage run R { input: { user_message: \"hi\" } }";
let pr = recover(src);
assert!(pr.has_errors());
}
#[test]
fn parse_result_has_errors_and_is_clean_invert() {
let pr_clean = recover("flow F() { }");
assert!(pr_clean.is_clean());
assert!(!pr_clean.has_errors());
let pr_err = recover("garbage");
assert!(!pr_err.is_clean());
assert!(pr_err.has_errors());
}
#[test]
fn parse_result_program_field_holds_partial_program() {
let pr = recover("garbage flow F() { }");
assert!(!pr.program.declarations.is_empty());
}
#[test]
fn parse_result_errors_carry_line_and_column() {
let pr = recover("garbage");
assert!(!pr.errors.is_empty());
let e = &pr.errors[0];
assert!(e.line >= 1);
let _ = e.column;
assert!(!e.message.is_empty());
}
#[test]
fn parse_result_debug_renders() {
let pr = recover("flow F() { }");
let s = format!("{pr:?}");
assert!(s.contains("ParseResult"));
}
#[test]
fn empty_source_is_clean() {
let pr = recover("");
assert!(pr.is_clean());
assert!(pr.program.declarations.is_empty());
}
#[test]
fn whitespace_only_source_is_clean() {
let pr = recover(" \n\n\t \n");
assert!(pr.is_clean());
assert!(pr.program.declarations.is_empty());
}
#[test]
fn only_garbage_does_not_crash() {
let pr = recover("foo bar baz { qux quux } corge { grault }");
assert!(pr.has_errors());
}
#[test]
fn unbalanced_close_brace_does_not_crash() {
let pr = recover("} flow F() { }");
let names: Vec<&str> = pr
.program
.declarations
.iter()
.filter_map(|d| match d {
Declaration::Flow(f) => Some(f.name.as_str()),
_ => None,
})
.collect();
assert!(names.contains(&"F"), "F not recovered: {names:?}");
}
#[test]
fn error_at_eof_does_not_loop() {
let pr = recover("flow F() { ");
let _ = pr.errors.len();
}
#[test]
fn nested_braces_inside_error_still_balance() {
let src = "flow F() { not_a_step { inner } } flow G() { }";
let pr = recover(src);
let names: Vec<&str> = pr
.program
.declarations
.iter()
.filter_map(|d| match d {
Declaration::Flow(f) => Some(f.name.as_str()),
_ => None,
})
.collect();
assert!(names.contains(&"G"), "G not recovered: {names:?}");
}
#[derive(Clone, Copy)]
struct Xorshift(u64);
impl Xorshift {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
slice[(self.next() as usize) % slice.len()]
}
}
fn mutate(src: &str, rng: &mut Xorshift) -> String {
let mut bytes: Vec<u8> = src.bytes().collect();
if bytes.is_empty() {
return src.to_string();
}
let op = rng.next() % 4;
let pos = (rng.next() as usize) % bytes.len();
let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
match op {
0 => {
bytes.remove(pos);
}
1 => {
let b = rng.pick(safe);
bytes.insert(pos, b);
}
2 if pos + 1 < bytes.len() => {
bytes.swap(pos, pos + 1);
}
_ => {
let b = rng.pick(safe);
bytes[pos] = b;
}
}
bytes.retain(|b| b.is_ascii());
String::from_utf8_lossy(&bytes).into_owned()
}
#[test]
fn fuzz_recovery_never_crashes() {
let seed_bases = [
"flow F() { }",
"intent I { }",
"persona P { name: \"P\" role: \"R\" }",
"intent J { ask: \"a\" }",
"type T = String",
];
for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
let mut current = (*base).to_string();
for _ in 0..10 {
current = mutate(¤t, &mut rng);
let toks = match Lexer::new(¤t, "<fuzz>").tokenize() {
Ok(t) => t,
Err(_) => continue,
};
let _pr = Parser::new(toks).parse_with_recovery();
}
}
}
#[test]
fn missing_colon_hint_preserved_under_recovery() {
let src = "flow F() { run R { input { user_message: \"hi\" } } }";
let pr = recover(src);
if !pr.errors.is_empty() {
let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
assert!(any_msg);
}
}
#[test]
fn recovered_declarations_appear_in_source_order() {
let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
let pr = recover(src);
let names: Vec<&str> = pr
.program
.declarations
.iter()
.filter_map(|d| match d {
Declaration::Flow(f) => Some(f.name.as_str()),
_ => None,
})
.collect();
assert_eq!(names, vec!["A", "B", "C"]);
}
}
#[cfg(test)]
mod fase28_source_context_tests {
use super::*;
use crate::lexer::Lexer;
fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
SourceSnippet::new(
source.to_string(),
line,
column,
filename.to_string(),
)
.render()
}
#[test]
fn rustc_style_block_for_middle_line() {
let src = "line one\nline two\nline three\nline four\nline five";
let out = snippet(src, 3, 6, "x.axon");
assert!(out.contains("--> x.axon:3:6"));
assert!(out.contains("1 | line one"));
assert!(out.contains("2 | line two"));
assert!(out.contains("3 | line three"));
assert!(out.contains("4 | line four"));
assert!(out.contains("5 | line five"));
assert!(out.contains("\n | ^"), "out:\n{out}");
}
#[test]
fn caret_column_one_renders_correctly() {
let out = snippet("abc\n", 1, 1, "<source>");
assert!(out.contains("\n | ^"));
}
#[test]
fn first_line_clamps_context_before_to_zero() {
let src = "first\nsecond\nthird\nfourth\nfifth";
let out = snippet(src, 1, 1, "<source>");
assert!(out.contains("1 | first"));
assert!(out.contains("2 | second"));
assert!(out.contains("3 | third"));
assert!(!out.contains("4 | fourth"));
}
#[test]
fn last_line_clamps_context_after_to_eof() {
let src = "first\nsecond\nthird\nfourth\nfifth";
let out = snippet(src, 5, 2, "<source>");
assert!(out.contains("5 | fifth"));
assert!(out.contains("3 | third"));
assert!(out.contains("4 | fourth"));
assert!(!out.contains("2 | second"));
}
#[test]
fn gutter_width_grows_with_line_count() {
let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
let out = snippet(&src, 12, 1, "<source>");
assert!(out.contains("12 | line12"));
assert!(out.contains("10 | line10"));
}
#[test]
fn empty_source_returns_empty() {
assert_eq!(snippet("", 1, 1, "<source>"), "");
}
#[test]
fn zero_line_returns_empty() {
assert_eq!(snippet("hi", 0, 1, "<source>"), "");
}
#[test]
fn out_of_range_line_returns_empty() {
assert_eq!(snippet("hi", 99, 1, "<source>"), "");
}
#[test]
fn caret_clamps_past_eol() {
let out = snippet("hello", 1, 50, "<source>");
assert!(out.contains("\n | ^"), "out:\n{out}");
}
#[test]
fn unicode_codepoint_count_for_caret_clamp() {
let out = snippet("héllo", 1, 99, "<source>");
assert!(out.contains("\n | ^"), "out:\n{out}");
}
#[test]
fn trailing_newline_does_not_create_phantom_last_line() {
let out = snippet("first\nsecond\n", 2, 1, "<source>");
assert!(!out.contains("3 |"));
assert!(out.contains("2 | second"));
}
fn lex(src: &str) -> Vec<Token> {
Lexer::new(src, "<test>").tokenize().expect("lex")
}
#[test]
fn strict_parse_attaches_snippet_when_source_given() {
let src = "garbage_token\nflow F() { }";
let err = Parser::new(lex(src))
.with_source(src, "x.axon")
.parse()
.expect_err("must error");
assert!(err.source_snippet.is_some());
let display = format!("{err}");
assert!(display.contains("--> x.axon:"), "display: {display}");
}
#[test]
fn strict_parse_no_snippet_when_no_source() {
let src = "garbage_token";
let err = Parser::new(lex(src)).parse().expect_err("must error");
assert!(err.source_snippet.is_none());
let display = format!("{err}");
assert!(!display.contains("\n -->"));
}
#[test]
fn every_recovered_error_has_snippet() {
let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
let result = Parser::new(lex(src))
.with_source(src, "multi.axon")
.parse_with_recovery();
assert!(!result.errors.is_empty());
for err in &result.errors {
assert!(err.source_snippet.is_some());
let display = format!("{err}");
assert!(
display.contains("--> multi.axon:"),
"display: {display}"
);
}
}
#[test]
fn recovery_no_snippet_when_no_source() {
let src = "garbage1 garbage2";
let result = Parser::new(lex(src)).parse_with_recovery();
for err in &result.errors {
assert!(err.source_snippet.is_none());
}
}
#[test]
fn snippet_points_at_correct_line_for_each_error() {
let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
let result = Parser::new(lex(src))
.with_source(src, "x")
.parse_with_recovery();
for err in &result.errors {
let sn = err.source_snippet.as_ref().expect("snippet");
assert_eq!(sn.line, err.line);
}
}
#[test]
fn legacy_constructor_still_works() {
let src = "flow F() { }";
let prog = Parser::new(lex(src)).parse().expect("clean");
assert_eq!(prog.declarations.len(), 1);
}
#[test]
fn attach_source_idempotent() {
let err = ParseError {
message: "bad".to_string(),
line: 2,
column: 3,
..Default::default()
};
let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
let first = format!("{err2}");
let err3 = err.attach_source("a\nb\nc\n", "f.axon");
let second = format!("{err3}");
assert_eq!(first, second);
}
#[test]
fn attach_source_noop_when_line_zero() {
let err = ParseError {
message: "bad".to_string(),
line: 0,
column: 0,
..Default::default()
};
let err = err.attach_source("a\nb\nc\n", "f.axon");
assert!(err.source_snippet.is_none());
}
#[test]
fn golden_simple_three_line_block() {
let src = "alpha\nbeta\ngamma";
let out = snippet(src, 2, 3, "g.axon");
let expected = concat!(
" --> g.axon:2:3\n",
" |\n",
"1 | alpha\n",
"2 | beta\n",
" | ^\n",
"3 | gamma",
);
assert_eq!(out, expected);
}
#[test]
fn golden_first_line_caret() {
let src = "abc\ndef\n";
let out = snippet(src, 1, 1, "x");
let expected = concat!(
" --> x:1:1\n",
" |\n",
"1 | abc\n",
" | ^\n",
"2 | def",
);
assert_eq!(out, expected);
}
#[test]
fn golden_two_digit_gutter() {
let src: String = (1..=11)
.map(|i| format!("L{i}"))
.collect::<Vec<_>>()
.join("\n");
let out = snippet(&src, 10, 2, "big");
let expected = concat!(
" --> big:10:2\n",
" |\n",
" 8 | L8\n",
" 9 | L9\n",
"10 | L10\n",
" | ^\n",
"11 | L11",
);
assert_eq!(out, expected);
}
}
#[cfg(test)]
mod fase28_smart_suggest_parser_tests {
use super::*;
use crate::lexer::Lexer;
fn lex(src: &str) -> Vec<Token> {
Lexer::new(src, "<test>").tokenize().expect("lex")
}
#[test]
fn top_level_typo_suggests_flow() {
let src = "flwo F() { }";
let err = Parser::new(lex(src)).parse().expect_err("must error");
assert!(
err.message.contains("Did you mean `flow`?"),
"msg: {}",
err.message
);
}
#[test]
fn top_level_unknown_far_no_suggestion() {
let src = "qwerty F() { }";
let err = Parser::new(lex(src)).parse().expect_err("must error");
assert!(
!err.message.contains("Did you mean"),
"msg: {}",
err.message
);
}
#[test]
fn flow_body_typo_suggests_step() {
let src = "flow F() { stepp S {} }";
let err = Parser::new(lex(src)).parse().expect_err("must error");
assert!(
err.message.contains("Did you mean `step`"),
"msg: {}",
err.message
);
}
#[test]
fn flow_body_typo_suggests_reason() {
let src = "flow F() { reasn R {} }";
let err = Parser::new(lex(src)).parse().expect_err("must error");
assert!(
err.message.contains("Did you mean `reason`?"),
"msg: {}",
err.message
);
}
#[test]
fn recovery_mode_carries_hint() {
let src = "flwo F() { }";
let result = Parser::new(lex(src)).parse_with_recovery();
assert!(
result
.errors
.iter()
.any(|e| e.message.contains("Did you mean `flow`?")),
"errors: {:?}",
result.errors
);
}
}
#[cfg(test)]
mod fase35m_mutate_purge_where_tests {
use super::*;
fn parse(src: &str) -> Program {
let tokens = crate::lexer::Lexer::new(src, "<test>")
.tokenize()
.expect("lex");
Parser::new(tokens).parse().expect("parse")
}
fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
for d in &prog.declarations {
if let Declaration::Flow(f) = d {
if f.name == flow {
return f.body.first().expect("flow has at least one step");
}
}
}
panic!("flow `{flow}` not found");
}
#[test]
fn mutate_captures_its_where_clause() {
let prog =
parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
match first_step(&prog, "F") {
FlowStep::Mutate(m) => {
assert_eq!(m.store_name, "accounts");
assert_eq!(m.where_expr, "id = 1");
}
other => panic!("expected Mutate, got {other:?}"),
}
}
#[test]
fn purge_captures_its_where_clause() {
let prog =
parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
match first_step(&prog, "F") {
FlowStep::Purge(p) => {
assert_eq!(p.store_name, "logs");
assert_eq!(p.where_expr, "ts < 100");
}
other => panic!("expected Purge, got {other:?}"),
}
}
#[test]
fn mutate_without_a_where_block_is_a_whole_store_op() {
let prog = parse("flow F() -> Unit { mutate accounts }");
match first_step(&prog, "F") {
FlowStep::Mutate(m) => {
assert_eq!(m.store_name, "accounts");
assert_eq!(m.where_expr, "");
}
other => panic!("expected Mutate, got {other:?}"),
}
}
}
#[cfg(test)]
mod fase35o_persist_fields_tests {
use super::*;
fn parse(src: &str) -> Program {
let tokens = crate::lexer::Lexer::new(src, "<test>")
.tokenize()
.expect("lex");
Parser::new(tokens).parse().expect("parse")
}
fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
for d in &prog.declarations {
if let Declaration::Flow(f) = d {
if f.name == flow {
return f.body.first().expect("flow has at least one step");
}
}
}
panic!("flow `{flow}` not found");
}
#[test]
fn persist_captures_its_field_block() {
let prog = parse(
"flow F() -> Unit { persist into chat_history { \
session_id: \"${session_id}\" sender: \"user\" \
content: \"${message}\" } }",
);
match first_step(&prog, "F") {
FlowStep::Persist(p) => {
assert_eq!(p.store_name, "chat_history");
assert_eq!(
p.fields,
vec![
("session_id".to_string(), "${session_id}".to_string()),
("sender".to_string(), "user".to_string()),
("content".to_string(), "${message}".to_string()),
]
);
}
other => panic!("expected Persist, got {other:?}"),
}
}
#[test]
fn persist_without_a_block_keeps_the_user_bindings_fallback() {
let prog = parse("flow F() -> Unit { persist events }");
match first_step(&prog, "F") {
FlowStep::Persist(p) => {
assert_eq!(p.store_name, "events");
assert!(p.fields.is_empty());
}
other => panic!("expected Persist, got {other:?}"),
}
}
#[test]
fn persist_accepts_the_optional_into_connector() {
let with =
parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
let without =
parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
for prog in [&with, &without] {
match first_step(prog, "F") {
FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
other => panic!("expected Persist, got {other:?}"),
}
}
}
#[test]
fn persist_into_without_a_block_resolves_the_store_name() {
let prog = parse("flow F() -> Unit { persist into events }");
match first_step(&prog, "F") {
FlowStep::Persist(p) => {
assert_eq!(p.store_name, "events");
assert!(p.fields.is_empty());
}
other => panic!("expected Persist, got {other:?}"),
}
}
#[test]
fn persist_fields_lower_into_the_ir() {
let prog = parse(
"flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
);
let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
match flow.steps.first().expect("one step") {
crate::ir_nodes::IRFlowNode::Persist(p) => {
assert_eq!(p.store_name, "chat");
assert_eq!(
p.fields,
vec![("content".to_string(), "${msg}".to_string())]
);
}
other => panic!("expected IRFlowNode::Persist, got {other:?}"),
}
}
}