Skip to content

Pattern matching crashes on opaque type variants retrieved from lists #9113

@lukewilliamboswell

Description

@lukewilliamboswell

Summary

Pattern matching on an opaque type crashes at runtime with "reached unreachable code" when:

  1. The opaque type is a platform module
  2. The variant being matched has an opaque type as payload
  3. The element is retrieved from a List via iteration (List.for_each!)
  4. The match body uses hosted effects (like Stdout.line!)

Variants with primitive payloads (like Str) work correctly in the same context.

Error Message

panic: reached unreachable code

Reproduction Steps

cd test/fx
roc list_opaque_pattern_match_bug.roc

Expected Behavior

Both tests should complete successfully and print "Done!".

Actual Behavior

Test 1 (Text variant with Str payload) works. Test 2 (Label variant with opaque NodeB payload) crashes:

Test 1: Text elements in list (should work)
Div branch
  iterating child
Text branch: Hello
  iterating child
Text branch: World

Test 2: Label element with opaque payload in list
panic: reached unreachable code

Minimal Reproduction

The reproduction consists of 4 files added to the fx test platform:

1. platform/NodeA.roc - A simple self-referential opaque type

NodeA := [
    SourceA,
    MappedA({ source : NodeA }),
].{
    source : {} -> NodeA
    source = |{}| SourceA
}

2. platform/NodeB.roc - An opaque type containing another opaque type

import NodeA exposing [NodeA]

NodeB := [
    ConstB(Str),
    FoldB({ initial : Str, event : NodeA }),
].{
    const : Str -> NodeB
    const = |s| ConstB(s)

    fold : Str, NodeA -> NodeB
    fold = |initial, event| FoldB({ initial, event })
}

3. platform/Element.roc - The main opaque type with List and pattern matching

import NodeB exposing [NodeB]
import Stdout

Element := [
    Div(List(Element)),
    Label(NodeB),        # <-- Opaque payload - CRASHES when matched from list
    Text(Str),           # <-- Primitive payload - works fine
].{
    div : List(Element) -> Element
    div = |children| Div(children)

    label : NodeB -> Element
    label = |node| Label(node)

    text : Str -> Element
    text = |s| Text(s)

    process! : Element => {}
    process! = |elem| {
        match elem {
            Div(children) => {
                Stdout.line!("Div branch")
                List.for_each!(children, |child| {
                    Stdout.line!("  iterating child")
                    Element.process!(child)  # Recursive call on list element
                })
            }
            Label(_node) => {
                Stdout.line!("Label branch")  # Never reached - crashes before
            }
            Text(s) => {
                Stdout.line!("Text branch: ${s}")  # Works fine
            }
        }
    }
}

4. platform/main.roc - Updated to expose the new modules

Added to exposes: NodeA, NodeB, Element
Added to imports: import NodeA, import NodeB, import Element

5. list_opaque_pattern_match_bug.roc - Test app

app [main!] { pf: platform "./platform/main.roc" }

import pf.Stdout
import pf.NodeA
import pf.NodeB
import pf.Element

main! = || {
    Stdout.line!("Test 1: Text elements in list (should work)")
    text_elem = Element.div([
        Element.text("Hello"),
        Element.text("World"),
    ])
    Element.process!(text_elem)

    Stdout.line!("")
    Stdout.line!("Test 2: Label element with opaque payload in list")
    event = NodeA.source({})
    signal = NodeB.fold("initial", event)
    label_elem = Element.div([
        Element.label(signal),
    ])
    Element.process!(label_elem)

    Stdout.line!("Done!")
}

Key Observations

Scenario Result
Text(Str) in list, pattern matched ✅ Works
Label(NodeB) in list, pattern matched ❌ Crashes
Label(NodeB) NOT in list, pattern matched ✅ Works
Opaque types as user modules (not platform) ✅ Works
Without hosted effects in match body ✅ Works

Conditions Required to Trigger Bug

All of these must be true:

  1. Platform modules: The opaque types must be defined in the platform/ directory and exposed via main.roc
  2. Nested opaque types: The crashing variant contains an opaque type that itself contains another opaque type (ElementNodeBNodeA)
  3. List iteration: Elements must be retrieved from a List using List.for_each!
  4. Hosted effects: The match body must call hosted effects like Stdout.line!

If any of these conditions is removed, the bug does not occur.

Environment

  • Roc: Built from source
  • Platform: macOS (arm64)
  • Test platform: test/fx

Notes

  • The code type-checks successfully with no errors
  • The crash occurs at runtime during pattern matching
  • The crash message "reached unreachable code" suggests the compiler-generated switch statement is hitting a default case that shouldn't be reachable
  • The tag discriminant may be incorrect when elements are retrieved from lists in this specific configuration

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions