onejs-sl 0.1.7 → 0.1.9

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9
4
+
5
+ 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.
6
+
7
+ - `texel` is one pixel in uv, and `centered` is uv with 0 at the centre and x scaled by aspect
8
+ - A local, parameter or uniform named `texel` or `centered` keeps working
9
+ - A local named after a builtin can be assigned to
10
+ - `fromGLSL` reads `u_time` and `u_resolution` as the inputs, and drops their declarations
11
+ - `fromGLSL` turns a mat2 rotation into `rotate()`, and points any other mat2 at it
12
+ - `INPUT_DOCS` say `uv` and `fragCoord` are the centre of the pixel
13
+
14
+ ## 0.1.8
15
+
16
+ A pasted GLSL shader becomes a `.sl` file (`Specs/SL_NEXT.md` 5). Nothing a program compiles to changes.
17
+
18
+ - `fromGLSL` converts a Shadertoy or WebGL fragment shader, with a note on everything it could not carry over
19
+ - `INPUT_DOCS` describe the output and the clock, not an element and an effect
20
+ - A body never closed is not also reported as a missing main
21
+
3
22
  ## 0.1.7
4
23
 
5
24
  `diagnose` finds a mistake inside a refused call's arguments and on both sides of an operator, found by the Play editor's first run in a real browser. Nothing a program compiles to changes.
package/README.md CHANGED
@@ -35,7 +35,7 @@ only what it calls.
35
35
 
36
36
  | Entry | Contents |
37
37
  |---|---|
38
- | `onejs-sl` | `parse` and `analyze`, `diagnose` (every error in a file, not just the first), `classify` (every token with its class, comments kept, never throws), the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, offset, length, the bare `text`, and a `fix` where one is certain) |
38
+ | `onejs-sl` | `parse` and `analyze`, `diagnose` (every error in a file, not just the first), `classify` (every token with its class, comments kept, never throws), `fromGLSL` (a pasted Shadertoy or WebGL shader as a `.sl` file, with notes on what did not carry over), the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, offset, length, the bare `text`, and a `fix` where one is certain) |
39
39
  | `onejs-sl/core` | the same without the parser: what a game needs at run time |
40
40
  | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`, `SL_KEYWORDS`, `SL_TYPES`, `TYPE_WIDTH`, `PRELUDE_NAMES`: 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)`, and `BUILTIN_DOCS`, `INPUT_DOCS` and `PRELUDE_DOCS` give each a line for its tooltip |
41
41
  | `onejs-sl/limits` | `vmFit(program)`: whether the VM runs it, and why not, without encoding |
@@ -166,9 +166,11 @@ Before the tag:
166
166
  unless the release says otherwise.
167
167
  3. Run PlaySite's checks in the container, where the Play editor bundles this
168
168
  package: `node scripts/gen-sl-parser.mjs`, then `npx vitest run`, in
169
- `PlaySite/`. It is a private repo, so no workflow here can.
169
+ `PlaySite/`. It is a private repo, so no workflow here can. When `diagnose`
170
+ or an error's fix changed, also run `node Tools/editor-sl-smoke/editor-sl-smoke.mjs`
171
+ from the container root, which drives the Play editor in real Monaco.
170
172
  4. Push `main` and wait for CI, whose `consumers` job runs onejs-unity's
171
- typecheck and tests against the commit (`consumers.yml`). `publish.yml`
173
+ and onejs-play's typecheck and tests against the commit (`consumers.yml`). `publish.yml`
172
174
  runs the same job and publishes nothing if it fails. When a consumer has to
173
175
  follow a change, land it here, fix the consumer, then re-run and tag.
174
176
 
@@ -343,8 +345,18 @@ reset takes the whole page's device, Unity's included.
343
345
  ## What a program is given
344
346
 
345
347
  `uv`, `fragCoord`, `resolution`, `time` and `aspect`, and the last three are the
346
- **target's**, not the window's. A program is drawn with `Graphics.Blit` into the
347
- 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
348
360
  leaves it alone for a blit: reading it from a 64x256 target answers with the
349
361
  game view's size. Both backends read the same wrong thing, so they agreed with
350
362
  each other and the eject test, which compares them, saw nothing. What saw it was
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.1.2",
3
+ "package": "0.1.9",
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.7",
3
+ "version": "0.1.9",
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/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * HLSL, WGSL or GLSL ES) is a separate entry, so a host bundles only what it
7
7
  * calls:
8
8
  *
9
- * onejs-sl this: parse, diagnose, classify, the TypeScript form, the IR, its JSON
9
+ * onejs-sl this: parse, diagnose, classify, fromGLSL, the TypeScript form, the IR, its JSON
10
10
  * onejs-sl/core the same without the parser, for a game at run time
11
11
  * onejs-sl/tables builtins, opcodes, inputs, shapes: what an editor reads
12
12
  * onejs-sl/limits whether a program fits the VM, without encoding it
