onejs-sl 0.1.4 → 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 +16 -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 +30 -5
- package/src/lang/check.ts +33 -15
- 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 +15 -1
- 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,21 @@
|
|
|
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
|
+
|
|
3
19
|
## 0.1.4
|
|
4
20
|
|
|
5
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.
|
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
|
@@ -168,11 +168,36 @@ 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
|
+
## 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`.
|
|
176
201
|
|
|
177
202
|
## See also
|
|
178
203
|
|
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. */
|
|
@@ -65,9 +77,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
65
77
|
// terminating control flow when the callee's `never` comes from an explicit
|
|
66
78
|
// type, so without this every `if (x.k !== "ident") fail(...)` below would
|
|
67
79
|
// 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)
|
|
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)
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
const funcs = new Map<string, FuncDecl>()
|
|
@@ -316,9 +328,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
316
328
|
target.pos, n.length,
|
|
317
329
|
)
|
|
318
330
|
}
|
|
319
|
-
if (!scope.has(n))
|
|
320
|
-
fail(unknown(n, scope), target.pos, n.length)
|
|
321
|
-
}
|
|
331
|
+
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
322
332
|
checkExpr(fn, s.value, scope)
|
|
323
333
|
break
|
|
324
334
|
}
|
|
@@ -382,7 +392,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
382
392
|
if (funcs.has(e.name) || BUILTINS[e.name] !== undefined) {
|
|
383
393
|
fail(`${e.name} is a function; call it, as in \`${e.name}(...)\``, e.pos, e.name.length)
|
|
384
394
|
}
|
|
385
|
-
|
|
395
|
+
glsl(e.name, e.pos)
|
|
396
|
+
unknown(e.name, scope, e.pos)
|
|
386
397
|
break
|
|
387
398
|
}
|
|
388
399
|
case "member":
|
|
@@ -493,10 +504,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
493
504
|
callee.pos, n.length,
|
|
494
505
|
)
|
|
495
506
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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)
|
|
500
517
|
}
|
|
501
518
|
|
|
502
519
|
function arity(e: Extract<Expr, { k: "call" }>, n: string, min: number, max: number): void {
|
|
@@ -505,13 +522,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
505
522
|
fail(`${n} takes ${want} argument${max === 1 ? "" : "s"}, got ${e.args.length}`, e.pos)
|
|
506
523
|
}
|
|
507
524
|
|
|
508
|
-
/**
|
|
509
|
-
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 {
|
|
510
527
|
const near = nearest(n, [
|
|
511
528
|
...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
512
529
|
...funcs.keys(), ...Object.keys(BUILTINS),
|
|
513
530
|
])
|
|
514
|
-
|
|
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))
|
|
515
533
|
}
|
|
516
534
|
|
|
517
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 =
|
|
@@ -676,3 +676,17 @@ function widthType(w: SLType): TypeName {
|
|
|
676
676
|
return (["float", "float2", "float3", "float4"] as const)[w - 1]!
|
|
677
677
|
}
|
|
678
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"
|