19%

Section 11: Foreign Function Interface (FFI)

Goal: Enable Ori to call C libraries, system APIs, and JavaScript APIs (WASM target)

Criticality: CRITICAL — Without FFI, Ori cannot integrate with the software ecosystem

Proposal: proposals/approved/platform-ffi-proposal.md

Dependencies: Section 6 (Capabilities — uses FFI capability system must be in place), Section 18 (Const Generics — Deep FFI Phase 5)

Verification summary (2026-03-29): ~30% infrastructure exists (parser/IR/formatter/typeck), concentrated in 11.1, 11.3, and 11.4. Zero end-to-end FFI capability — no Ori program can call a C function today. The heavy lifting (codegen, linking, CPtr type system, capability enforcement) remains. Grammar (grammar.ebnf) and spec (spec/26-ffi.md, 554 lines) are comprehensive.

Sync Points: CPtr and C ABI Types (multi-crate sync required)

Adding CPtr and C ABI types requires updates across these crates:

  1. ori_ir — Add CPtr type variant, C type aliases (c_int, c_long, etc.)
  2. ori_types — Register CPtr type, FFI-safety validation, Option<CPtr> nullability handling
  3. ori_eval — CPtr runtime value representation, pointer operations in unsafe blocks
  4. ori_llvm — CPtr maps to LLVM opaque ptr type, C calling convention codegen
  5. library/std/ffi.ori — CPtr type definition, FfiError type (for Deep FFI)

Design Decisions (Approved)

QuestionDecisionRationale
Should FFI require capability?Yes, FFI capabilityConsistent with effect tracking
Single or multiple capabilities?Single FFIPlatform-agnostic user code
Support C++ directly?NoC ABI only; C++ via extern “C”
Support callbacks?Yes (native)Required for many C APIs
Memory management?Manual in unsafe blocksC doesn’t know about ARC
Async WASM handling?Implicit JsPromise resolutionPreserves Ori’s “no await” philosophy
Unsafe operations?unsafe { ... } blocksExplicit marking for unverifiable ops

11.1 Extern Block Syntax

Spec section: spec/26-ffi.md § Extern Blocks

Native (C ABI)

extern "c" from "m" {
    @_sin (x: float) -> float as "sin"
    @_sqrt (x: float) -> float as "sqrt"
}

JavaScript (WASM)

extern "js" {
    @_sin (x: float) -> float as "Math.sin"
    @_now () -> float as "Date.now"
}

extern "js" from "./utils.js" {
    @_formatDate (timestamp: int) -> str as "formatDate"
}

Implementation

  • Spec: Add spec/26-ffi.md with extern block syntax (verified 2026-03-29)

    • Define extern block grammar — grammar.ebnf lines 209-215 (verified 2026-03-29)
    • Define calling conventions (“c”, “js”) (verified 2026-03-29)
    • Define linkage semantics (verified 2026-03-29)
  • Lexer: Add tokens (verified 2026-03-29)

    • extern keyword — TokenKind::Extern reserved keyword (verified 2026-03-29)
    • String literals for ABI (“c”, “js”) — standard string tokens, validated in parser (verified 2026-03-29)
  • Parser: Parse extern blocks (verified 2026-03-29) — 20 Rust parser phase tests pass in oric/tests/phases/parse/extern_def.rs

    • parse_extern_block() in parser — ori_parse/src/grammar/item/extern_def.rs:22 (verified 2026-03-29)
    • Add ExternBlock to AST — ori_ir/src/ast/items/extern_def.rs:64 (verified 2026-03-29)
    • Add ExternItem variants — ori_ir/src/ast/items/extern_def.rs:37 (verified 2026-03-29)
    • from "lib" library specification — contextual keyword check (verified 2026-03-29)
    • as "name" name mapping (verified 2026-03-29)
    • C variadic (...) parsing with is_c_variadic flag (verified 2026-03-29)
    • Unknown convention warning emitted (verified 2026-03-29)
    • Formatter: ori_fmt/src/declarations/extern_def.rs (verified 2026-03-29)
  • Type checker: Validate extern declarations — NEEDS IMPLEMENTATION

    • Ensure types are FFI-safe
    • Check for uses FFI in callers
  • Codegen: Generate external references — NEEDS IMPLEMENTATION (GAP: parsed ExternBlock AST is never consumed by codegen; declare_extern_function exists only for runtime functions)

    • Emit LLVM declare for C functions
    • Handle calling convention
    • Link external symbols
  • LLVM Support: LLVM codegen for extern blocks

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — extern blocks codegen (NOTE: file does not exist yet)

  • AOT Tests: No AOT coverage yet

  • Test: tests/spec/ffi/extern_blocks.ori — NEEDS TESTS (no spec tests exist; only 20 Rust parser phase tests)

    • Basic extern function declaration
    • Multiple functions in one block
    • Name mapping with as
    • Library specification with from
    • End-to-end test (parse -> typeck -> codegen -> execute C call)

