onejs-sl 0.1.4 → 0.1.6
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 +23 -0
- package/README.md +8 -4
- package/package.json +1 -1
- package/src/body.ts +20 -2
- package/src/index.ts +5 -3
- package/src/lang/README.md +43 -5
- package/src/lang/check.ts +123 -60
- package/src/lang/classify.ts +70 -0
- package/src/lang/index.ts +41 -2
- package/src/lang/lexer.ts +72 -15
- package/src/lang/lower.ts +88 -26
- package/src/lang/parser.ts +128 -43
- 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,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
`diagnose(source)` returns every error in a file, for an editor checking as it is typed (`Specs/SL_NEXT.md` 5). Nothing a program compiles to changes.
|
|
6
|
+
|
|
7
|
+
- `diagnose` finds a mistake in each statement and declaration, in source order, and never throws one
|
|
8
|
+
- A refused local is still declared, so its uses are not reported again
|
|
9
|
+
|
|
10
|
+
## 0.1.5
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
- `classify` returns every token with its class, comments included, and never throws
|
|
15
|
+
- `SL_KEYWORDS`, `SL_TYPES` and `TYPE_WIDTH` are exported from `onejs-sl/tables`
|
|
16
|
+
- `BUILTIN_DOCS`, `INPUT_DOCS` and `PRELUDE_DOCS` describe every builtin, input and prelude function in a line
|
|
17
|
+
- `SLParseError` carries `text`, its message alone, and `offset`
|
|
18
|
+
- An error carries a one click `fix` where the fix is certain: a GLSL rename or a "did you mean"
|
|
19
|
+
- A GLSL name used as a value, such as `gl_FragCoord`, gets the HLSL name
|
|
20
|
+
- `break`, `continue` and `switch` say why they are not there
|
|
21
|
+
- A keyword or a type cannot name a value, a function or a parameter
|
|
22
|
+
- Something missing at the end of a line or the file is marked on the last token before it
|
|
23
|
+
- Errors from inside the EDSL say `float3`, not `vec3`
|
|
24
|
+
- `BodyTarget.sample` documents what the body expects of a sample
|
|
25
|
+
|
|
3
26
|
## 0.1.4
|
|
4
27
|
|
|
5
28
|
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.
|
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`, `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) |
|
|
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.
|
|
@@ -175,7 +179,7 @@ generated HLSL after an eject, with no edit in between. Both backends exist:
|
|
|
175
179
|
and `hlsl.ts` feeds its `Editor/SLShaderGenerator.cs`. Nobody writes a manifest for the
|
|
176
180
|
second: an editor that interprets a program asks the encoded program for its
|
|
177
181
|
`hlsl` (a lazy getter, never read in Play), records it into
|
|
178
|
-
`Assets/OneJS
|
|
182
|
+
`Assets/OneJS/Recorded.sl.json`, generates the shader and
|
|
179
183
|
moves the live material onto it. `manifest()` is still there for an app that
|
|
180
184
|
would rather write its programs out at build time.
|
|
181
185
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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, diagnose, 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, diagnose, 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
|
@@ -168,11 +168,49 @@ lowering it changes recorded behaviour rather than parser behaviour.
|
|
|
168
168
|
|
|
169
169
|
## Errors
|
|
170
170
|
|
|
171
|
-
Every one carries file, line
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
## Several errors at once
|
|
190
|
+
|
|
191
|
+
`parse` throws the first error; `diagnose(source)` returns all of them, in
|
|
192
|
+
source order, and never throws one, for an editor checking as the file is
|
|
193
|
+
typed. Each stage carries on past a mistake: the parser skips to the next
|
|
194
|
+
statement or declaration, the checker and the lowering move to the next one,
|
|
195
|
+
and a local whose line was refused is still declared, as a zero of its type, so
|
|
196
|
+
its uses are not reported again. A later stage runs only when the earlier ones
|
|
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
|
|
199
|
+
matters, that whatever `parse` refuses a file with, `diagnose` reports at the
|
|
200
|
+
same place, over every program one token short of a real one.
|
|
201
|
+
|
|
202
|
+
## For an editor
|
|
203
|
+
|
|
204
|
+
`classify(source)` is every token with its class (keyword, type, builtin,
|
|
205
|
+
input, prelude, number, hex, comment, punct, member, ident, or invalid), built
|
|
206
|
+
on the parser's own scanner and word lists, so the highlighting is the parser's.
|
|
207
|
+
It never throws, since it runs on every keystroke. It classifies by spelling,
|
|
208
|
+
not scope: a local named `circle` still reads as a builtin where it is used.
|
|
209
|
+
|
|
210
|
+
`SL_KEYWORDS` and `SL_TYPES` (`words.ts`) are the lists the parser reads, and a
|
|
211
|
+
name may be neither. `onejs-sl/tables` carries them with a one line description
|
|
212
|
+
of every builtin, input and prelude function; a prelude function's is the
|
|
213
|
+
comment above it in `prelude-source.ts`.
|
|
176
214
|
|
|
177
215
|
## See also
|
|
178
216
|
|
package/src/lang/check.ts
CHANGED
|
@@ -34,10 +34,22 @@ import { SL_GLSL_HINT } from "../ops"
|
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
35
35
|
import { SL_SDF_SHAPES } from "../shapes"
|
|
36
36
|
import type { Expr, FuncDecl, Stmt, Unit } from "./ast"
|
|
37
|
-
import { SLParseError, type Pos } from "./lexer"
|
|
37
|
+
import { SLParseError, type Pos, type SLFix } from "./lexer"
|
|
38
38
|
|
|
39
39
|
const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
40
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
|
+
|
|
41
53
|
export interface Checked {
|
|
42
54
|
unit: Unit
|
|
43
55
|
/** Every callable function by name, the file's shadowing the prelude's. */
|
|
@@ -57,6 +69,8 @@ export interface Checked {
|
|
|
57
69
|
export interface CheckOptions {
|
|
58
70
|
/** Off for the prelude, which is functions only. See `parseUnit`. */
|
|
59
71
|
requireMain?: boolean
|
|
72
|
+
/** Where to collect errors instead of throwing the first. See `diagnose`. */
|
|
73
|
+
errors?: SLParseError[]
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {}): Checked {
|
|
@@ -65,10 +79,27 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
65
79
|
// terminating control flow when the callee's `never` comes from an explicit
|
|
66
80
|
// type, so without this every `if (x.k !== "ident") fail(...)` below would
|
|
67
81
|
// fail to narrow.
|
|
68
|
-
const fail: (message: string, pos: Pos, length?: number) => never =
|
|
69
|
-
(message, pos, length = 1) => {
|
|
70
|
-
throw new SLParseError(message, file, pos, length)
|
|
82
|
+
const fail: (message: string, pos: Pos, length?: number, fix?: SLFix) => never =
|
|
83
|
+
(message, pos, length = 1, fix) => {
|
|
84
|
+
throw new SLParseError(message, file, pos, length, fix)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One declaration's or one statement's checks. Collecting, an error is
|
|
89
|
+
* recorded and the next one is checked; the name it declares is still
|
|
90
|
+
* declared, so a later use of it is not a second, false error.
|
|
91
|
+
*/
|
|
92
|
+
const attempt = (check: () => void): boolean => {
|
|
93
|
+
if (options.errors === undefined) { check(); return true }
|
|
94
|
+
try {
|
|
95
|
+
check()
|
|
96
|
+
return true
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (!(e instanceof SLParseError)) throw e
|
|
99
|
+
options.errors.push(e)
|
|
100
|
+
return false
|
|
71
101
|
}
|
|
102
|
+
}
|
|
72
103
|
|
|
73
104
|
const funcs = new Map<string, FuncDecl>()
|
|
74
105
|
for (const fn of prelude) funcs.set(fn.name, fn)
|
|
@@ -114,11 +145,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
for (const u of unit.uniforms) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
attempt(() => {
|
|
149
|
+
const clash = valueClash(u.name)
|
|
150
|
+
if (clash !== null) fail(`"${u.name}" already names ${clash}`, u.pos, u.name.length)
|
|
151
|
+
})
|
|
152
|
+
if (!uniforms.has(u.name)) uniforms.set(u.name, u)
|
|
120
153
|
}
|
|
121
|
-
|
|
154
|
+
attempt(() => {
|
|
155
|
+
if (uniforms.size <= VM_UNIFORMS) return
|
|
122
156
|
const over = unit.uniforms[VM_UNIFORMS]!
|
|
123
157
|
fail(
|
|
124
158
|
`this file declares ${uniforms.size} uniforms and a program may hold ${VM_UNIFORMS}. ` +
|
|
@@ -127,14 +161,17 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
127
161
|
`a float4.`,
|
|
128
162
|
over.pos, over.name.length,
|
|
129
163
|
)
|
|
130
|
-
}
|
|
164
|
+
})
|
|
131
165
|
|
|
132
166
|
for (const t of unit.textures) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
167
|
+
attempt(() => {
|
|
168
|
+
const clash = valueClash(t.name)
|
|
169
|
+
if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
|
|
170
|
+
})
|
|
171
|
+
if (!textures.has(t.name)) textures.set(t.name, t)
|
|
136
172
|
}
|
|
137
|
-
|
|
173
|
+
attempt(() => {
|
|
174
|
+
if (textures.size <= VM_TEXTURES) return
|
|
138
175
|
const over = unit.textures[VM_TEXTURES]!
|
|
139
176
|
fail(
|
|
140
177
|
`this file declares ${textures.size} textures and a program may sample ${VM_TEXTURES}. ` +
|
|
@@ -142,24 +179,28 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
142
179
|
`and its own after an eject, which is two different pictures from one file.`,
|
|
143
180
|
over.pos, over.name.length,
|
|
144
181
|
)
|
|
145
|
-
}
|
|
182
|
+
})
|
|
146
183
|
|
|
147
184
|
for (const c of unit.consts) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
185
|
+
attempt(() => {
|
|
186
|
+
const clash = valueClash(c.name)
|
|
187
|
+
if (clash !== null) fail(`"${c.name}" already names ${clash}`, c.pos, c.name.length)
|
|
188
|
+
})
|
|
189
|
+
if (!consts.has(c.name)) consts.set(c.name, c)
|
|
151
190
|
}
|
|
152
191
|
|
|
153
192
|
for (const fn of unit.funcs) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
193
|
+
attempt(() => {
|
|
194
|
+
const clash = taken(fn.name)
|
|
195
|
+
if (clash !== null) fail(`"${fn.name}" already names ${clash}`, fn.pos, fn.name.length)
|
|
196
|
+
if (unit.funcs.filter((f) => f.name === fn.name).length > 1) {
|
|
197
|
+
fail(
|
|
198
|
+
`this file declares ${fn.name} more than once. There is no overloading: a function ` +
|
|
199
|
+
`inlines, so two bodies under one name have nothing to pick between them`,
|
|
200
|
+
fn.pos, fn.name.length,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
163
204
|
funcs.set(fn.name, fn)
|
|
164
205
|
}
|
|
165
206
|
|
|
@@ -167,19 +208,23 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
167
208
|
if (main === null) {
|
|
168
209
|
if (options.requireMain ?? true) throw new Error("check() reached a unit with no main")
|
|
169
210
|
for (const fn of unit.funcs) checkFunction(fn)
|
|
170
|
-
refuseRecursion
|
|
211
|
+
attempt(refuseRecursion)
|
|
171
212
|
return { unit, funcs, uniforms, textures, consts }
|
|
172
213
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
attempt(() => {
|
|
215
|
+
if (main.ret !== "float4") {
|
|
216
|
+
fail(`main returns a colour, so it is declared \`float4 main()\`, not ${main.ret}`, main.pos)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
attempt(() => {
|
|
220
|
+
if (main.params.length > 0) {
|
|
221
|
+
fail(
|
|
222
|
+
"main takes no parameters: what a program is given are the free identifiers uv, " +
|
|
223
|
+
"fragCoord, resolution, time and aspect",
|
|
224
|
+
main.params[0]!.pos,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
183
228
|
|
|
184
229
|
// MARK: bodies
|
|
185
230
|
|
|
@@ -189,7 +234,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
189
234
|
// which is not the author's mistake and not a name they can see.
|
|
190
235
|
for (const fn of [...unit.funcs, main]) checkFunction(fn)
|
|
191
236
|
|
|
192
|
-
refuseRecursion
|
|
237
|
+
attempt(refuseRecursion)
|
|
193
238
|
|
|
194
239
|
/**
|
|
195
240
|
* A function inlines, so a cycle is not slow, it is infinite.
|
|
@@ -260,9 +305,11 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
260
305
|
function checkFunction(fn: FuncDecl): void {
|
|
261
306
|
const params = new Set<string>()
|
|
262
307
|
for (const p of fn.params) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
308
|
+
attempt(() => {
|
|
309
|
+
const clash = valueClash(p.name)
|
|
310
|
+
if (clash !== null) fail(`"${p.name}" already names ${clash}`, p.pos, p.name.length)
|
|
311
|
+
if (params.has(p.name)) fail(`${fn.name} already has a parameter called "${p.name}"`, p.pos)
|
|
312
|
+
})
|
|
266
313
|
params.add(p.name)
|
|
267
314
|
}
|
|
268
315
|
checkBody(fn, fn.body, new Set(params), true)
|
|
@@ -279,8 +326,20 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
279
326
|
let returned: Stmt | null = null
|
|
280
327
|
for (const s of body) {
|
|
281
328
|
if (returned !== null) {
|
|
282
|
-
|
|
329
|
+
// Once: everything after it is the same mistake.
|
|
330
|
+
attempt(() => fail("this is after the return, so it can never run", s.pos))
|
|
331
|
+
break
|
|
283
332
|
}
|
|
333
|
+
attempt(() => checkStmt(s))
|
|
334
|
+
if (s.k === "var" || s.k === "const") scope.add(s.name)
|
|
335
|
+
// Only the outermost return ends a body; one inside an if is refused as that.
|
|
336
|
+
if (s.k === "return" && outermost) returned = s
|
|
337
|
+
}
|
|
338
|
+
if (outermost && returned === null) {
|
|
339
|
+
attempt(() => fail(`${fn.name} never returns a ${fn.ret}`, fn.pos, fn.name.length))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function checkStmt(s: Stmt): void {
|
|
284
343
|
switch (s.k) {
|
|
285
344
|
case "var":
|
|
286
345
|
case "const": {
|
|
@@ -288,7 +347,6 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
288
347
|
if (clash !== null) fail(`"${s.name}" already names ${clash}`, s.pos, s.name.length)
|
|
289
348
|
if (scope.has(s.name)) fail(`"${s.name}" is already declared in this body`, s.pos)
|
|
290
349
|
checkExpr(fn, s.init, scope)
|
|
291
|
-
scope.add(s.name)
|
|
292
350
|
break
|
|
293
351
|
}
|
|
294
352
|
case "assign": {
|
|
@@ -316,9 +374,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
316
374
|
target.pos, n.length,
|
|
317
375
|
)
|
|
318
376
|
}
|
|
319
|
-
if (!scope.has(n))
|
|
320
|
-
fail(unknown(n, scope), target.pos, n.length)
|
|
321
|
-
}
|
|
377
|
+
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
322
378
|
checkExpr(fn, s.value, scope)
|
|
323
379
|
break
|
|
324
380
|
}
|
|
@@ -337,8 +393,11 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
337
393
|
const inner = new Set(scope)
|
|
338
394
|
inner.add(s.counter)
|
|
339
395
|
counters.add(s.counter)
|
|
340
|
-
|
|
341
|
-
|
|
396
|
+
try {
|
|
397
|
+
checkBody(fn, s.body, inner, false)
|
|
398
|
+
} finally {
|
|
399
|
+
counters.delete(s.counter)
|
|
400
|
+
}
|
|
342
401
|
break
|
|
343
402
|
}
|
|
344
403
|
case "return":
|
|
@@ -351,13 +410,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
351
410
|
)
|
|
352
411
|
}
|
|
353
412
|
checkExpr(fn, s.value, scope)
|
|
354
|
-
returned = s
|
|
355
413
|
break
|
|
356
414
|
}
|
|
357
415
|
}
|
|
358
|
-
if (outermost && returned === null) {
|
|
359
|
-
fail(`${fn.name} never returns a ${fn.ret}`, fn.pos, fn.name.length)
|
|
360
|
-
}
|
|
361
416
|
}
|
|
362
417
|
|
|
363
418
|
// MARK: expressions
|
|
@@ -382,7 +437,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
382
437
|
if (funcs.has(e.name) || BUILTINS[e.name] !== undefined) {
|
|
383
438
|
fail(`${e.name} is a function; call it, as in \`${e.name}(...)\``, e.pos, e.name.length)
|
|
384
439
|
}
|
|
385
|
-
|
|
440
|
+
glsl(e.name, e.pos)
|
|
441
|
+
unknown(e.name, scope, e.pos)
|
|
386
442
|
break
|
|
387
443
|
}
|
|
388
444
|
case "member":
|
|
@@ -493,10 +549,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
493
549
|
callee.pos, n.length,
|
|
494
550
|
)
|
|
495
551
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
552
|
+
glsl(n, callee.pos, e.args.length)
|
|
553
|
+
unknown(n, scope, callee.pos)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Refuses a GLSL spelling by its HLSL name, with the fix where the rename is certain. */
|
|
557
|
+
function glsl(n: string, pos: Pos, args?: number): void {
|
|
558
|
+
const hint = SL_GLSL_HINT[n]
|
|
559
|
+
if (hint === undefined) return
|
|
560
|
+
const certain = GLSL_RENAMES.has(n) || (n === "atan" && args === 2)
|
|
561
|
+
fail(`${n} is GLSL; this is HLSL, so write ${hint}`, pos, n.length, certain ? rename(n, hint) : undefined)
|
|
500
562
|
}
|
|
501
563
|
|
|
502
564
|
function arity(e: Extract<Expr, { k: "call" }>, n: string, min: number, max: number): void {
|
|
@@ -505,13 +567,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
505
567
|
fail(`${n} takes ${want} argument${max === 1 ? "" : "s"}, got ${e.args.length}`, e.pos)
|
|
506
568
|
}
|
|
507
569
|
|
|
508
|
-
/**
|
|
509
|
-
function unknown(n: string, scope: Set<string
|
|
570
|
+
/** Refuses a name nothing declares, offering the nearest one spelled almost like it. */
|
|
571
|
+
function unknown(n: string, scope: Set<string>, pos: Pos): never {
|
|
510
572
|
const near = nearest(n, [
|
|
511
573
|
...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
512
574
|
...funcs.keys(), ...Object.keys(BUILTINS),
|
|
513
575
|
])
|
|
514
|
-
|
|
576
|
+
if (near === null) fail(`"${n}" is not declared`, pos, n.length)
|
|
577
|
+
fail(`"${n}" is not declared; did you mean ${near}?`, pos, n.length, rename(n, near))
|
|
515
578
|
}
|
|
516
579
|
|
|
517
580
|
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
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import type { Program } from "../ir"
|
|
21
21
|
import { check, type Checked } from "./check"
|
|
22
|
+
import { SLParseError } from "./lexer"
|
|
22
23
|
import { lower } from "./lower"
|
|
23
24
|
import { parseUnit, type ParseOptions } from "./parser"
|
|
24
25
|
import { preludeFunctions } from "./prelude"
|
|
@@ -29,9 +30,12 @@ export type {
|
|
|
29
30
|
Expr, FuncDecl, Param, Stmt, TextureDecl, TypeName, UniformDecl, Unit,
|
|
30
31
|
} from "./ast"
|
|
31
32
|
export { TYPE_WIDTH } from "./ast"
|
|
32
|
-
export { SLParseError }
|
|
33
|
-
export type { Pos, Token } from "./lexer"
|
|
33
|
+
export { SLParseError }
|
|
34
|
+
export type { Pos, SLFix, Token } from "./lexer"
|
|
34
35
|
export { tokenize } from "./lexer"
|
|
36
|
+
export { classify } from "./classify"
|
|
37
|
+
export type { SLClassifiedToken, SLTokenClass } from "./classify"
|
|
38
|
+
export { SL_KEYWORDS, SL_TYPES } from "./words"
|
|
35
39
|
export { parseUnit } from "./parser"
|
|
36
40
|
export { PRELUDE_SOURCE, preludeFunctions } from "./prelude"
|
|
37
41
|
export { BUILTINS, NOT_YET } from "./builtins"
|
|
@@ -53,3 +57,38 @@ export function analyze(source: string, options: ParseOptions = {}): Checked {
|
|
|
53
57
|
const unit = parseUnit(source, options)
|
|
54
58
|
return check(unit, preludeFunctions(), { requireMain: options.requireMain })
|
|
55
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Every error in a file, in source order, for an editor checking as it is
|
|
63
|
+
* typed. Empty when the file compiles. Never throws a `SLParseError`.
|
|
64
|
+
*
|
|
65
|
+
* Errors of one kind at a time, the first kind the file has: what cannot be
|
|
66
|
+
* read, then names and shapes (`check`), then types and widths (lowering).
|
|
67
|
+
* Each stage carries on past an error, a statement or a declaration at a time;
|
|
68
|
+
* a later stage runs only on a file the earlier ones passed, since its errors
|
|
69
|
+
* about a file with a hole in it would be about the hole.
|
|
70
|
+
*/
|
|
71
|
+
export function diagnose(source: string, options: ParseOptions = {}): SLParseError[] {
|
|
72
|
+
const errors: SLParseError[] = []
|
|
73
|
+
try {
|
|
74
|
+
const unit = parseUnit(source, { ...options, errors })
|
|
75
|
+
if (errors.length === 0) {
|
|
76
|
+
const checked = check(unit, preludeFunctions(), { requireMain: options.requireMain, errors })
|
|
77
|
+
if (errors.length === 0) lower(checked, errors)
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// The lexer's, which stops at the first character it cannot read.
|
|
81
|
+
if (!(e instanceof SLParseError)) throw e
|
|
82
|
+
errors.push(e)
|
|
83
|
+
}
|
|
84
|
+
// An error in a loop body or an inlined function is found once per copy.
|
|
85
|
+
const seen = new Set<string>()
|
|
86
|
+
return errors
|
|
87
|
+
.filter((e) => {
|
|
88
|
+
const key = `${e.offset}:${e.text}`
|
|
89
|
+
if (seen.has(key)) return false
|
|
90
|
+
seen.add(key)
|
|
91
|
+
return true
|
|
92
|
+
})
|
|
93
|
+
.sort((a, b) => a.offset - b.offset)
|
|
94
|
+
}
|