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/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,33 +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
51
  import {
36
- controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, writtenColour, type Program, type SLType, type UniformControl,
52
+ controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, truncateHeld, writtenColour,
53
+ type Program, type SLKind, type SLType, type UniformControl,
37
54
  } from "../ir"
55
+ import { SLOP } from "../ops"
38
56
  import type { SlSdfKind } from "../shapes"
39
57
  import * as sl from "../sl"
40
- import type { Num, Val } from "../sl"
41
- 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"
42
60
  import { readAttributes } from "./attributes"
43
61
  import { BUILTINS } from "./builtins"
44
- import type { Checked } from "./check"
62
+ import { terminates, type Checked } from "./check"
45
63
  import { SLParseError, type Pos } from "./lexer"
46
64
 
47
- /** A lowered value. A plain number is one that has not had to become a node yet. */
48
- 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 }
49
91
 
50
- interface Binding {
51
- /** The declared width. A reassignment has to match it. */
52
- 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. */
53
94
  value: LV
54
95
  }
55
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
+
56
111
  /** How many iterations a `for` may unroll to. `sl.repeat`'s ceiling, for the same reason. */
57
112
  const MAX_UNROLL = 64
58
113
 
114
+ /** The turn limit of a loop whose bound says nothing about how many turns it takes. */
115
+ const DEFAULT_TURNS = 1024
116
+
59
117
  class Scope {
60
118
  private readonly vars = new Map<string, Binding>()
61
119
 
@@ -69,11 +127,17 @@ class Scope {
69
127
  return this.parent?.lookup(name)
70
128
  }
71
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
+
72
136
  /**
73
- * 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.
74
138
  *
75
139
  * Uniforms, textures, consts and inputs live in the global scope and the
76
- * 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
77
141
  * consider one.
78
142
  */
79
143
  locals(): Binding[] {
@@ -137,17 +201,19 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
137
201
  }
138
202
  }
139
203
 
140
- /** A zero of a width, standing in for a value whose line was refused. */
141
- const blank = (width: SLType, pos: Pos): LV =>
142
- 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
+ }
143
211
 
144
212
  // The globals every helper below reads, built inside the recording callback
145
213
  // and held out here so a function being inlined can see them and nothing
146
214
  // else the caller had in scope.
147
215
  const global = new Scope(null, true)
148
216
  const samplers = new Map<string, sl.Texture>()
149
- /** Where the last `return` lowered was, so an error about it points at it. */
150
- let returnedAt: Pos = main.pos
151
217
 
152
218
  try {
153
219
  return record()
@@ -161,7 +227,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
161
227
  function record(): Program {
162
228
  return sl.program((inputs) => {
163
229
  for (const [name, width] of Object.entries(INPUTS)) {
164
- 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] })
165
231
  }
166
232
  // Read through, so a program that never names one records nothing
167
233
  // for it. The setter only serves a refused assignment, which the
@@ -170,6 +236,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
170
236
  let value: LV | undefined
171
237
  global.declare(name, {
172
238
  width: width as SLType,
239
+ kind: undefined,
173
240
  get value() { return value ??= (inputs as never)[name] },
174
241
  set value(v: LV) { value = v },
175
242
  })
@@ -182,8 +249,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
182
249
  for (const u of unit.uniforms) {
183
250
  const width = TYPE_WIDTH[u.type]
184
251
  const { components, colour } = recover(
185
- () => uniformDefault(u.type, u.init),
186
- () => ({ ...uniformDefault(u.type, null), colour: false }),
252
+ () => uniformDefault(u.type, u.init, u.pos),
253
+ () => ({ ...uniformDefault("float", null, u.pos), colour: false }),
187
254
  )
188
255
  // A hex default says the uniform IS a colour, so every read of it
189
256
  // converts, exactly as a hex literal in an expression does. Without
@@ -204,37 +271,20 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
204
271
  const value = (colour && u.init?.k === "hex") || read.colour
205
272
  ? at(u.pos, () => sl.uniform.colour(u.name, components, width as 3 | 4, control) as unknown as Val)
206
273
  : at(u.pos, () => declareUniform(u.name, u.type, components, control))
207
- global.declare(u.name, { width, value })
274
+ global.declare(u.name, { width, kind: TYPE_KIND[u.type], value })
208
275
  }
209
276
 
210
277
  for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
211
278
 
212
279
  for (const c of unit.consts) {
213
- const width = TYPE_WIDTH[c.type]
214
- const v = recover(() => {
215
- const v = fit(lowerExpr(c.init, global), width, c.pos)
216
- assertWidth(v, width, c.name, c.type, c.pos)
217
- return v
218
- }, () => blank(width, c.pos))
219
- 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 })
220
283
  }
