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/src/lang/lower.ts CHANGED
@@ -8,18 +8,28 @@
8
8
  * hash the same, and why the existing GPU and codegen golden tests cover the
9
9
  * text form without being told about it.
10
10
  *
11
- * Four things happen here that the file's author does not see:
11
+ * Five things happen here that the file's author does not see:
12
12
  *
13
13
  * **Locals become SSA.** A local is a JavaScript binding holding a recorded
14
14
  * value, so `p = p * 2;` rebinds and the IR never learns that a variable
15
15
  * existed. Reassignment is free; it is not a store.
16
16
  *
17
- * **`for` unrolls.** The bounds are constants, so the body is lowered once per
18
- * iteration with the counter substituted as a number.
17
+ * **`if` is a real branch** (`sl.branch`). Both sides are lowered, and every
18
+ * local either side changes comes out of the branch, which runs only the side
19
+ * its condition picks. A constant condition folds instead, and the other side
20
+ * is never lowered. What follows an if whose one side always returns goes into
21
+ * the other side, so `if (far) return clear;` skips the rest of the function.
19
22
  *
20
- * **`if` becomes `select`.** Both sides are lowered and every local assigned in
21
- * either is selected at the join. A constant condition folds instead, which is
22
- * the one case where a branch really does disappear.
23
+ * **Leaving early is carried as flags.** A return, a break or a continue that
24
+ * only some ways through reach sets a flag, and everything after it runs under
25
+ * a branch on that flag. So no statement is ever printed that returns or
26
+ * breaks, and every emitter prints the same structured body.
27
+ *
28
+ * **A `for` unrolls when it can**: when its turns are known at build time, are
29
+ * 64 or fewer, and nothing in it leaves early. Otherwise it is a real loop
30
+ * (`sl.loop`) carrying the locals its body changes, with a turn limit that
31
+ * guarantees it stops: the count its constant bound gives, or its uniform
32
+ * bound's Range, or 1024.
23
33
  *
24
34
  * **Functions inline.** A call lowers the callee's body against its arguments
25
35
  * in a fresh scope. `ring(p, 0.3, 0.01)` costs exactly what writing the body
@@ -29,31 +39,81 @@
29
39
  * `uv.mul(8)`, not as `uv.mul(sl.float(8))`, because the first broadcasts to a
30
40
  * float2 constant and the second builds a float plus a swizzle. Same picture,
31
41
  * different graph, different hash, so the distinction is load bearing rather
32
- * than an optimisation.
42
+ * than an optimisation. An int constant is a bigint and a bool constant a
43
+ * boolean for the same reason: folded until something needs a node.
44
+ *
45
+ * A COMPARISON WAITS to be used (`Test`). Where arithmetic or a `?:` reads it,
46
+ * it is the float that is 0 or 1 it always was, built the way it always was,
47
+ * so a program that uses no new construct keeps its graph and its hash. Where
48
+ * an if, a loop or a bool local reads it, it is a bool.
33
49
  */
34
50
 
35
- import { controlProblem, DERIVED_INPUTS, INPUTS, type Program, type SLType, type UniformControl } from "../ir"
51
+ import {
52
+ controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, truncateHeld, writtenColour,
53
+ type Program, type SLKind, type SLType, type UniformControl,
54
+ } from "../ir"
55
+ import { SLOP } from "../ops"
36
56
  import type { SlSdfKind } from "../shapes"
37
57
  import * as sl from "../sl"
38
- import type { Num, Val } from "../sl"
39
- import { TYPE_WIDTH, type Expr, type FuncDecl, type Stmt, type TypeName } from "./ast"
58
+ import { Val, type Num } from "../sl"
59
+ import { TYPE_KIND, TYPE_WIDTH, type BinaryOp, type Expr, type FuncDecl, type Stmt, type TypeName } from "./ast"
40
60
  import { readAttributes } from "./attributes"
41
61
  import { BUILTINS } from "./builtins"
42
- import type { Checked } from "./check"
62
+ import { terminates, type Checked } from "./check"
43
63
  import { SLParseError, type Pos } from "./lexer"
44
64
 
45
- /** A lowered value. A plain number is one that has not had to become a node yet. */
46
- type LV = Val | number
65
+ /**
66
+ * A comparison, or logic over comparisons, held until its use says which form
67
+ * it takes: `float`, the 0 or 1 every program built before bools existed, or
68
+ * `bool`, for an if, a loop and a bool local. Each is built once, when first read.
69
+ */
70
+ class Test {
71
+ private f: number | Val | undefined
72
+ private b: boolean | Val | undefined
73
+ constructor(
74
+ readonly width: SLType,
75
+ private readonly makeFloat: () => number | Val,
76
+ private readonly makeBool: () => boolean | Val,
77
+ ) {}
78
+ get float(): number | Val { return this.f ??= this.makeFloat() }
79
+ get bool(): boolean | Val { return this.b ??= this.makeBool() }
80
+ }
81
+
82
+ /**
83
+ * A lowered value. A number is a float, a bigint an int and a boolean a bool,
84
+ * none of which has had to become a node yet; a Test is a comparison not yet
85
+ * either of its forms.
86
+ */
87
+ type LV = Val | number | bigint | boolean | Test
88
+
89
+ /** A declared type: its width, and its kind when it is not floats. */
90
+ interface VType { width: SLType; kind: SLKind | undefined }
47
91
 
48
- interface Binding {
49
- /** The declared width. A reassignment has to match it. */
50
- width: SLType
92
+ interface Binding extends VType {
93
+ /** Always of the declared type: a Test is resolved and an int is a bigint or an int node. */
51
94
  value: LV
52
95
  }
53
96
 
97
+ /**
98
+ * Where a function body is: its return type, and whether, on the ways through
99
+ * so far, it has returned (and with what), broken out of the innermost loop, or
100
+ * continued it. Each flag is a constant until only some ways through set it.
101
+ */
102
+ interface Frame {
103
+ fn: FuncDecl
104
+ ret: VType
105
+ returned: boolean | Val
106
+ retval: LV | null
107
+ broke: boolean | Val
108
+ continued: boolean | Val
109
+ }
110
+
54
111
  /** How many iterations a `for` may unroll to. `sl.repeat`'s ceiling, for the same reason. */
55
112
  const MAX_UNROLL = 64
56
113
 
114
+ /** The turn limit of a loop whose bound says nothing about how many turns it takes. */
115
+ const DEFAULT_TURNS = 1024
116
+
57
117
  class Scope {
58
118
  private readonly vars = new Map<string, Binding>()
59
119
 
@@ -67,11 +127,17 @@ class Scope {
67
127
  return this.parent?.lookup(name)
68
128
  }
69
129
 
130
+ /** Whether `name` is a local here, rather than a global or nothing. */
131
+ isLocal(name: string): boolean {
132
+ if (this.isGlobal) return false
133
+ return this.vars.has(name) || (this.parent?.isLocal(name) ?? false)
134
+ }
135
+
70
136
  /**
71
- * Every binding an `if` could reassign: the locals, never the globals.
137
+ * Every binding a branch or a loop could change: the locals, never the globals.
72
138
  *
73
139
  * Uniforms, textures, consts and inputs live in the global scope and the
74
- * checker refuses assigning to any of them, so the join never has to
140
+ * checker refuses assigning to any of them, so a join never has to
75
141
  * consider one.
76
142
  */
77
143
  locals(): Binding[] {
@@ -135,17 +201,19 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
135
201
  }
136
202
  }
137
203
 
138
- /** A zero of a width, standing in for a value whose line was refused. */
139
- const blank = (width: SLType, pos: Pos): LV =>
140
- width === 1 ? 0 : at(pos, () => compose(width, new Array<LV>(width).fill(0)))
204
+ /** A zero of a type, standing in for a value whose line was refused, and for a return not yet made. */
205
+ const blank = (t: VType, pos: Pos): LV => {
206
+ if (t.kind === "int") return 0n
207
+ if (t.kind === "bool") return false
208
+ if (t.kind === "uint") return at(pos, () => sl.uint(0))
209
+ return t.width === 1 ? 0 : at(pos, () => compose(t.width, new Array<LV>(t.width).fill(0)))
210
+ }
141
211
 
142
212
  // The globals every helper below reads, built inside the recording callback
143
213
  // and held out here so a function being inlined can see them and nothing
144
214
  // else the caller had in scope.
145
215
  const global = new Scope(null, true)
146
216
  const samplers = new Map<string, sl.Texture>()
147
- /** Where the last `return` lowered was, so an error about it points at it. */
148
- let returnedAt: Pos = main.pos
149
217
 
150
218
  try {
151
219
  return record()
@@ -159,7 +227,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
159
227
  function record(): Program {
160
228
  return sl.program((inputs) => {
161
229
  for (const [name, width] of Object.entries(INPUTS)) {
162
- global.declare(name, { width: width as SLType, value: (inputs as never)[name] })
230
+ global.declare(name, { width: width as SLType, kind: undefined, value: (inputs as never)[name] })
163
231
  }
164
232
  // Read through, so a program that never names one records nothing
165
233
  // for it. The setter only serves a refused assignment, which the
@@ -168,6 +236,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
168
236
  let value: LV | undefined
169
237
  global.declare(name, {
170
238
  width: width as SLType,
239
+ kind: undefined,
171
240
  get value() { return value ??= (inputs as never)[name] },
172
241
  set value(v: LV) { value = v },
173
242
  })
@@ -180,8 +249,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
180
249
  for (const u of unit.uniforms) {
181
250
  const width = TYPE_WIDTH[u.type]
182
251
  const { components, colour } = recover(
183
- () => uniformDefault(u.type, u.init),
184
- () => ({ ...uniformDefault(u.type, null), colour: false }),
252
+ () => uniformDefault(u.type, u.init, u.pos),
253
+ () => ({ ...uniformDefault("float", null, u.pos), colour: false }),
185
254
  )
186
255
  // A hex default says the uniform IS a colour, so every read of it
187
256
  // converts, exactly as a hex literal in an expression does. Without
@@ -202,37 +271,20 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
202
271
  const value = (colour && u.init?.k === "hex") || read.colour
203
272
  ? at(u.pos, () => sl.uniform.colour(u.name, components, width as 3 | 4, control) as unknown as Val)
204
273
  : at(u.pos, () => declareUniform(u.name, u.type, components, control))
205
- global.declare(u.name, { width, value })
274
+ global.declare(u.name, { width, kind: TYPE_KIND[u.type], value })
206
275
  }
207
276
 
208
277
  for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
209
278
 
210
279
  for (const c of unit.consts) {
211
- const width = TYPE_WIDTH[c.type]
212
- const v = recover(() => {
213
- const v = fit(lowerExpr(c.init, global), width, c.pos)
214
- assertWidth(v, width, c.name, c.type, c.pos)
215
- return v
216
- }, () => blank(width, c.pos))
217
- global.declare(c.name, { width, value: v })
280
+ const t = typeOf(c.type)
281
+ const v = recover(() => fit(lowerExpr(c.init, global, want(t)), t, c.name, c.pos), () => blank(t, c.pos))
282
+ global.declare(c.name, { ...t, value: v })
218
283
  }
219
284
 
220
- const out = exec(main.body, new Scope(global), 4)
285
+ const out = callBody(main, new Scope(global))
221
286
  // What a refused line left behind is not the program's colour.
222
- if (errors !== undefined && errors.length > 0) return blank(4, main.pos) as never
223
- // Checked here rather than left to `sl.program`, which would refuse it
224
- // in the EDSL's words ("a vec4") about a file whose types are spelled
225
- // float4, and from outside the reach of a position.
226
- if (out === undefined || typeof out === "number" || out.width !== 4) {
227
- const got = out === undefined ? "nothing"
228
- : typeof out === "number" ? "a single number"
229
- : `a ${widthType(out.width)}`
230
- fail(
231
- `main returns a float4, a colour with alpha, and this returns ${got}. ` +
232
- `Wrap it: float4(value, 1)`,
233
- returnedAt, 6,
234
- )
235
- }
287
+ if (errors !== undefined && errors.length > 0) return blank({ width: 4, kind: undefined }, main.pos) as never
236
288
  return out as never
237
289
  })
238
290
  }
@@ -245,6 +297,10 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
245
297
  case "float2": return sl.uniform.vec2(name, [c[0]!, c[1]!], control) as unknown as Val
246
298
  case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!], control) as unknown as Val
247
299
  case "float4": return sl.uniform.vec4(name, [c[0]!, c[1]!, c[2]!, c[3]!], control) as unknown as Val
300
+ // A float slot, as every host binds, read as the whole number it holds.
301
+ case "int": return sl.int(sl.uniform.float(name, c[0]!, control)) as unknown as Val
302
+ case "uint":
303
+ case "bool": throw new Error(`the checker let a ${type} uniform through`)
248
304
  }
249
305
  }
250
306
 
@@ -258,7 +314,14 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
258
314
  * may read a uniform, so letting a uniform read a const would make the two
259
315
  * tables depend on each other.
260
316
  */
261
- function uniformDefault(type: TypeName, init: Expr | null): { components: number[]; colour: boolean } {
317
+ function uniformDefault(type: TypeName, init: Expr | null, pos: Pos): { components: number[]; colour: boolean } {
318
+ if (type === "uint" || type === "bool") {
319
+ fail(
320
+ `a uniform is a float, a float vector or an int, so not a ${type}. ` +
321
+ (type === "bool" ? "For an on and off switch, write [Toggle] uniform float" : "Write uniform int"),
322
+ pos, 7,
323
+ )
324
+ }
262
325
  const width = TYPE_WIDTH[type]
263
326
  if (init === null) {
264
327
  const zeros = new Array<number>(width).fill(0)
@@ -283,6 +346,9 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
283
346
  init.pos,
284
347
  )
285
348
  }
349
+ if (type === "int" && (c.length !== 1 || !Number.isInteger(c[0]))) {
350
+ fail("an int uniform's default is one whole number", init.pos)
351
+ }
286
352
  if (c.length === 1 && width > 1) return { components: new Array<number>(width).fill(c[0]!), colour: false }
287
353
  if (c.length === 4 && width === 3) return { components: c.slice(0, 3), colour: false }
288
354
  if (c.length !== width) {
@@ -304,7 +370,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
304
370
  if (v === null) return null
305
371
  if (e.op === "+") return v
306
372
  if (e.op === "-") return v.map((n) => -n)
307
- return v.map((n) => 1 - n)
373
+ if (e.op === "!") return v.map((n) => 1 - n)
374
+ return null
308
375
  }
309
376
  case "binary": {
310
377
  const a = constantComponents(e.a)
@@ -334,144 +401,469 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
334
401
  }
335
402
  }
336
403
 
404
+ // MARK: functions
405
+
406
+ /** A function's body run to its return, in `scope`, which holds its parameters. */
407
+ function callBody(fn: FuncDecl, scope: Scope): LV {
408
+ const frame: Frame = { fn, ret: typeOf(fn.ret), returned: false, retval: null, broke: false, continued: false }
409
+ exec(fn.body, scope, frame)
410
+ if (errors !== undefined && errors.length > 0) return frame.retval ?? blank(frame.ret, fn.pos)
411
+ // The checker proved every way through returns, and the flags carry
412
+ // that through to a constant; anything else is this file's mistake.
413
+ if (frame.returned !== true || frame.retval === null) throw new Error(`${fn.name} lowered to a body that does not always return`)
414
+ return frame.retval
415
+ }
416
+
337
417
  // MARK: statements
338
418
 
339
419
  /**
340
- * Runs a body. The value of its `return`, or nothing when it has none.
341
- * `ret` is the width that return has, for the stand in when it is refused.
420
+ * Runs a body. What it does to the locals is in their bindings and what it
421
+ * does to the flow (a return, a break) is in `frame`.
342
422
  */
