Skip to main content

lemon/dsl/
parse.rs

1//! DSL text → JSON `Expr` tree (a `serde_json::Value`).
2
3use std::collections::HashMap;
4
5use serde_json::{json, Map, Value};
6
7use super::lex::{lex, Token, TokenKind};
8use super::ops::{self, Field};
9use super::ParseError;
10
11/// Lower DSL text to a JSON `Expr` tree.
12pub fn parse(src: &str) -> Result<Value, ParseError> {
13    let toks = lex(src)?;
14    let mut p = Parser {
15        toks: &toks,
16        pos: 0,
17        env: HashMap::new(),
18    };
19    p.program()
20}
21
22struct Parser<'a> {
23    toks: &'a [Token],
24    pos: usize,
25    env: HashMap<String, Value>,
26}
27
28impl<'a> Parser<'a> {
29    fn peek(&self) -> &Token {
30        &self.toks[self.pos]
31    }
32
33    fn next(&mut self) -> Token {
34        let t = self.toks[self.pos].clone();
35        if self.pos + 1 < self.toks.len() {
36            self.pos += 1;
37        }
38        t
39    }
40
41    fn err(&self, message: impl Into<String>) -> ParseError {
42        let t = self.peek();
43        ParseError {
44            line: t.line,
45            col: t.col,
46            message: message.into(),
47        }
48    }
49
50    fn program(&mut self) -> Result<Value, ParseError> {
51        while self.peek().kind == TokenKind::Let {
52            self.next(); // `let`
53            let (name, line, col) = match self.next() {
54                Token {
55                    kind: TokenKind::Ident(n),
56                    line,
57                    col,
58                } => (n, line, col),
59                t => {
60                    return Err(ParseError {
61                        line: t.line,
62                        col: t.col,
63                        message: "expected name after `let`".into(),
64                    })
65                }
66            };
67            if self.next().kind != TokenKind::Eq {
68                return Err(self.err("expected `=` in let binding"));
69            }
70            let value = self.expr(0)?;
71            let value = promote(value).map_err(|m| self.err(m))?;
72            if self.env.contains_key(&name) {
73                return Err(ParseError {
74                    line,
75                    col,
76                    message: format!("`{name}` is already defined"),
77                });
78            }
79            self.env.insert(name, value);
80        }
81
82        let v = self.expr(0)?;
83        if self.peek().kind != TokenKind::Eof {
84            return Err(self.err("unexpected trailing input"));
85        }
86        if !v.is_object() {
87            return Err(self.err("a strategy must be an expression, not a bare constant"));
88        }
89        Ok(v)
90    }
91
92    fn expr(&mut self, min_bp: u8) -> Result<Value, ParseError> {
93        // prefix
94        let mut lhs = if let TokenKind::Op(op) = &self.peek().kind {
95            let op = op.clone();
96            if let Some(tag) = ops::prefix_tag(&op) {
97                self.next();
98                let operand = self.expr(7)?; // prefix binds tight
99                let of = promote(operand).map_err(|m| self.err(m))?;
100                json!({ "op": tag, "of": of })
101            } else {
102                return Err(self.err(format!("unexpected operator `{op}`")));
103            }
104        } else {
105            self.primary()?
106        };
107
108        loop {
109            let op = match &self.peek().kind {
110                TokenKind::Op(s) => s.clone(),
111                TokenKind::Ident(s) if s == "and" || s == "or" => s.clone(),
112                _ => break,
113            };
114            let Some((l_bp, r_bp, tag)) = infix_binding(&op) else {
115                break;
116            };
117            if l_bp < min_bp {
118                break;
119            }
120            self.next(); // consume operator
121            let rhs = self.expr(r_bp)?;
122            let l = promote(lhs).map_err(|m| self.err(m))?;
123            let r = promote(rhs).map_err(|m| self.err(m))?;
124            lhs = json!({ "op": tag, "l": l, "r": r });
125        }
126        Ok(lhs)
127    }
128
129    fn primary(&mut self) -> Result<Value, ParseError> {
130        let t = self.next();
131        match t.kind {
132            TokenKind::Num(n) => Ok(number(n)),
133            TokenKind::Str(s) => Ok(Value::String(s)),
134            TokenKind::LParen => {
135                let v = self.expr(0)?;
136                if self.next().kind != TokenKind::RParen {
137                    return Err(self.err("expected `)`"));
138                }
139                Ok(v)
140            }
141            TokenKind::Ident(name) => {
142                if name == "true" {
143                    return Ok(Value::Bool(true));
144                }
145                if name == "false" {
146                    return Ok(Value::Bool(false));
147                }
148                if self.peek().kind == TokenKind::LParen {
149                    self.call(&name, t.line, t.col)
150                } else if let Some(bound) = self.env.get(&name) {
151                    Ok(bound.clone())
152                } else {
153                    Ok(json!({ "op": "Data", "name": name }))
154                }
155            }
156            other => Err(ParseError {
157                line: t.line,
158                col: t.col,
159                message: format!("unexpected token {other:?}"),
160            }),
161        }
162    }
163
164    fn call(&mut self, name: &str, line: usize, col: usize) -> Result<Value, ParseError> {
165        let sig = ops::op_by_name(name).ok_or(ParseError {
166            line,
167            col,
168            message: format!("unknown op `{name}`"),
169        })?;
170        self.next(); // consume `(`
171
172        let mut positional: Vec<Value> = Vec::new();
173        let mut keyword: HashMap<String, Value> = HashMap::new();
174        let mut seen_keyword = false;
175
176        while self.peek().kind != TokenKind::RParen {
177            // keyword?  ident `=` value
178            if let TokenKind::Ident(k) = &self.peek().kind {
179                if self.toks[self.pos + 1].kind == TokenKind::Eq {
180                    let key = k.clone();
181                    self.next(); // ident
182                    self.next(); // `=`
183                    let val = self.arg_value()?;
184                    keyword.insert(key, val);
185                    seen_keyword = true;
186                    self.eat_comma()?;
187                    continue;
188                }
189            }
190            if seen_keyword {
191                return Err(self.err("positional argument after keyword argument"));
192            }
193            let val = self.arg_value()?;
194            positional.push(val);
195            self.eat_comma()?;
196        }
197        self.next(); // consume `)`
198
199        build(sig, positional, keyword).map_err(|m| ParseError {
200            line,
201            col,
202            message: m,
203        })
204    }
205
206    /// A single argument: `[ ... ]` list literal or a full expression.
207    fn arg_value(&mut self) -> Result<Value, ParseError> {
208        if self.peek().kind == TokenKind::LBracket {
209            self.next();
210            let mut items = Vec::new();
211            while self.peek().kind != TokenKind::RBracket {
212                items.push(self.expr(0)?);
213                if self.peek().kind == TokenKind::Comma {
214                    self.next();
215                }
216            }
217            self.next(); // `]`
218            return Ok(Value::Array(items));
219        }
220        self.expr(0)
221    }
222
223    fn eat_comma(&mut self) -> Result<(), ParseError> {
224        match self.peek().kind {
225            TokenKind::Comma => {
226                self.next();
227                Ok(())
228            }
229            TokenKind::RParen => Ok(()),
230            _ => Err(self.err("expected `,` or `)`")),
231        }
232    }
233}
234
235/// Emit an integral f64 as a JSON integer (so usize fields deserialize), else a float.
236fn number(n: f64) -> Value {
237    if n.fract() == 0.0 && n.abs() < 9.007e15 {
238        Value::from(n as i64)
239    } else {
240        Value::from(n)
241    }
242}
243
244/// A bare number used as an operand becomes a `Const`; objects pass through.
245fn promote(v: Value) -> Result<Value, String> {
246    match v {
247        Value::Number(n) => Ok(json!({ "op": "Const", "value": n })),
248        Value::Object(_) => Ok(v),
249        Value::Bool(_) => Err("a boolean cannot stand alone as an expression".into()),
250        Value::String(_) => Err("a string cannot stand alone as an expression".into()),
251        other => Err(format!("invalid expression: {other}")),
252    }
253}
254
255/// Place bound args into a JSON op object per the signature's field kinds.
256fn build(
257    sig: &ops::OpSig,
258    positional: Vec<Value>,
259    mut keyword: HashMap<String, Value>,
260) -> Result<Value, String> {
261    let mut obj = Map::new();
262    obj.insert("op".into(), Value::String(sig.tag.into()));
263
264    if positional.len() > sig.fields.len() {
265        return Err(format!(
266            "`{}` takes at most {} positional args",
267            sig.tag,
268            sig.fields.len()
269        ));
270    }
271
272    // Collect each field's value: positional by index, else keyword by name.
273    for (i, field) in sig.fields.iter().enumerate() {
274        let name = ops::field_name(field);
275        let provided = if i < positional.len() {
276            Some(positional[i].clone())
277        } else {
278            keyword.remove(name)
279        };
280        let Some(raw) = provided else {
281            // required fields must be present
282            if matches!(
283                field,
284                Field::Expr(_) | Field::Num(_) | Field::Str(_) | Field::ExprList(_)
285            ) {
286                return Err(format!("`{}` requires `{name}`", sig.tag));
287            }
288            continue;
289        };
290        obj.insert(name.into(), place(field, raw)?);
291    }
292
293    if let Some((k, _)) = keyword.into_iter().next() {
294        return Err(format!("`{}` has no field `{k}`", sig.tag));
295    }
296    Ok(Value::Object(obj))
297}
298
299fn place(field: &Field, raw: Value) -> Result<Value, String> {
300    match field {
301        Field::Expr(_) | Field::ExprOpt(_) => promote(raw),
302        Field::ExprList(n) => match raw {
303            Value::Array(items) => {
304                let mut out = Vec::new();
305                for it in items {
306                    out.push(promote(it)?);
307                }
308                Ok(Value::Array(out))
309            }
310            _ => Err(format!("`{n}` must be a list `[ ... ]`")),
311        },
312        Field::Num(n) | Field::NumOpt(n) => match raw {
313            Value::Number(_) => Ok(raw),
314            _ => Err(format!("`{n}` must be a number")),
315        },
316        Field::BoolOpt(n) => match raw {
317            Value::Bool(_) => Ok(raw),
318            _ => Err(format!("`{n}` must be true/false")),
319        },
320        Field::Str(n) | Field::StrOpt(n) => match raw {
321            Value::String(_) => Ok(raw),
322            _ => Err(format!("`{n}` must be a string")),
323        },
324        Field::StrListOpt(n) => match raw {
325            Value::Array(_) => Ok(raw),
326            _ => Err(format!("`{n}` must be a list of strings")),
327        },
328    }
329}
330
331/// (left_bp, right_bp, op_tag) for an infix operator, or None if not infix.
332fn infix_binding(op: &str) -> Option<(u8, u8, &'static str)> {
333    let level = match op {
334        "or" => 1,
335        "and" => 2,
336        ">" | "<" | ">=" | "<=" => 3,
337        "+" | "-" => 4,
338        "*" | "/" => 5,
339        _ => return None,
340    };
341    let tag = ops::binop_tag(op)?;
342    Some((level * 2, level * 2 + 1, tag))
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use serde_json::json;
349
350    fn p(src: &str) -> serde_json::Value {
351        parse(src).unwrap()
352    }
353
354    #[test]
355    fn data_leaf_and_number_call() {
356        assert_eq!(
357            p("sma(close, 2)"),
358            json!({"op":"Average","of":{"op":"Data","name":"close"},"n":2})
359        );
360    }
361
362    #[test]
363    fn keyword_argument() {
364        assert_eq!(
365            p("rank(close, ascending=false)"),
366            json!({"op":"Rank","of":{"op":"Data","name":"close"},"ascending":false})
367        );
368    }
369
370    #[test]
371    fn arity_and_unknown_keyword_errors() {
372        assert!(parse("sma(close)").is_err()); // missing n
373        assert!(parse("sma(close, 2, 3)").is_err()); // too many
374        assert!(parse("sma(close, bogus=2)").is_err()); // unknown keyword
375    }
376
377    #[test]
378    fn deserializes_into_expr() {
379        let v = p("sma(close, 2)");
380        let parsed: Result<crate::spec::Expr, _> = serde_json::from_value(v);
381        assert!(parsed.is_ok());
382    }
383
384    #[test]
385    fn comparison_with_const_promotion() {
386        assert_eq!(
387            p("close > 2"),
388            json!({"op":"Gt","l":{"op":"Data","name":"close"},"r":{"op":"Const","value":2}})
389        );
390    }
391
392    #[test]
393    fn and_or_precedence() {
394        // a and b or c  ==  (a and b) or c
395        assert_eq!(
396            p("a and b or c"),
397            json!({"op":"Or",
398                "l":{"op":"And","l":{"op":"Data","name":"a"},"r":{"op":"Data","name":"b"}},
399                "r":{"op":"Data","name":"c"}})
400        );
401    }
402
403    #[test]
404    fn arithmetic_precedence_and_unary() {
405        // 2 * value + quality  ==  (2*value) + quality ; and  -pe  -> Neg
406        assert_eq!(
407            p("rank(-pe)"),
408            json!({"op":"Rank","of":{"op":"Neg","of":{"op":"Data","name":"pe"}}})
409        );
410        assert_eq!(
411            p("2 * x + y"),
412            json!({"op":"Add",
413                "l":{"op":"Mul","l":{"op":"Const","value":2},"r":{"op":"Data","name":"x"}},
414                "r":{"op":"Data","name":"y"}})
415        );
416    }
417
418    #[test]
419    fn let_inlines_subtree() {
420        let src = "let ma = sma(close, 2)\nhold_until(entry = close > ma, exit = close < ma, nstocks_limit = 1)";
421        assert_eq!(
422            p(src),
423            json!({
424                "op":"HoldUntil",
425                "entry":{"op":"Gt","l":{"op":"Data","name":"close"},
426                         "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":2}},
427                "exit":{"op":"Lt","l":{"op":"Data","name":"close"},
428                        "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":2}},
429                "nstocks_limit":1
430            })
431        );
432    }
433
434    #[test]
435    fn rebinding_is_an_error() {
436        assert!(parse("let a = close\nlet a = pe\na > 1").is_err());
437    }
438
439    #[test]
440    fn reports_position_on_unclosed_paren() {
441        let err = parse("sma(close, 2").unwrap_err();
442        assert!(
443            err.message.contains("`,` or `)`") || err.message.contains(")"),
444            "{}",
445            err.message
446        );
447    }
448
449    #[test]
450    fn bare_constant_is_rejected() {
451        // Const may not stand alone (engine rule).
452        assert!(parse("42").is_err());
453    }
454
455    #[test]
456    fn unknown_op_names_the_token() {
457        let err = parse("frobnicate(close)").unwrap_err();
458        assert!(err.message.contains("frobnicate"), "{}", err.message);
459    }
460}