onejs-sl 0.1.3 → 0.1.4
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 +9 -0
- package/package.json +1 -1
- package/src/lang/README.md +19 -5
- package/src/lang/check.ts +52 -30
- package/src/lang/lower.ts +52 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
- A swizzle can be assigned to: `p.x = 1;`, `c.rgb *= 0.5;`
|
|
8
|
+
- A single number fills a vector in a declaration, an assignment or a uniform default: `float3 c = 0.5;`
|
|
9
|
+
- A float4 goes into a float3 by dropping its fourth component: `float3 c = #ff8040;`
|
|
10
|
+
- A local or a uniform may take a builtin's or a prelude function's name
|
|
11
|
+
|
|
3
12
|
## 0.1.3
|
|
4
13
|
|
|
5
14
|
`onejs-sl/tables` names every parameter, so an editor can offer `lerp(x, y, s)` rather than an argument count. Nothing a program compiles to changes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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/lang/README.md
CHANGED
|
@@ -127,11 +127,25 @@ is read unconverted.
|
|
|
127
127
|
and colours, and nothing else. It is baked into the program before anything
|
|
128
128
|
runs, so it cannot name a const, which could itself name a uniform.
|
|
129
129
|
|
|
130
|
-
**Shadowing is refused.** A local may not take the name of an
|
|
131
|
-
a
|
|
132
|
-
depend on where the question is asked, for no gain in a language whose
|
|
133
|
-
functions are six lines long. A
|
|
134
|
-
|
|
130
|
+
**Shadowing is refused between values.** A local may not take the name of an
|
|
131
|
+
input, a uniform, a texture or a const. Allowing it would make "is this a
|
|
132
|
+
texture?" depend on where the question is asked, for no gain in a language whose
|
|
133
|
+
functions are six lines long. A value *may* take a builtin's or a prelude
|
|
134
|
+
function's name (`float circle`, `uniform float turbulence`), which hides that
|
|
135
|
+
function wherever the value is visible, and a call there says so. A file
|
|
136
|
+
function may shadow a *prelude* function, which is the sanctioned way to replace
|
|
137
|
+
one.
|
|
138
|
+
|
|
139
|
+
**Declarations and assignments convert two ways, and only two.** A single
|
|
140
|
+
number fills every component (`float3 c = 0.5;`, and a uniform default such as
|
|
141
|
+
`uniform float2 offset = 0;`), and a float4 goes into a float3 by dropping its
|
|
142
|
+
fourth component, which is how a colour becomes an rgb. Every other width change
|
|
143
|
+
is refused with the swizzle that fixes it.
|
|
144
|
+
|
|
145
|
+
**A swizzle can be assigned to.** `p.x *= aspect;` rebuilds `p` from the
|
|
146
|
+
components it names and keeps the rest, so it is the same program as
|
|
147
|
+
`p = float2(p.x * aspect, p.y);`. A swizzle that names a component twice, or
|
|
148
|
+
one the local does not have, is refused.
|
|
135
149
|
|
|
136
150
|
## Not yet spellable
|
|
137
151
|
|
package/src/lang/check.ts
CHANGED
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
* whether a name exists, whether a body is shaped like a body, and whether the
|
|
15
15
|
* program fits the budgets before anything is built.
|
|
16
16
|
*
|
|
17
|
-
* SHADOWING IS REFUSED. HLSL would let a local called `time`
|
|
18
|
-
* a parameter called `tint` hide a uniform. Allowing it would
|
|
19
|
-
* question about a name ("is this a texture?") depends on where
|
|
20
|
-
* from, for no expressive gain in a language whose functions are
|
|
21
|
-
* lines long.
|
|
17
|
+
* SHADOWING IS REFUSED, between values. HLSL would let a local called `time`
|
|
18
|
+
* hide the input, or a parameter called `tint` hide a uniform. Allowing it would
|
|
19
|
+
* mean every later question about a name ("is this a texture?") depends on where
|
|
20
|
+
* it is asked from, for no expressive gain in a language whose functions are
|
|
21
|
+
* half a dozen lines long.
|
|
22
|
+
*
|
|
23
|
+
* A value MAY take the name of a builtin or a prelude function: `float circle`,
|
|
24
|
+
* `uniform float turbulence`. Those are the names an author reaches for first,
|
|
25
|
+
* and refusing them was the most common error left once the others were fixed
|
|
26
|
+
* (`Specs/SL_NEXT.md` 2d). Calls and values never meet in one position, so the
|
|
27
|
+
* only question left is a call to the name where the value is visible, and that
|
|
28
|
+
* is refused with the reason.
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
31
|
import { INPUTS } from "../ir"
|
|
@@ -84,8 +91,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
84
91
|
*
|
|
85
92
|
* Functions are NOT in here, because a file function shadowing a prelude
|
|
86
93
|
* function of the same name is the sanctioned way to replace one
|
|
87
|
-
* (`Specs/SL_TEXT.md` 3.8). Every other kind of declaration checks
|
|
88
|
-
*
|
|
94
|
+
* (`Specs/SL_TEXT.md` 3.8). Every other kind of declaration checks
|
|
95
|
+
* `valueClash`, which adds the file's own functions.
|
|
89
96
|
*/
|
|
90
97
|
const taken = (n: string): string | null => {
|
|
91
98
|
if (INPUT_NAMES.has(n)) return "an input"
|
|
@@ -98,14 +105,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
98
105
|
return null
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
|
|
108
|
+
/** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
|
|
109
|
+
const valueClash = (n: string): string | null => {
|
|
110
|
+
const why = taken(n)
|
|
111
|
+
if (why !== null && why !== "a builtin") return why
|
|
102
112
|
const fn = funcs.get(n)
|
|
103
|
-
|
|
104
|
-
return fn.prelude ? "a prelude function" : "a function"
|
|
113
|
+
return fn !== undefined && !fn.prelude ? "a function" : null
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
for (const u of unit.uniforms) {
|
|
108
|
-
const clash =
|
|
117
|
+
const clash = valueClash(u.name)
|
|
109
118
|
if (clash !== null) fail(`"${u.name}" already names ${clash}`, u.pos, u.name.length)
|
|
110
119
|
uniforms.set(u.name, u)
|
|
111
120
|
}
|
|
@@ -121,7 +130,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
for (const t of unit.textures) {
|
|
124
|
-
const clash =
|
|
133
|
+
const clash = valueClash(t.name)
|
|
125
134
|
if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
|
|
126
135
|
textures.set(t.name, t)
|
|
127
136
|
}
|
|
@@ -136,7 +145,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
for (const c of unit.consts) {
|
|
139
|
-
const clash =
|
|
148
|
+
const clash = valueClash(c.name)
|
|
140
149
|
if (clash !== null) fail(`"${c.name}" already names ${clash}`, c.pos, c.name.length)
|
|
141
150
|
consts.set(c.name, c)
|
|
142
151
|
}
|
|
@@ -251,7 +260,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
251
260
|
function checkFunction(fn: FuncDecl): void {
|
|
252
261
|
const params = new Set<string>()
|
|
253
262
|
for (const p of fn.params) {
|
|
254
|
-
const clash =
|
|
263
|
+
const clash = valueClash(p.name)
|
|
255
264
|
if (clash !== null) fail(`"${p.name}" already names ${clash}`, p.pos, p.name.length)
|
|
256
265
|
if (params.has(p.name)) fail(`${fn.name} already has a parameter called "${p.name}"`, p.pos)
|
|
257
266
|
params.add(p.name)
|
|
@@ -275,7 +284,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
275
284
|
switch (s.k) {
|
|
276
285
|
case "var":
|
|
277
286
|
case "const": {
|
|
278
|
-
const clash =
|
|
287
|
+
const clash = valueClash(s.name)
|
|
279
288
|
if (clash !== null) fail(`"${s.name}" already names ${clash}`, s.pos, s.name.length)
|
|
280
289
|
if (scope.has(s.name)) fail(`"${s.name}" is already declared in this body`, s.pos)
|
|
281
290
|
checkExpr(fn, s.init, scope)
|
|
@@ -283,26 +292,32 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
283
292
|
break
|
|
284
293
|
}
|
|
285
294
|
case "assign": {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
)
|
|
295
|
+
// `p.x = 1` and `c.rgb *= 0.5` write the components they name
|
|
296
|
+
// and keep the rest; lowering rebuilds the local from both.
|
|
297
|
+
let target = s.target
|
|
298
|
+
if (target.k === "member") {
|
|
299
|
+
const sw = target.name
|
|
300
|
+
if (!/^([xyzw]{1,4}|[rgba]{1,4})$/.test(sw)) {
|
|
301
|
+
fail(`${sw} is not a swizzle that can be assigned to; name components with xyzw or rgba`, target.pos, sw.length)
|
|
302
|
+
}
|
|
303
|
+
if (new Set(sw).size !== sw.length) {
|
|
304
|
+
fail(`${sw} names a component twice, so assigning to it would write one component two ways`, target.pos, sw.length)
|
|
305
|
+
}
|
|
306
|
+
target = target.obj
|
|
292
307
|
}
|
|
293
|
-
if (
|
|
294
|
-
const n =
|
|
308
|
+
if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
|
|
309
|
+
const n = target.name
|
|
295
310
|
const why = taken(n)
|
|
296
|
-
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`,
|
|
311
|
+
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
|
|
297
312
|
if (counters.has(n)) {
|
|
298
313
|
fail(
|
|
299
314
|
`"${n}" is a loop counter. The loop unrolls and substitutes it as a ` +
|
|
300
315
|
`number, so assigning to it would change nothing. Use another local`,
|
|
301
|
-
|
|
316
|
+
target.pos, n.length,
|
|
302
317
|
)
|
|
303
318
|
}
|
|
304
319
|
if (!scope.has(n)) {
|
|
305
|
-
fail(unknown(n, scope),
|
|
320
|
+
fail(unknown(n, scope), target.pos, n.length)
|
|
306
321
|
}
|
|
307
322
|
checkExpr(fn, s.value, scope)
|
|
308
323
|
break
|
|
@@ -313,7 +328,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
313
328
|
checkBody(fn, s.else, new Set(scope), false)
|
|
314
329
|
break
|
|
315
330
|
case "for": {
|
|
316
|
-
const clash =
|
|
331
|
+
const clash = valueClash(s.counter)
|
|
317
332
|
if (clash !== null) fail(`"${s.counter}" already names ${clash}`, s.pos, s.counter.length)
|
|
318
333
|
if (scope.has(s.counter)) fail(`"${s.counter}" is already declared in this body`, s.pos)
|
|
319
334
|
checkExpr(fn, s.from, scope)
|
|
@@ -435,6 +450,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
435
450
|
)
|
|
436
451
|
}
|
|
437
452
|
|
|
453
|
+
// A value that took a builtin's or a prelude function's name hides it
|
|
454
|
+
// wherever the value is visible.
|
|
455
|
+
const value = scope.has(n) ? "a local" : uniforms.has(n) ? "a uniform" : consts.has(n) ? "a const" : null
|
|
456
|
+
if (value !== null) {
|
|
457
|
+
fail(`"${n}" is ${value} here, so it cannot be called. Rename it to call ${n}(...)`, callee.pos, n.length)
|
|
458
|
+
}
|
|
459
|
+
if (textures.has(n)) {
|
|
460
|
+
fail(`"${n}" is a texture; sample it with \`tex2D(${n}, uv)\``, callee.pos, n.length)
|
|
461
|
+
}
|
|
462
|
+
|
|
438
463
|
const builtin = BUILTINS[n]
|
|
439
464
|
if (builtin !== undefined) {
|
|
440
465
|
if (n === "tex2D") {
|
|
@@ -471,9 +496,6 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
471
496
|
if (SL_GLSL_HINT[n] !== undefined) {
|
|
472
497
|
fail(`${n} is GLSL; this is HLSL, so write ${SL_GLSL_HINT[n]}`, callee.pos, n.length)
|
|
473
498
|
}
|
|
474
|
-
if (textures.has(n)) {
|
|
475
|
-
fail(`"${n}" is a texture; sample it with \`tex2D(${n}, uv)\``, callee.pos, n.length)
|
|
476
|
-
}
|
|
477
499
|
fail(unknown(n, scope), callee.pos, n.length)
|
|
478
500
|
}
|
|
479
501
|
|
package/src/lang/lower.ts
CHANGED
|
@@ -158,7 +158,7 @@ export function lower(checked: Checked): Program {
|
|
|
158
158
|
for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
|
|
159
159
|
|
|
160
160
|
for (const c of unit.consts) {
|
|
161
|
-
const v = lowerExpr(c.init, global)
|
|
161
|
+
const v = fit(lowerExpr(c.init, global), TYPE_WIDTH[c.type], c.pos)
|
|
162
162
|
assertWidth(v, TYPE_WIDTH[c.type], c.name, c.type, c.pos)
|
|
163
163
|
global.declare(c.name, { width: TYPE_WIDTH[c.type], value: v })
|
|
164
164
|
}
|
|
@@ -227,6 +227,8 @@ export function lower(checked: Checked): Program {
|
|
|
227
227
|
init.pos,
|
|
228
228
|
)
|
|
229
229
|
}
|
|
230
|
+
if (c.length === 1 && width > 1) return { components: new Array<number>(width).fill(c[0]!), colour: false }
|
|
231
|
+
if (c.length === 4 && width === 3) return { components: c.slice(0, 3), colour: false }
|
|
230
232
|
if (c.length !== width) {
|
|
231
233
|
fail(
|
|
232
234
|
`this default has ${c.length} component${c.length === 1 ? "" : "s"} and the uniform is ` +
|
|
@@ -284,19 +286,25 @@ export function lower(checked: Checked): Program {
|
|
|
284
286
|
switch (s.k) {
|
|
285
287
|
case "var":
|
|
286
288
|
case "const": {
|
|
287
|
-
const v = lowerExpr(s.init, scope)
|
|
289
|
+
const v = fit(lowerExpr(s.init, scope), TYPE_WIDTH[s.type], s.pos)
|
|
288
290
|
assertWidth(v, TYPE_WIDTH[s.type], s.name, s.type, s.pos)
|
|
289
291
|
scope.declare(s.name, { width: TYPE_WIDTH[s.type], value: v })
|
|
290
292
|
break
|
|
291
293
|
}
|
|
292
294
|
case "assign": {
|
|
293
|
-
|
|
295
|
+
// The checker allowed exactly two shapes: a local, or a
|
|
296
|
+
// swizzle of one with no component named twice.
|
|
297
|
+
const member = s.target.k === "member" ? s.target : null
|
|
298
|
+
const name = ((member?.obj ?? s.target) as Extract<Expr, { k: "ident" }>).name
|
|
294
299
|
const b = scope.lookup(name)!
|
|
295
300
|
let v = lowerExpr(s.value, scope)
|
|
296
301
|
if (s.op !== "=") {
|
|
297
302
|
const op = s.op[0] as ArithOp
|
|
298
|
-
|
|
303
|
+
const old = member === null ? b.value : lowerExpr(member, scope)
|
|
304
|
+
v = at(s.pos, () => arithmetic(op, old, v, s.pos))
|
|
299
305
|
}
|
|
306
|
+
if (member !== null) v = writeComponents(b, member, v, s.pos)
|
|
307
|
+
else v = fit(v, b.width, s.pos)
|
|
300
308
|
assertWidth(v, b.width, name, widthType(b.width), s.pos)
|
|
301
309
|
b.value = v
|
|
302
310
|
break
|
|
@@ -381,13 +389,52 @@ export function lower(checked: Checked): Program {
|
|
|
381
389
|
return typeof a !== "number" && typeof b !== "number" && a.ref === b.ref && a.width === b.width
|
|
382
390
|
}
|
|
383
391
|
|
|
392
|
+
/**
|
|
393
|
+
* The two conversions a declaration or an assignment makes for the author
|
|
394
|
+
* (`Specs/SL_NEXT.md` 2b and 2c): a single number fills every component, as
|
|
395
|
+
* `float3(0.5)` would, and a float4 goes into a float3 by dropping its
|
|
396
|
+
* fourth, which is how a colour (a hex, a ramp, a texture sample) becomes
|
|
397
|
+
* an rgb. Every other width change is left for `assertWidth` to refuse.
|
|
398
|
+
*/
|
|
399
|
+
function fit(v: LV, want: SLType, pos: Pos): LV {
|
|
400
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
401
|
+
if (got === 1 && want > 1) return at(pos, () => compose(want, new Array<LV>(want).fill(v)))
|
|
402
|
+
if (got === 4 && want === 3) return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
|
|
403
|
+
return v
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* `p.x = v`, `c.rgb *= k`: the local rebuilt from the components the
|
|
408
|
+
* swizzle names, taken from `v`, and the rest kept from what it held. A
|
|
409
|
+
* single number fills every named component.
|
|
410
|
+
*/
|
|
411
|
+
function writeComponents(b: Binding, member: Extract<Expr, { k: "member" }>, v: LV, pos: Pos): LV {
|
|
412
|
+
const letters = member.name.replace(/[rgba]/g, (c) => "xyzw"["rgba".indexOf(c)]!)
|
|
413
|
+
const names = "xyzw".slice(0, b.width)
|
|
414
|
+
for (const c of letters) {
|
|
415
|
+
if (!names.includes(c)) {
|
|
416
|
+
fail(`${member.name} writes a component a ${widthType(b.width)} does not have`, member.pos)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
420
|
+
if (got !== 1 && got !== letters.length) {
|
|
421
|
+
fail(`${member.name} is ${letters.length} components and this is a ${widthType(got as SLType)}`, pos)
|
|
422
|
+
}
|
|
423
|
+
const parts: LV[] = [...names].map((c) => {
|
|
424
|
+
const j = letters.indexOf(c)
|
|
425
|
+
if (j < 0) return b.width === 1 ? b.value : at(pos, () => asVal(b.value).swz(c) as unknown as Val)
|
|
426
|
+
return got === 1 ? v : at(pos, () => asVal(v).swz("xyzw"[j]!) as unknown as Val)
|
|
427
|
+
})
|
|
428
|
+
return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
|
|
429
|
+
}
|
|
430
|
+
|
|
384
431
|
function assertWidth(v: LV, want: SLType, name: string, type: TypeName, pos: Pos): void {
|
|
385
432
|
const got = typeof v === "number" ? 1 : v.width
|
|
386
433
|
if (got === want) return
|
|
387
434
|
// A declaration is an assertion, not an input to inference
|
|
388
435
|
// (`Specs/SL_TEXT.md` 3.2), so this says which half is wrong rather
|
|
389
436
|
// than quietly promoting one to the other.
|
|
390
|
-
const hint = got
|
|
437
|
+
const hint = got > want ? ` Take the components you want with a swizzle, as in .${"xyzw".slice(0, want)}.` : ""
|
|
391
438
|
fail(`${name} is declared ${type} and this is a ${widthType(got as SLType)}.${hint}`, pos)
|
|
392
439
|
}
|
|
393
440
|
|