onejs-sl 0.1.2 → 0.1.4

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ Four things that were errors now compile, the most common mistakes in cold runs of authors writing from the docs alone (`Specs/SL_NEXT.md` 2). Every program that compiled before compiles to exactly what it did.
6
+
7
+ - A swizzle can be assigned to: `p.x = 1;`, `c.rgb *= 0.5;`
8
+ - A single number fills a vector in a declaration, an assignment or a uniform default: `float3 c = 0.5;`
9
+ - A float4 goes into a float3 by dropping its fourth component: `float3 c = #ff8040;`
10
+ - A local or a uniform may take a builtin's or a prelude function's name
11
+
12
+ ## 0.1.3
13
+
14
+ `onejs-sl/tables` names every parameter, so an editor can offer `lerp(x, y, s)` rather than an argument count. Nothing a program compiles to changes.
15
+
16
+ - `BUILTIN_PARAMS` names every builtin's parameters
17
+ - `SL_SDF_PARAM_NAMES` names every shape's parameters
18
+ - `LIB_SIGNATURES` gives every library function's parameter names and types
19
+
3
20
  ## 0.1.2
4
21
 
5
22
  The helper library has one source, `lib/*.hlsl`, translated at build time into GLSL ES, WGSL and the shared subset and copied into OneJS as its `.cginc` files. The web draws exactly what it drew; the Unity shader's text changes where Metal needed it to, and draws the same.
package/README.md CHANGED
@@ -37,7 +37,7 @@ only what it calls.
37
37
  |---|---|
38
38
  | `onejs-sl` | `parse` and `analyze`, the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, length) |
39
39
  | `onejs-sl/core` | the same without the parser: what a game needs at run time |
40
- | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`: what completion and highlighting read |
40
+ | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`: what completion and highlighting read. `BUILTIN_PARAMS`, `SL_SDF_PARAM_NAMES` and `LIB_SIGNATURES` name every parameter, so an editor can show `lerp(x, y, s)` |
41
41
  | `onejs-sl/limits` | `vmFit(program)`: whether the VM runs it, and why not, without encoding |
42
42
  | `onejs-sl/vm` | `encode`, `SL_WIRE_VERSION` |
43
43
  | `onejs-sl/emit/hlsl-body` | `emitBody`: a program as a function body for a host's own frame; `emitLibrary`: the library functions it calls |
@@ -92,6 +92,8 @@ by `npm run lib`, and nobody edits the copies:
92
92
  the shape table read from `sl_sdfDistance`'s switch), and the text of each
93
93
  function as GLSL ES (`glsl.ts`), WGSL (`wgsl.ts`) and the shared subset
94
94
  (`hlsl.ts`). The web emitters and `emitLibrary` take only what a program calls.
95
+ `names.ts` holds each function's parameter names, which only
96
+ `onejs-sl/tables` reads.
95
97
 
96
98
  `lib/translate.ts` does the printing, at build time only. It reads a subset of
