Skip to content

[motoko_agent] Bug: cross-module nested record-type alias fails to unify post-3325d39f (TYP/MOD) #243

@sunholo-voight-kampff

Description

@sunholo-voight-kampff

Symptom

After commit 3325d39f (M-SCHEME-IMPORT-PRESERVE-ADT-HEAD, 2026-05-20), exported function calls fail to unify when a parameter contains a nested list of a record-type alias imported from another module. The substitution-tightening fix correctly removes leaky TVars but exposes an alias-env-propagation gap: imported aliases aren't visible to the unifier when checking cross-module call sites, so expandAlias() cannot resolve TCon("Inner") to its underlying TRecord and unification falls through to:

cannot unify type constructor Inner with *types.TRecord

Pre-3325d39f the case incidentally passed because over-polymorphic TVars satisfied unification at any shape.

ailang --version

AILANG v0.21.0-4-gdf2ed8de-dirty
Commit: df2ed8d

Minimal reproduction (3 files, ~25 lines)

ailang.toml:

[package]
name = "local/typebug"
version = "0.1.0"
edition = "1"
module_prefix = ""
ailang = ">=0.16.1"

[effects]
max = ["IO"]

typebug/types.ail:

module typebug/types

export type Inner = {
  name: string
}

typebug/lib.ail:

module typebug/lib

import typebug/types (Inner)

export type Outer = {
  items: [Inner]
}

pure func make_inner(n: string) -> Inner {
  { name: n }
}

pure func make_inners() -> [Inner] {
  [make_inner("a"), make_inner("b")]
}

export pure func build() -> Outer {
  { items: make_inners() }
}

export pure func use_outer(o: Outer) -> int {
  match o.items {
    [] => 0,
    _ :: _ => 1
  }
}

typebug/main.ail:

module typebug/main

import typebug/lib (build, use_outer)

export func main() -> int {
  let v = build();
  use_outer(v)
}

Run: ailang check typebug/main.ail (with AILANG_RELAX_MODULES=1).

Expected vs actual

  • Expected: build(): Outer is then passed to use_outer(o: Outer). Both refer to the same nominal type; should type-check cleanly (and does on v0.19.1).
  • Actual: unification fails at the call-site of use_outer(v) with the error above. The error names field 'items' correctly but cannot reconcile its element type Inner (a TCon after substitution) with the structural TRecord it sees on the other side.

Bisect

git bisect start df2ed8de v0.19.1
git bisect run /tmp/typebug-root/bisect-test.sh
→ first bad commit: 3325d39fd52d39dc17a4a7ae059f7027555de388
  "M-SCHEME-IMPORT-PRESERVE-ADT-HEAD: fix exported function schemes losing ADT heads"

Root cause analysis

expandAlias() at internal/types/unification_core.go:88 returns the input TCon unchanged when u.aliasEnv does not contain the alias name:

func (u *Unifier) expandAlias(t Type) Type {
    if u.aliasEnv == nil { return t }
    if con, ok := t.(*TCon); ok {
        if target, exists := u.aliasEnv[con.Name]; exists { ... return ... }
    }
    return t  // ← fall-through when alias not in env
}

The TCon → TRecord dispatch path at unification_core.go:233-238 is in place and calls this helper — but the helper can't find Inner because the alias was declared in typebug/types and the unifier instance servicing typebug/main's call-site type-check has an aliasEnv that doesn't include imports' alias declarations.

Most likely the fix lives wherever Unifier.aliasEnv is constructed for inter-module function-application checking: it needs to merge in imported modules' alias-type declarations (probably from their iface.json / ModuleRegistry). The M-WASM-TYPECHECK-FLOAT-DIVERGENCE fix (ad84b68d) did something similar for the WASM type-checker — "propagate imported type aliases + param annotations in ModuleRegistry" — but the native path appears not to have the equivalent merge for aliasEnv.

Where it surfaced

  • Repo: arniwesth/motoko_agent PR [cli] Bug #27 followup: math codegen has two issues ... #28 (CI: https://github.com/arniwesth/motoko_agent/actions/runs/26002230244).
  • File: pkg/sunholo/motoko_ext_mcp@0.2.7/register.ail:10:13 — call make_hooks(cfg) where cfg: McpConfig and McpConfig.servers[].tool_mappings: [McpToolMapping], with McpToolMapping imported from a separate types.ail module within the same package.
  • Symmetric pattern: any AILANG package that splits its record-type aliases into a types.ail and uses them nested in other modules' record-typed parameters will hit this in v0.21.0+. motoko_ext_omnigraph, motoko_ext_context_mode, motoko_ext_compaction_ai and similar packages have the same structure and will break on their next republish against current dev.

Suggested fix

Trace Unifier.aliasEnv construction; ensure it's populated from ModuleRegistry imports at the point where function-application type-checking creates a Unifier for a cross-module call. A regression test cloning the 3-file repro into internal/pipeline/ would lock this in — the minimal repro above is suitable.


Binary info (auto-attached):
ailang version: v0.21.0-4-gdf2ed8de-dirty
binary md5: 96ffb6c26f311f9fdcdb01deece69f4d
binary path: /Users/voightkampff/go/bin/ailang
git commit: df2ed8d


Reported by: motoko_agent via ailang messages

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug reportfrom:motoko_agentMessage from motoko_agent agent

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions