Documentation

Semantic Contract

Semantic Contract

IRx exposes a small but explicit semantic boundary between host parsing and backend lowering. That boundary is defined in code by irx.analysis.get_semantic_contract() and is enforced by the public analysis entrypoints in irx.analysis.api.

Stable Semantic Phases

IRx currently treats these phases as stable:

Metadata Required Before Codegen

Before lowering starts, IRx guarantees that analyzed nodes may carry node.semantic: SemanticInfo with these stable fields:

For multi-module compilation, IRx also guarantees the following CompilationSession state before lowering:

Lowering should consume this semantic metadata instead of re-deriving meaning from raw syntax.

When lowering or build layers discover that this contract has been violated, they now raise structured diagnostics instead of flattening the failure to a plain Python exception string. In other words:

Each of those exception types carries one Diagnostic record with stable code, phase, and best-effort source attribution when IRx can recover it.

Diagnostic Contract

IRx now uses one shared diagnostics model across semantic analysis, lowering, native artifact compilation, final linking, and runtime feature resolution.

Every diagnostic may include:

Semantic analysis still aggregates multiple diagnostics in DiagnosticBag and raises SemanticError only after the semantic pass completes. Later phases raise one structured diagnostic exception immediately because they do not have a bagging pass today.

Source Locations

IRx centralizes source extraction through shared helpers:

IRx does not invent fake spans. If an AST node does not carry location data, the diagnostic still formats cleanly without a location prefix.

Diagnostic Codes And Prefixes

Diagnostic codes are split into a stable logical identifier and a configurable display prefix.

Current high-level families:

Downstream compilers can override the prefix without forking IRx formatting logic:

from irx.diagnostics import set_diagnostic_code_prefix

set_diagnostic_code_prefix("ARX-")

After that override, the same logical code renders as ARX-S010, ARX-L001, ARX-R001, and so on.

Formatting

IRx keeps the first diagnostic line compact:

module_a:12:8: error[IRX-S010]: argument 1 of call to 'puts' expects UTF8String but got Int32

When extra context exists, the formatter appends indented follow-up lines:

module_a:4:2: error[IRX-S002]: Identifier already declared: value
  note: duplicate declarations in one scope are not allowed
  related: module_a:1:1: previous declaration is here

Non-semantic failures also keep their phase visible:

error[IRX-K001] (link): link failed while producing 'demo'
  note: command: clang /tmp/irx_module.o -o /tmp/demo
  note: stderr: undefined reference to `sqrt`

Function Signature And Calling Contract

Callable semantics are part of IRx's stable semantic boundary.

Calling conventions are classified semantically even when current LLVM emission is shared:

Current declaration metadata is intentionally narrow. When present on FunctionPrototype, IRx consumes:

Class Inheritance Contract

Class semantics are also part of IRx's stable semantic boundary.

Class Layout Contract

IRx now records a deterministic low-level object layout for every analyzed class before lowering.

Class Method Contract

IRx class methods now lower as explicit functions with analysis-owned dispatch metadata rather than as implicit runtime behavior.

Class Member Access Contract

IRx now distinguishes instance and class-qualified member access forms explicitly instead of inferring them from generic field syntax.

Class Access Control Contract

IRx enforces class visibility during semantic analysis instead of deferring it to lowering or runtime behavior.

Class Construction Contract

IRx exposes one analyzed class-construction path with optional lifecycle hooks.

See Class Model for lifetime and failure-path restrictions. These names do not imply Python finalization rules.

Native-Backed File Class

Hosts can register one validated class declaration through ParsedModule.native_file_class. The capability must reference a node in that module and have the required private native field and method ABI. A source name or origin string alone grants nothing. Class resolution uses declaring-module bindings and preserves already-qualified class type identities.

Public File(...) and builtin open(...) share argument validation and a ResolvedClassConstruction.native_file action. Lowering performs this native initialization in the constructing frame, checks status before output-slot use, and publishes the field before completing its phase. Native-backed method ResolvedFileOperation.receiver_field metadata pins the class receiver and selects its private handle at the real call site. No class-name/type lookup is performed by lowering. Raw astx.FileType remains available to low-level hosts under its existing unique-owner constraints.

Class ABI And Interop Contract

IRx now makes the internal class ABI explicit without treating it as a stable foreign object ABI.

Class Diagnostics Contract

IRx now treats class errors as stable semantic diagnostics rather than backend failures.

Public FFI Contract

IRx now treats explicit extern/native declarations as one public FFI layer instead of an incidental backend escape hatch.

