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/CHANGELOG.md +14 -0
- package/README.md +35 -18
- package/goldens/goldens.json +52 -3
- package/package.json +1 -1
- package/src/body.ts +103 -20
- package/src/hlsl.ts +1 -0
- package/src/ir.ts +170 -26
- package/src/lang/ast.ts +37 -7
- package/src/lang/check.ts +155 -59
- package/src/lang/glsl.ts +5 -8
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1015 -266
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +44 -1
- package/src/serial.ts +39 -8
- package/src/sl.ts +255 -11
- package/src/structure.ts +285 -0
- package/src/web.ts +108 -22
package/src/lang/parser.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
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
|
|
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"
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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.
|
|
357
|
-
|
|
358
|
-
|
|
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 === "
|
|
363
|
-
this.fail(
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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.
|
|
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
|
-
"
|
|
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;
|
|
427
|
-
*
|
|
428
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
451
|
-
|
|
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.
|
|
466
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
@@ -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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
}
|