onejs-sl 0.1.8 → 0.1.10

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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.10
4
+
5
+ Attributes on uniforms, for a host's controls (`Specs/SL_NEXT.md` 1). A program that has none compiles to exactly what it did, and one that has them hashes as it would without.
6
+
7
+ - `[Range]`, `[Toggle]`, `[Enum]`, `[Header]`, `[Label]`, `[Color]` and `[Hide]` before a uniform
8
+ - `UniformDecl` carries `range`, `toggle`, `options`, `header`, `label` and `hide`, never hashed
9
+ - `sl.uniform.float` and the rest take the same control as a last argument
10
+ - `sl.uniform.colour` takes components as well as a hex string
11
+ - `controlProblem` and `CONTROL_FIELDS` are exported from the core
12
+ - `ATTRIBUTE_NAMES` and `ATTRIBUTE_DOCS` are exported from `onejs-sl/tables`
13
+ - `classify` marks an attribute's name and a string
14
+
15
+ ## 0.1.9
16
+
17
+ Two inputs built from the others (`Specs/SL_NEXT.md` 6), and a converter that carries more of a pasted shader over. A program that names neither input compiles to exactly what it did.
18
+
19
+ - `texel` is one pixel in uv, and `centered` is uv with 0 at the centre and x scaled by aspect
20
+ - A local, parameter or uniform named `texel` or `centered` keeps working
21
+ - A local named after a builtin can be assigned to
22
+ - `fromGLSL` reads `u_time` and `u_resolution` as the inputs, and drops their declarations
23
+ - `fromGLSL` turns a mat2 rotation into `rotate()`, and points any other mat2 at it
24
+ - `INPUT_DOCS` say `uv` and `fragCoord` are the centre of the pixel
25
+
3
26
  ## 0.1.8
4
27
 
5
28
  A pasted GLSL shader becomes a `.sl` file (`Specs/SL_NEXT.md` 5). Nothing a program compiles to changes.
package/README.md CHANGED
@@ -345,8 +345,18 @@ reset takes the whole page's device, Unity's included.
345
345
  ## What a program is given
346
346
 
347
347
  `uv`, `fragCoord`, `resolution`, `time` and `aspect`, and the last three are the
348
- **target's**, not the window's. A program is drawn with `Graphics.Blit` into the
349
- element's own render texture, and Unity sets `_ScreenParams` per camera and
348
+ **target's**, not the window's. `uv` and `fragCoord` are the centre of the pixel
349
+ being drawn, so the first column's `fragCoord.x` is 0.5 on both backends.
350
+
351
+ Two more are built from those: `texel`, one pixel in uv (`1 / resolution`), and
352
+ `centered`, uv with 0 at the centre and x scaled by `aspect`, so a circle drawn
353
+ in it stays round. They are recorded only when a program names them, so every
354
+ program that does not compiles to the bytes it always did, and a program with a
355
+ `texel` of its own keeps it: a local, a parameter or a uniform of that name
356
+ shadows the input.
357
+
358
+ Why `resolution` and `aspect` are the target's: a program is drawn with
359
+ `Graphics.Blit` into the element's own render texture, and Unity sets `_ScreenParams` per camera and
350
360
  leaves it alone for a blit: reading it from a 64x256 target answers with the
351
361
  game view's size. Both backends read the same wrong thing, so they agreed with
352
362
  each other and the eject test, which compares them, saw nothing. What saw it was
@@ -357,6 +367,28 @@ sets `_Res` from the target and both backends read that.
357
367
  `fx` had already learned this: `ShaderEffectElement` sets `_Aspect` from the
358
368
  render texture with a comment saying why. The lesson did not travel.
359
369
 
