Skip to content

Commit 88d2168

Browse files
committed
♻️ rename Op discriminant to directive, open() name to id
Restructures the Op type so the discriminant field is `directive` instead of `id`, freeing `id` for the element identity string. The `name` parameter on `open()` and `OpenElement` becomes `id`, aligning with Clay's terminology. Drops the `idx++` seed from Clay_HashString — element IDs are now hashed with seed 0 and uniqueness is the caller's responsibility. Duplicate IDs within a frame are undefined behavior. Removes INV-7 (element identity disambiguation) from the spec and updates Sections 8.3.1, 9.3, and 10.3 accordingly. Closes #16
1 parent 9adb34b commit 88d2168

6 files changed

Lines changed: 58 additions & 79 deletions

File tree

AGENTS.md

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,34 @@
22

33
## Spec-driven development
44

5-
- Specs in `specs/` are the source of truth. Code conforms to specs,
6-
not the other way around.
5+
- Specs in `specs/` are the source of truth. Code conforms to specs, not the
6+
other way around.
77

8-
- Never add, remove, or change a public API in code without first
9-
updating the relevant spec and getting explicit approval from the
10-
user. This includes changes to `Term`, `createTerm`, `render()`,
11-
directive constructors (`open`, `close`, `text`), sizing helpers
12-
(`grow`, `fixed`, `fit`), and any future spec'd interfaces.
8+
- Never add, remove, or change a public API in code without first updating the
9+
relevant spec and getting explicit approval from the user. This includes
10+
changes to `Term`, `createTerm`, `render()`, directive constructors (`open`,
11+
`close`, `text`), sizing helpers (`grow`, `fixed`, `fit`), and any future
12+
spec'd interfaces.
1313

14-
- The workflow is: propose the spec change, wait for approval,
15-
then implement. Do not combine spec changes with implementation
16-
in a single step.
14+
- The workflow is: propose the spec change, wait for approval, then implement.
15+
Do not combine spec changes with implementation in a single step.
1716

18-
- The renderer and input parser are specified separately
19-
(`renderer-spec.md` and `input-spec.md`). They are architecturally
20-
independent. Do not introduce dependencies between them.
17+
- The renderer and input parser are specified separately (`renderer-spec.md` and
18+
`input-spec.md`). They are architecturally independent. Do not introduce
19+
dependencies between them.
2120

22-
- Each test file tests exactly one spec. Do not put tests for one
23-
spec into another spec's test file.
21+
- Each test file tests exactly one spec. Do not put tests for one spec into
22+
another spec's test file.
2423

2524
## Rendering invariants
2625

27-
- The renderer MUST NOT perform IO. It produces bytes; the caller
28-
writes them.
26+
- The renderer MUST NOT perform IO. It produces bytes; the caller writes them.
2927

30-
- The renderer MUST NOT manage terminal state (alternate buffer,
31-
cursor visibility, mouse reporting, keyboard protocol modes).
28+
- The renderer MUST NOT manage terminal state (alternate buffer, cursor
29+
visibility, mouse reporting, keyboard protocol modes).
3230

33-
- Each frame is a complete snapshot. The renderer carries no UI tree
34-
state between frames — only cell buffers for diffing.
31+
- Each frame is a complete snapshot. The renderer carries no UI tree state
32+
between frames — only cell buffers for diffing.
3533

36-
- Directives are plain objects. No classes, no methods, no prototype
37-
chains. The flat array pattern is normative.
34+
- Directives are plain objects. No classes, no methods, no prototype chains. The
35+
flat array pattern is normative.

ops.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ const PROP_BORDER = 0x08;
1111
const PROP_CLIP = 0x10;
1212
const PROP_FLOATING = 0x20;
1313

14-
/* ── Packing ──────────────────────────────────────────────────────── */
15-
1614
const encoder = new TextEncoder();
1715