97
99
  HLSL, listed at its top (functions, locals, `if`, bounded `for`, `switch`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL, GLSL ES or a VM buffer",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -127,11 +127,25 @@ is read unconverted.
127
127
  and colours, and nothing else. It is baked into the program before anything
128
128
  runs, so it cannot name a const, which could itself name a uniform.
129
129
 
130
- **Shadowing is refused.** A local may not take the name of an input, a uniform,
131
- a texture, a const or a builtin. Allowing it would make "is this a texture?"
132
- depend on where the question is asked, for no gain in a language whose
133
- functions are six lines long. A file function may shadow a *prelude* function,
134
- which is the sanctioned way to replace one.
130
+ **Shadowing is refused between values.** A local may not take the name of an
131
+ input, a uniform, a texture or a const. Allowing it would make "is this a
132
+ texture?" depend on where the question is asked, for no gain in a language whose
133
+ functions are six lines long. A value *may* take a builtin's or a prelude
134
+ function's name (`float circle`, `uniform float turbulence`), which hides that
135
+ function wherever the value is visible, and a call there says so. A file
136
+ function may shadow a *prelude* function, which is the sanctioned way to replace
137
+ one.
138
+
139
+ **Declarations and assignments convert two ways, and only two.** A single
140
+ number fills every component (`float3 c = 0.5;`, and a uniform default such as
141
+ `uniform float2 offset = 0;`), and a float4 goes into a float3 by dropping its
142
+ fourth component, which is how a colour becomes an rgb. Every other width change
143
+ is refused with the swizzle that fixes it.
144
+
145
+ **A swizzle can be assigned to.** `p.x *= aspect;` rebuilds `p` from the
146
+ components it names and keeps the rest, so it is the same program as
147
+ `p = float2(p.x * aspect, p.y);`. A swizzle that names a component twice, or
148
+ one the local does not have, is refused.
135
149
 
136
150
  ## Not yet spellable
137
151
 
package/src/lang/check.ts CHANGED
@@ -14,11 +14,18 @@
14
14
  * whether a name exists, whether a body is shaped like a body, and whether the
15
15
  * program fits the budgets before anything is built.
16
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.
17
+ * SHADOWING IS REFUSED, between values. HLSL would let a local called `time`
18
+ * hide the input, or a parameter called `tint` hide a uniform. Allowing it would
19
+ * mean every later question about a name ("is this a texture?") depends on where
20
+ * it is asked from, for no expressive gain in a language whose functions are
21
+ * half a dozen lines long.
22
+ *
23
+ * A value MAY take the name of a builtin or a prelude function: `float circle`,
24
+ * `uniform float turbulence`. Those are the names an author reaches for first,
25
+ * and refusing them was the most common error left once the others were fixed
26
+ * (`Specs/SL_NEXT.md` 2d). Calls and values never meet in one position, so the
27
+ * only question left is a call to the name where the value is visible, and that
28
+ * is refused with the reason.
22
29
  */
23
30
 
24
31
  import { INPUTS } from "../ir"
@@ -84,8 +91,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
84
91
  *
85
92
  * Functions are NOT in here, because a file function shadowing a prelude
86
93
  * 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.
94
+ * (`Specs/SL_TEXT.md` 3.8). Every other kind of declaration checks
95
+ * `valueClash`, which adds the file's own functions.
89
96
  */
90
97
  const taken = (n: string): string | null => {
91
98
  if (INPUT_NAMES.has(n)) return "an input"
@@ -98,14 +105,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
98
105
  return null
99
106
  }
100
107
 
101
- const asFunc = (n: string): string | null => {
108
+ /** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
109
+ const valueClash = (n: string): string | null => {
110
+ const why = taken(n)
111
+ if (why !== null && why !== "a builtin") return why
102
112
  const fn = funcs.get(n)
103
- if (fn === undefined) return null
104
- return fn.prelude ? "a prelude function" : "a function"
113
+ return fn !== undefined && !fn.prelude ? "a function" : null
105
114
  }
106
115
 
107
116
  for (const u of unit.uniforms) {
108
- const clash = taken(u.name) ?? asFunc(u.name)
117
+ const clash = valueClash(u.name)
109
118
  if (clash !== null) fail(`"${u.name}" already names ${clash}`, u.pos, u.name.length)
110
119
  uniforms.set(u.name, u)
111
120
  }
@@ -121,7 +130,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
121
130
  }
122
131
 
123
132
  for (const t of unit.textures) {
124
- const clash = taken(t.name) ?? asFunc(t.name)
133
+ const clash = valueClash(t.name)
125
134
  if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
126
135
  textures.set(t.name, t)
127
136
  }
@@ -136,7 +145,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
136
145
  }
137
146
 
138
147
  for (const c of unit.consts) {
139
- const clash = taken(c.name) ?? asFunc(c.name)
148
+ const clash = valueClash(c.name)
140
149
  if (clash !== null) fail(`"${c.name}" already names ${clash}`, c.pos, c.name.length)
141
150
  consts.set(c.name, c)
142
151
  }
@@ -251,7 +260,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
251
260
  function checkFunction(fn: FuncDecl): void {
252
261
  const params = new Set<string>()
253
262
  for (const p of fn.params) {
254
- const clash = taken(p.name) ?? asFunc(p.name)
263
+ const clash = valueClash(p.name)
255
264
  if (clash !== null) fail(`"${p.name}" already names ${clash}`, p.pos, p.name.length)
256
265
  if (params.has(p.name)) fail(`${fn.name} already has a parameter called "${p.name}"`, p.pos)
257
266
  params.add(p.name)
@@ -275,7 +284,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
275
284
  switch (s.k) {
276
285
  case "var":
277
286
  case "const": {
278
- const clash = taken(s.name) ?? asFunc(s.name)
287
+ const clash = valueClash(s.name)
279
288
  if (clash !== null) fail(`"${s.name}" already names ${clash}`, s.pos, s.name.length)
280
289
  if (scope.has(s.name)) fail(`"${s.name}" is already declared in this body`, s.pos)
281
290
  checkExpr(fn, s.init, scope)
@@ -283,26 +292,32 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
283
292
  break
284
293
  }
285
294
  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
- )
295
+ // `p.x = 1` and `c.rgb *= 0.5` write the components they name
296
+ // and keep the rest; lowering rebuilds the local from both.
297
+ let target = s.target
298
+ if (target.k === "member") {
299
+ const sw = target.name
300
+ if (!/^([xyzw]{1,4}|[rgba]{1,4})$/.test(sw)) {
301
+ fail(`${sw} is not a swizzle that can be assigned to; name components with xyzw or rgba`, target.pos, sw.length)
302
+ }
303
+ if (new Set(sw).size !== sw.length) {
304
+ fail(`${sw} names a component twice, so assigning to it would write one component two ways`, target.pos, sw.length)
305
+ }
306
+ target = target.obj
292
307
  }
293
- if (s.target.k !== "ident") fail("only a local can be assigned to", s.target.pos)
294
- const n = s.target.name
308
+ if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
309
+ const n = target.name
295
310
  const why = taken(n)
296
- if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, s.target.pos, n.length)
311
+ if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
297
312
  if (counters.has(n)) {
298
313
  fail(
299
314
  `"${n}" is a loop counter. The loop unrolls and substitutes it as a ` +
300
315
  `number, so assigning to it would change nothing. Use another local`,
301
- s.target.pos, n.length,
316
+ target.pos, n.length,
302
317
  )
303
318
  }
304
319
  if (!scope.has(n)) {
305
- fail(unknown(n, scope), s.target.pos, n.length)
320
+ fail(unknown(n, scope), target.pos, n.length)
306
321
  }
307
322
  checkExpr(fn, s.value, scope)
308
323
  break
@@ -313,7 +328,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
313
328
  checkBody(fn, s.else, new Set(scope), false)
314
329
  break
315
330
  case "for": {
316
- const clash = taken(s.counter) ?? asFunc(s.counter)
331
+ const clash = valueClash(s.counter)
317
332
  if (clash !== null) fail(`"${s.counter}" already names ${clash}`, s.pos, s.counter.length)
318
333
  if (scope.has(s.counter)) fail(`"${s.counter}" is already declared in this body`, s.pos)
319
334
  checkExpr(fn, s.from, scope)
@@ -435,6 +450,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
435
450
  )
436
451
  }
437
452
 
453
+ // A value that took a builtin's or a prelude function's name hides it
454
+ // wherever the value is visible.
455
+ const value = scope.has(n) ? "a local" : uniforms.has(n) ? "a uniform" : consts.has(n) ? "a const" : null
456
+ if (value !== null) {
457
+ fail(`"${n}" is ${value} here, so it cannot be called. Rename it to call ${n}(...)`, callee.pos, n.length)
458
+ }
459
+ if (textures.has(n)) {
460
+ fail(`"${n}" is a texture; sample it with \`tex2D(${n}, uv)\``, callee.pos, n.length)
461
+ }
462
+
438
463
  const builtin = BUILTINS[n]
439
464
  if (builtin !== undefined) {
440
465
  if (n === "tex2D") {
@@ -471,9 +496,6 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
471
496
  if (SL_GLSL_HINT[n] !== undefined) {
472
497
  fail(`${n} is GLSL; this is HLSL, so write ${SL_GLSL_HINT[n]}`, callee.pos, n.length)
473
498
  }
474
- if (textures.has(n)) {
475
- fail(`"${n}" is a texture; sample it with \`tex2D(${n}, uv)\``, callee.pos, n.length)
476
- }
477
499
  fail(unknown(n, scope), callee.pos, n.length)
478
500
  }
479
501
 
package/src/lang/lower.ts CHANGED
@@ -158,7 +158,7 @@ export function lower(checked: Checked): Program {
158
158
  for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
159
159
 
160
160
  for (const c of unit.consts) {
161
- const v = lowerExpr(c.init, global)
161
+ const v = fit(lowerExpr(c.init, global), TYPE_WIDTH[c.type], c.pos)
162
162
  assertWidth(v, TYPE_WIDTH[c.type], c.name, c.type, c.pos)
163
163
  global.declare(c.name, { width: TYPE_WIDTH[c.type], value: v })
164
164
  }
@@ -227,6 +227,8 @@ export function lower(checked: Checked): Program {
227
227
  init.pos,
228
228
  )
229
229
  }
230
+ if (c.length === 1 && width > 1) return { components: new Array<number>(width).fill(c[0]!), colour: false }
231
+ if (c.length === 4 && width === 3) return { components: c.slice(0, 3), colour: false }
230
232
  if (c.length !== width) {
231
233
  fail(
232
234
  `this default has ${c.length} component${c.length === 1 ? "" : "s"} and the uniform is ` +
@@ -284,19 +286,25 @@ export function lower(checked: Checked): Program {
284
286
  switch (s.k) {
285
287
  case "var":
286
288
  case "const": {
287
- const v = lowerExpr(s.init, scope)
289
+ const v = fit(lowerExpr(s.init, scope), TYPE_WIDTH[s.type], s.pos)
288
290
  assertWidth(v, TYPE_WIDTH[s.type], s.name, s.type, s.pos)
289
291
  scope.declare(s.name, { width: TYPE_WIDTH[s.type], value: v })
290
292
  break
291
293
  }
292
294
  case "assign": {
293
- const name = (s.target as Extract<Expr, { k: "ident" }>).name
295
+ // The checker allowed exactly two shapes: a local, or a
296
+ // swizzle of one with no component named twice.
297
+ const member = s.target.k === "member" ? s.target : null
298
+ const name = ((member?.obj ?? s.target) as Extract<Expr, { k: "ident" }>).name
294
299
  const b = scope.lookup(name)!
295
300
  let v = lowerExpr(s.value, scope)
296
301
  if (s.op !== "=") {
297
302
  const op = s.op[0] as ArithOp
298
- v = at(s.pos, () => arithmetic(op, b.value, v, s.pos))
303
+ const old = member === null ? b.value : lowerExpr(member, scope)
304
+ v = at(s.pos, () => arithmetic(op, old, v, s.pos))
299
305
  }
306
+ if (member !== null) v = writeComponents(b, member, v, s.pos)
307
+ else v = fit(v, b.width, s.pos)
300
308
  assertWidth(v, b.width, name, widthType(b.width), s.pos)
301
309
  b.value = v
302
310
  break
@@ -381,13 +389,52 @@ export function lower(checked: Checked): Program {
381
389
  return typeof a !== "number" && typeof b !== "number" && a.ref === b.ref && a.width === b.width
382
390
  }
383
391
 
392
+ /**
393
+ * The two conversions a declaration or an assignment makes for the author
394
+ * (`Specs/SL_NEXT.md` 2b and 2c): a single number fills every component, as
395
+ * `float3(0.5)` would, and a float4 goes into a float3 by dropping its
396
+ * fourth, which is how a colour (a hex, a ramp, a texture sample) becomes
397
+ * an rgb. Every other width change is left for `assertWidth` to refuse.
398
+ */
399
+ function fit(v: LV, want: SLType, pos: Pos): LV {
400
+ const got = typeof v === "number" ? 1 : v.width
401
+ if (got === 1 && want > 1) return at(pos, () => compose(want, new Array<LV>(want).fill(v)))
402
+ if (got === 4 && want === 3) return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
403
+ return v
404
+ }
405
+
406
+ /**
407
+ * `p.x = v`, `c.rgb *= k`: the local rebuilt from the components the
408
+ * swizzle names, taken from `v`, and the rest kept from what it held. A
409
+ * single number fills every named component.
410
+ */
411
+ function writeComponents(b: Binding, member: Extract<Expr, { k: "member" }>, v: LV, pos: Pos): LV {
412
+ const letters = member.name.replace(/[rgba]/g, (c) => "xyzw"["rgba".indexOf(c)]!)
413
+ const names = "xyzw".slice(0, b.width)
414
+ for (const c of letters) {
415
+ if (!names.includes(c)) {
416
+ fail(`${member.name} writes a component a ${widthType(b.width)} does not have`, member.pos)
417
+ }
418
+ }
419
+ const got = typeof v === "number" ? 1 : v.width
420
+ if (got !== 1 && got !== letters.length) {
421
+ fail(`${member.name} is ${letters.length} components and this is a ${widthType(got as SLType)}`, pos)
422
+ }
423
+ const parts: LV[] = [...names].map((c) => {
424
+ const j = letters.indexOf(c)
425
+ if (j < 0) return b.width === 1 ? b.value : at(pos, () => asVal(b.value).swz(c) as unknown as Val)
426
+ return got === 1 ? v : at(pos, () => asVal(v).swz("xyzw"[j]!) as unknown as Val)
427
+ })
428
+ return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
429
+ }
430
+
384
431
  function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
385
432
  const got = typeof v === "number" ? 1 : v.width
386
433
  if (got === want) return
387
434
  // A declaration is an assertion, not an input to inference
388
435
  // (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
389
436
  // than quietly promoting one to the other.
390
- const hint = got === 1 ? ` Wrap it in ${type}(...) to broadcast the value across ${want} components.` : ""
437
+ const hint = got > want ? ` Take the components you want with a swizzle, as in .${"xyzw".slice(0, want)}.` : ""
391
438
  fail(`${name} is declared ${type} and this is a ${widthType(got as SLType)}.${hint}`, pos)
392
439
  }
393
440
 
@@ -0,0 +1,77 @@
1
+ // GENERATED by lib/generate.ts from lib/*.hlsl. Do not edit: change the
2
+ // .hlsl and run `npm run lib`. src/lib/lib.test.ts fails when this is stale.
3
+
4
+ /** Parameter names as the source writes them, by `LIB_FUNCTIONS` index. `onejs-sl/tables` reads them. */
5
+ export const LIB_PARAM_NAMES: ReadonlyArray<readonly string[]> = [
6
+ ["x", "y"],
7
+ ["a", "b"],
8
+ ["v"],
9
+ ["p", "r"],
10
+ ["p", "b", "r"],
11
+ ["p", "b"],
12
+ ["p", "a", "b", "th"],
13
+ ["p", "a", "b"],
14
+ ["p", "b"],
15
+ ["p", "r1", "r2", "he"],
16
+ ["p", "wi", "he", "sk"],
17
+ ["p", "r"],
18
+ ["p", "q"],
19
+ ["p", "p0", "p1", "p2"],
20
+ ["p", "r1", "r2", "h"],
21
+ ["p", "r"],
22
+ ["p", "r"],
23
+ ["p", "r"],
24
+ ["p", "r"],
25
+ ["p", "r", "rf"],
26
+ ["p", "r", "n", "m"],
27
+ ["p", "c", "r"],
28
+ ["p", "r", "h"],
29
+ ["p", "sc", "ra", "rb"],
30
+ ["p", "n", "r", "th"],
31
+ ["p", "c", "r", "w"],
32
+ ["p", "r", "d"],
33
+ ["p", "a", "b", "w"],
34
+ ["p", "d", "ra", "rb"],
35
+ ["p", "h"],
36
+ ["p", "he", "ra", "rb", "bu"],
37
+ ["p"],
38
+ ["p", "b", "r"],
39
+ ["p", "w", "r"],
40
+ ["p", "ab"],
41
+ ["pos", "k"],
42
+ ["pos", "wi", "he"],
43
+ ["pos", "A", "B", "C"],
44
+ ["pos", "he"],
45
+ ["p", "wh"],
46
+ ["p", "wh", "n"],
47
+ ["p"],
48
+ ["p", "k", "he"],
49
+ ["p"],
50
+ ["p", "tb", "ra"],
51
+ ["p", "seed"],
52
+ ["p", "seed"],
53
+ ["p", "seed", "octaves", "lacunarity", "gain"],
54
+ ["p", "seed", "octaves"],
55
+ ["x"],
56
+ ["x"],
57
+ ["x"],
58
+ ["v"],
59
+ ["p", "seed"],
60
+ ["p", "seed", "octaves", "lacunarity", "gain"],
61
+ ["p", "seed", "octaves", "lacunarity", "gain"],
62
+ ["p", "seed", "octaves", "lacunarity", "gain"],
63
+ ["kind", "p", "seed", "octaves", "lacunarity", "gain"],
64
+ ["p"],
65
+ ["p"],
66
+ ["p"],
67
+ ["p", "octaves", "kind"],
68
+ ["c"],
69
+ ["id", "p", "a", "b"],
70
+ ["p"],
71
+ ["c"],
72
+ ["c"],
73
+ ["c"],
74
+ ["c"],
75
+ ["c"],
76
+ ["c"],
77
+ ]
package/src/params.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Parameter names, for an editor's completions and signature help: so it can
3
+ * offer `lerp(x, y, s)` rather than "lerp takes 3 arguments".
4
+ *
5
+ * Metadata only. Nothing that checks or emits a program reads this file, and
6
+ * only `onejs-sl/tables` imports it, so it cannot change what any program
7
+ * compiles to. Each table is built by a call marked pure, so a bundle that
8
+ * imports `onejs-sl/tables` for BUILTINS alone (the Play editor's parser) drops
9
+ * all of it.
10
+ *
11
+ * The intrinsics carry the names the HLSL documentation gives them. Everything
12
+ * the library implements is named as `lib/*.hlsl` writes it, read by the
13
+ * translator into `src/lib/names.ts` when `npm run lib` runs, so there is no
14
+ * second list of those to drift: a builtin that lowers to a library helper takes
15
+ * that helper's names, and a shape's parameters are named after its function's
16
+ * parameters, in the slots `sl_sdfDistance` passes them in.
17
+ */
18
+
19
+ import { BUILTINS } from "./lang/builtins"
20
+ import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
21
+ import { LIB_PARAM_NAMES } from "./lib/names"
22
+ import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
23
+
24
+ /** One function of the helper library, as its source declares it. */
25
+ export interface LibSignature {
26
+ name: string
27
+ params: ReadonlyArray<{ name: string; type: string }>
28
+ ret: string
29
+ }
30
+
31
+ /** Every function in the helper library, by `LIB_FUNCTIONS` index, overloads included. */
32
+ export const LIB_SIGNATURES: readonly LibSignature[] = /* @__PURE__ */ LIB_FUNCTIONS.map((f, i) => ({
33
+ name: f.name,
34
+ params: f.params.map((type, j) => ({ name: LIB_PARAM_NAMES[i]![j]!, type })),
35
+ ret: f.ret,
36
+ }))
37
+
38
+ /** A library function's parameter names, the same for every overload of it. */
39
+ function namesOf(fn: string): readonly string[] {
40
+ const found = LIB_SIGNATURES.filter((s) => s.name === fn).map((s) => s.params.map((p) => p.name).join(", "))
41
+ if (found.length === 0) throw new Error(`the library has no ${fn}`)
42
+ if (new Set(found).size > 1) throw new Error(`${fn}'s overloads name their parameters differently: ${found.join(" / ")}`)
43
+ return found[0]!.split(", ")
44
+ }
45
+
46
+ /** The HLSL documentation's names. */
47
+ const INTRINSIC: Record<string, readonly string[]> = {
48
+ sin: ["x"], cos: ["x"], tan: ["x"], asin: ["x"], acos: ["x"], exp: ["x"], log: ["x"], sqrt: ["x"],
49
+ sign: ["x"], ceil: ["x"], round: ["x"], abs: ["x"], floor: ["x"], frac: ["x"], saturate: ["x"],
50
+ rcp: ["x"], normalize: ["x"], length: ["x"],
51
+ pow: ["x", "y"], min: ["x", "y"], max: ["x", "y"], clamp: ["x", "min", "max"], atan2: ["y", "x"],
52
+ distance: ["x", "y"], dot: ["x", "y"], cross: ["x", "y"], reflect: ["i", "n"],
53
+ lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
54
+ tex2D: ["s", "t"],
55
+ }
56
+
57
+ /**
58
+ * The library helper each builtin lowers to (`emitBody` calls it, and a test
59
+ * checks that it does), and how many of its parameters an author writes: fbm's
60
+ * third, the kind, is the opcode's own.
61
+ */
62
+ export const LIBRARY: Readonly<Record<string, [string, number]>> = {
63
+ toLinear: ["sl_toLinear", 1], hsv2rgb: ["sl_hsv2rgb", 1], luminance: ["sl_luminance", 1],
64
+ noise: ["sl_valueNoise", 1], simplex: ["sl_simplex", 1], voronoi: ["sl_voronoi", 1],
65
+ fbm: ["sl_fbm", 2], turbulence: ["sl_fbm", 2], ridged: ["sl_fbm", 2],
66
+ // The point; the shape's own parameters are SL_SDF_PARAM_NAMES.
67
+ sdf: ["sl_sdfDistance", 0],
68
+ }
69
+
70
+ /** The two the language defines itself, named as the EDSL's `sl.remap` and `sl.ramp` name theirs. */
71
+ const MACRO: Record<string, readonly string[]> = {
72
+ remap: ["v", "fromMin", "fromMax", "toMin", "toMax"],
73
+ // At least two stops, and as many more as `max` allows.
74
+ ramp: ["t", "stop", "stop..."],
75
+ }
76
+
77
+ /**
78
+ * Each builtin's parameter names, one per argument up to its `max`; those past
79
+ * its `min` are optional. A name ending in `...` repeats up to `max`. `sdf` is
80
+ * written `sdf.circle(p, r)`: its entry names the point, and the shape's own
81
+ * parameters are `SL_SDF_PARAM_NAMES[shape]`.
82
+ */
83
+ export const BUILTIN_PARAMS: Readonly<Record<string, readonly string[]>> = /* @__PURE__ */ Object.fromEntries(
84
+ Object.keys(BUILTINS).map((name) => {
85
+ const lib = LIBRARY[name]
86
+ if (name === "sdf") return [name, [namesOf(lib![0])[1]!]]
87
+ const names = INTRINSIC[name] ?? MACRO[name] ?? (lib && namesOf(lib[0]).slice(0, lib[1]))
88
+ if (names === undefined) throw new Error(`builtin ${name} has no parameter names in params.ts`)
89
+ return [name, names]
90
+ }),
91
+ )
92
+
93
+ /**
94
+ * Each shape's parameters, in the order `sdf.<shape>(p, ...)` takes them, named
95
+ * after its function's: a vector parameter's components as `b.x`, `b.y`.
96
+ */
97
+ export const SL_SDF_PARAM_NAMES: Readonly<Record<SlSdfKind, readonly string[]>> = /* @__PURE__ */ shapeNames()
98
+
99
+ function shapeNames(): Record<SlSdfKind, readonly string[]> {
100
+ const out = {} as Record<SlSdfKind, readonly string[]>
101
+ for (const shape of Object.keys(SL_SDF_SHAPES) as SlSdfKind[]) {
102
+ const call = SDF_CALLS[SL_SDF_SHAPES[shape]]!
103
+ // The function's first parameter is the point, which `sl_sdfDistance` passes itself.
104
+ const fn = namesOf(call.fn).slice(1)
105
+ const slots = new Array<string>(SL_SDF_PARAMS[shape])
106
+ call.args.forEach((arg, i) => {
107
+ const at = "int" in arg ? [arg.int] : arg
108
+ at.forEach((slot, j) => { slots[slot] = at.length === 1 ? fn[i]! : `${fn[i]}.${"xyzw"[j]}` })
109
+ })
110
+ out[shape] = slots
111
+ }
112
+ return out
113
+ }
package/src/tables.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * `onejs-sl/tables`: what completion and highlighting read.
3
3
  *
4
4
  * The same tables the checker and the emitters use, so an editor offers
5
- * exactly the builtins that compile, with the arities they take.
5
+ * exactly the builtins that compile, with the arities they take and the names
6
+ * of their parameters (`params.ts`), and the helper library's signatures.
6
7
  */
7
8
  export { BUILTINS, NOT_YET } from "./lang/builtins"
8
9
  export type { Builtin } from "./lang/builtins"
@@ -12,4 +13,6 @@ export {
12
13
  export type { SLSurface } from "./ops"
13
14
  export { INPUTS } from "./ir"
14
15
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
16
+ export { BUILTIN_PARAMS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
17
+ export type { LibSignature } from "./params"
15
18
  export type { SlSdfKind } from "./shapes"