Skip to content

[Detail Bug] Type checks: is operator incorrectly constant-folds subtype and union checks to false #1048

@detail-app

Description

@detail-app

Summary

  • Context: The Is type operator in src/modules/expression/typeop/is.rs implements the expr is Type functionality in Amber, which is used for type checking and narrowing.
  • Bug: The Is operator 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 constant 0.
  • 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 return 1 (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 is checks to fail incorrectly, leading to unreachable code branches and broken type narrowing. For example, Int is Num will 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

  1. Replace expr_type == self.kind with expr_type.is_subseteq_of(&self.kind) in both analyze_control_flow and translate.
  2. In translate, when is_subseteq_of is false but can_intersect is true, generate a runtime check instead of returning a constant 0.
  3. Correct syntax_name!("Add") to syntax_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdetail

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions