onejs-sl 0.4.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/src/ir.ts CHANGED
@@ -64,12 +64,75 @@ export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INP
64
64
  /** Index into `Program.nodes`. Always refers backwards. */
65
65
  export type NodeRef = number
66
66
 
67
- export type SLNode =
68
- | { k: "const"; type: SLType; v: number[] }
69
- | { k: "input"; type: SLType; name: InputName }
70
- | { k: "uniform"; type: SLType; slot: number }
71
- | { k: "swizzle"; type: SLType; src: NodeRef; chans: number[] }
72
- | { k: "call"; type: SLType; op: SLOpCode; args: NodeRef[]; imm?: number[] }
67
+ /**
68
+ * What a value holds, beside its width. A node without one holds floats, which
69
+ * is every node before IR 4, so a program of floats is the graph and the hash
70
+ * it always was.
71
+ */
72
+ export type SLKind = "int" | "uint" | "bool"
73
+
74
+ /**
75
+ * A node that is a value. `param` is a loop's carried value at the top of an
76
+ * iteration, `proj` one result of an `if` or a `loop`.
77
+ */
78
+ export type ValueNode =
79
+ | { k: "const"; type: SLType; kind?: SLKind; v: number[] }
80
+ | { k: "input"; type: SLType; kind?: undefined; name: InputName }
81
+ | { k: "uniform"; type: SLType; kind?: undefined; slot: number }
82
+ | { k: "swizzle"; type: SLType; kind?: SLKind; src: NodeRef; chans: number[] }
83
+ | { k: "call"; type: SLType; kind?: SLKind; op: SLOpCode; args: NodeRef[]; imm?: number[] }
84
+ | { k: "param"; type: SLType; kind?: SLKind; loop: number; index: number }
85
+ | { k: "proj"; type: SLType; kind?: SLKind; src: NodeRef; index: number }
86
+
87
+ /**
88
+ * Structured control flow (IR 4), as regions of the one graph rather than as
89
+ * statements: the graph stays a pure DAG, hash consed and hashed as before, and
90
+ * an emitter places each node in the innermost region that uses it
91
+ * (`structure.ts`), which is what makes a branch skip what only it needs.
92
+ *
93
+ * if `then` or `else` are the results, per the bool `cond`; its `proj`s read them
94
+ * loop the `param`s of loop `id` start as `init`; while `cond` holds and fewer
95
+ * than `max` turns have run, they become `next`. Its `proj`s are the params
96
+ * when it stops. `cond` and `next` are computed from the params.
97
+ */
98
+ export type ControlNode =
99
+ | { k: "if"; cond: NodeRef; then: NodeRef[]; else: NodeRef[] }
100
+ | { k: "loop"; id: number; init: NodeRef[]; cond: NodeRef; next: NodeRef[]; max: number }
101
+
102
+ export type SLNode = ValueNode | ControlNode
103
+
104
+ /**
105
+ * A float to an int or a uint as the GPU converts one: as a 32 bit float,
106
+ * held to the widest such float the kind can hold, then truncated toward zero.
107
+ * The emitters clamp to these bounds, so a folded conversion
108
+ * and a computed one agree.
109
+ */
110
+ export function truncateHeld(v: number, kind: "int" | "uint"): number {
111
+ const [lo, hi] = INT_BOUNDS[kind]
112
+ return Math.trunc(Math.min(hi, Math.max(lo, Math.fround(v))))
113
+ }
114
+
115
+ /** The widest 32 bit floats an int and a uint hold, which a conversion clamps to first. */
116
+ export const INT_BOUNDS = { int: [-2147483648, 2147483520], uint: [0, 4294967040] } as const
117
+
118
+ /** The value node at `ref`. A control node is only ever read through its projs. */
119
+ export function valueAt(nodes: readonly SLNode[], ref: NodeRef): ValueNode {
120
+ const n = nodes[ref]
121
+ if (n === undefined || n.k === "if" || n.k === "loop") throw new SLError(`node ${ref} is not a value`)
122
+ return n
123
+ }
124
+
125
+ /** The refs a node reads, in order. */
126
+ export function operands(n: SLNode): NodeRef[] {
127
+ switch (n.k) {
128
+ case "swizzle": return [n.src]
129
+ case "call": return n.args
130
+ case "proj": return [n.src]
131
+ case "if": return [n.cond, ...n.then, ...n.else]
132
+ case "loop": return [...n.init, n.cond, ...n.next]
133
+ default: return []
134
+ }
135
+ }
73
136
 