370
+ ## Controls
371
+
372
+ A uniform can say how a host should present it: `range` (a slider, with an
373
+ optional `step`), `toggle`, `options` (a dropdown whose value is the index),
374
+ `header`, `label` and `hide`, all on the `UniformDecl`. A `.sl` file writes them
375
+ as Unity's attributes (`[Range(0, 2)] uniform float warp = 1;`), the EDSL as the
376
+ last argument of `sl.uniform.float` and the rest. They are metadata: the hash
377
+ never reads them, so two programs that differ only in a control share one
378
+ compiled shader, and `toJSON` carries them for a host that stores a program.
379
+
380
+ The generated shader's Properties block does not carry them, and that was
381
+ measured rather than assumed (Unity 6000.5, 2026-09-27). Unity binds a material
382
+ property by the HLSL variable's type, not the Properties type: a `float4`
383
+ variable takes only `SetVector`, a `float` only `SetFloat`. Every uniform is a
384
+ `float4` set with `SetVector`, so a `Range(0, 2)` property over one draws a
385
+ slider whose default and every drag never reach the shader. A `Color` property
386
+ is worse, since `SetVector` on one converts sRGB to linear and the program
387
+ already converts a colour uniform. Carrying them means declaring a scalar
388
+ control as `float`, with a matching Properties line (`[ToggleUI]`, since
389
+ `[Toggle]` also defines a keyword; `[Enum]` as name, value pairs), and the bridge
390
+ setting it with `SetFloat`. `Specs/SL_NEXT.md` 1 keeps that as a later change.
391
+
360
392
  ## Colours
361
393
 
362
394
  A hex colour is sRGB as written, the way CSS reads it, and the target holds
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.1.2",
3
+ "package": "0.1.10",
4
4
  "ir": 2,
5
5
  "target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
6
6
  "size": [
@@ -108,6 +108,14 @@
108
108
  "1.25": [255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255]
109
109
  }
110
110
  },
111
+ "inputs.sl": {
112
+ "hash": "07cd901a",
113
+ "source": "// texel and centered, the inputs built from the others (Specs/SL_NEXT.md 6).\n// A ring that stays round in any output shape, over cells 16 pixels wide.\nfloat4 main() {\n float ring = smoothstep(0.02, 0.0, abs(length(centered) - 0.3));\n float2 cell = frac(uv / (texel * 16.0));\n return float4(ring, cell, 1);\n}\n",
114
+ "samples": {
115
+ "0": [0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,173,171,86,255,139,212,86,255,0,244,86,255,0,110,86,255,207,171,86,255,24,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,253,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,193,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,173,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,139,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,253,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,188,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,207,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,207,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,226,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,24,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,193,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,253,212,237,255,188,244,237,255,207,110,237,255,226,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255],
116
+ "1.25": [0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,173,171,86,255,139,212,86,255,0,244,86,255,0,110,86,255,207,171,86,255,24,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,253,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,193,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,173,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,139,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,253,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,188,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,207,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,207,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,226,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,24,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,193,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,253,212,237,255,188,244,237,255,207,110,237,255,226,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255]
117
+ }
118
+ },
111
119
  "orient.sl": {
112
120
  "hash": "f3b299ff",
113
121
  "source": "// An anchor: red is uv.x, green is uv.y, so a golden's orientation and its\n// linear-to-sRGB store can be checked against arithmetic, not against itself.\nfloat4 main() { return float4(uv.x, uv.y, 0, 1); }\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",
package/src/core.ts CHANGED
@@ -11,9 +11,10 @@ export * as sl from "./sl"
11
11
  export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, Texture } from "./sl"
12
12
  export {
13
13
  TYPE, INPUTS, MAX_TEXTURES, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, inputsUsed,
14
+ CONTROL_FIELDS, controlProblem,
14
15
  } from "./ir"
15
16
  export type {
16
- SLType, InputName, NodeRef, SLNode, Program, UniformDecl, TextureDecl,
17
+ SLType, InputName, NodeRef, SLNode, Program, UniformDecl, UniformControl, TextureDecl,
17
18
  } from "./ir"
18
19
  export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling } from "./ops"
19
20
  export type { SLOpCode } from "./ops"
package/src/ir.ts CHANGED
@@ -42,6 +42,27 @@ export const INPUTS = {
42
42
  } as const
43
43
  export type InputName = keyof typeof INPUTS
44
44
 
