1use 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
11pub 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 (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 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)?; 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(); 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(); 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 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(); self.next(); 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(); build(sig, positional, keyword).map_err(|m| ParseError {
200 line,
201 col,
202 message: m,
203 })
204 }
205
206 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(); 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
235fn 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
244fn 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
255fn 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 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 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
331fn 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()); assert!(parse("sma(close, 2, 3)").is_err()); assert!(parse("sma(close, bogus=2)").is_err()); }
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 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 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 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}