onejs-sl 0.3.0 → 0.5.0
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 +29 -0
- package/README.md +70 -28
- package/goldens/goldens.json +147 -63
- package/package.json +1 -1
- package/src/body.ts +132 -26
- package/src/core.ts +1 -1
- package/src/hlsl.ts +7 -3
- package/src/ir.ts +293 -33
- package/src/lang/README.md +2 -2
- package/src/lang/ast.ts +37 -7
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +179 -70
- package/src/lang/glsl.ts +6 -10
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1034 -289
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +48 -4
- package/src/params.ts +2 -0
- package/src/serial.ts +100 -17
- package/src/sl.ts +343 -33
- package/src/structure.ts +285 -0
- package/src/web.ts +152 -33
package/src/lang/check.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { TEXTURE_SLOTS, UNIFORM_SLOTS } from "../ops"
|
|
|
33
33
|
import { SL_GLSL_HINT } from "../ops"
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
35
35
|
import { SL_SDF_SHAPES } from "../shapes"
|
|
36
|
-
import type
|
|
36
|
+
import { TYPE_WIDTH, type Expr, type FuncDecl, type Stmt, type Unit } from "./ast"
|
|
37
37
|
import { readAttributes } from "./attributes"
|
|
38
38
|
import { SLParseError, type Pos, type SLFix } from "./lexer"
|
|
39
39
|
|
|
@@ -49,11 +49,15 @@ const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
|
|
|
49
49
|
* The GLSL spellings whose HLSL name means the same wherever it is written, so
|
|
50
50
|
* the hint can be a one click fix (`Specs/SL_NEXT.md` 5, Decision 1 A). The
|
|
51
51
|
* rest of `SL_GLSL_HINT` gets the hint alone: `mod` floors where `%` truncates,
|
|
52
|
-
* `ivec2` truncates where `float2` does not, `atan` is `atan2` only with two
|
|
53
|
-
* arguments (handled at the call)
|
|
54
|
-
* implemented yet.
|
|
52
|
+
* `ivec2` truncates where `float2` does not, and `atan` is `atan2` only with two
|
|
53
|
+
* arguments (handled at the call).
|
|
55
54
|
*/
|
|
56
|
-
const GLSL_RENAMES = new Set([
|
|
55
|
+
const GLSL_RENAMES = new Set([
|
|
56
|
+
"mix", "fract", "texture", "textureLod", "vec2", "vec3", "vec4", "gl_FragCoord", "iTime", "iResolution",
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/** The builtins whose first argument is a texture, which is not a value anywhere else. */
|
|
60
|
+
const SAMPLES = new Set(["tex2D", "tex2Dlod"])
|
|
57
61
|
|
|
58
62
|
const rename = (from: string, to: string): SLFix => ({ title: `Replace ${from} with ${to}`, replacement: to })
|
|
59
63
|
|
|
@@ -292,8 +296,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
292
296
|
case "const": expr(s.init); return
|
|
293
297
|
case "assign": expr(s.value); return
|
|
294
298
|
case "if": expr(s.cond); s.then.forEach(stmt); s.else.forEach(stmt); return
|
|
295
|
-
case "for": expr(s.from); expr(s.
|
|
299
|
+
case "for": expr(s.from); expr(s.cond); stmt(s.update); s.body.forEach(stmt); return
|
|
300
|
+
case "while": expr(s.cond); s.body.forEach(stmt); return
|
|
301
|
+
case "switch":
|
|
302
|
+
expr(s.value)
|
|
303
|
+
for (const c of s.cases) { c.labels.forEach((l) => { if (l !== null) expr(l) }); c.body.forEach(stmt) }
|
|
304
|
+
return
|
|
296
305
|
case "return": expr(s.value); return
|
|
306
|
+
case "block": s.body.forEach(stmt); return
|
|
307
|
+
case "break":
|
|
308
|
+
case "continue": return
|
|
297
309
|
}
|
|
298
310
|
}
|
|
299
311
|
body.forEach(stmt)
|
|
@@ -310,31 +322,33 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
310
322
|
})
|
|
311
323
|
params.add(p.name)
|
|
312
324
|
}
|
|
313
|
-
checkBody(fn, fn.body, new Set(params),
|
|
325
|
+
checkBody(fn, fn.body, new Set(params), { loop: false, switchCase: false })
|
|
326
|
+
if (!terminates(fn.body)) {
|
|
327
|
+
attempt(() => fail(
|
|
328
|
+
returnsSomewhere(fn.body)
|
|
329
|
+
? `not every way through ${fn.name} returns a ${fn.ret}; add a return at the end`
|
|
330
|
+
: `${fn.name} never returns a ${fn.ret}`,
|
|
331
|
+
fn.pos, fn.name.length,
|
|
332
|
+
))
|
|
333
|
+
}
|
|
314
334
|
}
|
|
315
335
|
|
|
316
336
|
/**
|
|
317
|
-
* `
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* sides, so there is nothing for an early return to skip. Refusing it is
|
|
321
|
-
* what keeps that cost honest rather than pretending a branch happened.
|
|
337
|
+
* `where` says what a break or a continue would leave. A break inside a
|
|
338
|
+
* case, other than the one that ends it, is refused: it would leave the
|
|
339
|
+
* switch early, which an if says more plainly.
|
|
322
340
|
*/
|
|
323
|
-
function checkBody(fn: FuncDecl, body: Stmt[], scope: Set<string>,
|
|
324
|
-
let
|
|
341
|
+
function checkBody(fn: FuncDecl, body: Stmt[], scope: Set<string>, where: { loop: boolean; switchCase: boolean }): void {
|
|
342
|
+
let ended: Stmt | null = null
|
|
325
343
|
for (const s of body) {
|
|
326
|
-
if (
|
|
344
|
+
if (ended !== null) {
|
|
327
345
|
// Once: everything after it is the same mistake.
|
|
328
|
-
attempt(() => fail(
|
|
346
|
+
attempt(() => fail(unreachable(ended!), s.pos))
|
|
329
347
|
break
|
|
330
348
|
}
|
|
331
349
|
attempt(() => checkStmt(s))
|
|
332
350
|
if (s.k === "var" || s.k === "const") scope.add(s.name)
|
|
333
|
-
|
|
334
|
-
if (s.k === "return" && outermost) returned = s
|
|
335
|
-
}
|
|
336
|
-
if (outermost && returned === null) {
|
|
337
|
-
attempt(() => fail(`${fn.name} never returns a ${fn.ret}`, fn.pos, fn.name.length))
|
|
351
|
+
if (terminates([s])) ended = s
|
|
338
352
|
}
|
|
339
353
|
|
|
340
354
|
function checkStmt(s: Stmt): void {
|
|
@@ -347,72 +361,120 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
347
361
|
checkExpr(fn, s.init, scope)
|
|
348
362
|
break
|
|
349
363
|
}
|
|
350
|
-
case "assign":
|
|
351
|
-
|
|
352
|
-
// and keep the rest; lowering rebuilds the local from both.
|
|
353
|
-
let target = s.target
|
|
354
|
-
if (target.k === "member") {
|
|
355
|
-
const sw = target.name
|
|
356
|
-
if (!/^([xyzw]{1,4}|[rgba]{1,4})$/.test(sw)) {
|
|
357
|
-
fail(`${sw} is not a swizzle that can be assigned to; name components with xyzw or rgba`, target.pos, sw.length)
|
|
358
|
-
}
|
|
359
|
-
if (new Set(sw).size !== sw.length) {
|
|
360
|
-
fail(`${sw} names a component twice, so assigning to it would write one component two ways`, target.pos, sw.length)
|
|
361
|
-
}
|
|
362
|
-
target = target.obj
|
|
363
|
-
}
|
|
364
|
-
if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
|
|
365
|
-
const n = target.name
|
|
366
|
-
// A local shadows what it is named after, a builtin or a
|
|
367
|
-
// derived input, so it is assignable like any other local.
|
|
368
|
-
const why = scope.has(n) ? null : taken(n)
|
|
369
|
-
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
|
|
370
|
-
if (counters.has(n)) {
|
|
371
|
-
fail(
|
|
372
|
-
`"${n}" is a loop counter. The loop unrolls and substitutes it as a ` +
|
|
373
|
-
`number, so assigning to it would change nothing. Use another local`,
|
|
374
|
-
target.pos, n.length,
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
378
|
-
checkExpr(fn, s.value, scope)
|
|
364
|
+
case "assign":
|
|
365
|
+
checkAssign(s, scope)
|
|
379
366
|
break
|
|
380
|
-
}
|
|
381
367
|
case "if":
|
|
382
368
|
checkExpr(fn, s.cond, scope)
|
|
383
|
-
checkBody(fn, s.then, new Set(scope),
|
|
384
|
-
checkBody(fn, s.else, new Set(scope),
|
|
369
|
+
checkBody(fn, s.then, new Set(scope), where)
|
|
370
|
+
checkBody(fn, s.else, new Set(scope), where)
|
|
385
371
|
break
|
|
386
372
|
case "for": {
|
|
387
373
|
const clash = valueClash(s.counter)
|
|
388
374
|
if (clash !== null) fail(`"${s.counter}" already names ${clash}`, s.pos, s.counter.length)
|
|
389
375
|
if (scope.has(s.counter)) fail(`"${s.counter}" is already declared in this body`, s.pos)
|
|
390
376
|
checkExpr(fn, s.from, scope)
|
|
391
|
-
checkExpr(fn, s.to, scope)
|
|
392
|
-
checkExpr(fn, s.step, scope)
|
|
393
377
|
const inner = new Set(scope)
|
|
394
378
|
inner.add(s.counter)
|
|
379
|
+
checkExpr(fn, s.cond, inner)
|
|
380
|
+
checkExpr(fn, s.update.value, inner)
|
|
395
381
|
counters.add(s.counter)
|
|
396
382
|
try {
|
|
397
|
-
checkBody(fn, s.body, inner, false)
|
|
383
|
+
checkBody(fn, s.body, new Set(inner), { loop: true, switchCase: false })
|
|
398
384
|
} finally {
|
|
399
385
|
counters.delete(s.counter)
|
|
400
386
|
}
|
|
401
387
|
break
|
|
402
388
|
}
|
|
403
|
-
case "
|
|
404
|
-
|
|
389
|
+
case "while":
|
|
390
|
+
checkExpr(fn, s.cond, scope)
|
|
391
|
+
checkBody(fn, s.body, new Set(scope), { loop: true, switchCase: false })
|
|
392
|
+
break
|
|
393
|
+
case "break":
|
|
394
|
+
if (where.switchCase) {
|
|
405
395
|
fail(
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
|
|
409
|
-
s.pos,
|
|
396
|
+
"this break would leave the switch before the end of its case. Put the rest " +
|
|
397
|
+
"of the case under an if instead",
|
|
398
|
+
s.pos, 5,
|
|
410
399
|
)
|
|
411
400
|
}
|
|
401
|
+
if (!where.loop) fail("break leaves a loop, or ends a switch's case, and this is in neither", s.pos, 5)
|
|
402
|
+
break
|
|
403
|
+
case "continue":
|
|
404
|
+
if (!where.loop) fail("continue starts a loop's next turn, and this is not in a loop", s.pos, 8)
|
|
405
|
+
break
|
|
406
|
+
case "switch": {
|
|
407
|
+
checkExpr(fn, s.value, scope)
|
|
408
|
+
let defaults = 0
|
|
409
|
+
for (const c of s.cases) {
|
|
410
|
+
for (const l of c.labels) {
|
|
411
|
+
if (l === null) {
|
|
412
|
+
defaults++
|
|
413
|
+
if (defaults > 1) fail("this switch already has a default", c.pos, 7)
|
|
414
|
+
} else {
|
|
415
|
+
checkExpr(fn, l, scope)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (c.body.length === 0 && !c.closed) {
|
|
419
|
+
fail("this case has no body; a case that shares the next one's body stacks its label on it", c.pos, 4)
|
|
420
|
+
}
|
|
421
|
+
checkBody(fn, c.body, new Set(scope), { loop: where.loop, switchCase: true })
|
|
422
|
+
if (!c.closed && !terminates(c.body)) {
|
|
423
|
+
fail(
|
|
424
|
+
"this case does not end in break or return. Cases never fall through into the " +
|
|
425
|
+
"next one here, so end it with `break;`",
|
|
426
|
+
c.pos, 4,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
break
|
|
431
|
+
}
|
|
432
|
+
case "return":
|
|
412
433
|
checkExpr(fn, s.value, scope)
|
|
413
434
|
break
|
|
435
|
+
case "block":
|
|
436
|
+
checkBody(fn, s.body, new Set(scope), where)
|
|
437
|
+
break
|
|
414
438
|
}
|
|
415
439
|
}
|
|
440
|
+
|
|
441
|
+
function checkAssign(s: Extract<Stmt, { k: "assign" }>, scope: Set<string>): void {
|
|
442
|
+
// `p.x = 1` and `c.rgb *= 0.5` write the components they name
|
|
443
|
+
// and keep the rest; lowering rebuilds the local from both.
|
|
444
|
+
let target = s.target
|
|
445
|
+
if (target.k === "member") {
|
|
446
|
+
const sw = target.name
|
|
447
|
+
if (!/^([xyzw]{1,4}|[rgba]{1,4})$/.test(sw)) {
|
|
448
|
+
fail(`${sw} is not a swizzle that can be assigned to; name components with xyzw or rgba`, target.pos, sw.length)
|
|
449
|
+
}
|
|
450
|
+
if (new Set(sw).size !== sw.length) {
|
|
451
|
+
fail(`${sw} names a component twice, so assigning to it would write one component two ways`, target.pos, sw.length)
|
|
452
|
+
}
|
|
453
|
+
target = target.obj
|
|
454
|
+
}
|
|
455
|
+
if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
|
|
456
|
+
const n = target.name
|
|
457
|
+
// A local shadows what it is named after, a builtin or a
|
|
458
|
+
// derived input, so it is assignable like any other local.
|
|
459
|
+
const why = scope.has(n) ? null : taken(n)
|
|
460
|
+
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
|
|
461
|
+
if (counters.has(n)) {
|
|
462
|
+
fail(
|
|
463
|
+
`"${n}" is a loop counter, which only the loop's update changes, so the loop's ` +
|
|
464
|
+
`turns can be counted. Use another local`,
|
|
465
|
+
target.pos, n.length,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
469
|
+
checkExpr(fn, s.value, scope)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Why the statement after `s` never runs. */
|
|
474
|
+
function unreachable(s: Stmt): string {
|
|
475
|
+
if (s.k === "return" || s.k === "break" || s.k === "continue") return `this is after the ${s.k}, so it can never run`
|
|
476
|
+
const above = s.k === "block" ? "braces" : s.k
|
|
477
|
+
return `this can never run: every way through the ${above} above ends in a return, break or continue`
|
|
416
478
|
}
|
|
417
479
|
|
|
418
480
|
// MARK: expressions
|
|
@@ -421,13 +483,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
421
483
|
switch (e.k) {
|
|
422
484
|
case "num":
|
|
423
485
|
case "hex":
|
|
486
|
+
case "bool":
|
|
424
487
|
return
|
|
425
488
|
case "ident": {
|
|
426
489
|
if (scope.has(e.name) || INPUT_NAMES.has(e.name) || DERIVED_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
|
|
427
490
|
if (textures.has(e.name)) {
|
|
428
491
|
fail(
|
|
429
492
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
430
|
-
`tex2D. Write \`tex2D(${e.name}, uv)\``,
|
|
493
|
+
`tex2D or tex2Dlod. Write \`tex2D(${e.name}, uv)\``,
|
|
431
494
|
e.pos, e.name.length,
|
|
432
495
|
)
|
|
433
496
|
}
|
|
@@ -458,9 +521,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
458
521
|
// Only reached while collecting: the call was refused, and its
|
|
459
522
|
// arguments are separate questions, so a mistake inside one is
|
|
460
523
|
// not hidden by the call's own. `fract(uv * wrap)` is two
|
|
461
|
-
// mistakes.
|
|
462
|
-
//
|
|
463
|
-
const tex = e.callee.k === "ident" && e.callee.name
|
|
524
|
+
// mistakes. A sample's first argument is the texture, which is
|
|
525
|
+
// not a value.
|
|
526
|
+
const tex = e.callee.k === "ident" && SAMPLES.has(e.callee.name)
|
|
464
527
|
for (const a of tex ? e.args.slice(1) : e.args) attempt(() => checkExpr(fn, a, scope))
|
|
465
528
|
return
|
|
466
529
|
}
|
|
@@ -503,7 +566,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
503
566
|
}
|
|
504
567
|
const n = callee.name
|
|
505
568
|
|
|
506
|
-
|
|
569
|
+
// A constructor, or a conversion: `float3(...)`, `int(x)`, `bool(x)`.
|
|
570
|
+
if (n in TYPE_WIDTH) {
|
|
507
571
|
for (const a of e.args) checkExpr(fn, a, scope)
|
|
508
572
|
if (e.args.length === 0) fail(`${n}() needs at least one component`, e.pos)
|
|
509
573
|
return
|
|
@@ -529,13 +593,22 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
529
593
|
|
|
530
594
|
const builtin = BUILTINS[n]
|
|
531
595
|
if (builtin !== undefined) {
|
|
532
|
-
if (n
|
|
596
|
+
if (SAMPLES.has(n)) {
|
|
533
597
|
const [tex, ...rest] = e.args
|
|
598
|
+
// HLSL's own tex2Dlod packs the level into a float4 with the uv.
|
|
599
|
+
if (n === "tex2Dlod" && e.args.length === 2) {
|
|
600
|
+
fail(
|
|
601
|
+
"tex2Dlod takes the uv and the mip level separately, `tex2Dlod(t, uv, lod)`, " +
|
|
602
|
+
"rather than HLSL's float4(uv, 0, lod)",
|
|
603
|
+
e.pos,
|
|
604
|
+
)
|
|
605
|
+
}
|
|
534
606
|
arity(e, n, builtin.min, builtin.max)
|
|
535
607
|
if (tex === undefined || tex.k !== "ident" || !textures.has(tex.name)) {
|
|
608
|
+
const lod = n === "tex2Dlod" ? ", 0" : ""
|
|
536
609
|
fail(
|
|
537
|
-
|
|
538
|
-
`then
|
|
610
|
+
`${n} samples a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
611
|
+
`then \`${n}(grain, uv${lod})\``,
|
|
539
612
|
(tex ?? e).pos,
|
|
540
613
|
)
|
|
541
614
|
}
|
|
@@ -635,3 +708,39 @@ function distance(a: string, b: string): number {
|
|
|
635
708
|
}
|
|
636
709
|
return rows[a.length]![b.length]!
|
|
637
710
|
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Whether a body ends on every way through it in a return, a break or a
|
|
714
|
+
* continue, so nothing after it runs. A loop never counts: its condition, or
|
|
715
|
+
* its turn limit, can always end it. The lowering reads this too, to put what
|
|
716
|
+
* follows an if into the side that does not end.
|
|
717
|
+
*/
|
|
718
|
+
export function terminates(body: Stmt[]): boolean {
|
|
719
|
+
return body.some((s) => {
|
|
720
|
+
switch (s.k) {
|
|
721
|
+
case "return":
|
|
722
|
+
case "break":
|
|
723
|
+
case "continue": return true
|
|
724
|
+
case "if": return terminates(s.then) && terminates(s.else)
|
|
725
|
+
case "block": return terminates(s.body)
|
|
726
|
+
// A case's break only ends the switch, so it counts only when every
|
|
727
|
+
// case leaves by some other way and a default leaves nothing out.
|
|
728
|
+
case "switch": return s.cases.some((c) => c.labels.includes(null)) && s.cases.every((c) => !c.closed && terminates(c.body))
|
|
729
|
+
default: return false
|
|
730
|
+
}
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function returnsSomewhere(body: Stmt[]): boolean {
|
|
735
|
+
return body.some((s) => {
|
|
736
|
+
switch (s.k) {
|
|
737
|
+
case "return": return true
|
|
738
|
+
case "if": return returnsSomewhere(s.then) || returnsSomewhere(s.else)
|
|
739
|
+
case "block": return returnsSomewhere(s.body)
|
|
740
|
+
case "for":
|
|
741
|
+
case "while": return returnsSomewhere(s.body)
|
|
742
|
+
case "switch": return s.cases.some((c) => returnsSomewhere(c.body))
|
|
743
|
+
default: return false
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
}
|
package/src/lang/glsl.ts
CHANGED
|
@@ -75,10 +75,13 @@ const GLSL_TYPES = new Set(["float", "int", "uint", "bool", "vec2", "vec3", "vec
|
|
|
75
75
|
/** A straight rename, the meaning unchanged. */
|
|
76
76
|
const RENAME: Record<string, string> = {
|
|
77
77
|
vec2: "float2", vec3: "float3", vec4: "float4", mix: "lerp", fract: "frac",
|
|
78
|
-
texture: "tex2D", texture2D: "tex2D", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
78
|
+
texture: "tex2D", texture2D: "tex2D", textureLod: "tex2Dlod", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* A rename that changes what the value can hold, so it is said. `int`, `uint`
|
|
83
|
+
* and `bool` are types here too and are kept; their vectors are not yet.
|
|
84
|
+
*/
|
|
82
85
|
const NARROW: Record<string, [string, string]> = {
|
|
83
86
|
ivec2: ["float2", "an ivec2 is a float2 here, so dividing one no longer truncates"],
|
|
84
87
|
ivec3: ["float3", "an ivec3 is a float3 here, so dividing one no longer truncates"],
|
|
@@ -87,9 +90,6 @@ const NARROW: Record<string, [string, string]> = {
|
|
|
87
90
|
uvec4: ["float4", "a uvec4 is a float4 here"],
|
|
88
91
|
bvec2: ["float2", "a bvec2 is a float2 of 0s and 1s here"], bvec3: ["float3", "a bvec3 is a float3 of 0s and 1s here"],
|
|
89
92
|
bvec4: ["float4", "a bvec4 is a float4 of 0s and 1s here"],
|
|
90
|
-
bool: ["float", "a bool is a float that is 0 or 1 here; a comparison already is one"],
|
|
91
|
-
int: ["float", "an int is a float here, so dividing one no longer truncates; wrap it in floor() where that mattered"],
|
|
92
|
-
uint: ["float", "a uint is a float here"],
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** Shadertoy's inputs with no counterpart yet, and why. */
|
|
@@ -101,7 +101,6 @@ const MISSING: Record<string, string> = {
|
|
|
101
101
|
iChannelTime: "a texture has no time of its own",
|
|
102
102
|
iChannelResolution: "a texture's size cannot be read yet",
|
|
103
103
|
iSampleRate: "there is no audio",
|
|
104
|
-
textureLod: "tex2Dlod is not implemented yet; tex2D samples with the texture's own filtering",
|
|
105
104
|
texelFetch: "a texture cannot be read without filtering yet",
|
|
106
105
|
textureSize: "a texture's size cannot be read yet",
|
|
107
106
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
|
@@ -502,7 +501,7 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
502
501
|
const span = spans.get(i)
|
|
503
502
|
if (span !== undefined && span.end < to) { out += span.render(inEntry); i = span.end; continue }
|
|
504
503
|
if (t.kind === "directive") { out += directive(t); continue }
|
|
505
|
-
if (t.kind === "number") { out += t.text.replace(/
|
|
504
|
+
if (t.kind === "number") { out += t.text.replace(/U$/, "u"); continue }
|
|
506
505
|
if (t.kind !== "ident") { out += t.text; continue }
|
|
507
506
|
|
|
508
507
|
const name = t.text
|
|
@@ -548,9 +547,6 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
548
547
|
continue
|
|
549
548
|
}
|
|
550
549
|
if (NARROW[name] !== undefined) {
|
|
551
|
-
// A for loop's counter is the one int the language has.
|
|
552
|
-
const forCounter = name === "int" && is(back(i - 1), "(") && is(back(back(i - 1) - 1), "for")
|
|
553
|
-
if (forCounter) { out += name; continue }
|
|
554
550
|
const [to2, why] = NARROW[name]!
|
|
555
551
|
noteOnce("narrow:" + name, t.line, why)
|
|
556
552
|
out += to2
|
package/src/lang/lexer.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface Token extends Pos {
|
|
|
31
31
|
text: string
|
|
32
32
|
/** Numbers only: the parsed value. */
|
|
33
33
|
value?: number
|
|
34
|
+
/** Numbers only: written with no point, no exponent and no `f`, as `3` or `0xff`. */
|
|
35
|
+
whole?: boolean
|
|
36
|
+
/** Numbers only: written with a `u`, as `3u`. */
|
|
37
|
+
unsigned?: boolean
|
|
34
38
|
/** Strings only: the text between the quotes, with `\"` and `\\` read. */
|
|
35
39
|
str?: string
|
|
36
40
|
}
|
|
@@ -87,9 +91,10 @@ export class SLParseError extends SLError {
|
|
|
87
91
|
* baffling about the second half.
|
|
88
92
|
*/
|
|
89
93
|
const PUNCT = [
|
|
90
|
-
"
|
|
94
|
+
"<<=", ">>=",
|
|
95
|
+
"<=", ">=", "==", "!=", "&&", "||", "++", "--", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<", ">>",
|
|
91
96
|
"(", ")", "{", "}", "[", "]", ",", ";", ".", "+", "-", "*", "/", "%",
|
|
92
|
-
"<", ">", "!", "?", ":", "=",
|
|
97
|
+
"<", ">", "!", "?", ":", "=", "&", "|", "^", "~",
|
|
93
98
|
]
|
|
94
99
|
|
|
95
100
|
/** Directives that exist in HLSL and deliberately do not exist here. */
|
|
@@ -185,6 +190,19 @@ export function lex(source: string, file: string, keep: boolean): Token[] | Lexe
|
|
|
185
190
|
continue
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
// A whole number in hex, as an integer hash writes its constants: `0x9E3779B9u`.
|
|
194
|
+
if (c === "0" && (source[i + 1] === "x" || source[i + 1] === "X") && isHex(source[i + 2] ?? "")) {
|
|
195
|
+
const start = here()
|
|
196
|
+
let j = i + 2
|
|
197
|
+
while (j < source.length && isHex(source[j]!)) j++
|
|
198
|
+
const text = source.slice(i, j)
|
|
199
|
+
const unsigned = source[j] === "u" || source[j] === "U"
|
|
200
|
+
if (unsigned) j++
|
|
201
|
+
push({ kind: "number", text, value: Number(text), whole: true, unsigned, ...start }, j)
|
|
202
|
+
i = j
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
if (isDigit(c) || (c === "." && isDigit(source[i + 1] ?? ""))) {
|
|
189
207
|
const start = here()
|
|
190
208
|
let j = i
|
|
@@ -196,15 +214,21 @@ export function lex(source: string, file: string, keep: boolean): Token[] | Lexe
|
|
|
196
214
|
if (isDigit(source[k] ?? "")) { k++; while (k < source.length && isDigit(source[k]!)) k++; j = k }
|
|
197
215
|
}
|
|
198
216
|
const text = source.slice(i, j)
|
|
217
|
+
let whole = /^[0-9]+$/.test(text)
|
|
218
|
+
let unsigned = false
|
|
199
219
|
// HLSL's float suffix. Accepted and dropped: `1.0f` is the same
|
|
200
220
|
// number, and refusing it would only teach the author that this is
|
|
201
|
-
// not quite the language they think it is.
|
|
202
|
-
if (source[j] === "f" || source[j] === "F") {
|
|
203
|
-
|
|
221
|
+
// not quite the language they think it is. It does make a float.
|
|
222
|
+
if ((source[j] === "f" || source[j] === "F") && !isIdentPart(source[j + 1] ?? "")) {
|
|
223
|
+
j++
|
|
224
|
+
whole = false
|
|
225
|
+
} else if ((source[j] === "u" || source[j] === "U") && whole && !isIdentPart(source[j + 1] ?? "")) {
|
|
226
|
+
j++
|
|
227
|
+
unsigned = true
|
|
204
228
|
}
|
|
205
229
|
const value = Number(text)
|
|
206
230
|
if (!Number.isFinite(value)) fail(`"${text}" is not a number`, start, text.length)
|
|
207
|
-
else push({ kind: "number", text, value, ...start }, j)
|
|
231
|
+
else push({ kind: "number", text, value, whole, unsigned, ...start }, j)
|
|
208
232
|
i = j
|
|
209
233
|
continue
|
|
210
234
|
}
|