JVM implementation

THC executes exported GHC Core on Truffle and Graal. This reference covers the Java implementation. Host applications should start with thc.Main.executionContext and thc.Main.loadManagedExports, and read the site's embedding guide for authority and lifetimes.

There are two namespaces. thc loads and checks programs and provides the host entrypoints. thc.runtime contains their execution machinery. Within the runtime, distinguish values, executable nodes, and layout metadata: a Haskell closure, the node that calls it, and the description of its captured fields have different jobs and lifetimes.

This is an implementation reference, not a stable SDK. Java public visibility does not promise stable runtime carriers, node layouts, frame slots, calling conventions, or generated Truffle APIs. Generated DSL and SIMD sources and test fixtures are intentionally excluded.

Package thc

This namespace is the boundary between an exported program, its host, and the runtime. Start with executionContext and loadManagedExports when embedding THC; loadEntry supplies the separate typed Core and executable contracts.

Package thc.runtime

This namespace contains both the objects a Haskell program computes with and the interpreter that computes them. They are deliberately different kinds of object, even where their names sound similar.

Values, nodes and descriptions

Role Examples What an instance represents
Guest values DataValue, internal Closure and Thunk, primitive scalars, fixed-species JDK Vector API values A constructor, function, delayed computation or computed result. A value can be shared by several calls.
Value storage CapturedFrame, HandoffStorage, managed arrays and addresses Fields holding values, with a specific ownership and lifetime. Storage is not executable code.
Executable Truffle nodes GuestRoot, BytecodeRoot, internal Expr, Force and call nodes Instructions and control flow, with children, specialization state and call-site caches. Nodes execute against invocation frames and runtime values.
Layout and lowering metadata DataLayout, CaptureLayout, HandoffLayout, internal CoreRepresentation How to interpret or allocate fields and which execution path is justified. A layout describes storage; it does not contain a particular value's fields.
Program construction and linkage Program, BytecodeProgram, ExecutableProgram Lowering, linked bindings, root call targets and host entrypoints. These are not themselves expression nodes or guest closures.
Context services Language.State and its thread, file and native-resource registries Resources and mutable state belonging to one polyglot context.

SIMD values use JDK ByteVector, ShortVector, IntVector, LongVector, FloatVector and DoubleVector directly. GHC names such as Int16X8# describe guest types, not JVM wrapper classes. Their exact VecRep metadata retains lane count and signedness; durable heap storage may retain primitive lanes instead.

For example, a Closure holds a target, an optional CapturedFrame and any already supplied arguments. A call node invokes that target. CaptureLayout describes the captured fields. The closure and its environment can outlive the invocation that created them; a Truffle VirtualFrame cannot simply escape with them. CapturedFrame is selective heap storage, not a saved VirtualFrame.

Some runtime classes are public because generated Truffle code must construct or access them. Public visibility does not make them a supported embedding ABI. Internal node types are named here to explain the implementation; they are not all included in this public API index.

JVM execution and compilation

The loader supplies optimized Core together with representation, demand and source information. Program lowers it to an AST; BytecodeProgram lowers it to Truffle Bytecode DSL instructions, locals and labels. Both use the runtime's value representations, lazy evaluation protocol and application machinery.

Truffle specializes execution using the node structure, observed types and call targets. Graal partially evaluates that interpreter and builds its own compiler graph before producing machine code. The Truffle nodes documented here are not Graal IR nodes. A constructor allocation or call visible in interpreter source may disappear after partial evaluation, inlining and escape analysis; that is an optimization outcome, not a different source-level value contract.

The Native Image workflows separately provide a pure interpreter and an experimental selected-Core compiled cache. They have narrower admission and resource-lifecycle contracts than the JVM runtime.

Values and representation

DataValue stores an ordinary algebraic constructor, including lifted tuples. Its DataLayout fixes the constructor identity and field representations. Truffle StaticShape supplies typed fields. Where a generated storage class has a unique layout owner, that class can identify the layout; a shared-carrier fallback keeps an explicit layout reference.

A thunk is a separate updateable value. Forcing it may execute a target, wait for another evaluator, return an existing answer or propagate a guest failure. Successful update releases the original body and environment. A node that forces thunks has its own execution state and target caches; it is not the thunk's shared evaluation state.

Unboxed tuples are different from lifted tuple constructors. Their components can occupy several physical slots; a State# component occupies none. Supported aggregate fields in boxed constructors likewise flatten into typed properties: DataLayout.arity counts physical fields, not logical constructor arguments. Representation proofs determine which primitive lanes can be used, rather than a Haskell type name alone. Unsupported representations are rejected at admission.

Typed execution uses primitive frame slots and specialized operations where the available proof permits them. An adaptive path can widen when its assumptions fail. A fixed representation proof is stronger than an observed profile: it justifies a concrete representation without guessing from the first value seen.

Calls, joins and loops

The application machinery handles exact, partial and excess application. Partially supplied arguments are runtime data owned by the resulting closure; direct-call caches belong to executable call nodes. Call-target identity and argument layout metadata let the compiler specialize a call without making the closure itself part of the Truffle node tree.

Core join points lower to local control flow where their checked shape permits it. Recursive joins and tail transfers use loop and dispatch machinery to avoid repeatedly growing the Java stack. Non-tail stack behavior depends on the backend and continuation mode; tail-call support alone is not a general stack-safety guarantee. The AST backend uses expression, call and loop nodes; the bytecode backend also has explicit labels and branches. These are the control-flow portions of the interpreter, distinct from the constructor and closure objects they manipulate.

Dense handoff storage is a calling-convention choice. Reusable incoming loans must be consumed into durable locals before guest execution can re-enter the runtime. Escaping closure captures and partial applications must own their retained data. A HandoffStorage object is therefore transport storage, not a Haskell tuple or an invocation's VirtualFrame; its ownership matters as much as its field types.

Exceptions, continuations and stack inspection

GuestException transports a Haskell exception payload through Truffle. RuntimeFault reports a runtime or contract failure. Neither should be confused with the internal control transfers used for tail calls and suspension.

A suspended computation needs executable resumption state as well as retained values. Internal continuation segments serve that purpose on supported paths. ManagedStackSnapshot instead stores detached binding names and source coordinates for inspection. It retains no live frames, nodes or call targets and cannot resume execution. A printable stack is not an AP_STACK equivalent.

Asynchronous delivery and foreign-call boundaries have additional admission and ownership constraints. The presence of continuation support in an internal backend does not imply that every public call boundary supports it; consult the backend and entrypoint contracts when adding a new path.