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