Ceylon compiler rejects String-only operations in else branch after `is String` check
0 reputation · 01 Apr 2023, 23:44 UTC
0 reputation · 01 Apr 2023, 23:44 UTC
I have a Ceylon function whose parameter has the union type String|Integer. My understanding of the language specification is that flow-sensitive typing should narrow the value inside an if (is String val) branch to String, and in the else branch to the complement type (here, Integer). I am trying to rely on that complement narrowing instead of writing an explicit cast or a second is check.
The uncertainty is about exactly how far this flow-based narrowing extends: whether it survives re-assignment of the value, whether it applies when the union has more than two members (for example String|Integer|Float, where the else branch should narrow to Integer|Float), and whether behavior differs between the 1.x compiler releases, since I have seen hints that inference and narrowing details changed across minor versions.
Given the project is no longer actively maintained, I also want to confirm whether any unexpected narrowing behavior I observe should be treated as a permanent compiler characteristic rather than something fixable upstream.
else branch for multi-member unions, and does it hold after the variable is reassigned?is/exists/nonempty narrowing is applied?32775 reputation · 02 Apr 2023, 02:30 UTC
If val is declared String|Integer, then if (is String val) narrows val to String in the then-branch and to the complement of String in the else-branch — here Integer. Calling val.uppercased or any other String-only member in that else block is a type error by design, not a compiler defect. The compiler is applying the flow-sensitive typing rule, not misapplying it.
If your intent was to run String operations in the else block, the branches are inverted: the String case is the then-branch. If your intent was to handle “not a String” with String operations, that is precisely the case the type system forbids.
For String|Integer|Float, the else branch narrows to Integer|Float. Operations common to both members stay available; String-only members do not. This is the same rule applied to a larger complement, and it is what I would expect from the specification for disjoint union members. If your compiler does something else, treat that as a version-specific limitation rather than the language rule.
Narrowing attaches to the flow of a particular value, not to the name. Two consequences matter:
value declaration keeps its narrowed type for the rest of the block, because it cannot be reassigned.variable loses the narrowed type after any assignment that could change its type. Reassigning inside the else block widens val back to the declared union for subsequent statements, so later String-only or Integer-only calls can fail even though the branch check looked correct.Do not rely on narrowing surviving an assignment. If you need the narrowed type afterwards, re-check with is or copy the narrowed value into a new value.
I cannot confirm from memory which specific narrowing or inference details changed between Ceylon 1.2 and 1.3.x. The 1.3.x line is the final one, and the language specification is the reference; implementations can diverge from it, especially in an unmaintained toolchain. Treat any narrowing behavior you observe that contradicts the specification as a permanent characteristic of your installed compiler version rather than something that will be fixed upstream. This paragraph should be verified against your own compiler and the specification for your version.
ceylon --version.void f(String|Integer val) {
if (is String val) {
print(val.uppercased);
} else {
print(val + 1);
}
}String|Integer|Float. The else branch should accept Integer/Float operations and reject String-only ones.assert (is Integer val); to confirm the narrowed type directly, and read the error if it fails.variable, reassign it before and inside the branch, and observe whether narrowing is lost.Check the declared type exactly. If it is String|Integer? (or otherwise includes Null), the else branch is Integer|Null, and arithmetic on it still fails until you add exists. If the declared type is Object or an interface rather than the union you assume, the complement is correspondingly wider. The exact declared type and the exact error message are the two things worth checking before concluding anything about the compiler.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.