1816
function packAxis(view: DataView, offset: number, axis: SizingAxis): number {
@@ -73,18 +71,18 @@ export function pack(
7371
let o = offset;
7472

7573
for (let op of ops) {
76-
switch (op.id) {
74+
switch (op.directive) {
7775
case OP_CLOSE_ELEMENT:
78-
view.setUint32(o, op.id, true);
76+
view.setUint32(o, op.directive, true);
7977
o += 4;
8078
break;
8179

8280
case OP_OPEN_ELEMENT: {
8381
view.setUint32(o, OP_OPEN_ELEMENT, true);
8482
o += 4;
8583

86-
let id = encoder.encode(op.name);
87-
o = packString(view, id, o);
84+
let bytes = encoder.encode(op.id);
85+
o = packString(view, bytes, o);
8886

8987
let mask = 0;
9088
if (op.layout) mask |= PROP_LAYOUT;
@@ -210,15 +208,11 @@ export function pack(
210208
return (o - offset) / 4;
211209
}
212210

213-
/* ── Color ────────────────────────────────────────────────────────── */
214-
215211
export function rgba(r: number, g: number, b: number, a = 255): number {
216212
return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) |
217213
(b & 0xFF);
218214
}
219215

220-
/* ── Sizing axis types ────────────────────────────────────────────── */
221-
222216
export type SizingAxis =
223217
| { type: "fit"; min?: number; max?: number }
224218
| { type: "grow"; min?: number; max?: number }
@@ -241,15 +235,13 @@ export const percent = (value: number): SizingAxis => ({
241235
});
242236
export const fixed = (value: number): SizingAxis => ({ type: "fixed", value });
243237

244-
/* ── Op descriptors ───────────────────────────────────────────────── */
245-
246238
export interface CloseElement {
247-
id: typeof OP_CLOSE_ELEMENT;
239+
directive: typeof OP_CLOSE_ELEMENT;
248240
}
249241

250242
export interface OpenElement {
251-
id: typeof OP_OPEN_ELEMENT;
252-
name: string;
243+
directive: typeof OP_OPEN_ELEMENT;
244+
id: string;
253245
layout?: {
254246
width?: SizingAxis;
255247
height?: SizingAxis;
@@ -280,7 +272,7 @@ export interface OpenElement {
280272
}
281273

282274
export interface Text {
283-
id: typeof OP_TEXT;
275+
directive: typeof OP_TEXT;
284276
content: string;
285277
color?: number;
286278
fontSize?: number;
@@ -291,22 +283,20 @@ export interface Text {
291283

292284
export type Op = OpenElement | Text | CloseElement;
293285

294-
/* ── Descriptor constructors ──────────────────────────────────────── */
295-
296286
export function open(
297-
name: string,
298-
props: Omit<OpenElement, "id" | "name"> = {},
287+
id: string,
288+
props: Omit<OpenElement, "directive" | "id"> = {},
299289
): OpenElement {
300-
return { id: OP_OPEN_ELEMENT, name, ...props };
290+
return { directive: OP_OPEN_ELEMENT, id, ...props };
301291
}
302292

303293
export function text(
304294
content: string,
305-
props: Omit<Text, "id" | "content"> = {},
295+
props: Omit<Text, "directive" | "content"> = {},
306296
): Text {
307-
return { id: OP_TEXT, content, ...props };
297+
return { directive: OP_TEXT, content, ...props };
308298
}
309299

310300
export function close(): CloseElement {
311-
return { id: OP_CLOSE_ELEMENT };
301+
return { directive: OP_CLOSE_ELEMENT };
312302
}

specs/renderer-spec.md

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,7 @@ stages. The caller MUST NOT need to perform any of these operations.
225225
symmetric: both calls occur within the same render transaction, in the same
226226
function scope.
227227

228-
**INV-7. Element identity disambiguation.** When multiple elements within a
229-
frame share the same id, the renderer MUST disambiguate their identities so that
230-
the layout engine does not conflate them. The disambiguation mechanism is an
231-
implementation detail, but the guarantee is normative: identical ids MUST NOT
232-
cause layout corruption or element conflation.
233-
234-
**INV-8. Separation of concerns.** The rendering concern and the input-parsing
228+
**INV-7. Separation of concerns.** The rendering concern and the input-parsing
235229
concern MUST remain independent. Neither MUST depend on the other's state,
236230
types, or API surface. They MAY share a compiled WASM binary for loading
237231
efficiency, but this is an implementation convenience, not an architectural
@@ -374,7 +368,8 @@ open(id: string, props?): OpenElement
374368

375369
Creates an element-open directive. The `id` parameter provides an identity for
376370
the element within the frame, used by the underlying layout engine for element
377-
tracking and hit-testing. The optional `props` parameter carries configuration
371+
tracking and hit-testing. IDs MUST be unique within a frame; passing duplicate
372+
IDs is undefined behavior. The optional `props` parameter carries configuration
378373
for layout, styling, and behavior.
379374

380375
Elements opened with `open()` MUST be closed with a corresponding `close()`
@@ -473,12 +468,10 @@ transfer mechanism is an implementation detail described in Section 12.1.
473468

474469
### 9.3 Directive identity
475470

476-
Each element directive is assigned an identity within the frame for use by the
477-
underlying layout engine. When multiple elements share the same id (the `id`
478-
parameter to `open()`), the renderer MUST disambiguate their identities
479-
automatically. The disambiguation mechanism is an implementation detail. The
480-
normative requirement is that the caller MUST NOT need to provide globally
481-
unique ids; the renderer handles uniqueness internally.
471+
Each element directive carries an `id` provided by the caller via `open()`.
472+
Element IDs MUST be unique within a frame. The renderer uses the ID directly as
473+
the element's identity for the layout engine. Passing duplicate IDs within a
474+
single frame is undefined behavior.
482475

483476
---
484477

@@ -501,10 +494,9 @@ renderer processes directives in the order they appear in the array.
501494

502495
### 10.3 Element identity within a frame
503496

504-
Within a single frame, each element MUST have an unambiguous identity for the
505-
layout engine. As specified in Section 9.3, the renderer handles disambiguation.
506-
Two elements with the same id in the same frame MUST NOT cause layout
507-
corruption, hash collision, or identity conflation.
497+
Within a single frame, each element MUST have a unique identity for the layout
498+
engine. As specified in Section 9.3, element IDs MUST be unique within a frame.
499+
Passing duplicate IDs is undefined behavior.
508500

509501
### 10.4 No cross-frame identity
510502

src/clayterm.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,6 @@ struct Clayterm *init(void *mem, int w, int h) {
437437

438438
void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {
439439
int i = 0;
440-
uint32_t idx = 0;
441440

442441
Clay_BeginLayout();
443442

@@ -454,7 +453,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {
454453

455454
if (id_len > 0) {
456455
Clay_String str = {.length = (int32_t)id_len, .chars = id_chars};
457-
Clay_ElementId eid = Clay__HashString(str, idx++);
456+
Clay_ElementId eid = Clay__HashString(str, 0);
458457
Clay__OpenElementWithId(eid);
459458
} else {
460459
Clay__OpenElement();

test/validate.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@ describe("validate", () => {
1919
expect(validate([])).toBe(true);
2020
});
2121

22-
it("rejects ops with wrong id", () => {
23-
expect(validate([{ id: 0xff }])).toBe(false);
22+
it("rejects ops with wrong directive", () => {
23+
expect(validate([{ directive: 0xff }])).toBe(false);
2424
});
2525

26-
it("rejects open element missing name", () => {
27-
expect(validate([{ id: 0x02 }])).toBe(false);
26+
it("rejects open element missing id", () => {
27+
expect(validate([{ directive: 0x02 }])).toBe(false);
2828
});
2929

3030
it("rejects text missing content", () => {
31-
expect(validate([{ id: 0x03 }])).toBe(false);
31+
expect(validate([{ directive: 0x03 }])).toBe(false);
3232
});
3333

3434
it("rejects non-array", () => {
@@ -40,7 +40,7 @@ describe("validate", () => {
4040
});
4141

4242
it("assert throws TypeError on bad input", () => {
43-
expect(() => assert([{ id: 0x02 }])).toThrow(TypeError);
43+
expect(() => assert([{ directive: 0x02 }])).toThrow(TypeError);
4444
});
4545

4646
it("rejects padding > 255 (u8 overflow)", () => {
@@ -106,6 +106,6 @@ describe("validated", () => {
106106

107107
it("throws on invalid ops", () => {
108108
// deno-lint-ignore no-explicit-any
109-
expect(() => term.render([{ id: 0xff }] as any)).toThrow(TypeError);
109+
expect(() => term.render([{ directive: 0xff }] as any)).toThrow(TypeError);
110110
});
111111
});

validate.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ const Floating = Type.Object({
8989
zIndex: Type.Optional(u16),
9090
});
9191

92-
/* ── Op types (discriminated on `id`) ─────────────────────────────── */
92+
/* ── Op types (discriminated on `directive`) ──────────────────────── */
9393

94-
const CloseElement = Type.Object({ id: Type.Literal(0x04) });
94+
const CloseElement = Type.Object({ directive: Type.Literal(0x04) });
9595

9696
const OpenElement = Type.Object({
97-
id: Type.Literal(0x02),
98-
name: Type.String(),
97+
directive: Type.Literal(0x02),
98+
id: Type.String(),
9999
layout: Type.Optional(Layout),
100100
bg: Type.Optional(rgba),
101101
cornerRadius: Type.Optional(CornerRadius),
@@ -105,7 +105,7 @@ const OpenElement = Type.Object({
105105
});
106106

107107
const TextOp = Type.Object({
108-
id: Type.Literal(0x03),
108+
directive: Type.Literal(0x03),
109109
content: Type.String(),
110110
color: Type.Optional(rgba),
111111
fontSize: Type.Optional(u8),

0 commit comments

Comments
 (0)