diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b01461167..03be49f54 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -397,6 +397,14 @@ pub enum Expr { fn_name: String, captures: Vec, }, + + /// Gleam-style `todo "reason"` — satisfies any return type; panics at runtime + /// with the given reason message. Use when a branch is not yet implemented. + Todo(Box), + + /// Gleam-style `panic "reason"` — satisfies any return type; panics at runtime + /// with the given reason message. Use to mark branches that should never execute. + Panic(Box), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -721,6 +729,7 @@ fn resolve_aliases_expr(expr: &mut Expr) { resolve_aliases_expr(cap); } } + Expr::Todo(inner) | Expr::Panic(inner) => resolve_aliases_expr(inner), Expr::Literal(_) | Expr::Field { .. } | Expr::Index { .. } => {} } } @@ -936,6 +945,7 @@ fn desugar_expr(expr: &mut Expr, scope: &[String], rf: &std::collections::HashSe desugar_expr(c, scope, rf); } } + Expr::Todo(inner) | Expr::Panic(inner) => desugar_expr(inner, scope, rf), Expr::Literal(_) | Expr::Ref(_) => {} } diff --git a/src/codegen/fmt.rs b/src/codegen/fmt.rs index 91d4f70c1..2357a36f8 100644 --- a/src/codegen/fmt.rs +++ b/src/codegen/fmt.rs @@ -647,6 +647,8 @@ fn fmt_expr(expr: &Expr, mode: FmtMode) -> String { let caps: Vec = captures.iter().map(|c| fmt_expr(c, mode)).collect(); format!("{}[{}]", fn_name, caps.join(" ")) } + Expr::Todo(reason) => format!("todo {}", fmt_expr(reason, mode)), + Expr::Panic(reason) => format!("panic {}", fmt_expr(reason, mode)), } } diff --git a/src/codegen/python.rs b/src/codegen/python.rs index 39888e5e8..29d0148db 100644 --- a/src/codegen/python.rs +++ b/src/codegen/python.rs @@ -1044,6 +1044,14 @@ fn emit_expr(out: &mut String, level: usize, expr: &Expr) -> String { }; format!("(lambda *_a: {}(*_a{}))", py_name(fn_name), cap_str) } + Expr::Todo(reason) => { + let msg = emit_expr(out, level, reason); + format!("(_ := (_ for _ in ()).throw(NotImplementedError({msg})))") + } + Expr::Panic(reason) => { + let msg = emit_expr(out, level, reason); + format!("(_ := (_ for _ in ()).throw(RuntimeError({msg})))") + } } } diff --git a/src/graph.rs b/src/graph.rs index 12415290a..8b58f4dd6 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -153,6 +153,7 @@ fn collect_calls(expr: &Expr, calls: &mut BTreeSet, types: &mut BTreeSet collect_calls(cap, calls, types); } } + Expr::Todo(inner) | Expr::Panic(inner) => collect_calls(inner, calls, types), Expr::Literal(_) | Expr::Ref(_) => {} } } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 0365ddf3c..0e10f92e7 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -8491,6 +8491,7 @@ fn expr_refers_to(name: &str, expr: &Expr) -> bool { || expr_refers_to(name, else_expr) } Expr::MakeClosure { captures, .. } => captures.iter().any(|c| expr_refers_to(name, c)), + Expr::Todo(inner) | Expr::Panic(inner) => expr_refers_to(name, inner), Expr::Literal(_) => false, } } @@ -9216,6 +9217,20 @@ fn eval_expr(env: &mut Env, expr: &Expr) -> Result { captures: cap_vals, }) } + Expr::Todo(reason) => { + let msg = match eval_expr(env, reason)? { + Value::Text(s) => s.to_string(), + v => format!("{v}"), + }; + Err(RuntimeError::new("ILO-R020", format!("todo: {msg}"))) + } + Expr::Panic(reason) => { + let msg = match eval_expr(env, reason)? { + Value::Text(s) => s.to_string(), + v => format!("{v}"), + }; + Err(RuntimeError::new("ILO-R021", format!("panic: {msg}"))) + } } } @@ -15851,4 +15866,36 @@ mod tests { let src = r#"f>b;r=run2!! "true" [];?r.exit{0:true;_:false}"#; assert_eq!(run_str(src, Some("f"), vec![]), Value::Bool(true)); } + + // ---- todo / panic typed expressions (ILO-410) ---- + + #[test] + fn todo_expr_produces_runtime_error() { + let prog = parse_program(r#"f>n;todo "not yet""#); + let result = run(&prog, None, vec![]); + match result { + Err(e) => { + assert_eq!(e.code, "ILO-R020", "expected ILO-R020, got {}", e.code); + assert!(e.message.contains("not yet"), "message was: {}", e.message); + } + Ok(v) => panic!("expected runtime error from todo, got value: {:?}", v), + } + } + + #[test] + fn panic_expr_produces_runtime_error() { + let prog = parse_program(r#"f>n;panic "unreachable""#); + let result = run(&prog, None, vec![]); + match result { + Err(e) => { + assert_eq!(e.code, "ILO-R021", "expected ILO-R021, got {}", e.code); + assert!( + e.message.contains("unreachable"), + "message was: {}", + e.message + ); + } + Ok(v) => panic!("expected runtime error from panic, got value: {:?}", v), + } + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 63a9ecaec..774a02af8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4782,6 +4782,21 @@ results first: `r={first_op}a b;…r` keeps each step explicit." let expr = self.parse_field_chain(expr, None)?; Ok(expr) } + // Gleam-style `todo "reason"` / `panic "reason"` typed expressions. + // Parsed as contextual keywords: the ident "todo" or "panic" followed + // by a mandatory text argument. Satisfy any return type at the verifier + // and abort at runtime with the given message. + Some(Token::Ident(ref name)) if name == "todo" || name == "panic" => { + let is_todo = name == "todo"; + self.advance(); + // The reason argument is required. + let reason = self.parse_expr_inner()?; + if is_todo { + return Ok(Expr::Todo(Box::new(reason))); + } else { + return Ok(Expr::Panic(Box::new(reason))); + } + } Some(Token::Ident(name)) => { self.advance(); // Zero-arg builtins used as operands (arguments to other calls) @@ -5332,6 +5347,9 @@ For variable-position list indexing bind the head first: \ self.collect_free_in_expr(cap, params, local, free); } } + Expr::Todo(inner) | Expr::Panic(inner) => { + self.collect_free_in_expr(inner, params, local, free); + } } } diff --git a/src/verify.rs b/src/verify.rs index d927de375..a04258e80 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -5894,6 +5894,12 @@ ilo has no tuple type." let _ = fn_name; Ty::Unknown } + // `todo "reason"` and `panic "reason"` are divergent expressions: + // they abort at runtime and satisfy any return type. + Expr::Todo(reason) | Expr::Panic(reason) => { + self.infer_expr(func, scope, reason, span); + Ty::Unknown + } } } @@ -10638,4 +10644,36 @@ mod tests { // Sanity: valid call typechecks. assert!(parse_and_verify("f>t;fmt2 3.14 2").is_ok()); } + + // ---- todo / panic typed expressions (ILO-410) ---- + + #[test] + fn verify_todo_satisfies_number_return() { + // `todo` in a number-returning function should not cause a type error. + assert!(parse_and_verify("f>n;todo \"not yet\"").is_ok()); + } + + #[test] + fn verify_panic_satisfies_number_return() { + assert!(parse_and_verify("f>n;panic \"unreachable\"").is_ok()); + } + + #[test] + fn verify_todo_satisfies_text_return() { + assert!(parse_and_verify("f>t;todo \"stub\"").is_ok()); + } + + #[test] + fn verify_panic_satisfies_text_return() { + assert!(parse_and_verify("f>t;panic \"stub\"").is_ok()); + } + + #[test] + fn verify_todo_in_branch() { + // `todo` as one branch of a ternary should not cause type errors. + assert!( + parse_and_verify("f x:n>n;?=x 0(todo \"zero case\")(+x 1)").is_ok(), + "todo in ternary branch should typecheck" + ); + } } diff --git a/src/vm/mod.rs b/src/vm/mod.rs index be49e60f4..6c954f3ca 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -6274,6 +6274,17 @@ impl RegCompiler { } dest } + // `todo "reason"` / `panic "reason"`: compile reason, wrap as + // Err, then OP_PANIC_UNWRAP to abort with the message. The + // returned dest register is never read (execution halts at the + // panic), but we allocate one to satisfy the register contract. + Expr::Todo(reason) | Expr::Panic(reason) => { + let dest = self.alloc_reg(); + let reason_reg = self.compile_expr(reason); + self.emit_abc(OP_WRAPERR, reason_reg, reason_reg, 0); + self.emit_abc(OP_PANIC_UNWRAP, 0, reason_reg, 0); + dest + } } } }