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.
@@ -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
+ }