343
- function exec(body: Stmt[], scope: Scope, ret: SLType): LV | undefined {
344
- for (const s of body) {
345
- if (s.k === "return") {
346
- returnedAt = s.pos
347
- return recover(() => lowerExpr(s.value, scope), () => blank(ret, s.pos))
423
+ function exec(body: Stmt[], scope: Scope, frame: Frame): void {
424
+ for (let i = 0; i < body.length; i++) {
425
+ const stop = stopped(frame)
426
+ if (stop === true) return
427
+ if (stop !== false) {
428
+ // Some ways through what came before left early and some did
429
+ // not: the rest runs only on the ones that did not.
430
+ const rest = body.slice(i)
431
+ fork(stop, scope, frame, () => knownStopped(frame), () => {
432
+ frame.returned = false
433
+ frame.broke = false
434
+ frame.continued = false
435
+ exec(rest, new Scope(scope), frame)
436
+ })
437
+ return
438
+ }
439
+ const s = body[i]!
440
+ if (s.k === "if") {
441
+ if (recover(() => runIf(s, body.slice(i + 1), scope, frame), () => false)) return
442
+ continue
348
443
  }
349
444
  if (s.k === "var" || s.k === "const") {
350
- const width = TYPE_WIDTH[s.type]
351
- const v = recover(() => {
352
- const v = fit(lowerExpr(s.init, scope), width, s.pos)
353
- assertWidth(v, width, s.name, s.type, s.pos)
354
- return v
355
- }, () => blank(width, s.pos))
356
- scope.declare(s.name, { width, value: v })
445
+ const t = typeOf(s.type)
446
+ const v = recover(() => fit(lowerExpr(s.init, scope, want(t)), t, s.name, s.pos), () => blank(t, s.pos))
447
+ scope.declare(s.name, { ...t, value: v })
357
448
  continue
358
449
  }
359
- recover(() => run(s, scope, ret), () => undefined)
360
- }
361
- return undefined
362
- }
363
-
364
- /** An assignment, an if or a for. */
365
- function run(s: Stmt, scope: Scope, ret: SLType): void {
366
- switch (s.k) {
367
- case "assign": {
368
- // The checker allowed exactly two shapes: a local, or a
369
- // swizzle of one with no component named twice.
370
- const member = s.target.k === "member" ? s.target : null
371
- const name = ((member?.obj ?? s.target) as Extract<Expr, { k: "ident" }>).name
372
- const b = scope.lookup(name)!
373
- let v = lowerExpr(s.value, scope)
374
- if (s.op !== "=") {
375
- const op = s.op[0] as ArithOp
376
- const old = member === null ? b.value : lowerExpr(member, scope)
377
- v = at(s.pos, () => arithmetic(op, old, v, s.pos))
378
- }
379
- if (member !== null) v = writeComponents(b, member, v, s.pos)
380
- else v = fit(v, b.width, s.pos)
381
- assertWidth(v, b.width, name, widthType(b.width), s.pos)
382
- b.value = v
383
- break
384
- }
385
- case "if": {
386
- const cond = lowerExpr(s.cond, scope)
387
- if (typeof cond === "number") {
388
- // The one case where a branch really does vanish: the
389
- // other side is never lowered, so it costs nothing.
390
- exec(cond >= 0.5 ? s.then : s.else, new Scope(scope), ret)
391
- break
392
- }
393
- const locals = scope.locals()
394
- const before = locals.map((b) => b.value)
395
- exec(s.then, new Scope(scope), ret)
396
- const whenTrue = locals.map((b) => b.value)
397
- locals.forEach((b, i) => { b.value = before[i]! })
398
- exec(s.else, new Scope(scope), ret)
399
- const whenFalse = locals.map((b) => b.value)
400
- locals.forEach((b, i) => {
401
- const t = whenTrue[i]!
402
- const f = whenFalse[i]!
403
- b.value = same(t, f) ? t : at(s.pos, () => sl.select(cond, t, f))
404
- })
405
- break
406
- }
407
- case "for": {
408
- const from = constantBound(s.from, "start", scope)
409
- const to = constantBound(s.to, "bound", scope)
410
- const step = constantBound(s.step, "step", scope)
411
- if (!(step > 0)) {
412
- fail(
413
- `this loop steps by ${step}, so it never ends. A for loop unrolls, which ` +
414
- `means its count has to be known and finite`,
415
- s.pos,
416
- )
417
- }
418
- let count = 0
419
- for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
420
- count++
421
- if (count > MAX_UNROLL) {
422
- fail(
423
- `this loop would unroll to more than ${MAX_UNROLL} iterations. A for ` +
424
- `loop unrolls at build time, so every iteration is written out in full`,
425
- s.pos,
426
- )
427
- }
450
+ recover(() => run(s, scope, frame), () => undefined)
451
+ }
452
+ }
453
+
454
+ /** Whether the ways through so far have left: a constant when they agree, a bool when they part. */
455
+ function stopped(frame: Frame): boolean | Val {
456
+ return orBool(orBool(frame.returned, frame.broke), frame.continued)
457
+ }
458
+
459
+ /**
460
+ * On the side of a join where the flow is known to have left: when only one
461
+ * flag could have done it, that flag is true there, which is what lets a
462
+ * body whose every way returns end with `returned` the constant true.
463
+ */
464
+ function knownStopped(frame: Frame): void {
465
+ const live = (["returned", "broke", "continued"] as const).filter((f) => frame[f] !== false)
466
+ if (live.length === 1) frame[live[0]!] = true
467
+ }
468
+
469
+ /**
470
+ * An if. Returns whether it also ran `rest`, the statements after it, which
471
+ * it does when exactly one side always leaves: the rest then belongs to the
472
+ * other side, and runs only there.
473
+ */
474
+ function runIf(s: Extract<Stmt, { k: "if" }>, rest: Stmt[], scope: Scope, frame: Frame): boolean {
475
+ const cond = control(lowerExpr(s.cond, scope), "an if", s.cond.pos)
476
+ if (typeof cond === "boolean") {
477
+ // The one case where a side really does vanish: it is never lowered.
478
+ exec(cond ? s.then : s.else, new Scope(scope), frame)
479
+ return false
480
+ }
481
+ const thenEnds = terminates(s.then)
482
+ const elseEnds = terminates(s.else)
483
+ const side = (body: Stmt[], withRest: boolean) => () => {
484
+ exec(body, new Scope(scope), frame)
485
+ if (withRest) exec(rest, new Scope(scope), frame)
486
+ }
487
+ fork(cond, scope, frame, side(s.then, elseEnds && !thenEnds), side(s.else, thenEnds && !elseEnds), s.pos)
488
+ return thenEnds || elseEnds
489
+ }
490
+
491
+ /**
492
+ * Runs both sides of a branch on `cond` and joins them: every local and
493
+ * flag the two leave different comes out of one `sl.branch`.
494
+ */
495
+ function fork(cond: Val, scope: Scope, frame: Frame, whenTrue: () => void, whenFalse: () => void, pos: Pos = frame.fn.pos): void {
496
+ const locals = scope.locals()
497
+ const snap = () => ({
498
+ values: locals.map((b) => b.value),
499
+ returned: frame.returned, retval: frame.retval, broke: frame.broke, continued: frame.continued,
500
+ })
501
+ const restore = (st: ReturnType<typeof snap>) => {
502
+ locals.forEach((b, i) => { b.value = st.values[i]! })
503
+ frame.returned = st.returned
504
+ frame.retval = st.retval
505
+ frame.broke = st.broke
506
+ frame.continued = st.continued
507
+ }
508
+ const before = snap()
509
+ whenTrue()
510
+ const t = snap()
511
+ restore(before)
512
+ whenFalse()
513
+ const f = snap()
514
+
515
+ interface Slot { type: VType; t: LV; f: LV; set: (v: LV) => void }
516
+ const slots: Slot[] = []
517
+ locals.forEach((b, i) => slots.push({ type: b, t: t.values[i]!, f: f.values[i]!, set: (v) => { b.value = v } }))
518
+ const flag = (name: "returned" | "broke" | "continued") =>
519
+ slots.push({ type: BOOL, t: t[name], f: f[name], set: (v) => { frame[name] = v as boolean | Val } })
520
+ flag("returned")
521
+ flag("broke")
522
+ flag("continued")
523
+ if (t.retval !== null || f.retval !== null) {
524
+ slots.push({
525
+ type: frame.ret, t: t.retval ?? blank(frame.ret, pos), f: f.retval ?? blank(frame.ret, pos),
526
+ set: (v) => { frame.retval = v },
527
+ })
528
+ }
529
+ const differ = slots.filter((x) => !same(x.t, x.f))
530
+ for (const x of slots) if (!differ.includes(x)) x.set(x.t)
531
+ if (differ.length === 0) return
532
+ const out = at(pos, () => sl.branch(cond, () => differ.map((x) => node(x.t, x.type)), () => differ.map((x) => node(x.f, x.type))))
533
+ differ.forEach((x, i) => x.set(out[i]!))
534
+ }
535
+
536
+ /** An assignment, a loop, a switch, a block, a return, a break or a continue. */
537
+ function run(s: Stmt, scope: Scope, frame: Frame): void {
538
+ switch (s.k) {
539
+ case "assign": assign(s, scope); break
540
+ case "for":
541
+ case "while": runLoop(s, scope, frame); break
542
+ case "switch": exec([switchAsIf(s, scope)], scope, frame); break
543
+ case "block": exec(s.body, new Scope(scope), frame); break
544
+ case "return": {
545
+ const v = lowerExpr(s.value, scope, want(frame.ret))
546
+ frame.retval = returned(v, frame, s.pos)
547
+ frame.returned = true
548
+ break
549
+ }
550
+ case "break": frame.broke = true; break
551
+ case "continue": frame.continued = true; break
552
+ default: break
553
+ }
554
+ }
555
+
556
+ function assign(s: Extract<Stmt, { k: "assign" }>, scope: Scope): void {
557
+ // The checker allowed exactly two shapes: a local, or a swizzle of one
558
+ // with no component named twice.
559
+ const member = s.target.k === "member" ? s.target : null
560
+ const name = ((member?.obj ?? s.target) as Extract<Expr, { k: "ident" }>).name
561
+ const b = scope.lookup(name)!
562
+ // `x op= v` is `x = x op v`, so a whole number beside an int local is an
563
+ // int. The value is lowered before the local is read, as it always was.
564
+ let v: LV
565
+ if (s.op === "=") {
566
+ v = lowerExpr(s.value, scope, want(b))
567
+ } else {
568
+ const op = s.op.slice(0, -1) as BinaryOp
569
+ const inner = operandWant(op, want(b))
570
+ const value = lowerExpr(s.value, scope, inner)
571
+ const old = lowerExpr(member ?? s.target, scope, inner)
572
+ v = combine(op, old, value, member ?? s.target, s.value, scope, s.pos)
573
+ }
574
+ if (member !== null) {
575
+ if (b.kind !== undefined) fail(`${name} is ${article(b)} ${typeName(b)}, which has no components to write`, member.pos)
576
+ v = writeComponents(b, member, toFloat(v, s.pos), s.pos)
577
+ }
578
+ b.value = fit(v, b, name, s.pos)
579
+ }
580
+
581
+ /** A return's value as the function's type. Only the kind converts; a width has to match. */
582
+ function returned(v: LV, frame: Frame, pos: Pos): LV {
583
+ const fn = frame.fn
584
+ const t = frame.ret
585
+ const got = widthOf(v)
586
+ if (got !== t.width) {
587
+ if (fn === main) {
588
+ const what = typeof v === "number" || typeof v === "bigint" ? "a single number" : `a ${widthType(got)}`
589
+ fail(`main returns a float4, a colour with alpha, and this returns ${what}. Wrap it: float4(value, 1)`, pos, 6)
590
+ }
591
+ fail(`${fn.name} is declared to return a ${fn.ret} and returns a ${widthType(got)}`, fn.pos)
592
+ }
593
+ return fit(v, t, `${fn.name}'s result`, pos)
594
+ }
595
+
596
+ // MARK: loops
597
+
598
+ function runLoop(s: Extract<Stmt, { k: "for" | "while" }>, scope: Scope, frame: Frame): void {
599
+ const loopScope = new Scope(scope)
600
+ let counter: Binding | null = null
601
+ if (s.k === "for") {
602
+ const t = typeOf(s.type)
603
+ if (t.width !== 1 || t.kind === "bool") fail(`a loop counts with an int, a uint or a float, and this is ${article(t)} ${s.type}`, s.pos)
604
+ counter = { ...t, value: fit(lowerExpr(s.from, scope, want(t)), t, s.counter, s.pos) }
605
+ loopScope.declare(s.counter, counter)
606
+ const turns = leavesEarly(s.body) || readsAny([s.cond, s.update.value], assignedIn(s.body)) ? null : countTurns(s, loopScope, counter, MAX_UNROLL)
607
+ if (turns !== null && turns.length <= MAX_UNROLL) {
608
+ at(s.pos, () => {
609
+ for (const v of turns) {
610
+ const inner = new Scope(loopScope)
611
+ inner.declare(s.counter, { ...t, value: v })
612
+ exec(s.body, inner, frame)
428
613
  }
429
- at(s.pos, () => {
430
- for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
431
- const inner = new Scope(scope)
432
- inner.declare(s.counter, { width: 1, value: i })
433
- exec(s.body, inner, ret)
434
- }
435
- })
436
- break
437
- }
438
- default:
439
- break
614
+ })
615
+ return
440
616
  }
617
+ }
618
+
619
+ // A real loop, carrying the locals its body changes and whichever
620
+ // flags its body can set.
621
+ const names = assignedIn(s.body)
622
+ const carried = [...names].filter((n) => scope.isLocal(n)).map((n) => scope.lookup(n)!)
623
+ const hasBreak = breaksOut(s.body)
624
+ const hasReturn = returnsIn(s.body)
625
+ const outer = frame.retval
626
+ const init: Array<{ v: LV; t: VType }> = []
627
+ if (counter !== null) init.push({ v: counter.value, t: counter })
628
+ for (const b of carried) init.push({ v: b.value, t: b })
629
+ if (hasBreak) init.push({ v: false, t: BOOL })
630
+ if (hasReturn) init.push({ v: false, t: BOOL }, { v: outer ?? blank(frame.ret, s.pos), t: frame.ret })
631
+ const max = s.k === "for" ? turnLimit(s, loopScope, counter!) : DEFAULT_TURNS
632
+
633
+ const out = at(s.pos, () => sl.loop(init.map((x) => node(x.v, x.t)), (params) => {
634
+ let k = 0
635
+ if (counter !== null) counter.value = params[k++]!
636
+ for (const b of carried) b.value = params[k++]!
637
+ let cond = control(lowerExpr(s.cond, loopScope), "a loop", s.cond.pos)
638
+ if (hasBreak) cond = andBool(notBool(params[k++]!), cond)
639
+ if (hasReturn) cond = andBool(notBool(params[k]!), cond)
640
+ return cond
641
+ }, (params) => {
642
+ frame.returned = false
643
+ frame.broke = false
644
+ frame.continued = false
645
+ if (hasReturn) frame.retval = params[params.length - 1]!
646
+ exec(s.body, new Scope(loopScope), frame)
647
+ // The update runs on every turn that goes on, one that continued
648
+ // included. It also runs on the turn that breaks, which nothing
649
+ // sees: the counter is not in scope after the loop.
650
+ if (s.k === "for") assign(s.update, loopScope)
651
+ const next: Num[] = []
652
+ if (counter !== null) next.push(node(counter.value, counter))
653
+ for (const b of carried) next.push(node(b.value, b))
654
+ if (hasBreak) next.push(node(frame.broke, BOOL))
655
+ if (hasReturn) next.push(node(frame.returned, BOOL), node(frame.retval ?? blank(frame.ret, s.pos), frame.ret))
656
+ return next
657
+ }, max))
658
+
659
+ let k = counter === null ? 0 : 1
660
+ for (const b of carried) b.value = out[k++]!
661
+ if (hasBreak) k++
662
+ frame.broke = false
663
+ frame.continued = false
664
+ if (hasReturn) {
665
+ frame.returned = out[k]!
666
+ frame.retval = out[k + 1]!
667
+ } else {
668
+ frame.returned = false
669
+ frame.retval = outer
670
+ }
441
671
  }
442
672
 
443
- function constantBound(e: Expr, what: string, scope: Scope): number {
444
- const v = lowerExpr(e, scope)
445
- if (typeof v !== "number") {
446
- fail(
447
- `a for loop unrolls at build time, so its ${what} has to be a constant. This one is ` +
448
- `computed while the shader runs`,
449
- e.pos,
450
- )
673
+ /**
674
+ * The counter's value on each turn of a for loop whose turns are known at
675
+ * build time, or null when they are not (a bound read from a uniform), or
676
+ * when there are more than `limit`, which then holds one more.
677
+ */
678
+ function countTurns(s: Extract<Stmt, { k: "for" }>, loopScope: Scope, counter: Binding, limit: number): LV[] | null {
679
+ const start = counter.value
680
+ const values: LV[] = []
681
+ try {
682
+ for (;;) {
683
+ if (!isConstant(counter.value)) return null
684
+ const c = control(lowerExpr(s.cond, loopScope), "a loop", s.cond.pos)
685
+ if (typeof c !== "boolean") return null
686
+ if (!c) return values
687
+ values.push(counter.value)
688
+ if (values.length > limit) return values
689
+ assign(s.update, loopScope)
690
+ }
691
+ } finally {
692
+ counter.value = start
451
693
  }
452
- return v
453
694
  }
454
695
 
455
- /** Two lowered values that are certainly the same node, so a join can skip the select. */
456
- function same(a: LV, b: LV): boolean {
457
- if (a === b) return true
458
- return typeof a !== "number" && typeof b !== "number" && a.ref === b.ref && a.width === b.width
696
+ /**
697
+ * How many turns a real for loop may take: what its constant bound gives,
698
+ * or its uniform bound's Range, or `DEFAULT_TURNS` when the bound says
699
+ * nothing. A float counter gets one more, since the GPU's float may land a
700
+ * hair either side of the bound the count was made with.
701
+ */
702
+ function turnLimit(s: Extract<Stmt, { k: "for" }>, loopScope: Scope, counter: Binding): number {
703
+ const known = countTurns(s, loopScope, counter, 1 << 16)
704
+ const slack = counter.kind === undefined ? 1 : 0
705
+ if (known !== null) return Math.max(1, known.length + slack)
706
+ // `i < n`, `i <= n`, `i > n`, `i >= n`, with `n` a uniform (or a
707
+ // conversion of one) and the counter stepping by a constant.
708
+ const c = s.cond
709
+ if (c.k !== "binary" || !["<", "<=", ">", ">="].includes(c.op)) return DEFAULT_TURNS
710
+ const flip = c.b.k === "ident" && c.b.name === s.counter
711
+ if (!flip && !(c.a.k === "ident" && c.a.name === s.counter)) return DEFAULT_TURNS
712
+ const op = flip ? ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[c.op]! : c.op
713
+ const from = counter.value
714
+ if (!isConstant(from)) return DEFAULT_TURNS
715
+ assign(s.update, loopScope)
716
+ const after = counter.value
717
+ counter.value = from
718
+ if (!isConstant(after)) return DEFAULT_TURNS
719
+ const step = num(after) - num(from)
720
+ const range = uniformRange(lowerExpr(flip ? c.a : c.b, loopScope))
721
+ if (range === null) return DEFAULT_TURNS
722
+ const up = op === "<" || op === "<="
723
+ if (up !== step > 0) return DEFAULT_TURNS
724
+ const bound = up ? range.max : range.min
725
+ const span = up ? bound - num(from) : num(from) - bound
726
+ const turns = op === "<" || op === ">" ? Math.ceil(span / Math.abs(step)) : Math.floor(span / Math.abs(step)) + 1
727
+ return Math.max(1, Math.min(1 << 16, turns + slack))
728
+ }
729
+
730
+ /** The Range of the uniform a bound reads, directly or through a conversion. */
731
+ function uniformRange(v: LV): { min: number; max: number } | null {
732
+ if (!(v instanceof Val)) return null
733
+ let n = v.owner.nodes[v.ref]!
734
+ if (n.k === "call" && n.op === SLOP.CAST) n = v.owner.nodes[n.args[0]!]!
735
+ if (n.k !== "uniform") return null
736
+ const r = v.owner.uniforms[n.slot]?.range
737
+ return r === undefined ? null : { min: r.min, max: r.max }
459
738
  }
460
739
 
740
+ // MARK: switch
741
+
742
+ /**
743
+ * A switch as the if chain it means: `x == 1 || x == 2` per case, in order,
744
+ * and the default last. Cases never fall through, so nothing is lost.
745
+ */
746
+ function switchAsIf(s: Extract<Stmt, { k: "switch" }>, scope: Scope): Stmt {
747
+ const v = lowerExpr(s.value, scope)
748
+ if (!isIntLike(v)) {
749
+ fail(`a switch picks by an int, and this is ${describeLV(v)}. Convert it: switch (int(x))`, s.value.pos)
750
+ }
751
+ const seen = new Set<bigint | number>()
752
+ let otherwise: Stmt[] = []
753
+ const arms: Array<{ cond: Expr; body: Stmt[]; pos: Pos }> = []
754
+ for (const c of s.cases) {
755
+ if (c.labels.includes(null)) { otherwise = c.body; continue }
756
+ let cond: Expr | null = null
757
+ for (const l of c.labels as Expr[]) {
758
+ const k = lowerExpr(l, scope, v instanceof Val && v.kind === "uint" ? "uint" : "int")
759
+ const key = typeof k === "bigint" ? k : k instanceof Val ? constValue(k) : null
760
+ if (key === null) fail("a case label is a whole number, or a const int", l.pos)
761
+ if (seen.has(key)) fail(`this switch already has a case ${key}`, l.pos)
762
+ seen.add(key)
763
+ const eq: Expr = { k: "binary", op: "==", a: s.value, b: l, pos: l.pos }
764
+ cond = cond === null ? eq : { k: "binary", op: "||", a: cond, b: eq, pos: l.pos }
765
+ }
766
+ arms.push({ cond: cond!, body: c.body, pos: c.pos })
767
+ }
768
+ let out: Stmt[] = otherwise
769
+ for (let i = arms.length - 1; i >= 0; i--) {
770
+ const a = arms[i]!
771
+ out = [{ k: "if", cond: a.cond, then: a.body, else: out, pos: a.pos }]
772
+ }
773
+ return out[0] ?? { k: "if", cond: { k: "bool", value: true, pos: s.pos }, then: [], else: [], pos: s.pos }
774
+ }
775
+
776
+ // MARK: conversions
777
+
461
778
  /**
462
- * The two conversions a declaration or an assignment makes for the author
463
- * (`Specs/SL_NEXT.md` 2b and 2c): a single number fills every component, as
464
- * `float3(0.5)` would, and a float4 goes into a float3 by dropping its
465
- * fourth, which is how a colour (a hex, a ramp, a texture sample) becomes
466
- * an rgb. Every other width change is left for `assertWidth` to refuse.
779
+ * A value as a declared type, as a declaration, an assignment and a
780
+ * return make it. Into a float anything numeric converts (an int, a bool,
781
+ * a comparison), a single number fills every component, and a float4 goes
782
+ * into a float3 by dropping its fourth (`Specs/SL_NEXT.md` 2b, 2c). Into an
783
+ * int only an int goes: a float is refused with the conversion that says
784
+ * how, since which rounding was meant is the author's to say.
467
785
  */
468
- function fit(v: LV, want: SLType, pos: Pos): LV {
469
- const got = typeof v === "number" ? 1 : v.width
470
- if (got === 1 && want > 1) return at(pos, () => compose(want, new Array<LV>(want).fill(v)))
471
- if (got === 4 && want === 3) return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
786
+ function fit(v: LV, t: VType, name: string, pos: Pos): LV {
787
+ switch (t.kind) {
788
+ case undefined: {
789
+ let x = toFloat(v, pos)
790
+ const got = widthOf(x)
791
+ if (got === 1 && t.width > 1) x = at(pos, () => compose(t.width, new Array<LV>(t.width).fill(x)))
792
+ else if (got === 4 && t.width === 3) x = at(pos, () => asVal(x).swz("xyz") as unknown as Val)
793
+ assertWidth(x, t.width, name, widthType(t.width), pos)
794
+ return x
795
+ }
796
+ case "int":
797
+ if (isInt(v)) return v
798
+ if (v instanceof Val && v.kind === "uint") fail(`${name} is an int and this is a uint; convert it with int(...)`, pos)
799
+ if (isBoolish(v)) fail(`${name} is an int and this is a bool; convert it with int(...)`, pos)
800
+ fail(`${name} is an int and this is a ${widthType(widthOf(v))}; convert it with int(...), which truncates toward zero`, pos)
801
+ break
802
+ case "uint":
803
+ if (v instanceof Val && v.kind === "uint") return v
804
+ if (typeof v === "bigint" && v >= 0n) return at(pos, () => sl.uint(Number(v)) as unknown as Val)
805
+ fail(`${name} is a uint and this is ${describeLV(v)}; convert it with uint(...)`, pos)
806
+ break
807
+ case "bool": {
808
+ const b = v instanceof Test ? v.bool : v
809
+ if (typeof b === "boolean" || (b instanceof Val && b.kind === "bool")) {
810
+ if (widthOf(b) !== 1) fail(`${name} is one bool and this compares ${widthOf(b)} components; combine them with && or ||`, pos)
811
+ return b
812
+ }
813
+ fail(`${name} is a bool, and this is ${describeLV(v)}. A bool holds a comparison, as in x > 0`, pos)
814
+ }
815
+ }
472
816
  return v
473
817
  }
474
818
 
819
+ /** A value as floats: an int converts, a bool is 0 or 1, and a comparison is the float it always was. */
820
+ function toFloat(v: LV, pos: Pos): number | Val {
821
+ if (typeof v === "number") return v
822
+ if (typeof v === "bigint") return Number(v)
823
+ if (typeof v === "boolean") return v ? 1 : 0
824
+ if (v instanceof Test) return v.float
825
+ return v.kind === undefined ? v : at(pos, () => sl.float(v) as unknown as Val)
826
+ }
827
+
828
+ /**
829
+ * A condition for an if or a loop: one bool. A float is true from 0.5, as
830
+ * a `?:` has always read one, and an int when it is not zero.
831
+ */
832
+ function control(v: LV, what: string, pos: Pos): boolean | Val {
833
+ const b = boolOf(v, pos)
834
+ if (widthOf(b) !== 1) fail(`${what} takes one condition, and this compares ${widthOf(b)} components; combine them with && or ||`, pos)
835
+ return b
836
+ }
837
+
838
+ function boolOf(v: LV, pos: Pos): boolean | Val {
839
+ if (typeof v === "boolean") return v
840
+ if (typeof v === "number") return v >= 0.5
841
+ if (typeof v === "bigint") return v !== 0n
842
+ if (v instanceof Test) return v.bool
843
+ if (v.kind === "bool") return v
844
+ return at(pos, () => (v.kind === undefined ? v.ge(0.5) : v.ne(0)) as unknown as Val)
845
+ }
846
+
847
+ /** A value as a node of its type, for a branch's or a loop's results. */
848
+ function node(v: LV, t: VType): Val {
849
+ if (v instanceof Val) return v
850
+ if (t.kind === "int") return sl.int(num(v)) as unknown as Val
851
+ if (t.kind === "uint") return sl.uint(num(v)) as unknown as Val
852
+ if (t.kind === "bool") return sl.bool(v instanceof Test ? (v.bool as boolean) : v !== false && v !== 0 && v !== 0n) as unknown as Val
853
+ const f = v instanceof Test ? v.float : toFloat(v, main.pos)
854
+ return (typeof f === "number" ? t.width === 1 ? sl.float(f) : compose(t.width, new Array<LV>(t.width).fill(f)) : f) as Val
855
+ }
856
+
857
+ function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
858
+ const got = widthOf(v)
859
+ if (got === want) return
860
+ // A declaration is an assertion, not an input to inference
861
+ // (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
862
+ // than quietly promoting one to the other.
863
+ const hint = got > want ? ` Take the components you want with a swizzle, as in .${"xyzw".slice(0, want)}.` : ""
864
+ fail(`${name} is declared ${type} and this is a ${widthType(got)}.${hint}`, pos)
865
+ }
866
+
475
867
  /**
476
868
  * `p.x = v`, `c.rgb *= k`: the local rebuilt from the components the
477
869
  * swizzle names, taken from `v`, and the rest kept from what it held. A
@@ -485,9 +877,9 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
485
877
  fail(`${member.name} writes a component a ${widthType(b.width)} does not have`, member.pos)
486
878
  }
487
879
  }
488
- const got = typeof v === "number" ? 1 : v.width
880
+ const got = widthOf(v)
489
881
  if (got !== 1 && got !== letters.length) {
490
- fail(`${member.name} is ${letters.length} components and this is a ${widthType(got as SLType)}`, pos)
882
+ fail(`${member.name} is ${letters.length} components and this is a ${widthType(got)}`, pos)
491
883
  }
492
884
  const parts: LV[] = [...names].map((c) => {
493
885
  const j = letters.indexOf(c)
@@ -497,21 +889,20 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
497
889
  return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
498
890
  }
499
891
 
500
- function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
501
- const got = typeof v === "number" ? 1 : v.width
502
- if (got === want) return
503
- // A declaration is an assertion, not an input to inference
504
- // (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
505
- // than quietly promoting one to the other.
506
- const hint = got > want ? ` Take the components you want with a swizzle, as in .${"xyzw".slice(0, want)}.` : ""
507
- fail(`${name} is declared ${type} and this is a ${widthType(got as SLType)}.${hint}`, pos)
508
- }
509
-
510
892
  // MARK: expressions
511
893
 
512
- function lowerExpr(e: Expr, scope: Scope): LV {
894
+ /**
895
+ * `want` is the kind the value is going into, an int or a uint, which makes
896
+ * a whole number written in it that kind: `int n = 3;`, `i += 1`.
897
+ */
898
+ function lowerExpr(e: Expr, scope: Scope, want?: "int" | "uint"): LV {
513
899
  switch (e.k) {
514
- case "num": return e.value
900
+ case "num":
901
+ if (e.unsigned === true) return at(e.pos, () => sl.uint(e.value) as unknown as Val)
902
+ if (e.whole && want === "int") return BigInt.asIntN(32, BigInt(e.value))
903
+ if (e.whole && want === "uint") return at(e.pos, () => sl.uint(e.value) as unknown as Val)
904
+ return e.value
905
+ case "bool": return e.value
515
906
  case "hex": return at(e.pos, () => sl.color(e.hex) as unknown as Val)
516
907
  case "ident": {
517
908
  const b = scope.lookup(e.name)
@@ -520,208 +911,350 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
520
911
  }
521
912
  case "member": {
522
913
  const obj = lowerExpr(e.obj, scope)
523
- return at(e.pos, () => asVal(obj).swz(e.name) as unknown as Val)
524
- }
525
- case "unary": {
526
- const v = lowerExpr(e.arg, scope)
527
- if (e.op === "+") return v
528
- if (e.op === "-") return typeof v === "number" ? -v : at(e.pos, () => v.neg())
529
- return oneMinus(v, e.pos)
530
- }
531
- case "binary": return lowerBinary(e, scope)
532
- case "cond": {
533
- const cond = lowerExpr(e.cond, scope)
534
- if (typeof cond === "number") {
535
- return lowerExpr(cond >= 0.5 ? e.then : e.else, scope)
536
- }
537
- const t = lowerExpr(e.then, scope)
538
- const f = lowerExpr(e.else, scope)
539
- return at(e.pos, () => sl.select(cond, t, f))
914
+ return at(e.pos, () => asVal(obj instanceof Test ? obj.float : obj).swz(e.name) as unknown as Val)
540
915
  }
916
+ case "unary": return lowerUnary(e, scope, want)
917
+ case "binary": return lowerBinary(e, scope, want)
918
+ case "cond": return lowerChoice(e, scope, want)
541
919
  case "call": return lowerCall(e, scope)
542
920
  }
543
921
  }
544
922
 
545
- function lowerBinary(e: Extract<Expr, { k: "binary" }>, scope: Scope): LV {
546
- const a = lowerExpr(e.a, scope)
547
- const b = lowerExpr(e.b, scope)
923
+ function lowerUnary(e: Extract<Expr, { k: "unary" }>, scope: Scope, want?: "int" | "uint"): LV {
924
+ const v = lowerExpr(e.arg, scope, want)
925
+ switch (e.op) {
926
+ case "+": return v
927
+ case "-":
928
+ if (typeof v === "number") return -v
929
+ if (typeof v === "bigint") return BigInt.asIntN(32, -v)
930
+ if (v instanceof Val && v.kind !== "bool") return at(e.pos, () => v.neg())
931
+ return at(e.pos, () => asVal(toFloat(v, e.pos)).neg())
932
+ case "!":
933
+ if (typeof v === "boolean") return !v
934
+ if (typeof v === "number") return 1 - v
935
+ if (typeof v === "bigint") return v === 0n
936
+ if (v instanceof Test) return new Test(v.width, () => oneMinus(v.float, e.pos), () => notBool(v.bool))
937
+ if (v.kind === "bool") return at(e.pos, () => v.not())
938
+ if (v.kind === undefined) return oneMinus(v, e.pos)
939
+ return at(e.pos, () => v.eq(0))
940
+ case "~":
941
+ if (typeof v === "bigint") return BigInt.asIntN(32, ~v)
942
+ if (v instanceof Val && (v.kind === "int" || v.kind === "uint")) return at(e.pos, () => v.bnot())
943
+ fail(`~ flips the bits of an int or a uint, and this is ${describeLV(v)}`, e.pos)
944
+ }
945
+ }
946
+
947
+ function lowerBinary(e: Extract<Expr, { k: "binary" }>, scope: Scope, want?: "int" | "uint"): LV {
548
948
  const pos = e.pos
549
- return at(pos, () => {
550
- switch (e.op) {
551
- case "+": case "-": case "*": case "/": case "%":
552
- return arithmetic(e.op, a, b, pos)
553
- // Comparisons and logic are arithmetic the IR already has, as
554
- // `Specs/SL_TEXT.md` 3.5 lays out. Nothing here branches, and
555
- // every result is a float that is 0 or 1.
556
- case "<=": return sl.step(a, b)
557
- case ">=": return sl.step(b, a)
558
- case "<": return oneMinus(sl.step(b, a), pos)
559
- case ">": return oneMinus(sl.step(a, b), pos)
560
- case "==": return oneMinus(notEqual(a, b, pos), pos)
561
- case "!=": return notEqual(a, b, pos)
562
- case "&&": return typeof a === "number" && typeof b === "number"
563
- ? Math.min(a, b) : asVal(a).min(b)
564
- case "||": return typeof a === "number" && typeof b === "number"
565
- ? Math.max(a, b) : asVal(a).max(b)
566
- }
567
- })
949
+ if (e.op === "&&" || e.op === "||") return lowerLogic(e, scope)
950
+ const inner = operandWant(e.op, want)
951
+ const a = lowerExpr(e.a, scope, inner)
952
+ const b = lowerExpr(e.b, scope, inner)
953
+ return combine(e.op, a, b, e.a, e.b, scope, pos)
568
954
  }
569
955
 
570
- /** `abs(sign(a - b))`: 0 when equal, 1 otherwise, component wise. */
571
- function notEqual(a: LV, b: LV, pos: Pos): LV {
572
- const d = arithmetic("-", a, b, pos)
573
- if (typeof d === "number") return Math.abs(Math.sign(d))
574
- return sl.sign(d).abs()
956
+ /** What an operator's operands are lowered as. Bits are an int's, so a whole number under one is an int: `h ^ 0x5bd1e995`. */
957
+ function operandWant(op: BinaryOp, want?: "int" | "uint"): "int" | "uint" | undefined {
958
+ return COMPARE.has(op) ? undefined : BITS.has(op) ? want ?? "int" : want
575
959
  }
576
960
 
577
- function oneMinus(v: LV, pos: Pos): LV {
578
- if (typeof v === "number") return 1 - v
579
- return at(pos, () => sl.float(1).sub(v))
961
+ /** A binary op on operands already lowered from `ea` and `eb`. */
962
+ function combine(op: BinaryOp, a: LV, b: LV, ea: Expr, eb: Expr, scope: Scope, pos: Pos): LV {
963
+ // A whole number beside an int is an int: `i < 10`, `i % 3`.
964
+ if (isIntLike(a) && typeof b === "number" && wholeExpr(eb)) b = lowerExpr(eb, scope, kindWant(a))
965
+ if (isIntLike(b) && typeof a === "number" && wholeExpr(ea)) a = lowerExpr(ea, scope, kindWant(b))
966
+ if (COMPARE.has(op)) return compare(op, a, b, pos)
967
+ if (BITS.has(op)) return bitwise(op as BitOp, a, b, pos)
968
+ return arithmetic(op as ArithOp, a, b, pos)
580
969
  }
581
970
 
582
971
  function arithmetic(op: ArithOp, a: LV, b: LV, pos: Pos): LV {
972
+ if (isInt(a) && isInt(b)) {
973
+ if (typeof a === "bigint" && typeof b === "bigint") {
974
+ if ((op === "/" || op === "%") && b === 0n) return 0n
975
+ return BigInt.asIntN(32, INT_ARITH[op](a, b))
976
+ }
977
+ return at(pos, () => intVal(a)[METHOD[op]](typeof b === "bigint" ? Number(b) : b))
978
+ }
979
+ if (isUint(a) || isUint(b)) {
980
+ if (!isIntLike(a) || !isIntLike(b)) fail(`a uint only meets another uint here; convert with uint(...) or float(...)`, pos)
981
+ return at(pos, () => intVal(a)[METHOD[op]](typeof b === "bigint" ? Number(b) : b as Val))
982
+ }
983
+ return floatArithmetic(op, toFloat(a, pos), toFloat(b, pos), pos)
984
+ }
985
+
986
+ /** Arithmetic on floats, exactly as it has always been built. */
987
+ function floatArithmetic(op: ArithOp, a: number | Val, b: number | Val, pos: Pos): number | Val {
583
988
  if (typeof a === "number" && typeof b === "number") {
584
989
  if ((op === "/" || op === "%") && b === 0) {
585
990
  fail(`this divides ${a} by zero, and the result would not be a number`, pos)
586
991
  }
587
992
  return ARITH[op](a, b)
588
993
  }
589
- const m = METHOD[op]
590
994
  // A scalar on the LEFT has to become a value first, so it broadcasts
591
995
  // through a swizzle rather than as a wide constant. That is what
592
996
  // `sl.float(n).sub(v)` does too, and the two forms have to agree.
593
- return asVal(a)[m](b)
997
+ return at(pos, () => asVal(a)[METHOD[op]](b))
998
+ }
999
+
1000
+ function bitwise(op: BitOp, a: LV, b: LV, pos: Pos): LV {
1001
+ for (const v of [a, b]) {
1002
+ if (!isIntLike(v)) fail(`${op} works on the bits of an int or a uint, and this is ${describeLV(v)}${isBoolish(v) ? "; use && or ||" : ""}`, pos)
1003
+ }
1004
+ if (typeof a === "bigint" && typeof b === "bigint") return BigInt.asIntN(32, INT_BITS[op](a, b))
1005
+ const x = intVal(a as bigint | Val)
1006
+ const y = typeof b === "bigint" ? Number(b) : b as Val
1007
+ return at(pos, () => {
1008
+ switch (op) {
1009
+ case "&": return x.band(y)
1010
+ case "|": return x.bor(y)
1011
+ case "^": return x.bxor(y)
1012
+ case "<<": return x.shl(y)
1013
+ case ">>": return x.shr(y)
1014
+ }
1015
+ })
1016
+ }
1017
+
1018
+ /**
1019
+ * A comparison. Two ints give a bool. Anything else is held as a Test: its
1020
+ * float form is the step the language always built, so a comparison read
1021
+ * as a number is the graph it always was, and its bool form a comparison node.
1022
+ */
1023
+ function compare(op: string, a: LV, b: LV, pos: Pos): LV {
1024
+ if (isInt(a) && isInt(b) && (a instanceof Val || b instanceof Val)) {
1025
+ return at(pos, () => intVal(a)[CMP_METHOD[op]!](typeof b === "bigint" ? Number(b) : b as Val) as unknown as Val)
1026
+ }
1027
+ if ((isUint(a) || isUint(b)) && isIntLike(a) && isIntLike(b)) {
1028
+ return at(pos, () => intVal(a)[CMP_METHOD[op]!](typeof b === "bigint" ? Number(b) : b as Val) as unknown as Val)
1029
+ }
1030
+ const fa = toFloat(a, pos)
1031
+ const fb = toFloat(b, pos)
1032
+ const width = Math.max(widthOf(fa), widthOf(fb)) as SLType
1033
+ return new Test(width, () => at(pos, () => stepForm(op, fa, fb, pos)), () => {
1034
+ if (typeof fa === "number" && typeof fb === "number") return FOLD_CMP[op]!(fa, fb)
1035
+ return at(pos, () => asVal(fa)[CMP_METHOD[op]!](fb) as unknown as Val)
1036
+ })
1037
+ }
1038
+
1039
+ /** A comparison as the float that is 0 or 1 it was before bools. Nothing here branches. */
1040
+ function stepForm(op: string, a: number | Val, b: number | Val, pos: Pos): number | Val {
1041
+ switch (op) {
1042
+ case "<=": return sl.step(a, b)
1043
+ case ">=": return sl.step(b, a)
1044
+ case "<": return oneMinus(sl.step(b, a), pos)
1045
+ case ">": return oneMinus(sl.step(a, b), pos)
1046
+ case "==": return oneMinus(notEqual(a, b, pos), pos)
1047
+ default: return notEqual(a, b, pos)
1048
+ }
1049
+ }
1050
+
1051
+ /** `abs(sign(a - b))`: 0 when equal, 1 otherwise, component wise. */
1052
+ function notEqual(a: number | Val, b: number | Val, pos: Pos): number | Val {
1053
+ const d = floatArithmetic("-", a, b, pos)
1054
+ if (typeof d === "number") return Math.abs(Math.sign(d))
1055
+ return sl.sign(d).abs()
1056
+ }
1057
+
1058
+ function oneMinus(v: number | Val, pos: Pos): number | Val {
1059
+ if (typeof v === "number") return 1 - v
1060
+ return at(pos, () => sl.float(1).sub(v))
1061
+ }
1062
+
1063
+ /** `&&` and `||`: min and max of floats, as they always were, until a branch reads them as bools. */
1064
+ function lowerLogic(e: Extract<Expr, { k: "binary" }>, scope: Scope): LV {
1065
+ const a = lowerExpr(e.a, scope)
1066
+ const b = lowerExpr(e.b, scope)
1067
+ const and = e.op === "&&"
1068
+ if (typeof a === "boolean" && typeof b === "boolean") return and ? a && b : a || b
1069
+ const width = Math.max(widthOf(a), widthOf(b)) as SLType
1070
+ return new Test(width, () => {
1071
+ const fa = toFloat(a, e.pos)
1072
+ const fb = toFloat(b, e.pos)
1073
+ if (typeof fa === "number" && typeof fb === "number") return and ? Math.min(fa, fb) : Math.max(fa, fb)
1074
+ return at(e.pos, () => (and ? asVal(fa).min(fb) : asVal(fa).max(fb)))
1075
+ }, () => {
1076
+ const x = boolOf(a, e.pos)
1077
+ const y = boolOf(b, e.pos)
1078
+ return and ? andBool(x, y) : orBool(x, y)
1079
+ })
1080
+ }
1081
+
1082
+ /** `c ? a : b`: always a select, both sides computed. A float condition is the select it always was. */
1083
+ function lowerChoice(e: Extract<Expr, { k: "cond" }>, scope: Scope, want?: "int" | "uint"): LV {
1084
+ const cond = lowerExpr(e.cond, scope)
1085
+ if (typeof cond === "boolean" || typeof cond === "number" || typeof cond === "bigint") {
1086
+ return lowerExpr(boolOf(cond, e.pos) ? e.then : e.else, scope, want)
1087
+ }
1088
+ // Built before the sides, as it always was, so a program's nodes keep
1089
+ // their order and its shader text is the text it was. A choice between
1090
+ // ints then leaves this float form unused.
1091
+ if (cond instanceof Test) void cond.float
1092
+ let t = lowerExpr(e.then, scope, want)
1093
+ let f = lowerExpr(e.else, scope, want)
1094
+ if (isIntLike(t) && typeof f === "number" && wholeExpr(e.else)) f = lowerExpr(e.else, scope, kindWant(t))
1095
+ if (isIntLike(f) && typeof t === "number" && wholeExpr(e.then)) t = lowerExpr(e.then, scope, kindWant(f))
1096
+ const kinded = [t, f].some((v) => typeof v === "bigint" || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
1097
+ if (!kinded) {
1098
+ const c = cond instanceof Test ? cond.float : cond
1099
+ return at(e.pos, () => sl.select(c, toFloat(t, e.pos), toFloat(f, e.pos)))
1100
+ }
1101
+ const c = boolOf(cond, e.pos)
1102
+ const side = (v: LV): Num => (typeof v === "bigint" ? sl.int(Number(v)) : typeof v === "boolean" ? sl.bool(v) : v instanceof Test ? v.float : v) as Num
1103
+ return at(e.pos, () => sl.select(c, side(t), side(f)))
594
1104
  }
595
1105
 
596
1106
  function lowerCall(e: Extract<Expr, { k: "call" }>, scope: Scope): LV {
597
1107
  const callee = e.callee
1108
+ const floats = (args: Expr[]) => args.map((a) => toFloat(lowerExpr(a, scope), a.pos))
598
1109
 
599
1110
  if (callee.k === "member") {
600
1111
  const shape = callee.name as SlSdfKind
601
- const p = lowerExpr(e.args[0]!, scope)
1112
+ const p = toFloat(lowerExpr(e.args[0]!, scope), e.args[0]!.pos)
602
1113
  if (typeof p === "number" || p.width !== 2) {
603
1114
  fail(`sdf.${shape} measures the distance to a point, so it takes a float2`, e.args[0]!.pos)
604
1115
  }
605
- const params: number[] = []
606
- for (const a of e.args.slice(1)) {
607
- const v = lowerExpr(a, scope)
608
- if (typeof v !== "number") {
609
- fail(
610
- `sdf.${shape}'s shape parameters are part of the operation, so they have to ` +
611
- `be constants. A size that changes belongs on the point: scale or offset it ` +
612
- `before the call`,
613
- a.pos,
614
- )
615
- }
616
- params.push(v)
617
- }
1116
+ const params = floats(e.args.slice(1))
618
1117
  return at(e.pos, () => sl.sdf(shape, p as never, params))
619
1118
  }
620
1119
 
621
1120
  const n = (callee as Extract<Expr, { k: "ident" }>).name
622
1121
 
623
- if (n in TYPE_WIDTH) {
624
- return construct(n as TypeName, e.args.map((a) => lowerExpr(a, scope)), e.pos)
625
- }
1122
+ if (n === "int" || n === "uint" || n === "bool") return convert(n, e, scope)
1123
+ if (n in TYPE_WIDTH) return construct(n as TypeName, floats(e.args), e.pos)
626
1124
 
627
- if (n === "tex2D") {
1125
+ if (n === "tex2D" || n === "tex2Dlod") {
628
1126
  const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
629
1127
  const tex = samplers.get(texName)!
630
- const uv = lowerExpr(e.args[1]!, scope)
1128
+ const uv = toFloat(lowerExpr(e.args[1]!, scope), e.args[1]!.pos)
631
1129
  if (typeof uv === "number" || uv.width !== 2) {
632
- fail("tex2D samples at a float2", e.args[1]!.pos)
1130
+ fail(`${n} samples at a float2`, e.args[1]!.pos)
1131
+ }
1132
+ if (n === "tex2D") return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
1133
+ const lod = toFloat(lowerExpr(e.args[2]!, scope), e.args[2]!.pos)
1134
+ if (typeof lod !== "number" && lod.width !== 1) {
1135
+ fail(`a mip level is one number, and this is a ${widthType(lod.width)}`, e.args[2]!.pos)
633
1136
  }
634
- return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
1137
+ return at(e.pos, () => tex.sampleLevel(uv as never, lod) as unknown as Val)
635
1138
  }
636
1139
 
637
1140
  if (n === "ramp") {
638
- const t = lowerExpr(e.args[0]!, scope)
639
- const stops: string[] = []
640
- for (const a of e.args.slice(1)) {
641
- if (a.k !== "hex") {
642
- fail(
643
- "a ramp's stops are constants, written as colours: `ramp(t, #000018, #0080ff, " +
644
- "#ffffff)`. A stop that changes wants lerp between two uniforms instead",
645
- a.pos,
646
- )
1141
+ const t = toFloat(lowerExpr(e.args[0]!, scope), e.args[0]!.pos)
1142
+ // A hex stays the string it was, so a ramp of hexes is the program
1143
+ // it always was. Anything else is a value, which has to be a colour
1144
+ // as written (`sl.ramp` says why), a colour uniform say.
1145
+ const stops = e.args.slice(1).map((a) => {
1146
+ if (a.k === "hex") return a.hex
1147
+ const v = toFloat(lowerExpr(a, scope), a.pos)
1148
+ if (typeof v === "number" || v.width < 3) {
1149
+ fail(`a ramp's stop is a colour, a float3 or a float4, and this is a ${typeof v === "number" ? "float" : widthType(v.width)}`, a.pos)
647
1150
  }
648
- stops.push(a.hex)
649
- }
1151
+ if (writtenColour(v.owner.nodes, v.ref) === null) fail(RAMP_STOP_COMPUTED, a.pos)
1152
+ return v as never
1153
+ })
650
1154
  return at(e.pos, () => sl.ramp(t, stops) as unknown as Val)
651
1155
  }
652
1156
 
653
1157
  const builtin = BUILTINS[n]
654
1158
  if (builtin !== undefined) {
655
- const args = e.args.map((a) => lowerExpr(a, scope))
656
- return at(e.pos, () => builtin.lower!(args as Num[]))
1159
+ let args = e.args.map((a) => lowerExpr(a, scope))
1160
+ // min, max, abs and clamp keep ints ints; everything else takes floats.
1161
+ if (INT_BUILTINS.has(n) && args.some(isIntLike) && args.every((v, i) => isIntLike(v) || (typeof v === "number" && wholeExpr(e.args[i]!)))) {
1162
+ const k = kindWant(args.find(isIntLike)!)
1163
+ args = e.args.map((a) => lowerExpr(a, scope, k)).map((v) => (typeof v === "bigint" ? sl.int(Number(v)) : v) as LV)
1164
+ return at(e.pos, () => builtin.lower!(args as Num[]))
1165
+ }
1166
+ const fs = args.map((v, i) => toFloat(v, e.args[i]!.pos))
1167
+ return at(e.pos, () => builtin.lower!(fs))
657
1168
  }
658
1169
 
659
1170
  const fn = funcs.get(n)!
660
- return inline(fn, e.args.map((a) => lowerExpr(a, scope)), e)
1171
+ return inline(fn, e, scope)
1172
+ }
1173
+
1174
+ /** `int(x)`, `uint(x)`, `bool(x)`: a float truncates toward zero, held to the range; anything but zero is true. */
1175
+ function convert(n: "int" | "uint" | "bool", e: Extract<Expr, { k: "call" }>, scope: Scope): LV {
1176
+ if (e.args.length !== 1) fail(`${n}(x) takes one value`, e.pos)
1177
+ const v = lowerExpr(e.args[0]!, scope, n === "bool" ? undefined : n)
1178
+ if (widthOf(v) !== 1) fail(`${n}(x) takes one component, and this is a ${widthType(widthOf(v))}; convert each one`, e.pos)
1179
+ const x = v instanceof Test ? v.bool : v
1180
+ if (n === "int") {
1181
+ if (typeof x === "bigint") return x
1182
+ if (typeof x === "number") return BigInt(truncateHeld(x, "int"))
1183
+ if (typeof x === "boolean") return x ? 1n : 0n
1184
+ }
1185
+ if (n === "bool") {
1186
+ if (typeof x === "boolean") return x
1187
+ if (typeof x === "number") return x !== 0
1188
+ if (typeof x === "bigint") return x !== 0n
1189
+ }
1190
+ const c = typeof x === "bigint" ? Number(x) : x
1191
+ return at(e.pos, () => (n === "int" ? sl.int(c) : n === "uint" ? sl.uint(c) : sl.bool(c)) as unknown as Val)
661
1192
  }
662
1193
 
663
1194
  /**
664
1195
  * Inlines a call: the body, lowered against these arguments, in a fresh
665
1196
  * scope that can see the globals and nothing else the caller had.
666
1197
  */
667
- function inline(fn: FuncDecl, args: LV[], e: Extract<Expr, { k: "call" }>): LV {
1198
+ function inline(fn: FuncDecl, e: Extract<Expr, { k: "call" }>, caller: Scope): LV {
668
1199
  const scope = new Scope(global)
669
1200
  fn.params.forEach((p, i) => {
670
- const v = args[i]!
671
- const got = typeof v === "number" ? 1 : v.width
672
- if (got !== TYPE_WIDTH[p.type]) {
1201
+ const t = typeOf(p.type)
1202
+ const arg = e.args[i]!
1203
+ // In the caller's scope, where the argument is written; as the
1204
+ // parameter's kind, so a whole number passed to an int is one.
1205
+ const v = lowerExpr(arg, caller, want(t))
1206
+ const got = widthOf(v)
1207
+ if (got !== t.width) {
673
1208
  fail(
674
1209
  `${fn.name}'s parameter ${p.name} is a ${p.type} and this argument is a ` +
675
- `${widthType(got as SLType)}`,
676
- e.args[i]!.pos,
1210
+ `${widthType(got)}`,
1211
+ arg.pos,
677
1212
  )
678
1213
  }
679
- scope.declare(p.name, { width: TYPE_WIDTH[p.type], value: v })
1214
+ scope.declare(p.name, { ...t, value: fit(v, t, `${fn.name}'s parameter ${p.name}`, arg.pos) })
680
1215
  })
681
- const out = exec(fn.body, scope, TYPE_WIDTH[fn.ret])
682
- if (out === undefined) fail(`${fn.name} never returns a value`, e.pos)
683
- const got = typeof out === "number" ? 1 : out.width
684
- if (got !== TYPE_WIDTH[fn.ret]) {
685
- fail(`${fn.name} is declared to return a ${fn.ret} and returns a ${widthType(got as SLType)}`, fn.pos)
686
- }
687
- return out
1216
+ return callBody(fn, scope)
688
1217
  }
689
1218
 
690
- function construct(type: TypeName, args: LV[], pos: Pos): LV {
1219
+ function construct(type: TypeName, args: Array<number | Val>, pos: Pos): LV {
691
1220
  const width = TYPE_WIDTH[type]
692
1221
  if (width === 1) {
693
1222
  const v = args[0]!
694
- const got = typeof v === "number" ? 1 : v.width
695
- if (args.length !== 1 || got !== 1) {
1223
+ if (args.length !== 1 || widthOf(v) !== 1) {
696
1224
  fail("float(x) takes one float; use a swizzle to narrow a wider value", pos)
697
1225
  }
698
1226
  return v
699
1227
  }
700
1228
  if (args.length === 1) {
701
1229
  const v = args[0]!
702
- const got = typeof v === "number" ? 1 : v.width
1230
+ const got = widthOf(v)
703
1231
  // HLSL's scalar broadcast, and the identity for a value that is
704
1232
  // already this wide. `float4(v4)` is `v4`, not four swizzles.
705
1233
  if (got === width) return v
706
1234
  if (got === 1) return at(pos, () => compose(width, new Array<LV>(width).fill(v)))
707
- fail(`${type}(v) broadcasts a float; a ${widthType(got as SLType)} needs its components spelled out`, pos)
1235
+ fail(`${type}(v) broadcasts a float; a ${widthType(got)} needs its components spelled out`, pos)
708
1236
  }
709
1237
  return at(pos, () => compose(width, args))
710
1238
  }
711
1239
 
712
1240
  function compose(width: SLType, parts: LV[]): Val {
713
- const ps = parts as Num[]
1241
+ const ps = parts.map((p) => (p instanceof Test ? p.float : typeof p === "bigint" ? Number(p) : typeof p === "boolean" ? (p ? 1 : 0) : p)) as Num[]
714
1242
  if (width === 2) return sl.vec2(...ps) as unknown as Val
715
1243
  if (width === 3) return sl.vec3(...ps) as unknown as Val
716
1244
  return sl.vec4(...ps) as unknown as Val
717
1245
  }
718
-
719
- function asVal(v: LV): Val {
720
- return typeof v === "number" ? (sl.float(v) as unknown as Val) : v
721
- }
722
1246
  }
723
1247
 
1248
+ // MARK: tables and small helpers
1249
+
724
1250
  type ArithOp = "+" | "-" | "*" | "/" | "%"
1251
+ type BitOp = "&" | "|" | "^" | "<<" | ">>"
1252
+
1253
+ const BOOL: VType = { width: 1, kind: "bool" }
1254
+
1255
+ const COMPARE = new Set<string>(["<", "<=", ">", ">=", "==", "!="])
1256
+ const BITS = new Set<string>(["&", "|", "^", "<<", ">>"])
1257
+ const INT_BUILTINS = new Set(["min", "max", "abs", "clamp"])
725
1258
 
726
1259
  const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
727
1260
  "+": (a, b) => a + b,
@@ -733,6 +1266,33 @@ const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
733
1266
  "%": (a, b) => a % b,
734
1267
  }
735
1268
 
1269
+ /** Int arithmetic as the GPU does it, on 32 bits: bigint division and remainder truncate, as HLSL's do. */
1270
+ const INT_ARITH: Record<ArithOp, (a: bigint, b: bigint) => bigint> = {
1271
+ "+": (a, b) => a + b,
1272
+ "-": (a, b) => a - b,
1273
+ "*": (a, b) => a * b,
1274
+ "/": (a, b) => a / b,
1275
+ "%": (a, b) => a % b,
1276
+ }
1277
+
1278
+ /** A shift takes its count modulo 32 on every backend, and an int shifts right keeping its sign. */
1279
+ const INT_BITS: Record<BitOp, (a: bigint, b: bigint) => bigint> = {
1280
+ "&": (a, b) => a & b,
1281
+ "|": (a, b) => a | b,
1282
+ "^": (a, b) => a ^ b,
1283
+ "<<": (a, b) => a << (b & 31n),
1284
+ ">>": (a, b) => a >> (b & 31n),
1285
+ }
1286
+
1287
+ const FOLD_CMP: Record<string, (a: number, b: number) => boolean> = {
1288
+ "<": (a, b) => a < b, "<=": (a, b) => a <= b, ">": (a, b) => a > b,
1289
+ ">=": (a, b) => a >= b, "==": (a, b) => a === b, "!=": (a, b) => a !== b,
1290
+ }
1291
+
1292
+ const CMP_METHOD: Record<string, "lt" | "le" | "gt" | "ge" | "eq" | "ne"> = {
1293
+ "<": "lt", "<=": "le", ">": "gt", ">=": "ge", "==": "eq", "!=": "ne",
1294
+ }
1295
+
736
1296
  const METHOD: Record<ArithOp, "add" | "sub" | "mul" | "div" | "mod"> = {
737
1297
  "+": "add", "-": "sub", "*": "mul", "/": "div", "%": "mod",
738
1298
  }
@@ -741,10 +1301,195 @@ function isArith(op: string): op is ArithOp {
741
1301
  return op in ARITH
742
1302
  }
743
1303
 
1304
+ function typeOf(name: TypeName): VType {
1305
+ return { width: TYPE_WIDTH[name], kind: TYPE_KIND[name] }
1306
+ }
1307
+
1308
+ function want(t: VType): "int" | "uint" | undefined {
1309
+ return t.kind === "int" || t.kind === "uint" ? t.kind : undefined
1310
+ }
1311
+
1312
+ function isInt(v: LV): v is bigint | Val {
1313
+ return typeof v === "bigint" || (v instanceof Val && v.kind === "int")
1314
+ }
1315
+
1316
+ function isUint(v: LV): v is Val {
1317
+ return v instanceof Val && v.kind === "uint"
1318
+ }
1319
+
1320
+ function isIntLike(v: LV): v is bigint | Val {
1321
+ return isInt(v) || isUint(v)
1322
+ }
1323
+
1324
+ function isBoolish(v: LV): boolean {
1325
+ return typeof v === "boolean" || v instanceof Test || (v instanceof Val && v.kind === "bool")
1326
+ }
1327
+
1328
+ function isConstant(v: LV): v is number | bigint {
1329
+ return typeof v === "number" || typeof v === "bigint"
1330
+ }
1331
+
1332
+ function kindWant(v: bigint | Val): "int" | "uint" {
1333
+ return v instanceof Val && v.kind === "uint" ? "uint" : "int"
1334
+ }
1335
+
1336
+ function num(v: LV): number {
1337
+ return typeof v === "bigint" ? Number(v) : v as number
1338
+ }
1339
+
1340
+ /** An int operand as a node the EDSL's int methods take. */
1341
+ function intVal(v: bigint | Val): Val {
1342
+ return typeof v === "bigint" ? sl.int(Number(v)) as unknown as Val : v
1343
+ }
1344
+
1345
+ function asVal(v: LV): Val {
1346
+ if (v instanceof Val) return v
1347
+ return sl.float(typeof v === "number" ? v : num(v)) as unknown as Val
1348
+ }
1349
+
1350
+ function widthOf(v: LV): SLType {
1351
+ if (v instanceof Val || v instanceof Test) return v.width
1352
+ return 1
1353
+ }
1354
+
744
1355
  function widthType(w: SLType): TypeName {
745
1356
  return (["float", "float2", "float3", "float4"] as const)[w - 1]!
746
1357
  }
747
1358
 
1359
+ function typeName(t: VType): string {
1360
+ return t.kind === undefined ? widthType(t.width) : t.kind
1361
+ }
1362
+
1363
+ function article(t: VType): string {
1364
+ return t.kind === "int" ? "an" : "a"
1365
+ }
1366
+
1367
+ function describeLV(v: LV): string {
1368
+ if (typeof v === "bigint") return "an int"
1369
+ if (typeof v === "boolean" || v instanceof Test) return "a bool"
1370
+ if (typeof v === "number") return "a float"
1371
+ return v.kind === undefined ? `a ${widthType(v.width)}` : v.kind === "int" ? "an int" : `a ${v.kind}`
1372
+ }
1373
+
1374
+ /** A constant int node's value, for a case label read through a const. */
1375
+ function constValue(v: Val): bigint | null {
1376
+ const n = v.owner.nodes[v.ref]!
1377
+ return n.k === "const" && n.kind !== undefined ? BigInt(n.v[0]!) : null
1378
+ }
1379
+
1380
+ /** Two lowered values that are certainly the same, so a join can leave them out. */
1381
+ function same(a: LV, b: LV): boolean {
1382
+ if (a === b) return true
1383
+ return a instanceof Val && b instanceof Val && a.ref === b.ref && a.width === b.width && a.kind === b.kind
1384
+ }
1385
+
1386
+ function notBool(v: boolean | Val): boolean | Val {
1387
+ return typeof v === "boolean" ? !v : v.not()
1388
+ }
1389
+
1390
+ function andBool(a: boolean | Val, b: boolean | Val): boolean | Val {
1391
+ if (a === false || b === false) return false
1392
+ if (a === true) return b
1393
+ if (b === true) return a
1394
+ return a.and(b)
1395
+ }
1396
+
1397
+ function orBool(a: boolean | Val, b: boolean | Val): boolean | Val {
1398
+ if (a === true || b === true) return true
1399
+ if (a === false) return b
1400
+ if (b === false) return a
1401
+ return a.or(b)
1402
+ }
1403
+
1404
+ /** Whether an expression is whole numbers only, `3` or `-(2 * 4)`, so beside an int it is an int. */
1405
+ function wholeExpr(e: Expr): boolean {
1406
+ switch (e.k) {
1407
+ case "num": return e.whole
1408
+ case "unary": return (e.op === "-" || e.op === "+") && wholeExpr(e.arg)
1409
+ case "binary": return isArith(e.op) && wholeExpr(e.a) && wholeExpr(e.b)
1410
+ default: return false
1411
+ }
1412
+ }
1413
+
1414
+ /** The names a body assigns, anywhere in it: what a real loop has to carry. */
1415
+ function assignedIn(body: Stmt[]): Set<string> {
1416
+ const out = new Set<string>()
1417
+ const walk = (s: Stmt): void => {
1418
+ switch (s.k) {
1419
+ case "assign": {
1420
+ const t = s.target.k === "member" ? s.target.obj : s.target
1421
+ if (t.k === "ident") out.add(t.name)
1422
+ return
1423
+ }
1424
+ case "if": s.then.forEach(walk); s.else.forEach(walk); return
1425
+ case "block": s.body.forEach(walk); return
1426
+ case "for": walk(s.update); s.body.forEach(walk); return
1427
+ case "while": s.body.forEach(walk); return
1428
+ case "switch": s.cases.forEach((c) => c.body.forEach(walk)); return
1429
+ default: return
1430
+ }
1431
+ }
1432
+ body.forEach(walk)
1433
+ return out
1434
+ }
1435
+
1436
+ /** Whether any of `exprs` reads one of `names`. */
1437
+ function readsAny(exprs: Expr[], names: Set<string>): boolean {
1438
+ const walk = (e: Expr): boolean => {
1439
+ switch (e.k) {
1440
+ case "ident": return names.has(e.name)
1441
+ case "member": return walk(e.obj)
1442
+ case "unary": return walk(e.arg)
1443
+ case "binary": return walk(e.a) || walk(e.b)
1444
+ case "cond": return walk(e.cond) || walk(e.then) || walk(e.else)
1445
+ case "call": return (e.callee.k === "member" && walk(e.callee.obj)) || e.args.some(walk)
1446
+ default: return false
1447
+ }
1448
+ }
1449
+ return exprs.some(walk)
1450
+ }
1451
+
1452
+ /** Whether a loop body can leave its turn early: a break or a continue of its own, or a return anywhere. */
1453
+ function leavesEarly(body: Stmt[]): boolean {
1454
+ return breaksOut(body) || continuesIn(body) || returnsIn(body)
1455
+ }
1456
+
1457
+ function breaksOut(body: Stmt[]): boolean {
1458
+ return ownStatement(body, "break")
1459
+ }
1460
+
1461
+ function continuesIn(body: Stmt[]): boolean {
1462
+ return ownStatement(body, "continue")
1463
+ }
1464
+
1465
+ /** A break or a continue that belongs to this loop, not to one nested in it. */
1466
+ function ownStatement(body: Stmt[], k: "break" | "continue"): boolean {
1467
+ return body.some((s) => {
1468
+ switch (s.k) {
1469
+ case "break":
1470
+ case "continue": return s.k === k
1471
+ case "if": return ownStatement(s.then, k) || ownStatement(s.else, k)
1472
+ case "block": return ownStatement(s.body, k)
1473
+ case "switch": return s.cases.some((c) => ownStatement(c.body, k))
1474
+ default: return false
1475
+ }
1476
+ })
1477
+ }
1478
+
1479
+ function returnsIn(body: Stmt[]): boolean {
1480
+ return body.some((s) => {
1481
+ switch (s.k) {
1482
+ case "return": return true
1483
+ case "if": return returnsIn(s.then) || returnsIn(s.else)
1484
+ case "block": return returnsIn(s.body)
1485
+ case "for":
1486
+ case "while": return returnsIn(s.body)
1487
+ case "switch": return s.cases.some((c) => returnsIn(c.body))
1488
+ default: return false
1489
+ }
1490
+ })
1491
+ }
1492
+
748
1493
  /**
749
1494
  * An EDSL message in the words of a `.sl` file.
750
1495
  *