45
+ /**
46
+ * Inputs a program can name that are built from the ones above rather than
47
+ * handed over by the host (`Specs/SL_NEXT.md` 6). Each read records the same
48
+ * few nodes a program would if it wrote the expression out, and the builder's
49
+ * hash consing makes every later read the same node. So there is no new
50
+ * instruction, no VM or host change, and a program that never names one holds
51
+ * no extra node, compiling to the bytes it always did.
52
+ *
53
+ * texel 1 / resolution: one pixel, in uv
54
+ * centered (uv - 0.5) * float2(aspect, 1): 0 at the centre, circles stay round
55
+ */
56
+ export const DERIVED_INPUTS = {
57
+ texel: TYPE.VEC2,
58
+ centered: TYPE.VEC2,
59
+ } as const
60
+ export type DerivedInputName = keyof typeof DERIVED_INPUTS
61
+
62
+ /** Every input a program can name: the host's, then the derived ones. */
63
+ export type SourceInputName = InputName | DerivedInputName
64
+ export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INPUTS, ...DERIVED_INPUTS }
65
+
45
66
  /** Index into `Program.nodes`. Always refers backwards. */
46
67
  export type NodeRef = number
47
68
 
@@ -52,7 +73,86 @@ export type SLNode =
52
73
  | { k: "swizzle"; type: SLType; src: NodeRef; chans: number[] }
53
74
  | { k: "call"; type: SLType; op: SLOpCode; args: NodeRef[]; imm?: number[] }
54
75
 
55
- export interface UniformDecl {
76
+ /**
77
+ * How a host presents a uniform: a slider, a checkbox, a dropdown, a heading
78
+ * above it, the name it shows, or no control at all (`Specs/SL_NEXT.md` 1).
79
+ * Written in a `.sl` file as Unity's attributes, `[Range(0, 2)]` and the rest,
80
+ * and in the EDSL as the last argument of `sl.uniform.float`.
81
+ *
82
+ * Metadata for a host, like `colour`: it changes nothing a program computes, so
83
+ * it is never part of the hash, and two programs that differ only in it share
84
+ * one compiled shader.
85
+ */
86
+ export interface UniformControl {
87
+ /** A slider from `min` to `max`, moving by `step` when there is one. A float only. */
88
+ range?: { min: number; max: number; step?: number }
89
+ /** A checkbox. The value is 0 or 1. A float only. */
90
+ toggle?: true
91
+ /** A dropdown of these names. The value is the index of the chosen one. A float only. */
92
+ options?: string[]
93
+ /** A heading shown above this uniform's control, starting a group. */
94
+ header?: string
95
+ /** The name the control shows, in place of the uniform's own. */
96
+ label?: string
97
+ /** No control: the host sets this one from code. */
98
+ hide?: true
99
+ }
100
+
101
+ /** The fields of `UniformControl`, in the order a host reads them. */
102
+ export const CONTROL_FIELDS = ["range", "toggle", "options", "header", "label", "hide"] as const
103
+
104
+ /**
105
+ * What is wrong with giving a uniform of `type` and default `value` this
106
+ * control, or null. Names the field at fault, so the text form can mark the
107
+ * attribute that wrote it. The one check, for `.sl` files and the EDSL alike.
108
+ */
109
+ export function controlProblem(type: SLType, value: readonly number[], c: UniformControl): { field: keyof UniformControl; message: string } | null {
110
+ const kinds = (["range", "toggle", "options"] as const).filter((k) => c[k] !== undefined)
111
+ if (kinds.length > 1) {
112
+ return { field: kinds[1]!, message: `a uniform is one control, so it cannot have both ${ATTRIBUTE_OF[kinds[0]!]} and ${ATTRIBUTE_OF[kinds[1]!]}` }
113
+ }
114
+ const kind = kinds[0]
115
+ if (kind !== undefined && type !== TYPE.FLOAT) {
116
+ return { field: kind, message: `${ATTRIBUTE_OF[kind]} is for a float, and this is a ${widthName(type)}` }
117
+ }
118
+ const v = value[0] ?? 0
119
+ if (c.range !== undefined) {
120
+ const { min, max, step } = c.range
121
+ if (![min, max].every(Number.isFinite) || !(min < max)) {
122
+ return { field: "range", message: `a range runs from a smaller number to a larger one, and this is ${min} to ${max}` }
123
+ }
124
+ if (step !== undefined && !(Number.isFinite(step) && step > 0)) {
125
+ return { field: "range", message: `a range's step is a number above 0, and this is ${step}` }
126
+ }
127
+ if (v < min || v > max) {
128
+ return { field: "range", message: `the default ${v} is outside the range ${min} to ${max}` }
129
+ }
130
+ }
131
+ if (c.toggle === true && v !== 0 && v !== 1) {
132
+ return { field: "toggle", message: `a toggle is 0 or 1, and the default is ${v}` }
133
+ }
134
+ if (c.options !== undefined) {
135
+ if (c.options.length < 2) return { field: "options", message: "an enum lists at least two options" }
136
+ const empty = c.options.findIndex((o) => o.trim() === "")
137
+ if (empty >= 0) return { field: "options", message: `option ${empty + 1} of the enum has no name` }
138
+ const twice = c.options.findIndex((o, i) => c.options!.indexOf(o) !== i)
139
+ if (twice >= 0) return { field: "options", message: `the enum lists "${c.options[twice]}" twice` }
140
+ if (!Number.isInteger(v) || v < 0 || v >= c.options.length) {
141
+ return { field: "options", message: `an enum's value is the index of an option, 0 to ${c.options.length - 1}, and the default is ${v}` }
142
+ }
143
+ }
144
+ for (const f of ["header", "label"] as const) {
145
+ if (c[f] !== undefined && c[f]!.trim() === "") return { field: f, message: `${ATTRIBUTE_OF[f]} needs some text` }
146
+ }
147
+ return null
148
+ }
149
+
150
+ /** How each field is written in a `.sl` file, for the messages above. */
151
+ const ATTRIBUTE_OF: Record<keyof UniformControl, string> = {
152
+ range: "[Range]", toggle: "[Toggle]", options: "[Enum]", header: "[Header]", label: "[Label]", hide: "[Hide]",
153
+ }
154
+
155
+ export interface UniformDecl extends UniformControl {
56
156
  name: string
57
157
  type: SLType
58
158
  /** Default, used when a caller does not supply the uniform. */
@@ -161,7 +261,7 @@ export class Builder {
161
261
  return this.add({ k: "const", type: v.length as SLType, v: v.slice() })
162
262
  }
163
263
 
164
- uniform(name: string, type: SLType, value: number[], colour = false): NodeRef {
264
+ uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
165
265
  const existing = this.uniforms.findIndex((u) => u.name === name)
166
266
  if (existing >= 0) {
167
267
  const u = this.uniforms[existing]
@@ -171,10 +271,27 @@ export class Builder {
171
271
  if ((u.colour === true) !== colour) {
172
272
  throw new SLError(`uniform "${name}" is declared both as a colour and as plain numbers`)
173
273
  }
274
+ // A second read may say nothing about the control, or the same thing again.
275
+ const merged = { ...controlOf(u) }
276
+ for (const f of CONTROL_FIELDS) {
277
+ if (control[f] === undefined) continue
278
+ if (merged[f] !== undefined && JSON.stringify(merged[f]) !== JSON.stringify(control[f])) {
279
+ throw new SLError(`uniform "${name}" is given two different ${f === "options" ? "enums" : f + "s"}`)
280
+ }
281
+ Object.assign(merged, { [f]: control[f] })
282
+ }
283
+ const problem = controlProblem(type, u.value, merged)
284
+ if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
285
+ Object.assign(u, merged)
174
286
  return this.add({ k: "uniform", type, slot: existing })
175
287
  }
288
+ const problem = controlProblem(type, value, control)
289
+ if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
176
290
  const slot = this.uniforms.length
177
- this.uniforms.push(colour ? { name, type, value: value.slice(), colour: true } : { name, type, value: value.slice() })
291
+ const decl: UniformDecl = { name, type, value: value.slice() }
292
+ if (colour) decl.colour = true
293
+ Object.assign(decl, controlOf(control))
294
+ this.uniforms.push(decl)
178
295
  return this.add({ k: "uniform", type, slot })
179
296
  }
180
297
 
@@ -317,6 +434,18 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
317
434
  return fnv1a(parts.join("|"))
318
435
  }
319
436
 
437
+ /** The control fields of `c` that are set, copied, in `CONTROL_FIELDS` order. */
438
+ export function controlOf(c: UniformControl): UniformControl {
439
+ const out: UniformControl = {}
440
+ if (c.range !== undefined) out.range = c.range.step === undefined ? { min: c.range.min, max: c.range.max } : { ...c.range }
441
+ if (c.toggle === true) out.toggle = true
442
+ if (c.options !== undefined) out.options = c.options.slice()
443
+ if (c.header !== undefined) out.header = c.header
444
+ if (c.label !== undefined) out.label = c.label
445
+ if (c.hide === true) out.hide = true
446
+ return out
447
+ }
448
+
320
449
  /** Bumped when the hashing scheme changes, which invalidates generated shaders. */
321
450
  export const SL_HASH_VERSION = 1
322
451
 
@@ -121,7 +121,8 @@ the generated shader's Properties block shows, and every read of `tint` goes
121
121
  through `toLinear`. Without that rule, `#ff8040` written as a literal and
122
122
  `#ff8040` written as a default would be two different colours in one file.
123
123
  A default built out of numbers (`float4(1, 0.5, 0.25, 1)`) is not a colour and
124
- is read unconverted.
124
+ is read unconverted, unless the uniform says `[Color]`, which makes it one
125
+ exactly as a hex default does.
125
126
 
126
127
  **A uniform's default has to be written out.** Numbers, constructors of numbers
127
128
  and colours, and nothing else. It is baked into the program before anything
@@ -166,6 +167,26 @@ browser and correct after an eject. A `.sl` file is held to the real number, 4,
166
167
  reported at the declaration. The EDSL's ceiling is untouched here because
167
168
  lowering it changes recorded behaviour rather than parser behaviour.
168
169
 
170
+ ## Attributes
171
+
172
+ Unity's names, before a uniform, say what its control is: `[Range(0, 2)]`, with
173
+ an optional third number for the step, `[Toggle]`, `[Enum(Soft, Hard, Glow)]`,
174
+ `[Header("Shape")]`, `[Label("Glow colour")]`, `[Color]` and `[Hide]`. They go
175
+ on the uniform's line or the lines above it, in any order. A string exists only
176
+ here, which is the one reason the lexer reads one.
177
+
178
+ Three files share the work. The parser reads the shape (`[Name(args)]`, numbers,
179
+ words and strings); `attributes.ts` says what each one means and refuses what it
180
+ cannot mean, offering Unity's and English spellings as a fix (`Colour` to
181
+ `Color`, `HideInInspector` to `Hide`); `controlProblem` in `ir.ts` refuses what
182
+ does not fit the uniform's type and default, and is the same check the EDSL's
183
+ `sl.uniform.float(name, value, control)` runs. The last one needs the default,
184
+ so its errors come from lowering, marked on the attribute at fault.
185
+
186
+ What they produce is metadata on the uniform (`range`, `toggle`, `options`,
187
+ `header`, `label`, `hide`), which the hash never reads. `[Color]` is the
188
+ exception, because it changes the reads: it is the same as a hex default.
189
+
169
190
  ## Errors
170
191
 
171
192
  Every one carries file, line, column, offset and length, and `text`, the
@@ -204,14 +225,15 @@ same place, over every program one token short of a real one.
204
225
  ## For an editor
205
226
 
206
227
  `classify(source)` is every token with its class (keyword, type, builtin,
207
- input, prelude, number, hex, comment, punct, member, ident, or invalid), built
228
+ input, prelude, number, hex, string, comment, punct, member, attribute, ident,
229
+ or invalid), built
208
230
  on the parser's own scanner and word lists, so the highlighting is the parser's.
