onejs-sl 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +34 -2
- package/goldens/goldens.json +9 -1
- package/package.json +1 -1
- package/src/core.ts +2 -1
- package/src/ir.ts +132 -3
- package/src/lang/README.md +38 -5
- package/src/lang/ast.ts +17 -0
- package/src/lang/attributes.ts +116 -0
- package/src/lang/check.ts +17 -6
- package/src/lang/classify.ts +7 -3
- package/src/lang/glsl.ts +280 -0
- package/src/lang/lexer.ts +23 -1
- package/src/lang/lower.ts +32 -9
- package/src/lang/parser.ts +49 -4
- package/src/params.ts +21 -4
- package/src/serial.ts +22 -2
- package/src/sl.ts +39 -20
- package/src/tables.ts +5 -2
package/src/lang/classify.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* wants that should ask it.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { SOURCE_INPUTS } from "../ir"
|
|
17
17
|
import { BUILTINS } from "./builtins"
|
|
18
18
|
import { lex } from "./lexer"
|
|
19
19
|
import { PRELUDE_NAMES } from "./prelude-source"
|
|
@@ -21,7 +21,9 @@ import { KEYWORD_SET, TYPE_SET } from "./words"
|
|
|
21
21
|
|
|
22
22
|
export type SLTokenClass =
|
|
23
23
|
| "keyword" | "type" | "builtin" | "input" | "prelude"
|
|
24
|
-
| "number" | "hex" | "comment" | "punct"
|
|
24
|
+
| "number" | "hex" | "string" | "comment" | "punct"
|
|
25
|
+
/** An attribute's name, as in `[Range(0, 1)]`. */
|
|
26
|
+
| "attribute"
|
|
25
27
|
/** A component after a dot, as in `c.rgb`. */
|
|
26
28
|
| "member"
|
|
27
29
|
/** Any other name: a uniform, a local, a function the file declares. */
|
|
@@ -40,7 +42,7 @@ export interface SLClassifiedToken {
|
|
|
40
42
|
length: number
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(
|
|
45
|
+
const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(SOURCE_INPUTS))
|
|
44
46
|
|
|
45
47
|
/** The tokens of `source`, comments included, in order, each with its class. */
|
|
46
48
|
export function classify(source: string): SLClassifiedToken[] {
|
|
@@ -54,6 +56,8 @@ export function classify(source: string): SLClassifiedToken[] {
|
|
|
54
56
|
let kind: SLTokenClass
|
|
55
57
|
if (t.kind === "ident") {
|
|
56
58
|
if (before === ".") kind = beforeThat === "sdf" ? "builtin" : "member"
|
|
59
|
+
// There are no arrays, so a name straight after "[" is an attribute's.
|
|
60
|
+
else if (before === "[") kind = "attribute"
|
|
57
61
|
else if (KEYWORD_SET.has(t.text)) kind = "keyword"
|
|
58
62
|
else if (TYPE_SET.has(t.text)) kind = "type"
|
|
59
63
|
else if (INPUT_NAMES.has(t.text)) kind = "input"
|
package/src/lang/glsl.ts
CHANGED
|
@@ -107,6 +107,83 @@ const MISSING: Record<string, string> = {
|
|
|
107
107
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Uniforms other hosts declare for what is an input here: the Book of Shaders'
|
|
112
|
+
* and glslCanvas's `u_time`, glslsandbox's `time`, and Shadertoy's own when a
|
|
113
|
+
* tool declares them. The declaration goes and every use reads the input, so a
|
|
114
|
+
* pasted shader does not arrive frozen on a uniform nothing sets.
|
|
115
|
+
*/
|
|
116
|
+
const HOST_UNIFORMS: Record<string, string> = {
|
|
117
|
+
u_time: "time", u_resolution: "resolution", time: "time", resolution: "resolution",
|
|
118
|
+
iTime: "time", iGlobalTime: "time", iResolution: "resolution", iMouse: "mouse",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const QUALIFIERS = new Set(["highp", "mediump", "lowp"])
|
|
122
|
+
/** Words that take a bracket without being called. */
|
|
123
|
+
const CONTROL = new Set(["if", "for", "while", "switch", "return"])
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether a `mat2(...)`, given its four arguments as words, is a rotation, and
|
|
127
|
+
* which way: +1 for `mat2(c, s, -s, c)`, the matrix R(a), and -1 for
|
|
128
|
+
* `mat2(c, -s, s, c)`, which is R(-a) since GLSL fills a matrix by columns.
|
|
129
|
+
* `cos` and `sin` are the words that stand for the cosine and sine.
|
|
130
|
+
*/
|
|
131
|
+
function rotationSign(matArgs: string[], cos: string[], sin: string[]): 1 | -1 | null {
|
|
132
|
+
const kind = (w: string) => cos.includes(w) ? "C" : sin.includes(w) ? "S"
|
|
133
|
+
: w.startsWith("- ") && sin.includes(w.slice(2)) ? "-S" : "?"
|
|
134
|
+
const k = matArgs.map(kind).join(",")
|
|
135
|
+
return k === "C,S,-S,C" ? 1 : k === "C,-S,S,C" ? -1 : null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Words split at the commas outside any bracket. */
|
|
139
|
+
function splitTop(words: string[]): string[] {
|
|
140
|
+
const out: string[] = []
|
|
141
|
+
let depth = 0
|
|
142
|
+
let cur: string[] = []
|
|
143
|
+
for (const w of words) {
|
|
144
|
+
if (w === "(" || w === "[") depth++
|
|
145
|
+
else if (w === ")" || w === "]") depth--
|
|
146
|
+
if (w === "," && depth === 0) { out.push(cur.join(" ")); cur = [] } else cur.push(w)
|
|
147
|
+
}
|
|
148
|
+
out.push(cur.join(" "))
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The arguments of `mat2 ( ... )` as words, when `words` is exactly that, in any number of brackets. */
|
|
153
|
+
function mat2Args(words: string[]): string[] | null {
|
|
154
|
+
while (words[0] === "(" && words[words.length - 1] === ")") words = words.slice(1, -1)
|
|
155
|
+
if (words[0] !== "mat2" || words[1] !== "(" || words[words.length - 1] !== ")") return null
|
|
156
|
+
const inner = words.slice(2, -1)
|
|
157
|
+
let depth = 0
|
|
158
|
+
for (const w of inner) { if (w === "(") depth++; else if (w === ")" && --depth < 0) return null }
|
|
159
|
+
return splitTop(inner)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The sign of a function body that returns a rotation of its parameter:
|
|
164
|
+
* `return mat2(cos(a), -sin(a), sin(a), cos(a));`, or the same after
|
|
165
|
+
* `float c = cos(a), s = sin(a);`. Null for anything else.
|
|
166
|
+
*/
|
|
167
|
+
function rotationBody(words: string[], param: string): 1 | -1 | null {
|
|
168
|
+
const cos = [`cos ( ${param} )`]
|
|
169
|
+
const sin = [`sin ( ${param} )`]
|
|
170
|
+
const statements = words.join(" ").split(/\s*;\s*/).filter((x) => x !== "")
|
|
171
|
+
const last = statements.pop()
|
|
172
|
+
const binding = new RegExp(`^(\\w+) = (cos|sin) \\( ${param} \\)$`)
|
|
173
|
+
for (const st of statements) {
|
|
174
|
+
const parts = splitTop(st.replace(/^float /, "").split(" "))
|
|
175
|
+
for (const part of parts) {
|
|
176
|
+
const m = binding.exec(part)
|
|
177
|
+
if (m === null) return null
|
|
178
|
+
if (m[2] === "cos") cos.push(m[1]!)
|
|
179
|
+
else sin.push(m[1]!)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (last === undefined || !last.startsWith("return ")) return null
|
|
183
|
+
const a = mat2Args(last.slice("return ".length).split(" "))
|
|
184
|
+
return a === null || a.length !== 4 ? null : rotationSign(a, cos, sin)
|
|
185
|
+
}
|
|
186
|
+
|
|
110
187
|
/** Names a pasted value or function cannot keep, because the language already means something by them. */
|
|
111
188
|
function taken(name: string, isFunction: boolean): boolean {
|
|
112
189
|
if (name in INPUTS || (SL_KEYWORDS as readonly string[]).includes(name) || (SL_TYPES as readonly string[]).includes(name)) return true
|
|
@@ -141,6 +218,61 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
141
218
|
}
|
|
142
219
|
return toks.length
|
|
143
220
|
}
|
|
221
|
+
/** The index of the bracket opening the one at `close`, or -1. */
|
|
222
|
+
const opening = (close: number): number => {
|
|
223
|
+
const [o, c] = toks[close]!.text === ")" ? ["(", ")"] : ["[", "]"]
|
|
224
|
+
let depth = 0
|
|
225
|
+
for (let i = close; i >= 0; i--) {
|
|
226
|
+
if (toks[i]!.kind !== "punct") continue
|
|
227
|
+
if (toks[i]!.text === c) depth++
|
|
228
|
+
else if (toks[i]!.text === o && --depth === 0) return i
|
|
229
|
+
}
|
|
230
|
+
return -1
|
|
231
|
+
}
|
|
232
|
+
/** Words for tokens `from` to `to`: no layout, no comments, one space between. */
|
|
233
|
+
const words = (from: number, to: number) =>
|
|
234
|
+
toks.slice(from, to).filter((t) => t.kind !== "ws" && t.kind !== "comment").map((t) => t.text)
|
|
235
|
+
/**
|
|
236
|
+
* The last token of the operand starting at `i`: a name, a number, a call
|
|
237
|
+
* or a bracket, then any members and indexes. -1 when it is none of those.
|
|
238
|
+
*/
|
|
239
|
+
const operandAfter = (i: number): number => {
|
|
240
|
+
const t = toks[i]
|
|
241
|
+
let j: number
|
|
242
|
+
if (t === undefined) return -1
|
|
243
|
+
if (t.kind === "number") j = i
|
|
244
|
+
else if (is(i, "(")) j = closing(i)
|
|
245
|
+
else if (t.kind === "ident") { const n = skip(i + 1); j = is(n, "(") ? closing(n) : i }
|
|
246
|
+
else return -1
|
|
247
|
+
for (;;) {
|
|
248
|
+
if (j >= toks.length) return -1
|
|
249
|
+
const n = skip(j + 1)
|
|
250
|
+
if (is(n, ".") && toks[skip(n + 1)]?.kind === "ident") j = skip(n + 1)
|
|
251
|
+
else if (is(n, "[")) j = closing(n)
|
|
252
|
+
else return j
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** The first token of the operand ending at `j`, the same shapes read backwards. -1 when there is none. */
|
|
256
|
+
const operandBefore = (j: number): number => {
|
|
257
|
+
let i = j
|
|
258
|
+
for (;;) {
|
|
259
|
+
if (is(i, "]")) { const o = opening(i); if (o < 0) return -1; i = back(o - 1); continue }
|
|
260
|
+
let start: number
|
|
261
|
+
if (is(i, ")")) {
|
|
262
|
+
const o = opening(i)
|
|
263
|
+
if (o < 0) return -1
|
|
264
|
+
const p = back(o - 1)
|
|
265
|
+
start = toks[p]?.kind === "ident" && !CONTROL.has(toks[p]!.text) ? p : o
|
|
266
|
+
} else if (toks[i]?.kind === "ident" || toks[i]?.kind === "number") {
|
|
267
|
+
start = i
|
|
268
|
+
} else {
|
|
269
|
+
return -1
|
|
270
|
+
}
|
|
271
|
+
const p = back(start - 1)
|
|
272
|
+
if (!is(p, ".")) return start
|
|
273
|
+
i = back(p - 1)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
144
276
|
/** A call's arguments, as token ranges, for the call whose "(" is at `open`. */
|
|
145
277
|
const args = (open: number, close: number): Array<[number, number]> => {
|
|
146
278
|
const out: Array<[number, number]> = []
|
|
@@ -173,6 +305,142 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
173
305
|
if (taken(name, isFunction)) clashes.set(name, name + "_")
|
|
174
306
|
}
|
|
175
307
|
|
|
308
|
+
// ---------- what other hosts declare ----------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Token ranges written as something else, keyed by their first token: a
|
|
312
|
+
* declaration that goes, a rotation that becomes a call. They nest, and
|
|
313
|
+
* never cross.
|
|
314
|
+
*/
|
|
315
|
+
const spans = new Map<number, { end: number; render: (inEntry: boolean) => string }>()
|
|
316
|
+
const crosses = (start: number, end: number) =>
|
|
317
|
+
[...spans].some(([s, v]) => (s < start && start <= v.end && v.end < end) || (start < s && s <= end && end < v.end))
|
|
318
|
+
/** A range and the whitespace after it, gone. */
|
|
319
|
+
const drop = (start: number, end: number) => {
|
|
320
|
+
spans.set(start, { end: toks[end + 1]?.kind === "ws" ? end + 1 : end, render: () => "" })
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const hostUniforms = new Set<string>()
|
|
324
|
+
for (let i = 0; i < toks.length; i++) {
|
|
325
|
+
if (!is(i, "uniform")) continue
|
|
326
|
+
let t = skip(i + 1)
|
|
327
|
+
while (QUALIFIERS.has(toks[t]?.text ?? "")) t = skip(t + 1)
|
|
328
|
+
const n = skip(t + 1)
|
|
329
|
+
const name = toks[n]?.text ?? ""
|
|
330
|
+
const semi = skip(n + 1)
|
|
331
|
+
if (toks[n]?.kind !== "ident" || HOST_UNIFORMS[name] === undefined || !is(semi, ";")) continue
|
|
332
|
+
hostUniforms.add(name)
|
|
333
|
+
// Its name is the input, not a clash with it.
|
|
334
|
+
clashes.delete(name)
|
|
335
|
+
const input = HOST_UNIFORMS[name]!
|
|
336
|
+
noteOnce("host:" + name, toks[i]!.line, `the uniform ${name} is the input ${input}, so its declaration is dropped`)
|
|
337
|
+
drop(i, semi)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------- rotations ----------
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* There are no matrices, and the matrix a shader almost always has is a
|
|
344
|
+
* rotation: `mat2 rot(float a)` returning `mat2(c, -s, s, c)`, the same as
|
|
345
|
+
* a macro, or written in place. Each use that multiplies a vector becomes
|
|
346
|
+
* `rotate(p, angle)`, the angle's sign set by the matrix and by which side
|
|
347
|
+
* the vector is on, since GLSL's `p * M` is M transposed times p. A
|
|
348
|
+
* function or macro whose every use is converted goes. Anything else keeps
|
|
349
|
+
* its `mat2`, which `errors` then points at, with a note naming rotate().
|
|
350
|
+
*/
|
|
351
|
+
const rotations = new Map<string, { sign: 1 | -1; def: [number, number]; line: number; uses: number; converted: number }>()
|
|
352
|
+
for (let i = 0; i < toks.length; i++) {
|
|
353
|
+
const t = toks[i]!
|
|
354
|
+
if (t.kind === "directive") {
|
|
355
|
+
const m = /^#\s*define\s+([A-Za-z_]\w*)\(\s*([A-Za-z_]\w*)\s*\)(.*)$/s.exec(t.text)
|
|
356
|
+
if (m === null) continue
|
|
357
|
+
const body = tokenize(m[3]!.replace(/\\\n/g, " ")).filter((x) => x.kind !== "ws" && x.kind !== "comment").map((x) => x.text)
|
|
358
|
+
const a = mat2Args(body)
|
|
359
|
+
const sign = a === null || a.length !== 4 ? null : rotationSign(a, [`cos ( ${m[2]} )`], [`sin ( ${m[2]} )`])
|
|
360
|
+
if (sign !== null) rotations.set(m[1]!, { sign, def: [i, i], line: t.line, uses: 0, converted: 0 })
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
if (!is(i, "mat2")) continue
|
|
364
|
+
const n = skip(i + 1)
|
|
365
|
+
const open = skip(n + 1)
|
|
366
|
+
if (toks[n]?.kind !== "ident" || !is(open, "(")) continue
|
|
367
|
+
const close = closing(open)
|
|
368
|
+
const params = words(open + 1, close).filter((w) => w !== "in")
|
|
369
|
+
const body = skip(close + 1)
|
|
370
|
+
if (params.length !== 2 || params[0] !== "float" || !is(body, "{")) continue
|
|
371
|
+
const end = closing(body)
|
|
372
|
+
const sign = rotationBody(words(body + 1, end), params[1]!)
|
|
373
|
+
if (sign !== null) rotations.set(toks[n]!.text, { sign, def: [i, end], line: t.line, uses: 0, converted: 0 })
|
|
374
|
+
}
|
|
375
|
+
// A file with a rotate of its own would have the calls written here call it.
|
|
376
|
+
const ownRotate = toks.some((t, i) => t.text === "rotate" && t.kind === "ident" && GLSL_TYPES.has(toks[back(i - 1)]?.text ?? "") &&
|
|
377
|
+
!(rotations.has("rotate") && toks[back(i - 1)]!.text === "mat2"))
|
|
378
|
+
|
|
379
|
+
/** The angle, negated when `sign` is -1, as text. */
|
|
380
|
+
const angle = (range: [number, number], sign: 1 | -1, inEntry: boolean) => {
|
|
381
|
+
const a = emit(range[0], range[1], inEntry).trim()
|
|
382
|
+
if (sign === 1) return a
|
|
383
|
+
if (/^-[\w.]+$/.test(a)) return a.slice(1)
|
|
384
|
+
return /^[\w.]+$/.test(a) ? `-${a}` : `-(${a})`
|
|
385
|
+
}
|
|
386
|
+
for (let i = 0; i < toks.length && !ownRotate; i++) {
|
|
387
|
+
if (toks[i]!.kind !== "ident" || is(back(i - 1), ".")) continue
|
|
388
|
+
const open = skip(i + 1)
|
|
389
|
+
if (!is(open, "(")) continue
|
|
390
|
+
const close = closing(open)
|
|
391
|
+
const a = args(open, close)
|
|
392
|
+
const fn = rotations.get(toks[i]!.text)
|
|
393
|
+
let sign: 1 | -1 | null = null
|
|
394
|
+
let range: [number, number] | null = null
|
|
395
|
+
if (fn !== undefined && (i < fn.def[0] || i > fn.def[1])) {
|
|
396
|
+
fn.uses++
|
|
397
|
+
if (a.length === 1) { sign = fn.sign; range = a[0]! }
|
|
398
|
+
} else if (is(i, "mat2") && a.length === 4) {
|
|
399
|
+
// In place: the angle is whatever cos() is given.
|
|
400
|
+
const first = skip(a[0]![0])
|
|
401
|
+
const inner = skip(first + 1)
|
|
402
|
+
if (is(first, "cos") && is(inner, "(") && closing(inner) === back(a[0]![1] - 1)) {
|
|
403
|
+
const x = words(inner + 1, closing(inner)).join(" ")
|
|
404
|
+
sign = rotationSign(a.map(([x0, x1]) => words(x0, x1).join(" ")), [`cos ( ${x} )`], [`sin ( ${x} )`])
|
|
405
|
+
range = [inner + 1, closing(inner)]
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (sign === null || range === null) continue
|
|
409
|
+
const r = range
|
|
410
|
+
const s = sign
|
|
411
|
+
const before = back(i - 1)
|
|
412
|
+
const after = skip(close + 1)
|
|
413
|
+
let span: [number, number, (inEntry: boolean) => string] | null = null
|
|
414
|
+
if (is(before, "=") && is(before - 1, "*") && is(after, ";")) {
|
|
415
|
+
// p *= M is p = p * M.
|
|
416
|
+
const lhsEnd = back(before - 2)
|
|
417
|
+
const lhs = operandBefore(lhsEnd)
|
|
418
|
+
if (lhs >= 0) span = [lhs, close, (e) => { const p = emit(lhs, lhsEnd + 1, e).trim(); return `${p} = rotate(${p}, ${angle(r, -s as 1 | -1, e)})` }]
|
|
419
|
+
} else if (is(after, "*") && !is(after + 1, "=") && !["/", "%", "*"].includes(toks[before]?.text ?? "")) {
|
|
420
|
+
// M * p.
|
|
421
|
+
const x = skip(after + 1)
|
|
422
|
+
const xEnd = operandAfter(x)
|
|
423
|
+
if (xEnd >= 0) span = [i, xEnd, (e) => `rotate(${emit(x, xEnd + 1, e).trim()}, ${angle(r, s, e)})`]
|
|
424
|
+
} else if (is(before, "*") && !is(after, "*")) {
|
|
425
|
+
// p * M.
|
|
426
|
+
const xEnd = back(before - 1)
|
|
427
|
+
const x = operandBefore(xEnd)
|
|
428
|
+
if (x >= 0 && !["/", "%"].includes(toks[back(x - 1)]?.text ?? "")) {
|
|
429
|
+
span = [x, close, (e) => `rotate(${emit(x, xEnd + 1, e).trim()}, ${angle(r, -s as 1 | -1, e)})`]
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (span === null || crosses(span[0], span[1]) || spans.has(span[0])) continue
|
|
433
|
+
const [start, end, render] = span
|
|
434
|
+
spans.set(start, { end, render })
|
|
435
|
+
noteOnce("rotation", toks[i]!.line, "a mat2 rotation is rotate(p, angle) here, since there are no matrices; the angle's sign follows which side p was multiplied on")
|
|
436
|
+
if (fn !== undefined) fn.converted++
|
|
437
|
+
}
|
|
438
|
+
for (const [name, fn] of rotations) {
|
|
439
|
+
if (ownRotate || fn.converted !== fn.uses) continue
|
|
440
|
+
drop(fn.def[0], fn.def[1])
|
|
441
|
+
note(fn.line, `${name} built a rotation matrix, and each use of it is rotate() now, so it is removed`)
|
|
442
|
+
}
|
|
443
|
+
|
|
176
444
|
// ---------- the entry point ----------
|
|
177
445
|
|
|
178
446
|
let colour = "fragColor"
|
|
@@ -231,6 +499,8 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
231
499
|
let out = ""
|
|
232
500
|
for (let i = from; i < to; i++) {
|
|
233
501
|
const t = toks[i]!
|
|
502
|
+
const span = spans.get(i)
|
|
503
|
+
if (span !== undefined && span.end < to) { out += span.render(inEntry); i = span.end; continue }
|
|
234
504
|
if (t.kind === "directive") { out += directive(t); continue }
|
|
235
505
|
if (t.kind === "number") { out += t.text.replace(/[uU]$/, ""); continue }
|
|
236
506
|
if (t.kind !== "ident") { out += t.text; continue }
|
|
@@ -286,6 +556,16 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
286
556
|
out += to2
|
|
287
557
|
continue
|
|
288
558
|
}
|
|
559
|
+
if (hostUniforms.has(name) && (name === "u_time" || name === "u_resolution")) { out += HOST_UNIFORMS[name]; continue }
|
|
560
|
+
if (name === "mat2") {
|
|
561
|
+
noteOnce("mat2", t.line, "there are no matrices here; a mat2 rotation is rotate(p, angle): p *= mat2(c, -s, s, c) is rotate(p, a), and mat2(c, -s, s, c) * p is rotate(p, -a)")
|
|
562
|
+
out += name
|
|
563
|
+
continue
|
|
564
|
+
}
|
|
565
|
+
const unconverted = rotations.get(name)
|
|
566
|
+
if (unconverted !== undefined && called && (i < unconverted.def[0] || i > unconverted.def[1])) {
|
|
567
|
+
noteOnce("rot:" + t.line + name, t.line, `${name} builds a rotation matrix, and this use of it was not converted; write it as rotate(p, angle)`)
|
|
568
|
+
}
|
|
289
569
|
if (/^iChannel\d$/.test(name)) { textures.add(name); out += name; continue }
|
|
290
570
|
if (name === "iMouse") {
|
|
291
571
|
mouse = true
|
package/src/lang/lexer.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import { SLError } from "../ir"
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** `string` only ever appears in an attribute: `[Label("Glow colour")]`. */
|
|
17
|
+
export type TokenKind = "ident" | "number" | "hex" | "string" | "punct" | "eof"
|
|
17
18
|
|
|
18
19
|
export interface Pos {
|
|
19
20
|
/** 1 based, the way every editor counts. */
|
|
@@ -30,6 +31,8 @@ export interface Token extends Pos {
|
|
|
30
31
|
text: string
|
|
31
32
|
/** Numbers only: the parsed value. */
|
|
32
33
|
value?: number
|
|
34
|
+
/** Strings only: the text between the quotes, with `\"` and `\\` read. */
|
|
35
|
+
str?: string
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -206,6 +209,25 @@ export function lex(source: string, file: string, keep: boolean): Token[] | Lexe
|
|
|
206
209
|
continue
|
|
207
210
|
}
|
|
208
211
|
|
|
212
|
+
if (c === "\"") {
|
|
213
|
+
const start = here()
|
|
214
|
+
let j = i + 1
|
|
215
|
+
let str = ""
|
|
216
|
+
while (j < source.length && source[j] !== "\"" && source[j] !== "\n") {
|
|
217
|
+
if (source[j] === "\\" && (source[j + 1] === "\"" || source[j + 1] === "\\")) j++
|
|
218
|
+
str += source[j]
|
|
219
|
+
j++
|
|
220
|
+
}
|
|
221
|
+
if (source[j] !== "\"") {
|
|
222
|
+
fail("this string is never closed; it ends at the end of its line", start, j - i)
|
|
223
|
+
} else {
|
|
224
|
+
push({ kind: "string", text: source.slice(i, j + 1), str, ...start }, j + 1)
|
|
225
|
+
j++
|
|
226
|
+
}
|
|
227
|
+
i = j
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
if (isIdentStart(c)) {
|
|
210
232
|
const start = here()
|
|
211
233
|
let j = i
|
package/src/lang/lower.ts
CHANGED
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
* than an optimisation.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { INPUTS, type Program, type SLType } from "../ir"
|
|
36
|
+
import { controlProblem, DERIVED_INPUTS, INPUTS, type Program, type SLType, type UniformControl } from "../ir"
|
|
37
37
|
import type { SlSdfKind } from "../shapes"
|
|
38
38
|
import * as sl from "../sl"
|
|
39
39
|
import type { Num, Val } from "../sl"
|
|
40
40
|
import { TYPE_WIDTH, type Expr, type FuncDecl, type Stmt, type TypeName } from "./ast"
|
|
41
|
+
import { readAttributes } from "./attributes"
|
|
41
42
|
import { BUILTINS } from "./builtins"
|
|
42
43
|
import type { Checked } from "./check"
|
|
43
44
|
import { SLParseError, type Pos } from "./lexer"
|
|
@@ -161,6 +162,17 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
161
162
|
for (const [name, width] of Object.entries(INPUTS)) {
|
|
162
163
|
global.declare(name, { width: width as SLType, value: (inputs as never)[name] })
|
|
163
164
|
}
|
|
165
|
+
// Read through, so a program that never names one records nothing
|
|
166
|
+
// for it. The setter only serves a refused assignment, which the
|
|
167
|
+
// checker has already reported and lowering carries on past.
|
|
168
|
+
for (const [name, width] of Object.entries(DERIVED_INPUTS)) {
|
|
169
|
+
let value: LV | undefined
|
|
170
|
+
global.declare(name, {
|
|
171
|
+
width: width as SLType,
|
|
172
|
+
get value() { return value ??= (inputs as never)[name] },
|
|
173
|
+
set value(v: LV) { value = v },
|
|
174
|
+
})
|
|
175
|
+
}
|
|
164
176
|
|
|
165
177
|
// Uniforms and textures take their slots in DECLARATION order, before
|
|
166
178
|
// anything is lowered, so a slot is a property of the file rather than
|
|
@@ -177,9 +189,20 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
177
189
|
// that, `#ff8040` and a uniform defaulting to `#ff8040` would be two
|
|
178
190
|
// different colours in one file. `sl.uniform.colour` also marks
|
|
179
191
|
// the declaration, for a host's colour picker.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
// What the attributes say the control is, and whether the default
|
|
193
|
+
// fits it, marked on the attribute that does not fit.
|
|
194
|
+
const read = readAttributes(u)
|
|
195
|
+
const problem = recover(() => {
|
|
196
|
+
const p = controlProblem(width as SLType, components, read.control)
|
|
197
|
+
const attr = p === null ? undefined : read.from[p.field]
|
|
198
|
+
if (p !== null) fail(inFileWords(p.message), attr?.pos ?? u.pos, attr?.length ?? 1)
|
|
199
|
+
return false
|
|
200
|
+
}, () => true)
|
|
201
|
+
const control = problem ? {} : read.control
|
|
202
|
+
// `[Color]` says the same as a hex default, for a default written as numbers.
|
|
203
|
+
const value = (colour && u.init?.k === "hex") || read.colour
|
|
204
|
+
? at(u.pos, () => sl.uniform.colour(u.name, components, width as 3 | 4, control) as unknown as Val)
|
|
205
|
+
: at(u.pos, () => declareUniform(u.name, u.type, components, control))
|
|
183
206
|
global.declare(u.name, { width, value })
|
|
184
207
|
}
|
|
185
208
|
|
|
@@ -217,12 +240,12 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
217
240
|
|
|
218
241
|
// MARK: declarations
|
|
219
242
|
|
|
220
|
-
function declareUniform(name: string, type: TypeName, c: number[]): Val {
|
|
243
|
+
function declareUniform(name: string, type: TypeName, c: number[], control: UniformControl): Val {
|
|
221
244
|
switch (type) {
|
|
222
|
-
case "float": return sl.uniform.float(name, c[0]
|
|
223
|
-
case "float2": return sl.uniform.vec2(name, [c[0]!, c[1]!]) as unknown as Val
|
|
224
|
-
case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!]) as unknown as Val
|
|
225
|
-
case "float4": return sl.uniform.vec4(name, [c[0]!, c[1]!, c[2]!, c[3]!]) as unknown as Val
|
|
245
|
+
case "float": return sl.uniform.float(name, c[0]!, control) as unknown as Val
|
|
246
|
+
case "float2": return sl.uniform.vec2(name, [c[0]!, c[1]!], control) as unknown as Val
|
|
247
|
+
case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!], control) as unknown as Val
|
|
248
|
+
case "float4": return sl.uniform.vec4(name, [c[0]!, c[1]!, c[2]!, c[3]!], control) as unknown as Val
|
|
226
249
|
}
|
|
227
250
|
}
|
|
228
251
|
|
package/src/lang/parser.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
type AssignOp, type BinaryOp, type Expr, type FuncDecl, type Param, type Stmt,
|
|
15
|
+
type AssignOp, type Attribute, type AttributeArg, type BinaryOp, type Expr, type FuncDecl, type Param, type Stmt,
|
|
16
16
|
type TextureDecl, type TypeName, type UniformDecl, type Unit,
|
|
17
17
|
} from "./ast"
|
|
18
18
|
import { SLParseError, tokenize, type Pos, type SLFix, type Token } from "./lexer"
|
|
@@ -199,7 +199,18 @@ class Parser {
|
|
|
199
199
|
private parseDeclaration(unit: Unit): void {
|
|
200
200
|
const t = this.peek()
|
|
201
201
|
|
|
202
|
-
if (t.text === "
|
|
202
|
+
if (t.text === "[") {
|
|
203
|
+
const attrs = this.parseAttributes()
|
|
204
|
+
if (!this.at("uniform")) {
|
|
205
|
+
this.fail(
|
|
206
|
+
`an attribute belongs on a uniform, as in \`[Range(0, 1)] uniform float amount = 0.5;\`, ` +
|
|
207
|
+
`and this one is followed by ${describe(this.peek())}`, t,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
unit.uniforms.push(this.parseUniform(attrs))
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if (t.text === "uniform") { unit.uniforms.push(this.parseUniform([])); return }
|
|
203
214
|
if (t.text === "texture2D") { unit.textures.push(this.parseTexture()); return }
|
|
204
215
|
if (t.text === "const") { unit.consts.push(this.parseConst()); return }
|
|
205
216
|
if (t.kind === "ident") {
|
|
@@ -214,14 +225,45 @@ class Parser {
|
|
|
214
225
|
)
|
|
215
226
|
}
|
|
216
227
|
|
|
217
|
-
private parseUniform(): UniformDecl {
|
|
228
|
+
private parseUniform(attrs: Attribute[]): UniformDecl {
|
|
218
229
|
const kw = this.next()
|
|
219
230
|
const type = this.expectType("a type after uniform")
|
|
220
231
|
const name = this.expectName("a uniform name")
|
|
221
232
|
let init: Expr | null = null
|
|
222
233
|
if (this.eat("=")) init = this.parseExpr()
|
|
223
234
|
this.expect(";", "after a uniform declaration")
|
|
224
|
-
return { name: name.text, type, init, pos: kw }
|
|
235
|
+
return { name: name.text, type, init, attrs, pos: kw }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `[Name]` or `[Name(arg, ...)]`, one or more. Shape only: the checker says what each means. */
|
|
239
|
+
private parseAttributes(): Attribute[] {
|
|
240
|
+
const out: Attribute[] = []
|
|
241
|
+
while (this.eat("[")) {
|
|
242
|
+
const name = this.expectIdent("an attribute's name, such as Range")
|
|
243
|
+
const args: AttributeArg[] = []
|
|
244
|
+
if (this.eat("(")) {
|
|
245
|
+
if (!this.at(")")) {
|
|
246
|
+
do args.push(this.parseAttributeArg())
|
|
247
|
+
while (this.eat(","))
|
|
248
|
+
}
|
|
249
|
+
this.expect(")", "to close the attribute's arguments")
|
|
250
|
+
}
|
|
251
|
+
this.expect("]", "to close the attribute")
|
|
252
|
+
out.push({ name: name.text, args, pos: name, length: name.text.length })
|
|
253
|
+
}
|
|
254
|
+
return out
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private parseAttributeArg(): AttributeArg {
|
|
258
|
+
const t = this.next()
|
|
259
|
+
if (t.kind === "string") return { k: "str", text: t.str!, pos: t, length: t.text.length }
|
|
260
|
+
if (t.kind === "ident") return { k: "ident", name: t.text, pos: t, length: t.text.length }
|
|
261
|
+
if (t.kind === "number") return { k: "num", value: t.value!, pos: t, length: t.text.length }
|
|
262
|
+
if (t.text === "-" && this.peek().kind === "number") {
|
|
263
|
+
const n = this.next()
|
|
264
|
+
return { k: "num", value: -n.value!, pos: t, length: n.offset + n.text.length - t.offset }
|
|
265
|
+
}
|
|
266
|
+
this.fail(`expected a number, a word or a "string" as an attribute's argument, got ${describe(t)}`, t)
|
|
225
267
|
}
|
|
226
268
|
|
|
227
269
|
private parseTexture(): TextureDecl {
|
|
@@ -512,6 +554,9 @@ class Parser {
|
|
|
512
554
|
if (t.kind === "number") return { k: "num", value: t.value!, pos: t }
|
|
513
555
|
if (t.kind === "hex") return { k: "hex", hex: t.text, pos: t }
|
|
514
556
|
if (t.kind === "ident") return { k: "ident", name: t.text, pos: t }
|
|
557
|
+
if (t.kind === "string") {
|
|
558
|
+
this.fail(`a string is only an attribute's argument, as in [Label("Glow colour")]; a value is a number`, t)
|
|
559
|
+
}
|
|
515
560
|
if (t.text === "(") {
|
|
516
561
|
const inner = this.parseExpr()
|
|
517
562
|
this.expect(")", "to close a group")
|
package/src/params.ts
CHANGED
|
@@ -20,7 +20,8 @@ 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 {
|
|
23
|
+
import type { SourceInputName } from "./ir"
|
|
24
|
+
import type { AttributeName } from "./lang/attributes"
|
|
24
25
|
|
|
25
26
|
/** One function of the helper library, as its source declares it. */
|
|
26
27
|
export interface LibSignature {
|
|
@@ -164,15 +165,31 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
|
164
165
|
ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* One line on each attribute, for the same tooltips: what the control is, and
|
|
170
|
+
* how it is written.
|
|
171
|
+
*/
|
|
172
|
+
export const ATTRIBUTE_DOCS: Readonly<Record<AttributeName, string>> = {
|
|
173
|
+
Range: "A slider from the first number to the second: [Range(0, 2)]. A third number is its step. A float only.",
|
|
174
|
+
Toggle: "A checkbox. The value is 0 or 1. A float only.",
|
|
175
|
+
Enum: "A dropdown of names: [Enum(Soft, Hard, Glow)]. The value is the chosen name's index, from 0. A float only.",
|
|
176
|
+
Header: "A heading above this control, starting a group: [Header(\"Shape\")].",
|
|
177
|
+
Label: "The name the control shows, in place of the uniform's: [Label(\"Glow colour\")].",
|
|
178
|
+
Color: "A colour picker, the value a colour as written, the same as a hex default. A float3 or a float4.",
|
|
179
|
+
Hide: "No control: the host sets this uniform from code.",
|
|
180
|
+
}
|
|
181
|
+
|
|
167
182
|
/**
|
|
168
183
|
* One line on each input, for the same tooltips. In the words of no one host:
|
|
169
184
|
* the output is a UI element in OneJS and an image on a Magerie board, and the
|
|
170
185
|
* clock is whichever one the host runs.
|
|
171
186
|
*/
|
|
172
|
-
export const INPUT_DOCS: Readonly<Record<
|
|
173
|
-
uv: "
|
|
174
|
-
fragCoord: "
|
|
187
|
+
export const INPUT_DOCS: Readonly<Record<SourceInputName, string>> = {
|
|
188
|
+
uv: "The centre of this pixel in the output, 0 to 1 on each axis, with y up.",
|
|
189
|
+
fragCoord: "The centre of this pixel in the output, in pixels, with y up: the first column is at 0.5.",
|
|
175
190
|
resolution: "The output's size in pixels.",
|
|
176
191
|
time: "The clock, in seconds.",
|
|
177
192
|
aspect: "The output's width divided by its height.",
|
|
193
|
+
texel: "One pixel, in uv: 1 / resolution. Step by it to read a neighbouring pixel.",
|
|
194
|
+
centered: "uv with 0 at the centre and x scaled by aspect, so a circle drawn in it stays round.",
|
|
178
195
|
}
|
package/src/serial.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* older one is migrated to this one and rehashed under it.
|
|
10
10
|
*/
|
|
11
11
|
import {
|
|
12
|
-
INPUTS, SLError, SL_IR_VERSION, TYPE, hashProgram,
|
|
12
|
+
INPUTS, SLError, SL_IR_VERSION, TYPE, controlOf, controlProblem, hashProgram,
|
|
13
13
|
type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
14
14
|
} from "./ir"
|
|
15
15
|
import { SL_ARITY, SL_NAME } from "./ops"
|
|
@@ -125,7 +125,27 @@ function uniform(u: unknown, i: number): UniformDecl {
|
|
|
125
125
|
}
|
|
126
126
|
const out: UniformDecl = { name: x.name!, type: x.type as SLType, value: x.value!.slice() }
|
|
127
127
|
if (x.colour === true) out.colour = true
|
|
128
|
-
|
|
128
|
+
// Its control: shapes checked here, sense checked by the one check the
|
|
129
|
+
// compiler uses, so a hand edited file cannot give a host a range it would
|
|
130
|
+
// choke on.
|
|
131
|
+
const r = x.range as { min?: unknown; max?: unknown; step?: unknown } | undefined
|
|
132
|
+
if (r !== undefined && (typeof r !== "object" || r === null || typeof r.min !== "number" || typeof r.max !== "number" ||
|
|
133
|
+
(r.step !== undefined && typeof r.step !== "number"))) {
|
|
134
|
+
fail(`uniform ${i}'s range needs a min and a max, and a step if any, as numbers`)
|
|
135
|
+
}
|
|
136
|
+
if (x.options !== undefined && (!Array.isArray(x.options) || !x.options.every((o) => typeof o === "string"))) {
|
|
137
|
+
fail(`uniform ${i}'s options must be a list of names`)
|
|
138
|
+
}
|
|
139
|
+
for (const f of ["header", "label"] as const) {
|
|
140
|
+
if (x[f] !== undefined && typeof x[f] !== "string") fail(`uniform ${i}'s ${f} must be text`)
|
|
141
|
+
}
|
|
142
|
+
for (const f of ["toggle", "hide"] as const) {
|
|
143
|
+
if (x[f] !== undefined && x[f] !== true) fail(`uniform ${i}'s ${f} can only be true`)
|
|
144
|
+
}
|
|
145
|
+
const control = controlOf(x)
|
|
146
|
+
const problem = controlProblem(out.type, out.value, control)
|
|
147
|
+
if (problem !== null) fail(`uniform ${i}: ${problem.message}`)
|
|
148
|
+
return Object.assign(out, control)
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
function texture(t: unknown, i: number): TextureDecl {
|