Skip to main content

ternlang_compat/
owlet.rs

1//! Owlet S-expression front-end for ternlang
2//!
3//! Parses Owlet-style S-expression ternary programs into ternlang AST
4//! nodes, which can then be compiled to BET bytecode and run on the VM.
5//!
6//! ## Owlet syntax
7//! ```text
8//! (+ 1 -1)              ; add two trits → consensus
9//! (neg -1)              ; negate → +1
10//! (cons 0 1)            ; consensus
11//! (fn f (x) (neg x))   ; define function f
12//! (f 1)                 ; call f(+1) → -1
13//! ```
14//!
15//! All numbers in Owlet are signed by default. Only -1, 0, +1 are valid trit values.
16
17use ternlang_core::ast::{Expr, Stmt, Function, Program, Type};
18
19/// A parsed S-expression token tree.
20#[derive(Debug, PartialEq, Clone)]
21pub enum Sexp {
22    Atom(String),
23    List(Vec<Sexp>),
24}
25
26/// Parse an S-expression string into a `Sexp` tree.
27pub fn parse_sexp(input: &str) -> Result<Sexp, String> {
28    let tokens = tokenise(input);
29    let mut pos = 0;
30    parse_sexp_tokens(&tokens, &mut pos)
31}
32
33fn tokenise(input: &str) -> Vec<String> {
34    let mut tokens  = Vec::new();
35    let mut current = String::new();
36    let mut in_comment = false;
37
38    for ch in input.chars() {
39        if in_comment {
40            if ch == '\n' { in_comment = false; }
41            continue;
42        }
43        match ch {
44            ';' => {
45                if !current.is_empty() { tokens.push(current.clone()); current.clear(); }
46                in_comment = true;
47            }
48            '(' | ')' => {
49                if !current.is_empty() { tokens.push(current.clone()); current.clear(); }
50                tokens.push(ch.to_string());
51            }
52            ' ' | '\t' | '\n' | '\r' => {
53                if !current.is_empty() { tokens.push(current.clone()); current.clear(); }
54            }
55            _ => current.push(ch),
56        }
57    }
58    if !current.is_empty() { tokens.push(current); }
59    tokens
60}
61
62fn parse_sexp_tokens(tokens: &[String], pos: &mut usize) -> Result<Sexp, String> {
63    if *pos >= tokens.len() {
64        return Err("Unexpected end of input".to_string());
65    }
66    let tok = tokens[*pos].clone();
67    *pos += 1;
68
69    if tok == "(" {
70        let mut list = Vec::new();
71        while *pos < tokens.len() && tokens[*pos] != ")" {
72            list.push(parse_sexp_tokens(tokens, pos)?);
73        }
74        if *pos >= tokens.len() {
75            return Err("Unmatched '('".to_string());
76        }
77        *pos += 1; // consume ')'
78        Ok(Sexp::List(list))
79    } else if tok == ")" {
80        Err("Unexpected ')'".to_string())
81    } else {
82        Ok(Sexp::Atom(tok))
83    }
84}
85
86// ─────────────────────────────────────────────────────────────────────────────
87// Sexp → ternlang AST
88// ─────────────────────────────────────────────────────────────────────────────
89
90/// Converts an S-expression tree into a ternlang `Expr`.
91pub fn sexp_to_expr(sexp: &Sexp) -> Result<Expr, String> {
92    match sexp {
93        Sexp::Atom(s) => atom_to_expr(s),
94        Sexp::List(items) => list_to_expr(items),
95    }
96}
97
98fn atom_to_expr(s: &str) -> Result<Expr, String> {
99    match s {
100        "1"  | "+1" => Ok(Expr::TritLiteral(1)),
101        "0"         => Ok(Expr::TritLiteral(0)),
102        "-1"        => Ok(Expr::TritLiteral(-1)),
103        "true"      => Ok(Expr::TritLiteral(1)),
104        "false"     => Ok(Expr::TritLiteral(-1)),
105        _           => Ok(Expr::Ident(s.to_string())),
106    }
107}
108
109fn make_call(callee: &str, args: Vec<Expr>) -> Expr {
110    Expr::Call { callee: callee.to_string(), args }
111}
112
113fn list_to_expr(items: &[Sexp]) -> Result<Expr, String> {
114    if items.is_empty() {
115        return Err("Empty S-expression list".to_string());
116    }
117
118    let head = match &items[0] {
119        Sexp::Atom(s) => s.as_str(),
120        _ => return Err("Expected operator/function name as first element".to_string()),
121    };
122
123    let args: Result<Vec<Expr>, _> = items[1..].iter().map(sexp_to_expr).collect();
124    let args = args?;
125
126    match head {
127        // Arithmetic / ternary ops
128        "+" | "add"  => {
129            require(head, &args, 2)?;
130            Ok(make_call("consensus", args))
131        }
132        "neg" | "-"  => {
133            if args.len() == 1 {
134                Ok(make_call("invert", args))
135            } else if args.len() == 2 {
136                let neg_b = make_call("invert", vec![args[1].clone()]);
137                Ok(make_call("consensus", vec![args[0].clone(), neg_b]))
138            } else {
139                Err(format!("{}: expected 1 or 2 args, got {}", head, args.len()))
140            }
141        }
142        "mul" | "*"  => { require(head, &args, 2)?; Ok(make_call("mul", args)) }
143        "cons"       => { require(head, &args, 2)?; Ok(make_call("consensus", args)) }
144        "invert"     => { require(head, &args, 1)?; Ok(make_call("invert", args)) }
145
146        // Builtins
147        "truth"    => Ok(make_call("truth",    vec![])),
148        "hold"     => Ok(make_call("hold",     vec![])),
149        "conflict" => Ok(make_call("conflict", vec![])),
150
151        // If (3-way): (if cond on+1 on0 on-1)
152        "if" => {
153            require(head, &args, 4)?;
154            // Map to ternlang IfTernary statement wrapped in a block expression.
155            // Since Expr doesn't have an if-expr variant, we build a Call to a
156            // synthetic helper that the codegen handles via ternary select.
157            // For now: represent as (consensus (cond × branch+1), (invert cond × branch-1))
158            // A cleaner approach: return a Match-like call. We use a Stmt::IfTernary
159            // indirectly by returning the positive branch expression only (simplified).
160            // Full if-expr requires extending Expr — leave as function call (Architecture defined).
161            Ok(make_call("__owlet_if__", args))
162        }
163
164        // Generic function call: (f arg1 arg2 ...)
165        name => Ok(make_call(name, args)),
166    }
167}
168
169fn require(head: &str, args: &[Expr], n: usize) -> Result<(), String> {
170    if args.len() != n {
171        Err(format!("{}: expected {} args, got {}", head, n, args.len()))
172    } else {
173        Ok(())
174    }
175}
176
177// ─────────────────────────────────────────────────────────────────────────────
178// Full Owlet program → ternlang Program
179// ─────────────────────────────────────────────────────────────────────────────
180
181/// Parse an Owlet program (multi-line S-expressions) into a ternlang `Program`.
182///
183/// Top-level `(fn name (params...) body)` become `Function` definitions.
184/// Everything else becomes the body of a generated `main` function.
185pub struct OwletParser;
186
187impl OwletParser {
188    /// Parse a complete Owlet source string into a ternlang `Program`.
189    pub fn parse(source: &str) -> Result<Program, String> {
190        let mut functions: Vec<Function> = Vec::new();
191        let mut main_body: Vec<Stmt>     = Vec::new();
192
193        // Accumulate balanced-paren expressions across lines
194        let mut depth  = 0usize;
195        let mut buffer = String::new();
196
197        for line in source.lines() {
198            // Strip comments from line
199            let line = if let Some(idx) = line.find(';') { &line[..idx] } else { line }.trim();
200            if line.is_empty() { continue; }
201
202            for ch in line.chars() {
203                match ch { '(' => depth += 1, ')' => depth = depth.saturating_sub(1), _ => {} }
204            }
205            buffer.push(' ');
206            buffer.push_str(line);
207
208            if depth == 0 && !buffer.trim().is_empty() {
209                let sexp = parse_sexp(buffer.trim())?;
210                buffer.clear();
211
212                match &sexp {
213                    Sexp::List(items) if !items.is_empty() => {
214                        if let Sexp::Atom(head) = &items[0] {
215                            match head.as_str() {
216                                "fn" | "def" => {
217                                    functions.push(parse_fn_def(items)?);
218                                    continue;
219                                }
220                                "let" if items.len() >= 3 => {
221                                    let name = match &items[1] {
222                                        Sexp::Atom(n) => n.clone(),
223                                        _ => return Err("let: expected name".to_string()),
224                                    };
225                                    let val = sexp_to_expr(&items[2])?;
226                                    main_body.push(Stmt::Let { name, ty: Type::Trit, value: val });
227                                    if items.len() >= 4 {
228                                        let body = sexp_to_expr(&items[3])?;
229                                        main_body.push(Stmt::Return(body));
230                                    }
231                                    continue;
232                                }
233                                _ => {}
234                            }
235                        }
236                    }
237                    _ => {}
238                }
239
240                let expr = sexp_to_expr(&sexp)?;
241                main_body.push(Stmt::Return(expr));
242            }
243        }
244
245        if !main_body.is_empty() {
246            functions.push(Function {
247                name:        "main".to_string(),
248                params:      vec![],
249                return_type: Type::Trit,
250                body:        main_body,
251                directive:   None,
252            });
253        }
254
255        Ok(Program { imports: vec![], import_specs: vec![], structs: vec![], agents: vec![], functions })
256    }
257}
258
259fn parse_fn_def(items: &[Sexp]) -> Result<Function, String> {
260    // (fn name (params...) body)
261    if items.len() < 4 {
262        return Err("fn: expected (fn name (params) body)".to_string());
263    }
264    let name = match &items[1] {
265        Sexp::Atom(n) => n.clone(),
266        _ => return Err("fn: expected function name".to_string()),
267    };
268    let params: Vec<(String, Type)> = match &items[2] {
269        Sexp::List(ps) => ps.iter().map(|p| match p {
270            Sexp::Atom(n) => Ok((n.clone(), Type::Trit)),
271            _ => Err("fn: expected param name atom".to_string()),
272        }).collect::<Result<_, _>>()?,
273        Sexp::Atom(p) if p == "()" => vec![],
274        _ => return Err("fn: expected param list".to_string()),
275    };
276    let body_expr = sexp_to_expr(&items[3])?;
277    Ok(Function {
278        name,
279        params,
280        return_type: Type::Trit,
281        body: vec![Stmt::Return(body_expr)],
282        directive: None,
283    })
284}
285
286// ─────────────────────────────────────────────────────────────────────────────
287// Tests
288// ─────────────────────────────────────────────────────────────────────────────
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_parse_atom_pos() {
296        let s = parse_sexp("1").unwrap();
297        assert_eq!(sexp_to_expr(&s).unwrap(), Expr::TritLiteral(1));
298    }
299
300    #[test]
301    fn test_parse_atom_neg() {
302        let s = parse_sexp("-1").unwrap();
303        assert_eq!(sexp_to_expr(&s).unwrap(), Expr::TritLiteral(-1));
304    }
305
306    #[test]
307    fn test_parse_atom_zero() {
308        let s = parse_sexp("0").unwrap();
309        assert_eq!(sexp_to_expr(&s).unwrap(), Expr::TritLiteral(0));
310    }
311
312    #[test]
313    fn test_parse_invert_call() {
314        let s = parse_sexp("(neg -1)").unwrap();
315        let expr = sexp_to_expr(&s).unwrap();
316        assert!(matches!(expr, Expr::Call { ref callee, .. } if callee == "invert"));
317    }
318
319    #[test]
320    fn test_parse_consensus_call() {
321        let s = parse_sexp("(cons 1 0)").unwrap();
322        let expr = sexp_to_expr(&s).unwrap();
323        assert!(matches!(expr, Expr::Call { ref callee, .. } if callee == "consensus"));
324    }
325
326    #[test]
327    fn test_parse_add_maps_to_consensus() {
328        let s = parse_sexp("(+ 1 -1)").unwrap();
329        let expr = sexp_to_expr(&s).unwrap();
330        assert!(matches!(expr, Expr::Call { ref callee, .. } if callee == "consensus"));
331    }
332
333    #[test]
334    fn test_parse_nested() {
335        let s = parse_sexp("(neg (neg 1))").unwrap();
336        let expr = sexp_to_expr(&s).unwrap();
337        assert!(matches!(expr, Expr::Call { ref callee, .. } if callee == "invert"));
338    }
339
340    #[test]
341    fn test_unmatched_paren_error() {
342        assert!(parse_sexp("(+ 1 2").is_err());
343    }
344
345    #[test]
346    fn test_empty_list_error() {
347        let s = parse_sexp("()").unwrap();
348        assert!(sexp_to_expr(&s).is_err());
349    }
350
351    #[test]
352    fn test_owlet_fn_parse() {
353        let src = "(fn negate (x) (neg x))";
354        let prog = OwletParser::parse(src).unwrap();
355        assert_eq!(prog.functions.len(), 1);
356        assert_eq!(prog.functions[0].name, "negate");
357        assert_eq!(prog.functions[0].params.len(), 1);
358        assert_eq!(prog.functions[0].params[0].0, "x");
359    }
360
361    #[test]
362    fn test_owlet_top_level_expr_becomes_main() {
363        let src = "(neg -1)";
364        let prog = OwletParser::parse(src).unwrap();
365        assert_eq!(prog.functions.last().unwrap().name, "main");
366    }
367
368    #[test]
369    fn test_owlet_comment_stripped() {
370        let src = "; this is a comment\n(neg 1)";
371        let prog = OwletParser::parse(src).unwrap();
372        assert!(!prog.functions.is_empty());
373    }
374
375    #[test]
376    fn test_owlet_let_binding() {
377        let src = "(let x 1)";
378        let prog = OwletParser::parse(src).unwrap();
379        let main = prog.functions.last().unwrap();
380        assert!(main.body.iter().any(|s| matches!(s, Stmt::Let { name, .. } if name == "x")));
381    }
382
383    #[test]
384    fn test_owlet_mul() {
385        let s = parse_sexp("(mul 1 -1)").unwrap();
386        let expr = sexp_to_expr(&s).unwrap();
387        assert!(matches!(expr, Expr::Call { ref callee, .. } if callee == "mul"));
388    }
389}