209
231
  It never throws, since it runs on every keystroke. It classifies by spelling,
210
232
  not scope: a local named `circle` still reads as a builtin where it is used.
211
233
 
212
234
  `SL_KEYWORDS` and `SL_TYPES` (`words.ts`) are the lists the parser reads, and a
213
- name may be neither. `onejs-sl/tables` carries them with a one line description
214
- of every builtin, input and prelude function; a prelude function's is the
235
+ name may be neither. `onejs-sl/tables` carries them, and `ATTRIBUTE_NAMES`, with
236
+ a one line description of every builtin, input, attribute and prelude function; a prelude function's is the
215
237
  comment above it in `prelude-source.ts`.
216
238
 
217
239
  ## From GLSL
@@ -234,7 +256,18 @@ where they were, so the result reads as the shader it came from. What it does:
234
256
  same. Shadertoy's `uv = fragCoord / iResolution.xy` line is dropped, since
235
257
  that is the input `uv`.
236
258
  - `iResolution`, `iMouse`, `iChannel0` to `3` and `uniform sampler2D` become
237
- `resolution`, a `mouse` uniform and declared textures. `#define NAME value`
259
+ `resolution`, a `mouse` uniform and declared textures. The Book of Shaders'
260
+ `u_time` and `u_resolution`, glslsandbox's `time` and `resolution`, and
261
+ Shadertoy's own inputs where a tool declares them are the inputs: their
262
+ `uniform` declarations go, so the shader does not arrive frozen.
263
+ - A mat2 rotation becomes `rotate(p, angle)`: a function or macro returning
264
+ `mat2(c, -s, s, c)` or `mat2(c, s, -s, c)` of one angle, or the same written
265
+ in place, wherever a vector is multiplied by it (`p *= rot(a)`,
266
+ `rot(a) * p`, `p * rot(a)`). The sign of the angle comes from the matrix and
267
+ from which side the vector is on, since GLSL fills a matrix by columns. The
268
+ function or macro goes once every use is converted; otherwise it stays, and
269
+ the notes point at `rotate()`. A file with a `rotate` of its own is left
270
+ alone, since the calls would reach it. `#define NAME value`
238
271
  becomes a const; any other directive is kept as a comment, with a note.
