onejs-sl 0.4.0 → 0.6.0

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