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 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. `inputsUsed`
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.5.0",
3
+ "package": "0.6.0",
4
4
  "ir": 4,
5
5
  "target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
6
6
  "size": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.5.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 and a bool constant a
43
- * boolean for the same reason: folded until something needs a node.
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 lowered value. A number is a float, a bigint an int and a boolean a bool,
84
- * none of which has had to become a node yet; a Test is a comparison not yet
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
- type LV = Val | number | bigint | boolean | Test
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 at(pos, () => sl.uint(0))
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.int(sl.uniform.float(name, c[0]!, control)) as unknown as Val
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 = typeof v === "number" || typeof v === "bigint" ? "a single number" : `a ${widthType(got)}`
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 instanceof Val && v.kind === "uint" ? "uint" : "int")
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 instanceof Val && v.kind === "uint") fail(`${name} is an int and this is a uint; convert it with int(...)`, pos)
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 instanceof Val && v.kind === "uint") return v
804
- if (typeof v === "bigint" && v >= 0n) return at(pos, () => sl.uint(Number(v)) as unknown as Val)
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) return at(e.pos, () => sl.uint(e.value) as unknown as Val)
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
- return at(pos, () => intVal(a)[METHOD[op]](typeof b === "bigint" ? Number(b) : b as Val))
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
- const x = intVal(a as bigint | Val)
1006
- const y = typeof b === "bigint" ? Number(b) : b as Val
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
- return at(pos, () => intVal(a)[CMP_METHOD[op]!](typeof b === "bigint" ? Number(b) : b as Val) as unknown as Val)
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 => (typeof v === "bigint" ? sl.int(Number(v)) : typeof v === "boolean" ? sl.bool(v) : v instanceof Test ? v.float : v) as 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) => (typeof v === "bigint" ? sl.int(Number(v)) : v) as LV)
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 = typeof x === "bigint" ? Number(x) : x
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: bigint | Val): "int" | "uint" {
1333
- return v instanceof Val && v.kind === "uint" ? "uint" : "int"
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
- return typeof v === "bigint" ? sl.int(Number(v)) as unknown as Val : v
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)