-
Notifications
You must be signed in to change notification settings - Fork 113
Description
Summary
- Context: The
Istype operator insrc/modules/expression/typeop/is.rsimplements theexpr is Typefunctionality in Amber, which is used for type checking and narrowing. - Bug: The
Isoperator incorrectly uses strict equality to check types at both compile-time and during translation to Bash, instead of checking for subtyping relationships. Furthermore, it fails to generate runtime type checks for Union types, instead returning a constant0. - Actual vs. expected: It returns
0(false) for cases where an expression's type is a subtype of the target type (e.g.,Int is Num) or when the expression has a Union type that could contain the target type at runtime. It should return1(true) if the subtype relationship is known at compile-time, and generate a runtime check if it's not statically determined. - Impact: This bug causes
ischecks to fail incorrectly, leading to unreachable code branches and broken type narrowing. For example,Int is Numwill always evaluate to false, and runtime type checks on Union types will always fail unless the types are identical.
Code with bug
impl Is {
pub fn analyze_control_flow(&self) -> Option<bool> {
let expr_type = self.expr.get_type();
// If types are identical, it's always true
if expr_type == self.kind { // <-- BUG 🔴 [Should use is_subseteq_of]
return Some(true);
}
// ...
impl TranslateModule for Is {
fn translate(&self, _meta: &mut TranslateMetadata) -> FragmentKind {
if self.expr.get_type() == self.kind { // <-- BUG 🔴 [Should use is_subseteq_of and handle runtime checks for Unions]
fragments!("1")
} else {
fragments!("0")
}
}
}
// ...
impl SyntaxModule<ParserMetadata> for Is {
syntax_name!("Add"); // <-- BUG 🔴 [Should be "Is"]Evidence
1. Subtyping Failure (Int is Num)
Created an Amber script test_bug_5.ab:
fun check(x: Int) {
if x is Num {
echo("Int is Num")
} else {
echo("Int is not Num")
}
}
check(1)
Compiled Bash output:
check__0_v0() {
local x_1="${1}"
if [ 0 != 0 ]; then # <-- BUG: Statically evaluated to false
echo "Int is Num"
else
echo "Int is not Num"
fi
}Even though Int is a subtype of Num, the compiler incorrectly constant-folded the check to 0 because Type::Int != Type::Num.
2. Runtime Union Failure
Created an Amber script test_bug_3.ab:
fun check(x: Text | Int) {
if x is Int {
echo("Int")
} else {
echo("Text")
}
}
fun get_val(i: Int): Text | Int {
if i == 0 { return 1 } else { return "foo" }
}
check(get_val(0))
check(get_val(1))
Compiled Bash output:
check__0_v0() {
local x_3="${1}"
if [ 0 != 0 ]; then # <-- BUG: Always false, even when x is an Int at runtime
echo "Int"
else
echo "Text"
fi
}The compiler produced Text for both calls, failing to recognize that x could be an Int at runtime.
Why has this bug gone undetected?
Existing tests often rely on function specialization where the concrete type of a variable becomes known at compile-time (e.g., x: Text | Int called with 42 becomes specialized to x: Int). In those cases, expr_type == kind happens to be Int == Int, which is true. However, when specialization is not possible (e.g., values from functions returning Unions) or when subtyping is involved (e.g., Int is Num), the bug manifests.
Recommended fix
- Replace
expr_type == self.kindwithexpr_type.is_subseteq_of(&self.kind)in bothanalyze_control_flowandtranslate. - In
translate, whenis_subseteq_ofis false butcan_intersectis true, generate a runtime check instead of returning a constant0. - Correct
syntax_name!("Add")tosyntax_name!("Is").
History
This bug was introduced in commit 09af993 (@Ph0enixKM, 2025-12-19, PR #940). While the is operator had used strict equality since its initial implementation, this commit introduced control flow analysis and fact extraction which relied on the same flawed logic, causing incorrect constant-folding of subtype checks and failing to account for the recently introduced Union and Int types.