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.
- package/CHANGELOG.md +25 -0
- package/README.md +41 -19
- 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 +198 -27
- 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 +1073 -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 +46 -9
- package/src/sl.ts +264 -11
- package/src/structure.ts +285 -0
- package/src/web.ts +108 -22
package/src/ir.ts
CHANGED
|
@@ -64,12 +64,75 @@ export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INP
|
|
|
64
64
|
/** Index into `Program.nodes`. Always refers backwards. */
|
|
65
65
|
export type NodeRef = number
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
/**
|
|
68
|
+
* What a value holds, beside its width. A node without one holds floats, which
|
|
69
|
+
* is every node before IR 4, so a program of floats is the graph and the hash
|
|
70
|
+
* it always was.
|
|
71
|
+
*/
|
|
72
|
+
export type SLKind = "int" | "uint" | "bool"
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A node that is a value. `param` is a loop's carried value at the top of an
|
|
76
|
+
* iteration, `proj` one result of an `if` or a `loop`.
|
|
77
|
+
*/
|
|
78
|
+
export type ValueNode =
|
|
79
|
+
| { k: "const"; type: SLType; kind?: SLKind; v: number[] }
|
|
80
|
+
| { k: "input"; type: SLType; kind?: undefined; name: InputName }
|
|
81
|
+
| { k: "uniform"; type: SLType; kind?: undefined; slot: number }
|
|
82
|
+
| { k: "swizzle"; type: SLType; kind?: SLKind; src: NodeRef; chans: number[] }
|
|
83
|
+
| { k: "call"; type: SLType; kind?: SLKind; op: SLOpCode; args: NodeRef[]; imm?: number[] }
|
|
84
|
+
| { k: "param"; type: SLType; kind?: SLKind; loop: number; index: number }
|
|
85
|
+
| { k: "proj"; type: SLType; kind?: SLKind; src: NodeRef; index: number }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Structured control flow (IR 4), as regions of the one graph rather than as
|
|
89
|
+
* statements: the graph stays a pure DAG, hash consed and hashed as before, and
|
|
90
|
+
* an emitter places each node in the innermost region that uses it
|
|
91
|
+
* (`structure.ts`), which is what makes a branch skip what only it needs.
|
|
92
|
+
*
|
|
93
|
+
* if `then` or `else` are the results, per the bool `cond`; its `proj`s read them
|
|
94
|
+
* loop the `param`s of loop `id` start as `init`; while `cond` holds and fewer
|
|
95
|
+
* than `max` turns have run, they become `next`. Its `proj`s are the params
|
|
96
|
+
* when it stops. `cond` and `next` are computed from the params.
|
|
97
|
+
*/
|
|
98
|
+
export type ControlNode =
|
|
99
|
+
| { k: "if"; cond: NodeRef; then: NodeRef[]; else: NodeRef[] }
|
|
100
|
+
| { k: "loop"; id: number; init: NodeRef[]; cond: NodeRef; next: NodeRef[]; max: number }
|
|
101
|
+
|
|
102
|
+
export type SLNode = ValueNode | ControlNode
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A float to an int or a uint as the GPU converts one: as a 32 bit float,
|
|
106
|
+
* held to the widest such float the kind can hold, then truncated toward zero.
|
|
107
|
+
* The emitters clamp to these bounds, so a folded conversion
|
|
108
|
+
* and a computed one agree.
|
|
109
|
+
*/
|
|
110
|
+
export function truncateHeld(v: number, kind: "int" | "uint"): number {
|
|
111
|
+
const [lo, hi] = INT_BOUNDS[kind]
|
|
112
|
+
return Math.trunc(Math.min(hi, Math.max(lo, Math.fround(v))))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The widest 32 bit floats an int and a uint hold, which a conversion clamps to first. */
|
|
116
|
+
export const INT_BOUNDS = { int: [-2147483648, 2147483520], uint: [0, 4294967040] } as const
|
|
117
|
+
|
|
118
|
+
/** The value node at `ref`. A control node is only ever read through its projs. */
|
|
119
|
+
export function valueAt(nodes: readonly SLNode[], ref: NodeRef): ValueNode {
|
|
120
|
+
const n = nodes[ref]
|
|
121
|
+
if (n === undefined || n.k === "if" || n.k === "loop") throw new SLError(`node ${ref} is not a value`)
|
|
122
|
+
return n
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The refs a node reads, in order. */
|
|
126
|
+
export function operands(n: SLNode): NodeRef[] {
|
|
127
|
+
switch (n.k) {
|
|
128
|
+
case "swizzle": return [n.src]
|
|
129
|
+
case "call": return n.args
|
|
130
|
+
case "proj": return [n.src]
|
|
131
|
+
case "if": return [n.cond, ...n.then, ...n.else]
|
|
132
|
+
case "loop": return [...n.init, n.cond, ...n.next]
|
|
133
|
+
default: return []
|
|
134
|
+
}
|
|
135
|
+
}
|
|
73
136
|
|
|
74
137
|
/**
|
|
75
138
|
* How a host presents a uniform: a slider, a checkbox, a dropdown, a heading
|
|
@@ -162,6 +225,27 @@ export interface UniformDecl extends UniformControl {
|
|
|
162
225
|
* conversion is already in the graph, so it is not part of the hash.
|
|
163
226
|
*/
|
|
164
227
|
colour?: true
|
|
228
|
+
/**
|
|
229
|
+
* An int: `value` is a whole number and the program reads it as one, so a
|
|
230
|
+
* host shows a whole number field rather than a float's. The slot is a
|
|
231
|
+
* float, as every host binds it, and `type` is FLOAT. Set by `uniform int`
|
|
232
|
+
* in a `.sl` file or `sl.uniform.int`. Metadata for a host like `colour`:
|
|
233
|
+
* the reads' conversion to an int is already in the graph, so it is not
|
|
234
|
+
* part of the hash. A float uniform has none.
|
|
235
|
+
*/
|
|
236
|
+
kind?: "int"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** What is wrong with an int uniform of `type` and default `value`, or null. The one check, for the builder and `fromJSON`. */
|
|
240
|
+
export function intUniformProblem(type: SLType, value: readonly number[]): string | null {
|
|
241
|
+
if (type !== TYPE.FLOAT) return `an int uniform is one number, and this is a ${widthName(type)}`
|
|
242
|
+
const v = value[0]!
|
|
243
|
+
// Its slot is a float, and a read is held to INT_BOUNDS: a default the
|
|
244
|
+
// program would read as another number is refused rather than shown.
|
|
245
|
+
if (!Number.isInteger(v) || Math.fround(v) !== v || v < INT_BOUNDS.int[0] || v > INT_BOUNDS.int[1]) {
|
|
246
|
+
return `an int uniform's default is a whole number its float slot holds exactly, and this is ${v}`
|
|
247
|
+
}
|
|
248
|
+
return null
|
|
165
249
|
}
|
|
166
250
|
|
|
167
251
|
export interface TextureDecl {
|
|
@@ -254,6 +338,10 @@ export class Builder {
|
|
|
254
338
|
readonly textures: TextureDecl[] = []
|
|
255
339
|
private readonly interned = new Map<string, NodeRef>()
|
|
256
340
|
|
|
341
|
+
/** The next loop's id: unique within the program, so two loops' params never intern to one node. */
|
|
342
|
+
private loops = 0
|
|
343
|
+
newLoop(): number { return this.loops++ }
|
|
344
|
+
|
|
257
345
|
add(node: SLNode): NodeRef {
|
|
258
346
|
if (this.nodes.length >= MAX_NODES) {
|
|
259
347
|
throw new SLError(`a program may hold at most ${MAX_NODES} operations`)
|
|
@@ -266,7 +354,7 @@ export class Builder {
|
|
|
266
354
|
return ref
|
|
267
355
|
}
|
|
268
356
|
|
|
269
|
-
call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[]): NodeRef {
|
|
357
|
+
call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[], kind?: SLKind): NodeRef {
|
|
270
358
|
const arity = SL_ARITY[op]
|
|
271
359
|
if (arity >= 0 && args.length !== arity) {
|
|
272
360
|
throw new SLError(`${SL_NAME[op]} takes ${arity} argument(s), got ${args.length}`)
|
|
@@ -276,17 +364,22 @@ export class Builder {
|
|
|
276
364
|
throw new SLError(`${SL_NAME[op]} refers to a node that does not exist yet`)
|
|
277
365
|
}
|
|
278
366
|
}
|
|
279
|
-
|
|
367
|
+
const node: SLNode = imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm }
|
|
368
|
+
if (kind !== undefined) node.kind = kind
|
|
369
|
+
return this.add(node)
|
|
280
370
|
}
|
|
281
371
|
|
|
282
|
-
constant(v: number[]): NodeRef {
|
|
372
|
+
constant(v: number[], kind?: SLKind): NodeRef {
|
|
283
373
|
for (const n of v) {
|
|
284
374
|
if (!Number.isFinite(n)) throw new SLError(`a constant must be finite, got ${n}`)
|
|
375
|
+
if (kind !== undefined && !Number.isInteger(n)) throw new SLError(`an ${kind} constant must be a whole number, got ${n}`)
|
|
285
376
|
}
|
|
286
|
-
|
|
377
|
+
const node: SLNode = { k: "const", type: v.length as SLType, v: v.slice() }
|
|
378
|
+
if (kind !== undefined) node.kind = kind
|
|
379
|
+
return this.add(node)
|
|
287
380
|
}
|
|
288
381
|
|
|
289
|
-
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
|
|
382
|
+
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}, kind?: "int"): NodeRef {
|
|
290
383
|
const existing = this.uniforms.findIndex((u) => u.name === name)
|
|
291
384
|
if (existing >= 0) {
|
|
292
385
|
const u = this.uniforms[existing]
|
|
@@ -296,6 +389,9 @@ export class Builder {
|
|
|
296
389
|
if ((u.colour === true) !== colour) {
|
|
297
390
|
throw new SLError(`uniform "${name}" is declared both as a colour and as plain numbers`)
|
|
298
391
|
}
|
|
392
|
+
if (u.kind !== kind) {
|
|
393
|
+
throw new SLError(`uniform "${name}" is declared as both an int and a float`)
|
|
394
|
+
}
|
|
299
395
|
// A second read may say nothing about the control, or the same thing again.
|
|
300
396
|
const merged = { ...controlOf(u) }
|
|
301
397
|
for (const f of CONTROL_FIELDS) {
|
|
@@ -312,10 +408,13 @@ export class Builder {
|
|
|
312
408
|
}
|
|
313
409
|
const problem = controlProblem(type, value, control)
|
|
314
410
|
if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
|
|
411
|
+
const kindProblem = kind === undefined ? null : intUniformProblem(type, value)
|
|
412
|
+
if (kindProblem !== null) throw new SLError(`uniform "${name}": ${kindProblem}`)
|
|
315
413
|
const slot = this.uniforms.length
|
|
316
414
|
if (slot >= UNIFORM_SLOTS) throw new SLError(tooManyUniforms(slot + 1, "program"))
|
|
317
415
|
const decl: UniformDecl = { name, type, value: value.slice() }
|
|
318
416
|
if (colour) decl.colour = true
|
|
417
|
+
if (kind !== undefined) decl.kind = kind
|
|
319
418
|
Object.assign(decl, controlOf(control))
|
|
320
419
|
this.uniforms.push(decl)
|
|
321
420
|
return this.add({ k: "uniform", type, slot })
|
|
@@ -348,9 +447,7 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
|
348
447
|
const ref = stack.pop()!
|
|
349
448
|
if (keep.has(ref)) continue
|
|
350
449
|
keep.add(ref)
|
|
351
|
-
const
|
|
352
|
-
if (n.k === "swizzle") stack.push(n.src)
|
|
353
|
-
else if (n.k === "call") for (const a of n.args) stack.push(a)
|
|
450
|
+
for (const a of operands(nodes[ref]!)) stack.push(a)
|
|
354
451
|
}
|
|
355
452
|
// Ascending, which is still topological because a node only refers backwards.
|
|
356
453
|
return [...keep].sort((x, y) => x - y)
|
|
@@ -372,12 +469,17 @@ export function inputsUsed(p: Program): InputName[] {
|
|
|
372
469
|
|
|
373
470
|
/** Structural key for hash consing. Order matters and is fixed by the node shape. */
|
|
374
471
|
function keyOf(n: SLNode): string {
|
|
472
|
+
const kind = "kind" in n && n.kind !== undefined ? `@${n.kind}` : ""
|
|
375
473
|
switch (n.k) {
|
|
376
|
-
case "const": return `c:${n.type}:${n.v.map(
|
|
474
|
+
case "const": return `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}${kind}`
|
|
377
475
|
case "input": return `i:${n.name}`
|
|
378
476
|
case "uniform": return `u:${n.slot}`
|
|
379
|
-
case "swizzle": return `s:${n.src}:${n.chans.join("")}`
|
|
380
|
-
case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
|
|
477
|
+
case "swizzle": return `s:${n.src}:${n.chans.join("")}${kind}`
|
|
478
|
+
case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}${kind}`
|
|
479
|
+
case "param": return `p:${n.loop}:${n.index}`
|
|
480
|
+
case "proj": return `j:${n.src}:${n.index}`
|
|
481
|
+
case "if": return `?:${n.cond}:${n.then.join(",")}:${n.else.join(",")}`
|
|
482
|
+
case "loop": return `L:${n.max}:${n.init.join(",")}:${n.cond}:${n.next.join(",")}`
|
|
381
483
|
}
|
|
382
484
|
}
|
|
383
485
|
|
|
@@ -390,6 +492,11 @@ export function fixed(n: number): string {
|
|
|
390
492
|
return Object.is(n, -0) ? "0" : n.toPrecision(9)
|
|
391
493
|
}
|
|
392
494
|
|
|
495
|
+
/** A constant's value as keyed and hashed: a float at `fixed`'s precision, an int, a uint or a bool exactly. */
|
|
496
|
+
function constText(n: number, kind: SLKind | undefined): string {
|
|
497
|
+
return kind === undefined ? fixed(n) : String(n)
|
|
498
|
+
}
|
|
499
|
+
|
|
393
500
|
export function widthName(t: SLType): string {
|
|
394
501
|
return t === 1 ? "float" : `vec${t}`
|
|
395
502
|
}
|
|
@@ -431,14 +538,33 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
431
538
|
// Only where every backend computes the same bits either way: min and max
|
|
432
539
|
// are left out, since which of -0 and +0 they return depends on the order.
|
|
433
540
|
const digest = new Map<NodeRef, string>()
|
|
541
|
+
const depth = loopDepths(nodes, result)
|
|
434
542
|
|
|
435
543
|
const of = (ref: NodeRef): string => {
|
|
436
544
|
const seen = digest.get(ref)
|
|
437
545
|
if (seen !== undefined) return seen
|
|
438
|
-
const n = nodes[ref]
|
|
546
|
+
const n = nodes[ref]!
|
|
439
547
|
let body: string
|
|
440
548
|
switch (n.k) {
|
|
441
|
-
|
|
549
|
+
// Control nodes have no type of their own: their projs carry it.
|
|
550
|
+
case "if": {
|
|
551
|
+
body = `?:${of(n.cond)}:${n.then.map(of).join(",")}:${n.else.map(of).join(",")}`
|
|
552
|
+
const d = fnv1a(body)
|
|
553
|
+
digest.set(ref, d)
|
|
554
|
+
return d
|
|
555
|
+
}
|
|
556
|
+
case "loop": {
|
|
557
|
+
body = `L:${n.max}:${n.init.map(of).join(",")}:${of(n.cond)}:${n.next.map(of).join(",")}`
|
|
558
|
+
const d = fnv1a(body)
|
|
559
|
+
digest.set(ref, d)
|
|
560
|
+
return d
|
|
561
|
+
}
|
|
562
|
+
// By nesting depth, not by id: two programs that nest the same
|
|
563
|
+
// loops the same way agree however their ids were handed out, and
|
|
564
|
+
// two loops nested in one another never share a digest for a param.
|
|
565
|
+
case "param": body = `p:${depth.get(n.loop) ?? 1}:${n.index}`; break
|
|
566
|
+
case "proj": body = `j:${of(n.src)}:${n.index}`; break
|
|
567
|
+
case "const": body = `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}`; break
|
|
442
568
|
case "input": body = `i:${n.name}`; break
|
|
443
569
|
// By slot rather than by name: the slot is what the generated shader
|
|
444
570
|
// lays out, so two programs whose uniforms differ only in name still
|
|
@@ -447,8 +573,8 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
447
573
|
case "uniform": body = `u:${n.slot}:${n.type}`; break
|
|
448
574
|
case "swizzle": {
|
|
449
575
|
const src = nodes[n.src]!
|
|
450
|
-
body = src.k === "const"
|
|
451
|
-
? `c:${n.type}:${n.chans.map((c) =>
|
|
576
|
+
body = src.k === "const" && src.kind === n.kind
|
|
577
|
+
? `c:${n.type}:${n.chans.map((c) => constText(src.v[c]!, src.kind)).join(",")}`
|
|
452
578
|
: `s:${of(n.src)}:${n.chans.join("")}`
|
|
453
579
|
break
|
|
454
580
|
}
|
|
@@ -459,7 +585,7 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
459
585
|
break
|
|
460
586
|
}
|
|
461
587
|
}
|
|
462
|
-
const d = fnv1a(body + "|" + n.type)
|
|
588
|
+
const d = fnv1a(body + "|" + n.type + (n.kind === undefined ? "" : n.kind))
|
|
463
589
|
digest.set(ref, d)
|
|
464
590
|
return d
|
|
465
591
|
}
|
|
@@ -473,12 +599,44 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
473
599
|
// includes the library's text when it compiles, and none caches a compiled
|
|
474
600
|
// program by hash across a library update, so the new text reaches every
|
|
475
601
|
// shader without a bump.
|
|
476
|
-
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes)}`, of(result)]
|
|
602
|
+
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result)}`, of(result)]
|
|
477
603
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
478
604
|
for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
|
|
479
605
|
return fnv1a(parts.join("|"))
|
|
480
606
|
}
|
|
481
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Each reachable loop's nesting depth, 1 for a loop inside no other: 1 more than
|
|
610
|
+
* the deepest loop whose params it reads. Read from the graph, so it does not
|
|
611
|
+
* depend on the order anything was built or walked in.
|
|
612
|
+
*/
|
|
613
|
+
function loopDepths(nodes: readonly SLNode[], result: NodeRef): Map<number, number> {
|
|
614
|
+
const free = new Map<NodeRef, Set<number>>()
|
|
615
|
+
const loops = new Map<number, Extract<SLNode, { k: "loop" }>>()
|
|
616
|
+
const freeOf = (ref: NodeRef): Set<number> => free.get(ref) ?? new Set()
|
|
617
|
+
for (const ref of reachable(nodes as SLNode[], result)) {
|
|
618
|
+
const n = nodes[ref]!
|
|
619
|
+
const out = new Set<number>()
|
|
620
|
+
if (n.k === "param") out.add(n.loop)
|
|
621
|
+
for (const a of operands(n)) for (const l of freeOf(a)) out.add(l)
|
|
622
|
+
if (n.k === "loop") { out.delete(n.id); loops.set(n.id, n) }
|
|
623
|
+
free.set(ref, out)
|
|
624
|
+
}
|
|
625
|
+
const loopFree = new Map<number, Set<number>>()
|
|
626
|
+
for (const [ref, n] of nodes.entries()) if (n.k === "loop" && free.has(ref)) loopFree.set(n.id, free.get(ref)!)
|
|
627
|
+
const depth = new Map<number, number>()
|
|
628
|
+
const of = (id: number): number => {
|
|
629
|
+
const seen = depth.get(id)
|
|
630
|
+
if (seen !== undefined) return seen
|
|
631
|
+
let d = 1
|
|
632
|
+
for (const outer of loopFree.get(id) ?? []) d = Math.max(d, of(outer) + 1)
|
|
633
|
+
depth.set(id, d)
|
|
634
|
+
return d
|
|
635
|
+
}
|
|
636
|
+
for (const id of loops.keys()) of(id)
|
|
637
|
+
return depth
|
|
638
|
+
}
|
|
639
|
+
|
|
482
640
|
/** The control fields of `c` that are set, copied, in `CONTROL_FIELDS` order. */
|
|
483
641
|
export function controlOf(c: UniformControl): UniformControl {
|
|
484
642
|
const out: UniformControl = {}
|
|
@@ -500,20 +658,32 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
500
658
|
* predates the bump still reads it. Every node counts, used or not: the JSON
|
|
501
659
|
* carries them all, and an older reader has to refuse by the version rather
|
|
502
660
|
* than halfway through the nodes.
|
|
661
|
+
*
|
|
662
|
+
* Only the nodes the result reads count: lowering leaves dead nodes behind (an
|
|
663
|
+
* int constant a fold used up, say), and they draw nothing. `toJSON` writes
|
|
664
|
+
* only the live ones, so an older reader never meets a dead newer node.
|
|
503
665
|
*/
|
|
504
|
-
export function programVersion(nodes: readonly SLNode[]): number {
|
|
666
|
+
export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
|
|
505
667
|
let v = 2
|
|
506
|
-
for (const
|
|
668
|
+
for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
|
|
507
669
|
return v
|
|
508
670
|
}
|
|
509
671
|
|
|
672
|
+
/** The ops IR 4 added. */
|
|
673
|
+
const IR4_OPS = new Set<number>([
|
|
674
|
+
SLOP.CAST, SLOP.LT, SLOP.LE, SLOP.GT, SLOP.GE, SLOP.EQ, SLOP.NE, SLOP.AND, SLOP.OR, SLOP.NOT,
|
|
675
|
+
SLOP.BIT_AND, SLOP.BIT_OR, SLOP.BIT_XOR, SLOP.BIT_NOT, SLOP.SHL, SLOP.SHR, SLOP.CHOOSE,
|
|
676
|
+
])
|
|
677
|
+
|
|
510
678
|
/**
|
|
511
679
|
* The version that added this node's form. An sdf or octave call with one
|
|
512
680
|
* operand is the form before IR 3, which only a file an older compiler wrote
|
|
513
681
|
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
514
682
|
*/
|
|
515
683
|
function nodeVersion(n: SLNode): number {
|
|
684
|
+
if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
|
|
516
685
|
if (n.k !== "call") return 2
|
|
686
|
+
if (IR4_OPS.has(n.op)) return 4
|
|
517
687
|
switch (n.op) {
|
|
518
688
|
case SLOP.SAMPLE_LOD: return 3
|
|
519
689
|
case SLOP.SDF:
|
|
@@ -537,7 +707,7 @@ function nodeVersion(n: SLNode): number {
|
|
|
537
707
|
* one immediate
|
|
538
708
|
*/
|
|
539
709
|
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
540
|
-
const widths = n.args.map((a) => nodes[a]
|
|
710
|
+
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
541
711
|
switch (n.op) {
|
|
542
712
|
case SLOP.SDF:
|
|
543
713
|
if (widths === "2,4,2" && n.imm?.length === 1) return null
|
|
@@ -588,8 +758,9 @@ export const SL_HASH_VERSION = 2
|
|
|
588
758
|
* 2 an SDF call carries up to six shape parameters, not four (#129)
|
|
589
759
|
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
590
760
|
* operands, where they were immediates
|
|
761
|
+
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
591
762
|
*/
|
|
592
|
-
export const SL_IR_VERSION =
|
|
763
|
+
export const SL_IR_VERSION = 4
|
|
593
764
|
|
|
594
765
|
function fnv1a(s: string): string {
|
|
595
766
|
let h = 0x811c9dc5
|
package/src/lang/ast.ts
CHANGED
|
@@ -11,41 +11,71 @@
|
|
|
11
11
|
* pass over the source.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import type { SLKind } from "../ir"
|
|
14
15
|
import type { Pos } from "./lexer"
|
|
15
16
|
|
|
16
|
-
/** The
|
|
17
|
-
export type TypeName = "float" | "float2" | "float3" | "float4"
|
|
17
|
+
/** The value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
|
|
18
|
+
export type TypeName = "float" | "float2" | "float3" | "float4" | "int" | "uint" | "bool"
|
|
18
19
|
|
|
19
20
|
export const TYPE_WIDTH: Record<TypeName, 1 | 2 | 3 | 4> = {
|
|
20
|
-
float: 1, float2: 2, float3: 3, float4: 4,
|
|
21
|
+
float: 1, float2: 2, float3: 3, float4: 4, int: 1, uint: 1, bool: 1,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What a type holds beside its width; the floats hold nothing else. */
|
|
25
|
+
export const TYPE_KIND: Record<TypeName, SLKind | undefined> = {
|
|
26
|
+
float: undefined, float2: undefined, float3: undefined, float4: undefined, int: "int", uint: "uint", bool: "bool",
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export type BinaryOp =
|
|
24
30
|
| "+" | "-" | "*" | "/" | "%"
|
|
25
31
|
| "<" | "<=" | ">" | ">=" | "==" | "!="
|
|
26
32
|
| "&&" | "||"
|
|
33
|
+
| "&" | "|" | "^" | "<<" | ">>"
|
|
27
34
|
|
|
28
35
|
export type Expr =
|
|
29
|
-
|
|
36
|
+
/**
|
|
37
|
+
* `whole` is a number written with no point and no exponent, `3` not `3.0`:
|
|
38
|
+
* a float, except beside an int, where it is an int. `unsigned` is `3u`.
|
|
39
|
+
*/
|
|
40
|
+
| { k: "num"; value: number; whole: boolean; unsigned?: boolean; pos: Pos }
|
|
41
|
+
| { k: "bool"; value: boolean; pos: Pos }
|
|
30
42
|
| { k: "hex"; hex: string; pos: Pos }
|
|
31
43
|
| { k: "ident"; name: string; pos: Pos }
|
|
32
44
|
/** A swizzle, or the shape half of `sdf.circle`. Which one is decided later. */
|
|
33
45
|
| { k: "member"; obj: Expr; name: string; pos: Pos }
|
|
34
46
|
| { k: "call"; callee: Expr; args: Expr[]; pos: Pos }
|
|
35
|
-
| { k: "unary"; op: "-" | "+" | "!"; arg: Expr; pos: Pos }
|
|
47
|
+
| { k: "unary"; op: "-" | "+" | "!" | "~"; arg: Expr; pos: Pos }
|
|
36
48
|
| { k: "binary"; op: BinaryOp; a: Expr; b: Expr; pos: Pos }
|
|
37
49
|
| { k: "cond"; cond: Expr; then: Expr; else: Expr; pos: Pos }
|
|
38
50
|
|
|
39
|
-
export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%="
|
|
51
|
+
export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
|
|
52
|
+
|
|
53
|
+
/** One `case` group of a switch: its labels, `null` for `default`, and its body without the closing break. */
|
|
54
|
+
export interface SwitchCase {
|
|
55
|
+
labels: Array<Expr | null>
|
|
56
|
+
body: Stmt[]
|
|
57
|
+
/** Whether it ended in a break, which is dropped from `body`. */
|
|
58
|
+
closed: boolean
|
|
59
|
+
pos: Pos
|
|
60
|
+
}
|
|
40
61
|
|
|
41
62
|
export type Stmt =
|
|
42
63
|
| { k: "var"; type: TypeName; name: string; init: Expr; pos: Pos }
|
|
43
64
|
/** A compile time constant: usable as a value and as a `for` bound. */
|
|
44
65
|
| { k: "const"; type: TypeName; name: string; init: Expr; pos: Pos }
|
|
66
|
+
/** `x++` is `x += 1`, with the 1 whole. */
|
|
45
67
|
| { k: "assign"; target: Expr; op: AssignOp; value: Expr; pos: Pos }
|
|
46
68
|
| { k: "if"; cond: Expr; then: Stmt[]; else: Stmt[]; pos: Pos }
|
|
47
|
-
|
|
69
|
+
/** `for (type counter = from; cond; update)`: the update changes the counter and nothing else. */
|
|
70
|
+
| { k: "for"; type: TypeName; counter: string; from: Expr; cond: Expr; update: Extract<Stmt, { k: "assign" }>; body: Stmt[]; pos: Pos }
|
|
71
|
+
| { k: "while"; cond: Expr; body: Stmt[]; pos: Pos }
|
|
72
|
+
| { k: "break"; pos: Pos }
|
|
73
|
+
| { k: "continue"; pos: Pos }
|
|
74
|
+
/** Cases never fall through: each ends in break, return or continue, and the break is dropped here. */
|
|
75
|
+
| { k: "switch"; value: Expr; cases: SwitchCase[]; pos: Pos }
|
|
48
76
|
| { k: "return"; value: Expr; pos: Pos }
|
|
77
|
+
/** `{ ... }` on its own: a body with its own scope, as a case's braces are. */
|
|
78
|
+
| { k: "block"; body: Stmt[]; pos: Pos }
|
|
49
79
|
|
|
50
80
|
export interface Param {
|
|
51
81
|
type: TypeName
|