221
284
 
222
- const out = exec(main.body, new Scope(global), 4)
285
+ const out = callBody(main, new Scope(global))
223
286
  // What a refused line left behind is not the program's colour.
224
- if (errors !== undefined && errors.length > 0) return blank(4, main.pos) as never
225
- // Checked here rather than left to `sl.program`, which would refuse it
226
- // in the EDSL's words ("a vec4") about a file whose types are spelled
227
- // float4, and from outside the reach of a position.
228
- if (out === undefined || typeof out === "number" || out.width !== 4) {
229
- const got = out === undefined ? "nothing"
230
- : typeof out === "number" ? "a single number"
231
- : `a ${widthType(out.width)}`
232
- fail(
233
- `main returns a float4, a colour with alpha, and this returns ${got}. ` +
234
- `Wrap it: float4(value, 1)`,
235
- returnedAt, 6,
236
- )
237
- }
287
+ if (errors !== undefined && errors.length > 0) return blank({ width: 4, kind: undefined }, main.pos) as never
238
288
  return out as never
239
289
  })
240
290
  }
@@ -247,6 +297,10 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
247
297
  case "float2": return sl.uniform.vec2(name, [c[0]!, c[1]!], control) as unknown as Val
248
298
  case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!], control) as unknown as Val
249
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`)
250
304
  }
251
305
  }
252
306
 
@@ -260,7 +314,14 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
260
314
  * may read a uniform, so letting a uniform read a const would make the two
261
315
  * tables depend on each other.
262
316
  */
263
- 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
+ }
264
325
  const width = TYPE_WIDTH[type]
265
326
  if (init === null) {
266
327
  const zeros = new Array<number>(width).fill(0)
@@ -285,6 +346,9 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
285
346
  init.pos,
286
347
  )
287
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
+ }
288
352
  if (c.length === 1 && width > 1) return { components: new Array<number>(width).fill(c[0]!), colour: false }
289
353
  if (c.length === 4 && width === 3) return { components: c.slice(0, 3), colour: false }
290
354
  if (c.length !== width) {
@@ -306,7 +370,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
306
370
  if (v === null) return null
307
371
  if (e.op === "+") return v
308
372
  if (e.op === "-") return v.map((n) => -n)
309
- return v.map((n) => 1 - n)
373
+ if (e.op === "!") return v.map((n) => 1 - n)
374
+ return null
310
375
  }
311
376
  case "binary": {
312
377
  const a = constantComponents(e.a)
@@ -336,144 +401,469 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
336
401
  }
337
402
  }
338
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
+
339
417
  // MARK: statements
340
418
 
341
419
  /**
342
- * Runs a body. The value of its `return`, or nothing when it has none.
343
- * `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`.
344
422
  */
345
- function exec(body: Stmt[], scope: Scope, ret: SLType): LV | undefined {
346
- for (const s of body) {
347
- if (s.k === "return") {
348
- returnedAt = s.pos
349
- 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
350
443
  }
351
444
  if (s.k === "var" || s.k === "const") {
352
- const width = TYPE_WIDTH[s.type]
353
- const v = recover(() => {
354
- const v = fit(lowerExpr(s.init, scope), width, s.pos)
355
- assertWidth(v, width, s.name, s.type, s.pos)
356
- return v
357
- }, () => blank(width, s.pos))
358
- 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 })
359
448
  continue
360
449
  }
361
- recover(() => run(s, scope, ret), () => undefined)
362
- }
363
- return undefined
364
- }
365
-
366
- /** An assignment, an if or a for. */
367
- function run(s: Stmt, scope: Scope, ret: SLType): void {
368
- switch (s.k) {
369
- case "assign": {
370
- // The checker allowed exactly two shapes: a local, or a
371
- // swizzle of one with no component named twice.
372
- const member = s.target.k === "member" ? s.target : null
373
- const name = ((member?.obj ?? s.target) as Extract<Expr, { k: "ident" }>).name
374
- const b = scope.lookup(name)!
375
- let v = lowerExpr(s.value, scope)
376
- if (s.op !== "=") {
377
- const op = s.op[0] as ArithOp
378
- const old = member === null ? b.value : lowerExpr(member, scope)
379
- v = at(s.pos, () => arithmetic(op, old, v, s.pos))
380
- }
381
- if (member !== null) v = writeComponents(b, member, v, s.pos)
382
- else v = fit(v, b.width, s.pos)
383
- assertWidth(v, b.width, name, widthType(b.width), s.pos)
384
- b.value = v
385
- break
386
- }
387
- case "if": {
388
- const cond = lowerExpr(s.cond, scope)
389
- if (typeof cond === "number") {
390
- // The one case where a branch really does vanish: the
391
- // other side is never lowered, so it costs nothing.
392
- exec(cond >= 0.5 ? s.then : s.else, new Scope(scope), ret)
393
- break
394
- }
395
- const locals = scope.locals()
396
- const before = locals.map((b) => b.value)
397
- exec(s.then, new Scope(scope), ret)
398
- const whenTrue = locals.map((b) => b.value)
399
- locals.forEach((b, i) => { b.value = before[i]! })
400
- exec(s.else, new Scope(scope), ret)
401
- const whenFalse = locals.map((b) => b.value)
402
- locals.forEach((b, i) => {
403
- const t = whenTrue[i]!
404
- const f = whenFalse[i]!
405
- b.value = same(t, f) ? t : at(s.pos, () => sl.select(cond, t, f))
406
- })
407
- break
408
- }
409
- case "for": {
410
- const from = constantBound(s.from, "start", scope)
411
- const to = constantBound(s.to, "bound", scope)
412
- const step = constantBound(s.step, "step", scope)
413
- if (!(step > 0)) {
414
- fail(
415
- `this loop steps by ${step}, so it never ends. A for loop unrolls, which ` +
416
- `means its count has to be known and finite`,
417
- s.pos,
418
- )
419
- }
420
- let count = 0
421
- for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
422
- count++
423
- if (count > MAX_UNROLL) {
424
- fail(
425
- `this loop would unroll to more than ${MAX_UNROLL} iterations. A for ` +
426
- `loop unrolls at build time, so every iteration is written out in full`,
427
- s.pos,
428
- )
429
- }
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)
430
613
  }
431
- at(s.pos, () => {
432
- for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
433
- const inner = new Scope(scope)
434
- inner.declare(s.counter, { width: 1, value: i })
435
- exec(s.body, inner, ret)
436
- }
437
- })
438
- break
439
- }
440
- default:
441
- break
614
+ })
615
+ return
442
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
+ }
443
671
  }
444
672
 
445
- function constantBound(e: Expr, what: string, scope: Scope): number {
446
- const v = lowerExpr(e, scope)
447
- if (typeof v !== "number") {
448
- fail(
449
- `a for loop unrolls at build time, so its ${what} has to be a constant. This one is ` +
450
- `computed while the shader runs`,
451
- e.pos,
452
- )
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
453
693
  }
454
- return v
455
694
  }
456
695
 
457
- /** Two lowered values that are certainly the same node, so a join can skip the select. */
458
- function same(a: LV, b: LV): boolean {
459
- if (a === b) return true
460
- 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))
461
728
  }
462
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 }
738
+ }
739
+
740
+ // MARK: switch
741
+
463
742
  /**
464
- * The two conversions a declaration or an assignment makes for the author
465
- * (`Specs/SL_NEXT.md` 2b and 2c): a single number fills every component, as
466
- * `float3(0.5)` would, and a float4 goes into a float3 by dropping its
467
- * fourth, which is how a colour (a hex, a ramp, a texture sample) becomes
468
- * an rgb. Every other width change is left for `assertWidth` to refuse.
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.
469
745
  */
470
- function fit(v: LV, want: SLType, pos: Pos): LV {
471
- const got = typeof v === "number" ? 1 : v.width
472
- if (got === 1 && want > 1) return at(pos, () => compose(want, new Array<LV>(want).fill(v)))
473
- if (got === 4 && want === 3) return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
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
+
778
+ /**
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.
785
+ */
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
+ }
474
816
  return v
475
817
  }
476
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
+
477
867
  /**
478
868
  * `p.x = v`, `c.rgb *= k`: the local rebuilt from the components the
479
869
  * swizzle names, taken from `v`, and the rest kept from what it held. A
@@ -487,9 +877,9 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
487
877
  fail(`${member.name} writes a component a ${widthType(b.width)} does not have`, member.pos)
488
878
  }
489
879
  }
490
- const got = typeof v === "number" ? 1 : v.width
880
+ const got = widthOf(v)
491
881
  if (got !== 1 && got !== letters.length) {
492
- 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)
493
883
  }
494
884
  const parts: LV[] = [...names].map((c) => {
495
885
  const j = letters.indexOf(c)
@@ -499,21 +889,20 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
499
889
  return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
500
890
  }
501
891
 
502
- function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
503
- const got = typeof v === "number" ? 1 : v.width
504
- if (got === want) return
505
- // A declaration is an assertion, not an input to inference
506
- // (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
507
- // than quietly promoting one to the other.
508
- const hint = got > want ? ` Take the components you want with a swizzle, as in .${"xyzw".slice(0, want)}.` : ""
509
- fail(`${name} is declared ${type} and this is a ${widthType(got as SLType)}.${hint}`, pos)
510
- }
511
-
512
892
  // MARK: expressions
513
893
 
514
- 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 {
515
899
  switch (e.k) {
516
- 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
517
906
  case "hex": return at(e.pos, () => sl.color(e.hex) as unknown as Val)
518
907
  case "ident": {
519
908
  const b = scope.lookup(e.name)
@@ -522,107 +911,226 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
522
911
  }
523
912
  case "member": {
524
913
  const obj = lowerExpr(e.obj, scope)
525
- return at(e.pos, () => asVal(obj).swz(e.name) as unknown as Val)
526
- }
527
- case "unary": {
528
- const v = lowerExpr(e.arg, scope)
529
- if (e.op === "+") return v
530
- if (e.op === "-") return typeof v === "number" ? -v : at(e.pos, () => v.neg())
531
- return oneMinus(v, e.pos)
532
- }
533
- case "binary": return lowerBinary(e, scope)
534
- case "cond": {
535
- const cond = lowerExpr(e.cond, scope)
536
- if (typeof cond === "number") {
537
- return lowerExpr(cond >= 0.5 ? e.then : e.else, scope)
538
- }
539
- const t = lowerExpr(e.then, scope)
540
- const f = lowerExpr(e.else, scope)
541
- 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)
542
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)
543
919
  case "call": return lowerCall(e, scope)
544
920
  }
545
921
  }
546
922
 
547
- function lowerBinary(e: Extract<Expr, { k: "binary" }>, scope: Scope): LV {
548
- const a = lowerExpr(e.a, scope)
549
- 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 {
550
948
  const pos = e.pos
551
- return at(pos, () => {
552
- switch (e.op) {
553
- case "+": case "-": case "*": case "/": case "%":
554
- return arithmetic(e.op, a, b, pos)
555
- // Comparisons and logic are arithmetic the IR already has, as
556
- // `Specs/SL_TEXT.md` 3.5 lays out. Nothing here branches, and
557
- // every result is a float that is 0 or 1.
558
- case "<=": return sl.step(a, b)
559
- case ">=": return sl.step(b, a)
560
- case "<": return oneMinus(sl.step(b, a), pos)
561
- case ">": return oneMinus(sl.step(a, b), pos)
562
- case "==": return oneMinus(notEqual(a, b, pos), pos)
563
- case "!=": return notEqual(a, b, pos)
564
- case "&&": return typeof a === "number" && typeof b === "number"
565
- ? Math.min(a, b) : asVal(a).min(b)
566
- case "||": return typeof a === "number" && typeof b === "number"
567
- ? Math.max(a, b) : asVal(a).max(b)
568
- }
569
- })
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)
570
954
  }
571
955
 
572
- /** `abs(sign(a - b))`: 0 when equal, 1 otherwise, component wise. */
573
- function notEqual(a: LV, b: LV, pos: Pos): LV {
574
- const d = arithmetic("-", a, b, pos)
575
- if (typeof d === "number") return Math.abs(Math.sign(d))
576
- 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
577
959
  }
578
960
 
579
- function oneMinus(v: LV, pos: Pos): LV {
580
- if (typeof v === "number") return 1 - v
581
- 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)
582
969
  }
583
970
 
584
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 {
585
988
  if (typeof a === "number" && typeof b === "number") {
586
989
  if ((op === "/" || op === "%") && b === 0) {
587
990
  fail(`this divides ${a} by zero, and the result would not be a number`, pos)
588
991
  }
589
992
  return ARITH[op](a, b)
590
993
  }
591
- const m = METHOD[op]
592
994
  // A scalar on the LEFT has to become a value first, so it broadcasts
593
995
  // through a swizzle rather than as a wide constant. That is what
594
996
  // `sl.float(n).sub(v)` does too, and the two forms have to agree.
595
- 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)))
596
1104
  }
597
1105
 
598
1106
  function lowerCall(e: Extract<Expr, { k: "call" }>, scope: Scope): LV {
599
1107
  const callee = e.callee
1108
+ const floats = (args: Expr[]) => args.map((a) => toFloat(lowerExpr(a, scope), a.pos))
600
1109
 
601
1110
  if (callee.k === "member") {
602
1111
  const shape = callee.name as SlSdfKind
603
- const p = lowerExpr(e.args[0]!, scope)
1112
+ const p = toFloat(lowerExpr(e.args[0]!, scope), e.args[0]!.pos)
604
1113
  if (typeof p === "number" || p.width !== 2) {
605
1114
  fail(`sdf.${shape} measures the distance to a point, so it takes a float2`, e.args[0]!.pos)
606
1115
  }
607
- const params = e.args.slice(1).map((a) => lowerExpr(a, scope))
1116
+ const params = floats(e.args.slice(1))
608
1117
  return at(e.pos, () => sl.sdf(shape, p as never, params))
609
1118
  }
610
1119
 
611
1120
  const n = (callee as Extract<Expr, { k: "ident" }>).name
612
1121
 
613
- if (n in TYPE_WIDTH) {
614
- return construct(n as TypeName, e.args.map((a) => lowerExpr(a, scope)), e.pos)
615
- }
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)
616
1124
 
617
1125
  if (n === "tex2D" || n === "tex2Dlod") {
618
1126
  const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
619
1127
  const tex = samplers.get(texName)!
620
- const uv = lowerExpr(e.args[1]!, scope)
1128
+ const uv = toFloat(lowerExpr(e.args[1]!, scope), e.args[1]!.pos)
621
1129
  if (typeof uv === "number" || uv.width !== 2) {
622
1130
  fail(`${n} samples at a float2`, e.args[1]!.pos)
623
1131
  }
624
1132
  if (n === "tex2D") return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
625
- const lod = lowerExpr(e.args[2]!, scope)
1133
+ const lod = toFloat(lowerExpr(e.args[2]!, scope), e.args[2]!.pos)
626
1134
  if (typeof lod !== "number" && lod.width !== 1) {
627
1135
  fail(`a mip level is one number, and this is a ${widthType(lod.width)}`, e.args[2]!.pos)
628
1136
  }
@@ -630,13 +1138,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
630
1138
  }
631
1139
 
632
1140
  if (n === "ramp") {
633
- const t = lowerExpr(e.args[0]!, scope)
1141
+ const t = toFloat(lowerExpr(e.args[0]!, scope), e.args[0]!.pos)
634
1142
  // A hex stays the string it was, so a ramp of hexes is the program
635
1143
  // it always was. Anything else is a value, which has to be a colour
636
1144
  // as written (`sl.ramp` says why), a colour uniform say.
637
1145
  const stops = e.args.slice(1).map((a) => {
638
1146
  if (a.k === "hex") return a.hex
639
- const v = lowerExpr(a, scope)
1147
+ const v = toFloat(lowerExpr(a, scope), a.pos)
640
1148
  if (typeof v === "number" || v.width < 3) {
641
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)
642
1150
  }
@@ -648,76 +1156,105 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
648
1156
 
649
1157
  const builtin = BUILTINS[n]
650
1158
  if (builtin !== undefined) {
651
- const args = e.args.map((a) => lowerExpr(a, scope))
652
- 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))
653
1168
  }
654
1169
 
655
1170
  const fn = funcs.get(n)!
656
- 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)
657
1192
  }
658
1193
 
659
1194
  /**
660
1195
  * Inlines a call: the body, lowered against these arguments, in a fresh
661
1196
  * scope that can see the globals and nothing else the caller had.
662
1197
  */
663
- 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 {
664
1199
  const scope = new Scope(global)
665
1200
  fn.params.forEach((p, i) => {
666
- const v = args[i]!
667
- const got = typeof v === "number" ? 1 : v.width
668
- 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) {
669
1208
  fail(
670
1209
  `${fn.name}'s parameter ${p.name} is a ${p.type} and this argument is a ` +
