onejs-sl 0.1.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 +10 -0
- package/README.md +266 -0
- package/package.json +60 -0
- package/src/color.ts +16 -0
- package/src/core.ts +24 -0
- package/src/emit/unity.ts +3 -0
- package/src/emit/web.ts +3 -0
- package/src/encode.ts +462 -0
- package/src/hlsl.ts +275 -0
- package/src/index.ts +24 -0
- package/src/ir.ts +301 -0
- package/src/lang/README.md +167 -0
- package/src/lang/ast.ts +87 -0
- package/src/lang/builtins.ts +168 -0
- package/src/lang/check.ts +541 -0
- package/src/lang/index.ts +55 -0
- package/src/lang/lexer.ts +176 -0
- package/src/lang/lower.ts +628 -0
- package/src/lang/parser.ts +460 -0
- package/src/lang/prelude.ts +75 -0
- package/src/limits.ts +50 -0
- package/src/ops.ts +346 -0
- package/src/serial.ts +137 -0
- package/src/shapes.ts +49 -0
- package/src/sl.ts +721 -0
- package/src/tables.ts +15 -0
- package/src/vm.ts +11 -0
- package/src/web.ts +273 -0
- package/src/weblib.ts +1303 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST to IR, through the EDSL.
|
|
3
|
+
*
|
|
4
|
+
* Phase A of `Specs/SL_TEXT.md` section 4, and the file the parity test is
|
|
5
|
+
* about. Nothing here touches the Builder: `sin(x)` in a file becomes the same
|
|
6
|
+
* `sl.sin(x)` call a TypeScript author would have written, so the two surfaces
|
|
7
|
+
* cannot produce different graphs. That is why a `.sl` file and its EDSL twin
|
|
8
|
+
* hash the same, and why the existing GPU and codegen golden tests cover the
|
|
9
|
+
* text form without being told about it.
|
|
10
|
+
*
|
|
11
|
+
* Four things happen here that the file's author does not see:
|
|
12
|
+
*
|
|
13
|
+
* **Locals become SSA.** A local is a JavaScript binding holding a recorded
|
|
14
|
+
* value, so `p = p * 2;` rebinds and the IR never learns that a variable
|
|
15
|
+
* existed. Reassignment is free; it is not a store.
|
|
16
|
+
*
|
|
17
|
+
* **`for` unrolls.** The bounds are constants, so the body is lowered once per
|
|
18
|
+
* iteration with the counter substituted as a number. `sl.unrolled` records the
|
|
19
|
+
* span so the instruction ceiling error can name the loop that spent it.
|
|
20
|
+
*
|
|
21
|
+
* **`if` becomes `select`.** Both sides are lowered and every local assigned in
|
|
22
|
+
* either is selected at the join. A constant condition folds instead, which is
|
|
23
|
+
* the one case where a branch really does disappear.
|
|
24
|
+
*
|
|
25
|
+
* **Functions inline.** A call lowers the callee's body against its arguments
|
|
26
|
+
* in a fresh scope. `ring(p, 0.3, 0.01)` costs exactly what writing the body
|
|
27
|
+
* out would.
|
|
28
|
+
*
|
|
29
|
+
* A NUMBER STAYS A NUMBER as long as it can. `uv * 8` reaches the EDSL as
|
|
30
|
+
* `uv.mul(8)`, not as `uv.mul(sl.float(8))`, because the first broadcasts to a
|
|
31
|
+
* float2 constant and the second builds a float plus a swizzle. Same picture,
|
|
32
|
+
* different graph, different hash, so the distinction is load bearing rather
|
|
33
|
+
* than an optimisation.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { INPUTS, type Program, type SLType } from "../ir"
|
|
37
|
+
import type { SlSdfKind } from "../shapes"
|
|
38
|
+
import * as sl from "../sl"
|
|
39
|
+
import type { Num, Val } from "../sl"
|
|
40
|
+
import { TYPE_WIDTH, type Expr, type FuncDecl, type Stmt, type TypeName } from "./ast"
|
|
41
|
+
import { BUILTINS } from "./builtins"
|
|
42
|
+
import type { Checked } from "./check"
|
|
43
|
+
import { SLParseError, type Pos } from "./lexer"
|
|
44
|
+
|
|
45
|
+
/** A lowered value. A plain number is one that has not had to become a node yet. */
|
|
46
|
+
type LV = Val | number
|
|
47
|
+
|
|
48
|
+
interface Binding {
|
|
49
|
+
/** The declared width. A reassignment has to match it. */
|
|
50
|
+
width: SLType
|
|
51
|
+
value: LV
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How many iterations a `for` may unroll to. `sl.repeat`'s ceiling, for the same reason. */
|
|
55
|
+
const MAX_UNROLL = 64
|
|
56
|
+
|
|
57
|
+
class Scope {
|
|
58
|
+
private readonly vars = new Map<string, Binding>()
|
|
59
|
+
|
|
60
|
+
constructor(readonly parent: Scope | null, readonly isGlobal = false) {}
|
|
61
|
+
|
|
62
|
+
declare(name: string, binding: Binding): void { this.vars.set(name, binding) }
|
|
63
|
+
|
|
64
|
+
lookup(name: string): Binding | undefined {
|
|
65
|
+
const own = this.vars.get(name)
|
|
66
|
+
if (own !== undefined) return own
|
|
67
|
+
return this.parent?.lookup(name)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every binding an `if` could reassign: the locals, never the globals.
|
|
72
|
+
*
|
|
73
|
+
* Uniforms, textures, consts and inputs live in the global scope and the
|
|
74
|
+
* checker refuses assigning to any of them, so the join never has to
|
|
75
|
+
* consider one.
|
|
76
|
+
*/
|
|
77
|
+
locals(): Binding[] {
|
|
78
|
+
if (this.isGlobal) return []
|
|
79
|
+
const out = this.parent === null ? [] : this.parent.locals()
|
|
80
|
+
for (const b of this.vars.values()) out.push(b)
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function lower(checked: Checked): Program {
|
|
86
|
+
const { unit, funcs } = checked
|
|
87
|
+
const file = unit.file
|
|
88
|
+
const main = unit.main!
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Runs an EDSL call and gives its error a place in the file.
|
|
92
|
+
*
|
|
93
|
+
* The EDSL's messages are already the right words ("z is component 3 of a
|
|
94
|
+
* vec2, which has 2"); what they lack is a line. Rethrowing here rather
|
|
95
|
+
* than writing a second set of messages is what keeps the two surfaces
|
|
96
|
+
* saying the same thing about the same mistake.
|
|
97
|
+
*/
|
|
98
|
+
const at = <T,>(pos: Pos, fn: () => T): T => {
|
|
99
|
+
try {
|
|
100
|
+
return fn()
|
|
101
|
+
} catch (e) {
|
|
102
|
+
throw located(e, pos)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const located = (e: unknown, pos: Pos): unknown => {
|
|
107
|
+
if (e instanceof SLParseError) return e
|
|
108
|
+
const raw = e instanceof Error ? e.message : String(e)
|
|
109
|
+
return new SLParseError(raw.replace(/^\[onejs sl] /, ""), file, pos)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fail: (message: string, pos: Pos, length?: number) => never =
|
|
113
|
+
(message, pos, length = 1) => {
|
|
114
|
+
throw new SLParseError(message, file, pos, length)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The globals every helper below reads, built inside the recording callback
|
|
118
|
+
// and held out here so a function being inlined can see them and nothing
|
|
119
|
+
// else the caller had in scope.
|
|
120
|
+
const global = new Scope(null, true)
|
|
121
|
+
const samplers = new Map<string, sl.Texture>()
|
|
122
|
+
/** Where the last `return` lowered was, so an error about it points at it. */
|
|
123
|
+
let returnedAt: Pos = main.pos
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
return record()
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// Nothing should reach here with a position already: every EDSL call
|
|
129
|
+
// below is wrapped. This is the net, so that a program level refusal
|
|
130
|
+
// still names the file rather than arriving as a bare SLError.
|
|
131
|
+
throw located(e, main.pos)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function record(): Program {
|
|
135
|
+
return sl.program((inputs) => {
|
|
136
|
+
for (const [name, width] of Object.entries(INPUTS)) {
|
|
137
|
+
global.declare(name, { width: width as SLType, value: (inputs as never)[name] })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Uniforms and textures take their slots in DECLARATION order, before
|
|
141
|
+
// anything is lowered, so a slot is a property of the file rather than
|
|
142
|
+
// of which uniform the program happens to read first. An unused
|
|
143
|
+
// declaration still takes its slot and still reaches the host.
|
|
144
|
+
for (const u of unit.uniforms) {
|
|
145
|
+
const width = TYPE_WIDTH[u.type]
|
|
146
|
+
const { components, colour } = uniformDefault(u.type, u.init)
|
|
147
|
+
const raw = at(u.pos, () => declareUniform(u.name, u.type, components))
|
|
148
|
+
// A hex default says the uniform IS a colour, so every read of it
|
|
149
|
+
// converts, exactly as a hex literal in an expression does. Without
|
|
150
|
+
// that, `#ff8040` and a uniform defaulting to `#ff8040` would be two
|
|
151
|
+
// different colours in one file.
|
|
152
|
+
global.declare(u.name, { width, value: colour ? at(u.pos, () => sl.toLinear(raw)) : raw })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
|
|
156
|
+
|
|
157
|
+
for (const c of unit.consts) {
|
|
158
|
+
const v = lowerExpr(c.init, global)
|
|
159
|
+
assertWidth(v, TYPE_WIDTH[c.type], c.name, c.type, c.pos)
|
|
160
|
+
global.declare(c.name, { width: TYPE_WIDTH[c.type], value: v })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const out = exec(main.body, new Scope(global))
|
|
164
|
+
// Checked here rather than left to `sl.program`, which would refuse it
|
|
165
|
+
// in the EDSL's words ("a vec4") about a file whose types are spelled
|
|
166
|
+
// float4, and from outside the reach of a position.
|
|
167
|
+
if (out === undefined || typeof out === "number" || out.width !== 4) {
|
|
168
|
+
const got = out === undefined ? "nothing"
|
|
169
|
+
: typeof out === "number" ? "a single number"
|
|
170
|
+
: `a ${widthType(out.width)}`
|
|
171
|
+
fail(
|
|
172
|
+
`main returns a float4, a colour with alpha, and this returns ${got}. ` +
|
|
173
|
+
`Wrap it: float4(value, 1)`,
|
|
174
|
+
returnedAt, 6,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
return out as never
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: declarations
|
|
182
|
+
|
|
183
|
+
function declareUniform(name: string, type: TypeName, c: number[]): Val {
|
|
184
|
+
switch (type) {
|
|
185
|
+
case "float": return sl.uniform.float(name, c[0]!) as unknown as Val
|
|
186
|
+
case "float2": return sl.uniform.vec2(name, [c[0]!, c[1]!]) as unknown as Val
|
|
187
|
+
case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!]) as unknown as Val
|
|
188
|
+
case "float4": return sl.uniform.vec4(name, [c[0]!, c[1]!, c[2]!, c[3]!]) as unknown as Val
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A uniform's default, folded to numbers.
|
|
194
|
+
*
|
|
195
|
+
* LITERALS ONLY, deliberately. The default is baked into the program before
|
|
196
|
+
* anything runs and it is what the generated shader writes into its
|
|
197
|
+
* Properties block, so it cannot depend on a value. Refusing identifiers
|
|
198
|
+
* outright also side steps an ordering puzzle nobody would enjoy: a const
|
|
199
|
+
* may read a uniform, so letting a uniform read a const would make the two
|
|
200
|
+
* tables depend on each other.
|
|
201
|
+
*/
|
|
202
|
+
function uniformDefault(type: TypeName, init: Expr | null): { components: number[]; colour: boolean } {
|
|
203
|
+
const width = TYPE_WIDTH[type]
|
|
204
|
+
if (init === null) {
|
|
205
|
+
const zeros = new Array<number>(width).fill(0)
|
|
206
|
+
if (width === 4) zeros[3] = 1
|
|
207
|
+
return { components: zeros, colour: false }
|
|
208
|
+
}
|
|
209
|
+
if (init.k === "hex") {
|
|
210
|
+
if (width < 3) {
|
|
211
|
+
fail(
|
|
212
|
+
`a colour has three or four components, so ${type} cannot default to ${init.hex}`,
|
|
213
|
+
init.pos, init.hex.length,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
const c = at(init.pos, () => sl.parseColor(init.hex))
|
|
217
|
+
return { components: c.slice(0, width), colour: true }
|
|
218
|
+
}
|
|
219
|
+
const c = constantComponents(init)
|
|
220
|
+
if (c === null) {
|
|
221
|
+
fail(
|
|
222
|
+
"a uniform's default is baked into the program before anything runs, so it has to be " +
|
|
223
|
+
"written out: a number, a float2, float3 or float4 of numbers, or a colour like #ff8040",
|
|
224
|
+
init.pos,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
if (c.length !== width) {
|
|
228
|
+
fail(
|
|
229
|
+
`this default has ${c.length} component${c.length === 1 ? "" : "s"} and the uniform is ` +
|
|
230
|
+
`declared ${type}`,
|
|
231
|
+
init.pos,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
return { components: c, colour: false }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The literal arithmetic a uniform default is allowed to be, or nothing. */
|
|
238
|
+
function constantComponents(e: Expr): number[] | null {
|
|
239
|
+
switch (e.k) {
|
|
240
|
+
case "num": return [e.value]
|
|
241
|
+
case "unary": {
|
|
242
|
+
const v = constantComponents(e.arg)
|
|
243
|
+
if (v === null) return null
|
|
244
|
+
if (e.op === "+") return v
|
|
245
|
+
if (e.op === "-") return v.map((n) => -n)
|
|
246
|
+
return v.map((n) => 1 - n)
|
|
247
|
+
}
|
|
248
|
+
case "binary": {
|
|
249
|
+
const a = constantComponents(e.a)
|
|
250
|
+
const b = constantComponents(e.b)
|
|
251
|
+
if (a === null || b === null) return null
|
|
252
|
+
if (!isArith(e.op)) return null
|
|
253
|
+
const fold = ARITH[e.op]
|
|
254
|
+
const n = Math.max(a.length, b.length)
|
|
255
|
+
if (a.length !== b.length && a.length !== 1 && b.length !== 1) return null
|
|
256
|
+
const out: number[] = []
|
|
257
|
+
for (let i = 0; i < n; i++) out.push(fold(a[a.length === 1 ? 0 : i]!, b[b.length === 1 ? 0 : i]!))
|
|
258
|
+
return out
|
|
259
|
+
}
|
|
260
|
+
case "call": {
|
|
261
|
+
if (e.callee.k !== "ident" || !(e.callee.name in TYPE_WIDTH)) return null
|
|
262
|
+
const width = TYPE_WIDTH[e.callee.name as TypeName]
|
|
263
|
+
const parts: number[] = []
|
|
264
|
+
for (const a of e.args) {
|
|
265
|
+
const v = constantComponents(a)
|
|
266
|
+
if (v === null) return null
|
|
267
|
+
parts.push(...v)
|
|
268
|
+
}
|
|
269
|
+
if (parts.length === 1 && width > 1) return new Array<number>(width).fill(parts[0]!)
|
|
270
|
+
return parts.length === width ? parts : null
|
|
271
|
+
}
|
|
272
|
+
default: return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// MARK: statements
|
|
277
|
+
|
|
278
|
+
/** Runs a body. The value of its `return`, or nothing when it has none. */
|
|
279
|
+
function exec(body: Stmt[], scope: Scope): LV | undefined {
|
|
280
|
+
for (const s of body) {
|
|
281
|
+
switch (s.k) {
|
|
282
|
+
case "var":
|
|
283
|
+
case "const": {
|
|
284
|
+
const v = lowerExpr(s.init, scope)
|
|
285
|
+
assertWidth(v, TYPE_WIDTH[s.type], s.name, s.type, s.pos)
|
|
286
|
+
scope.declare(s.name, { width: TYPE_WIDTH[s.type], value: v })
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
case "assign": {
|
|
290
|
+
const name = (s.target as Extract<Expr, { k: "ident" }>).name
|
|
291
|
+
const b = scope.lookup(name)!
|
|
292
|
+
let v = lowerExpr(s.value, scope)
|
|
293
|
+
if (s.op !== "=") {
|
|
294
|
+
const op = s.op[0] as ArithOp
|
|
295
|
+
v = at(s.pos, () => arithmetic(op, b.value, v, s.pos))
|
|
296
|
+
}
|
|
297
|
+
assertWidth(v, b.width, name, widthType(b.width), s.pos)
|
|
298
|
+
b.value = v
|
|
299
|
+
break
|
|
300
|
+
}
|
|
301
|
+
case "if": {
|
|
302
|
+
const cond = lowerExpr(s.cond, scope)
|
|
303
|
+
if (typeof cond === "number") {
|
|
304
|
+
// The one case where a branch really does vanish: the
|
|
305
|
+
// other side is never lowered, so it costs nothing.
|
|
306
|
+
exec(cond >= 0.5 ? s.then : s.else, new Scope(scope))
|
|
307
|
+
break
|
|
308
|
+
}
|
|
309
|
+
const locals = scope.locals()
|
|
310
|
+
const before = locals.map((b) => b.value)
|
|
311
|
+
exec(s.then, new Scope(scope))
|
|
312
|
+
const whenTrue = locals.map((b) => b.value)
|
|
313
|
+
locals.forEach((b, i) => { b.value = before[i]! })
|
|
314
|
+
exec(s.else, new Scope(scope))
|
|
315
|
+
const whenFalse = locals.map((b) => b.value)
|
|
316
|
+
locals.forEach((b, i) => {
|
|
317
|
+
const t = whenTrue[i]!
|
|
318
|
+
const f = whenFalse[i]!
|
|
319
|
+
b.value = same(t, f) ? t : at(s.pos, () => sl.select(cond, t, f))
|
|
320
|
+
})
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case "for": {
|
|
324
|
+
const from = constantBound(s.from, "start", scope)
|
|
325
|
+
const to = constantBound(s.to, "bound", scope)
|
|
326
|
+
const step = constantBound(s.step, "step", scope)
|
|
327
|
+
if (!(step > 0)) {
|
|
328
|
+
fail(
|
|
329
|
+
`this loop steps by ${step}, so it never ends. A for loop unrolls, which ` +
|
|
330
|
+
`means its count has to be known and finite`,
|
|
331
|
+
s.pos,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
let count = 0
|
|
335
|
+
for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
|
|
336
|
+
count++
|
|
337
|
+
if (count > MAX_UNROLL) {
|
|
338
|
+
fail(
|
|
339
|
+
`this loop would unroll to more than ${MAX_UNROLL} iterations. There is ` +
|
|
340
|
+
`no loop on either backend, so every iteration is emitted in full and ` +
|
|
341
|
+
`the VM runs at most 256 instructions`,
|
|
342
|
+
s.pos,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
at(s.pos, () => sl.unrolled(count, () => {
|
|
347
|
+
for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
|
|
348
|
+
const inner = new Scope(scope)
|
|
349
|
+
inner.declare(s.counter, { width: 1, value: i })
|
|
350
|
+
exec(s.body, inner)
|
|
351
|
+
}
|
|
352
|
+
}))
|
|
353
|
+
break
|
|
354
|
+
}
|
|
355
|
+
case "return":
|
|
356
|
+
returnedAt = s.pos
|
|
357
|
+
return lowerExpr(s.value, scope)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return undefined
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function constantBound(e: Expr, what: string, scope: Scope): number {
|
|
364
|
+
const v = lowerExpr(e, scope)
|
|
365
|
+
if (typeof v !== "number") {
|
|
366
|
+
fail(
|
|
367
|
+
`a for loop unrolls at build time, so its ${what} has to be a constant. This one is ` +
|
|
368
|
+
`computed while the shader runs, and neither backend has a loop to run it in`,
|
|
369
|
+
e.pos,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
return v
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Two lowered values that are certainly the same node, so a join can skip the select. */
|
|
376
|
+
function same(a: LV, b: LV): boolean {
|
|
377
|
+
if (a === b) return true
|
|
378
|
+
return typeof a !== "number" && typeof b !== "number" && a.ref === b.ref && a.width === b.width
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
|
|
382
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
383
|
+
if (got === want) return
|
|
384
|
+
// A declaration is an assertion, not an input to inference
|
|
385
|
+
// (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
|
|
386
|
+
// than quietly promoting one to the other.
|
|
387
|
+
const hint = got === 1 ? ` Wrap it in ${type}(...) to broadcast the value across ${want} components.` : ""
|
|
388
|
+
fail(`${name} is declared ${type} and this is a ${widthType(got as SLType)}.${hint}`, pos)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// MARK: expressions
|
|
392
|
+
|
|
393
|
+
function lowerExpr(e: Expr, scope: Scope): LV {
|
|
394
|
+
switch (e.k) {
|
|
395
|
+
case "num": return e.value
|
|
396
|
+
case "hex": return at(e.pos, () => sl.color(e.hex) as unknown as Val)
|
|
397
|
+
case "ident": {
|
|
398
|
+
const b = scope.lookup(e.name)
|
|
399
|
+
if (b === undefined) fail(`"${e.name}" is not declared`, e.pos, e.name.length)
|
|
400
|
+
return b.value
|
|
401
|
+
}
|
|
402
|
+
case "member": {
|
|
403
|
+
const obj = lowerExpr(e.obj, scope)
|
|
404
|
+
return at(e.pos, () => asVal(obj).swz(e.name) as unknown as Val)
|
|
405
|
+
}
|
|
406
|
+
case "unary": {
|
|
407
|
+
const v = lowerExpr(e.arg, scope)
|
|
408
|
+
if (e.op === "+") return v
|
|
409
|
+
if (e.op === "-") return typeof v === "number" ? -v : at(e.pos, () => v.neg())
|
|
410
|
+
return oneMinus(v, e.pos)
|
|
411
|
+
}
|
|
412
|
+
case "binary": return lowerBinary(e, scope)
|
|
413
|
+
case "cond": {
|
|
414
|
+
const cond = lowerExpr(e.cond, scope)
|
|
415
|
+
if (typeof cond === "number") {
|
|
416
|
+
return lowerExpr(cond >= 0.5 ? e.then : e.else, scope)
|
|
417
|
+
}
|
|
418
|
+
const t = lowerExpr(e.then, scope)
|
|
419
|
+
const f = lowerExpr(e.else, scope)
|
|
420
|
+
return at(e.pos, () => sl.select(cond, t, f))
|
|
421
|
+
}
|
|
422
|
+
case "call": return lowerCall(e, scope)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function lowerBinary(e: Extract<Expr, { k: "binary" }>, scope: Scope): LV {
|
|
427
|
+
const a = lowerExpr(e.a, scope)
|
|
428
|
+
const b = lowerExpr(e.b, scope)
|
|
429
|
+
const pos = e.pos
|
|
430
|
+
return at(pos, () => {
|
|
431
|
+
switch (e.op) {
|
|
432
|
+
case "+": case "-": case "*": case "/": case "%":
|
|
433
|
+
return arithmetic(e.op, a, b, pos)
|
|
434
|
+
// Comparisons and logic are arithmetic the IR already has, as
|
|
435
|
+
// `Specs/SL_TEXT.md` 3.5 lays out. Nothing here branches, and
|
|
436
|
+
// every result is a float that is 0 or 1.
|
|
437
|
+
case "<=": return sl.step(a, b)
|
|
438
|
+
case ">=": return sl.step(b, a)
|
|
439
|
+
case "<": return oneMinus(sl.step(b, a), pos)
|
|
440
|
+
case ">": return oneMinus(sl.step(a, b), pos)
|
|
441
|
+
case "==": return oneMinus(notEqual(a, b, pos), pos)
|
|
442
|
+
case "!=": return notEqual(a, b, pos)
|
|
443
|
+
case "&&": return typeof a === "number" && typeof b === "number"
|
|
444
|
+
? Math.min(a, b) : asVal(a).min(b)
|
|
445
|
+
case "||": return typeof a === "number" && typeof b === "number"
|
|
446
|
+
? Math.max(a, b) : asVal(a).max(b)
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** `abs(sign(a - b))`: 0 when equal, 1 otherwise, component wise. */
|
|
452
|
+
function notEqual(a: LV, b: LV, pos: Pos): LV {
|
|
453
|
+
const d = arithmetic("-", a, b, pos)
|
|
454
|
+
if (typeof d === "number") return Math.abs(Math.sign(d))
|
|
455
|
+
return sl.sign(d).abs()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function oneMinus(v: LV, pos: Pos): LV {
|
|
459
|
+
if (typeof v === "number") return 1 - v
|
|
460
|
+
return at(pos, () => sl.float(1).sub(v))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function arithmetic(op: ArithOp, a: LV, b: LV, pos: Pos): LV {
|
|
464
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
465
|
+
if ((op === "/" || op === "%") && b === 0) {
|
|
466
|
+
fail(`this divides ${a} by zero, and the result would not be a number`, pos)
|
|
467
|
+
}
|
|
468
|
+
return ARITH[op](a, b)
|
|
469
|
+
}
|
|
470
|
+
const m = METHOD[op]
|
|
471
|
+
// A scalar on the LEFT has to become a value first, so it broadcasts
|
|
472
|
+
// through a swizzle rather than as a wide constant. That is what
|
|
473
|
+
// `sl.float(n).sub(v)` does too, and the two forms have to agree.
|
|
474
|
+
return asVal(a)[m](b)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function lowerCall(e: Extract<Expr, { k: "call" }>, scope: Scope): LV {
|
|
478
|
+
const callee = e.callee
|
|
479
|
+
|
|
480
|
+
if (callee.k === "member") {
|
|
481
|
+
const shape = callee.name as SlSdfKind
|
|
482
|
+
const p = lowerExpr(e.args[0]!, scope)
|
|
483
|
+
if (typeof p === "number" || p.width !== 2) {
|
|
484
|
+
fail(`sdf.${shape} measures the distance to a point, so it takes a float2`, e.args[0]!.pos)
|
|
485
|
+
}
|
|
486
|
+
const params: number[] = []
|
|
487
|
+
for (const a of e.args.slice(1)) {
|
|
488
|
+
const v = lowerExpr(a, scope)
|
|
489
|
+
if (typeof v !== "number") {
|
|
490
|
+
fail(
|
|
491
|
+
`sdf.${shape}'s shape parameters ride inside the instruction, so they have to ` +
|
|
492
|
+
`be constants. A size that changes belongs on the point: scale or offset it ` +
|
|
493
|
+
`before the call`,
|
|
494
|
+
a.pos,
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
params.push(v)
|
|
498
|
+
}
|
|
499
|
+
return at(e.pos, () => sl.sdf(shape, p as never, params))
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const n = (callee as Extract<Expr, { k: "ident" }>).name
|
|
503
|
+
|
|
504
|
+
if (n in TYPE_WIDTH) {
|
|
505
|
+
return construct(n as TypeName, e.args.map((a) => lowerExpr(a, scope)), e.pos)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (n === "tex2D") {
|
|
509
|
+
const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
|
|
510
|
+
const tex = samplers.get(texName)!
|
|
511
|
+
const uv = lowerExpr(e.args[1]!, scope)
|
|
512
|
+
if (typeof uv === "number" || uv.width !== 2) {
|
|
513
|
+
fail("tex2D samples at a float2", e.args[1]!.pos)
|
|
514
|
+
}
|
|
515
|
+
return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (n === "ramp") {
|
|
519
|
+
const t = lowerExpr(e.args[0]!, scope)
|
|
520
|
+
const stops: string[] = []
|
|
521
|
+
for (const a of e.args.slice(1)) {
|
|
522
|
+
if (a.k !== "hex") {
|
|
523
|
+
fail(
|
|
524
|
+
"a ramp's stops are constants, written as colours: `ramp(t, #000018, #0080ff, " +
|
|
525
|
+
"#ffffff)`. A stop that changes wants lerp between two uniforms instead",
|
|
526
|
+
a.pos,
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
stops.push(a.hex)
|
|
530
|
+
}
|
|
531
|
+
return at(e.pos, () => sl.ramp(t, stops) as unknown as Val)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const builtin = BUILTINS[n]
|
|
535
|
+
if (builtin !== undefined) {
|
|
536
|
+
const args = e.args.map((a) => lowerExpr(a, scope))
|
|
537
|
+
return at(e.pos, () => builtin.lower!(args as Num[]))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const fn = funcs.get(n)!
|
|
541
|
+
return inline(fn, e.args.map((a) => lowerExpr(a, scope)), e)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Inlines a call: the body, lowered against these arguments, in a fresh
|
|
546
|
+
* scope that can see the globals and nothing else the caller had.
|
|
547
|
+
*/
|
|
548
|
+
function inline(fn: FuncDecl, args: LV[], e: Extract<Expr, { k: "call" }>): LV {
|
|
549
|
+
const scope = new Scope(global)
|
|
550
|
+
fn.params.forEach((p, i) => {
|
|
551
|
+
const v = args[i]!
|
|
552
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
553
|
+
if (got !== TYPE_WIDTH[p.type]) {
|
|
554
|
+
fail(
|
|
555
|
+
`${fn.name}'s parameter ${p.name} is a ${p.type} and this argument is a ` +
|
|
556
|
+
`${widthType(got as SLType)}`,
|
|
557
|
+
e.args[i]!.pos,
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
scope.declare(p.name, { width: TYPE_WIDTH[p.type], value: v })
|
|
561
|
+
})
|
|
562
|
+
const out = exec(fn.body, scope)
|
|
563
|
+
if (out === undefined) fail(`${fn.name} never returns a value`, e.pos)
|
|
564
|
+
const got = typeof out === "number" ? 1 : out.width
|
|
565
|
+
if (got !== TYPE_WIDTH[fn.ret]) {
|
|
566
|
+
fail(`${fn.name} is declared to return a ${fn.ret} and returns a ${widthType(got as SLType)}`, fn.pos)
|
|
567
|
+
}
|
|
568
|
+
return out
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function construct(type: TypeName, args: LV[], pos: Pos): LV {
|
|
572
|
+
const width = TYPE_WIDTH[type]
|
|
573
|
+
if (width === 1) {
|
|
574
|
+
const v = args[0]!
|
|
575
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
576
|
+
if (args.length !== 1 || got !== 1) {
|
|
577
|
+
fail("float(x) takes one float; use a swizzle to narrow a wider value", pos)
|
|
578
|
+
}
|
|
579
|
+
return v
|
|
580
|
+
}
|
|
581
|
+
if (args.length === 1) {
|
|
582
|
+
const v = args[0]!
|
|
583
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
584
|
+
// HLSL's scalar broadcast, and the identity for a value that is
|
|
585
|
+
// already this wide. `float4(v4)` is `v4`, not four swizzles.
|
|
586
|
+
if (got === width) return v
|
|
587
|
+
if (got === 1) return at(pos, () => compose(width, new Array<LV>(width).fill(v)))
|
|
588
|
+
fail(`${type}(v) broadcasts a float; a ${widthType(got as SLType)} needs its components spelled out`, pos)
|
|
589
|
+
}
|
|
590
|
+
return at(pos, () => compose(width, args))
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function compose(width: SLType, parts: LV[]): Val {
|
|
594
|
+
const ps = parts as Num[]
|
|
595
|
+
if (width === 2) return sl.vec2(...ps) as unknown as Val
|
|
596
|
+
if (width === 3) return sl.vec3(...ps) as unknown as Val
|
|
597
|
+
return sl.vec4(...ps) as unknown as Val
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function asVal(v: LV): Val {
|
|
601
|
+
return typeof v === "number" ? (sl.float(v) as unknown as Val) : v
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
type ArithOp = "+" | "-" | "*" | "/" | "%"
|
|
606
|
+
|
|
607
|
+
const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
|
|
608
|
+
"+": (a, b) => a + b,
|
|
609
|
+
"-": (a, b) => a - b,
|
|
610
|
+
"*": (a, b) => a * b,
|
|
611
|
+
"/": (a, b) => a / b,
|
|
612
|
+
// JavaScript's % is the truncated remainder, which is what HLSL's fmod is
|
|
613
|
+
// and what the VM's OP_MOD implements. Folding it in JS is the same answer.
|
|
614
|
+
"%": (a, b) => a % b,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const METHOD: Record<ArithOp, "add" | "sub" | "mul" | "div" | "mod"> = {
|
|
618
|
+
"+": "add", "-": "sub", "*": "mul", "/": "div", "%": "mod",
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function isArith(op: string): op is ArithOp {
|
|
622
|
+
return op in ARITH
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function widthType(w: SLType): TypeName {
|
|
626
|
+
return (["float", "float2", "float3", "float4"] as const)[w - 1]!
|
|
627
|
+
}
|
|
628
|
+
|