mirror of
https://github.com/aphrody-labs/opentui.git
synced 2026-10-17 12:46:32 +00:00
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.
80 lines
3.7 KiB
TypeScript
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,
|
|
)
|
|
})
|