onejs-sl 0.1.3 → 0.1.5
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 +25 -0
- package/README.md +7 -3
- package/package.json +1 -1
- package/src/body.ts +20 -2
- package/src/index.ts +5 -3
- package/src/lang/README.md +49 -10
- package/src/lang/check.ts +84 -44
- package/src/lang/classify.ts +70 -0
- package/src/lang/index.ts +4 -1
- package/src/lang/lexer.ts +72 -15
- package/src/lang/lower.ts +67 -6
- package/src/lang/parser.ts +46 -16
- package/src/lang/prelude-source.ts +53 -0
- package/src/lang/prelude.ts +2 -34
- package/src/lang/words.ts +23 -0
- package/src/params.ts +61 -0
- package/src/tables.ts +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
What an editor needs, and errors that fix themselves (`Specs/SL_NEXT.md` 5 and 9). Every program that compiled before compiles to exactly what it did, unless it names something after a keyword or a type.
|
|
6
|
+
|
|
7
|
+
- `classify` returns every token with its class, comments included, and never throws
|
|
8
|
+
- `SL_KEYWORDS`, `SL_TYPES` and `TYPE_WIDTH` are exported from `onejs-sl/tables`
|
|
9
|
+
- `BUILTIN_DOCS`, `INPUT_DOCS` and `PRELUDE_DOCS` describe every builtin, input and prelude function in a line
|
|
10
|
+
- `SLParseError` carries `text`, its message alone, and `offset`
|
|
11
|
+
- An error carries a one click `fix` where the fix is certain: a GLSL rename or a "did you mean"
|
|
12
|
+
- A GLSL name used as a value, such as `gl_FragCoord`, gets the HLSL name
|
|
13
|
+
- `break`, `continue` and `switch` say why they are not there
|
|
14
|
+
- A keyword or a type cannot name a value, a function or a parameter
|
|
15
|
+
- Something missing at the end of a line or the file is marked on the last token before it
|
|
16
|
+
- Errors from inside the EDSL say `float3`, not `vec3`
|
|
17
|
+
- `BodyTarget.sample` documents what the body expects of a sample
|
|
18
|
+
|
|
19
|
+
## 0.1.4
|
|
20
|
+
|
|
21
|
+
Four things that were errors now compile, the most common mistakes in cold runs of authors writing from the docs alone (`Specs/SL_NEXT.md` 2). Every program that compiled before compiles to exactly what it did.
|
|
22
|
+
|
|
23
|
+
- A swizzle can be assigned to: `p.x = 1;`, `c.rgb *= 0.5;`
|
|
24
|
+
- A single number fills a vector in a declaration, an assignment or a uniform default: `float3 c = 0.5;`
|
|
25
|
+
- A float4 goes into a float3 by dropping its fourth component: `float3 c = #ff8040;`
|
|
26
|
+
- A local or a uniform may take a builtin's or a prelude function's name
|
|
27
|
+
|
|
3
28
|
## 0.1.3
|
|
4
29
|
|
|
5
30
|
`onejs-sl/tables` names every parameter, so an editor can offer `lerp(x, y, s)` rather than an argument count. Nothing a program compiles to changes.
|
package/README.md
CHANGED
|
@@ -35,9 +35,9 @@ only what it calls.
|
|
|
35
35
|
|
|
36
36
|
| Entry | Contents |
|
|
37
37
|
|---|---|
|
|
38
|
-
| `onejs-sl` | `parse` and `analyze`, the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, length) |
|
|
38
|
+
| `onejs-sl` | `parse` and `analyze`, `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) |
|
|
39
39
|
| `onejs-sl/core` | the same without the parser: what a game needs at run time |
|
|
40
|
-
| `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`: what completion and highlighting read. `BUILTIN_PARAMS`, `SL_SDF_PARAM_NAMES` and `LIB_SIGNATURES` name every parameter, so an editor can show `lerp(x, y, s)` |
|
|
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 |
|
|
42
42
|
| `onejs-sl/vm` | `encode`, `SL_WIRE_VERSION` |
|
|
43
43
|
| `onejs-sl/emit/hlsl-body` | `emitBody`: a program as a function body for a host's own frame; `emitLibrary`: the library functions it calls |
|
|
@@ -56,7 +56,11 @@ intrinsic at its exact type, since Metal overloads where HLSL converts). The `Bo
|
|
|
56
56
|
for each input, the float4 holding a uniform slot, a texture sample, whether
|
|
57
57
|
`toLinear` is real (`colour: "linear"`) or the identity (`"gamma"`), and
|
|
58
58
|
optionally a local to assign the result to. It returns the uniform and texture
|
|
59
|
-
slots the body uses and the library functions it calls.
|
|
59
|
+
slots the body uses and the library functions it calls. The sample's contract
|
|
60
|
+
is on `BodyTarget.sample`: straight alpha in and out, rgb in the space `colour`
|
|
61
|
+
names (an sRGB texture decoded before filtering when it is `linear`), and
|
|
62
|
+
filtering and wrapping left to the host, which OneJS's hosts take from the
|
|
63
|
+
bound texture's own settings. OneJS's Unity shader is
|
|
60
64
|
one frame over it (`hlsl.ts`); Magerie's compute kernel is another, and its
|
|
61
65
|
target is in `src/body.test.ts` so an opcode cannot change without the text
|
|
62
66
|
Magerie compiles changing in front of a test.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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/body.ts
CHANGED
|
@@ -31,12 +31,30 @@ export interface BodyTarget {
|
|
|
31
31
|
inputs: Record<InputName, string>
|
|
32
32
|
/** The float4 holding a uniform slot. The body swizzles it down to the uniform's width. */
|
|
33
33
|
uniform: (slot: number, name: string) => string
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* A float4 sample of the texture in `slot` at the float2 expression `uv`.
|
|
36
|
+
*
|
|
37
|
+
* The contract the body is written against, which is what OneJS's own hosts
|
|
38
|
+
* do with the GPU's sampler:
|
|
39
|
+
*
|
|
40
|
+
* - **Straight alpha**, not premultiplied, in and out: the body never
|
|
41
|
+
* multiplies or divides by alpha, and its result is straight too.
|
|
42
|
+
* - **In the space `colour` names.** With `linear`, rgb is linear light, so
|
|
43
|
+
* an sRGB texture is decoded before it is filtered, as an sRGB sampler
|
|
44
|
+
* does; with `gamma`, rgb is as stored.
|
|
45
|
+
* - **Filtered and wrapped by the host.** The body does neither. OneJS's
|
|
46
|
+
* hosts, the generated shader, the VM and the web backends alike, use the
|
|
47
|
+
* bound texture's own filter and wrap modes, so a host that filters by
|
|
48
|
+
* hand draws the same picture by following the texture's settings.
|
|
49
|
+
*
|
|
50
|
+
* What becomes of the result is the host's: OneJS writes it to the target
|
|
51
|
+
* as it is, and a host that composites premultiplied multiplies on write.
|
|
52
|
+
*/
|
|
35
53
|
sample: (slot: number, uv: string) => string
|
|
36
54
|
/**
|
|
37
55
|
* `linear`: `toLinear` calls `sl_toLinear`, for a host whose target holds
|
|
38
56
|
* linear light. `gamma`: `toLinear` is the identity, decided here, so hex
|
|
39
|
-
* colours and ramps stay as written.
|
|
57
|
+
* colours and ramps stay as written. `sample` reads in the same space.
|
|
40
58
|
*/
|
|
41
59
|
colour: "gamma" | "linear"
|
|
42
60
|
/** Assign the result to this local instead of returning it. */
|
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, the TypeScript form, the IR, its JSON
|
|
9
|
+
* onejs-sl this: parse, classify, 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
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export * from "./core"
|
|
21
21
|
export {
|
|
22
|
-
analyze, parse, parseUnit, preludeFunctions, tokenize, PRELUDE_SOURCE, SLParseError,
|
|
22
|
+
analyze, classify, parse, parseUnit, preludeFunctions, tokenize, PRELUDE_SOURCE, SLParseError,
|
|
23
|
+
} from "./lang"
|
|
24
|
+
export type {
|
|
25
|
+
Checked, Expr, FuncDecl, ParseOptions, Pos, SLClassifiedToken, SLFix, SLTokenClass, Stmt, Token, Unit,
|
|
23
26
|
} from "./lang"
|
|
24
|
-
export type { Checked, Expr, FuncDecl, ParseOptions, Pos, Stmt, Token, Unit } from "./lang"
|
package/src/lang/README.md
CHANGED
|
@@ -127,11 +127,25 @@ is read unconverted.
|
|
|
127
127
|
and colours, and nothing else. It is baked into the program before anything
|
|
128
128
|
runs, so it cannot name a const, which could itself name a uniform.
|
|
129
129
|
|
|
130
|
-
**Shadowing is refused.** A local may not take the name of an
|
|
131
|
-
a
|
|
132
|
-
depend on where the question is asked, for no gain in a language whose
|
|
133
|
-
functions are six lines long. A
|
|
134
|
-
|
|
130
|
+
**Shadowing is refused between values.** A local may not take the name of an
|
|
131
|
+
input, a uniform, a texture or a const. Allowing it would make "is this a
|
|
132
|
+
texture?" depend on where the question is asked, for no gain in a language whose
|
|
133
|
+
functions are six lines long. A value *may* take a builtin's or a prelude
|
|
134
|
+
function's name (`float circle`, `uniform float turbulence`), which hides that
|
|
135
|
+
function wherever the value is visible, and a call there says so. A file
|
|
136
|
+
function may shadow a *prelude* function, which is the sanctioned way to replace
|
|
137
|
+
one.
|
|
138
|
+
|
|
139
|
+
**Declarations and assignments convert two ways, and only two.** A single
|
|
140
|
+
number fills every component (`float3 c = 0.5;`, and a uniform default such as
|
|
141
|
+
`uniform float2 offset = 0;`), and a float4 goes into a float3 by dropping its
|
|
142
|
+
fourth component, which is how a colour becomes an rgb. Every other width change
|
|
143
|
+
is refused with the swizzle that fixes it.
|
|
144
|
+
|
|
145
|
+
**A swizzle can be assigned to.** `p.x *= aspect;` rebuilds `p` from the
|
|
146
|
+
components it names and keeps the rest, so it is the same program as
|
|
147
|
+
`p = float2(p.x * aspect, p.y);`. A swizzle that names a component twice, or
|
|
148
|
+
one the local does not have, is refused.
|
|
135
149
|
|
|
136
150
|
## Not yet spellable
|
|
137
151
|
|
|
@@ -154,11 +168,36 @@ lowering it changes recorded behaviour rather than parser behaviour.
|
|
|
154
168
|
|
|
155
169
|
## Errors
|
|
156
170
|
|
|
157
|
-
Every one carries file, line
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
Every one carries file, line, column, offset and length, and `text`, the
|
|
172
|
+
message without its tag and its `file:line:col: `, for an editor that puts the
|
|
173
|
+
words in a marker of its own. An error that starts inside the EDSL keeps the
|
|
174
|
+
EDSL's wording, with its type names put in the file's: `"z" is component 3 of
|
|
175
|
+
a float2, which has 2` was already the right sentence; what it lacked was a
|
|
176
|
+
place. `errors.test.ts` checks the positions as strictly as the words, because
|
|
177
|
+
a marker under the wrong character sends the reader to look at something that
|
|
178
|
+
is fine. Something missing at the end of a line or the file marks the last
|
|
179
|
+
token before it, where there is a character to underline.
|
|
180
|
+
|
|
181
|
+
Where the fix is certain, the error carries it as `fix`: a title and the text
|
|
182
|
+
for the marked range, one click in an editor. A GLSL spelling that means the
|
|
183
|
+
same in HLSL (`mix`, `fract`, `vec3`, `gl_FragCoord`, `iTime`, two argument
|
|
184
|
+
`atan`) and a "did you mean" both have one. `mod` does not, since GLSL's floors
|
|
185
|
+
and `%` truncates, and neither does `int`, since float would change what
|
|
186
|
+
dividing it does. That is Decision 1 kept as `Specs/SL_NEXT.md` 5 A has it:
|
|
187
|
+
one spelling, and every hint a fix.
|
|
188
|
+
|
|
189
|
+
## For an editor
|
|
190
|
+
|
|
191
|
+
`classify(source)` is every token with its class (keyword, type, builtin,
|
|
192
|
+
input, prelude, number, hex, comment, punct, member, ident, or invalid), built
|
|
193
|
+
on the parser's own scanner and word lists, so the highlighting is the parser's.
|
|
194
|
+
It never throws, since it runs on every keystroke. It classifies by spelling,
|
|
195
|
+
not scope: a local named `circle` still reads as a builtin where it is used.
|
|
196
|
+
|
|
197
|
+
`SL_KEYWORDS` and `SL_TYPES` (`words.ts`) are the lists the parser reads, and a
|
|
198
|
+
name may be neither. `onejs-sl/tables` carries them with a one line description
|
|
199
|
+
of every builtin, input and prelude function; a prelude function's is the
|
|
200
|
+
comment above it in `prelude-source.ts`.
|
|
162
201
|
|
|
163
202
|
## See also
|
|
164
203
|
|
package/src/lang/check.ts
CHANGED
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
* whether a name exists, whether a body is shaped like a body, and whether the
|
|
15
15
|
* program fits the budgets before anything is built.
|
|
16
16
|
*
|
|
17
|
-
* SHADOWING IS REFUSED. HLSL would let a local called `time`
|
|
18
|
-
* a parameter called `tint` hide a uniform. Allowing it would
|
|
19
|
-
* question about a name ("is this a texture?") depends on where
|
|
20
|
-
* from, for no expressive gain in a language whose functions are
|
|
21
|
-
* lines long.
|
|
17
|
+
* SHADOWING IS REFUSED, between values. HLSL would let a local called `time`
|
|
18
|
+
* hide the input, or a parameter called `tint` hide a uniform. Allowing it would
|
|
19
|
+
* mean every later question about a name ("is this a texture?") depends on where
|
|
20
|
+
* it is asked from, for no expressive gain in a language whose functions are
|
|
21
|
+
* half a dozen lines long.
|
|
22
|
+
*
|
|
23
|
+
* A value MAY take the name of a builtin or a prelude function: `float circle`,
|
|
24
|
+
* `uniform float turbulence`. Those are the names an author reaches for first,
|
|
25
|
+
* and refusing them was the most common error left once the others were fixed
|
|
26
|
+
* (`Specs/SL_NEXT.md` 2d). Calls and values never meet in one position, so the
|
|
27
|
+
* only question left is a call to the name where the value is visible, and that
|
|
28
|
+
* is refused with the reason.
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
31
|
import { INPUTS } from "../ir"
|
|
@@ -27,10 +34,22 @@ import { SL_GLSL_HINT } from "../ops"
|
|
|
27
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
28
35
|
import { SL_SDF_SHAPES } from "../shapes"
|
|
29
36
|
import type { Expr, FuncDecl, Stmt, Unit } from "./ast"
|
|
30
|
-
import { SLParseError, type Pos } from "./lexer"
|
|
37
|
+
import { SLParseError, type Pos, type SLFix } from "./lexer"
|
|
31
38
|
|
|
32
39
|
const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
33
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The GLSL spellings whose HLSL name means the same wherever it is written, so
|
|
43
|
+
* the hint can be a one click fix (`Specs/SL_NEXT.md` 5, Decision 1 A). The
|
|
44
|
+
* rest of `SL_GLSL_HINT` gets the hint alone: `mod` floors where `%` truncates,
|
|
45
|
+
* `ivec2` truncates where `float2` does not, `atan` is `atan2` only with two
|
|
46
|
+
* arguments (handled at the call), and `textureLod` names a builtin that is not
|
|
47
|
+
* implemented yet.
|
|
48
|
+
*/
|
|
49
|
+
const GLSL_RENAMES = new Set(["mix", "fract", "texture", "vec2", "vec3", "vec4", "gl_FragCoord", "iTime", "iResolution"])
|
|
50
|
+
|
|
51
|
+
const rename = (from: string, to: string): SLFix => ({ title: `Replace ${from} with ${to}`, replacement: to })
|
|
52
|
+
|
|
34
53
|
export interface Checked {
|
|
35
54
|
unit: Unit
|
|
36
55
|
/** Every callable function by name, the file's shadowing the prelude's. */
|
|
@@ -58,9 +77,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
58
77
|
// terminating control flow when the callee's `never` comes from an explicit
|
|
59
78
|
// type, so without this every `if (x.k !== "ident") fail(...)` below would
|
|
60
79
|
// fail to narrow.
|
|
61
|
-
const fail: (message: string, pos: Pos, length?: number) => never =
|
|
62
|
-
(message, pos, length = 1) => {
|
|
63
|
-
throw new SLParseError(message, file, pos, length)
|
|
80
|
+
const fail: (message: string, pos: Pos, length?: number, fix?: SLFix) => never =
|
|
81
|
+
(message, pos, length = 1, fix) => {
|
|
82
|
+
throw new SLParseError(message, file, pos, length, fix)
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
const funcs = new Map<string, FuncDecl>()
|
|
@@ -84,8 +103,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
84
103
|
*
|
|
85
104
|
* Functions are NOT in here, because a file function shadowing a prelude
|
|
86
105
|
* function of the same name is the sanctioned way to replace one
|
|
87
|
-
* (`Specs/SL_TEXT.md` 3.8). Every other kind of declaration checks
|
|
88
|
-
*
|
|
106
|
+
* (`Specs/SL_TEXT.md` 3.8). Every other kind of declaration checks
|
|
107
|
+
* `valueClash`, which adds the file's own functions.
|
|
89
108
|
*/
|
|
90
109
|
const taken = (n: string): string | null => {
|
|
91
110
|
if (INPUT_NAMES.has(n)) return "an input"
|
|
@@ -98,14 +117,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
98
117
|
return null
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
|
|
120
|
+
/** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
|
|
121
|
+
const valueClash = (n: string): string | null => {
|
|
122
|
+
const why = taken(n)
|
|
123
|
+
if (why !== null && why !== "a builtin") return why
|
|
102
124
|
const fn = funcs.get(n)
|
|
103
|
-
|
|
104
|
-
return fn.prelude ? "a prelude function" : "a function"
|
|
125
|
+
return fn !== undefined && !fn.prelude ? "a function" : null
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
for (const u of unit.uniforms) {
|
|
108
|
-
const clash =
|
|
129
|
+
const clash = valueClash(u.name)
|
|
109
130
|
if (clash !== null) fail(`"${u.name}" already names ${clash}`, u.pos, u.name.length)
|
|
110
131
|
uniforms.set(u.name, u)
|
|
111
132
|
}
|
|
@@ -121,7 +142,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
for (const t of unit.textures) {
|
|
124
|
-
const clash =
|
|
145
|
+
const clash = valueClash(t.name)
|
|
125
146
|
if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
|
|
126
147
|
textures.set(t.name, t)
|
|
127
148
|
}
|
|
@@ -136,7 +157,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
for (const c of unit.consts) {
|
|
139
|
-
const clash =
|
|
160
|
+
const clash = valueClash(c.name)
|
|
140
161
|
if (clash !== null) fail(`"${c.name}" already names ${clash}`, c.pos, c.name.length)
|
|
141
162
|
consts.set(c.name, c)
|
|
142
163
|
}
|
|
@@ -251,7 +272,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
251
272
|
function checkFunction(fn: FuncDecl): void {
|
|
252
273
|
const params = new Set<string>()
|
|
253
274
|
for (const p of fn.params) {
|
|
254
|
-
const clash =
|
|
275
|
+
const clash = valueClash(p.name)
|
|
255
276
|
if (clash !== null) fail(`"${p.name}" already names ${clash}`, p.pos, p.name.length)
|
|
256
277
|
if (params.has(p.name)) fail(`${fn.name} already has a parameter called "${p.name}"`, p.pos)
|
|
257
278
|
params.add(p.name)
|
|
@@ -275,7 +296,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
275
296
|
switch (s.k) {
|
|
276
297
|
case "var":
|
|
277
298
|
case "const": {
|
|
278
|
-
const clash =
|
|
299
|
+
const clash = valueClash(s.name)
|
|
279
300
|
if (clash !== null) fail(`"${s.name}" already names ${clash}`, s.pos, s.name.length)
|
|
280
301
|
if (scope.has(s.name)) fail(`"${s.name}" is already declared in this body`, s.pos)
|
|
281
302
|
checkExpr(fn, s.init, scope)
|
|
@@ -283,27 +304,31 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
283
304
|
break
|
|
284
305
|
}
|
|
285
306
|
case "assign": {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
)
|
|
307
|
+
// `p.x = 1` and `c.rgb *= 0.5` write the components they name
|
|
308
|
+
// and keep the rest; lowering rebuilds the local from both.
|
|
309
|
+
let target = s.target
|
|
310
|
+
if (target.k === "member") {
|
|
311
|
+
const sw = target.name
|
|
312
|
+
if (!/^([xyzw]{1,4}|[rgba]{1,4})$/.test(sw)) {
|
|
313
|
+
fail(`${sw} is not a swizzle that can be assigned to; name components with xyzw or rgba`, target.pos, sw.length)
|
|
314
|
+
}
|
|
315
|
+
if (new Set(sw).size !== sw.length) {
|
|
316
|
+
fail(`${sw} names a component twice, so assigning to it would write one component two ways`, target.pos, sw.length)
|
|
317
|
+
}
|
|
318
|
+
target = target.obj
|
|
292
319
|
}
|
|
293
|
-
if (
|
|
294
|
-
const n =
|
|
320
|
+
if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
|
|
321
|
+
const n = target.name
|
|
295
322
|
const why = taken(n)
|
|
296
|
-
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`,
|
|
323
|
+
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
|
|
297
324
|
if (counters.has(n)) {
|
|
298
325
|
fail(
|
|
299
326
|
`"${n}" is a loop counter. The loop unrolls and substitutes it as a ` +
|
|
300
327
|
`number, so assigning to it would change nothing. Use another local`,
|
|
301
|
-
|
|
328
|
+
target.pos, n.length,
|
|
302
329
|
)
|
|
303
330
|
}
|
|
304
|
-
if (!scope.has(n))
|
|
305
|
-
fail(unknown(n, scope), s.target.pos, n.length)
|
|
306
|
-
}
|
|
331
|
+
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
307
332
|
checkExpr(fn, s.value, scope)
|
|
308
333
|
break
|
|
309
334
|
}
|
|
@@ -313,7 +338,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
313
338
|
checkBody(fn, s.else, new Set(scope), false)
|
|
314
339
|
break
|
|
315
340
|
case "for": {
|
|
316
|
-
const clash =
|
|
341
|
+
const clash = valueClash(s.counter)
|
|
317
342
|
if (clash !== null) fail(`"${s.counter}" already names ${clash}`, s.pos, s.counter.length)
|
|
318
343
|
if (scope.has(s.counter)) fail(`"${s.counter}" is already declared in this body`, s.pos)
|
|
319
344
|
checkExpr(fn, s.from, scope)
|
|
@@ -367,7 +392,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
367
392
|
if (funcs.has(e.name) || BUILTINS[e.name] !== undefined) {
|
|
368
393
|
fail(`${e.name} is a function; call it, as in \`${e.name}(...)\``, e.pos, e.name.length)
|
|
369
394
|
}
|
|
370
|
-
|
|
395
|
+
glsl(e.name, e.pos)
|
|
396
|
+
unknown(e.name, scope, e.pos)
|
|
371
397
|
break
|
|
372
398
|
}
|
|
373
399
|
case "member":
|
|
@@ -435,6 +461,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
435
461
|
)
|
|
436
462
|
}
|
|
437
463
|
|
|
464
|
+
// A value that took a builtin's or a prelude function's name hides it
|
|
465
|
+
// wherever the value is visible.
|
|
466
|
+
const value = scope.has(n) ? "a local" : uniforms.has(n) ? "a uniform" : consts.has(n) ? "a const" : null
|
|
467
|
+
if (value !== null) {
|
|
468
|
+
fail(`"${n}" is ${value} here, so it cannot be called. Rename it to call ${n}(...)`, callee.pos, n.length)
|
|
469
|
+
}
|
|
470
|
+
if (textures.has(n)) {
|
|
471
|
+
fail(`"${n}" is a texture; sample it with \`tex2D(${n}, uv)\``, callee.pos, n.length)
|
|
472
|
+
}
|
|
473
|
+
|
|
438
474
|
const builtin = BUILTINS[n]
|
|
439
475
|
if (builtin !== undefined) {
|
|
440
476
|
if (n === "tex2D") {
|
|
@@ -468,13 +504,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
468
504
|
callee.pos, n.length,
|
|
469
505
|
)
|
|
470
506
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
507
|
+
glsl(n, callee.pos, e.args.length)
|
|
508
|
+
unknown(n, scope, callee.pos)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Refuses a GLSL spelling by its HLSL name, with the fix where the rename is certain. */
|
|
512
|
+
function glsl(n: string, pos: Pos, args?: number): void {
|
|
513
|
+
const hint = SL_GLSL_HINT[n]
|
|
514
|
+
if (hint === undefined) return
|
|
515
|
+
const certain = GLSL_RENAMES.has(n) || (n === "atan" && args === 2)
|
|
516
|
+
fail(`${n} is GLSL; this is HLSL, so write ${hint}`, pos, n.length, certain ? rename(n, hint) : undefined)
|
|
478
517
|
}
|
|
479
518
|
|
|
480
519
|
function arity(e: Extract<Expr, { k: "call" }>, n: string, min: number, max: number): void {
|
|
@@ -483,13 +522,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
483
522
|
fail(`${n} takes ${want} argument${max === 1 ? "" : "s"}, got ${e.args.length}`, e.pos)
|
|
484
523
|
}
|
|
485
524
|
|
|
486
|
-
/**
|
|
487
|
-
function unknown(n: string, scope: Set<string
|
|
525
|
+
/** Refuses a name nothing declares, offering the nearest one spelled almost like it. */
|
|
526
|
+
function unknown(n: string, scope: Set<string>, pos: Pos): never {
|
|
488
527
|
const near = nearest(n, [
|
|
489
528
|
...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
490
529
|
...funcs.keys(), ...Object.keys(BUILTINS),
|
|
491
530
|
])
|
|
492
|
-
|
|
531
|
+
if (near === null) fail(`"${n}" is not declared`, pos, n.length)
|
|
532
|
+
fail(`"${n}" is not declared; did you mean ${near}?`, pos, n.length, rename(n, near))
|
|
493
533
|
}
|
|
494
534
|
|
|
495
535
|
return { unit, funcs, uniforms, textures, consts }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every token of a file with what it is, for an editor's highlighting.
|
|
3
|
+
*
|
|
4
|
+
* `Specs/SL_NEXT.md` 9. Built on the parser's own scanner and the parser's own
|
|
5
|
+
* word lists, so what highlights as a keyword is exactly what parses as one,
|
|
6
|
+
* and a builtin is exactly what `BUILTINS` holds. It never throws: it runs on
|
|
7
|
+
* every keystroke over half typed text, and a character it cannot read comes
|
|
8
|
+
* back as `invalid` rather than ending the pass.
|
|
9
|
+
*
|
|
10
|
+
* Classification is by spelling and position alone, not by scope. A local
|
|
11
|
+
* that takes a builtin's name (`float circle = ...`) still reads `builtin`
|
|
12
|
+
* where it is used; telling the two apart needs `analyze`, and an editor that
|
|
13
|
+
* wants that should ask it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { INPUTS } from "../ir"
|
|
17
|
+
import { BUILTINS } from "./builtins"
|
|
18
|
+
import { lex } from "./lexer"
|
|
19
|
+
import { PRELUDE_NAMES } from "./prelude-source"
|
|
20
|
+
import { KEYWORD_SET, TYPE_SET } from "./words"
|
|
21
|
+
|
|
22
|
+
export type SLTokenClass =
|
|
23
|
+
| "keyword" | "type" | "builtin" | "input" | "prelude"
|
|
24
|
+
| "number" | "hex" | "comment" | "punct"
|
|
25
|
+
/** A component after a dot, as in `c.rgb`. */
|
|
26
|
+
| "member"
|
|
27
|
+
/** Any other name: a uniform, a local, a function the file declares. */
|
|
28
|
+
| "ident"
|
|
29
|
+
/** A character or `#word` the language cannot read, and would refuse. */
|
|
30
|
+
| "invalid"
|
|
31
|
+
|
|
32
|
+
export interface SLClassifiedToken {
|
|
33
|
+
kind: SLTokenClass
|
|
34
|
+
text: string
|
|
35
|
+
/** 1 based. */
|
|
36
|
+
line: number
|
|
37
|
+
/** 1 based. */
|
|
38
|
+
col: number
|
|
39
|
+
offset: number
|
|
40
|
+
length: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(INPUTS))
|
|
44
|
+
|
|
45
|
+
/** The tokens of `source`, comments included, in order, each with its class. */
|
|
46
|
+
export function classify(source: string): SLClassifiedToken[] {
|
|
47
|
+
const tokens = lex(source, "", true)
|
|
48
|
+
const out: SLClassifiedToken[] = []
|
|
49
|
+
// The last token that was not a comment, which decides what a name after a dot is.
|
|
50
|
+
let before = ""
|
|
51
|
+
let beforeThat = ""
|
|
52
|
+
for (const t of tokens) {
|
|
53
|
+
if (t.kind === "eof") break
|
|
54
|
+
let kind: SLTokenClass
|
|
55
|
+
if (t.kind === "ident") {
|
|
56
|
+
if (before === ".") kind = beforeThat === "sdf" ? "builtin" : "member"
|
|
57
|
+
else if (KEYWORD_SET.has(t.text)) kind = "keyword"
|
|
58
|
+
else if (TYPE_SET.has(t.text)) kind = "type"
|
|
59
|
+
else if (INPUT_NAMES.has(t.text)) kind = "input"
|
|
60
|
+
else if (BUILTINS[t.text] !== undefined) kind = "builtin"
|
|
61
|
+
else if (PRELUDE_NAMES.has(t.text)) kind = "prelude"
|
|
62
|
+
else kind = "ident"
|
|
63
|
+
} else {
|
|
64
|
+
kind = t.kind
|
|
65
|
+
}
|
|
66
|
+
out.push({ kind, text: t.text, line: t.line, col: t.col, offset: t.offset, length: t.length })
|
|
67
|
+
if (t.kind !== "comment") { beforeThat = before; before = t.text }
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
package/src/lang/index.ts
CHANGED
|
@@ -30,8 +30,11 @@ export type {
|
|
|
30
30
|
} from "./ast"
|
|
31
31
|
export { TYPE_WIDTH } from "./ast"
|
|
32
32
|
export { SLParseError } from "./lexer"
|
|
33
|
-
export type { Pos, Token } from "./lexer"
|
|
33
|
+
export type { Pos, SLFix, Token } from "./lexer"
|
|
34
34
|
export { tokenize } from "./lexer"
|
|
35
|
+
export { classify } from "./classify"
|
|
36
|
+
export type { SLClassifiedToken, SLTokenClass } from "./classify"
|
|
37
|
+
export { SL_KEYWORDS, SL_TYPES } from "./words"
|
|
35
38
|
export { parseUnit } from "./parser"
|
|
36
39
|
export { PRELUDE_SOURCE, preludeFunctions } from "./prelude"
|
|
37
40
|
export { BUILTINS, NOT_YET } from "./builtins"
|
package/src/lang/lexer.ts
CHANGED
|
@@ -32,6 +32,19 @@ export interface Token extends Pos {
|
|
|
32
32
|
value?: number
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A replacement for the characters an error marks, offered as one click.
|
|
37
|
+
*
|
|
38
|
+
* Only where the fix is certain: `mix` to `lerp` is, but `mod` to `%` is not
|
|
39
|
+
* (GLSL's `mod` floors and `%` truncates), so `mod` gets the hint and no fix.
|
|
40
|
+
*/
|
|
41
|
+
export interface SLFix {
|
|
42
|
+
/** What the editor's action says, as in "Replace mix with lerp". */
|
|
43
|
+
title: string
|
|
44
|
+
/** Text for the marked range, `offset` to `offset + length`. */
|
|
45
|
+
replacement: string
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/**
|
|
36
49
|
* An error with a place in a file.
|
|
37
50
|
*
|
|
@@ -43,15 +56,23 @@ export class SLParseError extends SLError {
|
|
|
43
56
|
readonly file: string
|
|
44
57
|
readonly line: number
|
|
45
58
|
readonly column: number
|
|
59
|
+
/** Into the source, where the marked range starts. */
|
|
60
|
+
readonly offset: number
|
|
46
61
|
readonly length: number
|
|
62
|
+
/** The message alone, without `message`'s tag and its `file:line:col: `. */
|
|
63
|
+
readonly text: string
|
|
64
|
+
readonly fix?: SLFix
|
|
47
65
|
|
|
48
|
-
constructor(message: string, file: string, pos: Pos, length = 1) {
|
|
66
|
+
constructor(message: string, file: string, pos: Pos, length = 1, fix?: SLFix) {
|
|
49
67
|
super(`${file}:${pos.line}:${pos.col}: ${message}`)
|
|
50
68
|
this.name = "SLParseError"
|
|
51
69
|
this.file = file
|
|
52
70
|
this.line = pos.line
|
|
53
71
|
this.column = pos.col
|
|
72
|
+
this.offset = pos.offset
|
|
54
73
|
this.length = length
|
|
74
|
+
this.text = message
|
|
75
|
+
if (fix !== undefined) this.fix = fix
|
|
55
76
|
}
|
|
56
77
|
}
|
|
57
78
|
|
|
@@ -79,14 +100,37 @@ const isIdentStart = (c: string) => c === "_" || (c >= "a" && c <= "z") || (c >=
|
|
|
79
100
|
const isIdentPart = (c: string) => isIdentStart(c) || isDigit(c)
|
|
80
101
|
|
|
81
102
|
export function tokenize(source: string, file: string): Token[] {
|
|
82
|
-
|
|
103
|
+
return lex(source, file, false)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A token as `classify` sees it: comments kept, and whatever cannot be read marked rather than thrown. */
|
|
107
|
+
export interface LexedToken extends Omit<Token, "kind"> {
|
|
108
|
+
kind: TokenKind | "comment" | "invalid"
|
|
109
|
+
/** Characters it covers, which for `1.0f` is one more than its text. */
|
|
110
|
+
length: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The scanner behind `tokenize` and `classify`.
|
|
115
|
+
*
|
|
116
|
+
* With `keep` off it is `tokenize`: comments dropped, the first unreadable
|
|
117
|
+
* character thrown. With it on, nothing throws and nothing is dropped, because
|
|
118
|
+
* a highlighter runs on every keystroke over text that is half typed.
|
|
119
|
+
*/
|
|
120
|
+
export function lex(source: string, file: string, keep: true): LexedToken[]
|
|
121
|
+
export function lex(source: string, file: string, keep: false): Token[]
|
|
122
|
+
export function lex(source: string, file: string, keep: boolean): Token[] | LexedToken[] {
|
|
123
|
+
const out: LexedToken[] = []
|
|
83
124
|
let i = 0
|
|
84
125
|
let line = 1
|
|
85
126
|
let lineStart = 0
|
|
86
127
|
const here = (): Pos => ({ line, col: i - lineStart + 1, offset: i })
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
/** Throws, or with `keep` records the span as invalid and lets the caller skip it. */
|
|
129
|
+
const fail = (message: string, at: Pos = here(), length = 1): void => {
|
|
130
|
+
if (!keep) throw new SLParseError(message, file, at, length)
|
|
131
|
+
out.push({ kind: "invalid", text: source.slice(at.offset, at.offset + length), length, ...at })
|
|
89
132
|
}
|
|
133
|
+
const push = (t: Omit<LexedToken, "length">, end: number) => { out.push({ ...t, length: end - t.offset }) }
|
|
90
134
|
|
|
91
135
|
while (i < source.length) {
|
|
92
136
|
const c = source[i]!
|
|
@@ -95,18 +139,25 @@ export function tokenize(source: string, file: string): Token[] {
|
|
|
95
139
|
if (c === " " || c === "\t" || c === "\r") { i++; continue }
|
|
96
140
|
|
|
97
141
|
if (c === "/" && source[i + 1] === "/") {
|
|
142
|
+
const start = here()
|
|
98
143
|
while (i < source.length && source[i] !== "\n") i++
|
|
144
|
+
if (keep) push({ kind: "comment", text: source.slice(start.offset, i), ...start }, i)
|
|
99
145
|
continue
|
|
100
146
|
}
|
|
101
147
|
if (c === "/" && source[i + 1] === "*") {
|
|
102
148
|
const open = here()
|
|
103
149
|
i += 2
|
|
104
150
|
for (;;) {
|
|
105
|
-
if (i >= source.length)
|
|
151
|
+
if (i >= source.length) {
|
|
152
|
+
// Highlighted to the end as the comment it will become once closed.
|
|
153
|
+
if (!keep) fail("this block comment is never closed", open, 2)
|
|
154
|
+
break
|
|
155
|
+
}
|
|
106
156
|
if (source[i] === "*" && source[i + 1] === "/") { i += 2; break }
|
|
107
157
|
if (source[i] === "\n") { line++; lineStart = i + 1 }
|
|
108
158
|
i++
|
|
109
159
|
}
|
|
160
|
+
if (keep) push({ kind: "comment", text: source.slice(open.offset, i), ...open }, i)
|
|
110
161
|
continue
|
|
111
162
|
}
|
|
112
163
|
|
|
@@ -122,11 +173,11 @@ export function tokenize(source: string, file: string): Token[] {
|
|
|
122
173
|
`function, which inlines.`,
|
|
123
174
|
start, body.length + 1,
|
|
124
175
|
)
|
|
125
|
-
}
|
|
126
|
-
if (body.length === 0 || ![...body].every(isHex)) {
|
|
176
|
+
} else if (body.length === 0 || ![...body].every(isHex)) {
|
|
127
177
|
fail(`"#${body}" is not a colour; use #rgb, #rrggbb or #rrggbbaa`, start, body.length + 1)
|
|
178
|
+
} else {
|
|
179
|
+
push({ kind: "hex", text: "#" + body, ...start }, j)
|
|
128
180
|
}
|
|
129
|
-
out.push({ kind: "hex", text: "#" + body, ...start })
|
|
130
181
|
i = j
|
|
131
182
|
continue
|
|
132
183
|
}
|
|
@@ -150,7 +201,7 @@ export function tokenize(source: string, file: string): Token[] {
|
|
|
150
201
|
}
|
|
151
202
|
const value = Number(text)
|
|
152
203
|
if (!Number.isFinite(value)) fail(`"${text}" is not a number`, start, text.length)
|
|
153
|
-
|
|
204
|
+
else push({ kind: "number", text, value, ...start }, j)
|
|
154
205
|
i = j
|
|
155
206
|
continue
|
|
156
207
|
}
|
|
@@ -159,18 +210,24 @@ export function tokenize(source: string, file: string): Token[] {
|
|
|
159
210
|
const start = here()
|
|
160
211
|
let j = i
|
|
161
212
|
while (j < source.length && isIdentPart(source[j]!)) j++
|
|
162
|
-
|
|
213
|
+
push({ kind: "ident", text: source.slice(i, j), ...start }, j)
|
|
163
214
|
i = j
|
|
164
215
|
continue
|
|
165
216
|
}
|
|
166
217
|
|
|
167
218
|
const start = here()
|
|
168
219
|
const p = PUNCT.find((op) => source.startsWith(op, i))
|
|
169
|
-
if (p === undefined)
|
|
170
|
-
|
|
171
|
-
|
|
220
|
+
if (p === undefined) {
|
|
221
|
+
fail(`"${c}" means nothing here`, start)
|
|
222
|
+
i++
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
push({ kind: "punct", text: p, ...start }, i + p.length)
|
|
226
|
+
i += p.length
|
|
172
227
|
}
|
|
173
228
|
|
|
174
|
-
out.push({ kind: "eof", text: "", line, col: i - lineStart + 1, offset: i })
|
|
175
|
-
return out
|
|
229
|
+
out.push({ kind: "eof", text: "", line, col: i - lineStart + 1, offset: i, length: 0 })
|
|
230
|
+
if (keep) return out
|
|
231
|
+
// `tokenize`'s tokens are the ones it always returned, with no length.
|
|
232
|
+
return out.map(({ length: _, ...t }) => t as Token)
|
|
176
233
|
}
|
package/src/lang/lower.ts
CHANGED
|
@@ -106,7 +106,7 @@ export function lower(checked: Checked): Program {
|
|
|
106
106
|
const located = (e: unknown, pos: Pos): unknown => {
|
|
107
107
|
if (e instanceof SLParseError) return e
|
|
108
108
|
const raw = e instanceof Error ? e.message : String(e)
|
|
109
|
-
return new SLParseError(raw.replace(/^\[onejs sl] /, ""), file, pos)
|
|
109
|
+
return new SLParseError(inFileWords(raw.replace(/^\[onejs sl] /, "")), file, pos)
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const fail: (message: string, pos: Pos, length?: number) => never =
|
|
@@ -158,7 +158,7 @@ export function lower(checked: Checked): Program {
|
|
|
158
158
|
for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
|
|
159
159
|
|
|
160
160
|
for (const c of unit.consts) {
|
|
161
|
-
const v = lowerExpr(c.init, global)
|
|
161
|
+
const v = fit(lowerExpr(c.init, global), TYPE_WIDTH[c.type], c.pos)
|
|
162
162
|
assertWidth(v, TYPE_WIDTH[c.type], c.name, c.type, c.pos)
|
|
163
163
|
global.declare(c.name, { width: TYPE_WIDTH[c.type], value: v })
|
|
164
164
|
}
|
|
@@ -227,6 +227,8 @@ export function lower(checked: Checked): Program {
|
|
|
227
227
|
init.pos,
|
|
228
228
|
)
|
|
229
229
|
}
|
|
230
|
+
if (c.length === 1 && width > 1) return { components: new Array<number>(width).fill(c[0]!), colour: false }
|
|
231
|
+
if (c.length === 4 && width === 3) return { components: c.slice(0, 3), colour: false }
|
|
230
232
|
if (c.length !== width) {
|
|
231
233
|
fail(
|
|
232
234
|
`this default has ${c.length} component${c.length === 1 ? "" : "s"} and the uniform is ` +
|
|
@@ -284,19 +286,25 @@ export function lower(checked: Checked): Program {
|
|
|
284
286
|
switch (s.k) {
|
|
285
287
|
case "var":
|
|
286
288
|
case "const": {
|
|
287
|
-
const v = lowerExpr(s.init, scope)
|
|
289
|
+
const v = fit(lowerExpr(s.init, scope), TYPE_WIDTH[s.type], s.pos)
|
|
288
290
|
assertWidth(v, TYPE_WIDTH[s.type], s.name, s.type, s.pos)
|
|
289
291
|
scope.declare(s.name, { width: TYPE_WIDTH[s.type], value: v })
|
|
290
292
|
break
|
|
291
293
|
}
|
|
292
294
|
case "assign": {
|
|
293
|
-
|
|
295
|
+
// The checker allowed exactly two shapes: a local, or a
|
|
296
|
+
// swizzle of one with no component named twice.
|
|
297
|
+
const member = s.target.k === "member" ? s.target : null
|
|
298
|
+
const name = ((member?.obj ?? s.target) as Extract<Expr, { k: "ident" }>).name
|
|
294
299
|
const b = scope.lookup(name)!
|
|
295
300
|
let v = lowerExpr(s.value, scope)
|
|
296
301
|
if (s.op !== "=") {
|
|
297
302
|
const op = s.op[0] as ArithOp
|
|
298
|
-
|
|
303
|
+
const old = member === null ? b.value : lowerExpr(member, scope)
|
|
304
|
+
v = at(s.pos, () => arithmetic(op, old, v, s.pos))
|
|
299
305
|
}
|
|
306
|
+
if (member !== null) v = writeComponents(b, member, v, s.pos)
|
|
307
|
+
else v = fit(v, b.width, s.pos)
|
|
300
308
|
assertWidth(v, b.width, name, widthType(b.width), s.pos)
|
|
301
309
|
b.value = v
|
|
302
310
|
break
|
|
@@ -381,13 +389,52 @@ export function lower(checked: Checked): Program {
|
|
|
381
389
|
return typeof a !== "number" && typeof b !== "number" && a.ref === b.ref && a.width === b.width
|
|
382
390
|
}
|
|
383
391
|
|
|
392
|
+
/**
|
|
393
|
+
* The two conversions a declaration or an assignment makes for the author
|
|
394
|
+
* (`Specs/SL_NEXT.md` 2b and 2c): a single number fills every component, as
|
|
395
|
+
* `float3(0.5)` would, and a float4 goes into a float3 by dropping its
|
|
396
|
+
* fourth, which is how a colour (a hex, a ramp, a texture sample) becomes
|
|
397
|
+
* an rgb. Every other width change is left for `assertWidth` to refuse.
|
|
398
|
+
*/
|
|
399
|
+
function fit(v: LV, want: SLType, pos: Pos): LV {
|
|
400
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
401
|
+
if (got === 1 && want > 1) return at(pos, () => compose(want, new Array<LV>(want).fill(v)))
|
|
402
|
+
if (got === 4 && want === 3) return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
|
|
403
|
+
return v
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* `p.x = v`, `c.rgb *= k`: the local rebuilt from the components the
|
|
408
|
+
* swizzle names, taken from `v`, and the rest kept from what it held. A
|
|
409
|
+
* single number fills every named component.
|
|
410
|
+
*/
|
|
411
|
+
function writeComponents(b: Binding, member: Extract<Expr, { k: "member" }>, v: LV, pos: Pos): LV {
|
|
412
|
+
const letters = member.name.replace(/[rgba]/g, (c) => "xyzw"["rgba".indexOf(c)]!)
|
|
413
|
+
const names = "xyzw".slice(0, b.width)
|
|
414
|
+
for (const c of letters) {
|
|
415
|
+
if (!names.includes(c)) {
|
|
416
|
+
fail(`${member.name} writes a component a ${widthType(b.width)} does not have`, member.pos)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
420
|
+
if (got !== 1 && got !== letters.length) {
|
|
421
|
+
fail(`${member.name} is ${letters.length} components and this is a ${widthType(got as SLType)}`, pos)
|
|
422
|
+
}
|
|
423
|
+
const parts: LV[] = [...names].map((c) => {
|
|
424
|
+
const j = letters.indexOf(c)
|
|
425
|
+
if (j < 0) return b.width === 1 ? b.value : at(pos, () => asVal(b.value).swz(c) as unknown as Val)
|
|
426
|
+
return got === 1 ? v : at(pos, () => asVal(v).swz("xyzw"[j]!) as unknown as Val)
|
|
427
|
+
})
|
|
428
|
+
return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
|
|
429
|
+
}
|
|
430
|
+
|
|
384
431
|
function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
|
|
385
432
|
const got = typeof v === "number" ? 1 : v.width
|
|
386
433
|
if (got === want) return
|
|
387
434
|
// A declaration is an assertion, not an input to inference
|
|
388
435
|
// (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
|
|
389
436
|
// than quietly promoting one to the other.
|
|
390
|
-
const hint = got
|
|
437
|
+
const hint = got > want ? ` Take the components you want with a swizzle, as in .${"xyzw".slice(0, want)}.` : ""
|
|
391
438
|
fail(`${name} is declared ${type} and this is a ${widthType(got as SLType)}.${hint}`, pos)
|
|
392
439
|
}
|
|
393
440
|
|
|
@@ -629,3 +676,17 @@ function widthType(w: SLType): TypeName {
|
|
|
629
676
|
return (["float", "float2", "float3", "float4"] as const)[w - 1]!
|
|
630
677
|
}
|
|
631
678
|
|
|
679
|
+
/**
|
|
680
|
+
* An EDSL message in the words of a `.sl` file.
|
|
681
|
+
*
|
|
682
|
+
* The EDSL names its types after its constructors, `sl.vec3`, so "cannot
|
|
683
|
+
* combine a vec3 with a vec2" is right for a TypeScript author and wrong for
|
|
684
|
+
* one who wrote float3 (`Specs/SL_NEXT.md` 5). One rewrite here, rather than a
|
|
685
|
+
* second set of messages, keeps both surfaces saying the same thing. A quoted
|
|
686
|
+
* name is the author's own and left as written.
|
|
687
|
+
*/
|
|
688
|
+
function inFileWords(message: string): string {
|
|
689
|
+
return message.split(/("[^"]*")/).map((part, i) =>
|
|
690
|
+
i % 2 === 1 ? part : part.replace(/\bsl\.vec([234])\(/g, "float$1(").replace(/\bvec([234])\b/g, "float$1"),
|
|
691
|
+
).join("")
|
|
692
|
+
}
|
package/src/lang/parser.ts
CHANGED
|
@@ -12,13 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
TYPE_WIDTH,
|
|
16
15
|
type AssignOp, type BinaryOp, type Expr, type FuncDecl, type Param, type Stmt,
|
|
17
16
|
type TextureDecl, type TypeName, type UniformDecl, type Unit,
|
|
18
17
|
} from "./ast"
|
|
19
|
-
import { SLParseError, tokenize, type Pos, type Token } from "./lexer"
|
|
20
|
-
|
|
21
|
-
const TYPE_NAMES = new Set(Object.keys(TYPE_WIDTH))
|
|
18
|
+
import { SLParseError, tokenize, type Pos, type SLFix, type Token } from "./lexer"
|
|
19
|
+
import { KEYWORD_SET, TYPE_SET } from "./words"
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
22
|
* Type spellings that exist in HLSL or GLSL and not here, each with the reason.
|
|
@@ -45,6 +43,11 @@ const NOT_A_TYPE: Record<string, string> = {
|
|
|
45
43
|
struct: "there are no structs",
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
/** The spellings above whose replacement is certain, so the error can offer it as a fix. */
|
|
47
|
+
const TYPE_FIX: Record<string, TypeName> = {
|
|
48
|
+
vec2: "float2", vec3: "float3", vec4: "float4", half: "float", double: "float", fixed: "float",
|
|
49
|
+
}
|
|
50
|
+
|
|
48
51
|
/** Binding power per binary operator, HLSL's. Higher binds tighter. */
|
|
49
52
|
const BINDING: Record<string, number> = {
|
|
50
53
|
"||": 1,
|
|
@@ -73,14 +76,21 @@ class Parser {
|
|
|
73
76
|
private at(text: string): boolean { const t = this.peek(); return t.kind !== "eof" && t.text === text }
|
|
74
77
|
private eat(text: string): boolean { if (this.at(text)) { this.i++; return true } return false }
|
|
75
78
|
|
|
76
|
-
private fail(message: string, at: Token = this.peek()): never {
|
|
77
|
-
|
|
79
|
+
private fail(message: string, at: Token = this.peek(), fix?: SLFix): never {
|
|
80
|
+
// The end of the file is no character, so an error there marks the last
|
|
81
|
+
// token instead of one past it, where an editor has nothing to underline.
|
|
82
|
+
if (at.kind === "eof" && this.tokens.length > 1) at = this.tokens[this.tokens.length - 2]!
|
|
83
|
+
throw new SLParseError(message, this.file, at as Pos, Math.max(1, at.text.length), fix)
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
private expect(text: string, what: string): Token {
|
|
81
87
|
if (!this.at(text)) {
|
|
82
88
|
const got = this.peek()
|
|
83
|
-
|
|
89
|
+
// Something missing at the end of a line is missing after the last
|
|
90
|
+
// token on it, which is where the author will look, not at the next line.
|
|
91
|
+
const prev = this.tokens[this.i - 1]
|
|
92
|
+
const at = prev !== undefined && got.line > prev.line ? prev : got
|
|
93
|
+
this.fail(`expected "${text}" ${what}, got ${describe(got)}`, at)
|
|
84
94
|
}
|
|
85
95
|
return this.next()
|
|
86
96
|
}
|
|
@@ -91,11 +101,21 @@ class Parser {
|
|
|
91
101
|
return this.next()
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/** A name being declared, which may not be a word the parser reads as syntax. */
|
|
105
|
+
private expectName(what: string): Token {
|
|
106
|
+
const t = this.expectIdent(what)
|
|
107
|
+
if (KEYWORD_SET.has(t.text)) this.fail(`"${t.text}" is a keyword, so it cannot be ${what}`, t)
|
|
108
|
+
if (TYPE_SET.has(t.text)) this.fail(`"${t.text}" is a type, so it cannot be ${what}`, t)
|
|
109
|
+
return t
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
private expectType(what: string): TypeName {
|
|
95
113
|
const t = this.expectIdent(what)
|
|
96
|
-
if (
|
|
114
|
+
if (TYPE_SET.has(t.text)) return t.text as TypeName
|
|
97
115
|
const why = NOT_A_TYPE[t.text]
|
|
98
|
-
|
|
116
|
+
const to = TYPE_FIX[t.text]
|
|
117
|
+
const fix = to === undefined ? undefined : { title: `Replace ${t.text} with ${to}`, replacement: to }
|
|
118
|
+
if (why !== undefined) this.fail(`"${t.text}" is not a type here: ${why}`, t, fix)
|
|
99
119
|
this.fail(`"${t.text}" is not a type; the types are float, float2, float3 and float4`, t)
|
|
100
120
|
}
|
|
101
121
|
|
|
@@ -146,7 +166,7 @@ class Parser {
|
|
|
146
166
|
private parseUniform(): UniformDecl {
|
|
147
167
|
const kw = this.next()
|
|
148
168
|
const type = this.expectType("a type after uniform")
|
|
149
|
-
const name = this.
|
|
169
|
+
const name = this.expectName("a uniform name")
|
|
150
170
|
let init: Expr | null = null
|
|
151
171
|
if (this.eat("=")) init = this.parseExpr()
|
|
152
172
|
this.expect(";", "after a uniform declaration")
|
|
@@ -155,7 +175,7 @@ class Parser {
|
|
|
155
175
|
|
|
156
176
|
private parseTexture(): TextureDecl {
|
|
157
177
|
const kw = this.next()
|
|
158
|
-
const name = this.
|
|
178
|
+
const name = this.expectName("a texture name")
|
|
159
179
|
if (this.at("=")) {
|
|
160
180
|
this.fail(
|
|
161
181
|
"a texture has no default; the host binds it by name through the textures prop",
|
|
@@ -169,7 +189,7 @@ class Parser {
|
|
|
169
189
|
private parseConst(): Extract<Stmt, { k: "const" }> {
|
|
170
190
|
const kw = this.next()
|
|
171
191
|
const type = this.expectType("a type after const")
|
|
172
|
-
const name = this.
|
|
192
|
+
const name = this.expectName("a const's name")
|
|
173
193
|
this.expect("=", "after a const's name; a const has to have a value")
|
|
174
194
|
const init = this.parseExpr()
|
|
175
195
|
this.expect(";", "after a const")
|
|
@@ -179,7 +199,7 @@ class Parser {
|
|
|
179
199
|
private parseFunction(): FuncDecl {
|
|
180
200
|
const start = this.peek()
|
|
181
201
|
const ret = this.expectType("a return type")
|
|
182
|
-
const name = this.
|
|
202
|
+
const name = this.expectName("a function name")
|
|
183
203
|
if (!this.at("(")) {
|
|
184
204
|
this.fail(
|
|
185
205
|
`expected "(" after ${name.text}. Only uniforms, textures, consts and functions live ` +
|
|
@@ -193,7 +213,7 @@ class Parser {
|
|
|
193
213
|
for (;;) {
|
|
194
214
|
const p = this.peek()
|
|
195
215
|
const ptype = this.expectType("a parameter type")
|
|
196
|
-
const pname = this.
|
|
216
|
+
const pname = this.expectName("a parameter name")
|
|
197
217
|
params.push({ type: ptype, name: pname.text, pos: p })
|
|
198
218
|
if (!this.eat(",")) break
|
|
199
219
|
}
|
|
@@ -238,6 +258,16 @@ class Parser {
|
|
|
238
258
|
if (t.text === "discard") {
|
|
239
259
|
this.fail("there is no discard; return a colour with alpha 0 instead", t)
|
|
240
260
|
}
|
|
261
|
+
if (t.text === "break" || t.text === "continue") {
|
|
262
|
+
this.fail(
|
|
263
|
+
`there is no ${t.text}: a for loop unrolls, so every iteration runs. Put the rest of ` +
|
|
264
|
+
`the body under an if instead`,
|
|
265
|
+
t,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
if (t.text === "switch") {
|
|
269
|
+
this.fail("there is no switch; write it as an if and else if", t)
|
|
270
|
+
}
|
|
241
271
|
if (t.text === "return") {
|
|
242
272
|
this.next()
|
|
243
273
|
const value = this.parseExpr()
|
|
@@ -253,7 +283,7 @@ class Parser {
|
|
|
253
283
|
// statement may be is the left of an assignment.
|
|
254
284
|
if (t.kind === "ident" && this.peek(1).kind === "ident") {
|
|
255
285
|
const type = this.expectType("a type")
|
|
256
|
-
const name = this.
|
|
286
|
+
const name = this.expectName("a local's name")
|
|
257
287
|
if (this.at("(")) {
|
|
258
288
|
this.fail(
|
|
259
289
|
"a function is declared at the top level of the file, not inside another function",
|
|
@@ -318,7 +348,7 @@ class Parser {
|
|
|
318
348
|
)
|
|
319
349
|
}
|
|
320
350
|
this.next()
|
|
321
|
-
const counter = this.
|
|
351
|
+
const counter = this.expectName("a counter name")
|
|
322
352
|
this.expect("=", "after the counter")
|
|
323
353
|
const from = this.parseExpr()
|
|
324
354
|
this.expect(";", "after the counter's start")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prelude's source, and what an editor reads from it without parsing it.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of `prelude.ts`, which parses it, so that `onejs-sl/tables` can offer
|
|
5
|
+
* the prelude's names and descriptions with no parser in the bundle. Each
|
|
6
|
+
* function's description is the comment above it, so there is one place to
|
|
7
|
+
* write it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const PRELUDE_SOURCE = `
|
|
11
|
+
// Rotate a point about the origin. Translate first if you want another centre.
|
|
12
|
+
float2 rotate(float2 p, float angle) {
|
|
13
|
+
float c = cos(angle);
|
|
14
|
+
float s = sin(angle);
|
|
15
|
+
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Cartesian to polar: x is the angle in radians, y is the radius.
|
|
19
|
+
float2 polar(float2 p) {
|
|
20
|
+
return float2(atan2(p.y, p.x), length(p));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Signed distance to a circle at the origin. Negative inside.
|
|
24
|
+
float circle(float2 p, float r) {
|
|
25
|
+
return length(p) - r;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Signed distance to an axis aligned box of the given half extents.
|
|
29
|
+
float box(float2 p, float2 size) {
|
|
30
|
+
float2 d = abs(p) - size;
|
|
31
|
+
return length(max(d, 0)) + min(max(d.x, d.y), 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Repeat a unit cell n times across the input, centred on zero in each cell.
|
|
35
|
+
float2 tile(float2 p, float n) {
|
|
36
|
+
return frac(p * n) - 0.5;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Inigo Quilez's cosine palette. Four float3s in, a colour out, no texture.
|
|
40
|
+
float3 palette(float t, float3 a, float3 b, float3 c, float3 d) {
|
|
41
|
+
return a + b * cos(6.28318530718 * (c * t + d));
|
|
42
|
+
}
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
/** Each prelude function's name, in the order the source declares them. */
|
|
46
|
+
const DECLARED = /* @__PURE__ */ [...PRELUDE_SOURCE.matchAll(/^\/\/ (.+)\n\w+ (\w+)\(/gm)]
|
|
47
|
+
|
|
48
|
+
export const PRELUDE_NAMES: ReadonlySet<string> = /* @__PURE__ */ new Set(DECLARED.map((m) => m[2]!))
|
|
49
|
+
|
|
50
|
+
/** Each prelude function's one line description: the comment above it. */
|
|
51
|
+
export const PRELUDE_DOCS: Readonly<Record<string, string>> = /* @__PURE__ */ Object.fromEntries(
|
|
52
|
+
DECLARED.map((m) => [m[2]!, m[1]!]),
|
|
53
|
+
)
|
package/src/lang/prelude.ts
CHANGED
|
@@ -21,41 +21,9 @@
|
|
|
21
21
|
import type { FuncDecl } from "./ast"
|
|
22
22
|
import { check } from "./check"
|
|
23
23
|
import { parseUnit } from "./parser"
|
|
24
|
+
import { PRELUDE_SOURCE } from "./prelude-source"
|
|
24
25
|
|
|
25
|
-
export
|
|
26
|
-
// Rotate a point about the origin. Translate first if you want another centre.
|
|
27
|
-
float2 rotate(float2 p, float angle) {
|
|
28
|
-
float c = cos(angle);
|
|
29
|
-
float s = sin(angle);
|
|
30
|
-
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Cartesian to polar: x is the angle in radians, y is the radius.
|
|
34
|
-
float2 polar(float2 p) {
|
|
35
|
-
return float2(atan2(p.y, p.x), length(p));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Signed distance to a circle at the origin. Negative inside.
|
|
39
|
-
float circle(float2 p, float r) {
|
|
40
|
-
return length(p) - r;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Signed distance to an axis aligned box of the given half extents.
|
|
44
|
-
float box(float2 p, float2 size) {
|
|
45
|
-
float2 d = abs(p) - size;
|
|
46
|
-
return length(max(d, 0)) + min(max(d.x, d.y), 0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Repeat a unit cell n times across the input, centred on zero in each cell.
|
|
50
|
-
float2 tile(float2 p, float n) {
|
|
51
|
-
return frac(p * n) - 0.5;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Inigo Quilez's cosine palette. Four float3s in, a colour out, no texture.
|
|
55
|
-
float3 palette(float t, float3 a, float3 b, float3 c, float3 d) {
|
|
56
|
-
return a + b * cos(6.28318530718 * (c * t + d));
|
|
57
|
-
}
|
|
58
|
-
`
|
|
26
|
+
export { PRELUDE_SOURCE }
|
|
59
27
|
|
|
60
28
|
let cached: FuncDecl[] | null = null
|
|
61
29
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words the parser reads as syntax, and the value types.
|
|
3
|
+
*
|
|
4
|
+
* One list, read by the parser (a name may not be one of these) and by
|
|
5
|
+
* `classify` (these highlight as keywords and types), so an editor that
|
|
6
|
+
* highlights with it agrees with the parser by construction rather than by a
|
|
7
|
+
* list it keeps in step by hand.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TYPE_WIDTH, type TypeName } from "./ast"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Declaration and statement words. `int` is here rather than among the types
|
|
14
|
+
* because it is only ever a for loop's counter, `for (int i = 0; i < 4; i++)`;
|
|
15
|
+
* a value cannot be declared as one.
|
|
16
|
+
*/
|
|
17
|
+
export const SL_KEYWORDS = ["uniform", "texture2D", "const", "if", "else", "for", "return", "int"] as const
|
|
18
|
+
|
|
19
|
+
/** The types a value, a uniform or a function can have. */
|
|
20
|
+
export const SL_TYPES = Object.keys(TYPE_WIDTH) as readonly TypeName[]
|
|
21
|
+
|
|
22
|
+
export const KEYWORD_SET: ReadonlySet<string> = new Set(SL_KEYWORDS)
|
|
23
|
+
export const TYPE_SET: ReadonlySet<string> = new Set(SL_TYPES)
|
package/src/params.ts
CHANGED
|
@@ -20,6 +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
24
|
|
|
24
25
|
/** One function of the helper library, as its source declares it. */
|
|
25
26
|
export interface LibSignature {
|
|
@@ -111,3 +112,63 @@ function shapeNames(): Record<SlSdfKind, readonly string[]> {
|
|
|
111
112
|
}
|
|
112
113
|
return out
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* One line on what each builtin does, for a completion's tooltip and the docs
|
|
118
|
+
* page: the other half of `BUILTIN_PARAMS`, and named in its words, so a line
|
|
119
|
+
* can say `s` and mean the third argument.
|
|
120
|
+
*/
|
|
121
|
+
export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
122
|
+
sin: "The sine of x, in radians.",
|
|
123
|
+
cos: "The cosine of x, in radians.",
|
|
124
|
+
tan: "The tangent of x, in radians.",
|
|
125
|
+
asin: "The angle whose sine is x, with x clamped to -1 to 1.",
|
|
126
|
+
acos: "The angle whose cosine is x, with x clamped to -1 to 1.",
|
|
127
|
+
exp: "e raised to the power x.",
|
|
128
|
+
log: "The natural logarithm of x.",
|
|
129
|
+
sqrt: "The square root of x.",
|
|
130
|
+
sign: "-1, 0 or 1, by the sign of x.",
|
|
131
|
+
ceil: "x rounded up to a whole number.",
|
|
132
|
+
round: "x rounded to the nearest whole number.",
|
|
133
|
+
toLinear: "A colour as written, in sRGB, converted to the space the target holds; alpha is left alone.",
|
|
134
|
+
abs: "x without its sign.",
|
|
135
|
+
floor: "x rounded down to a whole number.",
|
|
136
|
+
frac: "The part of x after the decimal point, 0 up to 1.",
|
|
137
|
+
saturate: "x clamped to 0 to 1.",
|
|
138
|
+
rcp: "1 / x.",
|
|
139
|
+
normalize: "x scaled to length 1.",
|
|
140
|
+
pow: "x raised to the power y.",
|
|
141
|
+
min: "The smaller of x and y, per component.",
|
|
142
|
+
max: "The larger of x and y, per component.",
|
|
143
|
+
clamp: "x held between min and max.",
|
|
144
|
+
atan2: "The angle of the point (x, y) from the positive x axis, in radians.",
|
|
145
|
+
length: "The length of the vector x.",
|
|
146
|
+
distance: "The distance between the points x and y.",
|
|
147
|
+
dot: "The dot product of x and y.",
|
|
148
|
+
cross: "The cross product of two float3s.",
|
|
149
|
+
reflect: "The direction i reflected off a surface with normal n.",
|
|
150
|
+
lerp: "Mixes x into y by s: x at 0, y at 1.",
|
|
151
|
+
step: "0 where x is below y, else 1.",
|
|
152
|
+
smoothstep: "0 below min, 1 above max, and a smooth curve between.",
|
|
153
|
+
remap: "v moved from the range fromMin to fromMax into the range toMin to toMax.",
|
|
154
|
+
hsv2rgb: "A colour from hue, saturation and value, each 0 to 1.",
|
|
155
|
+
luminance: "How bright a colour looks, as one number.",
|
|
156
|
+
noise: "Smooth value noise at p, about 0 to 1. Offset p for a different field.",
|
|
157
|
+
simplex: "Simplex noise at p, about 0 to 1, with no grid pattern.",
|
|
158
|
+
voronoi: "The distance from p to the nearest point of a jittered grid: cells.",
|
|
159
|
+
fbm: "Value noise layered in octaves, 1 to 4 (3 when left out): clouds and terrain.",
|
|
160
|
+
turbulence: "Simplex creases layered in octaves, 1 to 4: fire, smoke, marble.",
|
|
161
|
+
ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
|
|
162
|
+
tex2D: "The colour of texture s at the uv t, with straight alpha.",
|
|
163
|
+
sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
|
|
164
|
+
ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
|
|
165
|
+
}
|
|
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.",
|
|
174
|
+
}
|
package/src/tables.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* `onejs-sl/tables`: what completion and highlighting read.
|
|
3
3
|
*
|
|
4
4
|
* The same tables the checker and the emitters use, so an editor offers
|
|
5
|
-
* exactly the builtins that compile, with the arities they take
|
|
6
|
-
*
|
|
5
|
+
* exactly the builtins that compile, with the arities they take, the names of
|
|
6
|
+
* their parameters and a line on each (`params.ts`), the words the parser
|
|
7
|
+
* reads as syntax, and the helper library's signatures. `classify`, in the
|
|
8
|
+
* main entry, is the highlighter built on the same lists.
|
|
7
9
|
*/
|
|
8
10
|
export { BUILTINS, NOT_YET } from "./lang/builtins"
|
|
9
11
|
export type { Builtin } from "./lang/builtins"
|
|
@@ -13,6 +15,10 @@ export {
|
|
|
13
15
|
export type { SLSurface } from "./ops"
|
|
14
16
|
export { INPUTS } from "./ir"
|
|
15
17
|
export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
|
|
16
|
-
export { BUILTIN_PARAMS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
|
|
18
|
+
export { BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
|
|
19
|
+
export { PRELUDE_DOCS, PRELUDE_NAMES } from "./lang/prelude-source"
|
|
20
|
+
export { SL_KEYWORDS, SL_TYPES } from "./lang/words"
|
|
21
|
+
export { TYPE_WIDTH } from "./lang/ast"
|
|
22
|
+
export type { TypeName } from "./lang/ast"
|
|
17
23
|
export type { LibSignature } from "./params"
|
|
18
24
|
export type { SlSdfKind } from "./shapes"
|