1use std::sync::Arc;
6
7use crate::ast::{BinOp, Expr, Literal, Spanned};
8use crate::lexer::Lexer;
9use crate::parser::Parser;
10use crate::replay::{JsonValue, value_to_json};
11use crate::value::Value;
12
13pub fn parse_entry_call(src: &str) -> Result<(String, Vec<Value>), String> {
24 let mut lexer = Lexer::new(src);
25 let tokens = lexer
26 .tokenize()
27 .map_err(|e| format!("lex error in entry expression: {}", e))?;
28 let mut parser = Parser::new(tokens);
29 let spanned = parser
30 .parse_expr()
31 .map_err(|e| format!("parse error in entry expression: {}", e))?;
32
33 let (target, args) = match spanned.node {
34 Expr::FnCall(target, args) => (target, args),
35 _ => {
36 return Err(
37 "entry expression must be a function call like 'loadTaxRate(\"PL\")'".to_string(),
38 );
39 }
40 };
41
42 let fn_name = match &target.node {
43 Expr::Ident(name) => name.clone(),
44 _ => {
45 return Err("entry expression target must be a bare function name \
46 (qualified paths not supported yet)"
47 .to_string());
48 }
49 };
50
51 let mut values = Vec::with_capacity(args.len());
52 for (idx, arg) in args.iter().enumerate() {
53 let val = expr_to_value(&arg.node).map_err(|e| format!("arg #{}: {}", idx + 1, e))?;
54 values.push(val);
55 }
56
57 Ok((fn_name, values))
58}
59
60fn expr_to_value(expr: &Expr) -> Result<Value, String> {
66 match expr {
67 Expr::Literal(lit) => Ok(literal_to_value(lit)),
68 Expr::BinOp(BinOp::Sub, lhs, rhs) if matches!(lhs.node, Expr::Literal(Literal::Int(0))) => {
71 match &rhs.node {
72 Expr::Literal(Literal::Int(n)) => Ok(Value::Int(-*n)),
73 Expr::Literal(Literal::Float(f)) => Ok(Value::Float(-*f)),
74 _ => {
75 Err("unary '-' must be applied to a numeric literal in entry args".to_string())
76 }
77 }
78 }
79 Expr::Ident(name) if is_upper_camel(name) => constructor_value(name, &[]),
80 Expr::Attr(_, _) if dotted_upper_path(expr).is_some() => {
81 let path = dotted_upper_path(expr).unwrap();
82 constructor_value(&path, &[])
83 }
84 Expr::Constructor(name, arg) => {
85 let fields = constructor_arg_fields(arg.as_deref())?;
86 constructor_value(name, &fields)
87 }
88 Expr::FnCall(target, args) if dotted_upper_path(&target.node).is_some() => {
89 let path = dotted_upper_path(&target.node).unwrap();
90 let mut fields = Vec::with_capacity(args.len());
91 for a in args {
92 fields.push(expr_to_value(&a.node)?);
93 }
94 constructor_value(&path, &fields)
95 }
96 Expr::List(items) => {
97 let mut out = Vec::with_capacity(items.len());
98 for e in items {
99 out.push(expr_to_value(&e.node)?);
100 }
101 Ok(Value::List(aver_rt::AverList::from_vec(out)))
102 }
103 Expr::Tuple(items) => {
104 let mut out = Vec::with_capacity(items.len());
105 for e in items {
106 out.push(expr_to_value(&e.node)?);
107 }
108 Ok(Value::Tuple(out))
109 }
110 _ => Err(
111 "unsupported expression shape (supported: literals, lists, tuples, \
112 ADT constructors like Shape.Circle(1.0) / Result.Ok(x) / Option.None)"
113 .to_string(),
114 ),
115 }
116}
117
118fn literal_to_value(lit: &Literal) -> Value {
119 match lit {
120 Literal::Int(i) => Value::Int(*i),
121 Literal::Float(f) => Value::Float(*f),
122 Literal::Str(s) => Value::Str(s.clone()),
123 Literal::Bool(b) => Value::Bool(*b),
124 Literal::Unit => Value::Unit,
125 }
126}
127
128fn is_upper_camel(name: &str) -> bool {
129 name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
130}
131
132fn dotted_upper_path(expr: &Expr) -> Option<String> {
133 match expr {
134 Expr::Ident(name) if is_upper_camel(name) => Some(name.clone()),
135 Expr::Attr(inner, field) if is_upper_camel(field) => {
136 let base = dotted_upper_path(&inner.node)?;
137 Some(format!("{}.{}", base, field))
138 }
139 _ => None,
140 }
141}
142
143fn constructor_arg_fields(arg: Option<&Spanned<Expr>>) -> Result<Vec<Value>, String> {
144 match arg {
145 None => Ok(Vec::new()),
146 Some(inner) => match &inner.node {
147 Expr::Tuple(items) => {
148 let mut out = Vec::with_capacity(items.len());
149 for e in items {
150 out.push(expr_to_value(&e.node)?);
151 }
152 Ok(out)
153 }
154 _ => Ok(vec![expr_to_value(&inner.node)?]),
155 },
156 }
157}
158
159fn constructor_value(path: &str, fields: &[Value]) -> Result<Value, String> {
160 match path {
163 "Result.Ok" | "Ok" => {
164 require_arity(path, fields, 1)?;
165 Ok(Value::Ok(Box::new(fields[0].clone())))
166 }
167 "Result.Err" | "Err" => {
168 require_arity(path, fields, 1)?;
169 Ok(Value::Err(Box::new(fields[0].clone())))
170 }
171 "Option.Some" | "Some" => {
172 require_arity(path, fields, 1)?;
173 Ok(Value::Some(Box::new(fields[0].clone())))
174 }
175 "Option.None" | "None" => {
176 require_arity(path, fields, 0)?;
177 Ok(Value::None)
178 }
179 _ => {
180 let mut parts = path.rsplitn(2, '.');
181 let variant = parts.next().ok_or("empty constructor path")?.to_string();
182 let type_name = parts
183 .next()
184 .ok_or_else(|| {
185 format!(
186 "constructor '{}' needs a type prefix (e.g. 'Shape.Circle')",
187 path
188 )
189 })?
190 .to_string();
191 Ok(Value::Variant {
192 type_name,
193 variant,
194 fields: Arc::<[Value]>::from(fields.to_vec()),
195 })
196 }
197 }
198}
199
200fn require_arity(path: &str, fields: &[Value], expected: usize) -> Result<(), String> {
201 if fields.len() != expected {
202 return Err(format!(
203 "constructor '{}' expects {} argument{}, got {}",
204 path,
205 expected,
206 if expected == 1 { "" } else { "s" },
207 fields.len()
208 ));
209 }
210 Ok(())
211}
212
213pub fn encode_entry_args(args: &[Value]) -> Result<JsonValue, String> {
220 match args.len() {
221 0 => Ok(JsonValue::Null),
222 1 => value_to_json(&args[0]),
223 _ => {
224 let jsons: Result<Vec<_>, _> = args.iter().map(value_to_json).collect();
225 jsons.map(JsonValue::Array)
226 }
227 }
228}
229
230pub fn recording_stem(fn_name: &str, args: &[Value]) -> String {
234 fn value_slug(v: &Value) -> Option<String> {
235 match v {
236 Value::Str(s) if is_slug_safe(s) && s.len() <= 32 => Some(s.clone()),
237 Value::Int(i) => Some(i.to_string()),
238 Value::Float(f) if f.is_finite() => Some(format!("{}", f).replace('.', "_")),
239 Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
240 _ => None,
241 }
242 }
243 fn is_slug_safe(s: &str) -> bool {
244 !s.is_empty()
245 && s.chars()
246 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
247 }
248
249 let slugs: Option<Vec<String>> = args.iter().map(value_slug).collect();
250 match slugs {
251 Some(parts) if !parts.is_empty() => format!("{}-{}", fn_name, parts.join("-")),
252 Some(_) => fn_name.to_string(),
253 None => {
254 use std::collections::hash_map::DefaultHasher;
255 use std::hash::{Hash, Hasher};
256 let mut hasher = DefaultHasher::new();
257 fn_name.hash(&mut hasher);
258 for v in args {
259 format!("{:?}", v).hash(&mut hasher);
260 }
261 let h = hasher.finish();
262 format!("{}-{:08x}", fn_name, (h & 0xffff_ffff) as u32)
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn parse(src: &str) -> (String, Vec<Value>) {
272 parse_entry_call(src).expect("should parse")
273 }
274
275 fn parse_err(src: &str) -> String {
276 parse_entry_call(src)
277 .expect_err("should reject")
278 .to_string()
279 }
280
281 #[test]
282 fn literal_args() {
283 let (name, args) = parse(r#"greet("Alice", 42, 3.14, true)"#);
284 assert_eq!(name, "greet");
285 assert_eq!(args.len(), 4);
286 assert!(matches!(args[0], Value::Str(ref s) if s == "Alice"));
287 assert!(matches!(args[1], Value::Int(42)));
288 let expected = 314.0 / 100.0;
289 assert!(matches!(args[2], Value::Float(f) if (f - expected).abs() < 1e-9));
290 assert!(matches!(args[3], Value::Bool(true)));
291 }
292
293 #[test]
294 fn negative_numeric_literals() {
295 let (_, args) = parse("loadTempBounds(-300.0, -40)");
296 assert!(matches!(args[0], Value::Float(f) if (f + 300.0).abs() < 1e-9));
297 assert!(matches!(args[1], Value::Int(-40)));
298 }
299
300 #[test]
301 fn negative_on_non_literal_is_rejected() {
302 let msg = parse_err("foo(-Shape.Circle(1.0))");
303 assert!(msg.contains("numeric literal"), "got: {}", msg);
304 }
305
306 #[test]
307 fn user_variant_single_and_multi_field() {
308 let (_, args) = parse("area(Shape.Circle(1.0))");
309 let Value::Variant {
310 type_name,
311 variant,
312 fields,
313 } = &args[0]
314 else {
315 panic!("expected Variant, got {:?}", args[0]);
316 };
317 assert_eq!(type_name, "Shape");
318 assert_eq!(variant, "Circle");
319 assert_eq!(fields.len(), 1);
320 assert!(matches!(fields[0], Value::Float(f) if (f - 1.0).abs() < 1e-9));
321
322 let (_, args) = parse("area(Shape.Rectangle(3.0, 4.0))");
323 let Value::Variant { fields, .. } = &args[0] else {
324 panic!("expected Variant");
325 };
326 assert_eq!(fields.len(), 2);
327 }
328
329 #[test]
330 fn builtin_wrapper_constructors() {
331 let (_, args) = parse(r#"handle(Result.Ok(5))"#);
332 assert!(matches!(&args[0], Value::Ok(inner) if matches!(**inner, Value::Int(5))));
333
334 let (_, args) = parse(r#"handle(Result.Err("bad"))"#);
335 assert!(
336 matches!(&args[0], Value::Err(inner) if matches!(**inner, Value::Str(ref s) if s == "bad"))
337 );
338
339 let (_, args) = parse("handle(Option.Some(1))");
340 assert!(matches!(&args[0], Value::Some(inner) if matches!(**inner, Value::Int(1))));
341
342 let (_, args) = parse("handle(Option.None)");
343 assert!(matches!(&args[0], Value::None));
344 }
345
346 #[test]
347 fn nested_constructors() {
348 let (_, args) = parse("handle(Result.Ok(Shape.Circle(2.0)))");
349 let Value::Ok(inner) = &args[0] else {
350 panic!("expected Ok");
351 };
352 let Value::Variant {
353 type_name, variant, ..
354 } = &**inner
355 else {
356 panic!("expected inner Variant");
357 };
358 assert_eq!(type_name, "Shape");
359 assert_eq!(variant, "Circle");
360 }
361
362 #[test]
363 fn list_and_tuple_args() {
364 let (_, args) = parse("sumAll([1, 2, 3])");
365 assert!(matches!(args[0], Value::List(_)));
366
367 let (_, args) = parse(r#"describe((1, "x"))"#);
368 assert!(matches!(args[0], Value::Tuple(ref items) if items.len() == 2));
369 }
370
371 #[test]
372 fn arity_mismatch_on_builtin_wrapper() {
373 let msg = parse_err("handle(Result.Ok(1, 2))");
374 assert!(msg.contains("Result.Ok"), "got: {}", msg);
375 }
376
377 #[test]
378 fn zero_arg_call_is_accepted() {
379 let (name, args) = parse("tick()");
380 assert_eq!(name, "tick");
381 assert!(args.is_empty());
382 }
383
384 #[test]
385 fn top_level_must_be_a_call() {
386 let msg = parse_err("42");
387 assert!(msg.contains("function call"), "got: {}", msg);
388 }
389
390 #[test]
391 fn arithmetic_arg_rejected() {
392 let msg = parse_err("foo(1 + 2)");
393 assert!(msg.contains("arg #1"), "got: {}", msg);
394 }
395
396 #[test]
397 fn function_call_arg_rejected() {
398 let msg = parse_err("foo(helper(5))");
400 assert!(msg.contains("arg #1"), "got: {}", msg);
401 }
402
403 #[test]
404 fn variable_arg_rejected() {
405 let msg = parse_err("foo(x)");
406 assert!(msg.contains("arg #1"), "got: {}", msg);
407 }
408
409 #[test]
410 fn qualified_target_rejected() {
411 let msg = parse_err("Math.abs(-5)");
412 assert!(msg.contains("bare function name"), "got: {}", msg);
413 }
414
415 #[test]
416 fn encode_entry_args_shape() {
417 use crate::replay::JsonValue;
418
419 match encode_entry_args(&[]).unwrap() {
420 JsonValue::Null => {}
421 other => panic!("expected Null for empty, got {:?}", other),
422 }
423
424 let single = encode_entry_args(&[Value::Int(5)]).unwrap();
425 assert!(matches!(single, JsonValue::Int(5)), "got: {:?}", single);
426
427 let multi = encode_entry_args(&[Value::Int(1), Value::Str("x".into())]).unwrap();
428 assert!(
429 matches!(&multi, JsonValue::Array(v) if v.len() == 2),
430 "got: {:?}",
431 multi
432 );
433 }
434
435 #[test]
436 fn recording_stem_literal_args() {
437 assert_eq!(
438 recording_stem("loadPort", &[Value::Str("PL".into())]),
439 "loadPort-PL"
440 );
441 assert_eq!(recording_stem("fib", &[Value::Int(10)]), "fib-10");
442 assert_eq!(recording_stem("flag", &[Value::Bool(false)]), "flag-false");
443 }
444
445 #[test]
446 fn recording_stem_complex_args_fall_back_to_hash() {
447 let stem = recording_stem(
448 "area",
449 &[Value::Variant {
450 type_name: "Shape".into(),
451 variant: "Circle".into(),
452 fields: Arc::<[Value]>::from(vec![Value::Float(1.0)]),
453 }],
454 );
455 assert!(stem.starts_with("area-"), "got: {}", stem);
456 assert_eq!(stem.len(), "area-".len() + 8, "expected 8-hex suffix");
457 }
458}