74
137
  /**
75
138
  * How a host presents a uniform: a slider, a checkbox, a dropdown, a heading
@@ -162,6 +225,27 @@ export interface UniformDecl extends UniformControl {
162
225
  * conversion is already in the graph, so it is not part of the hash.
163
226
  */
164
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
165
249
  }
166
250
 
167
251
  export interface TextureDecl {
@@ -254,6 +338,10 @@ export class Builder {
254
338
  readonly textures: TextureDecl[] = []
255
339
  private readonly interned = new Map<string, NodeRef>()
256
340
 
341
+ /** The next loop's id: unique within the program, so two loops' params never intern to one node. */
342
+ private loops = 0
343
+ newLoop(): number { return this.loops++ }
344
+
257
345
  add(node: SLNode): NodeRef {
258
346
  if (this.nodes.length >= MAX_NODES) {
259
347
  throw new SLError(`a program may hold at most ${MAX_NODES} operations`)
@@ -266,7 +354,7 @@ export class Builder {
266
354
  return ref
267
355
  }
268
356
 
269
- call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[]): NodeRef {
357
+ call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[], kind?: SLKind): NodeRef {
270
358
  const arity = SL_ARITY[op]
271
359
  if (arity >= 0 && args.length !== arity) {
272
360
  throw new SLError(`${SL_NAME[op]} takes ${arity} argument(s), got ${args.length}`)
@@ -276,17 +364,22 @@ export class Builder {
276
364
  throw new SLError(`${SL_NAME[op]} refers to a node that does not exist yet`)
277
365
  }
278
366
  }
279
- return this.add(imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm })
367
+ const node: SLNode = imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm }
368
+ if (kind !== undefined) node.kind = kind
369
+ return this.add(node)
280
370
  }
281
371
 
282
- constant(v: number[]): NodeRef {
372
+ constant(v: number[], kind?: SLKind): NodeRef {
283
373
  for (const n of v) {
284
374
  if (!Number.isFinite(n)) throw new SLError(`a constant must be finite, got ${n}`)
375
+ if (kind !== undefined && !Number.isInteger(n)) throw new SLError(`an ${kind} constant must be a whole number, got ${n}`)
285
376
  }
286
- return this.add({ k: "const", type: v.length as SLType, v: v.slice() })
377
+ const node: SLNode = { k: "const", type: v.length as SLType, v: v.slice() }
378
+ if (kind !== undefined) node.kind = kind
379
+ return this.add(node)
287
380
  }
288
381
 
289
- 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 {
290
383
  const existing = this.uniforms.findIndex((u) => u.name === name)
291
384
  if (existing >= 0) {
292
385
  const u = this.uniforms[existing]
@@ -296,6 +389,9 @@ export class Builder {
296
389
  if ((u.colour === true) !== colour) {
297
390
  throw new SLError(`uniform "${name}" is declared both as a colour and as plain numbers`)
298
391
  }
392
+ if (u.kind !== kind) {
393
+ throw new SLError(`uniform "${name}" is declared as both an int and a float`)
394
+ }
299
395
  // A second read may say nothing about the control, or the same thing again.
300
396
  const merged = { ...controlOf(u) }
301
397
  for (const f of CONTROL_FIELDS) {
@@ -312,10 +408,13 @@ export class Builder {
312
408
  }
313
409
  const problem = controlProblem(type, value, control)
314
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}`)
315
413
  const slot = this.uniforms.length
316
414
  if (slot >= UNIFORM_SLOTS) throw new SLError(tooManyUniforms(slot + 1, "program"))
317
415
  const decl: UniformDecl = { name, type, value: value.slice() }
318
416
  if (colour) decl.colour = true
417
+ if (kind !== undefined) decl.kind = kind
319
418
  Object.assign(decl, controlOf(control))
320
419
  this.uniforms.push(decl)
321
420
  return this.add({ k: "uniform", type, slot })
@@ -348,9 +447,7 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
348
447
  const ref = stack.pop()!
349
448
  if (keep.has(ref)) continue
350
449
  keep.add(ref)
351
- const n = nodes[ref]
352
- if (n.k === "swizzle") stack.push(n.src)
353
- else if (n.k === "call") for (const a of n.args) stack.push(a)
450
+ for (const a of operands(nodes[ref]!)) stack.push(a)
354
451
  }
355
452
  // Ascending, which is still topological because a node only refers backwards.
356
453
  return [...keep].sort((x, y) => x - y)
@@ -372,12 +469,17 @@ export function inputsUsed(p: Program): InputName[] {
372
469
 
373
470
  /** Structural key for hash consing. Order matters and is fixed by the node shape. */
374
471
  function keyOf(n: SLNode): string {
472
+ const kind = "kind" in n && n.kind !== undefined ? `@${n.kind}` : ""
375
473
  switch (n.k) {
376
- case "const": return `c:${n.type}:${n.v.map(fixed).join(",")}`
474
+ case "const": return `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}${kind}`
377
475
  case "input": return `i:${n.name}`
378
476
  case "uniform": return `u:${n.slot}`
379
- case "swizzle": return `s:${n.src}:${n.chans.join("")}`
380
- case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
477
+ case "swizzle": return `s:${n.src}:${n.chans.join("")}${kind}`
478
+ case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}${kind}`
479
+ case "param": return `p:${n.loop}:${n.index}`
480
+ case "proj": return `j:${n.src}:${n.index}`
481
+ case "if": return `?:${n.cond}:${n.then.join(",")}:${n.else.join(",")}`
482
+ case "loop": return `L:${n.max}:${n.init.join(",")}:${n.cond}:${n.next.join(",")}`
381
483
  }
382
484
  }
383
485
 
@@ -390,6 +492,11 @@ export function fixed(n: number): string {
390
492
  return Object.is(n, -0) ? "0" : n.toPrecision(9)
391
493
  }
392
494
 
495
+ /** A constant's value as keyed and hashed: a float at `fixed`'s precision, an int, a uint or a bool exactly. */
496
+ function constText(n: number, kind: SLKind | undefined): string {
497
+ return kind === undefined ? fixed(n) : String(n)
498
+ }
499
+
393
500
  export function widthName(t: SLType): string {
394
501
  return t === 1 ? "float" : `vec${t}`
395
502
  }
@@ -431,14 +538,33 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
431
538
  // Only where every backend computes the same bits either way: min and max
432
539
  // are left out, since which of -0 and +0 they return depends on the order.
433
540
  const digest = new Map<NodeRef, string>()
541
+ const depth = loopDepths(nodes, result)
434
542
 
435
543
  const of = (ref: NodeRef): string => {
436
544
  const seen = digest.get(ref)
437
545
  if (seen !== undefined) return seen
438
- const n = nodes[ref]
546
+ const n = nodes[ref]!
439
547
  let body: string
440
548
  switch (n.k) {
441
- case "const": body = `c:${n.type}:${n.v.map(fixed).join(",")}`; break
549
+ // Control nodes have no type of their own: their projs carry it.
550
+ case "if": {
551
+ body = `?:${of(n.cond)}:${n.then.map(of).join(",")}:${n.else.map(of).join(",")}`
552
+ const d = fnv1a(body)
553
+ digest.set(ref, d)
554
+ return d
555
+ }
556
+ case "loop": {
557
+ body = `L:${n.max}:${n.init.map(of).join(",")}:${of(n.cond)}:${n.next.map(of).join(",")}`
558
+ const d = fnv1a(body)
559
+ digest.set(ref, d)
560
+ return d
561
+ }
562
+ // By nesting depth, not by id: two programs that nest the same
563
+ // loops the same way agree however their ids were handed out, and
564
+ // two loops nested in one another never share a digest for a param.
565
+ case "param": body = `p:${depth.get(n.loop) ?? 1}:${n.index}`; break
566
+ case "proj": body = `j:${of(n.src)}:${n.index}`; break
567
+ case "const": body = `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}`; break
442
568
  case "input": body = `i:${n.name}`; break
443
569
  // By slot rather than by name: the slot is what the generated shader
444
570
  // lays out, so two programs whose uniforms differ only in name still
@@ -447,8 +573,8 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
447
573
  case "uniform": body = `u:${n.slot}:${n.type}`; break
448
574
  case "swizzle": {
449
575
  const src = nodes[n.src]!
450
- body = src.k === "const"
451
- ? `c:${n.type}:${n.chans.map((c) => fixed(src.v[c]!)).join(",")}`
576
+ body = src.k === "const" && src.kind === n.kind
577
+ ? `c:${n.type}:${n.chans.map((c) => constText(src.v[c]!, src.kind)).join(",")}`
452
578
  : `s:${of(n.src)}:${n.chans.join("")}`
453
579
  break
454
580
  }
@@ -459,7 +585,7 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
459
585
  break
460
586
  }
461
587
  }
462
- const d = fnv1a(body + "|" + n.type)
588
+ const d = fnv1a(body + "|" + n.type + (n.kind === undefined ? "" : n.kind))
463
589
  digest.set(ref, d)
464
590
  return d
465
591
  }
@@ -473,12 +599,44 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
473
599
  // includes the library's text when it compiles, and none caches a compiled
474
600
  // program by hash across a library update, so the new text reaches every
475
601
  // shader without a bump.
476
- const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes)}`, of(result)]
602
+ const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result)}`, of(result)]
477
603
  for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