NOTE: Zero end-to-end FFI capability — no Ori program can actually call a C function today. Parser infrastructure is complete but codegen gap means extern blocks are parsed and stored but never processed into callable functions.

  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)
  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.
  • Subsection close-out (11.1) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.1 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.1: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.
  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.
  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.

11.2 C ABI Types

Spec section: spec/26-ffi.md § C Types

Primitive Mappings

Ori TypeC TypeSize
c_charchar1 byte
c_shortshort2 bytes
c_intint4 bytes
c_longlongplatform
c_longlonglong long8 bytes
c_floatfloat4 bytes
c_doubledouble8 bytes
c_sizesize_tplatform

CPtr Type

type CPtr  // Opaque pointer - cannot be dereferenced in Ori

extern "c" from "sqlite3" {
    @sqlite3_open (filename: str, db: CPtr) -> int
    @sqlite3_close (db: CPtr) -> int
}

// Nullable pointers
extern "c" from "foo" {
    @get_resource (id: int) -> Option<CPtr>
}

Implementation

  • Spec: Add C types section

    • Primitive type mappings
    • CPtr opaque pointer type
    • Option<CPtr> for nullable pointers
  • Types: Add C primitive types

    • Add CPtr to type system
    • Add C type aliases (c_int, c_long, etc.) with correct Pool widths
    • Size/alignment handling — c_char=i8, c_short=i16, c_int=i32, c_longlong=i64, c_float=f32, c_double=f64
    • Platform-dependent sizes (c_long, c_size) — i32 on Win64/ILP32, i64 on LP64
    • Fixes BUG-02-004: Replace resolve_ffi_concrete() (well_known/mod.rs:327) which currently collapses ALL c_* types to Idx::INT/Idx::FLOAT (i64/f64), losing C ABI widths. Touches: ori_types/pool/ (new Tag variants + Idx constants), ori_types/check/well_known/, ori_llvm/codegen/type_info/store.rs (new TypeInfo variants), ori_repr/ (triviality), ori_eval/ (Value representation decision). Decide: implicit intc_int promotion vs explicit cast (resolves spec ambiguity). Pool resolution-redirect path was added by BUG-04-021 fix as a placeholder for ARC classification — must be removed in favor of proper variants.
  • Type checker: FFI type validation

    • Warn on non-FFI-safe types
    • Validate CPtr usage
  • LLVM Support: LLVM codegen for C ABI types

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — C ABI types codegen

  • AOT Tests: No AOT coverage yet

  • Test: tests/spec/ffi/c_types.ori

    • All primitive C types
    • CPtr operations
    • Option for nullable
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.2) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.2 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.2: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.3 #repr Attribute

Spec section: spec/26-ffi.md § C Structs Proposal: proposals/approved/repr-extensions-proposal.md

Syntax

AttributeEffect
#repr("c")C-compatible field layout and alignment
#repr("packed")No padding between fields
#repr("transparent")Same layout as single field
#repr("aligned", N)Minimum N-byte alignment (N must be power of two)
#repr("c")
type CTimeSpec = { tv_sec: c_long, tv_nsec: c_long }

#repr("packed")
type PacketHeader = { version: byte, flags: byte, length: c_short }

#repr("transparent")
type FileHandle = { fd: c_int }

#repr("aligned", 64)
type CacheAligned = { value: int }

Combining: #repr("c") may combine with #repr("aligned", N). Other combinations are invalid.

