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 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",
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",
@@ -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 input, a uniform,
131
- a texture, a const or a builtin. Allowing it would make "is this a texture?"
132
- depend on where the question is asked, for no gain in a language whose
133
- functions are six lines long. A file function may shadow a *prelude* function,
134
- which is the sanctioned way to replace one.
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` hide the input, or
18
- * a parameter called `tint` hide a uniform. Allowing it would mean every later
19
- * question about a name ("is this a texture?") depends on where it is asked
20
- * from, for no expressive gain in a language whose functions are half a dozen
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 `asFunc`
88
- * as well, so only a function may take a function's name.
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
- const asFunc = (n: string): string | null => {
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
- if (fn === undefined) return null
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 = taken(u.name) ?? asFunc(u.name)
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 = taken(t.name) ?? asFunc(t.name)
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 = taken(c.name) ?? asFunc(c.name)
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 = taken(p.name) ?? asFunc(p.name)
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 = taken(s.name) ?? asFunc(s.name)
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
- if (s.target.k === "member") {
287
- fail(
288
- "a swizzle is read only; build the value you want instead, as in " +
289
- "`p = float2(1, p.y);`",
290
- s.target.pos,
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 (s.target.k !== "ident") fail("only a local can be assigned to", s.target.pos)
294
- const n = s.target.name
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`, s.target.pos, n.length)
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
- s.target.pos, n.length,
316
+ target.pos, n.length,
302
317
  )
303
318
  }
304
319
  if (!scope.has(n)) {
305
- fail(unknown(n, scope), s.target.pos, n.length)
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 = taken(s.counter) ?? asFunc(s.counter)
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
- const name = (s.target as Extract<Expr, { k: "ident" }>).name
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
- v = at(s.pos, () => arithmetic(op, b.value, v, s.pos))
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 === 1 ? ` Wrap it in ${type}(...) to broadcast the value across ${want} components.` : ""
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