478
604
  for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
479
605
  return fnv1a(parts.join("|"))
480
606
  }
481
607
 
608
+ /**
609
+ * Each reachable loop's nesting depth, 1 for a loop inside no other: 1 more than
610
+ * the deepest loop whose params it reads. Read from the graph, so it does not
611
+ * depend on the order anything was built or walked in.
612
+ */
613
+ function loopDepths(nodes: readonly SLNode[], result: NodeRef): Map<number, number> {
614
+ const free = new Map<NodeRef, Set<number>>()
615
+ const loops = new Map<number, Extract<SLNode, { k: "loop" }>>()
616
+ const freeOf = (ref: NodeRef): Set<number> => free.get(ref) ?? new Set()
617
+ for (const ref of reachable(nodes as SLNode[], result)) {
618
+ const n = nodes[ref]!
619
+ const out = new Set<number>()
620
+ if (n.k === "param") out.add(n.loop)
621
+ for (const a of operands(n)) for (const l of freeOf(a)) out.add(l)
622
+ if (n.k === "loop") { out.delete(n.id); loops.set(n.id, n) }
623
+ free.set(ref, out)
624
+ }
625
+ const loopFree = new Map<number, Set<number>>()
626
+ for (const [ref, n] of nodes.entries()) if (n.k === "loop" && free.has(ref)) loopFree.set(n.id, free.get(ref)!)
627
+ const depth = new Map<number, number>()
628
+ const of = (id: number): number => {
629
+ const seen = depth.get(id)
630
+ if (seen !== undefined) return seen
631
+ let d = 1
632
+ for (const outer of loopFree.get(id) ?? []) d = Math.max(d, of(outer) + 1)
633
+ depth.set(id, d)
634
+ return d
635
+ }
636
+ for (const id of loops.keys()) of(id)
637
+ return depth
638
+ }
639
+
482
640
  /** The control fields of `c` that are set, copied, in `CONTROL_FIELDS` order. */