@@ -24,3 +24,5 @@ export {
24
24
  export type {
25
25
  Checked, Expr, FuncDecl, ParseOptions, Pos, SLClassifiedToken, SLFix, SLTokenClass, Stmt, Token, Unit,
26
26
  } from "./lang"
27
+ export { fromGLSL } from "./lang/glsl"
28
+ export type { FromGLSL } from "./lang/glsl"
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
 
@@ -195,7 +195,9 @@ statement or declaration, the checker and the lowering move to the next one,
195
195
  and a local whose line was refused is still declared, as a zero of its type, so
196
196
  its uses are not reported again. A later stage runs only when the earlier ones
197
197
  found nothing: names are not checked in a file that cannot be read, since those
198
- errors would be about the hole. `diagnose.test.ts` holds it to the property that
198
+ errors would be about the hole. For the same reason a file with a syntax error
199
+ is not also told it declares no main: an unclosed body reads to the end of the
200
+ file, main with it, and one mistake should be one error. `diagnose.test.ts` holds it to the property that
199
201
  matters, that whatever `parse` refuses a file with, `diagnose` reports at the
200
202
  same place, over every program one token short of a real one.
201
203
 
@@ -212,6 +214,48 @@ name may be neither. `onejs-sl/tables` carries them with a one line description
212
214
  of every builtin, input and prelude function; a prelude function's is the
213
215
  comment above it in `prelude-source.ts`.
214
216
 
217
+ ## From GLSL
218
+
219
+ `fromGLSL(glsl)` (`glsl.ts`) turns a pasted Shadertoy or WebGL fragment shader
220
+ into a `.sl` file, and returns it with `notes`, one line each on what it could
221
+ not carry over or changed the meaning of, and `errors`, which is `diagnose` on
222
+ the result. It never throws, since a paste arrives a piece at a time.
223
+
224
+ It rewrites tokens and keeps everything else: comments, layout and names stay
225
+ where they were, so the result reads as the shader it came from. What it does:
226
+
227
+ - The spellings that mean the same (`vec3`, `mix`, `fract`, `texture`,
228
+ `iTime`, `gl_FragCoord`) become the HLSL ones. So do the calls that need an
229
+ expression: `mod` floors (`a - b * floor(a / b)`, never `%`, which
230
+ truncates), and `atan`, `inversesqrt`, `radians`, `degrees`, `exp2` and
231
+ `log2` become what they compute.
232
+ - `mainImage` becomes `float4 main()`, with its colour parameter as a local it
233
+ returns; `void main()` writing `gl_FragColor` or a WebGL2 `out vec4` does the
234
+ same. Shadertoy's `uv = fragCoord / iResolution.xy` line is dropped, since
235
+ that is the input `uv`.
236
+ - `iResolution`, `iMouse`, `iChannel0` to `3` and `uniform sampler2D` become
237
+ `resolution`, a `mouse` uniform and declared textures. The Book of Shaders'
238
+ `u_time` and `u_resolution`, glslsandbox's `time` and `resolution`, and
239
+ Shadertoy's own inputs where a tool declares them are the inputs: their
240
+ `uniform` declarations go, so the shader does not arrive frozen.
241
+ - A mat2 rotation becomes `rotate(p, angle)`: a function or macro returning
242
+ `mat2(c, -s, s, c)` or `mat2(c, s, -s, c)` of one angle, or the same written
243
+ in place, wherever a vector is multiplied by it (`p *= rot(a)`,
244
+ `rot(a) * p`, `p * rot(a)`). The sign of the angle comes from the matrix and
245
+ from which side the vector is on, since GLSL fills a matrix by columns. The
246
+ function or macro goes once every use is converted; otherwise it stays, and
247
+ the notes point at `rotate()`. A file with a `rotate` of its own is left
248
+ alone, since the calls would reach it. `#define NAME value`
249
+ becomes a const; any other directive is kept as a comment, with a note.
250
+ - A name the language already means something by gets a `_` suffix, and an
251
+ `int` becomes a float (a for-loop counter stays an int), each with a note.
252
+ - What has no counterpart (`iFrame`, `textureLod`, `dFdx`, a function returning
253
+ nothing) is left in place with a note, for `errors` to point at.
254
+
255
+ It is not a GLSL parser, and is not meant to be: a shader it cannot follow comes
256
+ out with errors on the lines to look at, which is where a person pasting one
257
+ starts anyway.
258
+
215
259
  ## See also
216
260
 
217
261
  - `Specs/SL_TEXT.md`, the whole design, including the four decisions taken
package/src/lang/check.ts CHANGED
@@ -28,7 +28,7 @@
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"
@@ -37,6 +37,12 @@ import type { Expr, FuncDecl, Stmt, Unit } from "./ast"
37
37
  import { SLParseError, type Pos, type SLFix } from "./lexer"
38
38
 
39
39
  const INPUT_NAMES = new Set(Object.keys(INPUTS))
40
+ /**
41
+ * Inputs built from the others (`texel`, `centered`). A declaration may take
42
+ * one's name, as it may a builtin's, because programs wrote their own `texel`
43
+ * long before the language had one, and that must keep compiling.
44
+ */
45
+ const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
40
46
 
41
47
  /**
42
48
  * The GLSL spellings whose HLSL name means the same wherever it is written, so
@@ -133,13 +139,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
133
139
  if (uniforms.has(n)) return "a uniform"
134
140
  if (textures.has(n)) return "a texture"
135
141
  if (consts.has(n)) return "a const"
142
+ if (DERIVED_NAMES.has(n)) return "a derived input"
136
143
  return null
137
144
  }
138
145
 
139
146
  /** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
140
147
  const valueClash = (n: string): string | null => {
141
148
  const why = taken(n)
142
- if (why !== null && why !== "a builtin") return why
149
+ if (why !== null && why !== "a builtin" && why !== "a derived input") return why
143
150
  const fn = funcs.get(n)
144
151
  return fn !== undefined && !fn.prelude ? "a function" : null
145
152
  }
@@ -220,7 +227,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
220
227
  if (main.params.length > 0) {
221
228
  fail(
222
229
  "main takes no parameters: what a program is given are the free identifiers uv, " +
223
- "fragCoord, resolution, time and aspect",
230
+ "fragCoord, resolution, time, aspect, texel and centered",
224
231
  main.params[0]!.pos,
225
232
  )
226
233
  }
@@ -365,7 +372,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
365
372
  }
366
373
  if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
367
374
  const n = target.name
368
- const why = taken(n)
375
+ // A local shadows what it is named after, a builtin or a
376
+ // derived input, so it is assignable like any other local.
377
+ const why = scope.has(n) ? null : taken(n)
369
378
  if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
370
379
  if (counters.has(n)) {
371
380
  fail(
@@ -423,7 +432,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
423
432
  case "hex":
424
433
  return
425
434
  case "ident": {
426
- if (scope.has(e.name) || INPUT_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
435
+ 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
436
  if (textures.has(e.name)) {
428
437
  fail(
429
438
  `"${e.name}" is a texture, and a texture is only ever the first argument of ` +
@@ -581,7 +590,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
581
590
  /** Refuses a name nothing declares, offering the nearest one spelled almost like it. */
582
591
  function unknown(n: string, scope: Set<string>, pos: Pos): never {
583
592
  const near = nearest(n, [
584
- ...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
593
+ ...scope, ...INPUT_NAMES, ...DERIVED_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
585
594
  ...funcs.keys(), ...Object.keys(BUILTINS),
586
595
  ])
587
596
  if (near === null) fail(`"${n}" is not declared`, pos, n.length)
@@ -13,7 +13,7 @@
13
13
  * wants that should ask it.
14
14
  */
15
15
 
16
- import { INPUTS } from "../ir"
16
+ import { SOURCE_INPUTS } from "../ir"
17
17
  import { BUILTINS } from "./builtins"
18
18
  import { lex } from "./lexer"
19
19
  import { PRELUDE_NAMES } from "./prelude-source"
@@ -40,7 +40,7 @@ export interface SLClassifiedToken {
40
40
  length: number
41
41
  }
42
42
 
43
- const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(INPUTS))
43
+ const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(SOURCE_INPUTS))
44
44
 
45
45
  /** The tokens of `source`, comments included, in order, each with its class. */
46
46
  export function classify(source: string): SLClassifiedToken[] {
@@ -0,0 +1,696 @@
1
+ /**
2
+ * A pasted GLSL fragment shader, Shadertoy's `mainImage` included, rewritten as
3
+ * a `.sl` file.
4
+ *
5
+ * Decision 1 kept as `Specs/SL_NEXT.md` 5 A has it: the language accepts one
6
+ * spelling, and porting from the other is one command rather than a second
7
+ * column in every table. So this is a converter, not an alias list, and what
8
+ * it cannot carry over it says rather than guesses.
9
+ *
10
+ * A rewrite of tokens, not of a tree. Comments, layout and the author's own
11
+ * names survive, because the result is a file somebody goes on editing. The
12
+ * rewrites that need to know a call's arguments (`mod`, `atan`, `inversesqrt`)
13
+ * find them by matching brackets.
14
+ *
15
+ * What comes back is the text, what the conversion changed that the author
16
+ * should know (`notes`), and what still does not compile (`errors`, from
17
+ * `diagnose`, at their places in the new text).
18
+ */
19
+
20
+ import { INPUTS } from "../ir"
21
+ import { BUILTINS } from "./builtins"
22
+ import { diagnose } from "./index"
23
+ import type { SLParseError } from "./lexer"
24
+ import { SL_KEYWORDS, SL_TYPES } from "./words"
25
+
26
+ export interface FromGLSL {
27
+ /** The `.sl` file. */
28
+ source: string
29
+ /** What the conversion changed or dropped that the author should know, each naming the line it was on. */
30
+ notes: string[]
31
+ /** What still does not compile, at its place in `source`. Empty when it compiles. */
32
+ errors: SLParseError[]
33
+ }
34
+
35
+ type Kind = "ws" | "comment" | "ident" | "number" | "punct" | "directive"
36
+ interface Tok { kind: Kind; text: string; line: number }
37
+
38
+ /** Every character in exactly one token, so joining the texts gives the input back. */
39
+ function tokenize(src: string): Tok[] {
40
+ const out: Tok[] = []
41
+ let i = 0
42
+ let line = 1
43
+ const push = (kind: Kind, end: number) => {
44
+ const text = src.slice(i, end)
45
+ out.push({ kind, text, line })
46
+ for (const c of text) if (c === "\n") line++
47
+ i = end
48
+ }
49
+ const ident = /[A-Za-z_]\w*/y
50
+ const number = /(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?[uUfF]?/y
51
+ while (i < src.length) {
52
+ const c = src[i]!
53
+ if (/\s/.test(c)) { let j = i; while (j < src.length && /\s/.test(src[j]!)) j++; push("ws", j); continue }
54
+ if (src.startsWith("//", i)) { const j = src.indexOf("\n", i); push("comment", j < 0 ? src.length : j); continue }
55
+ if (src.startsWith("/*", i)) { const j = src.indexOf("*/", i + 2); push("comment", j < 0 ? src.length : j + 2); continue }
56
+ if (c === "#") {
57
+ // To the end of the line, a backslash continuing it.
58
+ let j = i
59
+ while (j < src.length && src[j] !== "\n") j += src[j] === "\\" && src[j + 1] === "\n" ? 2 : 1
60
+ push("directive", j)
61
+ continue
62
+ }
63
+ ident.lastIndex = i
64
+ if (ident.test(src)) { push("ident", ident.lastIndex); continue }
65
+ number.lastIndex = i
66
+ if (/[\d.]/.test(c) && number.test(src) && number.lastIndex > i + (c === "." ? 1 : 0)) { push("number", number.lastIndex); continue }
67
+ push("punct", i + 1)
68
+ }
69
+ return out
70
+ }
71
+
72
+ const GLSL_TYPES = new Set(["float", "int", "uint", "bool", "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4",
73
+ "uvec2", "uvec3", "uvec4", "bvec2", "bvec3", "bvec4", "mat2", "mat3", "mat4", "void", "sampler2D"])
74
+
75
+ /** A straight rename, the meaning unchanged. */
76
+ const RENAME: Record<string, string> = {
77
+ vec2: "float2", vec3: "float3", vec4: "float4", mix: "lerp", fract: "frac",
78
+ texture: "tex2D", texture2D: "tex2D", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
79
+ }
80
+
81
+ /** A rename that changes what the value can hold, so it is said. */
82
+ const NARROW: Record<string, [string, string]> = {
83
+ ivec2: ["float2", "an ivec2 is a float2 here, so dividing one no longer truncates"],
84
+ ivec3: ["float3", "an ivec3 is a float3 here, so dividing one no longer truncates"],
85
+ ivec4: ["float4", "an ivec4 is a float4 here, so dividing one no longer truncates"],
86
+ uvec2: ["float2", "a uvec2 is a float2 here"], uvec3: ["float3", "a uvec3 is a float3 here"],
87
+ uvec4: ["float4", "a uvec4 is a float4 here"],
88
+ bvec2: ["float2", "a bvec2 is a float2 of 0s and 1s here"], bvec3: ["float3", "a bvec3 is a float3 of 0s and 1s here"],
89
+ bvec4: ["float4", "a bvec4 is a float4 of 0s and 1s here"],
90
+ bool: ["float", "a bool is a float that is 0 or 1 here; a comparison already is one"],
91
+ int: ["float", "an int is a float here, so dividing one no longer truncates; wrap it in floor() where that mattered"],
92
+ uint: ["float", "a uint is a float here"],
93
+ }
94
+
95
+ /** Shadertoy's inputs with no counterpart yet, and why. */
96
+ const MISSING: Record<string, string> = {
97
+ iFrame: "there is no frame counter yet",
98
+ iTimeDelta: "there is no delta time yet",
99
+ iFrameRate: "there is no frame rate",
100
+ iDate: "there is no date",
101
+ iChannelTime: "a texture has no time of its own",
102
+ iChannelResolution: "a texture's size cannot be read yet",
103
+ iSampleRate: "there is no audio",
104
+ textureLod: "tex2Dlod is not implemented yet; tex2D samples with the texture's own filtering",
105
+ texelFetch: "a texture cannot be read without filtering yet",
106
+ textureSize: "a texture's size cannot be read yet",
107
+ dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
108
+ }
109
+
110
+ /**
111
+ * Uniforms other hosts declare for what is an input here: the Book of Shaders'
112
+ * and glslCanvas's `u_time`, glslsandbox's `time`, and Shadertoy's own when a
113
+ * tool declares them. The declaration goes and every use reads the input, so a
114
+ * pasted shader does not arrive frozen on a uniform nothing sets.
115
+ */
116
+ const HOST_UNIFORMS: Record<string, string> = {
117
+ u_time: "time", u_resolution: "resolution", time: "time", resolution: "resolution",
118
+ iTime: "time", iGlobalTime: "time", iResolution: "resolution", iMouse: "mouse",
119
+ }
120
+
121
+ const QUALIFIERS = new Set(["highp", "mediump", "lowp"])
122
+ /** Words that take a bracket without being called. */
123
+ const CONTROL = new Set(["if", "for", "while", "switch", "return"])
124
+
125
+ /**
126
+ * Whether a `mat2(...)`, given its four arguments as words, is a rotation, and
127
+ * which way: +1 for `mat2(c, s, -s, c)`, the matrix R(a), and -1 for
128
+ * `mat2(c, -s, s, c)`, which is R(-a) since GLSL fills a matrix by columns.
129
+ * `cos` and `sin` are the words that stand for the cosine and sine.
130
+ */
131
+ function rotationSign(matArgs: string[], cos: string[], sin: string[]): 1 | -1 | null {
132
+ const kind = (w: string) => cos.includes(w) ? "C" : sin.includes(w) ? "S"
133
+ : w.startsWith("- ") && sin.includes(w.slice(2)) ? "-S" : "?"
134
+ const k = matArgs.map(kind).join(",")
135
+ return k === "C,S,-S,C" ? 1 : k === "C,-S,S,C" ? -1 : null
136
+ }
137
+
138
+ /** Words split at the commas outside any bracket. */
139
+ function splitTop(words: string[]): string[] {
140
+ const out: string[] = []
141
+ let depth = 0
142
+ let cur: string[] = []
143
+ for (const w of words) {
144
+ if (w === "(" || w === "[") depth++
145
+ else if (w === ")" || w === "]") depth--
146
+ if (w === "," && depth === 0) { out.push(cur.join(" ")); cur = [] } else cur.push(w)
147
+ }
148
+ out.push(cur.join(" "))
149
+ return out
150
+ }
151
+
152
+ /** The arguments of `mat2 ( ... )` as words, when `words` is exactly that, in any number of brackets. */
153
+ function mat2Args(words: string[]): string[] | null {
154
+ while (words[0] === "(" && words[words.length - 1] === ")") words = words.slice(1, -1)
155
+ if (words[0] !== "mat2" || words[1] !== "(" || words[words.length - 1] !== ")") return null
156
+ const inner = words.slice(2, -1)
157
+ let depth = 0
158
+ for (const w of inner) { if (w === "(") depth++; else if (w === ")" && --depth < 0) return null }
159
+ return splitTop(inner)
160
+ }
161
+
162
+ /**
163
+ * The sign of a function body that returns a rotation of its parameter:
164
+ * `return mat2(cos(a), -sin(a), sin(a), cos(a));`, or the same after
165
+ * `float c = cos(a), s = sin(a);`. Null for anything else.
166
+ */
167
+ function rotationBody(words: string[], param: string): 1 | -1 | null {
168
+ const cos = [`cos ( ${param} )`]
169
+ const sin = [`sin ( ${param} )`]
170
+ const statements = words.join(" ").split(/\s*;\s*/).filter((x) => x !== "")
171
+ const last = statements.pop()
172
+ const binding = new RegExp(`^(\\w+) = (cos|sin) \\( ${param} \\)$`)
173
+ for (const st of statements) {
174
+ const parts = splitTop(st.replace(/^float /, "").split(" "))
175
+ for (const part of parts) {
176
+ const m = binding.exec(part)
177
+ if (m === null) return null
178
+ if (m[2] === "cos") cos.push(m[1]!)
179
+ else sin.push(m[1]!)
180
+ }
181
+ }
182
+ if (last === undefined || !last.startsWith("return ")) return null
183
+ const a = mat2Args(last.slice("return ".length).split(" "))
184
+ return a === null || a.length !== 4 ? null : rotationSign(a, cos, sin)
185
+ }
186
+
187
+ /** Names a pasted value or function cannot keep, because the language already means something by them. */
188
+ function taken(name: string, isFunction: boolean): boolean {
189
+ if (name in INPUTS || (SL_KEYWORDS as readonly string[]).includes(name) || (SL_TYPES as readonly string[]).includes(name)) return true
190
+ // A value may take a builtin's name; a function may not.
191
+ return isFunction && BUILTINS[name] !== undefined
192
+ }
193
+
194
+ /** `fromGLSL(source)`: a GLSL fragment shader as a `.sl` file, with what changed and what still does not compile. */
195
+ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLSL {
196
+ const toks = tokenize(glsl.replace(/\r\n/g, "\n"))
197
+ const notes: string[] = []
198
+ const note = (line: number, text: string) => {
199
+ const n = `line ${line}: ${text}`
200
+ if (!notes.includes(n)) notes.push(n)
201
+ }
202
+ const said = new Set<string>()
203
+ const noteOnce = (key: string, line: number, text: string) => { if (!said.has(key)) { said.add(key); note(line, text) } }
204
+
205
+ // ---------- token plumbing ----------
206
+
207
+ const skip = (i: number) => { while (i < toks.length && (toks[i]!.kind === "ws" || toks[i]!.kind === "comment")) i++; return i }
208
+ const back = (i: number) => { while (i >= 0 && (toks[i]!.kind === "ws" || toks[i]!.kind === "comment")) i--; return i }
209
+ const is = (i: number, text: string) => i >= 0 && i < toks.length && toks[i]!.text === text && toks[i]!.kind !== "comment"
210
+ /** The index of the bracket closing the one at `open`, or the end. */
211
+ const closing = (open: number): number => {
212
+ const [o, c] = toks[open]!.text === "(" ? ["(", ")"] : toks[open]!.text === "{" ? ["{", "}"] : ["[", "]"]
213
+ let depth = 0
214
+ for (let i = open; i < toks.length; i++) {
215
+ if (toks[i]!.kind !== "punct") continue
216
+ if (toks[i]!.text === o) depth++
217
+ else if (toks[i]!.text === c && --depth === 0) return i
218
+ }
219
+ return toks.length
220
+ }
221
+ /** The index of the bracket opening the one at `close`, or -1. */
222
+ const opening = (close: number): number => {
223
+ const [o, c] = toks[close]!.text === ")" ? ["(", ")"] : ["[", "]"]
224
+ let depth = 0
225
+ for (let i = close; i >= 0; i--) {
226
+ if (toks[i]!.kind !== "punct") continue
227
+ if (toks[i]!.text === c) depth++
228
+ else if (toks[i]!.text === o && --depth === 0) return i
229
+ }
230
+ return -1
231
+ }
232
+ /** Words for tokens `from` to `to`: no layout, no comments, one space between. */
233
+ const words = (from: number, to: number) =>
234
+ toks.slice(from, to).filter((t) => t.kind !== "ws" && t.kind !== "comment").map((t) => t.text)
235
+ /**
236
+ * The last token of the operand starting at `i`: a name, a number, a call
237
+ * or a bracket, then any members and indexes. -1 when it is none of those.
238
+ */
239
+ const operandAfter = (i: number): number => {
240
+ const t = toks[i]
241
+ let j: number
242
+ if (t === undefined) return -1
243
+ if (t.kind === "number") j = i
244
+ else if (is(i, "(")) j = closing(i)
245
+ else if (t.kind === "ident") { const n = skip(i + 1); j = is(n, "(") ? closing(n) : i }
246
+ else return -1
247
+ for (;;) {
248
+ if (j >= toks.length) return -1
249
+ const n = skip(j + 1)
250
+ if (is(n, ".") && toks[skip(n + 1)]?.kind === "ident") j = skip(n + 1)
251
+ else if (is(n, "[")) j = closing(n)
252
+ else return j
253
+ }
254
+ }
255
+ /** The first token of the operand ending at `j`, the same shapes read backwards. -1 when there is none. */
256
+ const operandBefore = (j: number): number => {
257
+ let i = j
258
+ for (;;) {
259
+ if (is(i, "]")) { const o = opening(i); if (o < 0) return -1; i = back(o - 1); continue }
260
+ let start: number
261
+ if (is(i, ")")) {
262
+ const o = opening(i)
263
+ if (o < 0) return -1
264
+ const p = back(o - 1)
265
+ start = toks[p]?.kind === "ident" && !CONTROL.has(toks[p]!.text) ? p : o
266
+ } else if (toks[i]?.kind === "ident" || toks[i]?.kind === "number") {
267
+ start = i
268
+ } else {
269
+ return -1
270
+ }
271
+ const p = back(start - 1)
272
+ if (!is(p, ".")) return start
273
+ i = back(p - 1)
274
+ }
275
+ }
276
+ /** A call's arguments, as token ranges, for the call whose "(" is at `open`. */
277
+ const args = (open: number, close: number): Array<[number, number]> => {
278
+ const out: Array<[number, number]> = []
279
+ let depth = 0
280
+ let start = open + 1
281
+ for (let i = open + 1; i < close; i++) {
282
+ const t = toks[i]!
283
+ if (t.kind !== "punct") continue
284
+ if (t.text === "(" || t.text === "[" || t.text === "{") depth++
285
+ else if (t.text === ")" || t.text === "]" || t.text === "}") depth--
286
+ else if (t.text === "," && depth === 0) { out.push([start, i]); start = i + 1 }
287
+ }
288
+ if (skip(start) < close) out.push([start, close])
289
+ return out
290
+ }
291
+
292
+ // ---------- what the file declares ----------
293
+
294
+ /** Names the pasted file declares that the language already means something by. */
295
+ const clashes = new Map<string, string>()
296
+ const functions = new Set<string>()
297
+ for (let i = 0; i < toks.length; i++) {
298
+ if (toks[i]!.kind !== "ident" || !GLSL_TYPES.has(toks[i]!.text)) continue
299
+ const n = skip(i + 1)
300
+ if (toks[n]?.kind !== "ident") continue
301
+ const name = toks[n]!.text
302
+ const isFunction = is(skip(n + 1), "(")
303
+ if (isFunction) functions.add(name)
304
+ if (name === "mainImage" || name === "main") continue
305
+ if (taken(name, isFunction)) clashes.set(name, name + "_")
306
+ }
307
+
308
+ // ---------- what other hosts declare ----------
309
+
310
+ /**
311
+ * Token ranges written as something else, keyed by their first token: a
312
+ * declaration that goes, a rotation that becomes a call. They nest, and
313
+ * never cross.
314
+ */
315
+ const spans = new Map<number, { end: number; render: (inEntry: boolean) => string }>()
316
+ const crosses = (start: number, end: number) =>
317
+ [...spans].some(([s, v]) => (s < start && start <= v.end && v.end < end) || (start < s && s <= end && end < v.end))
318
+ /** A range and the whitespace after it, gone. */
319
+ const drop = (start: number, end: number) => {
320
+ spans.set(start, { end: toks[end + 1]?.kind === "ws" ? end + 1 : end, render: () => "" })
321
+ }
322
+
323
+ const hostUniforms = new Set<string>()
324
+ for (let i = 0; i < toks.length; i++) {
325
+ if (!is(i, "uniform")) continue
326
+ let t = skip(i + 1)
327
+ while (QUALIFIERS.has(toks[t]?.text ?? "")) t = skip(t + 1)
328
+ const n = skip(t + 1)
329
+ const name = toks[n]?.text ?? ""
330
+ const semi = skip(n + 1)
331
+ if (toks[n]?.kind !== "ident" || HOST_UNIFORMS[name] === undefined || !is(semi, ";")) continue
332
+ hostUniforms.add(name)
333
+ // Its name is the input, not a clash with it.
334
+ clashes.delete(name)
335
+ const input = HOST_UNIFORMS[name]!
336
+ noteOnce("host:" + name, toks[i]!.line, `the uniform ${name} is the input ${input}, so its declaration is dropped`)
337
+ drop(i, semi)
338
+ }
339
+
340
+ // ---------- rotations ----------
341
+
342
+ /**
343
+ * There are no matrices, and the matrix a shader almost always has is a
344
+ * rotation: `mat2 rot(float a)` returning `mat2(c, -s, s, c)`, the same as
345
+ * a macro, or written in place. Each use that multiplies a vector becomes
346
+ * `rotate(p, angle)`, the angle's sign set by the matrix and by which side
347
+ * the vector is on, since GLSL's `p * M` is M transposed times p. A
348
+ * function or macro whose every use is converted goes. Anything else keeps
349
+ * its `mat2`, which `errors` then points at, with a note naming rotate().
350
+ */
351
+ const rotations = new Map<string, { sign: 1 | -1; def: [number, number]; line: number; uses: number; converted: number }>()
352
+ for (let i = 0; i < toks.length; i++) {
353
+ const t = toks[i]!
354
+ if (t.kind === "directive") {
355
+ const m = /^#\s*define\s+([A-Za-z_]\w*)\(\s*([A-Za-z_]\w*)\s*\)(.*)$/s.exec(t.text)
356
+ if (m === null) continue
357
+ const body = tokenize(m[3]!.replace(/\\\n/g, " ")).filter((x) => x.kind !== "ws" && x.kind !== "comment").map((x) => x.text)
358
+ const a = mat2Args(body)
359
+ const sign = a === null || a.length !== 4 ? null : rotationSign(a, [`cos ( ${m[2]} )`], [`sin ( ${m[2]} )`])
360
+ if (sign !== null) rotations.set(m[1]!, { sign, def: [i, i], line: t.line, uses: 0, converted: 0 })
361
+ continue
362
+ }
363
+ if (!is(i, "mat2")) continue
364
+ const n = skip(i + 1)
365
+ const open = skip(n + 1)
366
+ if (toks[n]?.kind !== "ident" || !is(open, "(")) continue
367
+ const close = closing(open)
368
+ const params = words(open + 1, close).filter((w) => w !== "in")
369
+ const body = skip(close + 1)
370
+ if (params.length !== 2 || params[0] !== "float" || !is(body, "{")) continue
371
+ const end = closing(body)
372
+ const sign = rotationBody(words(body + 1, end), params[1]!)
373
+ if (sign !== null) rotations.set(toks[n]!.text, { sign, def: [i, end], line: t.line, uses: 0, converted: 0 })
374
+ }
375
+ // A file with a rotate of its own would have the calls written here call it.
376
+ const ownRotate = toks.some((t, i) => t.text === "rotate" && t.kind === "ident" && GLSL_TYPES.has(toks[back(i - 1)]?.text ?? "") &&
377
+ !(rotations.has("rotate") && toks[back(i - 1)]!.text === "mat2"))
378
+
379
+ /** The angle, negated when `sign` is -1, as text. */
380
+ const angle = (range: [number, number], sign: 1 | -1, inEntry: boolean) => {
381
+ const a = emit(range[0], range[1], inEntry).trim()
382
+ if (sign === 1) return a
383
+ if (/^-[\w.]+$/.test(a)) return a.slice(1)
384
+ return /^[\w.]+$/.test(a) ? `-${a}` : `-(${a})`
385
+ }
386
+ for (let i = 0; i < toks.length && !ownRotate; i++) {
387
+ if (toks[i]!.kind !== "ident" || is(back(i - 1), ".")) continue
388
+ const open = skip(i + 1)
389
+ if (!is(open, "(")) continue
390
+ const close = closing(open)
391
+ const a = args(open, close)
392
+ const fn = rotations.get(toks[i]!.text)
393
+ let sign: 1 | -1 | null = null
394
+ let range: [number, number] | null = null
395
+ if (fn !== undefined && (i < fn.def[0] || i > fn.def[1])) {
396
+ fn.uses++
397
+ if (a.length === 1) { sign = fn.sign; range = a[0]! }
398
+ } else if (is(i, "mat2") && a.length === 4) {
399
+ // In place: the angle is whatever cos() is given.
400
+ const first = skip(a[0]![0])
401
+ const inner = skip(first + 1)
402
+ if (is(first, "cos") && is(inner, "(") && closing(inner) === back(a[0]![1] - 1)) {
403
+ const x = words(inner + 1, closing(inner)).join(" ")
404
+ sign = rotationSign(a.map(([x0, x1]) => words(x0, x1).join(" ")), [`cos ( ${x} )`], [`sin ( ${x} )`])
405
+ range = [inner + 1, closing(inner)]
406
+ }
407
+ }
408
+ if (sign === null || range === null) continue
409
+ const r = range
410
+ const s = sign
411
+ const before = back(i - 1)
412
+ const after = skip(close + 1)
413
+ let span: [number, number, (inEntry: boolean) => string] | null = null
414
+ if (is(before, "=") && is(before - 1, "*") && is(after, ";")) {
415
+ // p *= M is p = p * M.
416
+ const lhsEnd = back(before - 2)
417
+ const lhs = operandBefore(lhsEnd)
418
+ if (lhs >= 0) span = [lhs, close, (e) => { const p = emit(lhs, lhsEnd + 1, e).trim(); return `${p} = rotate(${p}, ${angle(r, -s as 1 | -1, e)})` }]
419
+ } else if (is(after, "*") && !is(after + 1, "=") && !["/", "%", "*"].includes(toks[before]?.text ?? "")) {
420
+ // M * p.
421
+ const x = skip(after + 1)
422
+ const xEnd = operandAfter(x)
423
+ if (xEnd >= 0) span = [i, xEnd, (e) => `rotate(${emit(x, xEnd + 1, e).trim()}, ${angle(r, s, e)})`]
424
+ } else if (is(before, "*") && !is(after, "*")) {
425
+ // p * M.
426
+ const xEnd = back(before - 1)
427
+ const x = operandBefore(xEnd)
428
+ if (x >= 0 && !["/", "%"].includes(toks[back(x - 1)]?.text ?? "")) {
429
+ span = [x, close, (e) => `rotate(${emit(x, xEnd + 1, e).trim()}, ${angle(r, -s as 1 | -1, e)})`]
430
+ }
431
+ }
432
+ if (span === null || crosses(span[0], span[1]) || spans.has(span[0])) continue
433
+ const [start, end, render] = span
434
+ spans.set(start, { end, render })
435
+ noteOnce("rotation", toks[i]!.line, "a mat2 rotation is rotate(p, angle) here, since there are no matrices; the angle's sign follows which side p was multiplied on")
436
+ if (fn !== undefined) fn.converted++
437
+ }
438
+ for (const [name, fn] of rotations) {
439
+ if (ownRotate || fn.converted !== fn.uses) continue
440
+ drop(fn.def[0], fn.def[1])
441
+ note(fn.line, `${name} built a rotation matrix, and each use of it is rotate() now, so it is removed`)
442
+ }
443
+
444
+ // ---------- the entry point ----------
445
+
446
+ let colour = "fragColor"
447
+ /** The mainImage parameter that is Shadertoy's pixel position, when it is not called fragCoord. */
448
+ let coordParam: string | null = null
449
+ let entry: { header: [number, number]; open: number; close: number } | null = null
450
+ /** A WebGL2 style `out vec4 name;` at the top level, which becomes the local instead. */
451
+ let outDecl: [number, number] | null = null
452
+ for (let i = 0; i < toks.length; i++) {
453
+ if (!is(i, "void")) continue
454
+ const n = skip(i + 1)
455
+ const name = toks[n]?.text
456
+ if (name !== "mainImage" && name !== "main") continue
457
+ const open = skip(n + 1)
458
+ if (!is(open, "(")) continue
459
+ const close = closing(open)
460
+ const body = skip(close + 1)
461
+ if (!is(body, "{")) continue
462
+ if (name === "mainImage") {
463
+ for (const [a, b] of args(open, close)) {
464
+ const words = toks.slice(a, b).filter((t) => t.kind === "ident").map((t) => t.text)
465
+ const pname = words[words.length - 1]
466
+ if (pname === undefined) continue
467
+ if (words.includes("vec4")) colour = pname
468
+ else if (words.includes("vec2") && pname !== "fragCoord") coordParam = pname
469
+ }
470
+ }
471
+ entry = { header: [i, close + 1], open: body, close: closing(body) }
472
+ // Its pixel position is the input, not a clash with it.
473
+ if (name === "mainImage") clashes.delete("fragCoord")
474
+ break
475
+ }
476
+ if (entry === null) {
477
+ notes.push("there is no mainImage or main to convert; add `float4 main() { ... }` returning the colour")
478
+ }
479
+ for (let i = 0; i < toks.length; i++) {
480
+ if (!is(i, "out")) continue
481
+ const t = skip(i + 1)
482
+ const n = skip(t + 1)
483
+ const semi = skip(n + 1)
484
+ if (is(t, "vec4") && toks[n]?.kind === "ident" && is(semi, ";") && (entry === null || i < entry.header[0])) {
485
+ colour = toks[n]!.text
486
+ outDecl = [i, semi + 1]
487
+ break
488
+ }
489
+ }
490
+
491
+ // ---------- the rewrite ----------
492
+
493
+ const textures = new Set<string>()
494
+ let mouse = false
495
+ let uvDropped = false
496
+
497
+ /** Tokens `from` to `to`, converted. */
498
+ const emit = (from: number, to: number, inEntry: boolean): string => {
499
+ let out = ""
500
+ for (let i = from; i < to; i++) {
501
+ const t = toks[i]!
502
+ const span = spans.get(i)
503
+ if (span !== undefined && span.end < to) { out += span.render(inEntry); i = span.end; continue }
504
+ if (t.kind === "directive") { out += directive(t); continue }
505
+ if (t.kind === "number") { out += t.text.replace(/[uU]$/, ""); continue }
506
+ if (t.kind !== "ident") { out += t.text; continue }
507
+
508
+ const name = t.text
509
+ const prev = back(i - 1)
510
+ if (is(prev, ".")) { out += name; continue }
511
+ const next = skip(i + 1)
512
+ const called = is(next, "(")
513
+
514
+ if (name === "precision") {
515
+ // `precision mediump float;` means nothing outside GLSL ES.
516
+ let j = i
517
+ while (j < to && !is(j, ";")) j++
518
+ i = skip(j + 1) - 1
519
+ continue
520
+ }
521
+ if (name === "highp" || name === "mediump" || name === "lowp" || (name === "in" && inParams(i))) {
522
+ i = skip(i + 1) - 1
523
+ continue
524
+ }
525
+ if (name === "out" || name === "inout") {
526
+ note(t.line, `${name} parameters do not exist here; return the value instead`)
527
+ out += name
528
+ continue
529
+ }
530
+ if (name === "uniform" && is(skip(i + 1), "sampler2D")) {
531
+ // `uniform sampler2D tex;` is `texture2D tex;`.
532
+ out += "texture2D"
533
+ i = skip(i + 1)
534
+ continue
535
+ }
536
+ if (name === "sampler2D") { out += "texture2D"; continue }
537
+ if (inEntry && coordParam !== null && name === coordParam) { out += "fragCoord"; continue }
538
+ if (inEntry && name === "gl_FragColor") { out += colour; continue }
539
+ const clash = clashes.get(name)
540
+ if (clash !== undefined) {
541
+ noteOnce("clash:" + name, t.line, `${name} is renamed ${clash}, since ${name} already means something here`)
542
+ out += clash
543
+ continue
544
+ }
545
+ if (RENAME[name] !== undefined && !(name === "texture" && !called)) {
546
+ if (name === "gl_FragCoord" && !is(next, ".")) note(t.line, "gl_FragCoord is a vec4 in GLSL and fragCoord is a float2 here")
547
+ out += RENAME[name]
548
+ continue
549
+ }
550
+ if (NARROW[name] !== undefined) {
551
+ // A for loop's counter is the one int the language has.
552
+ const forCounter = name === "int" && is(back(i - 1), "(") && is(back(back(i - 1) - 1), "for")
553
+ if (forCounter) { out += name; continue }
554
+ const [to2, why] = NARROW[name]!
555
+ noteOnce("narrow:" + name, t.line, why)
556
+ out += to2
557
+ continue
558
+ }
559
+ if (hostUniforms.has(name) && (name === "u_time" || name === "u_resolution")) { out += HOST_UNIFORMS[name]; continue }
560
+ if (name === "mat2") {
561
+ noteOnce("mat2", t.line, "there are no matrices here; a mat2 rotation is rotate(p, angle): p *= mat2(c, -s, s, c) is rotate(p, a), and mat2(c, -s, s, c) * p is rotate(p, -a)")
562
+ out += name
563
+ continue
564
+ }
565
+ const unconverted = rotations.get(name)
566
+ if (unconverted !== undefined && called && (i < unconverted.def[0] || i > unconverted.def[1])) {
567
+ noteOnce("rot:" + t.line + name, t.line, `${name} builds a rotation matrix, and this use of it was not converted; write it as rotate(p, angle)`)
568
+ }
569
+ if (/^iChannel\d$/.test(name)) { textures.add(name); out += name; continue }
570
+ if (name === "iMouse") {
571
+ mouse = true
572
+ noteOnce("mouse", t.line, "iMouse is the uniform mouse, which the host sets; Shadertoy's sign encoding of clicks is not carried over")
573
+ out += "mouse"
574
+ continue
575
+ }
576
+ if (name === "iResolution") {
577
+ const dot = is(next, ".") ? skip(next + 1) : -1
578
+ const member = dot >= 0 ? toks[dot]?.text ?? null : null
579
+ if (member === null) {
580
+ out += "float3(resolution, 1.0)"
581
+ } else if (/^[xy]+$/.test(member)) {
582
+ out += "resolution"
583
+ } else {
584
+ note(t.line, `iResolution.${member}: resolution is a float2 here, and the third component was always 1`)
585
+ out += "resolution"
586
+ }
587
+ continue
588
+ }
589
+ if (MISSING[name] !== undefined) {
590
+ note(t.line, `${name} has no counterpart: ${MISSING[name]}`)
591
+ out += name
592
+ continue
593
+ }
594
+ if (name === "void" && !functions.has(toks[skip(i + 1)]?.text ?? "")) { out += name; continue }
595
+ if (name === "void") {
596
+ note(t.line, `${toks[skip(i + 1)]!.text} returns nothing, and every function here returns a value`)
597
+ out += name
598
+ continue
599
+ }
600
+
601
+ if (called) {
602
+ const close = closing(next)
603
+ const a = args(next, close).map(([x, y]) => emit(x, y, inEntry).trim())
604
+ const wrap = (s: string) => (/^[\w.]+$/.test(s) || /^[\w.]+\(.*\)$/.test(s) ? s : `(${s})`)
605
+ let call: string | null = null
606
+ if (name === "mod" && a.length === 2) {
607
+ // GLSL's mod floors; % and fmod truncate. Written out, so negative values stay right.
608
+ call = `(${wrap(a[0]!)} - ${wrap(a[1]!)} * floor(${wrap(a[0]!)} / ${wrap(a[1]!)}))`
609
+ } else if (name === "atan" && a.length === 2) {
610
+ call = `atan2(${a[0]}, ${a[1]})`
611
+ } else if (name === "atan" && a.length === 1) {
612
+ call = `atan2(${a[0]}, 1.0)`
613
+ } else if (name === "inversesqrt" && a.length === 1) {
614
+ call = `(1.0 / sqrt(${a[0]}))`
615
+ } else if (name === "radians" && a.length === 1) {
616
+ call = `(${wrap(a[0]!)} * 0.017453292519943295)`
617
+ } else if (name === "degrees" && a.length === 1) {
618
+ call = `(${wrap(a[0]!)} * 57.29577951308232)`
619
+ } else if (name === "exp2" && a.length === 1) {
620
+ call = `pow(2.0, ${a[0]})`
621
+ } else if (name === "log2" && a.length === 1) {
622
+ call = `(log(${a[0]}) * 1.4426950408889634)`
623
+ }
624
+ if (call !== null) { out += call; i = close; continue }
625
+ }
626
+ out += name
627
+ }
628
+ return out
629
+ }
630
+
631
+ /** Whether the token at `i` is inside a function's parameter list. */
632
+ function inParams(i: number): boolean {
633
+ let depth = 0
634
+ for (let j = i - 1; j >= 0; j--) {
635
+ const t = toks[j]!
636
+ if (t.kind !== "punct") continue
637
+ if (t.text === ")") depth++
638
+ else if (t.text === "(") { if (depth === 0) return toks[back(j - 1)]?.kind === "ident"; depth-- }
639
+ else if (t.text === ";" || t.text === "{" || t.text === "}") return false
640
+ }
641
+ return false
642
+ }
643
+
644
+ /**
645
+ * `#define NAME value` becomes a const, typed by a leading constructor
646
+ * (`vec3(...)` is a float3) and a float otherwise; a macro with parameters
647
+ * and any other directive are kept as a comment.
648
+ */
649
+ function directive(t: Tok): string {
650
+ const define = /^#\s*define\s+([A-Za-z_]\w*)\s+(.+?)\s*$/s.exec(t.text)
651
+ if (define !== null && !/^\(/.test(t.text.slice(t.text.indexOf(define[1]!) + define[1]!.length))) {
652
+ const value = define[2]!.replace(/\\\n/g, " ").replace(/\b(vec[234]|mix|fract|iTime|iGlobalTime)\b/g, (w) => RENAME[w]!)
653
+ const type = /^\s*(float[234])\s*\(/.exec(value)?.[1] ?? "float"
654
+ return `const ${type} ${define[1]} = ${value};`
655
+ }
656
+ const guard = /^#\s*(ifdef|ifndef|if|else|elif|endif)\b/.test(t.text) && /GL_ES|^#\s*(else|endif)/.test(t.text)
657
+ if (!guard) note(t.line, `${t.text.trim()} has no counterpart, since there is no preprocessor; it is kept as a comment`)
658
+ return "// " + t.text
659
+ }
660
+
661
+ // The entry point: its header, a local for the colour, the colour returned.
662
+ let body: string
663
+ if (entry === null) {
664
+ body = emit(0, toks.length, false)
665
+ } else {
666
+ const { header, open, close } = entry
667
+ // In file order, so a note names the first line a name is on.
668
+ const before = outDecl === null ? emit(0, header[0], false) : emit(0, outDecl[0], false) + emit(outDecl[1], header[0], false)
669
+ const inner = emit(open + 1, close, true)
670
+ .replace(/\breturn\s*;/g, `return ${colour};`)
671
+ // Shadertoy's own uv, which is exactly the input: y up, 0 to 1 across the element.
672
+ .replace(/\n?[ \t]*float2\s+uv_\s*=\s*fragCoord(\.xy)?\s*\/\s*resolution(\.xy)?\s*;[ \t]*/, () => {
673
+ uvDropped = true
674
+ return ""
675
+ })
676
+ const indent = /\n([ \t]+)\S/.exec(inner)?.[1] ?? " "
677
+ body = before + "float4 main()" + emit(header[1], open, true) + "{\n" + indent +
678
+ `float4 ${colour} = float4(0.0);` + inner.replace(/\s*$/, "") + "\n" + indent + `return ${colour};\n}` +
679
+ emit(close + 1, toks.length, false)
680
+ }
681
+ // Shadertoy's own uv line is exactly the input, so it goes, and the name is the input's again.
682
+ if (uvDropped) {
683
+ body = body.replace(/\buv_\b/g, "uv")
684
+ const renamed = notes.findIndex((n) => n.includes("uv is renamed uv_"))
685
+ if (renamed >= 0) notes.splice(renamed, 1)
686
+ const at = toks.findIndex((t, i) => t.text === "uv" && is(back(i - 1), "vec2"))
687
+ note(at >= 0 ? toks[at]!.line : 1, "the uv line is dropped: fragCoord / resolution is the input uv, y up")
688
+ }
689
+
690
+ const declared = [...textures].sort().map((t) => `texture2D ${t};`)
691
+ if (mouse) declared.unshift("uniform float4 mouse;")
692
+ const source = (declared.length > 0 ? declared.join("\n") + "\n" : "") + body.replace(/^\n+/, "")
693
+ const lineOf = (n: string) => Number(/^line (\d+)/.exec(n)?.[1] ?? 0)
694
+ notes.sort((a, b) => lineOf(a) - lineOf(b))
695
+ return { source, notes, errors: diagnose(source, { file: options.file ?? "program.sl" }) }
696
+ }
package/src/lang/lower.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  * than an optimisation.
34
34
  */
35
35
 
36
- import { INPUTS, type Program, type SLType } from "../ir"
36
+ import { DERIVED_INPUTS, INPUTS, type Program, type SLType } from "../ir"
37
37
  import type { SlSdfKind } from "../shapes"
38
38
  import * as sl from "../sl"
39
39
  import type { Num, Val } from "../sl"
@@ -161,6 +161,17 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
161
161
  for (const [name, width] of Object.entries(INPUTS)) {
162
162
  global.declare(name, { width: width as SLType, value: (inputs as never)[name] })
163
163
  }
164
+ // Read through, so a program that never names one records nothing
165
+ // for it. The setter only serves a refused assignment, which the
166
+ // checker has already reported and lowering carries on past.
167
+ for (const [name, width] of Object.entries(DERIVED_INPUTS)) {
168
+ let value: LV | undefined
169
+ global.declare(name, {
170
+ width: width as SLType,
171
+ get value() { return value ??= (inputs as never)[name] },
172
+ set value(v: LV) { value = v },
173
+ })
174
+ }
164
175
 
165
176
  // Uniforms and textures take their slots in DECLARATION order, before
166
177
  // anything is lowered, so a slot is a property of the file rather than
@@ -184,7 +184,9 @@ class Parser {
184
184
  this.recover(() => this.parseDeclaration(unit), "declaration")
185
185
  }
186
186
 
187
- if (unit.main === null && this.requireMain) {
187
+ // Collecting, a syntax error may have swallowed main (an unclosed body
188
+ // reads to the end of the file), so "no main" only when nothing else went wrong.
189
+ if (unit.main === null && this.requireMain && (this.errors?.length ?? 0) === 0) {
188
190
  this.report(
189
191
  "this file declares no main. A .sl file is one fragment function: add " +
190
192
  "`float4 main() { ... }`",
package/src/params.ts CHANGED
@@ -20,7 +20,7 @@ import { BUILTINS } from "./lang/builtins"
20
20
  import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
21
21
  import { LIB_PARAM_NAMES } from "./lib/names"
22
22
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
23
- import type { InputName } from "./ir"
23
+ import type { SourceInputName } from "./ir"
24
24
 
25
25
  /** One function of the helper library, as its source declares it. */
26
26
  export interface LibSignature {
@@ -164,11 +164,17 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
164
164
  ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
165
165
  }
166
166
 
167
- /** One line on each input, for the same tooltips. */
168
- export const INPUT_DOCS: Readonly<Record<InputName, string>> = {
169
- uv: "Where this pixel is in the element, 0 to 1 on each axis, with y up.",
170
- fragCoord: "Where this pixel is in the element, in pixels, with y up.",
171
- resolution: "The element's size in pixels.",
172
- time: "Seconds since the effect started.",
173
- aspect: "The element's width divided by its height.",
167
+ /**
168
+ * One line on each input, for the same tooltips. In the words of no one host:
169
+ * the output is a UI element in OneJS and an image on a Magerie board, and the
170
+ * clock is whichever one the host runs.
171
+ */
172
+ export const INPUT_DOCS: Readonly<Record<SourceInputName, string>> = {
173
+ uv: "The centre of this pixel in the output, 0 to 1 on each axis, with y up.",
174
+ fragCoord: "The centre of this pixel in the output, in pixels, with y up: the first column is at 0.5.",
175
+ resolution: "The output's size in pixels.",
176
+ time: "The clock, in seconds.",
177
+ aspect: "The output's width divided by its height.",
178
+ texel: "One pixel, in uv: 1 / resolution. Step by it to read a neighbouring pixel.",
179
+ centered: "uv with 0 at the centre and x scaled by aspect, so a circle drawn in it stays round.",
174
180
  }
package/src/sl.ts CHANGED
@@ -254,6 +254,10 @@ export interface ProgramInputs {
254
254
  resolution: Vec2
255
255
  time: Float
256
256
  aspect: Float
257
+ /** One pixel, in uv: `1 / resolution`. Recorded only if read. */
258
+ texel: Vec2
259
+ /** `(uv - 0.5) * vec2(aspect, 1)`: 0 at the centre, and a circle stays round. Recorded only if read. */
260
+ centered: Vec2
257
261
  }
258
262
 
259
263
  /**
@@ -271,6 +275,12 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
271
275
  const ref = b.add({ k: "input", type: width as SLType, name: name as InputName })
272
276
  ;(inputs as any)[name] = mk(b, ref, width as SLType)
273
277
  }
278
+ // Getters, so a program that never reads one records nothing for it
279
+ // (DERIVED_INPUTS in ir.ts). A second read is hash consed to the first.
280
+ Object.defineProperties(inputs, {
281
+ texel: { enumerable: true, get: () => vec2(1, 1).div(inputs.resolution) },
282
+ centered: { enumerable: true, get: () => inputs.uv.sub(0.5).mul(vec2(inputs.aspect, 1)) },
283
+ })
274
284
  const out = fn(inputs)
275
285
  if (!(out instanceof Val)) throw new SLError("a program must return an sl value, not " + typeof out)
276
286
  if (out.width !== TYPE.VEC4) {
package/src/tables.ts CHANGED
@@ -13,7 +13,8 @@ export {
13
13
  SLOP, SL_ARITY, SL_NAME, SL_HLSL, SL_CALL_NAMES, SL_GLSL_HINT, SL_UNIMPLEMENTED,
14
14
  } from "./ops"
15
15
  export type { SLSurface } from "./ops"
16
- export { INPUTS } from "./ir"
16
+ export { DERIVED_INPUTS, INPUTS, SOURCE_INPUTS } from "./ir"
17
+ export type { DerivedInputName, InputName, SourceInputName } from "./ir"
17
18
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
18
19
  export { BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
19
20
  export { PRELUDE_DOCS, PRELUDE_NAMES } from "./lang/prelude-source"