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
157 changes: 152 additions & 5 deletions src/ephapax-interp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -902,11 +902,6 @@ impl Interpreter {
) -> Result<Value, RuntimeError> {
let scrut_val = self.eval(scrutinee)?;
for arm in arms {
if arm.guard.is_some() {
return Err(RuntimeError::Unimplemented(
"match guards not yet implemented in interpreter".into(),
));
}
let mut new_bindings: Vec<(Var, Value)> = Vec::new();
if self.try_match_pattern(&scrut_val, &arm.pattern, &mut new_bindings)? {
let saved: Vec<(Var, Option<Value>, Option<bool>)> = new_bindings
Expand All @@ -922,6 +917,40 @@ impl Interpreter {
for (name, value) in new_bindings {
self.env.extend(name, value);
}

// Evaluate guard (if any) under the arm's pattern
// bindings. If it returns `false`, restore prior
// bindings and try the next arm.
let guard_ok = match &arm.guard {
None => true,
Some(g) => match self.eval(g)? {
Value::Bool(b) => b,
other => {
return Err(RuntimeError::TypeError {
expected: "Bool".into(),
found: other.type_name().into(),
});
}
},
};

if !guard_ok {
for (name, prev_val, prev_consumed) in saved.iter().rev() {
match prev_val {
Some(v) => {
self.env.bindings.insert(name.clone(), v.clone());
if let Some(c) = prev_consumed {
self.env.consumed.insert(name.clone(), *c);
}
}
None => {
self.env.remove(name);
}
}
}
continue;
}

let result = self.eval(&arm.body)?;
for (name, prev_val, prev_consumed) in saved.iter().rev() {
match prev_val {
Expand Down Expand Up @@ -1701,4 +1730,122 @@ mod tests {
// with the typing test module pattern).
let _ = Visibility::Private;
}

// ----- Match guards (#67) -----

/// Guard reads pattern-bound var and evaluates to `true` → arm body
/// runs. `match Some(7) { Some(v) if v > 0 => v | _ => 0 }` = 7.
#[test]
fn test_eval_match_guard_pass() {
let mut interp = Interpreter::new();
interp.load_module(&option_module());
let scrut = dummy_expr(ExprKind::Inr {
ty: Ty::Base(BaseTy::Unit),
value: Box::new(lit_i32_expr(7)),
});
let guard = dummy_expr(ExprKind::BinOp {
op: BinOp::Gt,
left: Box::new(dummy_expr(ExprKind::Var("v".into()))),
right: Box::new(lit_i32_expr(0)),
});
let expr = dummy_expr(ExprKind::Match {
scrutinee: Box::new(scrut),
arms: vec![
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: Some(Box::new(guard)),
body: dummy_expr(ExprKind::Var("v".into())),
},
MatchArm {
pattern: P::Wildcard,
guard: None,
body: lit_i32_expr(0),
},
],
});
let result = interp.eval(&expr).expect("guarded match should evaluate");
assert!(matches!(result, Value::I32(7)), "got {:?}", result);
}

/// Guard evaluates to `false` → arm is skipped, next arm runs.
/// `match Some(-3) { Some(v) if v > 0 => v | _ => 0 }` = 0.
#[test]
fn test_eval_match_guard_fail_falls_through() {
let mut interp = Interpreter::new();
interp.load_module(&option_module());
let scrut = dummy_expr(ExprKind::Inr {
ty: Ty::Base(BaseTy::Unit),
value: Box::new(lit_i32_expr(-3)),
});
let guard = dummy_expr(ExprKind::BinOp {
op: BinOp::Gt,
left: Box::new(dummy_expr(ExprKind::Var("v".into()))),
right: Box::new(lit_i32_expr(0)),
});
let expr = dummy_expr(ExprKind::Match {
scrutinee: Box::new(scrut),
arms: vec![
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: Some(Box::new(guard)),
body: dummy_expr(ExprKind::Var("v".into())),
},
MatchArm {
pattern: P::Wildcard,
guard: None,
body: lit_i32_expr(0),
},
],
});
let result = interp.eval(&expr).expect("guard fall-through should evaluate");
assert!(matches!(result, Value::I32(0)), "got {:?}", result);
}

/// Failed guard must not leak its bindings into subsequent arms.
/// `v` is pre-bound in the enclosing scope; the first arm tries to
/// rebind `v` and its guard fails — the second arm must see the
/// original `v`.
#[test]
fn test_eval_match_failed_guard_does_not_leak_bindings() {
let mut interp = Interpreter::new();
interp.load_module(&option_module());
interp.env.extend("v".into(), Value::I32(999));

let scrut = dummy_expr(ExprKind::Inr {
ty: Ty::Base(BaseTy::Unit),
value: Box::new(lit_i32_expr(-1)),
});
let guard = dummy_expr(ExprKind::BinOp {
op: BinOp::Gt,
left: Box::new(dummy_expr(ExprKind::Var("v".into()))),
right: Box::new(lit_i32_expr(0)),
});
let expr = dummy_expr(ExprKind::Match {
scrutinee: Box::new(scrut),
arms: vec![
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: Some(Box::new(guard)),
body: dummy_expr(ExprKind::Var("v".into())),
},
MatchArm {
pattern: P::Wildcard,
guard: None,
// Second arm reads the OUTER `v` (still 999).
body: dummy_expr(ExprKind::Var("v".into())),
},
],
});
let result = interp.eval(&expr).expect("fall-through should evaluate");
assert!(matches!(result, Value::I32(999)), "got {:?}", result);
}
}
163 changes: 161 additions & 2 deletions src/ephapax-typing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1473,8 +1473,9 @@ impl TypeChecker {

let bound = self.check_pattern(s, &arm.pattern, &scrutinee_ty)?;

if arm.guard.is_some() {
return Err(self.at(s, TypeError::NotYetSupportedInCore("match guards")));
if let Some(guard) = &arm.guard {
let guard_ty = self.check(guard)?;
self.unify(guard.span, &Ty::Base(BaseTy::Bool), &guard_ty)?;
}

let body_ty = self.check(&arm.body)?;
Expand Down Expand Up @@ -1651,8 +1652,12 @@ impl TypeChecker {
arms: &[MatchArm],
_scrutinee_ty: &Ty,
) -> Result<(), SpannedTypeError> {
// Guarded arms can refute at runtime — they don't contribute
// to coverage. Standard treatment: build the exhaustiveness
// matrix from arms whose `guard.is_none()`.
let matrix: Vec<Vec<Pattern>> = arms
.iter()
.filter(|a| a.guard.is_none())
.map(|a| vec![a.pattern.clone()])
.collect();
if let Some(witness) = is_useful(&matrix, 1, &self.data_registry) {
Expand Down Expand Up @@ -4331,4 +4336,158 @@ mod tests {
other => panic!("expected NonExhaustiveMatch, got {other:?}"),
}
}

// ----- Match guards (#67) -----

/// `match x { Some(v) if v > 0 => 1 | Some(v) => 2 | None => 0 }`
/// — guard uses bound pattern var; non-guarded `Some(v)` covers
/// the remaining case, so exhaustiveness holds.
#[test]
fn test_match_guard_typechecks_with_pattern_binding() {
let scrut = dummy_expr(ExprKind::Inr {
ty: Ty::Base(BaseTy::Unit),
value: Box::new(lit_i32(5)),
});
let arms = vec![
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: Some(Box::new(dummy_expr(ExprKind::BinOp {
op: BinOp::Gt,
left: Box::new(dummy_expr(ExprKind::Var("v".into()))),
right: Box::new(lit_i32(0)),
}))),
body: lit_i32(1),
},
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: None,
body: dummy_expr(ExprKind::Var("v".into())),
},
MatchArm {
pattern: P::Constructor {
ctor: "None".into(),
args: vec![],
},
guard: None,
body: lit_i32(0),
},
];
let body = dummy_expr(ExprKind::Match {
scrutinee: Box::new(scrut),
arms,
});
let module = Module {
name: "t".into(),
imports: vec![],
decls: vec![
option_data_decl(),
fn_decl("f", vec![], Ty::Base(BaseTy::I32), body),
],
};
type_check_module(&module).expect("guard binding to v: I32 should typecheck");
}

/// A guard whose expression is not `Bool` is rejected with a
/// type mismatch.
#[test]
fn test_match_guard_non_bool_rejected() {
let scrut = dummy_expr(ExprKind::Inr {
ty: Ty::Base(BaseTy::Unit),
value: Box::new(lit_i32(5)),
});
let arms = vec![
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: Some(Box::new(lit_i32(42))), // I32, not Bool
body: lit_i32(1),
},
MatchArm {
pattern: P::Wildcard,
guard: None,
body: lit_i32(0),
},
];
let body = dummy_expr(ExprKind::Match {
scrutinee: Box::new(scrut),
arms,
});
let module = Module {
name: "t".into(),
imports: vec![],
decls: vec![
option_data_decl(),
fn_decl("f", vec![], Ty::Base(BaseTy::I32), body),
],
};
let err = type_check_module(&module).unwrap_err();
assert!(
matches!(err.error, TypeError::TypeMismatch { .. }),
"expected TypeMismatch for non-Bool guard, got {:?}",
err.error
);
}

/// A guarded arm cannot make the match exhaustive on its own —
/// its guard could refute at runtime. `Some(v) if v > 0` followed
/// by `None` is missing the `Some` non-positive case.
#[test]
fn test_match_guarded_arm_not_counted_for_exhaustiveness() {
let scrut = dummy_expr(ExprKind::Inr {
ty: Ty::Base(BaseTy::Unit),
value: Box::new(lit_i32(5)),
});
let arms = vec![
MatchArm {
pattern: P::Constructor {
ctor: "Some".into(),
args: vec![P::Var("v".into())],
},
guard: Some(Box::new(dummy_expr(ExprKind::BinOp {
op: BinOp::Gt,
left: Box::new(dummy_expr(ExprKind::Var("v".into()))),
right: Box::new(lit_i32(0)),
}))),
body: lit_i32(1),
},
MatchArm {
pattern: P::Constructor {
ctor: "None".into(),
args: vec![],
},
guard: None,
body: lit_i32(0),
},
];
let body = dummy_expr(ExprKind::Match {
scrutinee: Box::new(scrut),
arms,
});
let module = Module {
name: "t".into(),
imports: vec![],
decls: vec![
option_data_decl(),
fn_decl("f", vec![], Ty::Base(BaseTy::I32), body),
],
};
let err = type_check_module(&module).unwrap_err();
match err.error {
TypeError::NonExhaustiveMatch { missing } => {
assert!(
missing.as_str().contains("Some"),
"expected witness mentioning Some, got {missing}"
);
}
other => panic!("expected NonExhaustiveMatch, got {other:?}"),
}
}
}
Loading