671
- `${widthType(got as SLType)}`,
672
- e.args[i]!.pos,
1210
+ `${widthType(got)}`,
1211
+ arg.pos,
673
1212
  )
674
1213
  }
675
- 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) })
676
1215
  })
677
- const out = exec(fn.body, scope, TYPE_WIDTH[fn.ret])
678
- if (out === undefined) fail(`${fn.name} never returns a value`, e.pos)
679
- const got = typeof out === "number" ? 1 : out.width
680
- if (got !== TYPE_WIDTH[fn.ret]) {
681
- fail(`${fn.name} is declared to return a ${fn.ret} and returns a ${widthType(got as SLType)}`, fn.pos)
682
- }
683
- return out
1216
+ return callBody(fn, scope)
684
1217
  }
685
1218
 
686
- function construct(type: TypeName, args: LV[], pos: Pos): LV {
1219
+ function construct(type: TypeName, args: Array<number | Val>, pos: Pos): LV {
687
1220
  const width = TYPE_WIDTH[type]
688
1221
  if (width === 1) {
689
1222
  const v = args[0]!
690
- const got = typeof v === "number" ? 1 : v.width
691
- if (args.length !== 1 || got !== 1) {
1223
+ if (args.length !== 1 || widthOf(v) !== 1) {
692
1224
  fail("float(x) takes one float; use a swizzle to narrow a wider value", pos)
693
1225
  }
694
1226
  return v
695
1227
  }
696
1228
  if (args.length === 1) {
697
1229
  const v = args[0]!
698
- const got = typeof v === "number" ? 1 : v.width
1230
+ const got = widthOf(v)
699
1231
  // HLSL's scalar broadcast, and the identity for a value that is
700
1232
  // already this wide. `float4(v4)` is `v4`, not four swizzles.
701
1233
  if (got === width) return v
702
1234
  if (got === 1) return at(pos, () => compose(width, new Array<LV>(width).fill(v)))
703
- 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)
704
1236
  }
705
1237
  return at(pos, () => compose(width, args))
706
1238
  }
707
1239
 
708
1240
  function compose(width: SLType, parts: LV[]): Val {
709
- 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[]
710
1242
  if (width === 2) return sl.vec2(...ps) as unknown as Val
711
1243
  if (width === 3) return sl.vec3(...ps) as unknown as Val
712
1244
  return sl.vec4(...ps) as unknown as Val
713
1245
  }
714
-
715
- function asVal(v: LV): Val {
716
- return typeof v === "number" ? (sl.float(v) as unknown as Val) : v
717
- }
718
1246
  }
719
1247
 
1248
+ // MARK: tables and small helpers
1249
+
720
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"])
721
1258
 
722
1259
  const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
723
1260
  "+": (a, b) => a + b,
@@ -729,6 +1266,33 @@ const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
729
1266
  "%": (a, b) => a % b,
730
1267
  }
731
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
+
732
1296
  const METHOD: Record<ArithOp, "add" | "sub" | "mul" | "div" | "mod"> = {
733
1297
  "+": "add", "-": "sub", "*": "mul", "/": "div", "%": "mod",
734
1298
  }
@@ -737,10 +1301,195 @@ function isArith(op: string): op is ArithOp {
737
1301
  return op in ARITH
738
1302
  }
739
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
+
740
1355
  function widthType(w: SLType): TypeName {
741
1356
  return (["float", "float2", "float3", "float4"] as const)[w - 1]!
742
1357
  }
743
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
+
744
1493
  /**
745
1494
  * An EDSL message in the words of a `.sl` file.
746
1495
  *