239
272
  - A name the language already means something by gets a `_` suffix, and an
240
273
  `int` becomes a float (a for-loop counter stays an int), each with a note.
package/src/lang/ast.ts CHANGED
@@ -63,11 +63,28 @@ export interface FuncDecl {
63
63
  prelude: boolean
64
64
  }
65
65
 
66
+ /** An argument to an attribute: a number, a quoted string, or a bare word. */
67
+ export type AttributeArg =
68
+ | { k: "num"; value: number; pos: Pos; length: number }
69
+ | { k: "str"; text: string; pos: Pos; length: number }
70
+ | { k: "ident"; name: string; pos: Pos; length: number }
71
+
72
+ /** `[Range(0, 2)]` before a uniform: Unity's spelling of what its control is. */
73
+ export interface Attribute {
74
+ name: string
75
+ args: AttributeArg[]
76
+ /** Where the name starts, and how long it is, for a marker. */
77
+ pos: Pos
78
+ length: number
79
+ }
80
+
66
81
  export interface UniformDecl {
67
82
  name: string
68
83
  type: TypeName
69
84
  /** Null when the declaration gave no default; the slot then starts at zero. */
70
85
  init: Expr | null
86
+ /** In the order written. What each means is decided by the checker, not the parser. */
87
+ attrs: Attribute[]
71
88
  pos: Pos
72
89
  }
73
90
 
