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,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarations, names and statement shape. Everything that can be known without
|
|
3
|
+
* knowing a type.
|
|
4
|
+
*
|
|
5
|
+
* Phase A of `Specs/SL_TEXT.md` section 4.
|
|
6
|
+
*
|
|
7
|
+
* WHY THE TYPES ARE NOT CHECKED HERE. Every value in this language carries its
|
|
8
|
+
* width, and the EDSL computes that width as it records. A second inference
|
|
9
|
+
* pass in this file would be a second implementation of the same rules, and two
|
|
10
|
+
* implementations of a type system are two type systems. So `lower.ts` infers
|
|
11
|
+
* once, through the EDSL, and asserts the width it got against the type the
|
|
12
|
+
* author declared; a declaration is an assertion rather than an input to
|
|
13
|
+
* inference. What is left for this file is the part lowering cannot see:
|
|
14
|
+
* whether a name exists, whether a body is shaped like a body, and whether the
|
|
15
|
+
* program fits the budgets before anything is built.
|
|
16
|
+
*
|
|
17
|
+
* SHADOWING IS REFUSED. HLSL would let a local called `time` hide the input, or
|
|
18
|
+
* a parameter called `tint` hide a uniform. Allowing it would mean every later
|
|
19
|
+
* question about a name ("is this a texture?") depends on where it is asked
|
|
20
|
+
* from, for no expressive gain in a language whose functions are half a dozen
|
|
21
|
+
* lines long.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { INPUTS } from "../ir"
|
|
25
|
+
import { VM_TEXTURES, VM_UNIFORMS } from "../ops"
|
|
26
|
+
import { SL_GLSL_HINT } from "../ops"
|
|
27
|
+
import { BUILTINS, NOT_YET } from "./builtins"
|
|
28
|
+
import { SL_SDF_SHAPES } from "../shapes"
|
|
29
|
+
import type { Expr, FuncDecl, Stmt, Unit } from "./ast"
|
|
30
|
+
import { SLParseError, type Pos } from "./lexer"
|
|
31
|
+
|
|
32
|
+
const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
33
|
+
|
|
34
|
+
export interface Checked {
|
|
35
|
+
unit: Unit
|
|
36
|
+
/** Every callable function by name, the file's shadowing the prelude's. */
|
|
37
|
+
funcs: Map<string, FuncDecl>
|
|
38
|
+
uniforms: Map<string, Unit["uniforms"][number]>
|
|
39
|
+
textures: Map<string, Unit["textures"][number]>
|
|
40
|
+
consts: Map<string, Extract<Stmt, { k: "const" }>>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates a unit against the prelude it will be lowered with.
|
|
45
|
+
*
|
|
46
|
+
* Returns the resolved tables rather than mutating the unit, so a caller that
|
|
47
|
+
* wants only the declarations (the Play editor's completion, Phase C) can stop
|
|
48
|
+
* here without building a graph.
|
|
49
|
+
*/
|
|
50
|
+
export interface CheckOptions {
|
|
51
|
+
/** Off for the prelude, which is functions only. See `parseUnit`. */
|
|
52
|
+
requireMain?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {}): Checked {
|
|
56
|
+
const file = unit.file
|
|
57
|
+
// Annotated rather than inferred: TypeScript only treats a call as
|
|
58
|
+
// terminating control flow when the callee's `never` comes from an explicit
|
|
59
|
+
// type, so without this every `if (x.k !== "ident") fail(...)` below would
|
|
60
|
+
// fail to narrow.
|
|
61
|
+
const fail: (message: string, pos: Pos, length?: number) => never =
|
|
62
|
+
(message, pos, length = 1) => {
|
|
63
|
+
throw new SLParseError(message, file, pos, length)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const funcs = new Map<string, FuncDecl>()
|
|
67
|
+
for (const fn of prelude) funcs.set(fn.name, fn)
|
|
68
|
+
|
|
69
|
+
const uniforms = new Map<string, Unit["uniforms"][number]>()
|
|
70
|
+
const textures = new Map<string, Unit["textures"][number]>()
|
|
71
|
+
const consts = new Map<string, Extract<Stmt, { k: "const" }>>()
|
|
72
|
+
/**
|
|
73
|
+
* Loop counters currently in scope.
|
|
74
|
+
*
|
|
75
|
+
* A counter is not a value, it is substituted as a number once per
|
|
76
|
+
* unrolled iteration, so assigning to it would look like it changed the
|
|
77
|
+
* loop and change nothing. Shadowing is refused everywhere else, so one
|
|
78
|
+
* flat set is enough to know whether a name is one.
|
|
79
|
+
*/
|
|
80
|
+
const counters = new Set<string>()
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Anything already spoken for at the top level, for the duplicate check.
|
|
84
|
+
*
|
|
85
|
+
* Functions are NOT in here, because a file function shadowing a prelude
|
|
86
|
+
* function of the same name is the sanctioned way to replace one
|
|
87
|
+
* (`Specs/SL_TEXT.md` 3.8). Every other kind of declaration checks `asFunc`
|
|
88
|
+
* as well, so only a function may take a function's name.
|
|
89
|
+
*/
|
|
90
|
+
const taken = (n: string): string | null => {
|
|
91
|
+
if (INPUT_NAMES.has(n)) return "an input"
|
|
92
|
+
if (BUILTINS[n] !== undefined) return "a builtin"
|
|
93
|
+
if (n === "sdf") return "the sdf shape family"
|
|
94
|
+
if (n === "main") return "the fragment function"
|
|
95
|
+
if (uniforms.has(n)) return "a uniform"
|
|
96
|
+
if (textures.has(n)) return "a texture"
|
|
97
|
+
if (consts.has(n)) return "a const"
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const asFunc = (n: string): string | null => {
|
|
102
|
+
const fn = funcs.get(n)
|
|
103
|
+
if (fn === undefined) return null
|
|
104
|
+
return fn.prelude ? "a prelude function" : "a function"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const u of unit.uniforms) {
|
|
108
|
+
const clash = taken(u.name) ?? asFunc(u.name)
|
|
109
|
+
if (clash !== null) fail(`"${u.name}" already names ${clash}`, u.pos, u.name.length)
|
|
110
|
+
uniforms.set(u.name, u)
|
|
111
|
+
}
|
|
112
|
+
if (uniforms.size > VM_UNIFORMS) {
|
|
113
|
+
const over = unit.uniforms[VM_UNIFORMS]!
|
|
114
|
+
fail(
|
|
115
|
+
`this file declares ${uniforms.size} uniforms and a program may hold ${VM_UNIFORMS}. ` +
|
|
116
|
+
`That is the size of the VM's uniform array; past it a slot reads the last one instead ` +
|
|
117
|
+
`of its own, so it is refused here rather than rendered wrong. Pack related values into ` +
|
|
118
|
+
`a float4.`,
|
|
119
|
+
over.pos, over.name.length,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const t of unit.textures) {
|
|
124
|
+
const clash = taken(t.name) ?? asFunc(t.name)
|
|
125
|
+
if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
|
|
126
|
+
textures.set(t.name, t)
|
|
127
|
+
}
|
|
128
|
+
if (textures.size > VM_TEXTURES) {
|
|
129
|
+
const over = unit.textures[VM_TEXTURES]!
|
|
130
|
+
fail(
|
|
131
|
+
`this file declares ${textures.size} textures and a program may sample ${VM_TEXTURES}. ` +
|
|
132
|
+
`The VM shader declares four samplers; a fifth would sample the fourth in the browser ` +
|
|
133
|
+
`and its own after an eject, which is two different pictures from one file.`,
|
|
134
|
+
over.pos, over.name.length,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const c of unit.consts) {
|
|
139
|
+
const clash = taken(c.name) ?? asFunc(c.name)
|
|
140
|
+
if (clash !== null) fail(`"${c.name}" already names ${clash}`, c.pos, c.name.length)
|
|
141
|
+
consts.set(c.name, c)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const fn of unit.funcs) {
|
|
145
|
+
const clash = taken(fn.name)
|
|
146
|
+
if (clash !== null) fail(`"${fn.name}" already names ${clash}`, fn.pos, fn.name.length)
|
|
147
|
+
if (unit.funcs.filter((f) => f.name === fn.name).length > 1) {
|
|
148
|
+
fail(
|
|
149
|
+
`this file declares ${fn.name} more than once. There is no overloading: a function ` +
|
|
150
|
+
`inlines, so two bodies under one name have nothing to pick between them`,
|
|
151
|
+
fn.pos, fn.name.length,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
funcs.set(fn.name, fn)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const main = unit.main
|
|
158
|
+
if (main === null) {
|
|
159
|
+
if (options.requireMain ?? true) throw new Error("check() reached a unit with no main")
|
|
160
|
+
for (const fn of unit.funcs) checkFunction(fn)
|
|
161
|
+
refuseRecursion()
|
|
162
|
+
return { unit, funcs, uniforms, textures, consts }
|
|
163
|
+
}
|
|
164
|
+
if (main.ret !== "float4") {
|
|
165
|
+
fail(`main returns a colour, so it is declared \`float4 main()\`, not ${main.ret}`, main.pos)
|
|
166
|
+
}
|
|
167
|
+
if (main.params.length > 0) {
|
|
168
|
+
fail(
|
|
169
|
+
"main takes no parameters: what a program is given are the free identifiers uv, " +
|
|
170
|
+
"fragCoord, resolution, time and aspect",
|
|
171
|
+
main.params[0]!.pos,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// MARK: bodies
|
|
176
|
+
|
|
177
|
+
// The prelude's own bodies are checked once, where it is built, against a
|
|
178
|
+
// unit that has no uniforms of its own. Re-checking them here would let a
|
|
179
|
+
// file declaring `uniform float p;` fail on a prelude parameter called `p`,
|
|
180
|
+
// which is not the author's mistake and not a name they can see.
|
|
181
|
+
for (const fn of [...unit.funcs, main]) checkFunction(fn)
|
|
182
|
+
|
|
183
|
+
refuseRecursion()
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A function inlines, so a cycle is not slow, it is infinite.
|
|
187
|
+
*
|
|
188
|
+
* Caught here rather than by a depth counter in lowering, because a depth
|
|
189
|
+
* limit reports "too deep" about whichever call happened to be at the
|
|
190
|
+
* bottom, while this names the cycle the author actually wrote.
|
|
191
|
+
*/
|
|
192
|
+
function refuseRecursion(): void {
|
|
193
|
+
const stack: string[] = []
|
|
194
|
+
const done = new Set<string>()
|
|
195
|
+
|
|
196
|
+
const walk = (n: string, at: Pos): void => {
|
|
197
|
+
const cycle = stack.indexOf(n)
|
|
198
|
+
if (cycle >= 0) {
|
|
199
|
+
const path = [...stack.slice(cycle), n].join(" calls ")
|
|
200
|
+
fail(
|
|
201
|
+
`${path}, and a function inlines rather than being called, so that cycle has no ` +
|
|
202
|
+
`bottom. Unroll it into a for loop with a constant count`,
|
|
203
|
+
at, n.length,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
if (done.has(n)) return
|
|
207
|
+
const fn = funcs.get(n)
|
|
208
|
+
if (fn === undefined) return
|
|
209
|
+
stack.push(n)
|
|
210
|
+
for (const c of callsIn(fn.body)) walk(c.name, c.pos)
|
|
211
|
+
stack.pop()
|
|
212
|
+
done.add(n)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const c of callsIn(main === null ? [] : main.body)) walk(c.name, c.pos)
|
|
216
|
+
for (const fn of unit.funcs) walk(fn.name, fn.pos)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function callsIn(body: Stmt[]): Array<{ name: string; pos: Pos }> {
|
|
220
|
+
const out: Array<{ name: string; pos: Pos }> = []
|
|
221
|
+
const expr = (e: Expr): void => {
|
|
222
|
+
switch (e.k) {
|
|
223
|
+
case "call":
|
|
224
|
+
if (e.callee.k === "ident" && funcs.has(e.callee.name)) {
|
|
225
|
+
out.push({ name: e.callee.name, pos: e.callee.pos })
|
|
226
|
+
}
|
|
227
|
+
if (e.callee.k === "member") expr(e.callee.obj)
|
|
228
|
+
for (const a of e.args) expr(a)
|
|
229
|
+
return
|
|
230
|
+
case "member": expr(e.obj); return
|
|
231
|
+
case "unary": expr(e.arg); return
|
|
232
|
+
case "binary": expr(e.a); expr(e.b); return
|
|
233
|
+
case "cond": expr(e.cond); expr(e.then); expr(e.else); return
|
|
234
|
+
default: return
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const stmt = (s: Stmt): void => {
|
|
238
|
+
switch (s.k) {
|
|
239
|
+
case "var":
|
|
240
|
+
case "const": expr(s.init); return
|
|
241
|
+
case "assign": expr(s.value); return
|
|
242
|
+
case "if": expr(s.cond); s.then.forEach(stmt); s.else.forEach(stmt); return
|
|
243
|
+
case "for": expr(s.from); expr(s.to); expr(s.step); s.body.forEach(stmt); return
|
|
244
|
+
case "return": expr(s.value); return
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
body.forEach(stmt)
|
|
248
|
+
return out
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function checkFunction(fn: FuncDecl): void {
|
|
252
|
+
const params = new Set<string>()
|
|
253
|
+
for (const p of fn.params) {
|
|
254
|
+
const clash = taken(p.name) ?? asFunc(p.name)
|
|
255
|
+
if (clash !== null) fail(`"${p.name}" already names ${clash}`, p.pos, p.name.length)
|
|
256
|
+
if (params.has(p.name)) fail(`${fn.name} already has a parameter called "${p.name}"`, p.pos)
|
|
257
|
+
params.add(p.name)
|
|
258
|
+
}
|
|
259
|
+
checkBody(fn, fn.body, new Set(params), true)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* `outermost` is the only place a `return` may stand.
|
|
264
|
+
*
|
|
265
|
+
* `Specs/SL_TEXT.md` 9.2: an `if` lowers to a `select`, which evaluates both
|
|
266
|
+
* sides, so there is nothing for an early return to skip. Refusing it is
|
|
267
|
+
* what keeps that cost honest rather than pretending a branch happened.
|
|
268
|
+
*/
|
|
269
|
+
function checkBody(fn: FuncDecl, body: Stmt[], scope: Set<string>, outermost: boolean): void {
|
|
270
|
+
let returned: Stmt | null = null
|
|
271
|
+
for (const s of body) {
|
|
272
|
+
if (returned !== null) {
|
|
273
|
+
fail("this is after the return, so it can never run", s.pos)
|
|
274
|
+
}
|
|
275
|
+
switch (s.k) {
|
|
276
|
+
case "var":
|
|
277
|
+
case "const": {
|
|
278
|
+
const clash = taken(s.name) ?? asFunc(s.name)
|
|
279
|
+
if (clash !== null) fail(`"${s.name}" already names ${clash}`, s.pos, s.name.length)
|
|
280
|
+
if (scope.has(s.name)) fail(`"${s.name}" is already declared in this body`, s.pos)
|
|
281
|
+
checkExpr(fn, s.init, scope)
|
|
282
|
+
scope.add(s.name)
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
case "assign": {
|
|
286
|
+
if (s.target.k === "member") {
|
|
287
|
+
fail(
|
|
288
|
+
"a swizzle is read only; build the value you want instead, as in " +
|
|
289
|
+
"`p = float2(1, p.y);`",
|
|
290
|
+
s.target.pos,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
if (s.target.k !== "ident") fail("only a local can be assigned to", s.target.pos)
|
|
294
|
+
const n = s.target.name
|
|
295
|
+
const why = taken(n)
|
|
296
|
+
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, s.target.pos, n.length)
|
|
297
|
+
if (counters.has(n)) {
|
|
298
|
+
fail(
|
|
299
|
+
`"${n}" is a loop counter. The loop unrolls and substitutes it as a ` +
|
|
300
|
+
`number, so assigning to it would change nothing. Use another local`,
|
|
301
|
+
s.target.pos, n.length,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
if (!scope.has(n)) {
|
|
305
|
+
fail(unknown(n, scope), s.target.pos, n.length)
|
|
306
|
+
}
|
|
307
|
+
checkExpr(fn, s.value, scope)
|
|
308
|
+
break
|
|
309
|
+
}
|
|
310
|
+
case "if":
|
|
311
|
+
checkExpr(fn, s.cond, scope)
|
|
312
|
+
checkBody(fn, s.then, new Set(scope), false)
|
|
313
|
+
checkBody(fn, s.else, new Set(scope), false)
|
|
314
|
+
break
|
|
315
|
+
case "for": {
|
|
316
|
+
const clash = taken(s.counter) ?? asFunc(s.counter)
|
|
317
|
+
if (clash !== null) fail(`"${s.counter}" already names ${clash}`, s.pos, s.counter.length)
|
|
318
|
+
if (scope.has(s.counter)) fail(`"${s.counter}" is already declared in this body`, s.pos)
|
|
319
|
+
checkExpr(fn, s.from, scope)
|
|
320
|
+
checkExpr(fn, s.to, scope)
|
|
321
|
+
checkExpr(fn, s.step, scope)
|
|
322
|
+
const inner = new Set(scope)
|
|
323
|
+
inner.add(s.counter)
|
|
324
|
+
counters.add(s.counter)
|
|
325
|
+
checkBody(fn, s.body, inner, false)
|
|
326
|
+
counters.delete(s.counter)
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
case "return":
|
|
330
|
+
if (!outermost) {
|
|
331
|
+
fail(
|
|
332
|
+
"a return inside an if or a for has nothing to skip: an if evaluates " +
|
|
333
|
+
"both sides and a for unrolls, so there is no branch to leave early. " +
|
|
334
|
+
"Assign to a local and return it once at the end, or use ?:",
|
|
335
|
+
s.pos,
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
checkExpr(fn, s.value, scope)
|
|
339
|
+
returned = s
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (outermost && returned === null) {
|
|
344
|
+
fail(`${fn.name} never returns a ${fn.ret}`, fn.pos, fn.name.length)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// MARK: expressions
|
|
349
|
+
|
|
350
|
+
function checkExpr(fn: FuncDecl, e: Expr, scope: Set<string>): void {
|
|
351
|
+
switch (e.k) {
|
|
352
|
+
case "num":
|
|
353
|
+
case "hex":
|
|
354
|
+
return
|
|
355
|
+
case "ident": {
|
|
356
|
+
if (scope.has(e.name) || INPUT_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
|
|
357
|
+
if (textures.has(e.name)) {
|
|
358
|
+
fail(
|
|
359
|
+
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
360
|
+
`tex2D. Write \`tex2D(${e.name}, uv)\``,
|
|
361
|
+
e.pos, e.name.length,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
if (e.name === "sdf") {
|
|
365
|
+
fail("sdf names a family of shapes; call one, as in `sdf.circle(p, r)`", e.pos, 3)
|
|
366
|
+
}
|
|
367
|
+
if (funcs.has(e.name) || BUILTINS[e.name] !== undefined) {
|
|
368
|
+
fail(`${e.name} is a function; call it, as in \`${e.name}(...)\``, e.pos, e.name.length)
|
|
369
|
+
}
|
|
370
|
+
fail(unknown(e.name, scope), e.pos, e.name.length)
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
case "member":
|
|
374
|
+
// `sdf.circle` is the one member that is not a swizzle, and it
|
|
375
|
+
// is only legal as the callee of a call, which `checkCall`
|
|
376
|
+
// handles before it ever gets here.
|
|
377
|
+
if (e.obj.k === "ident" && e.obj.name === "sdf") {
|
|
378
|
+
fail(
|
|
379
|
+
`sdf.${e.name} is a shape, so it has to be called: \`sdf.${e.name}(p, ...)\``,
|
|
380
|
+
e.pos, e.name.length,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
checkExpr(fn, e.obj, scope)
|
|
384
|
+
return
|
|
385
|
+
case "call":
|
|
386
|
+
checkCall(fn, e, scope)
|
|
387
|
+
return
|
|
388
|
+
case "unary":
|
|
389
|
+
checkExpr(fn, e.arg, scope)
|
|
390
|
+
return
|
|
391
|
+
case "binary":
|
|
392
|
+
checkExpr(fn, e.a, scope)
|
|
393
|
+
checkExpr(fn, e.b, scope)
|
|
394
|
+
return
|
|
395
|
+
case "cond":
|
|
396
|
+
checkExpr(fn, e.cond, scope)
|
|
397
|
+
checkExpr(fn, e.then, scope)
|
|
398
|
+
checkExpr(fn, e.else, scope)
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function checkCall(fn: FuncDecl, e: Extract<Expr, { k: "call" }>, scope: Set<string>): void {
|
|
404
|
+
const callee = e.callee
|
|
405
|
+
|
|
406
|
+
if (callee.k === "member" && callee.obj.k === "ident" && callee.obj.name === "sdf") {
|
|
407
|
+
if (!(callee.name in SL_SDF_SHAPES)) {
|
|
408
|
+
fail(
|
|
409
|
+
`"${callee.name}" is not a shape. The 42 names are in SL_SDF_SHAPES; the common ` +
|
|
410
|
+
`ones are circle, box, roundedBox, segment, hexagon, star5, pie, arc and heart`,
|
|
411
|
+
callee.pos, callee.name.length,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
for (const a of e.args) checkExpr(fn, a, scope)
|
|
415
|
+
arity(e, "sdf." + callee.name, BUILTINS.sdf!.min, BUILTINS.sdf!.max)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (callee.k !== "ident") {
|
|
420
|
+
fail("only a name can be called", callee.pos)
|
|
421
|
+
}
|
|
422
|
+
const n = callee.name
|
|
423
|
+
|
|
424
|
+
if (n === "float2" || n === "float3" || n === "float4" || n === "float") {
|
|
425
|
+
for (const a of e.args) checkExpr(fn, a, scope)
|
|
426
|
+
if (e.args.length === 0) fail(`${n}() needs at least one component`, e.pos)
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (n === "sdf") {
|
|
431
|
+
fail(
|
|
432
|
+
"sdf names a family of shapes, so the shape is part of the call: `sdf.circle(p, r)`, " +
|
|
433
|
+
"`sdf.box(p, float2(0.2, 0.1))`",
|
|
434
|
+
callee.pos, n.length,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const builtin = BUILTINS[n]
|
|
439
|
+
if (builtin !== undefined) {
|
|
440
|
+
if (n === "tex2D") {
|
|
441
|
+
const [tex, ...rest] = e.args
|
|
442
|
+
arity(e, n, builtin.min, builtin.max)
|
|
443
|
+
if (tex === undefined || tex.k !== "ident" || !textures.has(tex.name)) {
|
|
444
|
+
fail(
|
|
445
|
+
`tex2D samples a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
446
|
+
`then \`tex2D(grain, uv)\``,
|
|
447
|
+
(tex ?? e).pos,
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
for (const a of rest) checkExpr(fn, a, scope)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
for (const a of e.args) checkExpr(fn, a, scope)
|
|
454
|
+
arity(e, n, builtin.min, builtin.max)
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const user = funcs.get(n)
|
|
459
|
+
if (user !== undefined) {
|
|
460
|
+
for (const a of e.args) checkExpr(fn, a, scope)
|
|
461
|
+
arity(e, n, user.params.length, user.params.length)
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (NOT_YET[n] !== undefined) {
|
|
466
|
+
fail(
|
|
467
|
+
`${n} has an opcode but no implementation, so it cannot be written yet: ${NOT_YET[n]}`,
|
|
468
|
+
callee.pos, n.length,
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
if (SL_GLSL_HINT[n] !== undefined) {
|
|
472
|
+
fail(`${n} is GLSL; this is HLSL, so write ${SL_GLSL_HINT[n]}`, callee.pos, n.length)
|
|
473
|
+
}
|
|
474
|
+
if (textures.has(n)) {
|
|
475
|
+
fail(`"${n}" is a texture; sample it with \`tex2D(${n}, uv)\``, callee.pos, n.length)
|
|
476
|
+
}
|
|
477
|
+
fail(unknown(n, scope), callee.pos, n.length)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function arity(e: Extract<Expr, { k: "call" }>, n: string, min: number, max: number): void {
|
|
481
|
+
if (e.args.length >= min && e.args.length <= max) return
|
|
482
|
+
const want = min === max ? `${min}` : `${min} to ${max}`
|
|
483
|
+
fail(`${n} takes ${want} argument${max === 1 ? "" : "s"}, got ${e.args.length}`, e.pos)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** "unknown x" plus the nearest thing that is spelled almost like it. */
|
|
487
|
+
function unknown(n: string, scope: Set<string>): string {
|
|
488
|
+
const near = nearest(n, [
|
|
489
|
+
...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
490
|
+
...funcs.keys(), ...Object.keys(BUILTINS),
|
|
491
|
+
])
|
|
492
|
+
return near === null ? `"${n}" is not declared` : `"${n}" is not declared; did you mean ${near}?`
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { unit, funcs, uniforms, textures, consts }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* The closest candidate within an edit or two, or nothing.
|
|
500
|
+
*
|
|
501
|
+
* Deliberately tight. A suggestion that is merely the least bad of a list is
|
|
502
|
+
* worse than none: it sends an author to rename something that was never the
|
|
503
|
+
* problem.
|
|
504
|
+
*/
|
|
505
|
+
export function nearest(word: string, candidates: Iterable<string>): string | null {
|
|
506
|
+
let best: string | null = null
|
|
507
|
+
let bestScore = Infinity
|
|
508
|
+
const limit = word.length <= 4 ? 1 : 2
|
|
509
|
+
for (const c of candidates) {
|
|
510
|
+
if (c === word) continue
|
|
511
|
+
const d = distance(word, c)
|
|
512
|
+
if (d <= limit && d < bestScore) { best = c; bestScore = d }
|
|
513
|
+
}
|
|
514
|
+
return best
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Optimal string alignment: Levenshtein plus transposition at cost one.
|
|
519
|
+
*
|
|
520
|
+
* Plain Levenshtein charges two for a swapped pair, which is the single most
|
|
521
|
+
* common typo there is, so `wrap` for `warp` fell outside a distance of one and
|
|
522
|
+
* the suggestion that would have answered the question was never offered.
|
|
523
|
+
*/
|
|
524
|
+
function distance(a: string, b: string): number {
|
|
525
|
+
if (Math.abs(a.length - b.length) > 2) return Infinity
|
|
526
|
+
const rows: number[][] = []
|
|
527
|
+
for (let i = 0; i <= a.length; i++) rows.push(new Array<number>(b.length + 1).fill(0))
|
|
528
|
+
for (let i = 0; i <= a.length; i++) rows[i]![0] = i
|
|
529
|
+
for (let j = 0; j <= b.length; j++) rows[0]![j] = j
|
|
530
|
+
for (let i = 1; i <= a.length; i++) {
|
|
531
|
+
for (let j = 1; j <= b.length; j++) {
|
|
532
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
533
|
+
let d = Math.min(rows[i]![j - 1]! + 1, rows[i - 1]![j]! + 1, rows[i - 1]![j - 1]! + cost)
|
|
534
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
535
|
+
d = Math.min(d, rows[i - 2]![j - 2]! + 1)
|
|
536
|
+
}
|
|
537
|
+
rows[i]![j] = d
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return rows[a.length]![b.length]!
|
|
541
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.sl`: the shader language as a file.
|
|
3
|
+
*
|
|
4
|
+
* Phase A of `Specs/SL_TEXT.md`. Source text in, the same `Program` the EDSL
|
|
5
|
+
* records out. Pure TypeScript, no GPU and no filesystem, like everything else
|
|
6
|
+
* in `sl/`, so it runs in the Play editor's worker as happily as in a build.
|
|
7
|
+
*
|
|
8
|
+
* import { parse } from "onejs-sl"
|
|
9
|
+
*
|
|
10
|
+
* const plasma = parse(source, { file: "plasma.sl" })
|
|
11
|
+
* // ^ a Program: encode() it, emit HLSL from it, hash it
|
|
12
|
+
*
|
|
13
|
+
* The parser is not a second authoring surface with its own semantics. It
|
|
14
|
+
* lowers through the EDSL, so a `.sl` file and the `sl.program(...)` an author
|
|
15
|
+
* would otherwise have written produce the same graph and therefore the same
|
|
16
|
+
* hash, the same shader and the same pixels. `lang/parity.test.ts` is that
|
|
17
|
+
* claim, written down.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Program } from "../ir"
|
|
21
|
+
import { check, type Checked } from "./check"
|
|
22
|
+
import { lower } from "./lower"
|
|
23
|
+
import { parseUnit, type ParseOptions } from "./parser"
|
|
24
|
+
import { preludeFunctions } from "./prelude"
|
|
25
|
+
|
|
26
|
+
export type { Checked } from "./check"
|
|
27
|
+
export type { ParseOptions } from "./parser"
|
|
28
|
+
export type {
|
|
29
|
+
Expr, FuncDecl, Param, Stmt, TextureDecl, TypeName, UniformDecl, Unit,
|
|
30
|
+
} from "./ast"
|
|
31
|
+
export { TYPE_WIDTH } from "./ast"
|
|
32
|
+
export { SLParseError } from "./lexer"
|
|
33
|
+
export type { Pos, Token } from "./lexer"
|
|
34
|
+
export { tokenize } from "./lexer"
|
|
35
|
+
export { parseUnit } from "./parser"
|
|
36
|
+
export { PRELUDE_SOURCE, preludeFunctions } from "./prelude"
|
|
37
|
+
export { BUILTINS, NOT_YET } from "./builtins"
|
|
38
|
+
|
|
39
|
+
/** Source text to a recorded program. Throws `SLParseError` with a line and a column. */
|
|
40
|
+
export function parse(source: string, options: ParseOptions = {}): Program {
|
|
41
|
+
return lower(analyze(source, options))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Everything `parse` learns about a file, without building the graph.
|
|
46
|
+
*
|
|
47
|
+
* What the Play editor's completion and diagnostics want (Phase C): the
|
|
48
|
+
* declarations, the functions and their signatures, and the same errors, for a
|
|
49
|
+
* file that may be half typed. Kept here rather than reached for through
|
|
50
|
+
* `parseUnit` + `check` so a caller has one entry point to hold on to.
|
|
51
|
+
*/
|
|
52
|
+
export function analyze(source: string, options: ParseOptions = {}): Checked {
|
|
53
|
+
const unit = parseUnit(source, options)
|
|
54
|
+
return check(unit, preludeFunctions(), { requireMain: options.requireMain })
|
|
55
|
+
}
|