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/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
|
|
@@ -254,6 +317,10 @@ export class Builder {
|
|
|
254
317
|
readonly textures: TextureDecl[] = []
|
|
255
318
|
private readonly interned = new Map<string, NodeRef>()
|
|
256
319
|
|
|
320
|
+
/** The next loop's id: unique within the program, so two loops' params never intern to one node. */
|
|
321
|
+
private loops = 0
|
|
322
|
+
newLoop(): number { return this.loops++ }
|
|
323
|
+
|
|
257
324
|
add(node: SLNode): NodeRef {
|
|
258
325
|
if (this.nodes.length >= MAX_NODES) {
|
|
259
326
|
throw new SLError(`a program may hold at most ${MAX_NODES} operations`)
|
|
@@ -266,7 +333,7 @@ export class Builder {
|
|
|
266
333
|
return ref
|
|
267
334
|
}
|
|
268
335
|
|
|
269
|
-
call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[]): NodeRef {
|
|
336
|
+
call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[], kind?: SLKind): NodeRef {
|
|
270
337
|
const arity = SL_ARITY[op]
|
|
271
338
|
if (arity >= 0 && args.length !== arity) {
|
|
272
339
|
throw new SLError(`${SL_NAME[op]} takes ${arity} argument(s), got ${args.length}`)
|
|
@@ -276,14 +343,19 @@ export class Builder {
|
|
|
276
343
|
throw new SLError(`${SL_NAME[op]} refers to a node that does not exist yet`)
|
|
277
344
|
}
|
|
278
345
|
}
|
|
279
|
-
|
|
346
|
+
const node: SLNode = imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm }
|
|
347
|
+
if (kind !== undefined) node.kind = kind
|
|
348
|
+
return this.add(node)
|
|
280
349
|
}
|
|
281
350
|
|
|
282
|
-
constant(v: number[]): NodeRef {
|
|
351
|
+
constant(v: number[], kind?: SLKind): NodeRef {
|
|
283
352
|
for (const n of v) {
|
|
284
353
|
if (!Number.isFinite(n)) throw new SLError(`a constant must be finite, got ${n}`)
|
|
354
|
+
if (kind !== undefined && !Number.isInteger(n)) throw new SLError(`an ${kind} constant must be a whole number, got ${n}`)
|
|
285
355
|
}
|
|
286
|
-
|
|
356
|
+
const node: SLNode = { k: "const", type: v.length as SLType, v: v.slice() }
|
|
357
|
+
if (kind !== undefined) node.kind = kind
|
|
358
|
+
return this.add(node)
|
|
287
359
|
}
|
|
288
360
|
|
|
289
361
|
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
|
|
@@ -348,9 +420,7 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
|
348
420
|
const ref = stack.pop()!
|
|
349
421
|
if (keep.has(ref)) continue
|
|
350
422
|
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)
|
|
423
|
+
for (const a of operands(nodes[ref]!)) stack.push(a)
|
|
354
424
|
}
|
|
355
425
|
// Ascending, which is still topological because a node only refers backwards.
|
|
356
426
|
return [...keep].sort((x, y) => x - y)
|
|
@@ -372,12 +442,17 @@ export function inputsUsed(p: Program): InputName[] {
|
|
|
372
442
|
|
|
373
443
|
/** Structural key for hash consing. Order matters and is fixed by the node shape. */
|
|
374
444
|
function keyOf(n: SLNode): string {
|
|
445
|
+
const kind = "kind" in n && n.kind !== undefined ? `@${n.kind}` : ""
|
|
375
446
|
switch (n.k) {
|
|
376
|
-
case "const": return `c:${n.type}:${n.v.map(
|
|
447
|
+
case "const": return `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}${kind}`
|
|
377
448
|
case "input": return `i:${n.name}`
|
|
378
449
|
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(",")}`
|
|
450
|
+
case "swizzle": return `s:${n.src}:${n.chans.join("")}${kind}`
|
|
451
|
+
case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}${kind}`
|
|
452
|
+
case "param": return `p:${n.loop}:${n.index}`
|
|
453
|
+
case "proj": return `j:${n.src}:${n.index}`
|
|
454
|
+
case "if": return `?:${n.cond}:${n.then.join(",")}:${n.else.join(",")}`
|
|
455
|
+
case "loop": return `L:${n.max}:${n.init.join(",")}:${n.cond}:${n.next.join(",")}`
|
|
381
456
|
}
|
|
382
457
|
}
|
|
383
458
|
|
|
@@ -390,6 +465,11 @@ export function fixed(n: number): string {
|
|
|
390
465
|
return Object.is(n, -0) ? "0" : n.toPrecision(9)
|
|
391
466
|
}
|
|
392
467
|
|
|
468
|
+
/** A constant's value as keyed and hashed: a float at `fixed`'s precision, an int, a uint or a bool exactly. */
|
|
469
|
+
function constText(n: number, kind: SLKind | undefined): string {
|
|
470
|
+
return kind === undefined ? fixed(n) : String(n)
|
|
471
|
+
}
|
|
472
|
+
|
|
393
473
|
export function widthName(t: SLType): string {
|
|
394
474
|
return t === 1 ? "float" : `vec${t}`
|
|
395
475
|
}
|
|
@@ -431,14 +511,33 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
431
511
|
// Only where every backend computes the same bits either way: min and max
|
|
432
512
|
// are left out, since which of -0 and +0 they return depends on the order.
|
|
433
513
|
const digest = new Map<NodeRef, string>()
|
|
514
|
+
const depth = loopDepths(nodes, result)
|
|
434
515
|
|
|
435
516
|
const of = (ref: NodeRef): string => {
|
|
436
517
|
const seen = digest.get(ref)
|
|
437
518
|
if (seen !== undefined) return seen
|
|
438
|
-
const n = nodes[ref]
|
|
519
|
+
const n = nodes[ref]!
|
|
439
520
|
let body: string
|
|
440
521
|
switch (n.k) {
|
|
441
|
-
|
|
522
|
+
// Control nodes have no type of their own: their projs carry it.
|
|
523
|
+
case "if": {
|
|
524
|
+
body = `?:${of(n.cond)}:${n.then.map(of).join(",")}:${n.else.map(of).join(",")}`
|
|
525
|
+
const d = fnv1a(body)
|
|
526
|
+
digest.set(ref, d)
|
|
527
|
+
return d
|
|
528
|
+
}
|
|
529
|
+
case "loop": {
|
|
530
|
+
body = `L:${n.max}:${n.init.map(of).join(",")}:${of(n.cond)}:${n.next.map(of).join(",")}`
|
|
531
|
+
const d = fnv1a(body)
|
|
532
|
+
digest.set(ref, d)
|
|
533
|
+
return d
|
|
534
|
+
}
|
|
535
|
+
// By nesting depth, not by id: two programs that nest the same
|
|
536
|
+
// loops the same way agree however their ids were handed out, and
|
|
537
|
+
// two loops nested in one another never share a digest for a param.
|
|
538
|
+
case "param": body = `p:${depth.get(n.loop) ?? 1}:${n.index}`; break
|
|
539
|
+
case "proj": body = `j:${of(n.src)}:${n.index}`; break
|
|
540
|
+
case "const": body = `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}`; break
|
|
442
541
|
case "input": body = `i:${n.name}`; break
|
|
443
542
|
// By slot rather than by name: the slot is what the generated shader
|
|
444
543
|
// lays out, so two programs whose uniforms differ only in name still
|
|
@@ -447,8 +546,8 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
447
546
|
case "uniform": body = `u:${n.slot}:${n.type}`; break
|
|
448
547
|
case "swizzle": {
|
|
449
548
|
const src = nodes[n.src]!
|
|
450
|
-
body = src.k === "const"
|
|
451
|
-
? `c:${n.type}:${n.chans.map((c) =>
|
|
549
|
+
body = src.k === "const" && src.kind === n.kind
|
|
550
|
+
? `c:${n.type}:${n.chans.map((c) => constText(src.v[c]!, src.kind)).join(",")}`
|
|
452
551
|
: `s:${of(n.src)}:${n.chans.join("")}`
|
|
453
552
|
break
|
|
454
553
|
}
|
|
@@ -459,7 +558,7 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
459
558
|
break
|
|
460
559
|
}
|
|
461
560
|
}
|
|
462
|
-
const d = fnv1a(body + "|" + n.type)
|
|
561
|
+
const d = fnv1a(body + "|" + n.type + (n.kind === undefined ? "" : n.kind))
|
|
463
562
|
digest.set(ref, d)
|
|
464
563
|
return d
|
|
465
564
|
}
|
|
@@ -473,12 +572,44 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
473
572
|
// includes the library's text when it compiles, and none caches a compiled
|
|
474
573
|
// program by hash across a library update, so the new text reaches every
|
|
475
574
|
// shader without a bump.
|
|
476
|
-
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes)}`, of(result)]
|
|
575
|
+
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result)}`, of(result)]
|
|
477
576
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
478
577
|
for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
|
|
479
578
|
return fnv1a(parts.join("|"))
|
|
480
579
|
}
|
|
481
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Each reachable loop's nesting depth, 1 for a loop inside no other: 1 more than
|
|
583
|
+
* the deepest loop whose params it reads. Read from the graph, so it does not
|
|
584
|
+
* depend on the order anything was built or walked in.
|
|
585
|
+
*/
|
|
586
|
+
function loopDepths(nodes: readonly SLNode[], result: NodeRef): Map<number, number> {
|
|
587
|
+
const free = new Map<NodeRef, Set<number>>()
|
|
588
|
+
const loops = new Map<number, Extract<SLNode, { k: "loop" }>>()
|
|
589
|
+
const freeOf = (ref: NodeRef): Set<number> => free.get(ref) ?? new Set()
|
|
590
|
+
for (const ref of reachable(nodes as SLNode[], result)) {
|
|
591
|
+
const n = nodes[ref]!
|
|
592
|
+
const out = new Set<number>()
|
|
593
|
+
if (n.k === "param") out.add(n.loop)
|
|
594
|
+
for (const a of operands(n)) for (const l of freeOf(a)) out.add(l)
|
|
595
|
+
if (n.k === "loop") { out.delete(n.id); loops.set(n.id, n) }
|
|
596
|
+
free.set(ref, out)
|
|
597
|
+
}
|
|
598
|
+
const loopFree = new Map<number, Set<number>>()
|
|
599
|
+
for (const [ref, n] of nodes.entries()) if (n.k === "loop" && free.has(ref)) loopFree.set(n.id, free.get(ref)!)
|
|
600
|
+
const depth = new Map<number, number>()
|
|
601
|
+
const of = (id: number): number => {
|
|
602
|
+
const seen = depth.get(id)
|
|
603
|
+
if (seen !== undefined) return seen
|
|
604
|
+
let d = 1
|
|
605
|
+
for (const outer of loopFree.get(id) ?? []) d = Math.max(d, of(outer) + 1)
|
|
606
|
+
depth.set(id, d)
|
|
607
|
+
return d
|
|
608
|
+
}
|
|
609
|
+
for (const id of loops.keys()) of(id)
|
|
610
|
+
return depth
|
|
611
|
+
}
|
|
612
|
+
|
|
482
613
|
/** The control fields of `c` that are set, copied, in `CONTROL_FIELDS` order. */
|
|
483
614
|
export function controlOf(c: UniformControl): UniformControl {
|
|
484
615
|
const out: UniformControl = {}
|
|
@@ -500,20 +631,32 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
500
631
|
* predates the bump still reads it. Every node counts, used or not: the JSON
|
|
501
632
|
* carries them all, and an older reader has to refuse by the version rather
|
|
502
633
|
* than halfway through the nodes.
|
|
634
|
+
*
|
|
635
|
+
* Only the nodes the result reads count: lowering leaves dead nodes behind (an
|
|
636
|
+
* int constant a fold used up, say), and they draw nothing. `toJSON` writes
|
|
637
|
+
* only the live ones, so an older reader never meets a dead newer node.
|
|
503
638
|
*/
|
|
504
|
-
export function programVersion(nodes: readonly SLNode[]): number {
|
|
639
|
+
export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
|
|
505
640
|
let v = 2
|
|
506
|
-
for (const
|
|
641
|
+
for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
|
|
507
642
|
return v
|
|
508
643
|
}
|
|
509
644
|
|
|
645
|
+
/** The ops IR 4 added. */
|
|
646
|
+
const IR4_OPS = new Set<number>([
|
|
647
|
+
SLOP.CAST, SLOP.LT, SLOP.LE, SLOP.GT, SLOP.GE, SLOP.EQ, SLOP.NE, SLOP.AND, SLOP.OR, SLOP.NOT,
|
|
648
|
+
SLOP.BIT_AND, SLOP.BIT_OR, SLOP.BIT_XOR, SLOP.BIT_NOT, SLOP.SHL, SLOP.SHR, SLOP.CHOOSE,
|
|
649
|
+
])
|
|
650
|
+
|
|
510
651
|
/**
|
|
511
652
|
* The version that added this node's form. An sdf or octave call with one
|
|
512
653
|
* operand is the form before IR 3, which only a file an older compiler wrote
|
|
513
654
|
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
514
655
|
*/
|
|
515
656
|
function nodeVersion(n: SLNode): number {
|
|
657
|
+
if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
|
|
516
658
|
if (n.k !== "call") return 2
|
|
659
|
+
if (IR4_OPS.has(n.op)) return 4
|
|
517
660
|
switch (n.op) {
|
|
518
661
|
case SLOP.SAMPLE_LOD: return 3
|
|
519
662
|
case SLOP.SDF:
|
|
@@ -537,7 +680,7 @@ function nodeVersion(n: SLNode): number {
|
|
|
537
680
|
* one immediate
|
|
538
681
|
*/
|
|
539
682
|
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
540
|
-
const widths = n.args.map((a) => nodes[a]
|
|
683
|
+
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
541
684
|
switch (n.op) {
|
|
542
685
|
case SLOP.SDF:
|
|
543
686
|
if (widths === "2,4,2" && n.imm?.length === 1) return null
|
|
@@ -588,8 +731,9 @@ export const SL_HASH_VERSION = 2
|
|
|
588
731
|
* 2 an SDF call carries up to six shape parameters, not four (#129)
|
|
589
732
|
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
590
733
|
* operands, where they were immediates
|
|
734
|
+
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
591
735
|
*/
|
|
592
|
-
export const SL_IR_VERSION =
|
|
736
|
+
export const SL_IR_VERSION = 4
|
|
593
737
|
|
|
594
738
|
function fnv1a(s: string): string {
|
|
595
739
|
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
|