onejs-sl 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ir.ts CHANGED
@@ -64,12 +64,75 @@ export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INP
64
64
  /** Index into `Program.nodes`. Always refers backwards. */
65
65
  export type NodeRef = number
66
66
 
67
- export type SLNode =
68
- | { k: "const"; type: SLType; v: number[] }
69
- | { k: "input"; type: SLType; name: InputName }
70
- | { k: "uniform"; type: SLType; slot: number }
71
- | { k: "swizzle"; type: SLType; src: NodeRef; chans: number[] }
72
- | { k: "call"; type: SLType; op: SLOpCode; args: NodeRef[]; imm?: number[] }
67
+ /**
68
+ * What a value holds, beside its width. A node without one holds floats, which
69
+ * is every node before IR 4, so a program of floats is the graph and the hash
70
+ * it always was.
71
+ */
72
+ export type SLKind = "int" | "uint" | "bool"
73
+
74
+ /**
75
+ * A node that is a value. `param` is a loop's carried value at the top of an
76
+ * iteration, `proj` one result of an `if` or a `loop`.
77
+ */
78
+ export type ValueNode =
79
+ | { k: "const"; type: SLType; kind?: SLKind; v: number[] }
80
+ | { k: "input"; type: SLType; kind?: undefined; name: InputName }
81
+ | { k: "uniform"; type: SLType; kind?: undefined; slot: number }
82
+ | { k: "swizzle"; type: SLType; kind?: SLKind; src: NodeRef; chans: number[] }
83
+ | { k: "call"; type: SLType; kind?: SLKind; op: SLOpCode; args: NodeRef[]; imm?: number[] }
84
+ | { k: "param"; type: SLType; kind?: SLKind; loop: number; index: number }
85
+ | { k: "proj"; type: SLType; kind?: SLKind; src: NodeRef; index: number }
86
+
87
+ /**
88
+ * Structured control flow (IR 4), as regions of the one graph rather than as
89
+ * statements: the graph stays a pure DAG, hash consed and hashed as before, and
90
+ * an emitter places each node in the innermost region that uses it
91
+ * (`structure.ts`), which is what makes a branch skip what only it needs.
92
+ *
93
+ * if `then` or `else` are the results, per the bool `cond`; its `proj`s read them
94
+ * loop the `param`s of loop `id` start as `init`; while `cond` holds and fewer
95
+ * than `max` turns have run, they become `next`. Its `proj`s are the params
96
+ * when it stops. `cond` and `next` are computed from the params.
97
+ */
98
+ export type ControlNode =
99
+ | { k: "if"; cond: NodeRef; then: NodeRef[]; else: NodeRef[] }
100
+ | { k: "loop"; id: number; init: NodeRef[]; cond: NodeRef; next: NodeRef[]; max: number }
101
+
102
+ export type SLNode = ValueNode | ControlNode
103
+
104
+ /**
105
+ * A float to an int or a uint as the GPU converts one: as a 32 bit float,
106
+ * held to the widest such float the kind can hold, then truncated toward zero.
107
+ * The emitters clamp to these bounds, so a folded conversion
108
+ * and a computed one agree.
109
+ */
110
+ export function truncateHeld(v: number, kind: "int" | "uint"): number {
111
+ const [lo, hi] = INT_BOUNDS[kind]
112
+ return Math.trunc(Math.min(hi, Math.max(lo, Math.fround(v))))
113
+ }
114
+
115
+ /** The widest 32 bit floats an int and a uint hold, which a conversion clamps to first. */
116
+ export const INT_BOUNDS = { int: [-2147483648, 2147483520], uint: [0, 4294967040] } as const
117
+
118
+ /** The value node at `ref`. A control node is only ever read through its projs. */
119
+ export function valueAt(nodes: readonly SLNode[], ref: NodeRef): ValueNode {
120
+ const n = nodes[ref]
121
+ if (n === undefined || n.k === "if" || n.k === "loop") throw new SLError(`node ${ref} is not a value`)
122
+ return n
123
+ }
124
+
125
+ /** The refs a node reads, in order. */
126
+ export function operands(n: SLNode): NodeRef[] {
127
+ switch (n.k) {
128
+ case "swizzle": return [n.src]
129
+ case "call": return n.args
130
+ case "proj": return [n.src]
131
+ case "if": return [n.cond, ...n.then, ...n.else]
132
+ case "loop": return [...n.init, n.cond, ...n.next]
133
+ default: return []
134
+ }
135
+ }
73
136
 
