onejs-sl 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +41 -19
- package/goldens/goldens.json +52 -3
- package/package.json +1 -1
- package/src/body.ts +103 -20
- package/src/hlsl.ts +1 -0
- package/src/ir.ts +198 -27
- package/src/lang/ast.ts +37 -7
- package/src/lang/check.ts +155 -59
- package/src/lang/glsl.ts +5 -8
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1073 -266
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +44 -1
- package/src/serial.ts +46 -9
- package/src/sl.ts +264 -11
- package/src/structure.ts +285 -0
- package/src/web.ts +108 -22
package/src/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
|
-
*
|
|
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
|
-
* **`
|
|
18
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
63
|
+
import { terminates, type Checked } from "./check"
|
|
45
64
|
import { SLParseError, type Pos } from "./lexer"
|
|
46
65
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
|
141
|
-
const blank = (
|
|
142
|
-
|
|
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(
|
|
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
|
|
214
|
-
const v = recover(() =>
|
|
215
|
-
|
|
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 =
|
|
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.
|
|
343
|
-
*
|
|
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,
|
|
346
|
-
for (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
353
|
-
const v = recover(() =>
|
|
354
|
-
|
|
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,
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
/**
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* an
|
|
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,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
|
548
|
-
const
|
|
549
|
-
|
|
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
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
/**
|
|
573
|
-
function
|
|
574
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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)[
|
|
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)
|
|
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
|
|
614
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
|
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,
|
|
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
|
|
667
|
-
const
|
|
668
|
-
|
|
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
|
|
672
|
-
|
|
1248
|
+
`${widthType(got)}`,
|
|
1249
|
+
arg.pos,
|
|
673
1250
|
)
|
|
674
1251
|
}
|
|
675
|
-
scope.declare(p.name, {
|
|
1252
|
+
scope.declare(p.name, { ...t, value: fit(v, t, `${fn.name}'s parameter ${p.name}`, arg.pos) })
|
|
676
1253
|
})
|
|
677
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
*
|