@@ -0,0 +1,116 @@
1
+ /**
2
+ * A uniform's attributes, read into what its control is (`Specs/SL_NEXT.md` 1).
3
+ *
4
+ * Unity's names, so a Unity developer already knows them: `[Range(0, 2)]`,
5
+ * `[Toggle]`, `[Enum(Soft, Hard, Glow)]`, `[Header("Shape")]`,
6
+ * `[Label("Glow colour")]`, `[Color]` and `[Hide]`. The parser reads their
7
+ * shape only; this says what each one means and what is wrong with it, for the
8
+ * checker to report and for lowering to build. What depends on the default
9
+ * (a default outside its range, say) is `controlProblem`'s, which the EDSL
10
+ * shares.
11
+ */
12
+
13
+ import type { UniformControl } from "../ir"
14
+ import { TYPE_WIDTH, type Attribute, type AttributeArg, type UniformDecl } from "./ast"
15
+ import type { Pos, SLFix } from "./lexer"
16
+
17
+ export interface AttributeProblem {
18
+ message: string
19
+ pos: Pos
20
+ length: number
21
+ fix?: SLFix
22
+ }
23
+
24
+ export interface ReadAttributes {
25
+ /** Every attribute that could be read. One that could not is left out, and is in `problems`. */
26
+ control: UniformControl
27
+ /** `[Color]`: the value is a colour as written, the same as a hex default. */
28
+ colour: boolean
29
+ /** The attribute that wrote each field, to mark when the default does not fit it. */
30
+ from: Partial<Record<keyof UniformControl, Attribute>>
31
+ problems: AttributeProblem[]
32
+ }
33
+
34
+ /** Every attribute, in the order the docs list them. */
35
+ export const ATTRIBUTE_NAMES = ["Range", "Toggle", "Enum", "Header", "Label", "Color", "Hide"] as const
36
+ export type AttributeName = (typeof ATTRIBUTE_NAMES)[number]
37
+
38
+ /** Spellings from Unity, from other engines, or from English, that mean one of ours. */
39
+ const ALIASES: Record<string, AttributeName> = {
40
+ Colour: "Color", HideInInspector: "Hide", ToggleUI: "Toggle", KeywordEnum: "Enum", IntRange: "Range",
41
+ }
42
+
43
+ const LIST = "Range, Toggle, Enum, Header, Label, Color and Hide"
44
+
45
+ export function readAttributes(u: UniformDecl): ReadAttributes {
46
+ const out: ReadAttributes = { control: {}, colour: false, from: {}, problems: [] }
47
+ const seen = new Set<string>()
48
+ const problem = (at: Attribute | AttributeArg, message: string, fix?: SLFix) => {
49
+ out.problems.push({ message, pos: at.pos, length: at.length, ...(fix === undefined ? {} : { fix }) })
50
+ }
51
+ const text = (a: AttributeArg): string | null => a.k === "str" ? a.text : a.k === "ident" ? a.name : null
52
+ const none = (a: Attribute) => {
53
+ if (a.args.length === 0) return true
54
+ problem(a.args[0]!, `[${a.name}] takes no arguments`)
55
+ return false
56
+ }
57
+ const one = (a: Attribute, what: string): string | null => {
58
+ const t = a.args.length === 1 ? text(a.args[0]!) : null
59
+ if (t === null) problem(a.args[0] ?? a, `[${a.name}] takes ${what} in quotes, as in [${a.name}("${a.name === "Header" ? "Shape" : "Glow colour"}")]`)
60
+ return t
61
+ }
62
+
63
+ for (const a of u.attrs) {
64
+ const known = (ATTRIBUTE_NAMES as readonly string[]).includes(a.name)
65
+ if (!known) {
66
+ const to = ALIASES[a.name] ?? ATTRIBUTE_NAMES.find((n) => n.toLowerCase() === a.name.toLowerCase())
67
+ const hint = a.name === "IntRange" ? "; a whole number slider is [Range(min, max, 1)]" : ""
68
+ problem(a, `"${a.name}" is not an attribute; the attributes are ${LIST}${to === undefined ? "" : `. Did you mean ${to}?`}${hint}`,
69
+ to === undefined || a.name === "IntRange" ? undefined : { title: `Replace ${a.name} with ${to}`, replacement: to })
70
+ continue
71
+ }
72
+ if (seen.has(a.name)) { problem(a, `this uniform already has [${a.name}]`); continue }
73
+ seen.add(a.name)
74
+ switch (a.name) {
75
+ case "Range": {
76
+ const n = a.args.map((x) => x.k === "num" ? x.value : null)
77
+ if ((n.length !== 2 && n.length !== 3) || n.includes(null)) {
78
+ problem(a, "[Range] takes a minimum, a maximum and an optional step, all numbers: [Range(0, 1)] or [Range(1, 16, 1)]")
79
+ break
80
+ }
81
+ out.control.range = n.length === 3 ? { min: n[0]!, max: n[1]!, step: n[2]! } : { min: n[0]!, max: n[1]! }
82
+ out.from.range = a
83
+ break
84
+ }
85
+ case "Toggle":
86
+ if (none(a)) { out.control.toggle = true; out.from.toggle = a }
87
+ break
88
+ case "Enum": {
89
+ const num = a.args.find((x) => x.k === "num")
90
+ if (num !== undefined) {
91
+ problem(num, "[Enum] lists names only, as in [Enum(Soft, Hard, Glow)]; the value is the chosen name's index, from 0")
92
+ break
93
+ }
94
+ out.control.options = a.args.map((x) => text(x)!)
95
+ out.from.options = a
96
+ break
97
+ }
98
+ case "Header":
99
+ case "Label": {
100
+ const t = one(a, a.name === "Header" ? "a heading" : "a name")
101
+ const f = a.name === "Header" ? "header" : "label"
102
+ if (t !== null) { out.control[f] = t; out.from[f] = a }
103
+ break
104
+ }
105
+ case "Color":
106
+ if (!none(a)) break
107
+ if (TYPE_WIDTH[u.type] < 3) problem(a, `[Color] is for a float3 or a float4, and this is a ${u.type}`)
108
+ else out.colour = true
109
+ break
110
+ case "Hide":
111
+ if (none(a)) { out.control.hide = true; out.from.hide = a }
112
+ break
113
+ }
114
+ }
115
+ return out
116
+ }
package/src/lang/check.ts CHANGED
@@ -28,15 +28,22 @@
28
28
  * is refused with the reason.
29
29
  */
30
30
 
31
- import { INPUTS } from "../ir"
31
+ import { DERIVED_INPUTS, INPUTS } from "../ir"
32
32
  import { VM_TEXTURES, VM_UNIFORMS } from "../ops"
33
33
  import { SL_GLSL_HINT } from "../ops"
34
34
  import { BUILTINS, NOT_YET } from "./builtins"
35
35
  import { SL_SDF_SHAPES } from "../shapes"
36
36
  import type { Expr, FuncDecl, Stmt, Unit } from "./ast"
37
+ import { readAttributes } from "./attributes"
37
38
  import { SLParseError, type Pos, type SLFix } from "./lexer"
38
39
 
39
40
  const INPUT_NAMES = new Set(Object.keys(INPUTS))
41
+ /**
42
+ * Inputs built from the others (`texel`, `centered`). A declaration may take
43
+ * one's name, as it may a builtin's, because programs wrote their own `texel`
44
+ * long before the language had one, and that must keep compiling.
45
+ */
46
+ const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
40
47
 
41
48
  /**
42
49
  * The GLSL spellings whose HLSL name means the same wherever it is written, so
@@ -133,13 +140,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
133
140
  if (uniforms.has(n)) return "a uniform"
134
141
  if (textures.has(n)) return "a texture"
135
142
  if (consts.has(n)) return "a const"
143
+ if (DERIVED_NAMES.has(n)) return "a derived input"
136
144
  return null
137
145
  }
138
146
 
139
147
  /** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
140
148
  const valueClash = (n: string): string | null => {
141
149
  const why = taken(n)
142
- if (why !== null && why !== "a builtin") return why
150
+ if (why !== null && why !== "a builtin" && why !== "a derived input") return why
143
151
  const fn = funcs.get(n)
144
152
  return fn !== undefined && !fn.prelude ? "a function" : null
145
153
  }
@@ -149,6 +157,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
149
157
  const clash = valueClash(u.name)
150
158
  if (clash !== null) fail(`"${u.name}" already names ${clash}`, u.pos, u.name.length)
151
159
  })
160
+ for (const p of readAttributes(u).problems) attempt(() => fail(p.message, p.pos, p.length, p.fix))
152
161
  if (!uniforms.has(u.name)) uniforms.set(u.name, u)
153
162
  }
154
163
  attempt(() => {
@@ -220,7 +229,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
220
229
  if (main.params.length > 0) {
221
230
  fail(
222
231
  "main takes no parameters: what a program is given are the free identifiers uv, " +
223
- "fragCoord, resolution, time and aspect",
232
+ "fragCoord, resolution, time, aspect, texel and centered",
224
233
  main.params[0]!.pos,
225
234
  )
226
235
  }
@@ -365,7 +374,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
365
374
  }
366
375
  if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
367
376
  const n = target.name
368
- const why = taken(n)
377
+ // A local shadows what it is named after, a builtin or a
378
+ // derived input, so it is assignable like any other local.
379
+ const why = scope.has(n) ? null : taken(n)
369
380
  if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
370
381
  if (counters.has(n)) {
371
382
  fail(
@@ -423,7 +434,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
423
434
  case "hex":
424
435
  return
425
436
  case "ident": {
426
- if (scope.has(e.name) || INPUT_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
437
+ if (scope.has(e.name) || INPUT_NAMES.has(e.name) || DERIVED_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
427
438
  if (textures.has(e.name)) {
428
439
  fail(
429
440
  `"${e.name}" is a texture, and a texture is only ever the first argument of ` +
@@ -581,7 +592,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
581
592
  /** Refuses a name nothing declares, offering the nearest one spelled almost like it. */
582
593
  function unknown(n: string, scope: Set<string>, pos: Pos): never {
583
594
  const near = nearest(n, [
584
- ...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
595
+ ...scope, ...INPUT_NAMES, ...DERIVED_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
585
596
  ...funcs.keys(), ...Object.keys(BUILTINS),
586
597
  ])
587
598
  if (near === null) fail(`"${n}" is not declared`, pos, n.length)