74
137
  /**
75
138
  * How a host presents a uniform: a slider, a checkbox, a dropdown, a heading
@@ -254,6 +317,10 @@ export class Builder {
254
317
  readonly textures: TextureDecl[] = []
255
318
  private readonly interned = new Map<string, NodeRef>()
256
319
 
320
+ /** The next loop's id: unique within the program, so two loops' params never intern to one node. */
321
+ private loops = 0
322
+ newLoop(): number { return this.loops++ }
323
+
257
324
  add(node: SLNode): NodeRef {
258
325
  if (this.nodes.length >= MAX_NODES) {
259
326
  throw new SLError(`a program may hold at most ${MAX_NODES} operations`)
@@ -266,7 +333,7 @@ export class Builder {
266
333
  return ref
267
334
  }
268
335
 
269
- call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[]): NodeRef {
336
+ call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[], kind?: SLKind): NodeRef {
270
337
  const arity = SL_ARITY[op]
271
338
  if (arity >= 0 && args.length !== arity) {
272
339
  throw new SLError(`${SL_NAME[op]} takes ${arity} argument(s), got ${args.length}`)
@@ -276,14 +343,19 @@ export class Builder {
276
343
  throw new SLError(`${SL_NAME[op]} refers to a node that does not exist yet`)
277
344
  }
278
345
  }
279
- return this.add(imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm })
346
+ const node: SLNode = imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm }
347
+ if (kind !== undefined) node.kind = kind
348
+ return this.add(node)
280
349
  }
281
350
 
282
- constant(v: number[]): NodeRef {
351
+ constant(v: number[], kind?: SLKind): NodeRef {
283
352
  for (const n of v) {
284
353
  if (!Number.isFinite(n)) throw new SLError(`a constant must be finite, got ${n}`)
354
+ if (kind !== undefined && !Number.isInteger(n)) throw new SLError(`an ${kind} constant must be a whole number, got ${n}`)
285
355
  }
286
- return this.add({ k: "const", type: v.length as SLType, v: v.slice() })
356
+ const node: SLNode = { k: "const", type: v.length as SLType, v: v.slice() }
357
+ if (kind !== undefined) node.kind = kind
358
+ return this.add(node)
287
359
  }
288
360
 
289
361
  uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
@@ -348,9 +420,7 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
348
420
  const ref = stack.pop()!
349
421
  if (keep.has(ref)) continue
350
422
  keep.add(ref)
351
- const n = nodes[ref]
352
- if (n.k === "swizzle") stack.push(n.src)
353
- else if (n.k === "call") for (const a of n.args) stack.push(a)
423
+ for (const a of operands(nodes[ref]!)) stack.push(a)
354
424
  }
355
425
  // Ascending, which is still topological because a node only refers backwards.
356
426
  return [...keep].sort((x, y) => x - y)
