Skip to content

Conversation

@lukewilliamboswell
Copy link
Collaborator

When checking deferred static dispatch constraints for opaque types from imported modules, the compiler was unable to find the module environment because it only checked if the type was from the current module or the builtin module.

The fix adds support for types from other imported modules by searching the imported_modules list by module name when the type's origin_module doesn't match the current module or builtin module. This allows static dispatch (. syntax) to work correctly on opaque types exposed from platform modules.

Fixes #8928

When checking deferred static dispatch constraints for opaque types from
imported modules, the compiler was unable to find the module environment
because it only checked if the type was from the current module or the
builtin module.

The fix adds support for types from other imported modules by searching
the imported_modules list by module name when the type's origin_module
doesn't match the current module or builtin module. This allows static
dispatch (`.` syntax) to work correctly on opaque types exposed from
platform modules.

Fixes #8928

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@lukewilliamboswell lukewilliamboswell marked this pull request as ready for review January 5, 2026 11:02
@lukewilliamboswell
Copy link
Collaborator Author

Follow-up bug that is related enough that I hope to fix this also

Bug Report: Runtime Crash on Static Dispatch with Effect Methods

Summary

After the fix for compile-time static dispatch crashes, there's now a runtime crash when calling effect methods on opaque types using static dispatch (. syntax). Regular methods work fine, but effect methods cause panic: unreachable, node is not a pattern tag .type_header.

Environment

  • Roc Version: debug-2b756597 (with static dispatch compile-time fix)
  • Platform: macOS arm64
  • Error: thread panic: unreachable, node is not a pattern tag .type_header
  • Related: This occurs AFTER the fix for the compile-time crash in checkDeferredStaticDispatchConstraints

Minimal Reproduction

Note: This bug specifically affects effect methods, so it's easier to demonstrate with the basic-cli platform which has effect methods on opaque types. The test/fx platform doesn't currently have opaque types with effect methods.

Test with basic-cli platform

Location: /Users/luke/Documents/GitHub/basic-cli/test-effect-static.roc

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

import pf.Cmd
import pf.Stdout

main! = |_args| {
    # Build a command using static dispatch - THIS WORKS
    cmd = Cmd.new("echo").args(["Hello"])

    Stdout.line!("About to execute command...")

    # Call effect method using static dispatch - THIS CRASHES
    result = cmd.exec_exit_code!()

    match result {
        Ok(code) => Stdout.line!("Exit code: ${code.to_str()}")
        Err(_) => Stdout.line!("Error")
    }

    Ok({})
}

For reference, here's the Cmd module structure (simplified):

Cmd := {
    args : List(Str),
    program : Str,
}.{
    IOErr := [NotFound, PermissionDenied, ...]

    # Regular methods - WORK with static dispatch
    new : Str -> Cmd
    arg : Cmd, Str -> Cmd
    args : Cmd, List(Str) -> Cmd

    # Effect methods - CRASH with static dispatch
    exec_exit_code! : Cmd => Try(I32, [CmdErr(IOErr)])
}

To Reproduce

cd /path/to/basic-cli
roc build test-effect-static.roc
./test-effect-static

The test file is available at /Users/luke/Documents/GitHub/basic-cli/test-effect-static.roc

Expected Behavior

The program should execute the command and print the exit code, just like it does with qualified function call syntax.

Actual Behavior

Successfully built ./test-effect-static
About to execute command...
thread 1818127 panic: unreachable, node is not a pattern tag .type_header
???:?:?: 0x100c9393f in ??? (test-effect-static)
???:?:?: 0x100c8eb5b in ??? (test-effect-static)
???:?:?: 0x100d07b6b in ??? (test-effect-static)
[... stack trace continues ...]

The program prints "About to execute command..." successfully, then crashes when the effect method is called via static dispatch.

Analysis

What Works ✅

  1. Regular methods with static dispatch:

    cmd = Cmd.new("echo").args(["Hello"])  # Works perfectly
  2. Effect methods with qualified call:

    result = Cmd.exec_exit_code!(cmd)  # Works perfectly
  3. Building commands entirely with static dispatch:

    cmd = Cmd.new("ls").arg("-l").args(["-a", "-h"])  # All works

