1mod lex;
6mod ops;
7mod parse;
8mod print;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ParseError {
13 pub line: usize,
14 pub col: usize,
15 pub message: String,
16}
17
18impl std::fmt::Display for ParseError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "{}:{}: {}", self.line, self.col, self.message)
21 }
22}
23
24impl std::error::Error for ParseError {}
25
26pub use parse::parse;
27pub use print::format;
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32
33 #[test]
34 fn parse_error_displays_with_position() {
35 let e = ParseError {
36 line: 3,
37 col: 7,
38 message: "bad token".into(),
39 };
40 assert_eq!(e.to_string(), "3:7: bad token");
41 }
42}
43
44#[cfg(test)]
45mod roundtrip {
46 use super::{format, parse};
47 use crate::spec::Expr;
48 use serde_json::{json, Value};
49
50 fn corpus() -> Vec<Value> {
51 vec![
52 json!({"op":"Gt","l":{"op":"Data","name":"close"},
53 "r":{"op":"Average","of":{"op":"Data","name":"close"},"n":2}}),
54 json!({"op":"Rank","of":{"op":"Neg","of":{"op":"Data","name":"pe"}}}),
55 json!({"op":"Vwap","high":{"op":"Data","name":"high"},"low":{"op":"Data","name":"low"},
56 "close":{"op":"Data","name":"close"},"volume":{"op":"Data","name":"volume"},"n":20}),
57 json!({"op":"Mask","of":{"op":"IsLargest","of":{"op":"Data","name":"x"},"n":30},
58 "by":{"op":"Gt","l":{"op":"Data","name":"market_cap"},"r":{"op":"Const","value":500000000}}}),
59 json!({"op":"Sustain","of":{"op":"Data","name":"x"},"nwindow":3,"nsatisfy":2}),
60 json!({"op":"Rebalance","of":{"op":"Data","name":"x"},"freq":"ME"}),
61 json!({"op":"Neutralize","of":{"op":"Data","name":"x"},
62 "by":[{"op":"Data","name":"pe"},{"op":"Data","name":"market_cap"}]}),
63 json!({"op":"NeutralizeIndustry","of":{"op":"Data","name":"x"}}),
64 json!({"op":"HoldUntil",
65 "entry":{"op":"Gt","l":{"op":"Data","name":"close"},"r":{"op":"Const","value":1}},
66 "exit":{"op":"Lt","l":{"op":"Data","name":"close"},"r":{"op":"Const","value":1}},
67 "nstocks_limit":1}),
68 json!({"op":"Add",
69 "l":{"op":"Mul","l":{"op":"Const","value":2},"r":{"op":"Data","name":"x"}},
70 "r":{"op":"Data","name":"y"}}),
71 json!({"op":"IndustryRank","of":{"op":"Data","name":"x"},
72 "categories":["tech","fin"]}),
73 ]
74 }
75
76 #[test]
77 fn corpus_is_valid_expr() {
78 for tree in corpus() {
79 let r: Result<Expr, _> = serde_json::from_value(tree.clone());
80 assert!(r.is_ok(), "not a valid Expr: {tree}");
81 }
82 }
83
84 #[test]
85 fn parse_of_print_is_identity() {
86 for tree in corpus() {
87 let text = format(&tree);
88 let back = parse(&text).unwrap_or_else(|e| panic!("reparse failed for `{text}`: {e}"));
89 assert_eq!(back, tree, "round-trip mismatch via `{text}`");
90 }
91 }
92
93 #[test]
97 fn print_is_idempotent() {
98 for tree in corpus() {
99 let once = format(&tree);
100 let reparsed = parse(&once).unwrap();
101 assert_eq!(
102 format(&reparsed),
103 once,
104 "format not idempotent for `{once}`"
105 );
106 }
107 }
108}