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.
- package/CHANGELOG.md +29 -0
- package/README.md +70 -28
- package/goldens/goldens.json +147 -63
- package/package.json +1 -1
- package/src/body.ts +132 -26
- package/src/core.ts +1 -1
- package/src/hlsl.ts +7 -3
- package/src/ir.ts +293 -33
- package/src/lang/README.md +2 -2
- package/src/lang/ast.ts +37 -7
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +179 -70
- package/src/lang/glsl.ts +6 -10
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1034 -289
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +48 -4
- package/src/params.ts +2 -0
- package/src/serial.ts +100 -17
- package/src/sl.ts +343 -33
- package/src/structure.ts +285 -0
- package/src/web.ts +152 -33
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,
|
|
@@ -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
|
-
|
|
160
|
-
|
|
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 {
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
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
|
|
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
|
}
|