@@ -372,12 +442,17 @@ export function inputsUsed(p: Program): InputName[] {
372
442
 
373
443
  /** Structural key for hash consing. Order matters and is fixed by the node shape. */
374
444
  function keyOf(n: SLNode): string {
445
+ const kind = "kind" in n && n.kind !== undefined ? `@${n.kind}` : ""
375
446
  switch (n.k) {
376
- case "const": return `c:${n.type}:${n.v.map(fixed).join(",")}`
447
+ case "const": return `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}${kind}`
377
448
  case "input": return `i:${n.name}`
378
449
  case "uniform": return `u:${n.slot}`
379
- case "swizzle": return `s:${n.src}:${n.chans.join("")}`
380
- case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
450
+ case "swizzle": return `s:${n.src}:${n.chans.join("")}${kind}`
451
+ case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}${kind}`
452
+ case "param": return `p:${n.loop}:${n.index}`
453
+ case "proj": return `j:${n.src}:${n.index}`
454
+ case "if": return `?:${n.cond}:${n.then.join(",")}:${n.else.join(",")}`
455
+ case "loop": return `L:${n.max}:${n.init.join(",")}:${n.cond}:${n.next.join(",")}`
381
456
  }
382
457
  }
383
458
 
@@ -390,6 +465,11 @@ export function fixed(n: number): string {
390
465
  return Object.is(n, -0) ? "0" : n.toPrecision(9)
391
466
  }
392
467
 
468
+ /** A constant's value as keyed and hashed: a float at `fixed`'s precision, an int, a uint or a bool exactly. */
469
+ function constText(n: number, kind: SLKind | undefined): string {
470
+ return kind === undefined ? fixed(n) : String(n)
471
+ }
472
+
393
473
  export function widthName(t: SLType): string {
394
474
  return t === 1 ? "float" : `vec${t}`
395
475
  }
@@ -431,14 +511,33 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
431
511
  // Only where every backend computes the same bits either way: min and max
432
512
  // are left out, since which of -0 and +0 they return depends on the order.
433
513
  const digest = new Map<NodeRef, string>()
514
+ const depth = loopDepths(nodes, result)
434
515
 
435
516
  const of = (ref: NodeRef): string => {
436
517
  const seen = digest.get(ref)
437
518
  if (seen !== undefined) return seen
438
- const n = nodes[ref]
519
+ const n = nodes[ref]!
439
520
  let body: string
440
521
  switch (n.k) {
441
- case "const": body = `c:${n.type}:${n.v.map(fixed).join(",")}`; break
522
+ // Control nodes have no type of their own: their projs carry it.
523
+ case "if": {
524
+ body = `?:${of(n.cond)}:${n.then.map(of).join(",")}:${n.else.map(of).join(",")}`
525
+ const d = fnv1a(body)
526
+ digest.set(ref, d)
527
+ return d
528
+ }
529
+ case "loop": {
530
+ body = `L:${n.max}:${n.init.map(of).join(",")}:${of(n.cond)}:${n.next.map(of).join(",")}`
531
+ const d = fnv1a(body)
532
+ digest.set(ref, d)
533
+ return d
534
+ }
535
+ // By nesting depth, not by id: two programs that nest the same
536
+ // loops the same way agree however their ids were handed out, and
537
+ // two loops nested in one another never share a digest for a param.
538
+ case "param": body = `p:${depth.get(n.loop) ?? 1}:${n.index}`; break
539
+ case "proj": body = `j:${of(n.src)}:${n.index}`; break
540
+ case "const": body = `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}`; break
442
541
  case "input": body = `i:${n.name}`; break
443
542
  // By slot rather than by name: the slot is what the generated shader
444
543
  // lays out, so two programs whose uniforms differ only in name still
@@ -447,8 +546,8 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
447
546
  case "uniform": body = `u:${n.slot}:${n.type}`; break
448
547
  case "swizzle": {
449
548
  const src = nodes[n.src]!
450
- body = src.k === "const"
451
- ? `c:${n.type}:${n.chans.map((c) => fixed(src.v[c]!)).join(",")}`
549
+ body = src.k === "const" && src.kind === n.kind
550
+ ? `c:${n.type}:${n.chans.map((c) => constText(src.v[c]!, src.kind)).join(",")}`
452
551
  : `s:${of(n.src)}:${n.chans.join("")}`
453
552
  break
454
553
  }
@@ -459,7 +558,7 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
459
558
  break
460
559
  }
461
560
  }
462
- const d = fnv1a(body + "|" + n.type)
561
+ const d = fnv1a(body + "|" + n.type + (n.kind === undefined ? "" : n.kind))
463
562
  digest.set(ref, d)
464
563
  return d
465
564
  }
@@ -473,12 +572,44 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
473
572
  // includes the library's text when it compiles, and none caches a compiled
474
573
  // program by hash across a library update, so the new text reaches every
475
574
  // shader without a bump.
476
- const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes)}`, of(result)]
575
+ const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result)}`, of(result)]
477
576
  for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
478
577
  for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
479
578
  return fnv1a(parts.join("|"))
480
579
  }
481
580
 
581
+ /**
582
+ * Each reachable loop's nesting depth, 1 for a loop inside no other: 1 more than
583
+ * the deepest loop whose params it reads. Read from the graph, so it does not
584
+ * depend on the order anything was built or walked in.
585
+ */
586
+ function loopDepths(nodes: readonly SLNode[], result: NodeRef): Map<number, number> {
587
+ const free = new Map<NodeRef, Set<number>>()
588
+ const loops = new Map<number, Extract<SLNode, { k: "loop" }>>()
589
+ const freeOf = (ref: NodeRef): Set<number> => free.get(ref) ?? new Set()
590
+ for (const ref of reachable(nodes as SLNode[], result)) {
591
+ const n = nodes[ref]!
592
+ const out = new Set<number>()
593
+ if (n.k === "param") out.add(n.loop)
594
+ for (const a of operands(n)) for (const l of freeOf(a)) out.add(l)
595
+ if (n.k === "loop") { out.delete(n.id); loops.set(n.id, n) }
596
+ free.set(ref, out)
597
+ }
598
+ const loopFree = new Map<number, Set<number>>()
599
+ for (const [ref, n] of nodes.entries()) if (n.k === "loop" && free.has(ref)) loopFree.set(n.id, free.get(ref)!)
600
+ const depth = new Map<number, number>()
601
+ const of = (id: number): number => {
602
+ const seen = depth.get(id)
603
+ if (seen !== undefined) return seen
604
+ let d = 1
605
+ for (const outer of loopFree.get(id) ?? []) d = Math.max(d, of(outer) + 1)
606
+ depth.set(id, d)
607
+ return d
608
+ }
609
+ for (const id of loops.keys()) of(id)
610
+ return depth
611
+ }
612
+
482
613
  /** The control fields of `c` that are set, copied, in `CONTROL_FIELDS` order. */
483
614
  export function controlOf(c: UniformControl): UniformControl {
484
615
  const out: UniformControl = {}
@@ -500,20 +631,32 @@ export function controlOf(c: UniformControl): UniformControl {
500
631
  * predates the bump still reads it. Every node counts, used or not: the JSON
501
632
  * carries them all, and an older reader has to refuse by the version rather
502
633
  * than halfway through the nodes.
634
+ *
635
+ * Only the nodes the result reads count: lowering leaves dead nodes behind (an
636
+ * int constant a fold used up, say), and they draw nothing. `toJSON` writes
637
+ * only the live ones, so an older reader never meets a dead newer node.
503
638
  */
504
- export function programVersion(nodes: readonly SLNode[]): number {
639
+ export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
505
640
  let v = 2
506
- for (const n of nodes) v = Math.max(v, nodeVersion(n))
641
+ for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
507
642
  return v
508
643
  }
509
644
 
645
+ /** The ops IR 4 added. */
646
+ const IR4_OPS = new Set<number>([
647
+ SLOP.CAST, SLOP.LT, SLOP.LE, SLOP.GT, SLOP.GE, SLOP.EQ, SLOP.NE, SLOP.AND, SLOP.OR, SLOP.NOT,
648
+ SLOP.BIT_AND, SLOP.BIT_OR, SLOP.BIT_XOR, SLOP.BIT_NOT, SLOP.SHL, SLOP.SHR, SLOP.CHOOSE,
649
+ ])
650
+
510
651
  /**
511
652
  * The version that added this node's form. An sdf or octave call with one
512
653
  * operand is the form before IR 3, which only a file an older compiler wrote
513
654
  * holds, and `fromJSON` moves it to the form IR 3 has.
514
655
  */
515
656
  function nodeVersion(n: SLNode): number {
657
+ if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
516
658
  if (n.k !== "call") return 2
659
+ if (IR4_OPS.has(n.op)) return 4
517
660
  switch (n.op) {
518
661
  case SLOP.SAMPLE_LOD: return 3
519
662
  case SLOP.SDF:
@@ -537,7 +680,7 @@ function nodeVersion(n: SLNode): number {
537
680
  * one immediate
538
681
  */
539
682
  export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
540
- const widths = n.args.map((a) => nodes[a]!.type).join(",")
683
+ const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
541
684
  switch (n.op) {
542
685
  case SLOP.SDF:
543
686
  if (widths === "2,4,2" && n.imm?.length === 1) return null
@@ -588,8 +731,9 @@ export const SL_HASH_VERSION = 2
588
731
  * 2 an SDF call carries up to six shape parameters, not four (#129)
589
732
  * 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
590
733
  * operands, where they were immediates
734
+ * 4 control flow (if, loop, their params and projs), ints, uints and bools
591
735
  */
592
- export const SL_IR_VERSION = 3
736
+ export const SL_IR_VERSION = 4
593
737
 
594
738
  function fnv1a(s: string): string {
595
739
  let h = 0x811c9dc5
package/src/lang/ast.ts CHANGED
@@ -11,41 +11,71 @@
11
11
  * pass over the source.
12
12
  */
13
13
 
14
+ import type { SLKind } from "../ir"
14
15
  import type { Pos } from "./lexer"
15
16
 
16
- /** The four value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
17
- export type TypeName = "float" | "float2" | "float3" | "float4"
17
+ /** The value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
18
+ export type TypeName = "float" | "float2" | "float3" | "float4" | "int" | "uint" | "bool"
18
19
 
19
20
  export const TYPE_WIDTH: Record<TypeName, 1 | 2 | 3 | 4> = {
20
- float: 1, float2: 2, float3: 3, float4: 4,
21
+ float: 1, float2: 2, float3: 3, float4: 4, int: 1, uint: 1, bool: 1,
22
+ }
23
+
24
+ /** What a type holds beside its width; the floats hold nothing else. */
25
+ export const TYPE_KIND: Record<TypeName, SLKind | undefined> = {
26
+ float: undefined, float2: undefined, float3: undefined, float4: undefined, int: "int", uint: "uint", bool: "bool",
21
27
  }
22
28
 
23
29
  export type BinaryOp =
24
30
  | "+" | "-" | "*" | "/" | "%"
25
31
  | "<" | "<=" | ">" | ">=" | "==" | "!="
26
32
  | "&&" | "||"
33
+ | "&" | "|" | "^" | "<<" | ">>"
27
34
 
28
35
  export type Expr =
29
- | { k: "num"; value: number; pos: Pos }
36
+ /**
37
+ * `whole` is a number written with no point and no exponent, `3` not `3.0`:
38
+ * a float, except beside an int, where it is an int. `unsigned` is `3u`.
39
+ */
40
+ | { k: "num"; value: number; whole: boolean; unsigned?: boolean; pos: Pos }
41
+ | { k: "bool"; value: boolean; pos: Pos }
30
42
  | { k: "hex"; hex: string; pos: Pos }
31
43
  | { k: "ident"; name: string; pos: Pos }
32
44
  /** A swizzle, or the shape half of `sdf.circle`. Which one is decided later. */
33
45
  | { k: "member"; obj: Expr; name: string; pos: Pos }
34
46
  | { k: "call"; callee: Expr; args: Expr[]; pos: Pos }
35
- | { k: "unary"; op: "-" | "+" | "!"; arg: Expr; pos: Pos }
47
+ | { k: "unary"; op: "-" | "+" | "!" | "~"; arg: Expr; pos: Pos }
36
48
  | { k: "binary"; op: BinaryOp; a: Expr; b: Expr; pos: Pos }
37
49
  | { k: "cond"; cond: Expr; then: Expr; else: Expr; pos: Pos }
38
50
 
39
- export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%="
51
+ export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
52
+
53
+ /** One `case` group of a switch: its labels, `null` for `default`, and its body without the closing break. */
54
+ export interface SwitchCase {
55
+ labels: Array<Expr | null>
56
+ body: Stmt[]
57
+ /** Whether it ended in a break, which is dropped from `body`. */
58
+ closed: boolean
59
+ pos: Pos
60
+ }
40
61
 
41
62
  export type Stmt =
42
63
  | { k: "var"; type: TypeName; name: string; init: Expr; pos: Pos }
43
64
  /** A compile time constant: usable as a value and as a `for` bound. */
44
65
  | { k: "const"; type: TypeName; name: string; init: Expr; pos: Pos }
66
+ /** `x++` is `x += 1`, with the 1 whole. */
45
67
  | { k: "assign"; target: Expr; op: AssignOp; value: Expr; pos: Pos }
46
68
  | { k: "if"; cond: Expr; then: Stmt[]; else: Stmt[]; pos: Pos }
47
- | { k: "for"; counter: string; from: Expr; to: Expr; inclusive: boolean; step: Expr; body: Stmt[]; pos: Pos }
69
+ /** `for (type counter = from; cond; update)`: the update changes the counter and nothing else. */
70
+ | { k: "for"; type: TypeName; counter: string; from: Expr; cond: Expr; update: Extract<Stmt, { k: "assign" }>; body: Stmt[]; pos: Pos }
71
+ | { k: "while"; cond: Expr; body: Stmt[]; pos: Pos }
72
+ | { k: "break"; pos: Pos }
73
+ | { k: "continue"; pos: Pos }
74
+ /** Cases never fall through: each ends in break, return or continue, and the break is dropped here. */
75
+ | { k: "switch"; value: Expr; cases: SwitchCase[]; pos: Pos }
48
76
  | { k: "return"; value: Expr; pos: Pos }
77
+ /** `{ ... }` on its own: a body with its own scope, as a case's braces are. */
78
+ | { k: "block"; body: Stmt[]; pos: Pos }
49
79
 
50
80
  export interface Param {
51
81
  type: TypeName