//! Disposable B-syntax experiment. This is not the production language. //! All semantic checks are delegated to the real source3 compiler. use oa_core::{ Project, language::{self, TokenClass, TokenSpan}, source_project::{self, ProjectLimits, SourceBundle}, }; use std::collections::BTreeMap; pub mod writeback; #[derive(Debug, Clone)] struct Record { kind: String, header: String, alias: String, fields: BTreeMap, endpoint_ports: Option<(String, String)>, } #[derive(Debug)] enum Entry { Text(String), Record(Record), } /// Compile the complete B project after lowering its authored files to source3. pub fn compile(bundle: &SourceBundle) -> Result { let lowered = lower(bundle)?; source_project::compile_bundle(&lowered, ProjectLimits::default()) .map(|result| result.decoded.project) .map_err(|errors| format!("{errors:?}")) } pub fn lower(bundle: &SourceBundle) -> Result { let mut units = BTreeMap::new(); let mut owners = BTreeMap::new(); for (path, source) in &bundle.files { let entries = Parser::new(source)? .unit(path == &bundle.entry) .map_err(|error| format!("{path}: {error}"))?; for entry in &entries { if let Entry::Record(record) = entry && owners .insert(record.alias.clone(), record.fields.get("parent").cloned()) .is_some() { return Err(format!("Duplicate declaration alias @{}", record.alias)); } } units.insert(path.clone(), entries); } let mut files = BTreeMap::new(); for (path, entries) in units { let mut text = String::new(); for entry in entries { match entry { Entry::Text(raw) => { text.push_str(&raw); text.push('\n'); } Entry::Record(mut record) => { if let Some((source, target)) = &record.endpoint_ports { for (field, port) in [("source", source), ("target", target)] { let owner = owners .get(port) .and_then(|owner| owner.as_ref()) .ok_or_else(|| { format!("Endpoint @{port} has no explicit function owner") })?; record.fields.insert(field.into(), owner.clone()); record .fields .insert(format!("{field}_port"), format!("@{port}")); } } text.push_str(&record.header); text.push_str(" {\n"); for (name, value) in record.fields { text.push_str(&format!(" {name} {value};\n")); } text.push_str("}\n"); } } } files.insert(path, text); } Ok(SourceBundle { entry: bundle.entry.clone(), files, }) } struct Parser<'a> { source: &'a str, tokens: Vec, at: usize, } impl<'a> Parser<'a> { fn new(source: &'a str) -> Result { let tokens: Vec<_> = language::tokenize(source) .into_iter() .filter(|token| !matches!(token.class, TokenClass::Whitespace | TokenClass::Comment)) .collect(); if tokens.iter().any(|token| token.class == TokenClass::Error) { return Err("Invalid lexical token".into()); } Ok(Self { source, tokens, at: 0, }) } fn peek(&self) -> &'a str { self.tokens .get(self.at) .map_or("", |token| &self.source[token.span.start..token.span.end]) } fn take(&mut self, expected: &str) -> Result<(), String> { if self.peek() != expected { return Err(format!( "Expected {expected}, got {:?} at token {}", self.peek(), self.at )); } self.at += 1; Ok(()) } fn word(&mut self) -> Result { let token = self.tokens.get(self.at).ok_or("Unexpected end of input")?; if !matches!(token.class, TokenClass::Identifier | TokenClass::Keyword) { return Err(format!("Expected identifier, got {}", self.peek())); } let value = self.peek().to_owned(); self.at += 1; Ok(value) } fn string(&mut self) -> Result<(), String> { if self .tokens .get(self.at) .is_none_or(|token| token.class != TokenClass::String) { return Err("Expected quoted string".into()); } self.at += 1; Ok(()) } fn reference(&mut self) -> Result { self.take("@")?; self.word() } fn slice(&self, start: usize, end: usize) -> String { self.source[self.tokens[start].span.start..self.tokens[end - 1].span.end].to_owned() } fn value(&mut self) -> Result { let start = self.at; let mut closers = Vec::new(); loop { match self.peek() { "" => return Err("Unterminated field".into()), ";" if closers.is_empty() => break, "{" => closers.push("}"), "[" => closers.push("]"), "}" | "]" => { closers .pop() .filter(|closing| *closing == self.peek()) .ok_or("Unbalanced field")?; } _ => {} } if closers.len() > 64 { return Err("Field nesting exceeds experiment limit".into()); } self.at += 1; } if self.at == start { return Err("Missing field value".into()); } let value = self.slice(start, self.at); self.take(";")?; Ok(value) } fn unit(mut self, entry: bool) -> Result, String> { let mut out = Vec::new(); if entry { let start = self.at; self.take("arcadia")?; self.take("3")?; self.take(";")?; self.take("project")?; self.word()?; self.string()?; self.take("id")?; self.string()?; self.take("{")?; out.push(Entry::Text(self.slice(start, self.at))); } while !self.peek().is_empty() && !(entry && self.peek() == "}") { if self.peek() == "include" || (entry && self.peek() == "description") { let start = self.at; self.at += 1; self.value()?; out.push(Entry::Text(self.slice(start, self.at))); } else { self.record(None, &mut out, 0)?; } } if entry { self.take("}")?; out.push(Entry::Text("}".into())); } if !self.peek().is_empty() { return Err("Unexpected trailing input".into()); } Ok(out) } fn record( &mut self, owner: Option<(&str, &str)>, out: &mut Vec, depth: usize, ) -> Result { if depth > 32 { return Err("Ownership nesting exceeds experiment limit".into()); } let start = self.at; let kind = self.word()?; let alias = self.word()?; if kind != "node" { self.string()?; } self.take("id")?; self.string()?; let header = self.slice(start, self.at); let mut record = Record { kind: kind.clone(), header, alias: alias.clone(), fields: BTreeMap::new(), endpoint_ports: None, }; if matches!(kind.as_str(), "input" | "output") { let Some((owner, "sa_function")) = owner else { return Err("Typed function port shorthand requires an SA function owner".into()); }; self.take("items")?; let items = self.value()?; record.kind = "sa_port".into(); record.header = record.header.replacen(&kind, "sa_port", 1); record.fields.insert("parent".into(), format!("@{owner}")); let (variant, field) = if kind == "input" { ("FunctionInput", "incoming_items") } else { ("FunctionOutput", "outgoing_items") }; record.fields.insert( "contract".into(), format!("{{ {variant} {{ {field} {items}; }}; }}"), ); } else if kind == "functional_exchange" && self.peek() == "from" { self.take("from")?; let source = self.reference()?; self.take("to")?; let target = self.reference()?; self.take("items")?; let items = self.value()?; record.fields.insert("items".into(), items); record.endpoint_ports = Some((source, target)); if self.peek() == "{" { self.body(&mut record, out, depth)?; } for field in ["source", "target", "source_port", "target_port"] { if record.fields.contains_key(field) { return Err(format!( "Contradictory shorthand/explicit endpoint field {field}" )); } } } else { self.body(&mut record, out, depth)?; if let Some((owner, _)) = owner { if !kind.starts_with("oa_") && !kind.starts_with("sa_") { return Err("Only elements may use semantic ownership nesting".into()); } let inferred = format!("@{owner}"); if record .fields .get("parent") .is_some_and(|parent| parent != &inferred) { return Err(format!("Contradictory parent for @{alias}")); } record.fields.insert("parent".into(), inferred); } } out.push(Entry::Record(record)); Ok(alias) } fn body( &mut self, record: &mut Record, out: &mut Vec, depth: usize, ) -> Result<(), String> { self.take("{")?; while self.peek() != "}" { if self.peek().is_empty() { return Err("Unterminated declaration".into()); } let nested = self.peek().starts_with("oa_") || self.peek().starts_with("sa_") || matches!(self.peek(), "input" | "output"); if nested { self.record(Some((&record.alias, &record.kind)), out, depth + 1)?; continue; } let field = self.word()?; if record.fields.contains_key(&field) { return Err(format!("Duplicate field {field} on @{}", record.alias)); } let value = if field == "steps" && self.peek() == "{" { if !matches!( record.kind.as_str(), "oa_scenario" | "sa_scenario" | "oa_process" | "sa_functional_chain" ) { return Err("Ordered steps require a scenario/process/chain".into()); } self.take("{")?; let mut steps = Vec::new(); while self.peek() != "}" { if !matches!(self.peek(), "oa_occurrence" | "sa_occurrence") { return Err("Ordered steps contain occurrence declarations only".into()); } steps.push(format!( "@{}", self.record(Some((&record.alias, &record.kind)), out, depth + 1)? )); } self.take("}")?; self.take(";")?; format!("[{}]", steps.join(", ")) } else { self.value()? }; record.fields.insert(field, value); } self.take("}") } }