Newtypes: Newtypes (type T = U) are implicitly transparent.

Implementation

  • IR: ReprAttrKind enum in ori_ir/src/ast/items/types.rs — C, Packed, Transparent, Aligned(u64) (verified 2026-03-29)

  • Parser: Parse #repr attribute variants (verified 2026-03-29) — ori_parse/src/grammar/attr/repr.rs:77

    • #repr("c") (verified 2026-03-29)
    • #repr("packed") (verified 2026-03-29)
    • #repr("transparent") (verified 2026-03-29)
    • #repr("aligned", N) — validate power of two (verified 2026-03-29)
    • Combined syntax #repr("c", "aligned", 16) (verified 2026-03-29)
    • Unknown repr value error reporting (verified 2026-03-29)
  • Type checker: Validate #repr usage (verified 2026-03-29) — validate_and_merge_repr_attrs in ori_types/src/check/registration/user_types.rs

    • Only valid on struct types, not sum types — E2041 (verified 2026-03-29)
    • transparent requires exactly one field — E2041 (verified 2026-03-29)
    • aligned N must be power of two — E2041 (verified 2026-03-29)
    • Reject packed + aligned combination — E2041 (verified 2026-03-29)
    • Reject c + packed combination — E2041 (verified 2026-03-29)
    • Reject duplicate attrs — E2041 (verified 2026-03-29)
    • Reject repr on newtypes — E2041 (verified 2026-03-29)
  • Repr-opt crate: ori_repr has ReprAttribute enum and StructRepr with layout computation — 57 tests pass (verified 2026-03-29)

  • Codegen: Generate appropriate LLVM layout — NEEDS IMPLEMENTATION

    • #repr("c") — default struct, no packed
    • #repr("packed") — LLVM packed struct type
    • #repr("transparent") — same type as inner field
    • #repr("aligned", N) — align N on allocations
  • LLVM Support: LLVM codegen for #repr structs

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — #repr struct codegen (NOTE: file does not exist yet)

  • AOT Tests: No AOT coverage yet

  • Spec tests (verified 2026-03-29) — 17+ tests in tests/spec/types/repr_attr*.ori

    • Positive: repr_attr.ori (c, packed, transparent, aligned)
    • Positive: repr_attr_c_aligned.ori (stacked c + aligned)
    • Positive: repr_attr_c_aligned_combined.ori (combined syntax)
    • Negative (compile-fail): repr_attr_aligned_not_power_of_two.ori (E2041)
    • Negative: repr_attr_transparent_two_fields.ori (E2041)
    • Negative: repr_attr_transparent_zero_fields.ori (E2041)
    • Negative: repr_attr_aligned_zero.ori (E2041)
    • Negative: repr_attr_packed_aligned.ori (E2041)
    • Negative: repr_attr_c_packed.ori (E2041)
    • Negative: repr_attr_duplicate_c.ori (E2041)
    • Negative: repr_attr_duplicate_aligned.ori (E2041)
    • Negative: repr_attr_c_on_sum.ori (E2041)
    • Negative: repr_attr_aligned_on_sum.ori (E2041)
    • Negative: repr_attr_c_on_newtype.ori (E2041)
    • Negative: repr_attr_packed_on_newtype.ori (E2041)
    • Negative: repr_attr_aligned_on_newtype.ori (E2041)
    • Negative: repr_attr_transparent_on_newtype.ori (E2041)
    • Fmt tests: tests/fmt/declarations/types/repr_attr.ori, repr_with_target.ori
    • Codegen verification tests — deferred until codegen implemented

NOTE: E2041 compile-fail tests serve as semantic pins — they would break if repr validation logic is removed. Good positive + negative pairing (3 positive, 14 negative).

HYGIENE: ori_parse/src/grammar/attr/repr.rs line 5 contains stale plan annotation (TPR-01-043) — should be cleaned up per CLAUDE.md plan annotation rules.

  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)
  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.
  • Subsection close-out (11.3) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.3 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.3: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.
  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.
  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.

11.4 Unsafe Expressions

Spec section: spec/26-ffi.md § Unsafe Expressions

Syntax

@raw_memory_access (ptr: CPtr, offset: int) -> byte uses FFI =
    // Direct pointer arithmetic - Ori cannot verify safety
    unsafe { ptr_read_byte(ptr: ptr, offset: offset) }

Semantics

Inside unsafe:

  • Dereference raw pointers
  • Pointer arithmetic
  • Access mutable statics
  • Transmute types

Implementation

  • Spec: Define unsafe block semantics

    • List of unsafe operations
    • Scoping rules
    • Interaction with FFI capability
  • Parser: Parse unsafe blocks (verified 2026-03-29) — parse_unsafe_expr at ori_parse/src/grammar/expr/primary/specials.rs:111

    • unsafe keyword — parses unsafe { block_body } into ExprKind::Unsafe(inner) (verified 2026-03-29)
    • Block expression (verified 2026-03-29)
  • Type checker: Transparent pass-through (verified 2026-03-29) — ExprKind::Unsafe(inner) => infer_expr(engine, arena, *inner) at ori_types/src/infer/expr/mod.rs:216

    • Set in_unsafe flag — NOT IMPLEMENTED
    • Allow unsafe operations only in context — NOT IMPLEMENTED (no uses Unsafe capability enforcement)
  • Evaluator: Execute unsafe blocks (verified 2026-03-29) — CanExpr::Unsafe(inner) => self.eval_can(inner) at ori_eval/src/interpreter/can_eval/mod.rs:337

    • Pointer dereference — NOT IMPLEMENTED (pointer ops do not exist yet)
    • Raw memory access — NOT IMPLEMENTED
  • ARC lowering: CanExpr::Unsafe(inner) => self.lower_expr(inner) at ori_arc/src/lower/expr/mod.rs:297 (verified 2026-03-29)

  • LLVM Support: LLVM codegen for unsafe blocks (transparent — same as eval)

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — unsafe blocks codegen (NOTE: file does not exist yet)

  • AOT Tests: No AOT coverage yet

  • Spec tests: tests/spec/capabilities/unsafe_block.ori — 6 tests pass (verified 2026-03-29) WEAK TESTS

    • test_unsafe_single_expr (verified 2026-03-29)
    • test_unsafe_multi_stmt (verified 2026-03-29)
    • test_unsafe_nested (verified 2026-03-29)
    • test_unsafe_in_block (verified 2026-03-29)
    • test_unsafe_type (str) (verified 2026-03-29)
    • test_unsafe_bool (verified 2026-03-29)
    • Capability enforcement tests — NEEDS TESTS
    • Compile-fail: unsafe ops outside unsafe block — NEEDS TESTS
    • Interaction with closures, loops, match — NEEDS TESTS

WEAK TESTS: Current tests only verify unsafe { expr } preserves expression type. No tests for capability enforcement (uses Unsafe), no tests for operations that should require unsafe context, no interaction tests. Tests are correct for what they verify but incomplete for safety enforcement.

  • Test: tests/compile-fail/ffi/unsafe_required.ori

    • Pointer deref outside unsafe
    • Unsafe op without unsafe block
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.4) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.4 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.4: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.5 FFI Capability

Spec section: spec/26-ffi.md § FFI Capability

Design

// FFI functions require FFI capability
@call_c_function () -> int uses FFI = some_c_function()

// Callers must have capability
@main () -> void uses FFI = {
    let result = call_c_function()
    print(msg: `Result: {result}`)
}

// Or provide it explicitly in tests
@test_c_call tests @call_c_function () -> void = {
    with FFI = AllowFFI in
        assert_eq(actual: call_c_function(), expected: 42)
}

Implementation

  • Spec: FFI capability definition

    • As a marker capability (like Async)
    • Propagation rules
    • Testing patterns
  • Capability system: Add FFI capability

    • Define in prelude
    • Track in function signatures
    • Propagate to callers
  • Type checker: Enforce capability requirement

    • Require uses FFI for extern calls
    • Require uses FFI for unsafe blocks
  • LLVM Support: LLVM codegen for FFI capability

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — FFI capability codegen

  • AOT Tests: No AOT coverage yet

  • Test: tests/spec/ffi/ffi_capability.ori

    • Function requiring FFI
    • Providing FFI in tests
    • Missing capability error
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.5) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.5 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.5: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.6 Callbacks (Native)

