onejs-sl 0.5.0 → 0.6.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 +11 -0
- package/README.md +6 -1
- package/goldens/goldens.json +1 -1
- package/package.json +1 -1
- package/src/ir.ts +28 -1
- package/src/lang/lower.ts +96 -38
- package/src/serial.ts +7 -1
- package/src/sl.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
An int uniform says it is one, and uint constants fold as int ones do. A `.sl` program whose uint constants now fold gets a new hash and shader, which a build regenerates; every other program keeps its hash and its shader, and a program built in code keeps both, since the fold is the `.sl` file's. That hash change and `sl.uniform.int` make this a minor.
|
|
6
|
+
|
|
7
|
+
- An int uniform's declaration says so: `kind: "int"` on the `UniformDecl`, which `toJSON` and `fromJSON` keep, so a host can show a whole number field. A float uniform has no `kind`, and the hash does not read it; an older `fromJSON` ignores it
|
|
8
|
+
- An int uniform's default is refused when its float slot cannot hold it exactly (16777217, or anything past 2147483520), since the program would read another number
|
|
9
|
+
- `sl.uniform.int(name, value, control)`, which `uniform int` in a `.sl` file lowers to
|
|
10
|
+
- A uint constant folds as an int one does: `float(5u)` is 5 and `float(3u + 4u)` is 7, with no node. Arithmetic, bit operators, comparisons and conversions between constant uints, ints and floats come out as the value, on 32 bits and wrapping; a zero divisor gives 0, as a computed one does
|
|
11
|
+
- `uint(k)` of a negative constant int keeps its bits (`uint(-1)` through an int is 4294967295), as the GPU does, instead of 0
|
|
12
|
+
- A `for` counting a uint between constant bounds unrolls, as one counting an int does
|
|
13
|
+
|
|
3
14
|
## 0.5.0
|
|
4
15
|
|
|
5
16
|
Real control flow: an `if` runs only the side it picks, a loop that cannot unroll runs as a loop, and `int`, `uint` and `bool` are types. A program with an `if` statement gets a new hash, which a build regenerates; every other program keeps its hash and its shader.
|
package/README.md
CHANGED
|
@@ -76,7 +76,12 @@ supplies (`frac`, `lerp`).
|
|
|
76
76
|
|
|
77
77
|
A uniform declared with a hex default (`uniform float4 tint = #ff8040;`), or
|
|
78
78
|
with `sl.uniform.colour`, is marked `colour: true`: its value is sRGB as
|
|
79
|
-
written, which is what a colour picker shows, and its reads convert.
|
|
79
|
+
written, which is what a colour picker shows, and its reads convert. A
|
|
80
|
+
`uniform int`, or `sl.uniform.int`, is marked `kind: "int"`: its slot is a float
|
|
81
|
+
like any other, its default is a whole number that slot holds exactly, and the
|
|
82
|
+
program reads it as one, so a host shows a whole number field; a float uniform
|
|
83
|
+
has no `kind`. Neither mark is in the hash, since the reads' conversion already
|
|
84
|
+
is. `inputsUsed`
|
|
80
85
|
says which inputs the result depends on, dead nodes aside, so a host knows
|
|
81
86
|
whether a program animates.
|
|
82
87
|
|
package/goldens/goldens.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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 or GLSL ES",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
package/src/ir.ts
CHANGED
|
@@ -225,6 +225,27 @@ export interface UniformDecl extends UniformControl {
|
|
|
225
225
|
* conversion is already in the graph, so it is not part of the hash.
|
|
226
226
|
*/
|
|
227
227
|
colour?: true
|
|
228
|
+
/**
|
|
229
|
+
* An int: `value` is a whole number and the program reads it as one, so a
|
|
230
|
+
* host shows a whole number field rather than a float's. The slot is a
|
|
231
|
+
* float, as every host binds it, and `type` is FLOAT. Set by `uniform int`
|
|
232
|
+
* in a `.sl` file or `sl.uniform.int`. Metadata for a host like `colour`:
|
|
233
|
+
* the reads' conversion to an int is already in the graph, so it is not
|
|
234
|
+
* part of the hash. A float uniform has none.
|
|
235
|
+
*/
|
|
236
|
+
kind?: "int"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** What is wrong with an int uniform of `type` and default `value`, or null. The one check, for the builder and `fromJSON`. */
|
|
240
|
+
export function intUniformProblem(type: SLType, value: readonly number[]): string | null {
|
|
241
|
+
if (type !== TYPE.FLOAT) return `an int uniform is one number, and this is a ${widthName(type)}`
|
|
242
|
+
const v = value[0]!
|
|
243
|
+
// Its slot is a float, and a read is held to INT_BOUNDS: a default the
|
|
244
|
+
// program would read as another number is refused rather than shown.
|
|
245
|
+
if (!Number.isInteger(v) || Math.fround(v) !== v || v < INT_BOUNDS.int[0] || v > INT_BOUNDS.int[1]) {
|
|
246
|
+
return `an int uniform's default is a whole number its float slot holds exactly, and this is ${v}`
|
|
247
|
+
}
|
|
248
|
+
return null
|
|
228
249
|
}
|
|
229
250
|
|
|
230
251
|
export interface TextureDecl {
|
|
@@ -358,7 +379,7 @@ export class Builder {
|
|
|
358
379
|
return this.add(node)
|
|
359
380
|
}
|
|
360
381
|
|
|
361
|
-
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
|
|
382
|
+
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}, kind?: "int"): NodeRef {
|
|
362
383
|
const existing = this.uniforms.findIndex((u) => u.name === name)
|
|
363
384
|
if (existing >= 0) {
|
|
364
385
|
const u = this.uniforms[existing]
|
|
@@ -368,6 +389,9 @@ export class Builder {
|
|
|
368
389
|
if ((u.colour === true) !== colour) {
|
|
369
390
|
throw new SLError(`uniform "${name}" is declared both as a colour and as plain numbers`)
|
|
370
391
|
}
|
|
392
|
+
if (u.kind !== kind) {
|
|
393
|
+
throw new SLError(`uniform "${name}" is declared as both an int and a float`)
|
|
394
|
+
}
|
|
371
395
|
// A second read may say nothing about the control, or the same thing again.
|
|
372
396
|
const merged = { ...controlOf(u) }
|
|
373
397
|
for (const f of CONTROL_FIELDS) {
|
|
@@ -384,10 +408,13 @@ export class Builder {
|
|
|
384
408
|
}
|
|
385
409
|
const problem = controlProblem(type, value, control)
|
|
386
410
|
if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
|
|
411
|
+
const kindProblem = kind === undefined ? null : intUniformProblem(type, value)
|
|
412
|
+
if (kindProblem !== null) throw new SLError(`uniform "${name}": ${kindProblem}`)
|
|
387
413
|
const slot = this.uniforms.length
|
|
388
414
|
if (slot >= UNIFORM_SLOTS) throw new SLError(tooManyUniforms(slot + 1, "program"))
|
|
389
415
|
const decl: UniformDecl = { name, type, value: value.slice() }
|
|
390
416
|
if (colour) decl.colour = true
|
|
417
|
+
if (kind !== undefined) decl.kind = kind
|
|
391
418
|
Object.assign(decl, controlOf(control))
|
|
392
419
|
this.uniforms.push(decl)
|
|
393
420
|
return this.add({ k: "uniform", type, slot })
|
package/src/lang/lower.ts
CHANGED
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
* `uv.mul(8)`, not as `uv.mul(sl.float(8))`, because the first broadcasts to a
|
|
40
40
|
* float2 constant and the second builds a float plus a swizzle. Same picture,
|
|
41
41
|
* different graph, different hash, so the distinction is load bearing rather
|
|
42
|
-
* than an optimisation. An int constant is a bigint
|
|
43
|
-
* boolean for the same reason: folded until something
|
|
42
|
+
* than an optimisation. An int constant is a bigint, a uint constant a `Uint`
|
|
43
|
+
* and a bool constant a boolean for the same reason: folded until something
|
|
44
|
+
* needs a node.
|
|
44
45
|
*
|
|
45
46
|
* A COMPARISON WAITS to be used (`Test`). Where arithmetic or a `?:` reads it,
|
|
46
47
|
* it is the float that is 0 or 1 it always was, built the way it always was,
|
|
@@ -80,17 +81,30 @@ class Test {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
* A
|
|
84
|
-
*
|
|
85
|
-
* either of its forms.
|
|
84
|
+
* A uint constant, 0 to 2^32 - 1, not yet a node. A bigint is already an int,
|
|
85
|
+
* so a uint needs a form of its own to fold the way an int does.
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
class Uint {
|
|
88
|
+
readonly v: bigint
|
|
89
|
+
/**
|
|
90
|
+
* Any whole number, wrapped to 32 bits as a uint wraps. By a mask, not
|
|
91
|
+
* `BigInt.asUintN`, which QuickJS-ng answers with a negative from 2^31 up.
|
|
92
|
+
*/
|
|
93
|
+
constructor(v: bigint) { this.v = v & 0xffffffffn }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A lowered value. A number is a float, a bigint an int, a Uint a uint and a
|
|
98
|
+
* boolean a bool, none of which has had to become a node yet; a Test is a
|
|
99
|
+
* comparison not yet either of its forms.
|
|
100
|
+
*/
|
|
101
|
+
type LV = Val | number | bigint | Uint | boolean | Test
|
|
88
102
|
|
|
89
103
|
/** A declared type: its width, and its kind when it is not floats. */
|
|
90
104
|
interface VType { width: SLType; kind: SLKind | undefined }
|
|
91
105
|
|
|
92
106
|
interface Binding extends VType {
|
|
93
|
-
/** Always of the declared type: a Test is resolved and an int is a bigint or an int node. */
|
|
107
|
+
/** Always of the declared type: a Test is resolved and an int is a bigint or an int node, a uint a Uint or a uint node. */
|
|
94
108
|
value: LV
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -205,7 +219,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
205
219
|
const blank = (t: VType, pos: Pos): LV => {
|
|
206
220
|
if (t.kind === "int") return 0n
|
|
207
221
|
if (t.kind === "bool") return false
|
|
208
|
-
if (t.kind === "uint") return
|
|
222
|
+
if (t.kind === "uint") return new Uint(0n)
|
|
209
223
|
return t.width === 1 ? 0 : at(pos, () => compose(t.width, new Array<LV>(t.width).fill(0)))
|
|
210
224
|
}
|
|
211
225
|
|
|
@@ -298,7 +312,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
298
312
|
case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!], control) as unknown as Val
|
|
299
313
|
case "float4": return sl.uniform.vec4(name, [c[0]!, c[1]!, c[2]!, c[3]!], control) as unknown as Val
|
|
300
314
|
// A float slot, as every host binds, read as the whole number it holds.
|
|
301
|
-
case "int": return sl.
|
|
315
|
+
case "int": return sl.uniform.int(name, c[0]!, control) as unknown as Val
|
|
302
316
|
case "uint":
|
|
303
317
|
case "bool": throw new Error(`the checker let a ${type} uniform through`)
|
|
304
318
|
}
|
|
@@ -585,7 +599,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
585
599
|
const got = widthOf(v)
|
|
586
600
|
if (got !== t.width) {
|
|
587
601
|
if (fn === main) {
|
|
588
|
-
const what =
|
|
602
|
+
const what = isConstant(v) ? "a single number" : `a ${widthType(got)}`
|
|
589
603
|
fail(`main returns a float4, a colour with alpha, and this returns ${what}. Wrap it: float4(value, 1)`, pos, 6)
|
|
590
604
|
}
|
|
591
605
|
fail(`${fn.name} is declared to return a ${fn.ret} and returns a ${widthType(got)}`, fn.pos)
|
|
@@ -755,8 +769,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
755
769
|
if (c.labels.includes(null)) { otherwise = c.body; continue }
|
|
756
770
|
let cond: Expr | null = null
|
|
757
771
|
for (const l of c.labels as Expr[]) {
|
|
758
|
-
const k = lowerExpr(l, scope, v
|
|
759
|
-
const key = typeof k === "bigint" ? k : k instanceof Val ? constValue(k) : null
|
|
772
|
+
const k = lowerExpr(l, scope, kindWant(v))
|
|
773
|
+
const key = typeof k === "bigint" ? k : k instanceof Uint ? k.v : k instanceof Val ? constValue(k) : null
|
|
760
774
|
if (key === null) fail("a case label is a whole number, or a const int", l.pos)
|
|
761
775
|
if (seen.has(key)) fail(`this switch already has a case ${key}`, l.pos)
|
|
762
776
|
seen.add(key)
|
|
@@ -795,13 +809,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
795
809
|
}
|
|
796
810
|
case "int":
|
|
797
811
|
if (isInt(v)) return v
|
|
798
|
-
if (v
|
|
812
|
+
if (isUint(v)) fail(`${name} is an int and this is a uint; convert it with int(...)`, pos)
|
|
799
813
|
if (isBoolish(v)) fail(`${name} is an int and this is a bool; convert it with int(...)`, pos)
|
|
800
814
|
fail(`${name} is an int and this is a ${widthType(widthOf(v))}; convert it with int(...), which truncates toward zero`, pos)
|
|
801
815
|
break
|
|
802
816
|
case "uint":
|
|
803
|
-
if (v
|
|
804
|
-
if (typeof v === "bigint" && v >= 0n) return
|
|
817
|
+
if (isUint(v)) return v
|
|
818
|
+
if (typeof v === "bigint" && v >= 0n) return new Uint(v)
|
|
805
819
|
fail(`${name} is a uint and this is ${describeLV(v)}; convert it with uint(...)`, pos)
|
|
806
820
|
break
|
|
807
821
|
case "bool": {
|
|
@@ -820,6 +834,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
820
834
|
function toFloat(v: LV, pos: Pos): number | Val {
|
|
821
835
|
if (typeof v === "number") return v
|
|
822
836
|
if (typeof v === "bigint") return Number(v)
|
|
837
|
+
if (v instanceof Uint) return Number(v.v)
|
|
823
838
|
if (typeof v === "boolean") return v ? 1 : 0
|
|
824
839
|
if (v instanceof Test) return v.float
|
|
825
840
|
return v.kind === undefined ? v : at(pos, () => sl.float(v) as unknown as Val)
|
|
@@ -839,6 +854,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
839
854
|
if (typeof v === "boolean") return v
|
|
840
855
|
if (typeof v === "number") return v >= 0.5
|
|
841
856
|
if (typeof v === "bigint") return v !== 0n
|
|
857
|
+
if (v instanceof Uint) return v.v !== 0n
|
|
842
858
|
if (v instanceof Test) return v.bool
|
|
843
859
|
if (v.kind === "bool") return v
|
|
844
860
|
return at(pos, () => (v.kind === undefined ? v.ge(0.5) : v.ne(0)) as unknown as Val)
|
|
@@ -898,9 +914,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
898
914
|
function lowerExpr(e: Expr, scope: Scope, want?: "int" | "uint"): LV {
|
|
899
915
|
switch (e.k) {
|
|
900
916
|
case "num":
|
|
901
|
-
if (e.unsigned === true
|
|
917
|
+
if (e.unsigned === true || (e.whole && want === "uint")) return new Uint(BigInt(e.value))
|
|
902
918
|
if (e.whole && want === "int") return BigInt.asIntN(32, BigInt(e.value))
|
|
903
|
-
if (e.whole && want === "uint") return at(e.pos, () => sl.uint(e.value) as unknown as Val)
|
|
904
919
|
return e.value
|
|
905
920
|
case "bool": return e.value
|
|
906
921
|
case "hex": return at(e.pos, () => sl.color(e.hex) as unknown as Val)
|
|
@@ -927,18 +942,22 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
927
942
|
case "-":
|
|
928
943
|
if (typeof v === "number") return -v
|
|
929
944
|
if (typeof v === "bigint") return BigInt.asIntN(32, -v)
|
|
945
|
+
// As the node would: a uint negated is taken as a float.
|
|
946
|
+
if (v instanceof Uint) return -Number(v.v)
|
|
930
947
|
if (v instanceof Val && v.kind !== "bool") return at(e.pos, () => v.neg())
|
|
931
948
|
return at(e.pos, () => asVal(toFloat(v, e.pos)).neg())
|
|
932
949
|
case "!":
|
|
933
950
|
if (typeof v === "boolean") return !v
|
|
934
951
|
if (typeof v === "number") return 1 - v
|
|
935
952
|
if (typeof v === "bigint") return v === 0n
|
|
953
|
+
if (v instanceof Uint) return v.v === 0n
|
|
936
954
|
if (v instanceof Test) return new Test(v.width, () => oneMinus(v.float, e.pos), () => notBool(v.bool))
|
|
937
955
|
if (v.kind === "bool") return at(e.pos, () => v.not())
|
|
938
956
|
if (v.kind === undefined) return oneMinus(v, e.pos)
|
|
939
957
|
return at(e.pos, () => v.eq(0))
|
|
940
958
|
case "~":
|
|
941
959
|
if (typeof v === "bigint") return BigInt.asIntN(32, ~v)
|
|
960
|
+
if (v instanceof Uint) return new Uint(~v.v)
|
|
942
961
|
if (v instanceof Val && (v.kind === "int" || v.kind === "uint")) return at(e.pos, () => v.bnot())
|
|
943
962
|
fail(`~ flips the bits of an int or a uint, and this is ${describeLV(v)}`, e.pos)
|
|
944
963
|
}
|
|
@@ -978,7 +997,11 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
978
997
|
}
|
|
979
998
|
if (isUint(a) || isUint(b)) {
|
|
980
999
|
if (!isIntLike(a) || !isIntLike(b)) fail(`a uint only meets another uint here; convert with uint(...) or float(...)`, pos)
|
|
981
|
-
|
|
1000
|
+
if (a instanceof Uint && b instanceof Uint) {
|
|
1001
|
+
if ((op === "/" || op === "%") && b.v === 0n) return new Uint(0n)
|
|
1002
|
+
return new Uint(INT_ARITH[op](a.v, b.v))
|
|
1003
|
+
}
|
|
1004
|
+
return at(pos, () => intVal(a)[METHOD[op]](other(b)))
|
|
982
1005
|
}
|
|
983
1006
|
return floatArithmetic(op, toFloat(a, pos), toFloat(b, pos), pos)
|
|
984
1007
|
}
|
|
@@ -1002,8 +1025,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1002
1025
|
if (!isIntLike(v)) fail(`${op} works on the bits of an int or a uint, and this is ${describeLV(v)}${isBoolish(v) ? "; use && or ||" : ""}`, pos)
|
|
1003
1026
|
}
|
|
1004
1027
|
if (typeof a === "bigint" && typeof b === "bigint") return BigInt.asIntN(32, INT_BITS[op](a, b))
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1028
|
+
// A shift's count may be either kind, and the value shifted keeps its own.
|
|
1029
|
+
const shift = op === "<<" || op === ">>"
|
|
1030
|
+
const count = typeof b === "bigint" ? b : b instanceof Uint ? b.v : null
|
|
1031
|
+
if (a instanceof Uint && count !== null && (shift || b instanceof Uint)) return new Uint(INT_BITS[op](a.v, count))
|
|
1032
|
+
if (typeof a === "bigint" && shift && b instanceof Uint) return BigInt.asIntN(32, INT_BITS[op](a, b.v))
|
|
1033
|
+
const x = intVal(a as bigint | Uint | Val)
|
|
1034
|
+
const y = other(b as bigint | Uint | Val)
|
|
1007
1035
|
return at(pos, () => {
|
|
1008
1036
|
switch (op) {
|
|
1009
1037
|
case "&": return x.band(y)
|
|
@@ -1025,7 +1053,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1025
1053
|
return at(pos, () => intVal(a)[CMP_METHOD[op]!](typeof b === "bigint" ? Number(b) : b as Val) as unknown as Val)
|
|
1026
1054
|
}
|
|
1027
1055
|
if ((isUint(a) || isUint(b)) && isIntLike(a) && isIntLike(b)) {
|
|
1028
|
-
|
|
1056
|
+
if (a instanceof Uint && b instanceof Uint) return INT_CMP[op]!(a.v, b.v)
|
|
1057
|
+
return at(pos, () => intVal(a)[CMP_METHOD[op]!](other(b)) as unknown as Val)
|
|
1029
1058
|
}
|
|
1030
1059
|
const fa = toFloat(a, pos)
|
|
1031
1060
|
const fb = toFloat(b, pos)
|
|
@@ -1082,7 +1111,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1082
1111
|
/** `c ? a : b`: always a select, both sides computed. A float condition is the select it always was. */
|
|
1083
1112
|
function lowerChoice(e: Extract<Expr, { k: "cond" }>, scope: Scope, want?: "int" | "uint"): LV {
|
|
1084
1113
|
const cond = lowerExpr(e.cond, scope)
|
|
1085
|
-
if (typeof cond === "boolean" || typeof cond === "number" || typeof cond === "bigint") {
|
|
1114
|
+
if (typeof cond === "boolean" || typeof cond === "number" || typeof cond === "bigint" || cond instanceof Uint) {
|
|
1086
1115
|
return lowerExpr(boolOf(cond, e.pos) ? e.then : e.else, scope, want)
|
|
1087
1116
|
}
|
|
1088
1117
|
// Built before the sides, as it always was, so a program's nodes keep
|
|
@@ -1093,13 +1122,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1093
1122
|
let f = lowerExpr(e.else, scope, want)
|
|
1094
1123
|
if (isIntLike(t) && typeof f === "number" && wholeExpr(e.else)) f = lowerExpr(e.else, scope, kindWant(t))
|
|
1095
1124
|
if (isIntLike(f) && typeof t === "number" && wholeExpr(e.then)) t = lowerExpr(e.then, scope, kindWant(f))
|
|
1096
|
-
const kinded = [t, f].some((v) => typeof v === "bigint" || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
|
|
1125
|
+
const kinded = [t, f].some((v) => typeof v === "bigint" || v instanceof Uint || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
|
|
1097
1126
|
if (!kinded) {
|
|
1098
1127
|
const c = cond instanceof Test ? cond.float : cond
|
|
1099
1128
|
return at(e.pos, () => sl.select(c, toFloat(t, e.pos), toFloat(f, e.pos)))
|
|
1100
1129
|
}
|
|
1101
1130
|
const c = boolOf(cond, e.pos)
|
|
1102
|
-
const side = (v: LV): Num => (
|
|
1131
|
+
const side = (v: LV): Num => (isIntLike(v) ? intVal(v) : typeof v === "boolean" ? sl.bool(v) : v instanceof Test ? v.float : v) as Num
|
|
1103
1132
|
return at(e.pos, () => sl.select(c, side(t), side(f)))
|
|
1104
1133
|
}
|
|
1105
1134
|
|
|
@@ -1160,7 +1189,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1160
1189
|
// min, max, abs and clamp keep ints ints; everything else takes floats.
|
|
1161
1190
|
if (INT_BUILTINS.has(n) && args.some(isIntLike) && args.every((v, i) => isIntLike(v) || (typeof v === "number" && wholeExpr(e.args[i]!)))) {
|
|
1162
1191
|
const k = kindWant(args.find(isIntLike)!)
|
|
1163
|
-
args = e.args.map((a) => lowerExpr(a, scope, k)).map((v) => (
|
|
1192
|
+
args = e.args.map((a) => lowerExpr(a, scope, k)).map((v) => (isIntLike(v) ? intVal(v) : v) as LV)
|
|
1164
1193
|
return at(e.pos, () => builtin.lower!(args as Num[]))
|
|
1165
1194
|
}
|
|
1166
1195
|
const fs = args.map((v, i) => toFloat(v, e.args[i]!.pos))
|
|
@@ -1177,17 +1206,26 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1177
1206
|
const v = lowerExpr(e.args[0]!, scope, n === "bool" ? undefined : n)
|
|
1178
1207
|
if (widthOf(v) !== 1) fail(`${n}(x) takes one component, and this is a ${widthType(widthOf(v))}; convert each one`, e.pos)
|
|
1179
1208
|
const x = v instanceof Test ? v.bool : v
|
|
1209
|
+
// Between an int and a uint the bits are kept, as the GPU keeps them.
|
|
1180
1210
|
if (n === "int") {
|
|
1181
1211
|
if (typeof x === "bigint") return x
|
|
1212
|
+
if (x instanceof Uint) return BigInt.asIntN(32, x.v)
|
|
1182
1213
|
if (typeof x === "number") return BigInt(truncateHeld(x, "int"))
|
|
1183
1214
|
if (typeof x === "boolean") return x ? 1n : 0n
|
|
1184
1215
|
}
|
|
1216
|
+
if (n === "uint") {
|
|
1217
|
+
if (x instanceof Uint) return x
|
|
1218
|
+
if (typeof x === "bigint") return new Uint(x)
|
|
1219
|
+
if (typeof x === "number") return new Uint(BigInt(truncateHeld(x, "uint")))
|
|
1220
|
+
if (typeof x === "boolean") return new Uint(x ? 1n : 0n)
|
|
1221
|
+
}
|
|
1185
1222
|
if (n === "bool") {
|
|
1186
1223
|
if (typeof x === "boolean") return x
|
|
1187
1224
|
if (typeof x === "number") return x !== 0
|
|
1188
1225
|
if (typeof x === "bigint") return x !== 0n
|
|
1226
|
+
if (x instanceof Uint) return x.v !== 0n
|
|
1189
1227
|
}
|
|
1190
|
-
const c =
|
|
1228
|
+
const c = isIntLike(x) ? intVal(x) : x
|
|
1191
1229
|
return at(e.pos, () => (n === "int" ? sl.int(c) : n === "uint" ? sl.uint(c) : sl.bool(c)) as unknown as Val)
|
|
1192
1230
|
}
|
|
1193
1231
|
|
|
@@ -1238,7 +1276,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1238
1276
|
}
|
|
1239
1277
|
|
|
1240
1278
|
function compose(width: SLType, parts: LV[]): Val {
|
|
1241
|
-
const ps = parts.map((p) => (p instanceof Test ? p.float : typeof p === "bigint" ? Number(p) : typeof p === "boolean" ? (p ? 1 : 0) : p)) as Num[]
|
|
1279
|
+
const ps = parts.map((p) => (p instanceof Test ? p.float : typeof p === "bigint" ? Number(p) : p instanceof Uint ? Number(p.v) : typeof p === "boolean" ? (p ? 1 : 0) : p)) as Num[]
|
|
1242
1280
|
if (width === 2) return sl.vec2(...ps) as unknown as Val
|
|
1243
1281
|
if (width === 3) return sl.vec3(...ps) as unknown as Val
|
|
1244
1282
|
return sl.vec4(...ps) as unknown as Val
|
|
@@ -1284,6 +1322,12 @@ const INT_BITS: Record<BitOp, (a: bigint, b: bigint) => bigint> = {
|
|
|
1284
1322
|
">>": (a, b) => a >> (b & 31n),
|
|
1285
1323
|
}
|
|
1286
1324
|
|
|
1325
|
+
/** Comparisons of two uints, whose bigints compare as the values do. */
|
|
1326
|
+
const INT_CMP: Record<string, (a: bigint, b: bigint) => boolean> = {
|
|
1327
|
+
"<": (a, b) => a < b, "<=": (a, b) => a <= b, ">": (a, b) => a > b,
|
|
1328
|
+
">=": (a, b) => a >= b, "==": (a, b) => a === b, "!=": (a, b) => a !== b,
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1287
1331
|
const FOLD_CMP: Record<string, (a: number, b: number) => boolean> = {
|
|
1288
1332
|
"<": (a, b) => a < b, "<=": (a, b) => a <= b, ">": (a, b) => a > b,
|
|
1289
1333
|
">=": (a, b) => a >= b, "==": (a, b) => a === b, "!=": (a, b) => a !== b,
|
|
@@ -1313,11 +1357,11 @@ function isInt(v: LV): v is bigint | Val {
|
|
|
1313
1357
|
return typeof v === "bigint" || (v instanceof Val && v.kind === "int")
|
|
1314
1358
|
}
|
|
1315
1359
|
|
|
1316
|
-
function isUint(v: LV): v is Val {
|
|
1317
|
-
return v instanceof Val && v.kind === "uint"
|
|
1360
|
+
function isUint(v: LV): v is Uint | Val {
|
|
1361
|
+
return v instanceof Uint || (v instanceof Val && v.kind === "uint")
|
|
1318
1362
|
}
|
|
1319
1363
|
|
|
1320
|
-
function isIntLike(v: LV): v is bigint | Val {
|
|
1364
|
+
function isIntLike(v: LV): v is bigint | Uint | Val {
|
|
1321
1365
|
return isInt(v) || isUint(v)
|
|
1322
1366
|
}
|
|
1323
1367
|
|
|
@@ -1325,25 +1369,37 @@ function isBoolish(v: LV): boolean {
|
|
|
1325
1369
|
return typeof v === "boolean" || v instanceof Test || (v instanceof Val && v.kind === "bool")
|
|
1326
1370
|
}
|
|
1327
1371
|
|
|
1328
|
-
function isConstant(v: LV): v is number | bigint {
|
|
1329
|
-
return typeof v === "number" || typeof v === "bigint"
|
|
1372
|
+
function isConstant(v: LV): v is number | bigint | Uint {
|
|
1373
|
+
return typeof v === "number" || typeof v === "bigint" || v instanceof Uint
|
|
1330
1374
|
}
|
|
1331
1375
|
|
|
1332
|
-
function kindWant(v:
|
|
1333
|
-
return v
|
|
1376
|
+
function kindWant(v: LV): "int" | "uint" {
|
|
1377
|
+
return isUint(v) ? "uint" : "int"
|
|
1334
1378
|
}
|
|
1335
1379
|
|
|
1336
1380
|
function num(v: LV): number {
|
|
1337
|
-
return typeof v === "bigint" ? Number(v) : v as number
|
|
1381
|
+
return typeof v === "bigint" ? Number(v) : v instanceof Uint ? Number(v.v) : v as number
|
|
1338
1382
|
}
|
|
1339
1383
|
|
|
1340
|
-
/** An int operand as a node the EDSL's int methods take. */
|
|
1341
|
-
function intVal(v: bigint | Val): Val {
|
|
1342
|
-
|
|
1384
|
+
/** An int or uint operand as a node the EDSL's int methods take. */
|
|
1385
|
+
function intVal(v: bigint | Uint | Val): Val {
|
|
1386
|
+
if (typeof v === "bigint") return sl.int(Number(v)) as unknown as Val
|
|
1387
|
+
if (v instanceof Uint) return sl.uint(Number(v.v)) as unknown as Val
|
|
1388
|
+
return v
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* The second operand of an int method: an int constant as a number, which
|
|
1393
|
+
* takes the first operand's kind, and a uint one as a node, so an int meeting
|
|
1394
|
+
* it is refused rather than made a uint.
|
|
1395
|
+
*/
|
|
1396
|
+
function other(v: bigint | Uint | Val): number | Val {
|
|
1397
|
+
return typeof v === "bigint" ? Number(v) : intVal(v)
|
|
1343
1398
|
}
|
|
1344
1399
|
|
|
1345
1400
|
function asVal(v: LV): Val {
|
|
1346
1401
|
if (v instanceof Val) return v
|
|
1402
|
+
if (v instanceof Uint) return intVal(v)
|
|
1347
1403
|
return sl.float(typeof v === "number" ? v : num(v)) as unknown as Val
|
|
1348
1404
|
}
|
|
1349
1405
|
|
|
@@ -1366,6 +1422,7 @@ function article(t: VType): string {
|
|
|
1366
1422
|
|
|
1367
1423
|
function describeLV(v: LV): string {
|
|
1368
1424
|
if (typeof v === "bigint") return "an int"
|
|
1425
|
+
if (v instanceof Uint) return "a uint"
|
|
1369
1426
|
if (typeof v === "boolean" || v instanceof Test) return "a bool"
|
|
1370
1427
|
if (typeof v === "number") return "a float"
|
|
1371
1428
|
return v.kind === undefined ? `a ${widthType(v.width)}` : v.kind === "int" ? "an int" : `a ${v.kind}`
|
|
@@ -1380,6 +1437,7 @@ function constValue(v: Val): bigint | null {
|
|
|
1380
1437
|
/** Two lowered values that are certainly the same, so a join can leave them out. */
|
|
1381
1438
|
function same(a: LV, b: LV): boolean {
|
|
1382
1439
|
if (a === b) return true
|
|
1440
|
+
if (a instanceof Uint && b instanceof Uint) return a.v === b.v
|
|
1383
1441
|
return a instanceof Val && b instanceof Val && a.ref === b.ref && a.width === b.width && a.kind === b.kind
|
|
1384
1442
|
}
|
|
1385
1443
|
|
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_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram,
|
|
12
|
+
INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram, intUniformProblem,
|
|
13
13
|
programVersion, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
14
14
|
} from "./ir"
|
|
15
15
|
import { SLOP, SL_ARITY, SL_NAME } from "./ops"
|
|
@@ -209,6 +209,12 @@ function uniform(u: unknown, i: number): UniformDecl {
|
|
|
209
209
|
}
|
|
210
210
|
const out: UniformDecl = { name: x.name!, type: x.type as SLType, value: x.value!.slice() }
|
|
211
211
|
if (x.colour === true) out.colour = true
|
|
212
|
+
if (x.kind !== undefined) {
|
|
213
|
+
if (x.kind !== "int") fail(`uniform ${i} holds a "${String(x.kind)}"; a uniform is a float or an int`)
|
|
214
|
+
const problem = intUniformProblem(out.type, out.value)
|
|
215
|
+
if (problem !== null) fail(`uniform ${i}: ${problem}`)
|
|
216
|
+
out.kind = "int"
|
|
217
|
+
}
|
|
212
218
|
// Its control: shapes checked here, sense checked by the one check the
|
|
213
219
|
// compiler uses, so a hand edited file cannot give a host a range it would
|
|
214
220
|
// choke on.
|
package/src/sl.ts
CHANGED
|
@@ -956,6 +956,15 @@ export const uniform = {
|
|
|
956
956
|
const b = ctx()
|
|
957
957
|
return mk(b, b.uniform(name, TYPE.FLOAT, [value], false, control), TYPE.FLOAT)
|
|
958
958
|
},
|
|
959
|
+
/**
|
|
960
|
+
* An int, read as the whole number it holds. Its slot is a float, as every
|
|
961
|
+
* host binds one; the declaration says `kind: "int"`, so a host shows a
|
|
962
|
+
* whole number field for it.
|
|
963
|
+
*/
|
|
964
|
+
int(name: string, value = 0, control: UniformControl = {}): Int {
|
|
965
|
+
const b = ctx()
|
|
966
|
+
return int(mk(b, b.uniform(name, TYPE.FLOAT, [value], false, control, "int"), TYPE.FLOAT) as Float)
|
|
967
|
+
},
|
|
959
968
|
vec2(name: string, value: [number, number] = [0, 0], control: UniformControl = {}): Vec2 {
|
|
960
969
|
const b = ctx()
|
|
961
970
|
return mk(b, b.uniform(name, TYPE.VEC2, value, false, control), TYPE.VEC2)
|