What Qualifies As A Public FFI Callable

Public FFI Type Policy

IRx intentionally keeps the public FFI type surface narrow in this phase.

Accepted in extern signatures:

Rejected in extern signatures:

ABI-Compatible Structs For FFI

The public FFI layer accepts a validated subset of IRx structs:

Pointers And Opaque Handles

Symbol Resolution And Runtime Features

Host-Supplied Native Package Capabilities

irx.native defines immutable NativeArtifact, NativeFeature, and NativeBinding records without LLVM factories. Hosts supply resolved paths and bindings to actual extern identities through ParsedModule.native_features and native_bindings. IRx never discovers distributions or reads package manifests.

The reachable session collects consistent capabilities. Analysis validates feature availability, builtin isolation, and extern ABI/native requirements across modules, recording features on semantic signatures. Lowering consumes the same capabilities through a compilation-local registry and existing native linking. No external registration mutates the cached builtin registry. Source parameter aliases/defaults are not differences in the C ABI.

Arx exposes this declaratively through dependency-scoped native package metadata, not executable plugins or dynamic loading. Known NUL-containing literal extern arguments are rejected; owned extern string returns remain unsupported.

Intentionally Unsupported For Now

Minimal examples:

puts = astx.FunctionPrototype(
    "puts",
    args=astx.Arguments(astx.Argument("message", astx.UTF8String())),
    return_type=astx.Int32(),
)
puts.is_extern = True
puts.calling_convention = "c"
puts.symbol_name = "puts"
sqrt = astx.FunctionPrototype(
    "sqrt",
    args=astx.Arguments(astx.Argument("value", astx.Float64())),
    return_type=astx.Float64(),
)
sqrt.is_extern = True
sqrt.calling_convention = "c"
sqrt.symbol_name = "sqrt"
sqrt.runtime_feature = "libm"
open_handle = astx.FunctionPrototype(
    "open_handle",
    args=astx.Arguments(),
    return_type=astx.OpaqueHandleType("demo_handle"),
)
open_handle.is_extern = True
open_handle.calling_convention = "c"
open_handle.symbol_name = "open_handle"
astx.StructDefStmt(
    name="Point",
    attributes=[
        astx.VariableDeclaration(name="x", type_=astx.Float64()),
        astx.VariableDeclaration(name="y", type_=astx.Float64()),
    ],
)

take_point = astx.FunctionPrototype(
    "take_point",
    args=astx.Arguments(astx.Argument("point", astx.StructType("Point"))),
    return_type=astx.Int32(),
)
take_point.is_extern = True
take_point.calling_convention = "c"
take_point.symbol_name = "take_point"

Call And Return Validation

Function calls are validated through one semantic path before lowering:

Returns are also validated semantically before lowering:

Representative examples of the current semantic style:

Void and non-void usage is explicit:

Generator And Yield Contract

IRx supports an initial typed generator contract for named functions:

The current LLVM lowering supports straight-line named generator bodies with top-level YieldStmt nodes. Yield suspension inside nested control flow is diagnosed before lowering until frame and cleanup suspension are extended.

main Contract

main is part of the stable semantic contract rather than a backend caveat:

IRx no longer accepts loose void main behavior or non-deterministic fallthrough.

Scalar Numeric Foundation

Binary scalar numerics use one canonical promotion table:

Operand mix Promoted operand type
float + float wider float
float + integer float widened to cover the integer width floor (16, 32, or 64 bits), capped at Float64
signed + signed wider signed integer
unsigned + unsigned wider unsigned integer
signed + unsigned wider signed integer when the signed operand is strictly wider; otherwise the wider unsigned integer

Comparison operators (<, >, <=, >=, ==, !=) promote their operands with the same table and always return Boolean semantically and i1 in LLVM IR.

Boolean And Comparison Contract

Boolean behavior is part of the stable semantic boundary:

Lowering should branch directly on the analyzed Boolean i1 value for control flow instead of inventing zero-comparison truthiness rules during codegen.

Loop Contract

Loop semantics are now part of the stable lowering contract for structured control flow:

Context Manager Contract

WithStmt(manager, body, target=None) models a Python-like context-manager statement over IRx class values:

Lowering consumes the resolved context-manager sidecar instead of rediscovering method names in codegen.

Struct Contract

Structs are IRx's stable composite storage and ABI foundation.

For now, empty structs are rejected explicitly instead of relying on backend- specific behavior.

Buffer/View Model