What Crashes ❌

  1. Effect methods with static dispatch syntax:

    result = cmd.exec_exit_code!()  # Runtime crash
  2. Any effect method called via . syntax on opaque type:

    cmd.exec_output!()  # Crashes
    cmd.exec_cmd!()     # Crashes

Pattern

The crash specifically affects:

  • Effect methods (functions with ! suffix)
  • Called via static dispatch (. syntax)
  • On opaque types from platform modules

Regular methods on the same opaque types work fine with static dispatch.

Workaround

Use static dispatch for builder methods, but use qualified names for effect methods:

# Good: static dispatch for builders
cmd = Cmd.new("echo").args(["Hello"])

# Good: qualified call for effects
result = Cmd.exec_exit_code!(cmd)

# Bad: static dispatch for effects (crashes)
# result = cmd.exec_exit_code!()

@lukewilliamboswell lukewilliamboswell marked this pull request as draft January 5, 2026 23:53
@lukewilliamboswell
Copy link
Collaborator Author

Aaaand another

Bug Report: Effect Methods via Static Dispatch Trigger Interpreter Error

Summary

When calling effect methods on opaque types using static dispatch (. syntax), the compiled binary crashes with "Hosted functions cannot be called in the interpreter". Regular methods work fine with static dispatch, and effect methods work fine with qualified calls.

Environment

  • Roc Version: debug-0f26a4c8
  • Platform: macOS arm64
  • Build: Successfully compiles with roc build
  • Error: Roc crashed: Hosted functions cannot be called in the interpreter

Status

This issue appeared after fixes for:

  1. ✅ Compile-time crash in checkDeferredStaticDispatchConstraints - FIXED
  2. ✅ Runtime panic node is not a pattern tag .type_header - FIXED (or changed to this issue)

Minimal Reproduction

Platform Module Structure

platform/Cmd.roc (simplified):

Cmd := {
    args : List(Str),
    program : Str,
}.{
    IOErr := [NotFound, PermissionDenied, ...]

    # Regular methods - WORK with static dispatch
    new : Str -> Cmd
    new = |program| {
        args: [],
        program,
    }

    args : Cmd, List(Str) -> Cmd
    args = |cmd, new_args| {
        args: List.concat(cmd.args, new_args),
        program: cmd.program,
    }

    # Effect methods - CAUSE INTERPRETER ERROR with static dispatch
    exec_exit_code! : Cmd => Try(I32, [CmdErr(IOErr)])
}

Test File That Crashes

test-effect-static.roc:

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

import pf.Cmd
import pf.Stdout

main! = |_args| {
    # Build a command using static dispatch - THIS WORKS
    cmd = Cmd.new("echo").args(["Hello"])

    Stdout.line!("About to execute command...")

    # Call effect method using static dispatch - THIS TRIGGERS INTERPRETER ERROR
    result = cmd.exec_exit_code!()

    match result {
        Ok(code) => Stdout.line!("Exit code: ${code.to_str()}")
        Err(_) => Stdout.line!("Error")
    }

    Ok({})
}

To Reproduce

cd /path/to/basic-cli

# Rebuild platform
./build.sh

# Build test (succeeds)
roc build test-effect-static.roc

# Run test (crashes with interpreter error)
./test-effect-static

Expected Behavior

The program should execute successfully, calling the hosted function through the compiled binary, just like it does when using qualified function call syntax.

Actual Behavior

Successfully built ./test-effect-static
About to execute command...
[31mRoc crashed:[0m Hosted functions cannot be called in the interpreter

The program:

  1. ✅ Compiles successfully (no errors)
  2. ✅ Starts executing
  3. ✅ Prints "About to execute command..."
  4. ❌ Crashes when trying to call cmd.exec_exit_code!()

Analysis

What Works ✅

1. Regular methods with static dispatch:

cmd = Cmd.new("echo").args(["Hello"])  # Perfect!

2. Effect methods with qualified call:

result = Cmd.exec_exit_code!(cmd)  # Perfect!

3. Complete example with qualified effect:

$ roc build test-effect-qualified.roc && ./test-effect-qualified
Successfully built ./test-effect-qualified
About to execute command...
Hello
Exit code: 0

What Crashes ❌

1. Effect methods with static dispatch:

result = cmd.exec_exit_code!()  # Interpreter error!

2. Any effect on opaque type via . syntax:

cmd.exec_output!()  # Crashes
cmd.exec_cmd!()     # Crashes

Key Observation

The error message says "Hosted functions cannot be called in the interpreter" but:

  • The code was built with roc build
  • The binary was created successfully
  • The binary starts executing normally
  • Regular methods work fine
  • Only effect methods via static dispatch trigger this error

Comparison with Working Code

❌ Crashes - Static dispatch on effect

cmd = Cmd.new("echo").args(["Hello"])
result = cmd.exec_exit_code!()  # Interpreter error

✅ Works - Qualified call for effect

cmd = Cmd.new("echo").args(["Hello"])
result = Cmd.exec_exit_code!(cmd)  # Works perfectly

The only difference is:

  • cmd.exec_exit_code!() → interpreter error
  • Cmd.exec_exit_code!(cmd) → works correctly

Both should generate the same code since static dispatch is just syntactic sugar.

Hypothesis

When an effect method is called via static dispatch (. syntax):

  1. The type checker resolves it correctly (no compile error)
  2. The code generator produces code that incorrectly marks it for interpretation
  3. At runtime, the system tries to interpret a hosted function call
  4. This fails because hosted functions require compiled code

Possible causes:

  • Static dispatch code path doesn't properly mark effect calls as "compiled"
  • Effect method resolution via static dispatch uses a different code path than qualified calls
  • Missing flag or metadata that indicates the call should be compiled, not interpreted

Impact

This prevents using static dispatch for any effects on opaque types, forcing developers to:

  • Remember which methods are effects
  • Use different syntax for effects vs regular methods
  • Mix . syntax (for builders) with qualified syntax (for effects)

Example of required mixed syntax:

# Awkward: mixed syntax based on whether method is an effect
cmd = Cmd.new("echo").args(["Hello"])  # . syntax for regular methods
result = Cmd.exec_exit_code!(cmd)       # qualified for effects

Desired uniform syntax:

# Ideal: consistent syntax for all methods
result = Cmd.new("echo").args(["Hello"]).exec_exit_code!()

Test Files

Location: /Users/luke/Documents/GitHub/basic-cli/

  1. test-effect-static.roc - Demonstrates the interpreter error
  2. test-effect-qualified.roc - Shows the workaround (works correctly)
  3. test-simple-static.roc - Proves regular methods work fine

Workaround

Use static dispatch for regular methods, but qualified calls for effects:

# Use . for builders
cmd = Cmd.new("echo").args(["Hello"])

# Use qualified call for effects
result = Cmd.exec_exit_code!(cmd)

Related Issues

This is the third stage of static dispatch bugs:

  1. FIXED: Compile-time crash in checkDeferredStaticDispatchConstraints
  2. FIXED: Runtime panic node is not a pattern tag .type_header
  3. CURRENT: Interpreter error when calling effects via static dispatch

Each fix has revealed the next layer of the issue.

Fixed multiple issues introduced in commit 66dbe03:

1. Fixed control flow bug where hosted lambda check was falling through
   instead of being in an else branch, causing TypeMismatch errors for
   normal lambdas (List.sum tests were failing).

2. Fixed closure capture lookup by reverting lambda_expr_idx back to
   expr_idx for regular closures. The change to cls.lambda_idx broke
   closure captures, causing "e_lookup_local: definition not found" crashes.

3. Updated tryInvokeHostedClosure to unwrap e_closure expressions to
   find the underlying lambda, allowing it to work with both expr_idx
   (regular closures) and cls.lambda_idx (hosted effect methods).

4. Fixed hostedBuilderPrintValue to use hostedStdoutLine for output
   instead of direct stdout.writeAll, so test mode IO tracking works
   correctly.

Test results: 2280/2282 tests passing (99.9%)
- The 1 failing test (aoc_day2.roc NotNumeric) also fails on origin/main,
  so it's a pre-existing bug unrelated to this PR.
@lukewilliamboswell lukewilliamboswell marked this pull request as ready for review January 7, 2026 03:34
@lukewilliamboswell lukewilliamboswell merged commit c80b135 into main Jan 7, 2026
48 of 49 checks passed
@lukewilliamboswell lukewilliamboswell deleted the fix-issue-8928 branch January 7, 2026 03:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compiler Crash on Static Dispatch with Platform-Exposed Opaque Types

2 participants