onejs-sl 0.4.0 → 0.6.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.
@@ -13,7 +13,7 @@
13
13
 
14
14
  import {
15
15
  type AssignOp, type Attribute, type AttributeArg, type BinaryOp, type Expr, type FuncDecl, type Param, type Stmt,
16
- type TextureDecl, type TypeName, type UniformDecl, type Unit,
16
+ type SwitchCase, type TextureDecl, type TypeName, type UniformDecl, type Unit,
17
17
  } from "./ast"
18
18
  import { SLParseError, tokenize, type Pos, type SLFix, type Token } from "./lexer"
19
19
  import { KEYWORD_SET, TYPE_SET } from "./words"
@@ -29,9 +29,16 @@ const NOT_A_TYPE: Record<string, string> = {
29
29
  vec2: "use float2",
30
30
  vec3: "use float3",
31
31
  vec4: "use float4",
32
- int: "there are no integers; a whole number is a float, so write float",
33
- uint: "there are no integers; a whole number is a float, so write float",
34
- bool: "there is no bool; a comparison is a float that is 0 or 1",
32
+ int2: "there are no int vectors yet; use an int for each component",
33
+ int3: "there are no int vectors yet; use an int for each component",
34
+ int4: "there are no int vectors yet; use an int for each component",
35
+ uint2: "there are no uint vectors yet; use a uint for each component",
36
+ uint3: "there are no uint vectors yet; use a uint for each component",
37
+ uint4: "there are no uint vectors yet; use a uint for each component",
38
+ bool2: "there are no bool vectors; use a bool for each component",
39
+ bool3: "there are no bool vectors; use a bool for each component",
40
+ bool4: "there are no bool vectors; use a bool for each component",
41
+ ivec2: "there are no int vectors yet; use an int for each component",
35
42
  half: "use float; the IR has one precision",
36
43
  double: "use float; the IR has one precision",
37
44
  fixed: "use float; the IR has one precision",
@@ -52,13 +59,17 @@ const TYPE_FIX: Record<string, TypeName> = {
52
59
  const BINDING: Record<string, number> = {
53
60
  "||": 1,
54
61
  "&&": 2,
55
- "==": 3, "!=": 3,
56
- "<": 4, "<=": 4, ">": 4, ">=": 4,
57
- "+": 5, "-": 5,
58
- "*": 6, "/": 6, "%": 6,
62
+ "|": 3,
63
+ "^": 4,
64
+ "&": 5,
65
+ "==": 6, "!=": 6,
66
+ "<": 7, "<=": 7, ">": 7, ">=": 7,
67
+ "<<": 8, ">>": 8,
68
+ "+": 9, "-": 9,
69
+ "*": 10, "/": 10, "%": 10,
59
70
  }
60
71
 
61
- const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%="])
72
+ const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>="])
62
73
 
63
74
  class Parser {
64
75
  private i = 0
@@ -170,7 +181,7 @@ class Parser {
170
181
  const to = TYPE_FIX[t.text]
171
182
  const fix = to === undefined ? undefined : { title: `Replace ${t.text} with ${to}`, replacement: to }
172
183
  if (why !== undefined) this.fail(`"${t.text}" is not a type here: ${why}`, t, fix)
173
- this.fail(`"${t.text}" is not a type; the types are float, float2, float3 and float4`, t)
184
+ this.fail(`"${t.text}" is not a type; the types are float, float2, float3, float4, int, uint and bool`, t)
174
185
  }
175
186
 
176
187
  // MARK: the file
@@ -339,28 +350,25 @@ class Parser {
339
350
  private parseStmt(): Stmt {
340
351
  const t = this.peek()
341
352
 
353
+ if (t.text === "{") return { k: "block", body: this.parseBlock(), pos: t }
342
354
  if (t.text === "const") return this.parseConst()
343
355
  if (t.text === "if") return this.parseIf()
344
356
  if (t.text === "for") return this.parseFor()
345
- if (t.text === "while" || t.text === "do") {
346
- this.fail(
347
- `there is no ${t.text} loop: a loop unrolls at build time, and only a for loop ` +
348
- `with constant bounds can. See a for loop`,
349
- t,
350
- )
357
+ if (t.text === "while") return this.parseWhile()
358
+ if (t.text === "switch") return this.parseSwitch()
359
+ if (t.text === "do") {
360
+ this.fail("there is no do loop; write it as a while loop, `while (condition) { ... }`", t)
351
361
  }
352
362
  if (t.text === "discard") {
353
363
  this.fail("there is no discard; return a colour with alpha 0 instead", t)
354
364
  }
355
365
  if (t.text === "break" || t.text === "continue") {
356
- this.fail(
357
- `there is no ${t.text}: a for loop unrolls, so every iteration runs. Put the rest of ` +
358
- `the body under an if instead`,
359
- t,
360
- )
366
+ this.next()
367
+ this.expect(";", `after ${t.text}`)
368
+ return { k: t.text, pos: t }
361
369
  }
362
- if (t.text === "switch") {
363
- this.fail("there is no switch; write it as an if and else if", t)
370
+ if (t.text === "case" || t.text === "default") {
371
+ this.fail(`${t.text} belongs inside a switch's braces`, t)
364
372
  }
365
373
  if (t.text === "return") {
366
374
  this.next()
@@ -384,26 +392,50 @@ class Parser {
384
392
  this.peek(),
385
393
  )
386
394
  }
387
- this.expect("=", `after ${name.text}; every local is declared with a value`)
388
- const init = this.parseExpr()
389
- this.expect(";", "after a declaration")
395
+ // A local declared without a value holds zero, or false, until it
396
+ // is assigned: the same on every backend, where HLSL leaves it
397
+ // undefined.
398
+ let init: Expr
399
+ if (this.eat(";")) {
400
+ init = zeroOf(type, name)
401
+ } else {
402
+ this.expect("=", `after ${name.text}, or ";" to declare it holding zero`)
403
+ init = this.parseExpr()
404
+ this.expect(";", "after a declaration")
405
+ }
390
406
  return { k: "var", type, name: name.text, init, pos: t }
391
407
  }
392
408
 
409
+ const s = this.parseSimple(t)
410
+ this.expect(";", "after an assignment")
411
+ return s
412
+ }
413
+
414
+ /**
415
+ * An assignment, or `x++`, `++x`, `x--`, `--x`, with no semicolon: a
416
+ * statement's body, and a for loop's update.
417
+ */
418
+ private parseSimple(t: Token): Extract<Stmt, { k: "assign" }> {
419
+ const one = (at: Token): Expr => ({ k: "num", value: 1, whole: true, pos: at })
420
+ if (t.text === "++" || t.text === "--") {
421
+ this.next()
422
+ const target = this.parseUnary()
423
+ return { k: "assign", target, op: t.text === "++" ? "+=" : "-=", value: one(t), pos: t }
424
+ }
393
425
  const target = this.parseExpr()
394
426
  const op = this.peek()
395
427
  if (op.kind === "punct" && ASSIGN_OPS.has(op.text)) {
396
428
  this.next()
397
429
  const value = this.parseExpr()
398
- this.expect(";", "after an assignment")
399
430
  return { k: "assign", target, op: op.text as AssignOp, value, pos: t }
400
431
  }
401
432
  if (op.text === "++" || op.text === "--") {
402
- this.fail(`${op.text} is only a for loop's update; write \`x = x + 1;\``, op)
433
+ this.next()
434
+ return { k: "assign", target, op: op.text === "++" ? "+=" : "-=", value: one(op), pos: t }
403
435
  }
404
436
  this.fail(
405
437
  "this statement has no effect. A statement is a declaration, an assignment, an if, a " +
406
- "for or a return",
438
+ "loop, a switch or a return",
407
439
  t,
408
440
  )
409
441
  }
@@ -423,63 +455,86 @@ class Parser {
423
455
  }
424
456
 
425
457
  /**
426
- * `for (int i = A; i < B; i++)`, and nothing more exotic than `i += K`.
427
- *
428
- * The shape is narrow because the loop UNROLLS: the counter is a compile
429
- * time number, not a value, and every form this refuses is one where it
430
- * could not be. Section 3.6 of the spec, and the error says so rather than
431
- * leaving an author to guess which part offended.
458
+ * `for (int i = A; condition; update)`, where the update changes the
459
+ * counter: `i++`, `i--`, `i += k`, `i = i * 2`. The loop unrolls when its
460
+ * turns are known at build time and few, and is a real loop otherwise.
432
461
  */
433
462
  private parseFor(): Stmt {
434
463
  const kw = this.next()
435
464
  this.expect("(", "after for")
436
465
 
437
466
  const decl = this.peek()
438
- if (decl.text !== "int" && decl.text !== "float") {
467
+ if (decl.kind !== "ident" || this.peek(1).kind !== "ident") {
439
468
  this.fail(
440
469
  `a for loop starts by declaring its counter, as in \`for (int i = 0; i < 4; i++)\`, ` +
441
470
  `got ${describe(decl)}`, decl,
442
471
  )
443
472
  }
444
- this.next()
473
+ const type = this.expectType("the counter's type")
445
474
  const counter = this.expectName("a counter name")
446
475
  this.expect("=", "after the counter")
447
476
  const from = this.parseExpr()
448
477
  this.expect(";", "after the counter's start")
449
478
 
450
- const lhs = this.expectIdent("the counter in the loop's condition")
451
- if (lhs.text !== counter.text) {
452
- this.fail(`this loop counts ${counter.text}, so its condition has to test ${counter.text}`, lhs)
453
- }
454
- const cmp = this.peek()
455
- if (cmp.text !== "<" && cmp.text !== "<=") {
456
- this.fail(
457
- `a for loop counts up, so its condition is \`${counter.text} <\` or \`${counter.text} <=\`, ` +
458
- `got ${describe(cmp)}`, cmp,
459
- )
460
- }
461
- this.next()
462
- const to = this.parseExpr()
463
- this.expect(";", "after the loop's bound")
479
+ const cond = this.parseExpr()
480
+ this.expect(";", "after the loop's condition")
464
481
 
465
- const up = this.expectIdent("the counter in the loop's update")
466
- if (up.text !== counter.text) {
482
+ const up = this.peek()
483
+ const update = this.parseSimple(up)
484
+ const target = update.target.k === "ident" ? update.target.name : null
485
+ if (target !== counter.text) {
467
486
  this.fail(`this loop counts ${counter.text}, so its update has to change ${counter.text}`, up)
468
487
  }
469
- let step: Expr = { k: "num", value: 1, pos: up }
470
- if (this.eat("++")) {
471
- // nothing: the step is 1
472
- } else if (this.eat("+=")) {
473
- step = this.parseExpr()
474
- } else {
475
- this.fail(
476
- `a for loop's update is \`${counter.text}++\` or \`${counter.text} += n\`, got ` +
477
- `${describe(this.peek())}`, this.peek(),
478
- )
479
- }
480
488
  this.expect(")", "after the loop's update")
481
489
  const body = this.parseBody()
482
- return { k: "for", counter: counter.text, from, to, inclusive: cmp.text === "<=", step, body, pos: kw }
490
+ return { k: "for", type, counter: counter.text, from, cond, update, body, pos: kw }
491
+ }
492
+
493
+ private parseWhile(): Stmt {
494
+ const kw = this.next()
495
+ this.expect("(", "after while")
496
+ const cond = this.parseExpr()
497
+ this.expect(")", "after a while condition")
498
+ const body = this.parseBody()
499
+ return { k: "while", cond, body, pos: kw }
500
+ }
501
+
502
+ /**
503
+ * `switch (x) { case 0: ... break; case 1: case 2: ... return c; default: ... }`.
504
+ *
505
+ * Cases never fall through. Labels stack, and each group of them ends in a
506
+ * break, a return or a continue, which the checker holds it to; the closing
507
+ * break is dropped here, since leaving the switch is all it says.
508
+ */
509
+ private parseSwitch(): Stmt {
510
+ const kw = this.next()
511
+ this.expect("(", "after switch")
512
+ const value = this.parseExpr()
513
+ this.expect(")", "after the switch's value")
514
+ this.expect("{", "to open the switch's cases")
515
+ const cases: SwitchCase[] = []
516
+ while (!this.at("}")) {
517
+ const t = this.peek()
518
+ if (t.kind === "eof") this.fail("this switch is never closed", t)
519
+ if (t.text !== "case" && t.text !== "default") {
520
+ this.fail(`a switch holds cases, as in \`case 0: ... break;\`, got ${describe(t)}`, t)
521
+ }
522
+ const labels: Array<Expr | null> = []
523
+ while (this.at("case") || this.at("default")) {
524
+ if (this.next().text === "case") labels.push(this.parseExpr())
525
+ else labels.push(null)
526
+ this.expect(":", "after a case label")
527
+ }
528
+ const body: Stmt[] = []
529
+ while (!this.at("case") && !this.at("default") && !this.at("}")) {
530
+ if (this.peek().kind === "eof") this.fail("this switch is never closed", this.peek())
531
+ const s = this.recover(() => this.parseStmt(), "statement")
532
+ if (s !== null) body.push(s)
533
+ }
534
+ cases.push({ labels, body, closed: dropClosingBreak(body), pos: t })
535
+ }
536
+ this.next()
537
+ return { k: "switch", value, cases, pos: kw }
483
538
  }
484
539
 
485
540
  // MARK: expressions
@@ -511,12 +566,12 @@ class Parser {
511
566
 
512
567
  private parseUnary(): Expr {
513
568
  const t = this.peek()
514
- if (t.kind === "punct" && (t.text === "-" || t.text === "+" || t.text === "!")) {
569
+ if (t.kind === "punct" && (t.text === "-" || t.text === "+" || t.text === "!" || t.text === "~")) {
515
570
  this.next()
516
571
  return { k: "unary", op: t.text, arg: this.parseUnary(), pos: t }
517
572
  }
518
573
  if (t.text === "++" || t.text === "--") {
519
- this.fail(`${t.text} is only a for loop's update`, t)
574
+ this.fail(`${t.text} changes a local, so it is a statement of its own, \`i++;\`, and not part of an expression`, t)
520
575
  }
521
576
  return this.parsePostfix(this.parsePrimary())
522
577
  }
@@ -551,7 +606,10 @@ class Parser {
551
606
 
552
607
  private parsePrimary(): Expr {
553
608
  const t = this.next()
554
- if (t.kind === "number") return { k: "num", value: t.value!, pos: t }
609
+ if (t.kind === "number") return t.unsigned === true
610
+ ? { k: "num", value: t.value!, whole: true, unsigned: true, pos: t }
611
+ : { k: "num", value: t.value!, whole: t.whole === true, pos: t }
612
+ if (t.text === "true" || t.text === "false") return { k: "bool", value: t.text === "true", pos: t }
555
613
  if (t.kind === "hex") return { k: "hex", hex: t.text, pos: t }
556
614
  if (t.kind === "ident") return { k: "ident", name: t.text, pos: t }
557
615
  if (t.kind === "string") {
@@ -566,6 +624,26 @@ class Parser {
566
624
  }
567
625
  }
568
626
 
627
+ /** What a local declared without a value holds: zero in each component, or false. */
628
+ function zeroOf(type: TypeName, at: Pos): Expr {
629
+ if (type === "bool") return { k: "bool", value: false, pos: at }
630
+ return { k: "num", value: 0, whole: type === "int" || type === "uint", unsigned: type === "uint" || undefined, pos: at }
631
+ }
632
+
633
+ /**
634
+ * Whether a case's body ends in the break that closes it, which is dropped,
635
+ * since leaving the switch is all it says. The break may be the last thing
636
+ * inside braces that are the case's last statement, as in `case 0: { ...; break; }`.
637
+ */
638
+ function dropClosingBreak(body: Stmt[]): boolean {
639
+ const last = body.at(-1)
640
+ if (last?.k === "break") {
641
+ body.pop()
642
+ return true
643
+ }
644
+ return last?.k === "block" && dropClosingBreak(last.body)
645
+ }
646
+
569
647
  function describe(t: Token): string {
570
648
  if (t.kind === "eof") return "the end of the file"
571
649
  return `"${t.text}"`
package/src/lang/words.ts CHANGED
@@ -9,12 +9,11 @@
9
9
 
10
10
  import { TYPE_WIDTH, type TypeName } from "./ast"
11
11
 
12
- /**
13
- * Declaration and statement words. `int` is here rather than among the types
14
- * because it is only ever a for loop's counter, `for (int i = 0; i < 4; i++)`;
15
- * a value cannot be declared as one.
16
- */
17
- export const SL_KEYWORDS = ["uniform", "texture2D", "const", "if", "else", "for", "return", "int"] as const
12
+ /** Declaration and statement words, and the two bool literals. */
13
+ export const SL_KEYWORDS = [
14
+ "uniform", "texture2D", "const", "if", "else", "for", "while", "break", "continue", "switch", "case", "default",
15
+ "return", "true", "false",
16
+ ] as const
18
17
 
19
18
  /** The types a value, a uniform or a function can have. */
20
19
  export const SL_TYPES = Object.keys(TYPE_WIDTH) as readonly TypeName[]
package/src/ops.ts CHANGED
@@ -49,6 +49,9 @@ export const SLOP = {
49
49
  UNIFORM: 2,
50
50
  COMPOSE: 3, // build a wider value from narrower parts
51
51
  SWIZZLE: 4,
52
+ // To the node's kind (a float when it has none) from the operand's: a
53
+ // float to an int truncates toward zero, held to the int's range (IR 4).
54
+ CAST: 5,
52
55
 
53
56
  // Arithmetic
54
57
  ADD: 16,
@@ -59,6 +62,23 @@ export const SLOP = {
59
62
  POW: 21,
60
63
  NEG: 22,
61
64
  RECIP: 23,
65
+ // IR 4: comparisons give a bool; logic takes and gives bools; the bitwise
66
+ // ops take ints or uints. ADD to NEG take ints as well, by the node's kind.
67
+ LT: 24,
68
+ LE: 25,
69
+ GT: 26,
70
+ GE: 27,
71
+ EQ: 28,
72
+ NE: 29,
73
+ AND: 30,
74
+ OR: 31,
75
+ NOT: 32,
76
+ BIT_AND: 33,
77
+ BIT_OR: 34,
78
+ BIT_XOR: 35,
79
+ BIT_NOT: 36,
80
+ SHL: 37,
81
+ SHR: 38,
62
82
 
63
83
  // Maths
64
84
  SIN: 48,
@@ -95,6 +115,8 @@ export const SLOP = {
95
115
  SMOOTHSTEP: 98,
96
116
  SELECT: 99,
97
117
  REMAP: 100,
118
+ // `c ? a : b` on a bool, for values SELECT's arithmetic cannot carry (IR 4).
119
+ CHOOSE: 101,
98
120
 
99
121
  // Colour
100
122
  RAMP: 112,
@@ -137,7 +159,11 @@ export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
137
159
  */
138
160
  export const SL_ARITY: Record<number, number> = {
139
161
  [SLOP.CONST]: 0, [SLOP.INPUT]: 0, [SLOP.UNIFORM]: 0,
140
- [SLOP.COMPOSE]: -1, [SLOP.SWIZZLE]: 1,
162
+ [SLOP.COMPOSE]: -1, [SLOP.SWIZZLE]: 1, [SLOP.CAST]: 1,
163
+ [SLOP.LT]: 2, [SLOP.LE]: 2, [SLOP.GT]: 2, [SLOP.GE]: 2, [SLOP.EQ]: 2, [SLOP.NE]: 2,
164
+ [SLOP.AND]: 2, [SLOP.OR]: 2, [SLOP.NOT]: 1,
165
+ [SLOP.BIT_AND]: 2, [SLOP.BIT_OR]: 2, [SLOP.BIT_XOR]: 2, [SLOP.BIT_NOT]: 1, [SLOP.SHL]: 2, [SLOP.SHR]: 2,
166
+ [SLOP.CHOOSE]: 3,
141
167
 
142
168
  [SLOP.ADD]: 2, [SLOP.SUB]: 2, [SLOP.MUL]: 2, [SLOP.DIV]: 2,
143
169
  [SLOP.MOD]: 2, [SLOP.POW]: 2, [SLOP.NEG]: 1, [SLOP.RECIP]: 1,
@@ -208,6 +234,23 @@ export const SL_HLSL: Record<number, SLSurface> = {
208
234
  [SLOP.UNIFORM]: { syntax: "uniform <type> name = <default>;" },
209
235
  [SLOP.COMPOSE]: { syntax: "float2(), float3(), float4()" },
210
236
  [SLOP.SWIZZLE]: { syntax: ".xyzw / .rgba" },
237
+ [SLOP.CAST]: { syntax: "int(), uint(), float(), bool()" },
238
+ [SLOP.LT]: { syntax: "<" },
239
+ [SLOP.LE]: { syntax: "<=" },
240
+ [SLOP.GT]: { syntax: ">" },
241
+ [SLOP.GE]: { syntax: ">=" },
242
+ [SLOP.EQ]: { syntax: "==" },
243
+ [SLOP.NE]: { syntax: "!=" },
244
+ [SLOP.AND]: { syntax: "&&" },
245
+ [SLOP.OR]: { syntax: "||" },
246
+ [SLOP.NOT]: { syntax: "!" },
247
+ [SLOP.BIT_AND]: { syntax: "&" },
248
+ [SLOP.BIT_OR]: { syntax: "|" },
249
+ [SLOP.BIT_XOR]: { syntax: "^" },
250
+ [SLOP.BIT_NOT]: { syntax: "~" },
251
+ [SLOP.SHL]: { syntax: "<<" },
252
+ [SLOP.SHR]: { syntax: ">>" },
253
+ [SLOP.CHOOSE]: { syntax: "?: on a bool" },
211
254
 
212
255
  [SLOP.ADD]: { syntax: "+" },
213
256
  [SLOP.SUB]: { syntax: "-" },
package/src/serial.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * older one is migrated to this one and rehashed under it.
10
10
  */
11
11
  import {
12
- INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram,
12
+ INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram, intUniformProblem,
13
13
  programVersion, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
15
  import { SLOP, SL_ARITY, SL_NAME } from "./ops"
@@ -50,22 +50,24 @@ export function fromJSON(json: unknown): Program {
50
50
  // The lowest version that has these nodes, which is what the writer
51
51
  // recorded unless the file predates version 2 (whose nodes version 2 reads
52
52
  // as they are) or was edited. One that claims less than its nodes need
53
- // holds something its version never had.
54
- const needs = programVersion(written)
55
- if (v < needs && v >= 2) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
53
+ // holds something its version never had, and a version 1 file has
54
+ // nothing IR 3 added.
55
+ const needs = programVersion(written, j.result!)
56
+ if (v < needs && (v >= 2 || needs >= 3)) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
56
57
  const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
57
58
  nodes.forEach((n, i) => {
58
59
  const problem = n.k === "call" ? formProblem(n, nodes) : null
59
60
  if (problem !== null) fail(`node ${i} is not a form its op has: ${problem}`)
60
61
  })
61
- if (nodes[result]!.type !== TYPE.VEC4) fail("its result is not a float4")
62
+ const out = nodes[result]!
63
+ if (out.k === "if" || out.k === "loop" || out.type !== TYPE.VEC4 || out.kind !== undefined) fail("its result is not a float4")
62
64
  const uniforms = (j.uniforms ?? []).map(uniform)
63
65
  const textures = (j.textures ?? []).map(texture)
64
66
  checkCaps({ uniforms, textures })
65
67
  for (const n of nodes) {
66
68
  if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
67
69
  }
68
- const version = programVersion(nodes)
70
+ const version = programVersion(nodes, result)
69
71
  const hash = hashProgram(nodes, result, uniforms, textures)
70
72
  // The same IR version and hash scheme, so the same hash; a different one
71
73
  // means the file was edited or damaged, and its cached shader belongs to
@@ -113,6 +115,35 @@ function toOperands(written: readonly SLNode[], writtenResult: NodeRef): { nodes
113
115
  function node(n: unknown, i: number): SLNode {
114
116
  if (typeof n !== "object" || n === null) fail(`node ${i} is not an object`)
115
117
  const x = n as Record<string, unknown>
118
+ const refs = (a: unknown, what: string): NodeRef[] => {
119
+ if (!Array.isArray(a) || !a.every((r) => Number.isInteger(r) && r >= 0 && r < i)) fail(`node ${i}'s ${what} do not all refer to earlier nodes`)
120
+ return a as number[]
121
+ }
122
+ const whole = (v: unknown, what: string): number => {
123
+ if (!Number.isInteger(v) || (v as number) < 0) fail(`node ${i}'s ${what} is not a whole number`)
124
+ return v as number
125
+ }
126
+ // IR 4's control nodes carry no type of their own.
127
+ if (x.k === "if") {
128
+ const [cond, then, otherwise] = [refs([x.cond], "condition")[0]!, refs(x.then, "then results"), refs(x.else, "else results")]
129
+ if (then.length !== otherwise.length) fail(`node ${i} is an if whose two sides give different numbers of results`)
130
+ return { k: "if", cond, then, else: otherwise }
131
+ }
132
+ if (x.k === "loop") {
133
+ const init = refs(x.init, "starting values"), next = refs(x.next, "next values")
134
+ if (init.length !== next.length) fail(`node ${i} is a loop whose next values do not match its starting values`)
135
+ if (!Number.isInteger(x.max) || (x.max as number) < 1) fail(`node ${i} is a loop with no turn limit`)
136
+ return { k: "loop", id: whole(x.id, "loop id"), init, cond: refs([x.cond], "condition")[0]!, next, max: x.max as number }
137
+ }
138
+ const kind = x.kind
139
+ if (kind !== undefined && kind !== "int" && kind !== "uint" && kind !== "bool") fail(`node ${i} holds a "${String(kind)}", which is not a kind`)
140
+ const withKind = <T extends SLNode>(v: T): T => (kind === undefined ? v : { ...v, kind })
141
+ if (x.k === "param" || x.k === "proj") {
142
+ const type = x.type
143
+ if (type !== 1 && type !== 2 && type !== 3 && type !== 4) fail(`node ${i} has no width 1 to 4`)
144
+ if (x.k === "param") return withKind({ k: "param", type: type as SLType, loop: whole(x.loop, "loop"), index: whole(x.index, "index") })
145
+ return withKind({ k: "proj", type: type as SLType, src: refs([x.src], "source")[0]!, index: whole(x.index, "index") })
146
+ }
116
147
  const type = x.type
117
148
  if (type !== 1 && type !== 2 && type !== 3 && type !== 4) fail(`node ${i} has no width 1 to 4`)
118
149
  const t = type as SLType
@@ -132,7 +163,7 @@ function node(n: unknown, i: number): SLNode {
132
163
  case "const": {
133
164
  const v = nums(x.v, "value")
134
165
  if (v.length !== t) fail(`node ${i} is a float${t === 1 ? "" : t} constant with ${v.length} values`)
135
- return { k: "const", type: t, v }
166
+ return withKind({ k: "const", type: t, v })
136
167
  }
137
168
  case "input": {
138
169
  const name = x.name as keyof typeof INPUTS
@@ -148,7 +179,7 @@ function node(n: unknown, i: number): SLNode {
148
179
  if (chans.length !== t || !chans.every((c) => Number.isInteger(c) && c >= 0 && c <= 3)) {
149
180
  fail(`node ${i}'s swizzle does not pick ${t} channels`)
150
181
  }
151
- return { k: "swizzle", type: t, src: ref(x.src, "source"), chans }
182
+ return withKind({ k: "swizzle", type: t, src: ref(x.src, "source"), chans })
152
183
  }
153
184
  case "call": {
154
185
  const op = x.op as number
@@ -162,7 +193,7 @@ function node(n: unknown, i: number): SLNode {
162
193
  if (arity !== undefined && arity >= 0 && args.length !== arity) {
163
194
  fail(`node ${i} calls ${SL_NAME[op]} with ${args.length} arguments; it takes ${arity}`)
164
195
  }
165
- const out: SLNode = { k: "call", type: t, op: op as never, args }
196
+ const out: SLNode = withKind({ k: "call", type: t, op: op as never, args })
166
197
  if (x.imm !== undefined) out.imm = nums(x.imm, "immediates")
167
198
  return out
168
199
  }
@@ -178,6 +209,12 @@ function uniform(u: unknown, i: number): UniformDecl {
178
209
  }
179
210
  const out: UniformDecl = { name: x.name!, type: x.type as SLType, value: x.value!.slice() }
180
211
  if (x.colour === true) out.colour = true
212
+ if (x.kind !== undefined) {
213
+ if (x.kind !== "int") fail(`uniform ${i} holds a "${String(x.kind)}"; a uniform is a float or an int`)
214
+ const problem = intUniformProblem(out.type, out.value)
215
+ if (problem !== null) fail(`uniform ${i}: ${problem}`)
216
+ out.kind = "int"
217
+ }
181
218
  // Its control: shapes checked here, sense checked by the one check the
182
219
  // compiler uses, so a hand edited file cannot give a host a range it would
183
220
  // choke on.