Skip to content

Type checker: == rejected between compatible but unequal types (records, maps, tuples) #7841

Closed

Description

@bentsherman

The == operator is rejected between a record type and an anonymous record with the same fields, even though the anonymous record is assignable to the record type (records are structurally typed).

MRE

nextflow.enable.types = true

record Sample {
    id: String
}

def f(s: Sample) -> Boolean {
    return s == record(id: 'a')
}

Actual

$ NXF_SYNTAX_PARSER=v2 nextflow lint main.nf
Error main.nf:8:12: The `==` operator is not defined for operands with types Sample and Record {
    id: String
}
│   8 |     return s == record(id: 'a')
╰     |            ^^^^^^^^^^^^^^^^^^^^

s == t (both Sample) and record(id: 'a') == record(id: 'b') are accepted.

Same issue with maps and tuples

nextflow.enable.types = true

workflow {
    println([a: 1] == [a: 1.0])
    println(tuple(1, 2) == tuple(1.0, 2))
    println(1 == 1.0)
    println([1] == [1.0])
}
$ NXF_SYNTAX_PARSER=v2 nextflow lint main.nf
Error main.nf:4:13: The `==` operator is not defined for operands with types Map<String, Integer> and Map<String, Float>
│   4 |     println([a: 1] == [a: 1.0])
╰     |             ^^^^^^^^^^^^^^^^^^

Error main.nf:5:13: The `==` operator is not defined for operands with types Tuple<Integer, Integer> and Tuple<Float, Integer>
│   5 |     println(tuple(1, 2) == tuple(1.0, 2))
╰     |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

nextflow run prints true for all four. 1 == 1.0 and [1] == [1.0] (List<Integer> vs List<Float>) are accepted, so the checker is inconsistent.

Expected

No error. == should be allowed when either operand type is assignable to the other, for records, maps and tuples alike.

Version

Nextflow 26.10.0 and 26.10.3 (also present on current master)

Root cause

TypeCheckingVisitor.visitBinaryExpression, case COMPARE_EQUAL / COMPARE_NOT_EQUAL: it accepts only Types.isEqual(lhsType, rhsType) or a compareTo op. Types.isEqual compares the classes nominally, so a named record and a structurally compatible anonymous record are not equal. Checking Types.isAssignableFrom in either direction (which handles records structurally via isAssignableFromRecord) would fix it.

[1] == [1.0] passes only because the List shim declares compareTo(List<E>, List<E>) in ListOps, so the compareTo fallback resolves. Map has no compareTo op and Tuple has no shim, so they hit the Types.isEqual check, which compares generic arguments exactly.

Activity

  1. changed the title [-]Type checker: `==` rejected between a record type and a compatible anonymous record[/-] [+]Type checker: `==` rejected between compatible but unequal types (records, maps, tuples)[/+] on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions