Skip to content

Compiler panic when calling a number-typed function value stored in a container: "instantiation unified two different primitive types" #10209

Description

@rtfeldman

Calling a function value that was stored in a container (a record field, a tuple, a list, or a Result payload) panics the compiler during monotype lowering whenever the lambda's numeric types are a concrete non-default primitive (e.g. I64 or F64). The same code works if the numbers are left to default to Dec, or if the function value is first rebound to a plain local before calling it.

thread 3123713 panic: postcheck invariant violated: instantiation unified two different primitive types

This is a Debug-build panic via Common.invariant (src/postcheck/common.zig:112); in release builds the same path is unreachable, so release builds reach undefined behavior instead of a clean panic.

Steps to reproduce

  1. Build the compiler at current main (reproduced at 04802b59f7):

    zig build
    
  2. Save this as repro.roc:

    module []
    
    expect {
        r = { op: |x| x * 11.I64 }
        (r.op)(4.I64) == 44
    }

    (The module [] header prints an unrelated deprecation warning; it keeps the repro self-contained.)

  3. Run:

    ./zig-out/bin/roc test repro.roc
    

Expected: the expect runs (and passes). Actual: compiler panic before anything executes.

Stack trace

thread 3123713 panic: postcheck invariant violated: instantiation unified two different primitive types
src/postcheck/common.zig:112:24               invariant
src/postcheck/monotype/solve.zig:712:66       unifyConcrete
src/postcheck/monotype/solve.zig:662:47       unifyRoots
src/postcheck/monotype/solve.zig:617:32       unify
src/postcheck/monotype/lower.zig:16762:33     instantiateTargetCallNodeFromMonoArgs
src/postcheck/monotype/lower.zig:16746:71     instantiateTargetCallTypeFromMonoArgs
src/postcheck/monotype/lower.zig:20263:68     methodTargetMonoTypeFromPlan
src/postcheck/monotype/lower.zig:19376:50     lowerDispatchExprAtType
src/postcheck/monotype/lower.zig:18677:77     lowerExprAtType
src/postcheck/monotype/lower.zig:24438:85     lowerBindingContinuation
src/postcheck/monotype/lower.zig:9044:53      lowerLambdaArgsAndBody
src/postcheck/monotype/lower.zig:8984:56      lowerLambdaTemplate
src/postcheck/monotype/lower.zig:9109:44      lowerNestedLambdaTemplate
src/postcheck/monotype/lower.zig:9092:67      lowerNestedFunction
src/postcheck/monotype/lower.zig:3162:60      lowerNestedFnRequest
src/postcheck/monotype/lower.zig:3110:45      lowerNestedFnFromContext
src/postcheck/monotype/lower.zig:19258:95     lowerLambdaExpr
src/postcheck/monotype/lower.zig:9419:52      lowerExprWithType
src/postcheck/monotype/lower.zig:18712:59     lowerExprAtType
src/postcheck/monotype/lower.zig:19139:41     lowerRecordExpr
src/postcheck/monotype/lower.zig:9392:64      lowerExprWithType
src/postcheck/monotype/lower.zig:9214:42      lowerExpr
src/postcheck/monotype/lower.zig:26775:41     lowerPatternStatement
src/postcheck/monotype/lower.zig:26667:58     lowerStatement
src/postcheck/monotype/lower.zig:25310:75     lowerBlockStatements
src/postcheck/monotype/lower.zig:25272:52     lowerBlock
src/postcheck/monotype/lower.zig:9451:50      lowerExprWithType
src/postcheck/monotype/lower.zig:18721:51     lowerExprAtType
src/postcheck/monotype/lower.zig:9241:40      lowerComptimeRootExprAtType

Per the trace, the panic fires while lowering the lambda nested inside the container literal (lowerRecordExprlowerNestedFunction → lambda body): the static-dispatch lowering of the * inside the lambda body instantiates the method target's type and unifies a formal against an argument monotype, and the two sides carry different concrete primitives. Given the works/panics matrix below, this looks like the numeric default (Dec) leaking into the method-target instantiation context used for lambdas that are lowered as children of container literals, where it then collides with the lambda's actual concrete primitive (I64, F64, ...).

roc check on the same file reports 0 errors — the type checker is fine with the program; only the monotype lowering stage panics.

What panics vs. what works

All of these were verified individually with roc test on the build above. Captures are irrelevant (both capturing and capture-free variants behave identically); the discriminator is the concrete number type inside the lambda.

Panics — function value read out of a container and called, with .I64 (or .F64) typed numbers:

# record field
r = { op: |x| x * 11.I64 }
(r.op)(4.I64)

# tuple destructure
pair = (|x| x + 1.I64, |x| x * 2.I64)
match pair { (add, mul) => add(1.I64) + mul(3.I64) }

# list destructure
fs = [|x| x + 1.I64]
match fs { [f] => f(1.I64), _ => ... }

# Result payload
fs = [|x| x + 1.I64]
match List.first(fs) { Ok(f) => f(1.I64), Err(_) => ... }

Works — same shapes, numbers left to the Dec default:

fs = [|x| x + 1, |x| x + 2]
match List.first(fs) { Ok(f) => f(41), Err(_) => ... }   # passes

Works — same .I64 lambda in a container, but rebound to a local before the call:

r = { op: |x| x * 11.I64 }
op = r.op
op(4.I64)        # passes

Works — .I64 closures that never pass through a container: called directly, passed as function arguments, or returned from calls.

Note the existing eval-corpus case "top-level list constant containing callable values" (src/eval/test/eval_tests.zig) is the untyped-literal variant, which is why this never surfaced in the test suite: with a .I64 inside the lambdas, that same corpus case's shape panics too.

Where this is tracked in-repo

The Lambda Mono differential harness's generated sweep corpus keeps four repros as expected failures, flagged known_panic in src/eval/test/lambda_mono_generated_corpus.zig. Once this is fixed, those flags can be removed and the cases will run as normal differential checks:

zig build run-test-lambda-mono-differential -- generated-only --filter "closure"

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions