Files
Simon Klee c1bb64ba19 native: move the render tree into Zig (#1479)
TypeScript used to own the renderable tree. Each frame walked it in
JavaScript, read layout back from Yoga, and made one FFI call per draw,
so frame cost grew with node and draw count. Native objects lived in
global registries and crossed FFI as raw pointers, which made renderer
teardown fragile.

A Context-owned Scene now holds the retained tree, its Yoga nodes, and
its paint state. Renderables stage property changes and flush them in
one call per frame. Zig lays out, culls, and paints the built-in
widgets, fills the hit grid, and hands the frame to Session for the
terminal diff. A frame without paint hooks completes in one step call.
Paint hooks run in one batch before painting and record into a byte
stream that native replays in paint order, so painting never pauses
for JavaScript and a hook cannot tear the frame.

Every native resource sits behind a generational handle, so a stale
handle fails with an error instead of touching freed memory, and
destroying a Context releases everything it owns.
2026-10-08 11:47:53 +02:00

80 lines
3.7 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import type { AddressFields, PointerPolicies } from "./native-abi-pointers.js"
import { compileHeader, generateNativeABI, verifyNativeABI, type HeaderABI } from "./native-abi.js"
// `bun run check:abi` checks the committed output for the real header before `bun test` runs.
const buffer = { ffi: "buffer", nullable: "never", retention: "call", source: "view" } as const
const callback = { ffi: "ptr", nullable: "optional", retention: "call", source: "callback" } as const
function syntheticABI(): { abi: HeaderABI; policies: PointerPolicies; addresses: AddressFields } {
return {
abi: {
symbols: { ot_f: { args: ["*const u8", "callback(u32)->void", "u32"], returns: "i32" } },
callbacks: {},
layouts: {
ot_r: { size: 8, alignment: 8, fields: { address: { offset: 0, size: 8, alignment: 8, type: "u64" } } },
},
constants: { OT_STYLE_ENUM_A: 0, OT_STYLE_ENUM_A_MAX: 2 },
},
policies: { ot_f: { 0: buffer, 1: callback } },
addresses: {
ot_r: { address: { type: "u64", nullable: false, lifetime: "lease", release: "ot_f", element: "u8" } },
},
}
}
type Mutation = (input: ReturnType<typeof syntheticABI>) => void
describe("checked native ABI generation", () => {
test("a synthetic ABI generates, and stale output rejects", async () => {
const { abi, policies, addresses } = syntheticABI()
expect(await generateNativeABI(abi, policies, addresses)).toContain(
'ot_f: { args: ["buffer", "ptr", "u32"], returns: "i32" }',
)
expect(() => verifyNativeABI("stale")).toThrow("native-abi.generated.ts is stale")
})
test.each<[string, Mutation]>([
["Unsupported FFI type: ot_f returns: bool", ({ abi }) => (abi.symbols.ot_f!.returns = "bool")],
["Missing pointer policy: ot_f argument 0", ({ policies }) => delete policies.ot_f![0]],
["must use buffer: ot_f argument 0", ({ policies }) => (policies.ot_f![0] = { ...buffer, ffi: "ptr" })],
["must use ptr: ot_f argument 0", ({ policies }) => (policies.ot_f![0] = { ...buffer, nullable: "empty" })],
[
"Callback policy mismatch: ot_f argument 1",
({ policies }) => (policies.ot_f![1] = { ...callback, source: "view" }),
],
[
"Pointer returns must use ptr: ot_f argument returns",
({ abi, policies }) => {
abi.symbols.ot_f!.returns = "*u8"
policies.ot_f!.returns = buffer
},
],
["Unused pointer policy: ot_g argument 0", ({ policies }) => (policies.ot_g = { 0: buffer })],
["explicit ownership policy: ot_r.address", ({ abi }) => (abi.layouts.ot_r!.fields.address!.type = "*u8")],
["Address field drift: ot_r.address", ({ abi }) => (abi.layouts.ot_r!.fields.address!.type = "u32")],
["needs bigint support: OT_BIG", ({ abi }) => (abi.constants.OT_BIG = 2 ** 53)],
["Missing style enum constraint: OT_STYLE_ENUM_A", ({ abi }) => delete abi.constants.OT_STYLE_ENUM_A_MAX],
["Missing style enum constraint: OT_STYLE_ENUM_B", ({ abi }) => (abi.constants.OT_STYLE_ENUM_B = 2)],
])("generation rejects %s", async (message, mutate) => {
const input = syntheticABI()
mutate(input)
await expect(generateNativeABI(input.abi, input.policies, input.addresses)).rejects.toThrow(message)
})
test.each([
// Translate-C ignores #pragma pack, so only the C compiler sees this 12-byte layout.
[
"C layout differs from Translate-C: ot_z",
"#include <stdint.h>\n#pragma pack(push, 2)\ntypedef struct ot_z { uint32_t a; uint64_t b; } ot_z;\n#pragma pack(pop)",
],
["-Werror,-Wcomment", "/* match a/*.txt */"],
])(
"header compilation rejects %s",
(message, header) => {
expect(() => compileHeader({ header })).toThrow(message)
},
120_000,
)
})