IRx defines a canonical buffer owner plus buffer view substrate for low-level memory/container interop. This is not a user-facing scientific array API, and it does not define broadcasting, slicing syntax, reductions, or tensor algebra.

The canonical view descriptor is a plain stable struct conceptually equivalent to:

Stable built-in primitive dtype tokens are also available when a producer does not need an out-of-band dtype handle:

Semantic rules:

Lowering uses irx_buffer_view as a named plain struct with stable field order:

%"irx_buffer_view" = type {i8*, i8*, i8*, i32, i64*, i64*, i64, i32}

Runtime/native lifetime operations are feature-gated behind the buffer runtime feature. Plain descriptors do not pull native helper symbols into a module unless a helper is used.

Array Runtime Interop Contract

IRx exposes arrays through a builtin runtime surface backed by Arrow. The high-level abstraction is array-oriented; the low-level FFI ABI remains Arrow-specific. It is not a first-class language container model.

IRx also now exposes an initial internal Tensor layer alongside that one-dimensional array runtime:

Stable scope in this phase:

Import/export rules:

Nullability rules:

Arrow-to-buffer-view bridge rules:

Tensor layering rules:

Intentionally out of scope here:

Example scalar wrapper:

astx.StructDefStmt(
    name="ScalarBox",
    attributes=[
        astx.VariableDeclaration(name="value", type_=astx.Int32()),
    ],
)

Example nested record:

astx.StructDefStmt(
    name="Descriptor",
    attributes=[
        astx.VariableDeclaration(name="point", type_=astx.StructType("Point")),
        astx.VariableDeclaration(name="ready", type_=astx.Boolean()),
    ],
)

Canonical Cast Policy

Implicit promotions in variable initializers, assignments, call arguments, and returns are intentionally narrower than explicit casts:

Explicit Cast(...) expressions allow the full scalar conversions:

Error Boundaries

What Arx May Hand to IRx

Class Lifecycle Contract

ClassConstruct carries positional expressions and KeywordArgument nodes. ResolvedClassConstruction records source-order evaluations, ABI argument indices/types, and canonical ClassLifecyclePhase entries. Hooks are resolved only by analysis; lowering never selects them from source names.

ResourceKind.CLASS describes owned references, borrowed reads, retain/copy, move into storage, and owned returns. resolved_resource_store separates the store's ownership acquisition from the assignment expression's borrowed result. Parameter slots acquire independent local references; receivers remain pinned borrows. Mutable cleanup loads the current slot, not its original generation. Field metadata records resource kinds and concrete-descriptor access IDs. String-field reads resolve as owned snapshots: pinning the object alone cannot protect a string allocation replaced by a later argument.

The class native feature owns opaque reference counts and phase progress; generated concrete callbacks own field traversal. Analysis rejects recursive field graphs (including subtypes), escaping hook receivers, unsupported storage, and non-exact initialization of native file fields. See the class model and ownership inventory.

Bound Call Arguments

CallResolution.arguments records explicit evaluations in source order, their ABI destination indices, omitted defaults with resolved target types, and parameter/receiver symbols for default environments. Argument types and conversions are indexed by explicit evaluation order, not destination order. IRx binds names before validation, overload matching, and template inference; Arx only preserves call syntax. Declaration defaults retain their original semantic resolution. Lowering must require this metadata rather than infer missing defaults from a positional prefix or reorder/mutate source AST operands.

RecordBatch interoperability and FFI-owned strings

stdlib.arrow consists of ordinary class/call ASTx nodes and nominal OpaqueHandleType fields. The host supplies NativeBinding declarations for the existing record_batch feature; IRx resolves their ABI, symbols, conversions and class lifetimes before lowering. The frontend has no Arrow lowering code.

Extern prototypes may explicitly declare ffi_return_ownership="owned" for a string result. Analysis validates this contract and records it in FFICallableInfo.return_type.metadata["ownership"]; incompatible redeclarations are rejected. The result must be a non-null, malloc-compatible, NUL-terminated UTF-8 allocation transferred to IRx, which uses normal owned-string cleanup. Unannotated external string calls remain rejected. Lowering consumes the resolved ownership sidecar, not the source annotation. This contract is used by the checked Arrow facade's copied string/schema inspection results.

The native status ABI remains recoverable. The Arx facade checks status before reading output slots, reports ARX-RUNTIME-ARROW-001 without fabricated source spans, and terminates on errors. Typed null reads, incompatible schemas and unsupported Arrow types fail explicitly. Native C Stream callbacks follow Arrow errno/EOS conventions rather than IRx status values. See ownership and interoperability.

Search documentation

Type to search.