Skip to main content

sim_lib_openai_server/plan/
parse.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2
3use crate::plan::combinators::plan_symbol;
4
5/// Parses the textual plan surface syntax into a plan [`Expr`], erroring on
6/// trailing input or malformed combinators.
7pub fn parse_plan(input: &str) -> Result<Expr> {
8    let mut parser = PlanParser::new(input);
9    let plan = parser.parse_plan()?;
10    parser.skip_ws();
11    if parser.is_done() {
12        Ok(plan)
13    } else {
14        Err(Error::Eval(format!(
15            "unexpected trailing plan input near {:?}",
16            parser.remaining()
17        )))
18    }
19}
20
21struct PlanParser<'a> {
22    input: &'a str,
23    pos: usize,
24}
25
26impl<'a> PlanParser<'a> {
27    fn new(input: &'a str) -> Self {
28        Self { input, pos: 0 }
29    }
30
31    fn parse_plan(&mut self) -> Result<Expr> {
32        self.skip_ws();
33        let start = self.pos;
34        let name = self.parse_ident()?;
35        self.skip_ws();
36        if self.peek() == Some('(') {
37            self.bump();
38            return self.parse_combinator(name);
39        }
40        self.pos = start;
41        self.parse_atom()
42    }
43
44    fn parse_combinator(&mut self, name: String) -> Result<Expr> {
45        let mut args = Vec::new();
46        self.skip_ws();
47        if self.peek() == Some(')') {
48            self.bump();
49            return Ok(Expr::List(vec![Expr::Symbol(plan_symbol(&name))]));
50        }
51
52        loop {
53            args.push(self.parse_arg()?);
54            self.skip_ws();
55            match self.peek() {
56                Some(',') => {
57                    self.bump();
58                    self.skip_ws();
59                }
60                Some(')') => {
61                    self.bump();
62                    break;
63                }
64                _ => {
65                    return Err(Error::Eval(format!(
66                        "expected ',' or ')' in plan combinator near {:?}",
67                        self.remaining()
68                    )));
69                }
70            }
71        }
72
73        let mut items = Vec::with_capacity(args.len() + 1);
74        items.push(Expr::Symbol(plan_symbol(&name)));
75        items.extend(args);
76        Ok(Expr::List(items))
77    }
78
79    fn parse_arg(&mut self) -> Result<Expr> {
80        self.skip_ws();
81        let start = self.pos;
82        if let Ok(name) = self.parse_ident() {
83            self.skip_ws();
84            if self.peek() == Some(':') {
85                self.bump();
86                let value = self.parse_plan_or_literal()?;
87                return Ok(keyword_expr(name, value));
88            }
89        }
90        self.pos = start;
91        self.parse_plan()
92    }
93
94    fn parse_plan_or_literal(&mut self) -> Result<Expr> {
95        self.skip_ws();
96        let start = self.pos;
97        if self.parse_ident().is_ok() {
98            self.skip_ws();
99            if self.peek() == Some('(') {
100                self.pos = start;
101                return self.parse_plan();
102            }
103        }
104        self.pos = start;
105        self.parse_atom()
106    }
107
108    fn parse_atom(&mut self) -> Result<Expr> {
109        self.skip_ws();
110        let start = self.pos;
111        while let Some(ch) = self.peek() {
112            if ch == ',' || ch == ')' {
113                break;
114            }
115            self.bump();
116        }
117        let atom = self.input[start..self.pos].trim();
118        if atom.is_empty() {
119            return Err(Error::Eval("plan atom cannot be empty".to_owned()));
120        }
121        if atom.contains('(') {
122            return Err(Error::Eval(format!("invalid plan atom {atom:?}")));
123        }
124        Ok(Expr::List(vec![
125            Expr::Symbol(plan_symbol("atom")),
126            Expr::String(atom.to_owned()),
127        ]))
128    }
129
130    fn parse_ident(&mut self) -> Result<String> {
131        self.skip_ws();
132        let mut chars = self.remaining().char_indices();
133        let Some((_, first)) = chars.next() else {
134            return Err(Error::Eval("expected plan identifier".to_owned()));
135        };
136        if !first.is_ascii_alphabetic() {
137            return Err(Error::Eval("expected plan identifier".to_owned()));
138        }
139        let mut end = self.pos + first.len_utf8();
140        for (offset, ch) in chars {
141            if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
142                end = self.pos + offset + ch.len_utf8();
143            } else {
144                break;
145            }
146        }
147        let ident = self.input[self.pos..end].to_owned();
148        self.pos = end;
149        Ok(ident)
150    }
151
152    fn skip_ws(&mut self) {
153        while matches!(self.peek(), Some(ch) if ch.is_ascii_whitespace()) {
154            self.bump();
155        }
156    }
157
158    fn is_done(&self) -> bool {
159        self.pos >= self.input.len()
160    }
161
162    fn remaining(&self) -> &str {
163        &self.input[self.pos..]
164    }
165
166    fn peek(&self) -> Option<char> {
167        self.remaining().chars().next()
168    }
169
170    fn bump(&mut self) {
171        if let Some(ch) = self.peek() {
172            self.pos += ch.len_utf8();
173        }
174    }
175}
176
177fn keyword_expr(name: String, value: Expr) -> Expr {
178    Expr::Map(vec![
179        (
180            Expr::Symbol(Symbol::new("keyword")),
181            Expr::Symbol(Symbol::new(name)),
182        ),
183        (Expr::Symbol(Symbol::new("value")), value),
184    ])
185}