onejs-sl 0.4.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 +14 -0
- package/README.md +35 -18
- package/goldens/goldens.json +52 -3
- package/package.json +1 -1
- package/src/body.ts +103 -20
- package/src/hlsl.ts +1 -0
- package/src/ir.ts +170 -26
- package/src/lang/ast.ts +37 -7
- package/src/lang/check.ts +155 -59
- package/src/lang/glsl.ts +5 -8
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1015 -266
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +44 -1
- package/src/serial.ts +39 -8
- package/src/sl.ts +255 -11
- package/src/structure.ts +285 -0
- package/src/web.ts +108 -22
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
|
|
|
@@ -296,8 +296,16 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
296
296
|
case "const": expr(s.init); return
|
|
297
297
|
case "assign": expr(s.value); return
|
|
298
298
|
case "if": expr(s.cond); s.then.forEach(stmt); s.else.forEach(stmt); return
|
|
299
|
-
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
|
|
300
305
|
case "return": expr(s.value); return
|
|
306
|
+
case "block": s.body.forEach(stmt); return
|
|
307
|
+
case "break":
|
|
308
|
+
case "continue": return
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
body.forEach(stmt)
|
|
@@ -314,31 +322,33 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
314
322
|
})
|
|
315
323
|
params.add(p.name)
|
|
316
324
|
}
|
|
317
|
-
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
|
+
}
|
|
318
334
|
}
|
|
319
335
|
|
|
320
336
|
/**
|
|
321
|
-
* `
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* sides, so there is nothing for an early return to skip. Refusing it is
|
|
325
|
-
* 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.
|
|
326
340
|
*/
|
|
327
|
-
function checkBody(fn: FuncDecl, body: Stmt[], scope: Set<string>,
|
|
328
|
-
let
|
|
341
|
+
function checkBody(fn: FuncDecl, body: Stmt[], scope: Set<string>, where: { loop: boolean; switchCase: boolean }): void {
|
|
342
|
+
let ended: Stmt | null = null
|
|
329
343
|
for (const s of body) {
|
|
330
|
-
if (
|
|
344
|
+
if (ended !== null) {
|
|
331
345
|
// Once: everything after it is the same mistake.
|
|
332
|
-
attempt(() => fail(
|
|
346
|
+
attempt(() => fail(unreachable(ended!), s.pos))
|
|
333
347
|
break
|
|
334
348
|
}
|
|
335
349
|
attempt(() => checkStmt(s))
|
|
336
350
|
if (s.k === "var" || s.k === "const") scope.add(s.name)
|
|
337
|
-
|
|
338
|
-
if (s.k === "return" && outermost) returned = s
|
|
339
|
-
}
|
|
340
|
-
if (outermost && returned === null) {
|
|
341
|
-
attempt(() => fail(`${fn.name} never returns a ${fn.ret}`, fn.pos, fn.name.length))
|
|
351
|
+
if (terminates([s])) ended = s
|
|
342
352
|
}
|
|
343
353
|
|
|
344
354
|
function checkStmt(s: Stmt): void {
|
|
@@ -351,80 +361,129 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
351
361
|
checkExpr(fn, s.init, scope)
|
|
352
362
|
break
|
|
353
363
|
}
|
|
354
|
-
case "assign":
|
|
355
|
-
|
|
356
|
-
// and keep the rest; lowering rebuilds the local from both.
|
|
357
|
-
let target = s.target
|
|
358
|
-
if (target.k === "member") {
|
|
359
|
-
const sw = target.name
|
|
360
|
-
if (!/^([xyzw]{1,4}|[rgba]{1,4})$/.test(sw)) {
|
|
361
|
-
fail(`${sw} is not a swizzle that can be assigned to; name components with xyzw or rgba`, target.pos, sw.length)
|
|
362
|
-
}
|
|
363
|
-
if (new Set(sw).size !== sw.length) {
|
|
364
|
-
fail(`${sw} names a component twice, so assigning to it would write one component two ways`, target.pos, sw.length)
|
|
365
|
-
}
|
|
366
|
-
target = target.obj
|
|
367
|
-
}
|
|
368
|
-
if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
|
|
369
|
-
const n = target.name
|
|
370
|
-
// A local shadows what it is named after, a builtin or a
|
|
371
|
-
// derived input, so it is assignable like any other local.
|
|
372
|
-
const why = scope.has(n) ? null : taken(n)
|
|
373
|
-
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
|
|
374
|
-
if (counters.has(n)) {
|
|
375
|
-
fail(
|
|
376
|
-
`"${n}" is a loop counter. The loop unrolls and substitutes it as a ` +
|
|
377
|
-
`number, so assigning to it would change nothing. Use another local`,
|
|
378
|
-
target.pos, n.length,
|
|
379
|
-
)
|
|
380
|
-
}
|
|
381
|
-
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
382
|
-
checkExpr(fn, s.value, scope)
|
|
364
|
+
case "assign":
|
|
365
|
+
checkAssign(s, scope)
|
|
383
366
|
break
|
|
384
|
-
}
|
|
385
367
|
case "if":
|
|
386
368
|
checkExpr(fn, s.cond, scope)
|
|
387
|
-
checkBody(fn, s.then, new Set(scope),
|
|
388
|
-
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)
|
|
389
371
|
break
|
|
390
372
|
case "for": {
|
|
391
373
|
const clash = valueClash(s.counter)
|
|
392
374
|
if (clash !== null) fail(`"${s.counter}" already names ${clash}`, s.pos, s.counter.length)
|
|
393
375
|
if (scope.has(s.counter)) fail(`"${s.counter}" is already declared in this body`, s.pos)
|
|
394
376
|
checkExpr(fn, s.from, scope)
|
|
395
|
-
checkExpr(fn, s.to, scope)
|
|
396
|
-
checkExpr(fn, s.step, scope)
|
|
397
377
|
const inner = new Set(scope)
|
|
398
378
|
inner.add(s.counter)
|
|
379
|
+
checkExpr(fn, s.cond, inner)
|
|
380
|
+
checkExpr(fn, s.update.value, inner)
|
|
399
381
|
counters.add(s.counter)
|
|
400
382
|
try {
|
|
401
|
-
checkBody(fn, s.body, inner, false)
|
|
383
|
+
checkBody(fn, s.body, new Set(inner), { loop: true, switchCase: false })
|
|
402
384
|
} finally {
|
|
403
385
|
counters.delete(s.counter)
|
|
404
386
|
}
|
|
405
387
|
break
|
|
406
388
|
}
|
|
407
|
-
case "
|
|
408
|
-
|
|
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) {
|
|
409
395
|
fail(
|
|
410
|
-
"
|
|
411
|
-
"
|
|
412
|
-
|
|
413
|
-
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,
|
|
414
399
|
)
|
|
415
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":
|
|
416
433
|
checkExpr(fn, s.value, scope)
|
|
417
434
|
break
|
|
435
|
+
case "block":
|
|
436
|
+
checkBody(fn, s.body, new Set(scope), where)
|
|
437
|
+
break
|
|
438
|
+
}
|
|
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
|
+
)
|
|
418
467
|
}
|
|
468
|
+
if (!scope.has(n)) unknown(n, scope, target.pos)
|
|
469
|
+
checkExpr(fn, s.value, scope)
|
|
419
470
|
}
|
|
420
471
|
}
|
|
421
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`
|
|
478
|
+
}
|
|
479
|
+
|
|
422
480
|
// MARK: expressions
|
|
423
481
|
|
|
424
482
|
function checkExpr(fn: FuncDecl, e: Expr, scope: Set<string>): void {
|
|
425
483
|
switch (e.k) {
|
|
426
484
|
case "num":
|
|
427
485
|
case "hex":
|
|
486
|
+
case "bool":
|
|
428
487
|
return
|
|
429
488
|
case "ident": {
|
|
430
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
|
|
@@ -507,7 +566,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
507
566
|
}
|
|
508
567
|
const n = callee.name
|
|
509
568
|
|
|
510
|
-
|
|
569
|
+
// A constructor, or a conversion: `float3(...)`, `int(x)`, `bool(x)`.
|
|
570
|
+
if (n in TYPE_WIDTH) {
|
|
511
571
|
for (const a of e.args) checkExpr(fn, a, scope)
|
|
512
572
|
if (e.args.length === 0) fail(`${n}() needs at least one component`, e.pos)
|
|
513
573
|
return
|
|
@@ -648,3 +708,39 @@ function distance(a: string, b: string): number {
|
|
|
648
708
|
}
|
|
649
709
|
return rows[a.length]![b.length]!
|
|
650
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
|
@@ -78,7 +78,10 @@ const RENAME: Record<string, string> = {
|
|
|
78
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. */
|
|
@@ -501,7 +501,7 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
501
501
|
const span = spans.get(i)
|
|
502
502
|
if (span !== undefined && span.end < to) { out += span.render(inEntry); i = span.end; continue }
|
|
503
503
|
if (t.kind === "directive") { out += directive(t); continue }
|
|
504
|
-
if (t.kind === "number") { out += t.text.replace(/
|
|
504
|
+
if (t.kind === "number") { out += t.text.replace(/U$/, "u"); continue }
|
|
505
505
|
if (t.kind !== "ident") { out += t.text; continue }
|
|
506
506
|
|
|
507
507
|
const name = t.text
|
|
@@ -547,9 +547,6 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
547
547
|
continue
|
|
548
548
|
}
|
|
549
549
|
if (NARROW[name] !== undefined) {
|
|
550
|
-
// A for loop's counter is the one int the language has.
|
|
551
|
-
const forCounter = name === "int" && is(back(i - 1), "(") && is(back(back(i - 1) - 1), "for")
|
|
552
|
-
if (forCounter) { out += name; continue }
|
|
553
550
|
const [to2, why] = NARROW[name]!
|
|
554
551
|
noteOnce("narrow:" + name, t.line, why)
|
|
555
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
|
}
|