1use std::collections::{HashMap, HashSet};
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 parse_analyzed(src).map(|a| a.tree)
14}
15
16pub struct Analysis {
19 pub tree: Value,
20 pub data_refs: Vec<(String, usize, usize)>,
23 pub unused_lets: Vec<(String, usize, usize)>,
25}
26
27pub fn parse_analyzed(src: &str) -> Result<Analysis, ParseError> {
29 let toks = lex(src)?;
30 let mut p = Parser {
31 toks: &toks,
32 pos: 0,
33 env: HashMap::new(),
34 data_refs: Vec::new(),
35 let_defs: Vec::new(),
36 used_lets: HashSet::new(),
37 };
38 let tree = p.program()?;
39 let unused_lets = p
40 .let_defs
41 .iter()
42 .filter(|(n, _, _)| !p.used_lets.contains(n))
43 .cloned()
44 .collect();
45 Ok(Analysis {
46 tree,
47 data_refs: p.data_refs,
48 unused_lets,
49 })
50}
51
52struct Parser<'a> {
53 toks: &'a [Token],
54 pos: usize,
55 env: HashMap<String, Value>,
56 data_refs: Vec<(String, usize, usize)>,
57 let_defs: Vec<(String, usize, usize)>,
58 used_lets: HashSet<String>,
59}
60
61impl<'a> Parser<'a> {
62 fn peek(&self) -> &Token {
63 &self.toks[self.pos]
64 }
65
66 fn next(&mut self) -> Token {
67 let t = self.toks[self.pos].clone();
68 if self.pos + 1 < self.toks.len() {
69 self.pos += 1;
70 }
71 t
72 }
73
74 fn err(&self, message: impl Into<String>) -> ParseError {
75 let t = self.peek();
76 ParseError {
77 line: t.line,
78 col: t.col,
79 message: message.into(),
80 }
81 }
82
83 fn program(&mut self) -> Result<Value, ParseError> {
84 while self.peek().kind == TokenKind::Let {
85 self.next(); let (name, line, col) = match self.next() {
87 Token {
88 kind: TokenKind::Ident(n),
89 line,
90 col,
91 } => (n, line, col),
92 t => {
93 return Err(ParseError {
94 line: t.line,
95 col: t.col,
96 message: "expected name after `let`".into(),
97 })
98 }
99 };
100 if self.next().kind != TokenKind::Eq {
101 return Err(self.err("expected `=` in let binding"));
102 }
103 let value = self.expr(0)?;
104 let value = promote(value).map_err(|m| self.err(m))?;
105 if self.env.contains_key(&name) {
106 return Err(ParseError {
107 line,
108 col,
109 message: format!("`{name}` is already defined"),
110 });
111 }
112 self.let_defs.push((name.clone(), line, col));
113 self.env.insert(name, value);
114 }
115
116 let v = self.expr(0)?;
117 if self.peek().kind != TokenKind::Eof {
118 return Err(self.err("unexpected trailing input"));
119 }
120 if !v.is_object() {
121 return Err(self.err("a strategy must be an expression, not a bare constant"));
122 }
123 Ok(v)
124 }
125
126 fn expr(&mut self, min_bp: u8) -> Result<Value, ParseError> {
127 let mut lhs = match &self.peek().kind {
129 TokenKind::Op(op) => {
130 let op = op.clone();
131 if let Some(tag) = ops::prefix_tag(&op) {
132 self.next();
133 let operand = self.expr(7)?; let of = promote(operand).map_err(|m| self.err(m))?;
135 json!({ "op": tag, "of": of })
136 } else {
137 return Err(self.err(format!("unexpected operator `{op}`")));
138 }
139 }
140 TokenKind::Ident(s) if s == "not" => {
143 self.next();
144 let operand = self.expr(5)?;
145 let of = promote(operand).map_err(|m| self.err(m))?;
146 json!({ "op": "Not", "of": of })
147 }
148 _ => self.primary()?,
149 };
150
151 loop {
152 let op = match &self.peek().kind {
153 TokenKind::Op(s) => s.clone(),
154 TokenKind::Ident(s) if s == "and" || s == "or" => s.clone(),
155 _ => break,
156 };
157 let Some((l_bp, r_bp, tag)) = infix_binding(&op) else {
158 break;
159 };
160 if l_bp < min_bp {
161 break;
162 }
163 self.next(); let rhs = self.expr(r_bp)?;
165 let l = promote(lhs).map_err(|m| self.err(m))?;
166 let r = promote(rhs).map_err(|m| self.err(m))?;
167 lhs = json!({ "op": tag, "l": l, "r": r });
168 }
169 Ok(lhs)
170 }
171
172 fn primary(&mut self) -> Result<Value, ParseError> {
173 let t = self.next();
174 match t.kind {
175 TokenKind::Num(n) => Ok(number(n)),
176 TokenKind::Str(s) => Ok(Value::String(s)),
177 TokenKind::LParen => {
178 let v = self.expr(0)?;
179 if self.next().kind != TokenKind::RParen {
180 return Err(self.err("expected `)`"));
181 }
182 Ok(v)
183 }
184 TokenKind::Ident(name) => {
185 if name == "true" {
186 return Ok(Value::Bool(true));
187 }
188 if name == "false" {
189 return Ok(Value::Bool(false));
190 }
191 if self.peek().kind == TokenKind::LParen {
192 self.call(&name, t.line, t.col)
193 } else if let Some(bound) = self.env.get(&name) {
194 self.used_lets.insert(name);
195 Ok(bound.clone())
196 } else {
197 self.data_refs.push((name.clone(), t.line, t.col));
198 Ok(json!({ "op": "Data", "name": name }))
199 }
200 }
201 other => Err(ParseError {
202 line: t.line,
203 col: t.col,
204 message: format!("unexpected token {other:?}"),
205 }),
206 }
207 }
208
209 fn call(&mut self, name: &str, line: usize, col: usize) -> Result<Value, ParseError> {
210 let sig = ops::op_by_name(name).ok_or(ParseError {
211 line,
212 col,
213 message: format!("unknown op `{name}`"),
214 })?;
215 self.next(); let mut positional: Vec<Value> = Vec::new();
218 let mut keyword: HashMap<String, Value> = HashMap::new();
219 let mut seen_keyword = false;
220
221 while self.peek().kind != TokenKind::RParen {
222 if let TokenKind::Ident(k) = &self.peek().kind {
224 if self.toks[self.pos + 1].kind == TokenKind::Eq {
225 let key = k.clone();
226 self.next(); self.next(); let val = self.arg_value()?;
229 keyword.insert(key, val);
230 seen_keyword = true;
231 self.eat_comma()?;
232 continue;
233 }
234 }
235 if seen_keyword {
236 return Err(self.err("positional argument after keyword argument"));
237 }
238 let val = self.arg_value()?;
239 positional.push(val);
240 self.eat_comma()?;
241 }
242 self.next(); build(sig, positional, keyword).map_err(|m| ParseError {
245 line,
246 col,
247 message: m,
248 })
249 }
250
251 fn arg_value(&mut self) -> Result<Value, ParseError> {
253 if self.peek().kind == TokenKind::LBracket {
254 self.next();
255 let mut items = Vec::new();
256 while self.peek().kind != TokenKind::RBracket {
257 items.push(self.expr(0)?);
258 if self.peek().kind == TokenKind::Comma {
259 self.next();
260 }
261 }
262 self.next(); return Ok(Value::Array(items));
264 }
265 self.expr(0)
266 }
267
268 fn eat_comma(&mut self) -> Result<(), ParseError> {
269 match self.peek().kind {
270 TokenKind::Comma => {
271 self.next();
272 Ok(())
273 }
274 TokenKind::RParen => Ok(()),
275 _ => Err(self.err("expected `,` or `)`")),
276 }
277 }
278}
279
280fn number(n: f64) -> Value {
282 if n.fract() == 0.0 && n.abs() < 9.007e15 {
283 Value::from(n as i64)
284 } else {
285 Value::from(n)
286 }
287}
288
289fn promote(v: Value) -> Result<Value, String> {
291 match v {
292 Value::Number(n) => Ok(json!({ "op": "Const", "value": n })),
293 Value::Object(_) => Ok(v),
294 Value::Bool(_) => Err("a boolean cannot stand alone as an expression".into()),
295 Value::String(_) => Err("a string cannot stand alone as an expression".into()),
296 other => Err(format!("invalid expression: {other}")),
297 }
298}
299
300fn build(
302 sig: &ops::OpSig,
303 positional: Vec<Value>,
304 mut keyword: HashMap<String, Value>,
305) -> Result<Value, String> {
306 let mut obj = Map::new();
307 obj.insert("op".into(), Value::String(sig.tag.into()));
308
309 if positional.len() > sig.fields.len() {
310 return Err(format!(
311 "`{}` takes at most {} positional args",
312 sig.tag,
313 sig.fields.len()
314 ));
315 }
316
317 for (i, field) in sig.fields.iter().enumerate() {
319 let name = ops::field_name(field);
320 let provided = if i < positional.len() {
321 Some(positional[i].clone())
322 } else {
323 keyword.remove(name)
324 };
325 let Some(raw) = provided else {
326 if matches!(
328 field,
329 Field::Expr(_) | Field::Num(_) | Field::Str(_) | Field::ExprList(_)
330 ) {
331 return Err(format!("`{}` requires `{name}`", sig.tag));
332 }
333 continue;
334 };
335 obj.insert(name.into(), place(field, raw)?);
336 }
337
338 if let Some((k, _)) = keyword.into_iter().next() {
339 return Err(format!("`{}` has no field `{k}`", sig.tag));
340 }
341 Ok(Value::Object(obj))
342}
343
344fn place(field: &Field, raw: Value) -> Result<Value, String> {
345 match field {
346 Field::Expr(_) | Field::ExprOpt(_) => promote(raw),
347 Field::ExprList(n) => match raw {
348 Value::Array(items) => {
349 let mut out = Vec::new();
350 for it in items {
351 out.push(promote(it)?);
352 }
353 Ok(Value::Array(out))
354 }
355 _ => Err(format!("`{n}` must be a list `[ ... ]`")),
356 },
357 Field::Num(n) | Field::NumOpt(n) => match raw {
358 Value::Number(_) => Ok(raw),
359 _ => Err(format!("`{n}` must be a number")),
360 },
361 Field::BoolOpt(n) => match raw {
362 Value::Bool(_) => Ok(raw),
363 _ => Err(format!("`{n}` must be true/false")),
364 },
365 Field::Str(n) | Field::StrOpt(n) => match raw {
366 Value::String(_) => Ok(raw),
367 _ => Err(format!("`{n}` must be a string")),
368 },
369 Field::StrListOpt(n) => match raw {
370 Value::Array(_) => Ok(raw),
371 _ => Err(format!("`{n}` must be a list of strings")),
372 },
373 }
374}
375
376fn infix_binding(op: &str) -> Option<(u8, u8, &'static str)> {
378 let level = match op {
379 "or" => 1,
380 "and" => 2,
381 ">" | "<" | ">=" | "<=" => 3,
382 "+" | "-" => 4,
383 "*" | "/" => 5,
384 _ => return None,
385 };
386 let tag = ops::binop_tag(op)?;
387 Some((level * 2, level * 2 + 1, tag))
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use serde_json::json;
394
395 fn p(src: &str) -> serde_json::Value {
396 parse(src).unwrap()
397 }
398
399 #[test]
400 fn data_leaf_and_number_call() {
401 assert_eq!(
402 p("sma(close, 2)"),
403 json!({"op":"Average","of":{"op":"Data","name":"close"},"n":2})
404 );
405 }
406
407 #[test]
408 fn keyword_argument() {
409 assert_eq!(
410 p("rank(close, ascending=false)"),
411 json!({"op":"Rank","of":{"op":"Data","name":"close"},"ascending":false})
412 );
413 }
414
415 #[test]
416 fn arity_and_unknown_keyword_errors() {
417 assert!(parse("sma(close)").is_err()); assert!(parse("sma(close, 2, 3)").is_err()); assert!(parse("sma(close, bogus=2)").is_err()); }
421
422 #[test]
423 fn deserializes_into_expr() {
424 let v = p("sma(close, 2)");
425 let parsed: Result<crate::spec::Expr, _> = serde_json::from_value(v);
426 assert!(parsed.is_ok());
427 }
428
429 #[test]
430 fn comparison_with_const_promotion() {
431 assert_eq!(
432 p("close > 2"),
433 json!({"op":"Gt","l":{"op":"Data","name":"close"},"r":{"op":"Const","value":2}})
434 );
435 }
436
437 #[test]
438 fn and_or_precedence() {
439 assert_eq!(
441 p("a and b or c"),
442 json!({"op":"Or",
443 "l":{"op":"And","l":{"op":"Data","name":"a"},"r":{"op":"Data","name":"b"}},
444 "r":{"op":"Data","name":"c"}})
445 );
446 }
447
448 #[test]
449 fn arithmetic_precedence_and_unary() {
450 assert_eq!(
452 p("rank(-pe)"),
453 json!({"op":"Rank","of":{"op":"Neg","of":{"op":"Data","name":"pe"}}})
454 );
455 assert_eq!(
456 p("2 * x + y"),
457 json!({"op":"Add",
458 "l":{"op":"Mul","l":{"op":"Const","value":2},"r":{"op":"Data","name":"x"}},
459 "r":{"op":"Data","name":"y"}})
460 );
461 }
462
463 #[test]
464 fn not_binds_between_and_and_comparisons() {
465 assert_eq!(
467 p("not a > b"),
468 json!({"op":"Not","of":{"op":"Gt",
469 "l":{"op":"Data","name":"a"},"r":{"op":"Data","name":"b"}}})
470 );
471 assert_eq!(
473 p("not a and b"),
474 json!({"op":"And",
475 "l":{"op":"Not","of":{"op":"Data","name":"a"}},
476 "r":{"op":"Data","name":"b"}})
477 );
478 assert_eq!(
480 p("not not a"),
481 json!({"op":"Not","of":{"op":"Not","of":{"op":"Data","name":"a"}}})
482 );
483 assert_eq!(
485 p("not (a and b)"),
486 json!({"op":"Not","of":{"op":"And",
487 "l":{"op":"Data","name":"a"},"r":{"op":"Data","name":"b"}}})
488 );
489 }
490
491 #[test]
492 fn let_inlines_subtree() {
493 let src = "let ma = sma(close, 2)\nhold_until(entry = close > ma, exit = close < ma, nstocks_limit = 1)";
494 assert_eq!(
495 p(src),
496 json!({
497 "op":"HoldUntil",
498 "entry":{"op":"Gt","l":{"op":"Data","name":"close"},
499 "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":2}},
500 "exit":{"op":"Lt","l":{"op":"Data","name":"close"},
501 "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":2}},
502 "nstocks_limit":1
503 })
504 );
505 }
506
507 #[test]
508 fn exit_when_and_quantile_row_surface() {
509 assert_eq!(
510 p("exit_when(close > sma(close, 20), close < sma(close, 60))"),
511 json!({
512 "op":"ExitWhen",
513 "entry":{"op":"Gt","l":{"op":"Data","name":"close"},
514 "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":20}},
515 "exit":{"op":"Lt","l":{"op":"Data","name":"close"},
516 "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":60}}
517 })
518 );
519 assert_eq!(
520 p("quantile_row(roe, 0.5)"),
521 json!({"op":"QuantileRow","of":{"op":"Data","name":"roe"},"c":0.5})
522 );
523 assert_eq!(
524 p("quantile_row(of = pe, c = 0.75)"),
525 json!({"op":"QuantileRow","of":{"op":"Data","name":"pe"},"c":0.75})
526 );
527 }
528
529 #[test]
530 fn cs_preprocess_surface() {
531 assert_eq!(
532 p("winsorize(pe, 0.05, 0.95)"),
533 json!({"op":"Winsorize","of":{"op":"Data","name":"pe"},"lower":0.05,"upper":0.95})
534 );
535 assert_eq!(
536 p("zscore(roe)"),
537 json!({"op":"Zscore","of":{"op":"Data","name":"roe"}})
538 );
539 assert_eq!(
540 p("bucket(momentum, 5)"),
541 json!({"op":"Bucket","of":{"op":"Data","name":"momentum"},"n":5})
542 );
543 assert_eq!(
544 p("demean(pe)"),
545 json!({"op":"Demean","of":{"op":"Data","name":"pe"}})
546 );
547 }
548
549 #[test]
550 fn in_sector_surface() {
551 assert_eq!(
552 p(r#"in_sector(close, "Technology")"#),
553 json!({"op":"InSector","of":{"op":"Data","name":"close"},"name":"Technology"})
554 );
555 assert_eq!(
556 p(r#"mask(is_largest(roe, 10), in_sector(roe, "Energy"))"#),
557 json!({
558 "op":"Mask",
559 "of":{"op":"IsLargest","of":{"op":"Data","name":"roe"},"n":10},
560 "by":{"op":"InSector","of":{"op":"Data","name":"roe"},"name":"Energy"}
561 })
562 );
563 }
564
565 #[test]
566 fn rebinding_is_an_error() {
567 assert!(parse("let a = close\nlet a = pe\na > 1").is_err());
568 }
569
570 #[test]
571 fn reports_position_on_unclosed_paren() {
572 let err = parse("sma(close, 2").unwrap_err();
573 assert!(
574 err.message.contains("`,` or `)`") || err.message.contains(")"),
575 "{}",
576 err.message
577 );
578 }
579
580 #[test]
581 fn bare_constant_is_rejected() {
582 assert!(parse("42").is_err());
584 }
585
586 #[test]
587 fn unknown_op_names_the_token() {
588 let err = parse("frobnicate(close)").unwrap_err();
589 assert!(err.message.contains("frobnicate"), "{}", err.message);
590 }
591}