Summary
When a TypeScript file does import * as ns from "./mod.js" (or .ts) and calls a function exported from that module via ns.fn(args), perry resolves the receiver to TAG_TRUE and throws TypeError: (boolean).fn is not a function.
This is the followup to #611 noted in v0.5.716's CLAUDE.md changelog. The Effect framework hits this at Duration.ts:524 (order.make(...)).
Repro
```ts
// inner.ts
export const make = (s: string) => s.toUpperCase();
// index.ts
import * as inner from "./inner.js";
const result = inner.make("hello");
console.log("typeof inner:", typeof inner);
console.log("typeof inner.make:", typeof inner.make);
console.log("result:", result);
```
Pre-fix output:
```
Uncaught exception: TypeError: (boolean).make is not a function
```
Expected (Node):
```
typeof inner: object
typeof inner.make: function
result: HELLO
```
Root cause
For RESOLVED namespace imports (where the source IS a real .ts module — distinct from #629's UNRESOLVED node:* namespaces), the codegen path doesn't recognize the Call { callee: PropertyGet { ExternFuncRef("ns"), "fn" } } shape. The PropertyGet falls through to the generic method-dispatch path at lower_call.rs:2250, which calls lower_expr(ctx, object) against the namespace ExternFuncRef. The Expr::ExternFuncRef arm at expr.rs:9970 doesn't find the namespace name in class_ids or import_function_prefixes (the prefix map is keyed by the namespace's MEMBERS, not by the namespace name itself), falls through to TAG_TRUE, and js_native_call_method then dispatches "fn" against a boolean → typeerror.
Additionally, the CLI driver's namespace-import walk in compile.rs:2755-2836 populates import_function_prefixes for every namespace export but doesn't populate imported_vars — so even if codegen routes through the namespace member path, var-shaped exports (export const fn = (s) => ...) get treated as function-decls and the zero-arg getter symbol is called with N args.
Fix surface
compile.rs::lower_module_decl namespace-import walk — populate imported_vars for namespace exports that are vars (mirror the named-import VAR path at line 3013).
lower_call.rs::lower_call — add an early arm that detects Call { callee: PropertyGet { ExternFuncRef(ns), method } } where ns ∈ namespace_imports, and routes:
- var-shaped → zero-arg getter +
js_closure_callN
- function-decl-shaped → direct
perry_fn_<src>__<method>(args) with rest-bundling
Summary
When a TypeScript file does
import * as ns from "./mod.js"(or.ts) and calls a function exported from that module vians.fn(args), perry resolves the receiver to TAG_TRUE and throwsTypeError: (boolean).fn is not a function.This is the followup to #611 noted in v0.5.716's CLAUDE.md changelog. The Effect framework hits this at
Duration.ts:524(order.make(...)).Repro
```ts
// inner.ts
export const make = (s: string) => s.toUpperCase();
// index.ts
import * as inner from "./inner.js";
const result = inner.make("hello");
console.log("typeof inner:", typeof inner);
console.log("typeof inner.make:", typeof inner.make);
console.log("result:", result);
```
Pre-fix output:
```
Uncaught exception: TypeError: (boolean).make is not a function
```
Expected (Node):
```
typeof inner: object
typeof inner.make: function
result: HELLO
```
Root cause
For RESOLVED namespace imports (where the source IS a real .ts module — distinct from #629's UNRESOLVED node:* namespaces), the codegen path doesn't recognize the
Call { callee: PropertyGet { ExternFuncRef("ns"), "fn" } }shape. The PropertyGet falls through to the generic method-dispatch path atlower_call.rs:2250, which callslower_expr(ctx, object)against the namespace ExternFuncRef. TheExpr::ExternFuncRefarm atexpr.rs:9970doesn't find the namespace name inclass_idsorimport_function_prefixes(the prefix map is keyed by the namespace's MEMBERS, not by the namespace name itself), falls through toTAG_TRUE, andjs_native_call_methodthen dispatches "fn" against a boolean → typeerror.Additionally, the CLI driver's namespace-import walk in
compile.rs:2755-2836populatesimport_function_prefixesfor every namespace export but doesn't populateimported_vars— so even if codegen routes through the namespace member path, var-shaped exports (export const fn = (s) => ...) get treated as function-decls and the zero-arg getter symbol is called with N args.Fix surface
compile.rs::lower_module_declnamespace-import walk — populateimported_varsfor namespace exports that are vars (mirror the named-import VAR path at line 3013).lower_call.rs::lower_call— add an early arm that detectsCall { callee: PropertyGet { ExternFuncRef(ns), method } }wherens ∈ namespace_imports, and routes:js_closure_callNperry_fn_<src>__<method>(args)with rest-bundling