Spec section: spec/26-ffi.md § Callbacks

Syntax

extern "c" from "libc" {
    @qsort (
        base: [byte],
        nmemb: int,
        size: int,
        compar: (CPtr, CPtr) -> int
    ) -> void
}

@compare_ints (a: CPtr, b: CPtr) -> int uses FFI = ...
qsort(base: data, nmemb: len, size: 4, compar: compare_ints)

Implementation

  • Spec: Callback semantics

    • Function pointer type syntax
    • Conversion from Ori functions
    • Lifetime considerations
  • Types: Function pointer type

    • (CPtr, CPtr) -> int in type system
    • ABI specification
  • Codegen: Generate callback wrappers

    • Trampoline functions
    • ABI adaptation
  • LLVM Support: LLVM codegen for callbacks

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — callbacks codegen

  • AOT Tests: No AOT coverage yet

  • Test: tests/spec/ffi/callbacks.ori

    • Simple callback
    • qsort example
    • Callback with userdata
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.6) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.6 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.6: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.7 Build System Integration

Spec section: spec/26-ffi.md § Linking

ori.toml Configuration

[native]
libraries = ["m", "z", "pthread"]
library_paths = ["/usr/local/lib", "./native/lib"]

[native.linux]
libraries = ["m", "rt"]

[native.macos]
libraries = ["m"]
frameworks = ["Security", "Foundation"]

[native.windows]
libraries = ["msvcrt"]

Implementation

  • Spec: Link specification

    • ori.toml native section
    • Library kinds (static, dylib, framework)
    • Search paths
  • Codegen: Emit link directives

    • LLVM link metadata
    • Library search
  • Build system: Handle native dependencies

    • ori.toml parsing
    • pkg-config integration
  • LLVM Support: LLVM codegen for link directives

  • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — linking codegen

  • AOT Tests: No AOT coverage yet

  • Test: tests/spec/ffi/linking.ori

    • Link to libc
    • Link to libm
    • Custom library
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.7) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.7 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.7: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.8 compile_error Built-in

CROSS-REFERENCE: Section 13.10 also covers compile_error in the context of conditional compilation. Implementation should be done once and shared; avoid duplicate work.

Spec section: spec/annex-c-built-in-functions.md § Compile-Time Functions

Syntax

#target(arch: "wasm32")
compile_error("std.fs is not available for WASM.")

Implementation

  • Spec: Define compile_error semantics

    • Compile-time error with custom message
    • Works with conditional compilation
  • Parser: Parse compile_error

    • Built-in function syntax
    • String literal argument
  • Type checker: Trigger compile error

    • Evaluate during type checking
    • Only if code path is active
  • Test: tests/compile-fail/compile_error.ori

    • Basic compile_error
    • With conditional compilation
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.8) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.8 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.8: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.9 WASM Target (Section 2)

JS FFI

extern "js" {
    @_sin (x: float) -> float as "Math.sin"
    @_fetch (url: str) -> JsPromise<JsValue> as "fetch"
}

Implementation

  • Codegen: WASM code generation

    • WASM binary output
    • Import generation
  • Glue generation: Generate JS glue code

    • String marshalling (TextEncoder/TextDecoder)
    • Object heap slab
  • Test: tests/spec/ffi/js_ffi.ori

    • Basic JS function call
    • String marshalling
    • Object handles
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.9) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.9 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.9: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.10 JsValue and Async (Section 3-4)

JsValue Type

type JsValue = { _handle: int }

extern "js" {
    @_document_query (selector: str) -> JsValue
    @_drop_js_value (handle: JsValue) -> void
}

JsPromise with Implicit Resolution

extern "js" {
    @_fetch (url: str) -> JsPromise<JsValue> as "fetch"
}

// JsPromise auto-resolved at binding sites
@fetch_text (url: str) -> str uses Async, FFI =
    {
        let response = _fetch(url: url),  // auto-resolved
        text
    }

Implementation

  • Types: JsValue opaque handle type

    • Define in stdlib
    • Handle tracking
  • Types: JsPromise type

    • Compiler-recognized generic
    • Implicit resolution rules
  • Codegen: JSPI/Asyncify integration

    • Stack switching for async
    • Promise resolution glue
  • Test: tests/spec/ffi/js_async.ori

    • JsPromise implicit resolution
    • Async function with FFI
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.10) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.10 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.10: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


11.11 Deep FFI — Higher-Level FFI Abstractions

Proposal: proposals/approved/deep-ffi-proposal.md

Deep FFI layers five opt-in abstractions on top of the base FFI syntax: error protocols, ownership annotations, declarative marshalling, capability-gated testability, and const-generic safety. Each is independently useful and backward-compatible.

11.11.1 Error Protocols + out Parameters (Phase 1)

  • Implement: #error(errno | nonzero | null | negative | success: N | none) block/function attributes
    • Parser: Parse #error(...) on extern blocks and extern items
    • IR: Represent error protocol in extern block IR
    • Type checker: Transform return types when error protocol is active
    • Codegen: Generate error check + Result wrapping code after C calls
    • Rust Tests: ori_parse/src/tests/ — error protocol parsing
    • Ori Tests: tests/spec/ffi/error_protocol.ori
  • Implement: FfiError type in std.ffi
    • Library: Define FfiError type in library/std/ffi.ori
  • Implement: out parameter modifier (parser → IR → codegen)
    • Parser: Parse out modifier on extern params
    • IR: Represent out params in extern item IR
    • Type checker: Fold out params into return type
    • Codegen: Allocate stack slot, pass address, extract value
    • Rust Tests: ori_parse/src/tests/out param parsing
    • Ori Tests: tests/spec/ffi/out_params.ori
  • Implement: Errno reading infrastructure
    • Runtime: get_errno() as compiler intrinsic
    • Codegen: Read errno after C calls when #error(errno) active
    • LLVM Support: LLVM codegen for errno reading
    • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — errno codegen
    • AOT Tests: No AOT coverage yet

11.11.2 Ownership Annotations (Phase 2)

  • Implement: owned / borrowed annotations
    • Parser: Parse ownership annotations on extern params and return types
    • IR: Represent ownership in extern item IR
    • Type checker: Validate ownership combinations
    • Rust Tests: ori_parse/src/tests/ — ownership annotation parsing
    • Ori Tests: tests/spec/ffi/ownership.ori
  • Implement: #free(fn) attribute (block-level and per-function)
    • Parser: Parse #free(...) on extern blocks and items
    • Codegen: Wire up cleanup function
  • Implement: Auto-generated Drop impls for owned CPtr with #free
    • Type checker: Generate synthetic Drop impl for owned CPtr types
    • Codegen: Emit cleanup call on scope exit
    • LLVM Support: LLVM codegen for auto-Drop
    • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — ownership codegen
    • AOT Tests: No AOT coverage yet
  • Implement: str return defaults to borrowed (copy, don’t free)
    • Ori Tests: tests/spec/ffi/str_return_borrowed.ori
  • Implement: Compiler warnings for unannotated CPtr returns

11.11.3 Declarative Marshalling Extensions (Phase 3)

  • Implement: [byte] length elision — adjacent (ptr, len) pair insertion
    • Type checker: Detect [byte] params and expand to two C args
    • Codegen: Insert length argument at call site
    • Ori Tests: tests/spec/ffi/byte_length_elision.ori
  • Implement: mut [byte] parameter handling — adjacent (ptr, &len) pair
  • Implement: intc_int automatic narrowing/widening with bounds checks
  • Implement: boolc_int conversion
    • LLVM Support: LLVM codegen for marshalling extensions
    • LLVM Rust Tests: ori_llvm/tests/ffi_tests.rs — marshalling codegen
    • AOT Tests: No AOT coverage yet

11.11.4 Capability-Gated Testability (Phase 4)

  • Implement: Compiler generates internal traits from extern blocks
    • Type checker: Auto-generate trait from each extern block’s from clause
  • Implement: Parametric FFI("lib") capability
    • Type checker: Parse and validate parametric FFI capability
    • Backward compat: Unparameterized uses FFI remains shorthand for all
  • Implement: with FFI("lib") = handler { ... } in dispatch routing
    • Handler signature validation against generated traits
    • Stateless handler { ... } as sugar for handler(state: ()) { ... }
    • Fall-through to real C implementation for unmocked functions
    • Ori Tests: tests/spec/ffi/mock_handler.ori

11.11.5 Const-Generic Safety (Phase 5 — Future)

  • Design: Where clauses on extern items with const expressions

    • Depends on Section 18 (Const Generics) being complete
  • Implement: Buffer size validation at compile time

  • Implement: Fixed-capacity list [T, max N] at FFI boundary

  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (11.11) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-11.11 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 11.11: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


Section Completion Checklist

  • All items above have checkboxes marked [ ]
  • Spec file spec/26-ffi.md complete
  • CLAUDE.md updated with FFI syntax
  • grammar.ebnf updated with extern blocks
  • Can call libc functions (strlen, malloc, free)
  • Can call libm functions (sin, cos, sqrt)
  • Can create and use SQLite binding
  • All tests pass: ./test-all.sh
  • uses FFI properly enforced
  • unsafe blocks working
  • /tpr-review passed — independent Codex review found no critical or major issues (or all findings triaged)
  • /impl-hygiene-review passed — implementation hygiene review clean (phase boundaries, SSOT, algorithmic DRY, naming). MUST run AFTER /tpr-review is clean.
  • /improve-tooling retrospective completed — MANDATORY at section close, after both reviews are clean. Reflect on the section’s debugging journey (which diagnostics/ scripts you ran, which command sequences you repeated, where you added ad-hoc dbg!/tracing calls, where output was hard to interpret) and identify any tool/log/diagnostic improvement that would have made this section materially easier OR that would help the next section touching this area. Implement every accepted improvement NOW (zero deferral) and commit each via SEPARATE /commit-push. The retrospective is mandatory even when nothing felt painful — that is exactly when blind spots accumulate. See .claude/skills/improve-tooling/SKILL.md “Retrospective Mode” for the full protocol.

Exit Criteria: Can write a program that opens and queries a SQLite database


Example: SQLite Binding

Target capability demonstration:

extern "c" from "sqlite3" {
    @_sqlite3_open (filename: str, ppDb: CPtr) -> int as "sqlite3_open"
    @_sqlite3_close (db: CPtr) -> int as "sqlite3_close"
    @_sqlite3_exec (
        db: CPtr,
        sql: str,
        callback: (CPtr, int, CPtr, CPtr) -> int,
        userdata: CPtr,
        errmsg: CPtr
    ) -> int as "sqlite3_exec"
}

type SqliteDb = { handle: CPtr }

impl SqliteDb {
    pub @open (path: str) -> Result<SqliteDb, str> uses FFI =
        {
            let handle = CPtr.null()
            let result = _sqlite3_open(filename: path, ppDb: handle)
            if result == 0 then
                Ok(SqliteDb { handle: handle })
            else
                Err("Failed to open database")
        }

    pub @close (self) -> void uses FFI =
        _sqlite3_close(db: self.handle)
}

11.12 Post-FFI Sanitizer Tooling Upgrade

Depends on: 11.1-11.7 (working extern "c" from "lib" in AOT compilation)

Context: Section 08 of plans/llvm-verification-tooling implemented sanitizer integration (ASan/UBSan via Clang delegation) with a smoke test suite of 17 Ori programs. The suite includes a semantic pin — a test that deliberately triggers a heap-use-after-free to prove ASan detects real memory errors. However, the semantic pin currently uses a C-only workaround (tests/sanitizer/pin_helper.c compiled directly by scripts/sanitizer-smoke.sh) because Ori’s extern "c" from "lib" blocks don’t resolve in AOT compilation — the type checker rejects the extern function identifier before linking has a chance to find the library.

Once FFI is fully implemented (extern blocks resolve in AOT, CPtr operations work, from "lib" links local libraries), the sanitizer tooling should be upgraded to use Ori-native FFI instead of the C workaround.

Why this matters: The C-only semantic pin proves ASan works on the CI host but does NOT prove that Ori’s sanitizer pipeline (env var → SanitizerMode → Clang delegation → linker -fsanitize flags → ASan-instrumented ori_rt) correctly instruments Ori FFI calls. A Clang-compiled C program with ASan is a baseline that any CI host can pass. The real test — an Ori program that calls a C function through the full compilation pipeline and has ASan catch a UAF in the C code — requires working FFI.

Checklist

  • Restore Ori-native semantic pin — rewrite tests/sanitizer/semantic_pin_asan.ori to use extern "c" from "pin_helper" with the existing pin_helper.c:

    extern "c" from "pin_helper" {
        @_trigger_use_after_free () -> int as "trigger_use_after_free"
    }
    
    @main () -> int = {
        _trigger_use_after_free()
    }

    This program should compile via ori build with ORI_SANITIZE=address, link libpin_helper.a, and have ASan catch the UAF in the C code.

  • Update scripts/sanitizer-smoke.sh — the smoke script currently compiles pin_helper.c into a static library and runs it as a standalone C program. Once the Ori semantic pin compiles, change the script to:

    1. Build libpin_helper.a from pin_helper.c (same as now)
    2. Compile the Ori semantic pin via $ORI build --release tests/sanitizer/semantic_pin_asan.ori -o $TMPDIR/san_semantic_pin -L $PIN_LIB
    3. Run the resulting binary and verify ASan catches the UAF This validates the full pipeline: Ori source → type checker (resolves extern) → LLVM codegen → Clang delegation (adds sanitizer passes) → linker (links libpin_helper.a + ASan runtime) → binary with ASan active.
  • Add FFI-specific smoke tests — once extern blocks work in AOT, add these to tests/sanitizer/:

    • ffi_alloc_free.ori — call malloc/free via FFI, verify no leaks with ASan
    • ffi_string_pass.ori — pass Ori str to a C function via CPtr, verify no buffer overflow
    • ffi_callback.ori — pass an Ori closure as a C callback, verify the callback’s captures are correctly RC’d under ASan These exercise the FFI ↔ ARC interaction surface — the most likely source of sanitizer-detectable bugs.
  • Remove C-only workaround — once the Ori-native semantic pin is verified working in CI:

    1. Delete the C-only compilation path from sanitizer-smoke.sh
    2. Update pin_helper.c comment to note it’s linked via Ori FFI, not compiled standalone
    3. Update Section 08’s TPR block to note the upgrade
  • Update nightly-verification.yml — if any new smoke tests require library compilation, add the build step to the CI workflow.

  • Verify end-to-end — the upgraded semantic pin must:

    • Exit cleanly (0) when compiled WITHOUT ORI_SANITIZE
    • Exit with ASan error (heap-use-after-free report on stderr) when compiled WITH ORI_SANITIZE=address
    • Complete within the 60-second smoke budget

Bug Reference

This subsection exists because of BUG-04-074 (empty list literal unresolved type variables in AOT — tangentially related) and the broader issue that extern "c" from "lib" blocks don’t resolve in AOT mode. The latter is not yet filed as a standalone bug because it’s inherent to FFI being incomplete (Section 11.1 notes “typeck+codegen missing”). Once 11.1-11.7 are complete, the extern block resolution is a natural side-effect — no separate bug fix needed.

Cross-References

  • plans/llvm-verification-tooling/section-08-sanitizers.md — Section 08 implementation and TPR findings
  • tests/sanitizer/pin_helper.c — the C helper with the deliberate heap-use-after-free
  • scripts/sanitizer-smoke.sh — the smoke runner that handles the semantic pin

Verification Notes (2026-03-29)

  • Verified by: Claude Opus 4.6 (1M context)
  • Estimated completion at time of verification: ~30% infrastructure, ~0% end-to-end functionality
  • 11.1 parser/IR/formatter COMPLETE (20 Rust tests), typeck/codegen NEEDS IMPLEMENTATION
  • 11.3 parser/IR/typeck COMPLETE (17+ spec tests incl 14 compile-fail), codegen NEEDS IMPLEMENTATION
  • 11.4 parser/typeck/eval/ARC COMPLETE (6 spec tests), WEAK TESTS on capability enforcement
  • Zero E2E FFI capability — no Ori program can call a C function today
  • Stale plan annotation TPR-01-043 in ori_parse/src/grammar/attr/repr.rs:5
  • scripts/build-rt-asan.sh — builds the ASan-instrumented ori_rt