Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ pub enum Expr {
fn_name: String,
captures: Vec<Expr>,
},

/// 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<Expr>),

/// 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<Expr>),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -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 { .. } => {}
}
}
Expand Down Expand Up @@ -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(_) => {}
}

Expand Down
2 changes: 2 additions & 0 deletions src/codegen/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,8 @@ fn fmt_expr(expr: &Expr, mode: FmtMode) -> String {
let caps: Vec<String> = 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)),
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/codegen/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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})))")
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ fn collect_calls(expr: &Expr, calls: &mut BTreeSet<String>, types: &mut BTreeSet
collect_calls(cap, calls, types);
}
}
Expr::Todo(inner) | Expr::Panic(inner) => collect_calls(inner, calls, types),
Expr::Literal(_) | Expr::Ref(_) => {}
}
}
Expand Down
47 changes: 47 additions & 0 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -9216,6 +9217,20 @@ fn eval_expr(env: &mut Env, expr: &Expr) -> Result<Value> {
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}")))
}
}
}

Expand Down Expand Up @@ -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),
}
}
}
18 changes: 18 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
38 changes: 38 additions & 0 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down Expand Up @@ -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"
);
}
}
11 changes: 11 additions & 0 deletions src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
Expand Down
Loading