onejs-sl 0.3.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.
@@ -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,
@@ -156,8 +182,10 @@ export const SL_ARITY: Record<number, number> = {
156
182
 
157
183
  [SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
158
184
 
159
- [SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: 1, [SLOP.SDF]: 1,
160
- [SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1,
185
+ // An SDF takes the point, or the point and its parameters as operands; an
186
+ // octave op the point, or the point and its count (`formProblem` in ir.ts).
187
+ [SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: -1, [SLOP.SDF]: -1,
188
+ [SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
161
189
 
162
190
  [SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
163
191
  }
@@ -206,6 +234,23 @@ export const SL_HLSL: Record<number, SLSurface> = {
206
234
  [SLOP.UNIFORM]: { syntax: "uniform <type> name = <default>;" },
207
235
  [SLOP.COMPOSE]: { syntax: "float2(), float3(), float4()" },
208
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" },
209
254
 
210
255
  [SLOP.ADD]: { syntax: "+" },
211
256
  [SLOP.SUB]: { syntax: "-" },
@@ -282,7 +327,6 @@ export const SL_HLSL: Record<number, SLSurface> = {
282
327
  */
283
328
  export const SL_UNIMPLEMENTED: Record<number, string> = {
284
329
  [SLOP.RGB2HSV]: "no backend has a case for it yet",
285
- [SLOP.SAMPLE_LOD]: "no backend samples at an explicit LOD yet; use tex2D",
286
330
  [SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
287
331
  }
288
332
 
package/src/params.ts CHANGED
@@ -54,6 +54,7 @@ const INTRINSIC: Record<string, readonly string[]> = {
54
54
  distance: ["x", "y"], dot: ["x", "y"], cross: ["x", "y"], reflect: ["i", "n"],
55
55
  lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
56
56
  tex2D: ["s", "t"],
57
+ tex2Dlod: ["s", "t", "lod"],
57
58
  }
58
59
 
59
60
  /**
@@ -161,6 +162,7 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
161
162
  turbulence: "Simplex creases layered in octaves, 1 to 4: fire, smoke, marble.",
162
163
  ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
163
164
  tex2D: "The colour of texture s at the uv t, with straight alpha.",
165
+ tex2Dlod: "The colour of texture s at the uv t from mip level lod: 0 is the full size, 1 half, and so on.",
164
166
  sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
165
167
  ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
166
168
  }
package/src/serial.ts CHANGED
@@ -9,14 +9,16 @@
9
9
  * older one is migrated to this one and rehashed under it.
10
10
  */
11
11
  import {
12
- INPUTS, SLError, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, hashProgram,
13
- type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
12
+ INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram,
13
+ programVersion, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
- import { SL_ARITY, SL_NAME } from "./ops"
15
+ import { SLOP, SL_ARITY, SL_NAME } from "./ops"
16
16
 
17
17
  export interface ProgramJSON {
18
18
  /** `SL_IR_VERSION` of the writer. Absent means 1. */
19
19
  v: number
20
+ /** `SL_HASH_VERSION` of the writer, which `hash` was computed under. Absent means 1. */
21
+ h?: number
20
22
  nodes: SLNode[]
21
23
  result: NodeRef
22
24
  uniforms: UniformDecl[]
@@ -25,7 +27,10 @@ export interface ProgramJSON {
25
27
  }
26
28
 
27
29
  export function toJSON(p: Program): ProgramJSON {
28
- return { v: p.version, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures, hash: p.hash }
30
+ return {
31
+ v: p.version, h: SL_HASH_VERSION, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures,
32
+ hash: p.hash,
33
+ }
29
34
  }
30
35
 
31
36
  export function fromJSON(json: unknown): Program {
@@ -38,29 +43,107 @@ export function fromJSON(json: unknown): Program {
38
43
  `Update onejs-sl to read it, or rebuild it from its .sl source.`)
39
44
  }
40
45
  if (!Array.isArray(j.nodes) || j.nodes.length === 0) fail("it has no nodes")
41
- const nodes = j.nodes.map((n, i) => node(n, i))
42
- const result = j.result
43
- if (!Number.isInteger(result) || result! < 0 || result! >= nodes.length) fail(`its result ${String(result)} is not a node`)
44
- if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
46
+ const written = j.nodes.map((n, i) => node(n, i))
47
+ if (!Number.isInteger(j.result) || j.result! < 0 || j.result! >= written.length) {
48
+ fail(`its result ${String(j.result)} is not a node`)
49
+ }
50
+ // The lowest version that has these nodes, which is what the writer
51
+ // recorded unless the file predates version 2 (whose nodes version 2 reads
52
+ // as they are) or was edited. One that claims less than its nodes need
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`)
57
+ const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
58
+ nodes.forEach((n, i) => {
59
+ const problem = n.k === "call" ? formProblem(n, nodes) : null
60
+ if (problem !== null) fail(`node ${i} is not a form its op has: ${problem}`)
61
+ })
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")
45
64
  const uniforms = (j.uniforms ?? []).map(uniform)
46
65
  const textures = (j.textures ?? []).map(texture)
47
66
  checkCaps({ uniforms, textures })
48
67
  for (const n of nodes) {
49
68
  if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
50
69
  }
51
- const hash = hashProgram(nodes, result!, uniforms, textures)
52
- // Same version, so the same maths and the same hash; a different one means
53
- // the file was edited or damaged, and its cached shader belongs to
54
- // something else. An older version is rehashed under this one on purpose.
55
- if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
70
+ const version = programVersion(nodes, result)
71
+ const hash = hashProgram(nodes, result, uniforms, textures)
72
+ // The same IR version and hash scheme, so the same hash; a different one
73
+ // means the file was edited or damaged, and its cached shader belongs to
74
+ // something else. A file an older version or scheme wrote is rehashed under
75
+ // this one on purpose.
76
+ if (v === version && (j.h ?? 1) === SL_HASH_VERSION && j.hash !== undefined && j.hash !== hash) {
56
77
  fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
57
78
  }
58
- return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash }
79
+ return { version, nodes, result, uniforms, textures, hash }
80
+ }
81
+
82
+ /**
83
+ * A graph written before IR 3, with the forms it had then moved to the one each
84
+ * op has now: an sdf's shape parameters after its id and an octave op's count
85
+ * were immediates, and are operands. Each constant goes in just before the
86
+ * call that reads it, and every later reference moves along.
87
+ */
88
+ function toOperands(written: readonly SLNode[], writtenResult: NodeRef): { nodes: SLNode[]; result: NodeRef } {
89
+ const nodes: SLNode[] = []
90
+ const at: NodeRef[] = []
91
+ const constant = (type: SLType, v: number[]): NodeRef => nodes.push({ k: "const", type, v }) - 1
92
+ written.forEach((n, i) => {
93
+ const imm = n.k === "call" ? n.imm ?? [] : []
94
+ if (n.k === "call" && n.op === SLOP.SDF && n.args.length === 1) {
95
+ if (imm.length < 5) fail(`node ${i} is an sdf with ${imm.length} immediates; one written before IR 3 has at least 5`)
96
+ const q = constant(TYPE.VEC4, [imm[1]!, imm[2]!, imm[3]!, imm[4]!])
97
+ const r = constant(TYPE.VEC2, [imm[5] ?? 0, imm[6] ?? 0])
98
+ at[i] = nodes.push({ ...n, args: [at[n.args[0]!]!, q, r], imm: [imm[0]!] }) - 1
99
+ return
100
+ }
101
+ const octaves = n.k === "call" && (n.op === SLOP.FBM || n.op === SLOP.TURBULENCE || n.op === SLOP.RIDGED)
102
+ if (octaves && n.args.length === 1) {
103
+ if (imm.length < 1) fail(`node ${i} is ${SL_NAME[n.op]} with no octave count`)
104
+ const count = constant(TYPE.FLOAT, [imm[0]!])
105
+ at[i] = nodes.push({ ...n, args: [at[n.args[0]!]!, count], imm: imm.slice(1) }) - 1
106
+ return
107
+ }
108
+ const moved: SLNode = n.k === "call" ? { ...n, args: n.args.map((a) => at[a]!) }
109
+ : n.k === "swizzle" ? { ...n, src: at[n.src]! } : n
110
+ at[i] = nodes.push(moved) - 1
111
+ })
112
+ return { nodes, result: at[writtenResult]! }
59
113
  }
60
114
 
61
115
  function node(n: unknown, i: number): SLNode {
62
116
  if (typeof n !== "object" || n === null) fail(`node ${i} is not an object`)
63
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
+ }
64
147
  const type = x.type
65
148
  if (type !== 1 && type !== 2 && type !== 3 && type !== 4) fail(`node ${i} has no width 1 to 4`)
66
149
  const t = type as SLType
@@ -80,7 +163,7 @@ function node(n: unknown, i: number): SLNode {
80
163
  case "const": {
81
164
  const v = nums(x.v, "value")
82
165
  if (v.length !== t) fail(`node ${i} is a float${t === 1 ? "" : t} constant with ${v.length} values`)
83
- return { k: "const", type: t, v }
166
+ return withKind({ k: "const", type: t, v })
84
167
  }
85
168
  case "input": {
86
169
  const name = x.name as keyof typeof INPUTS
@@ -96,7 +179,7 @@ function node(n: unknown, i: number): SLNode {
96
179
  if (chans.length !== t || !chans.every((c) => Number.isInteger(c) && c >= 0 && c <= 3)) {
97
180
  fail(`node ${i}'s swizzle does not pick ${t} channels`)
98
181
  }
99
- return { k: "swizzle", type: t, src: ref(x.src, "source"), chans }
182
+ return withKind({ k: "swizzle", type: t, src: ref(x.src, "source"), chans })
100
183
  }
101
184
  case "call": {
102
185
  const op = x.op as number
@@ -110,7 +193,7 @@ function node(n: unknown, i: number): SLNode {
110
193
  if (arity !== undefined && arity >= 0 && args.length !== arity) {
111
194
  fail(`node ${i} calls ${SL_NAME[op]} with ${args.length} arguments; it takes ${arity}`)
112
195
  }
113
- 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 })
114
197
  if (x.imm !== undefined) out.imm = nums(x.imm, "immediates")
115
198
  return out
116
199
  }