redscript-mc 1.2.25 → 1.2.26

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.
Files changed (57) hide show
  1. package/dist/__tests__/cli.test.js +1 -1
  2. package/dist/__tests__/codegen.test.js +12 -6
  3. package/dist/__tests__/e2e.test.js +6 -6
  4. package/dist/__tests__/lowering.test.js +8 -8
  5. package/dist/__tests__/optimizer.test.js +31 -0
  6. package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
  7. package/dist/__tests__/stdlib-advanced.test.js +264 -0
  8. package/dist/__tests__/stdlib-math.test.d.ts +7 -0
  9. package/dist/__tests__/stdlib-math.test.js +352 -0
  10. package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
  11. package/dist/__tests__/stdlib-vec.test.js +264 -0
  12. package/dist/ast/types.d.ts +17 -1
  13. package/dist/codegen/mcfunction/index.js +154 -18
  14. package/dist/codegen/var-allocator.d.ts +17 -0
  15. package/dist/codegen/var-allocator.js +26 -0
  16. package/dist/compile.d.ts +14 -0
  17. package/dist/compile.js +62 -5
  18. package/dist/index.js +20 -1
  19. package/dist/ir/types.d.ts +4 -0
  20. package/dist/lexer/index.d.ts +1 -1
  21. package/dist/lexer/index.js +1 -0
  22. package/dist/lowering/index.d.ts +5 -0
  23. package/dist/lowering/index.js +83 -10
  24. package/dist/optimizer/dce.js +21 -5
  25. package/dist/optimizer/passes.js +18 -6
  26. package/dist/optimizer/structure.js +7 -0
  27. package/dist/parser/index.d.ts +5 -0
  28. package/dist/parser/index.js +43 -2
  29. package/dist/runtime/index.d.ts +6 -0
  30. package/dist/runtime/index.js +109 -9
  31. package/editors/vscode/package-lock.json +3 -3
  32. package/editors/vscode/package.json +1 -1
  33. package/package.json +1 -1
  34. package/src/__tests__/cli.test.ts +1 -1
  35. package/src/__tests__/codegen.test.ts +12 -6
  36. package/src/__tests__/e2e.test.ts +6 -6
  37. package/src/__tests__/lowering.test.ts +8 -8
  38. package/src/__tests__/optimizer.test.ts +33 -0
  39. package/src/__tests__/stdlib-advanced.test.ts +259 -0
  40. package/src/__tests__/stdlib-math.test.ts +374 -0
  41. package/src/__tests__/stdlib-vec.test.ts +259 -0
  42. package/src/ast/types.ts +11 -1
  43. package/src/codegen/mcfunction/index.ts +143 -19
  44. package/src/codegen/var-allocator.ts +29 -0
  45. package/src/compile.ts +72 -5
  46. package/src/index.ts +21 -1
  47. package/src/ir/types.ts +2 -0
  48. package/src/lexer/index.ts +2 -1
  49. package/src/lowering/index.ts +96 -10
  50. package/src/optimizer/dce.ts +22 -5
  51. package/src/optimizer/passes.ts +18 -5
  52. package/src/optimizer/structure.ts +6 -1
  53. package/src/parser/index.ts +47 -2
  54. package/src/runtime/index.ts +108 -10
  55. package/src/stdlib/advanced.mcrs +249 -0
  56. package/src/stdlib/math.mcrs +259 -19
  57. package/src/stdlib/vec.mcrs +246 -0
