Modules
ES module syntax — named/default/namespace imports, re-exports, aliasing, and static CommonJS require — resolved by a whole-program model rather than separate compilation units. The big divergence is architectural: the resolver merges the entry file plus every transitive import into one program before codegen, so there is no runtime module object, no live bindings, and no separate-compilation boundary. Namespace imports and import.meta.url are compile-time-only constructs, and an import cycle is restricted to declarations. Dynamic import() runs its literal-specifier target on first use, sharing the program's modules as in Node; -dynamic-import=lazy moves each target's code into a shared library loaded on first use.
export function / const/let/var / interface / type / enum
ExactThe export modifier on any top-level declaration — function, const/let/var, interface, type alias, or enum. It is a declaration-level modifier and nothing more: the resolver consumes it entirely while building each file's export table, so codegen never sees an export node.
export function add(a: number, b: number): number {
return a + b
}Local export list (export { a, b as c }, export {})
ExactExports the file's own top-level declarations, optionally under another name. A name the file does not declare at its top level is a compile error. `export {}` exports nothing: it only marks the file as a module.
const VERSION = "1.0"
function shout(s: string): string { return s.toUpperCase() + "!" }
export { VERSION, shout as loud }import { a, b } from './relative/path'
ExactNamed imports from a relative-path module — named imports only, relative paths only (`./`, `../`) with `.ts` auto-appended if omitted. Resolved at compile time against the importing file's own directory (not `cwd`), then merged into the single whole-program AST.
import { add, mul, Point, Direction, squareOf } from './math'Circular imports
Works, with caveatsTwo files may import from each other and call each other's exported functions. The cycle is resolved once into the merged program.
- Supported only for the declarations-only case — a file in an import cycle can't run arbitrary top-level side-effecting code
Diamond-shaped import graphs
ExactA file imported from multiple places is parsed once and merged once, memoized by absolute path, so a shared dependency in a diamond graph contributes its declarations and runs its top-level code exactly once.
Imported (non-entry) files may run top-level side-effecting code
Works, with caveatsAn acyclic imported file's top-level statements run exactly once, in real dependency order, strictly before whatever imports it — no restriction to declarations-only. Diamond-shared dependencies still run exactly once via per-path memoization.
import { ready } from './side_effects_setup'
console.log("main: ready = " + ready())- A file that genuinely participates in an import cycle keeps the declarations-only restriction (no bare executable top-level statements), and additionally requires a top-level
var/let/constinitializer to be a compile-time literal — no TDZ/live-binding modeling
True per-file module scope (mangled internal names)
ExactEvery top-level declaration gets a file-private mangled name, and every reference to it is rewritten via a real scope-aware walk, so two unrelated files may freely declare the same top-level name. Binding two different files' exports to the same local name in one importing file with no `as` is still rejected — a real conflict, same as real ES modules.
import { run as runA } from './scoping_a'
import { run as runB } from './scoping_b'Import aliasing (import { a as b })
ExactA direct consequence of the per-file rename mechanism: `b` is simply bound to the target's mangled name for `a`, with no separate rename step.
import { run as runA } from './scoping_a'
import { run as runB } from './scoping_b'Type-only imports and exports
Works, with caveatsEvery form is erased with its statement, including a builtin module's types (`import type { IncomingMessage } from 'http'`). A plain named import of a Node class this compiler implements only as a type (`import { IncomingMessage } from 'http'`) is a type import too. Using a type-only import as a value is TS1361, as in tsc.
import type { Box } from './type_only_shapes'
import { type Side, area } from './type_only_shapes'- A Node class implemented only as a type (
IncomingMessage,ServerResponse, …) can't be used as a value (x instanceof IncomingMessageis a compile error).
export default
ExactNamed (function/class/interface/type/enum) and anonymous function/class forms, plus an arbitrary expression (`export default 42`, wrapped as a synthetic const). Exposed only under the export key "default" — a named default export's own name stays usable for intra-file self-reference (recursion, etc.) but is not itself importable via `import { foo }`, matching real ES modules.
export default function greet(name: string): string {
return "hello " + name
}import x from './path' (default import)
ExactAlso combinable with a named list in one statement (`import x, { a, b } from './path'`). Sugar over the same specifier machinery as a named import — no separate resolver mechanism.
import greet, { shout } from './greet'import * as ns from '...' (namespace import)
Works, with caveats`ns.member` is rewritten directly to that member's mangled name during resolution, in values, `new ns.C()`, `extends ns.C` and types (`x: ns.T[]`). Only actually-exported members are reachable (`ns.privateHelper` is rejected, same visibility rule as a named import); `ns.default` reaches a default export the same way any other member does.
import * as greetLib from './greet'
console.log(greetLib.shout("hey"))
console.log(greetLib.default("there"))- Compile-time-only —
nsis never a runtime value, usable only as the object of a non-computed dotted access; assigning it to a variable, passing it as a value, orns["x"]reaches codegen as a plain undefined reference rather than a dedicated error message
Static CommonJS require('<literal>')
Works, with caveatsDesugared to the equivalent ES import at parse time, so it resolves and dispatches identically to `import` and interoperates with `import` in the same file. `const x = require('m')` binds the namespace (whole module object); `const { a, b } = require('m')` is a named import; bare `require('m')` is side-effect-only.
const path = require('path')
const { basename, extname } = require('path')- Top-level, string-literal specifier only.
const x = require('m')binds the namespace (whole module object);const { a, b: c } = require('m')is a named import; barerequire('m')is side-effect-only. A destructuring default/nested-pattern/rest on a require binding, or a dynamicrequire(expr)/ arequirenested in a function body (lazy loading), is a clean compile error — deferred to the runtime/lazy-loading capability
Re-exports (export { x } from './other', export * from './other')
Works, with caveatsNamed re-export list (with `as` aliasing) and star re-export both work, including default re-export in either direction. `export *` never forwards a default, matching real ES modules. Chains of arbitrary depth resolve transitively. A re-export forwards a name to this file's importers only — it does not become a usable bare identifier inside the re-exporting file itself.
export { add } from './re_export_core'
export { mul as multiply } from './re_export_core'
export { default as greet } from './re_export_core'- No namespace re-exports (
export * as ns from) — parsed and explicitly rejected with a clear error rather than silently mis-parsed, no concrete use case yet - Re-exporting from a built-in module (
export { readFileSync } from 'fs') is an explicit, rejected scope cut, not an oversight
Bare/package-style imports (import x from 'somepackage')
Works, with caveatsA bare specifier resolves against `klain_modules/<name>/klain.json`'s "main" field, found by walking upward once from the entry file's own directory (not re-walked per importing file, unlike Node). A real built-in module name (`'fs'`/`'path'`/etc.) is always checked, and always wins, before any `klain_modules` lookup is attempted.
import { greet } from 'greeter'- klmpm is Stage 1 only — deliberately just the resolution half; no klmpm tool exists yet to fetch/version/lock a dependency, so a
klain_modules/<name>/directory has to be hand-constructed today - npm/
node_modulesinterop is a separate, unstarted, differently-scoped mechanism
Dynamic import(...)
Works, with caveatsBy default the target is compiled into the binary and its top-level runs on first import(), as a task on the program's one event loop: it shares every module instance with the program, a top-level throw rejects the import(), and an unsettled top-level await exits with code 13, as in Node. -dynamic-import=lazy keeps those semantics with each target's code in a shared library beside the binary, loaded on first use. -dynamic-import=isolated makes each target its own program with its own module instances, so its import-time state stays apart from the program's.
const m = await import('./mod')
console.log(m.greeting, m.answer)- String-literal specifier only — a runtime-computed import(expr) is a clean compile error
- The result object exposes annotated scalar/string exports only; function/class/array/object exports are omitted
- import('node:…') of a builtin module is a clean compile error
- lazy and isolated need the sibling <binary>.d/ directory and refuse --static; on Windows lazy compiles thread-locals emulated (a call per access)
- isolated is a deliberate divergence: the target gets its own instance of every module it imports
import.meta.url
Works, with caveatsResolved entirely at compile time, per file, into a plain string literal (`"file://" + that file's own absolute path`, matching real Node/browser convention) — codegen never sees a real module metadata object.
console.log(import.meta.url.startsWith("file://"))
console.log(import.meta.url.endsWith("import_meta_url.ts"))import.meta.urlis the only supported member — bareimport.metaor any other member (import.meta.resolve, etc.) is a clean parse-time error
Import-gated built-in bindings (fs/path/os/querystring/assert/http/cluster/Memory)
Exact`import fs from 'fs'`, `import * as fs from 'fs'`, and `import { readFileSync } from 'fs'` (including aliasing and combining a default with named specifiers in one statement) all bind through the same scope-aware machinery as a real file import; every Node-named builtin also resolves under the node: prefix. A local variable of the same name correctly shadows it; importing an unknown member is a clean, explicit rejection. Node-style built-ins require a default, namespace, or named import; process/console stay ambient by design.
import { readFileSync, writeFileSync as wfs, existsSync, unlinkSync } from 'fs'