483
641
  export function controlOf(c: UniformControl): UniformControl {
484
642
  const out: UniformControl = {}
@@ -500,20 +658,32 @@ export function controlOf(c: UniformControl): UniformControl {
500
658
  * predates the bump still reads it. Every node counts, used or not: the JSON
501
659
  * carries them all, and an older reader has to refuse by the version rather
502
660
  * than halfway through the nodes.
661
+ *
662
+ * Only the nodes the result reads count: lowering leaves dead nodes behind (an
663
+ * int constant a fold used up, say), and they draw nothing. `toJSON` writes
664
+ * only the live ones, so an older reader never meets a dead newer node.
503
665
  */
504
- export function programVersion(nodes: readonly SLNode[]): number {
666
+ export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
505
667
  let v = 2
506
- for (const n of nodes) v = Math.max(v, nodeVersion(n))
668
+ for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
507
669
  return v
508
670
  }
509
671
 
672
+ /** The ops IR 4 added. */
673
+ const IR4_OPS = new Set<number>([
674
+ SLOP.CAST, SLOP.LT, SLOP.LE, SLOP.GT, SLOP.GE, SLOP.EQ, SLOP.NE, SLOP.AND, SLOP.OR, SLOP.NOT,
675
+ SLOP.BIT_AND, SLOP.BIT_OR, SLOP.BIT_XOR, SLOP.BIT_NOT, SLOP.SHL, SLOP.SHR, SLOP.CHOOSE,
676
+ ])
677
+
510
678
  /**
511
679
  * The version that added this node's form. An sdf or octave call with one
512
680
  * operand is the form before IR 3, which only a file an older compiler wrote
513
681
  * holds, and `fromJSON` moves it to the form IR 3 has.
514
682
  */
515
683
  function nodeVersion(n: SLNode): number {
684
+ if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
516
685
  if (n.k !== "call") return 2
686
+ if (IR4_OPS.has(n.op)) return 4
517
687
  switch (n.op) {
518
688
  case SLOP.SAMPLE_LOD: return 3
519
689
  case SLOP.SDF:
@@ -537,7 +707,7 @@ function nodeVersion(n: SLNode): number {
537
707
  * one immediate
538
708
  */
539
709
  export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
540
- const widths = n.args.map((a) => nodes[a]!.type).join(",")
710
+ const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
541
711
  switch (n.op) {
542
712
  case SLOP.SDF:
543
713
  if (widths === "2,4,2" && n.imm?.length === 1) return null
@@ -588,8 +758,9 @@ export const SL_HASH_VERSION = 2
588
758
  * 2 an SDF call carries up to six shape parameters, not four (#129)
589
759
  * 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
590
760
  * operands, where they were immediates
761
+ * 4 control flow (if, loop, their params and projs), ints, uints and bools
591
762
  */
592
- export const SL_IR_VERSION = 3
763
+ export const SL_IR_VERSION = 4
593
764
 
594
765
  function fnv1a(s: string): string {
595
766
  let h = 0x811c9dc5
package/src/lang/ast.ts CHANGED
@@ -11,41 +11,71 @@
11
11
  * pass over the source.
12
12
  */
13
13
 
14
+ import type { SLKind } from "../ir"
14
15
  import type { Pos } from "./lexer"
15
16
 
16
- /** The four value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
17
- export type TypeName = "float" | "float2" | "float3" | "float4"
17
+ /** The value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
18
+ export type TypeName = "float" | "float2" | "float3" | "float4" | "int" | "uint" | "bool"
18
19
 
19
20
  export const TYPE_WIDTH: Record<TypeName, 1 | 2 | 3 | 4> = {
20
- float: 1, float2: 2, float3: 3, float4: 4,
21
+ float: 1, float2: 2, float3: 3, float4: 4, int: 1, uint: 1, bool: 1,
22
+ }
23
+
24
+ /** What a type holds beside its width; the floats hold nothing else. */
25
+ export const TYPE_KIND: Record<TypeName, SLKind | undefined> = {
26
+ float: undefined, float2: undefined, float3: undefined, float4: undefined, int: "int", uint: "uint", bool: "bool",
21
27
  }
22
28
 
23
29
  export type BinaryOp =
24
30
  | "+" | "-" | "*" | "/" | "%"
25
31
  | "<" | "<=" | ">" | ">=" | "==" | "!="
26
32
  | "&&" | "||"
33
+ | "&" | "|" | "^" | "<<" | ">>"
27
34
 
28
35
  export type Expr =
29
- | { k: "num"; value: number; pos: Pos }
36
+ /**
37
+ * `whole` is a number written with no point and no exponent, `3` not `3.0`:
38
+ * a float, except beside an int, where it is an int. `unsigned` is `3u`.
39
+ */
40
+ | { k: "num"; value: number; whole: boolean; unsigned?: boolean; pos: Pos }
41
+ | { k: "bool"; value: boolean; pos: Pos }
30
42
  | { k: "hex"; hex: string; pos: Pos }
31
43
  | { k: "ident"; name: string; pos: Pos }
32
44
  /** A swizzle, or the shape half of `sdf.circle`. Which one is decided later. */
33
45
  | { k: "member"; obj: Expr; name: string; pos: Pos }
34
46
  | { k: "call"; callee: Expr; args: Expr[]; pos: Pos }
35
- | { k: "unary"; op: "-" | "+" | "!"; arg: Expr; pos: Pos }
47
+ | { k: "unary"; op: "-" | "+" | "!" | "~"; arg: Expr; pos: Pos }
36
48
  | { k: "binary"; op: BinaryOp; a: Expr; b: Expr; pos: Pos }
37
49
  | { k: "cond"; cond: Expr; then: Expr; else: Expr; pos: Pos }
38
50
 
39
- export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%="
51
+ export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
52
+
53
+ /** One `case` group of a switch: its labels, `null` for `default`, and its body without the closing break. */
54
+ export interface SwitchCase {
55
+ labels: Array<Expr | null>
56
+ body: Stmt[]
57
+ /** Whether it ended in a break, which is dropped from `body`. */
58
+ closed: boolean
59
+ pos: Pos
60
+ }
40
61
 
41
62
  export type Stmt =
42
63
  | { k: "var"; type: TypeName; name: string; init: Expr; pos: Pos }
43
64
  /** A compile time constant: usable as a value and as a `for` bound. */
44
65
  | { k: "const"; type: TypeName; name: string; init: Expr; pos: Pos }
66
+ /** `x++` is `x += 1`, with the 1 whole. */
45
67
  | { k: "assign"; target: Expr; op: AssignOp; value: Expr; pos: Pos }
46
68
  | { k: "if"; cond: Expr; then: Stmt[]; else: Stmt[]; pos: Pos }
47
- | { k: "for"; counter: string; from: Expr; to: Expr; inclusive: boolean; step: Expr; body: Stmt[]; pos: Pos }
69
+ /** `for (type counter = from; cond; update)`: the update changes the counter and nothing else. */
70
+ | { k: "for"; type: TypeName; counter: string; from: Expr; cond: Expr; update: Extract<Stmt, { k: "assign" }>; body: Stmt[]; pos: Pos }
71
+ | { k: "while"; cond: Expr; body: Stmt[]; pos: Pos }
72
+ | { k: "break"; pos: Pos }
73
+ | { k: "continue"; pos: Pos }
74
+ /** Cases never fall through: each ends in break, return or continue, and the break is dropped here. */
75
+ | { k: "switch"; value: Expr; cases: SwitchCase[]; pos: Pos }
48
76
  | { k: "return"; value: Expr; pos: Pos }
77
+ /** `{ ... }` on its own: a body with its own scope, as a case's braces are. */
78
+ | { k: "block"; body: Stmt[]; pos: Pos }
49
79
 
50
80
  export interface Param {
51
81
  type: TypeName