@@ -134,13 +134,18 @@ export class DeadCodeEliminator {
134
134
  const entries = new Set<string>()
135
135
 
136
136
  for (const fn of program.declarations) {
137
- // All top-level functions are entry points (callable via /function)
138
- // Exception: functions starting with _ are considered private/internal
139
- if (!fn.name.startsWith('_')) {
140
- entries.add(fn.name)
137
+ // Library functions (from `module library;` or `librarySources`) are
138
+ // NOT MC entry points they're only kept if reachable from user code.
139
+ // Exception: decorators like @tick / @load / @on / @keep always force inclusion.
140
+ if (!fn.isLibraryFn) {
141
+ // All top-level non-library functions are entry points (callable via /function)
142
+ // Exception: functions starting with _ are considered private/internal
143
+ if (!fn.name.startsWith('_')) {
144
+ entries.add(fn.name)
145
+ }
141
146
  }
142
147
 
143
- // Decorated functions are always entry points (even if prefixed with _)
148
+ // Decorated functions are always entry points regardless of library mode or _ prefix
144
149
  if (fn.decorators.some(decorator => [
145
150
  'tick',
146
151
  'load',
@@ -172,6 +177,18 @@ export class DeadCodeEliminator {
172
177
 
173
178
  this.reachableFunctions.add(fnName)
174
179
  this.collectFunctionRefs(fn)
180
+
181
+ // @requires("dep") — when fn is reachable, its required dependencies are
182
+ // also pulled into the reachable set so they survive DCE.
183
+ for (const decorator of fn.decorators) {
184
+ if (decorator.name === 'require_on_load') {
185
+ for (const arg of decorator.rawArgs ?? []) {
186
+ if (arg.kind === 'string') {
187
+ this.markReachable(arg.value)
188
+ }
189
+ }
190
+ }
191
+ }
175
192
  }
176
193
 
177
194
  private collectFunctionRefs(fn: FnDecl): void {
@@ -95,30 +95,43 @@ export function copyPropagation(fn: IRFunction): IRFunction {
95
95
  return copies.get(op.name) ?? op
96
96
  }
97
97
 
98
+ /**
99
+ * Invalidate all copies that became stale because `written` was modified.
100
+ * When $y is overwritten, any mapping copies[$tmp] = $y is now stale:
101
+ * reading $tmp would return the OLD $y value via the copy, but $y now holds
102
+ * a different value. Remove both the direct entry (copies[$y]) and every
103
+ * reverse entry that points at $y.
104
+ */
105
+ function invalidate(written: string): void {
106
+ copies.delete(written)
107
+ for (const [k, v] of copies) {
108
+ if (v.kind === 'var' && v.name === written) copies.delete(k)
109
+ }
110
+ }
111
+
98
112
  const newInstrs: IRInstr[] = []
99
113
  for (const instr of block.instrs) {
100
114
  switch (instr.op) {
101
115
  case 'assign': {
102
116
  const src = resolve(instr.src)
117
+ invalidate(instr.dst)
103
118
  // Only propagate scalars (var or const), not storage
104
119
  if (src.kind === 'var' || src.kind === 'const') {
105
120
  copies.set(instr.dst, src)
106
- } else {
107
- copies.delete(instr.dst)
108
121
  }
109
122
  newInstrs.push({ ...instr, src })
110
123
  break
111
124
  }
112
125
  case 'binop':
113
- copies.delete(instr.dst)
126
+ invalidate(instr.dst)
114
127
  newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
115
128
  break
116
129
  case 'cmp':
117
- copies.delete(instr.dst)
130
+ invalidate(instr.dst)
118
131
  newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
119
132
  break
120
133
  case 'call':
121
- if (instr.dst) copies.delete(instr.dst)
134
+ if (instr.dst) invalidate(instr.dst)
122
135
  newInstrs.push({ ...instr, args: instr.args.map(resolve) })
123
136
  break
124
137
  default:
@@ -24,7 +24,8 @@ function varRef(name: string): string {
24
24
  function operandToScore(op: Operand): string {
25
25
  if (op.kind === 'var') return `${varRef(op.name)} ${OBJ}`
26
26
  if (op.kind === 'const') return `$const_${op.value} ${OBJ}`
27
- throw new Error(`Cannot convert storage operand to score: ${op.path}`)
27
+ if (op.kind === 'param') return `$p${op.index} ${OBJ}`
28
+ throw new Error(`Cannot convert storage operand to score: ${(op as any).path}`)
28
29
  }
29
30
 
30
31
  function emitInstr(instr: IRInstr, namespace: string): IRCommand[] {
@@ -38,6 +39,10 @@ function emitInstr(instr: IRInstr, namespace: string): IRCommand[] {
38
39
  commands.push({
39
40
  cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = ${varRef(instr.src.name)} ${OBJ}`,
40
41
  })
42
+ } else if (instr.src.kind === 'param') {
43
+ commands.push({
44
+ cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $p${instr.src.index} ${OBJ}`,
45
+ })
41
46
  } else {
42
47
  commands.push({
43
48
  cmd: `execute store result score ${varRef(instr.dst)} ${OBJ} run data get storage ${instr.src.path}`,
@@ -76,6 +76,11 @@ export class Parser {
76
76
  private pos: number = 0
77
77
  private sourceLines: string[]
78
78
  private filePath?: string
79
+ /** Set to true once `module library;` is seen — all subsequent fn declarations
80
+ * will be marked isLibraryFn=true. When library sources are parsed via the
81
+ * `librarySources` compile option, each source is parsed by its own fresh
82
+ * Parser instance, so this flag never bleeds into user code. */
83
+ private inLibraryMode: boolean = false
79
84
 
80
85
  constructor(tokens: Token[], source?: string, filePath?: string) {
81
86
  this.tokens = tokens
@@ -169,6 +174,7 @@ export class Parser {
169
174
  const implBlocks: ImplBlock[] = []
170
175
  const enums: EnumDecl[] = []
171
176
  const consts: ConstDecl[] = []
177
+ let isLibrary = false
172
178
 
173
179
  // Check for namespace declaration
174
180
  if (this.check('namespace')) {
@@ -178,6 +184,20 @@ export class Parser {
178
184
  this.expect(';')
179
185
  }
180
186
 
187
+ // Check for module declaration: `module library;`
188
+ // Library-mode: all functions parsed from this point are marked isLibraryFn=true.
189
+ // When using the `librarySources` compile option, each library source is parsed
190
+ // by its own fresh Parser — so this flag never bleeds into user code.
191
+ if (this.check('module')) {
192
+ this.advance()
193
+ const modKind = this.expect('ident')
194
+ if (modKind.value === 'library') {
195
+ isLibrary = true
196
+ this.inLibraryMode = true
197
+ }
198
+ this.expect(';')
199
+ }
200
+
181
201
  // Parse struct and function declarations
182
202
  while (!this.check('eof')) {
183
203
  if (this.check('let')) {
@@ -199,7 +219,7 @@ export class Parser {
199
219
  }
200
220
  }
201
221
 
202
- return { namespace, globals, declarations, structs, implBlocks, enums, consts }
222
+ return { namespace, globals, declarations, structs, implBlocks, enums, consts, isLibrary }
203
223
  }
204
224
 
205
225
  // -------------------------------------------------------------------------
@@ -322,7 +342,11 @@ export class Parser {
322
342
 
323
343
  const body = this.parseBlock()
324
344
 
325
- return this.withLoc({ name, params, returnType, decorators, body }, fnToken)
345
+ const fn: import('../ast/types').FnDecl = this.withLoc(
346
+ { name, params, returnType, decorators, body, isLibraryFn: this.inLibraryMode || undefined },
347
+ fnToken,
348
+ )
349
+ return fn
326
350
  }
327
351
 
328
352
  /** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
@@ -397,6 +421,27 @@ export class Parser {
397
421
  }
398
422
  }
399
423
 
424
+ // @require_on_load(fn_name) — when this fn is used, fn_name is called from __load.
425
+ // Accepts bare identifiers (with optional leading _) or quoted strings.
426
+ if (name === 'require_on_load') {
427
+ const rawArgs: NonNullable<Decorator['rawArgs']> = []
428
+ for (const part of argsStr.split(',')) {
429
+ const trimmed = part.trim()
430
+ // Bare identifier: @require_on_load(_math_init)
431
+ const identMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
432
+ if (identMatch) {
433
+ rawArgs.push({ kind: 'string', value: identMatch[1] })
434
+ } else {
435
+ // Quoted string fallback: @require_on_load("_math_init")
436
+ const strMatch = trimmed.match(/^"([^"]*)"$/)
437
+ if (strMatch) {
438
+ rawArgs.push({ kind: 'string', value: strMatch[1] })
439
+ }
440
+ }
441
+ }
442
+ return { name, rawArgs }
443
+ }
444
+
400
445
  // Handle key=value format (e.g., rate=20)
401
446
  for (const part of argsStr.split(',')) {
402
447
  const [key, val] = part.split('=').map(s => s.trim())
@@ -266,6 +266,9 @@ export class MCRuntime {
266
266
  // Flag to stop function execution (for return)
267
267
  private shouldReturn: boolean = false
268
268
 
269
+ // Current MC macro context: key → value (set by 'function ... with storage')
270
+ private currentMacroContext: Record<string, any> | null = null
271
+
269
272
  constructor(namespace: string) {
270
273
  this.namespace = namespace
271
274
  // Initialize default objective
@@ -363,6 +366,13 @@ export class MCRuntime {
363
366
  cmd = cmd.trim()
364
367
  if (!cmd || cmd.startsWith('#')) return true
365
368
 
369
+ // MC macro command: line starts with '$'.
370
+ // Expand $(key) placeholders from currentMacroContext, then execute.
371
+ if (cmd.startsWith('$')) {
372
+ const expanded = this.expandMacro(cmd.slice(1))
373
+ return this.execCommand(expanded, executor)
374
+ }
375
+
366
376
  // Parse command
367
377
  if (cmd.startsWith('scoreboard ')) {
368
378
  return this.execScoreboard(cmd)
@@ -542,7 +552,10 @@ export class MCRuntime {
542
552
  // Track execute state
543
553
  let currentExecutor = executor
544
554
  let condition: boolean = true
545
- let storeTarget: { player: string; objective: string; type: 'result' | 'success' } | null = null
555
+ let storeTarget:
556
+ | { player: string; objective: string; type: 'result' | 'success' }
557
+ | { storagePath: string; field: string; type: 'result' }
558
+ | null = null
546
559
 
547
560
  while (rest.length > 0) {
548
561
  rest = rest.trimStart()
@@ -557,7 +570,11 @@ export class MCRuntime {
557
570
  const value = storeTarget.type === 'result'
558
571
  ? (this.returnValue ?? (result ? 1 : 0))
559
572
  : (result ? 1 : 0)
560
- this.setScore(storeTarget.player, storeTarget.objective, value)
573
+ if ('storagePath' in storeTarget) {
574
+ this.setStorageField(storeTarget.storagePath, storeTarget.field, value)
575
+ } else {
576
+ this.setScore(storeTarget.player, storeTarget.objective, value)
577
+ }
561
578
  }
562
579
 
563
580
  return result
@@ -630,15 +647,34 @@ export class MCRuntime {
630
647
  // Handle 'unless score ...'
631
648
  if (rest.startsWith('unless score ')) {
632
649
  rest = rest.slice(13)
633
- const scoreParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
634
- if (scoreParts) {
635
- const [, player, obj, rangeStr, remaining] = scoreParts
650
+ // unless score <player> <obj> matches <range>
651
+ const matchesParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
652
+ if (matchesParts) {
653
+ const [, player, obj, rangeStr, remaining] = matchesParts
636
654
  const range = parseRange(rangeStr)
637
655
  const score = this.getScore(player, obj)
638
656
  condition = condition && !matchesRange(score, range)
639
657
  rest = remaining.trim()
640
658
  continue
641
659
  }
660
+ // unless score <p1> <o1> <op> <p2> <o2>
661
+ const compareMatch = rest.match(/^(\S+)\s+(\S+)\s+([<>=]+)\s+(\S+)\s+(\S+)(.*)$/)
662
+ if (compareMatch) {
663
+ const [, p1, o1, op, p2, o2, remaining] = compareMatch
664
+ const v1 = this.getScore(p1, o1)
665
+ const v2 = this.getScore(p2, o2)
666
+ let matches = false
667
+ switch (op) {
668
+ case '=': matches = v1 === v2; break
669
+ case '<': matches = v1 < v2; break
670
+ case '<=': matches = v1 <= v2; break
671
+ case '>': matches = v1 > v2; break
672
+ case '>=': matches = v1 >= v2; break
673
+ }
674
+ condition = condition && !matches // unless = negate
675
+ rest = remaining.trim()
676
+ continue
677
+ }
642
678
  }
643
679
 
644
680
  // Handle 'if entity <selector>'
@@ -661,6 +697,19 @@ export class MCRuntime {
661
697
  continue
662
698
  }
663
699
 
700
+ // Handle 'store result storage <ns:path> <field> <type> <scale>'
701
+ if (rest.startsWith('store result storage ')) {
702
+ rest = rest.slice(21)
703
+ // format: <ns:path> <field> <type> <scale> <run-cmd>
704
+ const storageParts = rest.match(/^(\S+)\s+(\S+)\s+(\S+)\s+([\d.]+)\s+(.*)$/)
705
+ if (storageParts) {
706
+ const [, storagePath, field, , , remaining] = storageParts
707
+ storeTarget = { storagePath, field, type: 'result' }
708
+ rest = remaining.trim()
709
+ continue
710
+ }
711
+ }
712
+
664
713
  // Handle 'store result score <player> <obj>'
665
714
  if (rest.startsWith('store result score ')) {
666
715
  rest = rest.slice(19)
@@ -695,7 +744,11 @@ export class MCRuntime {
695
744
  const value = storeTarget.type === 'result'
696
745
  ? (this.returnValue ?? (condition ? 1 : 0))
697
746
  : (condition ? 1 : 0)
698
- this.setScore(storeTarget.player, storeTarget.objective, value)
747
+ if ('storagePath' in storeTarget) {
748
+ this.setStorageField(storeTarget.storagePath, storeTarget.field, value)
749
+ } else {
750
+ this.setScore(storeTarget.player, storeTarget.objective, value)
751
+ }
699
752
  }
700
753
 
701
754
  return condition
@@ -721,13 +774,39 @@ export class MCRuntime {
721
774
  // -------------------------------------------------------------------------
722
775
 
723
776
  private execFunctionCmd(cmd: string, executor?: Entity): boolean {
724
- const fnName = cmd.slice(9).trim() // remove 'function '
777
+ let fnRef = cmd.slice(9).trim() // remove 'function '
778
+
779
+ // Handle 'function ns:name with storage ns:path' — MC macro calling convention.
780
+ // The called function may have $( ) placeholders that need to be expanded
781
+ // using the provided storage compound. We execute the function after
782
+ // expanding its macro context.
783
+ const withStorageMatch = fnRef.match(/^(\S+)\s+with\s+storage\s+(\S+)$/)
784
+ if (withStorageMatch) {
785
+ const [, actualFnName, storagePath] = withStorageMatch
786
+ const macroContext = this.getStorageCompound(storagePath) ?? {}
787
+ const outerShouldReturn = this.shouldReturn
788
+ const outerMacroCtx = this.currentMacroContext
789
+ this.currentMacroContext = macroContext
790
+ this.execFunction(actualFnName, executor)
791
+ this.currentMacroContext = outerMacroCtx
792
+ this.shouldReturn = outerShouldReturn
793
+ return true
794
+ }
795
+
725
796
  const outerShouldReturn = this.shouldReturn
726
- this.execFunction(fnName, executor)
797
+ this.execFunction(fnRef, executor)
727
798
  this.shouldReturn = outerShouldReturn
728
799
  return true
729
800
  }
730
801
 
802
+ /** Expand MC macro placeholders: $(key) → value from currentMacroContext */
803
+ private expandMacro(cmd: string): string {
804
+ return cmd.replace(/\$\(([^)]+)\)/g, (_, key) => {
805
+ const val = this.currentMacroContext?.[key]
806
+ return val !== undefined ? String(val) : `$(${key})`
807
+ })
808
+ }
809
+
731
810
  // -------------------------------------------------------------------------
732
811
  // Data Commands
733
812
  // -------------------------------------------------------------------------
@@ -755,8 +834,19 @@ export class MCRuntime {
755
834
  return true
756
835
  }
757
836
 
758
- // data get storage <ns:path> <field>
759
- const getMatch = cmd.match(/^data get storage (\S+) (\S+)$/)
837
+ // data get storage <ns:path> <field>[<index>] [scale] (array element access)
838
+ const getArrMatch = cmd.match(/^data get storage (\S+) (\S+)\[(\d+)\](?:\s+[\d.]+)?$/)
839
+ if (getArrMatch) {
840
+ const [, storagePath, field, indexStr] = getArrMatch
841
+ const arr = this.getStorageField(storagePath, field)
842
+ const idx = parseInt(indexStr, 10)
843
+ const value = Array.isArray(arr) ? arr[idx] : undefined
844
+ this.returnValue = typeof value === 'number' ? value : 0
845
+ return true
846
+ }
847
+
848
+ // data get storage <ns:path> <field> [scale]
849
+ const getMatch = cmd.match(/^data get storage (\S+) (\S+)(?:\s+[\d.]+)?$/)
760
850
  if (getMatch) {
761
851
  const [, storagePath, field] = getMatch
762
852
  const value = this.getStorageField(storagePath, field)
@@ -803,6 +893,14 @@ export class MCRuntime {
803
893
  }
804
894
  }
805
895
 
896
+ /** Return the whole storage compound at storagePath as a flat key→value map.
897
+ * Used by 'function ... with storage' to provide macro context. */
898
+ private getStorageCompound(storagePath: string): Record<string, any> | null {
899
+ const data = this.storage.get(storagePath)
900
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return null
901
+ return data as Record<string, any>
902
+ }
903
+
806
904
  private getStorageField(storagePath: string, field: string): any {
807
905
  const data = this.storage.get(storagePath) ?? {}
808
906
  const segments = this.parseStoragePath(field)
@@ -0,0 +1,249 @@
1
+ // advanced.mcrs — Higher-order integer math and "fun" algorithms.
2
+ //
3
+ // Requires: math.mcrs (for lerp, smoothstep, mulfix, abs, isqrt, sqrt_fixed)
4
+ //
5
+ // Category 1: Number theory — fib, is_prime, collatz_steps, digit_sum, reverse_int, mod_pow
6
+ // Category 2: Hashing/noise — hash_int, noise1d
7
+ // Category 3: Curves — bezier_quad, bezier_cubic
8
+ // Category 4: Fractals 🤯 — mandelbrot_iter (fixed-point complex arithmetic)
9
+
10
+ module library;
11
+
12
+ // ─── Category 1: Number theory ───────────────────────────────────────────────
13
+
14
+ // Fibonacci number F(n) using simple iteration.
15
+ // Overflow: F(46) = 1836311903 ≈ INT_MAX; use n ≤ 46.
16
+ // fib(0) == 0, fib(1) == 1, fib(10) == 55
17
+ fn fib(n: int) -> int {
18
+ if (n <= 0) { return 0; }
19
+ if (n == 1) { return 1; }
20
+ let a: int = 0;
21
+ let b: int = 1;
22
+ let i: int = 2;
23
+ while (i <= n) {
24
+ let c: int = a + b;
25
+ a = b;
26
+ b = c;
27
+ i = i + 1;
28
+ }
29
+ return b;
30
+ }
31
+
32
+ // Primality test by trial division up to √n.
33
+ // Returns 1 if n is prime, 0 otherwise.
34
+ // is_prime(2) == 1, is_prime(4) == 0, is_prime(97) == 1
35
+ fn is_prime(n: int) -> int {
36
+ if (n < 2) { return 0; }
37
+ if (n == 2) { return 1; }
38
+ if (n % 2 == 0) { return 0; }
39
+ let i: int = 3;
40
+ while (i * i <= n) {
41
+ if (n % i == 0) { return 0; }
42
+ i = i + 2;
43
+ }
44
+ return 1;
45
+ }
46
+
47
+ // Number of steps in the Collatz sequence starting at n until reaching 1.
48
+ // collatz_steps(1) == 0
49
+ // collatz_steps(6) == 8
50
+ // collatz_steps(27) == 111 (world record among small numbers)
51
+ fn collatz_steps(n: int) -> int {
52
+ if (n <= 1) { return 0; }
53
+ let x: int = n;
54
+ let steps: int = 0;
55
+ while (x != 1) {
56
+ if (x % 2 == 0) {
57
+ x = x / 2;
58
+ } else {
59
+ x = 3 * x + 1;
60
+ }
61
+ steps = steps + 1;
62
+ }
63
+ return steps;
64
+ }
65
+
66
+ // Sum of decimal digits. Negative input uses absolute value.
67
+ // digit_sum(123) == 6, digit_sum(0) == 0
68
+ fn digit_sum(n: int) -> int {
69
+ let x: int = n;
70
+ if (x < 0) { x = 0 - x; }
71
+ if (x == 0) { return 0; }
72
+ let sum: int = 0;
73
+ while (x > 0) {
74
+ sum = sum + x % 10;
75
+ x = x / 10;
76
+ }
77
+ return sum;
78
+ }
79
+
80
+ // Count decimal digits. 0 has 1 digit. Negative: counts absolute digits.
81
+ // count_digits(0) == 1, count_digits(100) == 3
82
+ fn count_digits(n: int) -> int {
83
+ let x: int = n;
84
+ if (x < 0) { x = 0 - x; }
85
+ if (x == 0) { return 1; }
86
+ let cnt: int = 0;
87
+ while (x > 0) {
88
+ cnt = cnt + 1;
89
+ x = x / 10;
90
+ }
91
+ return cnt;
92
+ }
93
+
94
+ // Reverse the decimal digits of an integer. Sign is preserved.
95
+ // reverse_int(12345) == 54321, reverse_int(-42) == -24
96
+ fn reverse_int(n: int) -> int {
97
+ let x: int = n;
98
+ let neg: int = 0;
99
+ if (x < 0) { x = 0 - x; neg = 1; }
100
+ let result: int = 0;
101
+ while (x > 0) {
102
+ result = result * 10 + x % 10;
103
+ x = x / 10;
104
+ }
105
+ if (neg == 1) { return 0 - result; }
106
+ return result;
107
+ }
108
+
109
+ // Modular exponentiation: (base ^ exp) mod m using fast O(log exp) squaring.
110
+ // IMPORTANT: m must be ≤ 46340 to avoid b*b overflow (46340² < INT_MAX).
111
+ // mod_pow(2, 10, 1000) == 24 (1024 mod 1000)
112
+ fn mod_pow(base: int, exp: int, m: int) -> int {
113
+ if (m == 1) { return 0; }
114
+ let result: int = 1;
115
+ let b: int = base % m;
116
+ if (b < 0) { b = b + m; }
117
+ let e: int = exp;
118
+ while (e > 0) {
119
+ if (e % 2 == 1) {
120
+ result = result * b % m;
121
+ }
122
+ b = b * b % m;
123
+ e = e / 2;
124
+ }
125
+ return result;
126
+ }
127
+
128
+ // ─── Category 2: Hashing & noise ─────────────────────────────────────────────
129
+
130
+ // Integer hash function. Output is non-negative, range [0, ~2 × 10⁹).
131
+ // Deterministic, no randomness — same input always produces the same output.
132
+ // Suitable as a seeded pseudo-random value for procedural generation.
133
+ // hash_int(0) != hash_int(1), repeatable across runs.
134
+ fn hash_int(n: int) -> int {
135
+ let h: int = n;
136
+ if (h < 0) { h = 0 - h; }
137
+ h = h % 46340; // Clamp so h² fits in int32
138
+ h = h * 46337 + 97; // 46337 is prime; h*46337 ≤ 46340*46337 < INT_MAX
139
+ if (h < 0) { h = 0 - h; }
140
+ h = h % 46340;
141
+ h = h * 46337 + 211;
142
+ if (h < 0) { h = 0 - h; }
143
+ return h;
144
+ }
145
+
146
+ // 1D value noise. Input x in fixed-point (scale = 1000).
147
+ // Output in [0, 999] — smoothly interpolated between hashed lattice points.
148
+ // noise1d(0) == noise1d(0), noise1d(500) is between noise1d(0) and noise1d(1000).
149
+ fn noise1d(x: int) -> int {
150
+ let ix: int = x / 1000;
151
+ let frac: int = x % 1000;
152
+ // Handle negative x so frac is always in [0, 999]
153
+ if (frac < 0) {
154
+ ix = ix - 1;
155
+ frac = frac + 1000;
156
+ }
157
+ let h0: int = hash_int(ix) % 1000;
158
+ let h1: int = hash_int(ix + 1) % 1000;
159
+ if (h0 < 0) { h0 = 0 - h0; }
160
+ if (h1 < 0) { h1 = 0 - h1; }
161
+ // Smoothstep for C1 continuity
162
+ let t: int = smoothstep(0, 1000, frac);
163
+ return lerp(h0, h1, t);
164
+ }
165
+
166
+ // ─── Category 3: Curves ──────────────────────────────────────────────────────
167
+
168
+ // Quadratic Bezier: B(t) = lerp(lerp(p0,p1,t), lerp(p1,p2,t), t)
169
+ // De Casteljau's algorithm — numerically stable, safe for large coordinates.
170
+ // t in [0, 1000] (fixed-point).
171
+ //
172
+ // bezier_quad(0, 500, 1000, 0) == 0 (t=0: start)
173
+ // bezier_quad(0, 500, 1000, 500) == 500 (t=500: midpoint of curve)
174
+ // bezier_quad(0, 500, 1000, 1000) == 1000 (t=1000: end)
175
+ // bezier_quad(0, 1000, 0, 500) == 500 (arch at midpoint)
176
+ fn bezier_quad(p0: int, p1: int, p2: int, t: int) -> int {
177
+ let m0: int = lerp(p0, p1, t);
178
+ let m1: int = lerp(p1, p2, t);
179
+ return lerp(m0, m1, t);
180
+ }
181
+
182
+ // Cubic Bezier: 4-point curve using De Casteljau's algorithm.
183
+ // t in [0, 1000].
184
+ // bezier_cubic(0, 333, 667, 1000, 0) == 0
185
+ // bezier_cubic(0, 333, 667, 1000, 1000) == 1000
186
+ fn bezier_cubic(p0: int, p1: int, p2: int, p3: int, t: int) -> int {
187
+ let m0: int = lerp(p0, p1, t);
188
+ let m1: int = lerp(p1, p2, t);
189
+ let m2: int = lerp(p2, p3, t);
190
+ let n0: int = lerp(m0, m1, t);
191
+ let n1: int = lerp(m1, m2, t);
192
+ return lerp(n0, n1, t);
193
+ }
194
+
195
+ // ─── Category 4: Fractals 🤯 ─────────────────────────────────────────────────
196
+
197
+ // Mandelbrot set iteration count.
198
+ //
199
+ // cx, cy: fixed-point coordinates of the complex number c = cx/1000 + i*cy/1000
200
+ // cx = -2000..1000 (i.e. real part -2.0..1.0)
201
+ // cy = -1000..1000 (imaginary part -1.0..1.0)
202
+ //
203
+ // Returns the number of iterations before |z| > 2 (escape), or max_iter if
204
+ // the point is in the Mandelbrot set. Use the return value to colour blocks!
205
+ //
206
+ // Points in the set: mandelbrot_iter(-1000, 0, 100) == 100 (c = -1+0i)
207
+ // Points outside: mandelbrot_iter(1000, 0, 100) == 0 (c = 1+0i, escapes immediately)
208
+ // Boundary region: mandelbrot_iter(-500, 500, 50) → varies
209
+ //
210
+ // Algorithm: z₀ = 0, z_{n+1} = z_n² + c
211
+ // z_n = (zr + zi·i), z_n² = zr²−zi² + 2·zr·zi·i
212
+ // Escape when |z|² = (zr²+zi²)/10⁶ > 4 ↔ mulfix(zr,zr)+mulfix(zi,zi) > 4000
213
+ fn mandelbrot_iter(cx: int, cy: int, max_iter: int) -> int {
214
+ let zr: int = 0;
215
+ let zi: int = 0;
216
+ let i: int = 0;
217
+ while (i < max_iter) {
218
+ let zr2: int = mulfix(zr, zr) - mulfix(zi, zi) + cx;
219
+ let zi2: int = 2 * mulfix(zr, zi) + cy;
220
+ zr = zr2;
221
+ zi = zi2;
222
+ if (mulfix(zr, zr) + mulfix(zi, zi) > 4000) {
223
+ return i;
224
+ }
225
+ i = i + 1;
226
+ }
227
+ return max_iter;
228
+ }
229
+
230
+ // Julia set iteration count (generalised Mandelbrot with fixed c and variable z₀).
231
+ // z0r, z0i: starting point (fixed-point, scale=1000)
232
+ // cr, ci: constant c (fixed-point, scale=1000)
233
+ // Same escape condition as mandelbrot_iter.
234
+ fn julia_iter(z0r: int, z0i: int, cr: int, ci: int, max_iter: int) -> int {
235
+ let zr: int = z0r;
236
+ let zi: int = z0i;
237
+ let i: int = 0;
238
+ while (i < max_iter) {
239
+ let zr2: int = mulfix(zr, zr) - mulfix(zi, zi) + cr;
240
+ let zi2: int = 2 * mulfix(zr, zi) + ci;
241
+ zr = zr2;
242
+ zi = zi2;
243
+ if (mulfix(zr, zr) + mulfix(zi, zi) > 4000) {
244
+ return i;
245
+ }
246
+ i = i + 1;
247
+ }
248
+ return max_iter;
249
+ }