From 0a1bb70a6c6f2dc5fed5b507242eb53da725d624 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 12 Jun 2026 17:20:18 -0700 Subject: [PATCH 1/3] Panic for else on exhaustive sealed when Signed-off-by: Tom --- .../lang/temper/frontend/TypeStageTest.kt | 68 +++++++++++++++ .../kotlin/lang/temper/interp/IfTransform.kt | 82 ++++++++++++++----- 2 files changed, 129 insertions(+), 21 deletions(-) diff --git a/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt b/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt index ced16d90..6d69eb08 100644 --- a/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt +++ b/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt @@ -2394,6 +2394,74 @@ class TypeStageTest { |} """.trimMargin().stripDoubleHashCommentLinesToPutCommentsInlineBelow(), ) + + @Test + fun sealedWhen() = assertModuleAtStage( + stage = Stage.Type, + pseudoCodeDetail = PseudoCodeDetail.default.copy(showInferredTypes = true), + input = """ + |export sealed interface Shape {} + |export class Circle() extends Shape {} + |export class Square() extends Shape {} + |export let describe(s: Shape): String { + | when (s) { + | is Circle -> "circle"; + | is Square -> "square"; + | } + |} + """.trimMargin(), + want = """ + |{ + | type: { + | body: + | ``` + | @typeDecl(Shape) @stay @sealedType let `test//`.Shape ⦂ Type; + | `test//`.Shape = type (Shape); + | @typeDecl(Circle) @stay let `test//`.Circle ⦂ Type; + | `test//`.Circle = type (Circle); + | @typeDecl(Square) @stay let `test//`.Square ⦂ Type; + | `test//`.Square = type (Square); + | @fn let `test//`.describe ⦂(fn (Shape): String), @typePlaceholder(Shape) typePlaceholder#0: Empty; + | typePlaceholder#0 = {class: Empty__0}; + | @fn @visibility(\public) @stay @fromType(Circle) let constructor__0 ⦂(fn (Circle): Void); + | constructor__0 = (@stay fn constructor(@impliedThis(Circle) this__0: Circle) /* return__0 */: Void { + | return__0 = void + | }); + | @fn @visibility(\public) @stay @fromType(Square) let constructor__1 ⦂(fn (Square): Void); + | constructor__1 = (@stay fn constructor(@impliedThis(Square) this__1: Square) /* return__1 */: Void { + | return__1 = void + | }); + | `test//`.describe = (@stay fn describe(s__0 /* aka s */: Shape) /* return__2 */: String { + | var t#0 ⦂ Boolean, t#1 ⦂ Boolean; + | void; + | fn__0: do { + | if (!false) { + | t#0 = is(s__0, Circle) + | } else { + | t#0 = false + | }; + | if (t#0) { + | return__2 = "circle" + | } else { + | if (!false) { + | t#1 = is(s__0, Square) + | } else { + | t#1 = false + | }; + | if (t#1) { + | return__2 = "square" + | } else { + | return__2 = panic ⋖ String ⋗() + | } + | }; + | } + | }) + | + | ``` + | } + |} + """.trimMargin().stripDoubleHashCommentLinesToPutCommentsInlineBelow(), + ) } private object ImpureIgnoreFn : NamedBuiltinFun, CallableValue { diff --git a/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt b/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt index 71354ded..8de92c80 100644 --- a/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt +++ b/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt @@ -1,21 +1,29 @@ package lang.temper.interp +import lang.temper.builtin.BuiltinFuns import lang.temper.env.InterpMode import lang.temper.log.spanningPosition +import lang.temper.name.TemperName +import lang.temper.type.TypeDefinition +import lang.temper.type.TypeShape import lang.temper.type2.Signature2 import lang.temper.value.ActualValues +import lang.temper.value.CallTree import lang.temper.value.ControlFlow import lang.temper.value.Fail import lang.temper.value.MacroEnvironment import lang.temper.value.NamedBuiltinFun import lang.temper.value.NotYet import lang.temper.value.PartialResult +import lang.temper.value.ReifiedType import lang.temper.value.Result import lang.temper.value.SpecialFunction import lang.temper.value.TEdge import lang.temper.value.Value import lang.temper.value.elseIfSymbol import lang.temper.value.elseSymbol +import lang.temper.value.nameContained +import lang.temper.value.valueContained import lang.temper.value.void /** @@ -188,11 +196,21 @@ internal object IfTransform : ControlFlowTransform("if") { if (hasFinalElse) { null } else { - // Having a `void` here makes sure that static checks like UseBeforeInit - // get a diagnostic position for error logging. - ControlFlow.Stmt( - macroCursor.referenceToVoid(macroCursor.macroEnvironment.pos.rightEdge), - ) + when { + // If someone has checked all subtypes for some supertype, panic instead of void. + anySealedTypesExhaustive(branches) -> { + val pos = macroCursor.macroEnvironment.pos + val panicCall = macroCursor.macroEnvironment.document.treeFarm.grow(pos) { + Block { + Call { V(BuiltinFuns.vPanic) } + } + }.edge(0) + macroCursor.referenceTo(panicCall) + } + // Otherwise, having a `void` here makes sure that static checks like UseBeforeInit + // get a diagnostic position for error logging. + else -> macroCursor.referenceToVoid(macroCursor.macroEnvironment.pos.rightEdge) + }.let { ControlFlow.Stmt(it) } } while (branchIndex != 0) { branchIndex -= 1 @@ -217,25 +235,47 @@ internal object IfTransform : ControlFlowTransform("if") { body } } + controlFlow?.let { ControlFlowSubflow(it) } + } + } + } +} - // `if` chains that don't end in an `else` should be typed as Void. - // See LoopTransform comments on typing as for the problems with control-flow - // constructs that start with a condition and which don't reliably follow it - // with a typeable tree. - if (controlFlow != null && !hasFinalElse) { - controlFlow = ControlFlow.StmtBlock( - controlFlow.pos, - listOf( - controlFlow, - ControlFlow.Stmt( - macroCursor.referenceToVoid(controlFlow.pos.rightEdge), - ), - ), - ) +/** + * Check for simple sealed exhaustiveness. This requires that a single name be + * checked against each sealed subtype of some sealed supertype. If so, default + * to panic instead of void, which allows for type inference without an else. + * + * Rely on checks elsewhere for valid downcasts. And if the above holds and we + * could downcast, the check would be exhaustive in any case. + * + * TODO Fancy flowtyping check for exhaustiveness. + */ +private fun anySealedTypesExhaustive(branches: MutableList>): Boolean { + val allFoundSealedSubs = mutableMapOf>() + var checkedName: TemperName? = null + branches@ for (branch in branches) { + val condition = branch.first?.target as? CallTree ?: continue@branches + condition.childOrNull(0)?.valueContained?.stateVector == BuiltinFuns.isFn || continue@branches + val nextCheckedName = condition.childOrNull(1)?.nameContained ?: continue@branches + when (checkedName) { + null -> checkedName = nextCheckedName + else if checkedName != nextCheckedName -> break@branches + else -> {} + } + val subtype = condition.childOrNull(2)?.valueContained?.stateVector as? ReifiedType ?: continue@branches + val subdef = subtype.type2.definition + supertypes@ for (supertype in subdef.superTypes) { + val supershape = supertype.definition as? TypeShape + val sealedSubs = supershape?.sealedSubTypes ?: continue@supertypes + if (subdef in sealedSubs) { + val foundSealedSubs = allFoundSealedSubs.getOrPut(supershape) { mutableSetOf() } + foundSealedSubs.add(subdef) + if (foundSealedSubs.size == sealedSubs.size) { + return true } - - controlFlow?.let { ControlFlowSubflow(it) } } } } + return false } From 02adbc73e5edc8d30a4fd18d364589a3ad2ded9b Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 13 Jun 2026 04:52:09 -0700 Subject: [PATCH 2/3] Put extra void back in; test void case nearby Signed-off-by: Tom --- .../lang/temper/frontend/TypeStageTest.kt | 64 +++++++++++++++++++ .../kotlin/lang/temper/interp/IfTransform.kt | 22 ++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt b/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt index 6d69eb08..a9da5dd4 100644 --- a/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt +++ b/frontend/src/commonTest/kotlin/lang/temper/frontend/TypeStageTest.kt @@ -2407,6 +2407,7 @@ class TypeStageTest { | when (s) { | is Circle -> "circle"; | is Square -> "square"; + | // defaults to panic here because above is exhaustive | } |} """.trimMargin(), @@ -2462,6 +2463,69 @@ class TypeStageTest { |} """.trimMargin().stripDoubleHashCommentLinesToPutCommentsInlineBelow(), ) + + @Test + fun unsealedWhen() = assertModuleAtStage( + stage = Stage.Type, + pseudoCodeDetail = PseudoCodeDetail.default.copy(showInferredTypes = true), + input = """ + |export /* totally not sealed */ interface Shape {} + |export class Circle() extends Shape {} + |export class Square() extends Shape {} + |export let describe(s: Shape): String { + | when (s) { + | is Circle -> "circle"; + | is Square -> "square"; + | // defaults to void here + | } + |} + """.trimMargin(), + want = """ + |{ + | type: { + | body: + | ``` + | @typeDecl(Shape) @stay let `test//`.Shape ⦂ Type; + | `test//`.Shape = type (Shape); + | @typeDecl(Circle) @stay let `test//`.Circle ⦂ Type; + | `test//`.Circle = type (Circle); + | @typeDecl(Square) @stay let `test//`.Square ⦂ Type; + | `test//`.Square = type (Square); + | @fn let `test//`.describe ⦂(fn (Shape): String), @typePlaceholder(Shape) typePlaceholder#0: Empty; + | typePlaceholder#0 = {class: Empty__0}; + | @fn @visibility(\public) @stay @fromType(Circle) let constructor__0 ⦂(fn (Circle): Void); + | constructor__0 = (@stay fn constructor(@impliedThis(Circle) this__0: Circle) /* return__0 */: Void { + | return__0 = void + | }); + | @fn @visibility(\public) @stay @fromType(Square) let constructor__1 ⦂(fn (Square): Void); + | constructor__1 = (@stay fn constructor(@impliedThis(Square) this__1: Square) /* return__1 */: Void { + | return__1 = void + | }); + | `test//`.describe = (@stay fn describe(s__0 /* aka s */: Shape) /* return__2 */: String { + | var t#0 ⦂ Boolean, t#1 ⦂ Boolean; + | fn__0: do { + | if (!false) { + | t#0 = is(s__0, Circle) + | } else { + | t#0 = false + | }; + | if (!t#0) { + | if (!false) { + | t#1 = is(s__0, Square) + | } else { + | t#1 = false + | }; + | if (t#1) {} + | }; + | return__2 = void + | } + | }) + | + | ``` + | } + |} + """.trimMargin().stripDoubleHashCommentLinesToPutCommentsInlineBelow(), + ) } private object ImpureIgnoreFn : NamedBuiltinFun, CallableValue { diff --git a/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt b/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt index 8de92c80..675684c2 100644 --- a/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt +++ b/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt @@ -191,6 +191,7 @@ internal object IfTransform : ControlFlowTransform("if") { } // Build branching subsystems from `else` backwards to `if`. val hasFinalElse = branches.last().first == null + val exhaustiveAutoFinalElse = !hasFinalElse && anySealedTypesExhaustive(branches) var branchIndex = branches.size var controlFlow: ControlFlow? = if (hasFinalElse) { @@ -198,7 +199,7 @@ internal object IfTransform : ControlFlowTransform("if") { } else { when { // If someone has checked all subtypes for some supertype, panic instead of void. - anySealedTypesExhaustive(branches) -> { + exhaustiveAutoFinalElse -> { val pos = macroCursor.macroEnvironment.pos val panicCall = macroCursor.macroEnvironment.document.treeFarm.grow(pos) { Block { @@ -235,6 +236,25 @@ internal object IfTransform : ControlFlowTransform("if") { body } } + + // Non-exhaustive `if` chains that don't end in `else` should be typed as Void. + // See LoopTransform comments on typing as for the problems with control-flow + // constructs that start with a condition and which don't reliably follow it + // with a typeable tree. + if (controlFlow != null && !hasFinalElse && !exhaustiveAutoFinalElse) { + // TODO Record metadata at the start of the chain to improve error messaging? + // TODO Maybe: "Use final `else` to provide a default value when not checking all sealed subtypes"? + controlFlow = ControlFlow.StmtBlock( + controlFlow.pos, + listOf( + controlFlow, + ControlFlow.Stmt( + macroCursor.referenceToVoid(controlFlow.pos.rightEdge), + ), + ), + ) + } + controlFlow?.let { ControlFlowSubflow(it) } } } From 5dfe27dbc691b4fade17e5ab39d5a4d10e022ab2 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 13 Jun 2026 05:24:02 -0700 Subject: [PATCH 3/3] Improve comment Signed-off-by: Tom --- interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt b/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt index 675684c2..db1b4e1d 100644 --- a/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt +++ b/interp/src/commonMain/kotlin/lang/temper/interp/IfTransform.kt @@ -242,7 +242,7 @@ internal object IfTransform : ControlFlowTransform("if") { // constructs that start with a condition and which don't reliably follow it // with a typeable tree. if (controlFlow != null && !hasFinalElse && !exhaustiveAutoFinalElse) { - // TODO Record metadata at the start of the chain to improve error messaging? + // TODO Record metadata on the added voids for better messages in case of errors later? // TODO Maybe: "Use final `else` to provide a default value when not checking all sealed subtypes"? controlFlow = ControlFlow.StmtBlock( controlFlow.pos,