redscript-mc 1.2.26 → 1.2.27

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 (219) hide show
  1. package/README.md +67 -9
  2. package/README.zh.md +61 -4
  3. package/dist/__tests__/stdlib-advanced.test.js +114 -0
  4. package/dist/__tests__/stdlib-bigint.test.d.ts +7 -0
  5. package/dist/__tests__/stdlib-bigint.test.js +428 -0
  6. package/dist/data/arena/function/__load.mcfunction +6 -0
  7. package/dist/data/arena/function/__tick.mcfunction +2 -0
  8. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  9. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  10. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  11. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  12. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  13. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  14. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  15. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  16. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  17. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  18. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  19. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  20. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  21. package/dist/data/counter/function/__load.mcfunction +5 -0
  22. package/dist/data/counter/function/__tick.mcfunction +2 -0
  23. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  24. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  25. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  26. package/dist/data/gcd2/function/__load.mcfunction +3 -0
  27. package/dist/data/gcd2/function/abs/merge_2.mcfunction +3 -0
  28. package/dist/data/gcd2/function/abs/then_0.mcfunction +5 -0
  29. package/dist/data/gcd2/function/abs.mcfunction +7 -0
  30. package/dist/data/gcd2/function/gcd/loop_body_1.mcfunction +7 -0
  31. package/dist/data/gcd2/function/gcd/loop_check_0.mcfunction +5 -0
  32. package/dist/data/gcd2/function/gcd/loop_exit_2.mcfunction +3 -0
  33. package/dist/data/gcd2/function/gcd.mcfunction +14 -0
  34. package/dist/data/gcd3/function/__load.mcfunction +3 -0
  35. package/dist/data/gcd3/function/abs/merge_2.mcfunction +3 -0
  36. package/dist/data/gcd3/function/abs/then_0.mcfunction +5 -0
  37. package/dist/data/gcd3/function/abs.mcfunction +7 -0
  38. package/dist/data/gcd3/function/gcd/loop_body_1.mcfunction +7 -0
  39. package/dist/data/gcd3/function/gcd/loop_check_0.mcfunction +5 -0
  40. package/dist/data/gcd3/function/gcd/loop_exit_2.mcfunction +3 -0
  41. package/dist/data/gcd3/function/gcd.mcfunction +14 -0
  42. package/dist/data/gcd3/function/test.mcfunction +7 -0
  43. package/dist/data/gcd3nm/function/__load.mcfunction +3 -0
  44. package/dist/data/gcd3nm/function/abs/merge_2.mcfunction +3 -0
  45. package/dist/data/gcd3nm/function/abs/then_0.mcfunction +5 -0
  46. package/dist/data/gcd3nm/function/abs.mcfunction +7 -0
  47. package/dist/data/gcd3nm/function/gcd/loop_body_1.mcfunction +7 -0
  48. package/dist/data/gcd3nm/function/gcd/loop_check_0.mcfunction +5 -0
  49. package/dist/data/gcd3nm/function/gcd/loop_exit_2.mcfunction +3 -0
  50. package/dist/data/gcd3nm/function/gcd.mcfunction +14 -0
  51. package/dist/data/gcd3nm/function/test.mcfunction +7 -0
  52. package/dist/data/gcd_test/function/__load.mcfunction +3 -0
  53. package/dist/data/gcd_test/function/abs/merge_2.mcfunction +3 -0
  54. package/dist/data/gcd_test/function/abs/then_0.mcfunction +5 -0
  55. package/dist/data/gcd_test/function/abs.mcfunction +7 -0
  56. package/dist/data/gcd_test/function/gcd/loop_body_1.mcfunction +7 -0
  57. package/dist/data/gcd_test/function/gcd/loop_check_0.mcfunction +5 -0
  58. package/dist/data/gcd_test/function/gcd/loop_exit_2.mcfunction +3 -0
  59. package/dist/data/gcd_test/function/gcd.mcfunction +14 -0
  60. package/dist/data/isqrttest/function/__load.mcfunction +6 -0
  61. package/dist/data/isqrttest/function/isqrt/loop_body_4.mcfunction +12 -0
  62. package/dist/data/isqrttest/function/isqrt/loop_check_3.mcfunction +5 -0
  63. package/dist/data/isqrttest/function/isqrt/loop_exit_5.mcfunction +3 -0
  64. package/dist/data/isqrttest/function/isqrt/merge_2.mcfunction +4 -0
  65. package/dist/data/isqrttest/function/isqrt/merge_8.mcfunction +6 -0
  66. package/dist/data/isqrttest/function/isqrt/then_0.mcfunction +3 -0
  67. package/dist/data/isqrttest/function/isqrt/then_6.mcfunction +3 -0
  68. package/dist/data/isqrttest/function/isqrt.mcfunction +7 -0
  69. package/dist/data/isqrttest/function/test.mcfunction +6 -0
  70. package/dist/data/mathtest/function/__load.mcfunction +3 -0
  71. package/dist/data/mathtest/function/abs/merge_2.mcfunction +3 -0
  72. package/dist/data/mathtest/function/abs/then_0.mcfunction +5 -0
  73. package/dist/data/mathtest/function/abs.mcfunction +6 -0
  74. package/dist/data/mathtest/function/test.mcfunction +5 -0
  75. package/dist/data/minecraft/tags/function/load.json +5 -0
  76. package/dist/data/minecraft/tags/function/tick.json +5 -0
  77. package/dist/data/mypack/function/__load.mcfunction +13 -0
  78. package/dist/data/mypack/function/_atan_init.mcfunction +2 -0
  79. package/dist/data/mypack/function/abs/merge_2.mcfunction +3 -0
  80. package/dist/data/mypack/function/abs/then_0.mcfunction +5 -0
  81. package/dist/data/mypack/function/abs.mcfunction +6 -0
  82. package/dist/data/mypack/function/atan2_fixed/__sgi_1.mcfunction +2 -0
  83. package/dist/data/mypack/function/atan2_fixed/else_34.mcfunction +3 -0
  84. package/dist/data/mypack/function/atan2_fixed/loop_body_31.mcfunction +19 -0
  85. package/dist/data/mypack/function/atan2_fixed/loop_check_30.mcfunction +5 -0
  86. package/dist/data/mypack/function/atan2_fixed/loop_exit_32.mcfunction +6 -0
  87. package/dist/data/mypack/function/atan2_fixed/merge_11.mcfunction +6 -0
  88. package/dist/data/mypack/function/atan2_fixed/merge_14.mcfunction +3 -0
  89. package/dist/data/mypack/function/atan2_fixed/merge_17.mcfunction +6 -0
  90. package/dist/data/mypack/function/atan2_fixed/merge_2.mcfunction +5 -0
  91. package/dist/data/mypack/function/atan2_fixed/merge_20.mcfunction +5 -0
  92. package/dist/data/mypack/function/atan2_fixed/merge_23.mcfunction +5 -0
  93. package/dist/data/mypack/function/atan2_fixed/merge_26.mcfunction +6 -0
  94. package/dist/data/mypack/function/atan2_fixed/merge_29.mcfunction +4 -0
  95. package/dist/data/mypack/function/atan2_fixed/merge_38.mcfunction +5 -0
  96. package/dist/data/mypack/function/atan2_fixed/merge_41.mcfunction +5 -0
  97. package/dist/data/mypack/function/atan2_fixed/merge_44.mcfunction +5 -0
  98. package/dist/data/mypack/function/atan2_fixed/merge_47.mcfunction +5 -0
  99. package/dist/data/mypack/function/atan2_fixed/merge_5.mcfunction +5 -0
  100. package/dist/data/mypack/function/atan2_fixed/merge_8.mcfunction +3 -0
  101. package/dist/data/mypack/function/atan2_fixed/then_0.mcfunction +5 -0
  102. package/dist/data/mypack/function/atan2_fixed/then_12.mcfunction +3 -0
  103. package/dist/data/mypack/function/atan2_fixed/then_15.mcfunction +5 -0
  104. package/dist/data/mypack/function/atan2_fixed/then_18.mcfunction +5 -0
  105. package/dist/data/mypack/function/atan2_fixed/then_21.mcfunction +3 -0
  106. package/dist/data/mypack/function/atan2_fixed/then_24.mcfunction +3 -0
  107. package/dist/data/mypack/function/atan2_fixed/then_27.mcfunction +6 -0
  108. package/dist/data/mypack/function/atan2_fixed/then_3.mcfunction +3 -0
  109. package/dist/data/mypack/function/atan2_fixed/then_33.mcfunction +5 -0
  110. package/dist/data/mypack/function/atan2_fixed/then_36.mcfunction +5 -0
  111. package/dist/data/mypack/function/atan2_fixed/then_39.mcfunction +5 -0
  112. package/dist/data/mypack/function/atan2_fixed/then_42.mcfunction +3 -0
  113. package/dist/data/mypack/function/atan2_fixed/then_45.mcfunction +5 -0
  114. package/dist/data/mypack/function/atan2_fixed/then_6.mcfunction +3 -0
  115. package/dist/data/mypack/function/atan2_fixed/then_9.mcfunction +5 -0
  116. package/dist/data/mypack/function/atan2_fixed.mcfunction +7 -0
  117. package/dist/data/mypack/function/my_game.mcfunction +10 -0
  118. package/dist/data/quiz/function/__load.mcfunction +16 -0
  119. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  120. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  121. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  122. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  123. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  124. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  125. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  126. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  127. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  128. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  129. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  130. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  131. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  132. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  133. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  134. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  135. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  136. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  137. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  138. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  139. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  140. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  141. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  142. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  143. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  144. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  145. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  146. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  147. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  148. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  149. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  150. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  151. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  152. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  153. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  154. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  155. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  156. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  157. package/dist/data/reqtest/function/__load.mcfunction +4 -0
  158. package/dist/data/reqtest/function/_table_init.mcfunction +2 -0
  159. package/dist/data/reqtest/function/no_trig.mcfunction +3 -0
  160. package/dist/data/reqtest/function/use_table.mcfunction +4 -0
  161. package/dist/data/reqtest2/function/__load.mcfunction +3 -0
  162. package/dist/data/reqtest2/function/no_trig.mcfunction +3 -0
  163. package/dist/data/runtime/function/__load.mcfunction +5 -0
  164. package/dist/data/runtime/function/__tick.mcfunction +2 -0
  165. package/dist/data/runtime/function/counter_tick/then_0.mcfunction +3 -0
  166. package/dist/data/runtime/function/counter_tick.mcfunction +13 -0
  167. package/dist/data/shop/function/__load.mcfunction +7 -0
  168. package/dist/data/shop/function/__tick.mcfunction +3 -0
  169. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  170. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  171. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  172. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  173. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  174. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  175. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  176. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  177. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  178. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  179. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  180. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  181. package/dist/data/swap_test/function/__load.mcfunction +3 -0
  182. package/dist/data/swap_test/function/gcd_old/loop_body_1.mcfunction +7 -0
  183. package/dist/data/swap_test/function/gcd_old/loop_check_0.mcfunction +5 -0
  184. package/dist/data/swap_test/function/gcd_old/loop_exit_2.mcfunction +3 -0
  185. package/dist/data/swap_test/function/gcd_old.mcfunction +8 -0
  186. package/dist/data/turret/function/__load.mcfunction +5 -0
  187. package/dist/data/turret/function/__tick.mcfunction +4 -0
  188. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  189. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  190. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  191. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  192. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  193. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  194. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  195. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  196. package/dist/gcd2.map.json +15 -0
  197. package/dist/gcd3.map.json +17 -0
  198. package/dist/gcd_test.map.json +15 -0
  199. package/dist/isqrttest.map.json +15 -0
  200. package/dist/lowering/index.js +71 -4
  201. package/dist/mathtest.map.json +6 -0
  202. package/dist/mypack.map.json +27 -0
  203. package/dist/pack.mcmeta +6 -0
  204. package/dist/reqtest.map.json +4 -0
  205. package/dist/reqtest2.map.json +4 -0
  206. package/dist/runtime/index.js +26 -5
  207. package/dist/runtime.map.json +7 -0
  208. package/dist/swap_test.map.json +14 -0
  209. package/editors/vscode/package-lock.json +2 -2
  210. package/editors/vscode/package.json +1 -1
  211. package/examples/showcase.mcrs +505 -0
  212. package/package.json +1 -1
  213. package/src/__tests__/stdlib-advanced.test.ts +120 -0
  214. package/src/__tests__/stdlib-bigint.test.ts +427 -0
  215. package/src/lowering/index.ts +75 -4
  216. package/src/runtime/index.ts +27 -5
  217. package/src/stdlib/advanced.mcrs +81 -0
  218. package/src/stdlib/bigint.mcrs +205 -0
  219. package/src/stdlib/math.mcrs +19 -6
@@ -0,0 +1,427 @@
1
+ /**
2
+ * stdlib/bigint.mcrs — runtime behavioural tests
3
+ *
4
+ * Tests basic BigInt operations (base 10000, 8 limbs = 32 decimal digits).
5
+ * All arithmetic validated against known values.
6
+ */
7
+
8
+ import * as fs from 'fs'
9
+ import * as path from 'path'
10
+ import { compile } from '../compile'
11
+ import { MCRuntime } from '../runtime'
12
+
13
+ const MATH_SRC = fs.readFileSync(path.join(__dirname, '../../src/stdlib/math.mcrs'), 'utf-8')
14
+ const BIGINT_SRC = fs.readFileSync(path.join(__dirname, '../../src/stdlib/bigint.mcrs'), 'utf-8')
15
+
16
+ function run(driver: string): MCRuntime {
17
+ const result = compile(driver, {
18
+ namespace: 'bitest',
19
+ librarySources: [MATH_SRC, BIGINT_SRC],
20
+ })
21
+ if (!result.success) throw new Error(result.error?.message ?? 'compile failed')
22
+ const rt = new MCRuntime('bitest')
23
+ for (const file of result.files ?? []) {
24
+ if (!file.path.endsWith('.mcfunction')) continue
25
+ const match = file.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction$/)
26
+ if (!match) continue
27
+ rt.loadFunction(`${match[1]}:${match[2]}`, file.content.split('\n'))
28
+ }
29
+ rt.load()
30
+ return rt
31
+ }
32
+
33
+ function sc(rt: MCRuntime, key: string): number {
34
+ return rt.getScore('out', `bitest.${key}`) ?? 0
35
+ }
36
+
37
+ // ── storage_set_int roundtrip ─────────────────────────────────────────────────
38
+
39
+ describe('storage_set_int roundtrip', () => {
40
+ it('static index: write 99 to arr[2], read back', () => {
41
+ const rt = run(`fn test() {
42
+ storage_set_array("rs:t", "arr", "[10,20,30,40]");
43
+ storage_set_int("rs:t", "arr", 2, 99);
44
+ scoreboard_set("out", "r", storage_get_int("rs:t", "arr", 2));
45
+ }`)
46
+ rt.execFunction('test')
47
+ expect(sc(rt, 'r')).toBe(99)
48
+ })
49
+
50
+ it('runtime index: write 777 to arr[idx], read back', () => {
51
+ const rt = run(`fn test() {
52
+ storage_set_array("rs:t", "arr", "[1,2,3,4,5]");
53
+ let idx: int = 3;
54
+ storage_set_int("rs:t", "arr", idx, 777);
55
+ scoreboard_set("out", "r", storage_get_int("rs:t", "arr", idx));
56
+ }`)
57
+ rt.execFunction('test')
58
+ expect(sc(rt, 'r')).toBe(777)
59
+ })
60
+
61
+ it('loop write: fill arr[0..3] with 10,20,30,40', () => {
62
+ const rt = run(`fn test() {
63
+ storage_set_array("rs:t", "arr", "[0,0,0,0]");
64
+ let i: int = 0;
65
+ while (i < 4) {
66
+ storage_set_int("rs:t", "arr", i, (i + 1) * 10);
67
+ i = i + 1;
68
+ }
69
+ scoreboard_set("out", "a", storage_get_int("rs:t", "arr", 0));
70
+ scoreboard_set("out", "b", storage_get_int("rs:t", "arr", 1));
71
+ scoreboard_set("out", "c", storage_get_int("rs:t", "arr", 2));
72
+ scoreboard_set("out", "d", storage_get_int("rs:t", "arr", 3));
73
+ }`)
74
+ rt.execFunction('test')
75
+ expect(sc(rt, 'a')).toBe(10)
76
+ expect(sc(rt, 'b')).toBe(20)
77
+ expect(sc(rt, 'c')).toBe(30)
78
+ expect(sc(rt, 'd')).toBe(40)
79
+ })
80
+ })
81
+
82
+ // ── bigint_init + from_int ────────────────────────────────────────────────────
83
+
84
+ describe('bigint init and load', () => {
85
+ it('bigint_init zeros registers', () => {
86
+ const rt = run(`fn test() {
87
+ bigint_init();
88
+ scoreboard_set("out", "r", bigint_get_a(0));
89
+ }`)
90
+ rt.execFunction('test')
91
+ expect(sc(rt, 'r')).toBe(0)
92
+ })
93
+
94
+ it('bigint_from_int_a(12345678): limb0=5678, limb1=1234', () => {
95
+ const rt = run(`fn test() {
96
+ bigint_init();
97
+ bigint_from_int_a(12345678);
98
+ scoreboard_set("out", "l0", bigint_get_a(0));
99
+ scoreboard_set("out", "l1", bigint_get_a(1));
100
+ scoreboard_set("out", "l2", bigint_get_a(2));
101
+ }`)
102
+ rt.execFunction('test')
103
+ expect(sc(rt, 'l0')).toBe(5678)
104
+ expect(sc(rt, 'l1')).toBe(1234)
105
+ expect(sc(rt, 'l2')).toBe(0)
106
+ })
107
+
108
+ it('bigint_from_int_a(2000000000): limb0=0, limb1=0, limb2=20', () => {
109
+ // 2000000000 = 20 × 10000^2 (not 2000: 20 × 10^8 = 2×10^9 ✓)
110
+ const rt = run(`fn test() {
111
+ bigint_init();
112
+ bigint_from_int_a(2000000000);
113
+ scoreboard_set("out", "l0", bigint_get_a(0));
114
+ scoreboard_set("out", "l1", bigint_get_a(1));
115
+ scoreboard_set("out", "l2", bigint_get_a(2));
116
+ }`)
117
+ rt.execFunction('test')
118
+ expect(sc(rt, 'l0')).toBe(0)
119
+ expect(sc(rt, 'l1')).toBe(0)
120
+ expect(sc(rt, 'l2')).toBe(20)
121
+ })
122
+ })
123
+
124
+ // ── bigint_add ────────────────────────────────────────────────────────────────
125
+
126
+ describe('bigint_add', () => {
127
+ it('1 + 1 = 2 (simple)', () => {
128
+ const rt = run(`fn test() {
129
+ bigint_init();
130
+ bigint_from_int_a(1);
131
+ bigint_from_int_b(1);
132
+ bigint_add();
133
+ scoreboard_set("out", "r", bigint_get_c(0));
134
+ }`)
135
+ rt.execFunction('test')
136
+ expect(sc(rt, 'r')).toBe(2)
137
+ })
138
+
139
+ it('9999 + 1 = 10000: carry across limb boundary', () => {
140
+ // a=[9999,0,...] + b=[1,0,...] = c=[0,1,0,...] (carry to limb1)
141
+ const rt = run(`fn test() {
142
+ bigint_init();
143
+ bigint_from_int_a(9999);
144
+ bigint_from_int_b(1);
145
+ bigint_add();
146
+ scoreboard_set("out", "l0", bigint_get_c(0));
147
+ scoreboard_set("out", "l1", bigint_get_c(1));
148
+ }`)
149
+ rt.execFunction('test')
150
+ expect(sc(rt, 'l0')).toBe(0)
151
+ expect(sc(rt, 'l1')).toBe(1)
152
+ })
153
+
154
+ it('99990000 + 10000 = 100000000: multi-limb carry', () => {
155
+ // a=[0,9999,...] + b=[0,1,...] = c=[0,0,1,...] (carry to limb2)
156
+ const rt = run(`fn test() {
157
+ bigint_init();
158
+ bigint_from_int_a(99990000);
159
+ bigint_from_int_b(10000);
160
+ bigint_add();
161
+ scoreboard_set("out", "l0", bigint_get_c(0));
162
+ scoreboard_set("out", "l1", bigint_get_c(1));
163
+ scoreboard_set("out", "l2", bigint_get_c(2));
164
+ }`)
165
+ rt.execFunction('test')
166
+ expect(sc(rt, 'l0')).toBe(0)
167
+ expect(sc(rt, 'l1')).toBe(0)
168
+ expect(sc(rt, 'l2')).toBe(1)
169
+ })
170
+
171
+ it('large add: 999999999 + 999999999', () => {
172
+ // 999999999 = [9999, 9999, 9, 0, ...]
173
+ // + same = [9998, 9999, 18, 0, ...] after carry
174
+ // = 1999999998: l0=9998, l1=9999, l2=19 (carry: 9+9=18, no carry from l2)
175
+ // Wait: l0=9999+9999=19998, carry=1, l0=9998
176
+ // l1=9999+9999+1=19999, carry=1, l1=9999
177
+ // l2=9+9+1=19, carry=0, l2=19
178
+ const rt = run(`fn test() {
179
+ bigint_init();
180
+ bigint_from_int_a(999999999);
181
+ bigint_from_int_b(999999999);
182
+ bigint_add();
183
+ scoreboard_set("out", "l0", bigint_get_c(0));
184
+ scoreboard_set("out", "l1", bigint_get_c(1));
185
+ scoreboard_set("out", "l2", bigint_get_c(2));
186
+ }`)
187
+ rt.execFunction('test')
188
+ expect(sc(rt, 'l0')).toBe(9998) // 1999999998 % 10000 = 9998
189
+ expect(sc(rt, 'l1')).toBe(9999) // floor(1999999998 / 10000) % 10000 = 9999
190
+ expect(sc(rt, 'l2')).toBe(19) // floor(1999999998 / 100000000) = 19
191
+ })
192
+ })
193
+
194
+ // ── bigint_sub ────────────────────────────────────────────────────────────────
195
+
196
+ describe('bigint_sub', () => {
197
+ it('10 - 3 = 7', () => {
198
+ const rt = run(`fn test() {
199
+ bigint_init();
200
+ bigint_from_int_a(10);
201
+ bigint_from_int_b(3);
202
+ bigint_sub();
203
+ scoreboard_set("out", "r", bigint_get_c(0));
204
+ }`)
205
+ rt.execFunction('test')
206
+ expect(sc(rt, 'r')).toBe(7)
207
+ })
208
+
209
+ it('10000 - 1 = 9999: borrow across limb boundary', () => {
210
+ // a=[0,1,...] - b=[1,0,...] = c=[9999,0,...] (borrow from limb1)
211
+ const rt = run(`fn test() {
212
+ bigint_init();
213
+ bigint_from_int_a(10000);
214
+ bigint_from_int_b(1);
215
+ bigint_sub();
216
+ scoreboard_set("out", "l0", bigint_get_c(0));
217
+ scoreboard_set("out", "l1", bigint_get_c(1));
218
+ }`)
219
+ rt.execFunction('test')
220
+ expect(sc(rt, 'l0')).toBe(9999)
221
+ expect(sc(rt, 'l1')).toBe(0)
222
+ })
223
+ })
224
+
225
+ // ── bigint_compare ────────────────────────────────────────────────────────────
226
+
227
+ describe('bigint_compare', () => {
228
+ it('1 == 1 → 0', () => {
229
+ const rt = run(`fn test() {
230
+ bigint_init();
231
+ bigint_from_int_a(1);
232
+ bigint_from_int_b(1);
233
+ scoreboard_set("out", "r", bigint_compare());
234
+ }`)
235
+ rt.execFunction('test')
236
+ expect(sc(rt, 'r')).toBe(0)
237
+ })
238
+
239
+ it('2 > 1 → 1', () => {
240
+ const rt = run(`fn test() {
241
+ bigint_init();
242
+ bigint_from_int_a(2);
243
+ bigint_from_int_b(1);
244
+ scoreboard_set("out", "r", bigint_compare());
245
+ }`)
246
+ rt.execFunction('test')
247
+ expect(sc(rt, 'r')).toBe(1)
248
+ })
249
+
250
+ it('1 < 2 → -1', () => {
251
+ const rt = run(`fn test() {
252
+ bigint_init();
253
+ bigint_from_int_a(1);
254
+ bigint_from_int_b(2);
255
+ scoreboard_set("out", "r", bigint_compare());
256
+ }`)
257
+ rt.execFunction('test')
258
+ expect(sc(rt, 'r')).toBe(-1)
259
+ })
260
+ })
261
+
262
+ // ── bigint_mul_small ──────────────────────────────────────────────────────────
263
+
264
+ describe('bigint_mul_small', () => {
265
+ it('12345 * 2 = 24690', () => {
266
+ const rt = run(`fn test() {
267
+ bigint_init();
268
+ bigint_from_int_a(12345);
269
+ bigint_mul_small(2);
270
+ scoreboard_set("out", "l0", bigint_get_c(0));
271
+ scoreboard_set("out", "l1", bigint_get_c(1));
272
+ }`)
273
+ rt.execFunction('test')
274
+ expect(sc(rt, 'l0')).toBe(4690)
275
+ expect(sc(rt, 'l1')).toBe(2)
276
+ })
277
+
278
+ it('9999 * 9999 = 99980001: carry', () => {
279
+ // c[0] = 99980001 % 10000 = 1
280
+ // c[1] = floor(99980001 / 10000) = 9998
281
+ const rt = run(`fn test() {
282
+ bigint_init();
283
+ bigint_from_int_a(9999);
284
+ bigint_mul_small(9999);
285
+ scoreboard_set("out", "l0", bigint_get_c(0));
286
+ scoreboard_set("out", "l1", bigint_get_c(1));
287
+ }`)
288
+ rt.execFunction('test')
289
+ expect(sc(rt, 'l0')).toBe(1) // 99980001 % 10000 = 1 (actually 0001)
290
+ expect(sc(rt, 'l1')).toBe(9998) // 9998
291
+ })
292
+ })
293
+
294
+ // ── bigint_mul ────────────────────────────────────────────────────────────────
295
+
296
+ describe('bigint_mul', () => {
297
+ it('3 * 4 = 12', () => {
298
+ const rt = run(`fn test() {
299
+ bigint_init();
300
+ bigint_from_int_a(3);
301
+ bigint_from_int_b(4);
302
+ bigint_mul();
303
+ scoreboard_set("out", "r", bigint_get_c(0));
304
+ }`)
305
+ rt.execFunction('test')
306
+ expect(sc(rt, 'r')).toBe(12)
307
+ })
308
+
309
+ it('9999 * 9999 = 99980001', () => {
310
+ const rt = run(`fn test() {
311
+ bigint_init();
312
+ bigint_from_int_a(9999);
313
+ bigint_from_int_b(9999);
314
+ bigint_mul();
315
+ scoreboard_set("out", "l0", bigint_get_c(0));
316
+ scoreboard_set("out", "l1", bigint_get_c(1));
317
+ }`)
318
+ rt.execFunction('test')
319
+ expect(sc(rt, 'l0')).toBe(1)
320
+ expect(sc(rt, 'l1')).toBe(9998)
321
+ })
322
+
323
+ it('100000 * 100000 = 10^10: spans 3 limbs', () => {
324
+ // 10^10 = [0, 0, 0, 1, 0, ...] in base 10000 (1 * 10000^3 = 10^12? no)
325
+ // 10^10 / 10000^0 % 10000 = 0
326
+ // 10^10 / 10000^1 % 10000 = 0
327
+ // 10^10 / 10000^2 % 10000 = 10000 → wait: 10^10 / 10^8 = 100, 100 % 10000 = 100
328
+ // Actually: 10^10 = 100 * 10^8 = 100 * (10^4)^2
329
+ // l0 = 10^10 % 10^4 = 0
330
+ // l1 = floor(10^10 / 10^4) % 10^4 = floor(10^6) % 10000 = 0
331
+ // l2 = floor(10^10 / 10^8) % 10^4 = 100 % 10000 = 100
332
+ const rt = run(`fn test() {
333
+ bigint_init();
334
+ bigint_from_int_a(100000);
335
+ bigint_from_int_b(100000);
336
+ bigint_mul();
337
+ scoreboard_set("out", "l0", bigint_get_c(0));
338
+ scoreboard_set("out", "l1", bigint_get_c(1));
339
+ scoreboard_set("out", "l2", bigint_get_c(2));
340
+ }`)
341
+ rt.execFunction('test')
342
+ expect(sc(rt, 'l0')).toBe(0)
343
+ expect(sc(rt, 'l1')).toBe(0)
344
+ expect(sc(rt, 'l2')).toBe(100)
345
+ })
346
+ })
347
+
348
+ // ── bigint_fib ────────────────────────────────────────────────────────────────
349
+
350
+ describe('bigint_fib', () => {
351
+ it('F(0) = 0', () => {
352
+ const rt = run(`fn test() {
353
+ bigint_fib(0);
354
+ scoreboard_set("out", "r", bigint_get_a(0));
355
+ }`)
356
+ rt.execFunction('test')
357
+ expect(sc(rt, 'r')).toBe(0)
358
+ })
359
+
360
+ it('F(1) = 1', () => {
361
+ const rt = run(`fn test() {
362
+ bigint_fib(1);
363
+ scoreboard_set("out", "r", bigint_get_a(0));
364
+ }`)
365
+ rt.execFunction('test')
366
+ expect(sc(rt, 'r')).toBe(1)
367
+ })
368
+
369
+ it('F(10) = 55', () => {
370
+ const rt = run(`fn test() {
371
+ bigint_fib(10);
372
+ scoreboard_set("out", "r", bigint_get_a(0));
373
+ }`)
374
+ rt.execFunction('test')
375
+ expect(sc(rt, 'r')).toBe(55)
376
+ })
377
+
378
+ it('F(20) = 6765', () => {
379
+ const rt = run(`fn test() {
380
+ bigint_fib(20);
381
+ scoreboard_set("out", "r", bigint_get_a(0));
382
+ }`)
383
+ rt.execFunction('test')
384
+ expect(sc(rt, 'r')).toBe(6765)
385
+ })
386
+
387
+ it('F(50) = 12586269025: limb0=9025, limb1=8626, limb2=125', () => {
388
+ // F(50) = 12,586,269,025
389
+ // 12586269025 % 10000 = 9025
390
+ // floor(12586269025 / 10000) % 10000 = 1258626 % 10000 = 8626
391
+ // floor(12586269025 / 10^8) = 125
392
+ const rt = run(`fn test() {
393
+ bigint_fib(50);
394
+ scoreboard_set("out", "l0", bigint_get_a(0));
395
+ scoreboard_set("out", "l1", bigint_get_a(1));
396
+ scoreboard_set("out", "l2", bigint_get_a(2));
397
+ }`)
398
+ rt.execFunction('test')
399
+ expect(sc(rt, 'l0')).toBe(9025)
400
+ expect(sc(rt, 'l1')).toBe(8626)
401
+ expect(sc(rt, 'l2')).toBe(125)
402
+ })
403
+
404
+ it('F(100) low limbs check', () => {
405
+ // F(100) = 354224848179261915075
406
+ // % 10000 = 5075
407
+ // floor / 10000 % 10000 = floor(35422484817926191.5075) % 10000 = ...
408
+ // Let's compute:
409
+ // 354224848179261915075 % 10000 = 5075
410
+ // floor(354224848179261915075 / 10000) = 35422484817926191507 (JS BigInt)
411
+ // 35422484817926191507 % 10000 = 1507
412
+ // floor(35422484817926191507 / 10000) = 3542248481792619 (roughly)
413
+ // % 10000 = 2619
414
+ const rt = run(`fn test() {
415
+ bigint_fib(100);
416
+ scoreboard_set("out", "l0", bigint_get_a(0));
417
+ scoreboard_set("out", "l1", bigint_get_a(1));
418
+ scoreboard_set("out", "l2", bigint_get_a(2));
419
+ }`)
420
+ rt.execFunction('test')
421
+ const f100 = BigInt('354224848179261915075')
422
+ const b = BigInt(10000)
423
+ expect(sc(rt, 'l0')).toBe(Number(f100 % b))
424
+ expect(sc(rt, 'l1')).toBe(Number((f100 / b) % b))
425
+ expect(sc(rt, 'l2')).toBe(Number((f100 / b / b) % b))
426
+ })
427
+ })
@@ -108,6 +108,7 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
108
108
  clearInterval: () => null, // Special handling
109
109
  storage_get_int: () => null, // Special handling (dynamic NBT array read via macro)
110
110
  storage_set_array: () => null, // Special handling (write literal NBT array to storage)
111
+ storage_set_int: () => null, // Special handling (dynamic NBT array write via macro)
111
112
  }
112
113
 
113
114
  export interface Warning {
@@ -364,12 +365,24 @@ export class Lowering {
364
365
 
365
366
  private preScanExpr(expr: Expr, paramNames: Set<string>, macroParams: Set<string>): void {
366
367
  if (expr.kind === 'call' && BUILTINS[expr.fn] !== undefined) {
367
- // All ident args to macro-aware builtins that are params → macro params
368
- for (const arg of expr.args) {
369
- if (arg.kind === 'ident' && paramNames.has(arg.name)) {
370
- macroParams.add(arg.name)
368
+ // Only trigger macro param detection for builtins that actually emit
369
+ // MC commands with $(param) inline in the current function body.
370
+ // Special-handled builtins (storage_get_int / storage_set_int / etc.) are
371
+ // declared as `() => null` — they create their own sub-functions for macro
372
+ // indirection and do NOT require the surrounding function to be a macro.
373
+ const handler = BUILTINS[expr.fn]!
374
+ const isSpecialHandled: boolean = (() => {
375
+ try { return (handler as () => string | null)() === null } catch { return false }
376
+ })()
377
+ if (!isSpecialHandled) {
378
+ for (const arg of expr.args) {
379
+ if (arg.kind === 'ident' && paramNames.has(arg.name)) {
380
+ macroParams.add(arg.name)
381
+ }
371
382
  }
372
383
  }
384
+ // Always recurse into args for nested calls/expressions
385
+ for (const arg of expr.args) this.preScanExpr(arg, paramNames, macroParams)
373
386
  return
374
387
  }
375
388
  // Recurse into sub-expressions for other call types
@@ -2629,6 +2642,64 @@ export class Lowering {
2629
2642
  return { kind: 'const', value: 0 }
2630
2643
  }
2631
2644
 
2645
+ // storage_set_int(storage_ns, array_key, index, value) -> void
2646
+ // Writes one integer element into an NBT int-array stored in data storage.
2647
+ // storage_ns : e.g. "rs:bigint"
2648
+ // array_key : e.g. "a"
2649
+ // index : element index (const or runtime)
2650
+ // value : integer value to write (const or runtime)
2651
+ //
2652
+ // Const index + const value:
2653
+ // execute store result storage <ns> <key>[N] int 1 run scoreboard players set $const_V rs V
2654
+ // Runtime index or value: macro sub-function via rs:heap
2655
+ if (name === 'storage_set_int') {
2656
+ const storageNs = this.exprToString(args[0])
2657
+ const arrayKey = this.exprToString(args[1])
2658
+ const indexOperand = this.lowerExpr(args[2])
2659
+ const valueOperand = this.lowerExpr(args[3])
2660
+
2661
+ if (indexOperand.kind === 'const') {
2662
+ // Static index — use execute store result to write to the fixed slot
2663
+ const valVar = valueOperand.kind === 'var'
2664
+ ? valueOperand.name
2665
+ : this.operandToVar(valueOperand)
2666
+ this.builder.emitRaw(
2667
+ `execute store result storage ${storageNs} ${arrayKey}[${indexOperand.value}] int 1 run scoreboard players get ${valVar} rs`
2668
+ )
2669
+ } else {
2670
+ // Runtime index: we need a macro sub-function.
2671
+ // Store index + value into rs:heap, call macro that does:
2672
+ // $data modify storage <ns> <key>[$(idx_key)] set value $(val_key)
2673
+ const macroIdxKey = `__ssi_i_${this.foreachCounter++}`
2674
+ const macroValKey = `__ssi_v_${this.foreachCounter++}` // kept to pin valVar in optimizer
2675
+ const subFnName = `${this.currentFn}/__ssi_${this.foreachCounter++}`
2676
+ const indexVar = indexOperand.kind === 'var'
2677
+ ? indexOperand.name
2678
+ : this.operandToVar(indexOperand)
2679
+ const valVar = valueOperand.kind === 'var'
2680
+ ? valueOperand.name
2681
+ : this.operandToVar(valueOperand)
2682
+ this.builder.emitRaw(
2683
+ `execute store result storage rs:heap ${macroIdxKey} int 1 run scoreboard players get ${indexVar} rs`
2684
+ )
2685
+ // Pin valVar in the optimizer's read-set so the assignment is not dead-code-eliminated.
2686
+ // The value is stored to rs:heap but NOT used by the macro (the macro reads the scoreboard
2687
+ // slot directly to avoid the MC 'data modify set value $(n)' macro substitution bug).
2688
+ this.builder.emitRaw(
2689
+ `execute store result storage rs:heap ${macroValKey} int 1 run scoreboard players get ${valVar} rs`
2690
+ )
2691
+ this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
2692
+ // Use execute store result (not 'data modify set value $(val)') to avoid MC macro
2693
+ // substitution bugs with numeric values. The scoreboard slot ${valVar} is hardcoded
2694
+ // into the macro sub-function at compile time — only the array index is macro-substituted.
2695
+ this.emitRawSubFunction(
2696
+ subFnName,
2697
+ `\x01execute store result storage ${storageNs} ${arrayKey}[$(${macroIdxKey})] int 1 run scoreboard players get ${valVar} rs`
2698
+ )
2699
+ }
2700
+ return { kind: 'const', value: 0 }
2701
+ }
2702
+
2632
2703
  // data_merge(target, nbt) — merge NBT into entity/block/storage
2633
2704
  // data_merge(@s, { Invisible: 1b, Silent: 1b })
2634
2705
  if (name === 'data_merge') {
@@ -697,17 +697,27 @@ export class MCRuntime {
697
697
  continue
698
698
  }
699
699
 
700
- // Handle 'store result storage <ns:path> <field> <type> <scale>'
700
+ // Handle 'store result storage <ns:path> <field>[<idx>] <type> <scale>' (array element)
701
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+(.*)$/)
702
+ const sliced = rest.slice(21)
703
+ // Try array-index form first: <ns:path> <field>[<idx>] <type> <scale> <run-cmd>
704
+ // Use [^\[\s]+ for field (no brackets or spaces) so that \[ matches correctly.
705
+ const arrParts = sliced.match(/^(\S+)\s+([^\[\s]+)\[(\d+)\]\s+(\S+)\s+([\d.]+)\s+(.*)$/)
706
+ if (arrParts) {
707
+ const [, storagePath, field, indexStr, , , remaining] = arrParts
708
+ storeTarget = { storagePath, field: `${field}[${indexStr}]`, type: 'result' }
709
+ rest = remaining.trim()
710
+ continue
711
+ }
712
+ // Plain form: <ns:path> <field> <type> <scale> <run-cmd>
713
+ const storageParts = sliced.match(/^(\S+)\s+(\S+)\s+(\S+)\s+([\d.]+)\s+(.*)$/)
705
714
  if (storageParts) {
706
715
  const [, storagePath, field, , , remaining] = storageParts
707
716
  storeTarget = { storagePath, field, type: 'result' }
708
717
  rest = remaining.trim()
709
718
  continue
710
719
  }
720
+ rest = sliced
711
721
  }
712
722
 
713
723
  // Handle 'store result score <player> <obj>'
@@ -834,8 +844,20 @@ export class MCRuntime {
834
844
  return true
835
845
  }
836
846
 
847
+ // data modify storage <ns:path> <field>[<index>] set value <val> (array element write)
848
+ const setArrMatch = cmd.match(/^data modify storage (\S+) ([^\[\s]+)\[(\d+)\] set value (.+)$/)
849
+ if (setArrMatch) {
850
+ const [, storagePath, field, indexStr, valueStr] = setArrMatch
851
+ const arr = this.getStorageField(storagePath, field)
852
+ const idx = parseInt(indexStr, 10)
853
+ if (Array.isArray(arr) && idx >= 0 && idx < arr.length) {
854
+ arr[idx] = this.parseDataValue(valueStr)
855
+ }
856
+ return true
857
+ }
858
+
837
859
  // data get storage <ns:path> <field>[<index>] [scale] (array element access)
838
- const getArrMatch = cmd.match(/^data get storage (\S+) (\S+)\[(\d+)\](?:\s+[\d.]+)?$/)
860
+ const getArrMatch = cmd.match(/^data get storage (\S+) ([^\[\s]+)\[(\d+)\](?:\s+[\d.]+)?$/)
839
861
  if (getArrMatch) {
840
862
  const [, storagePath, field, indexStr] = getArrMatch
841
863
  const arr = this.getStorageField(storagePath, field)
@@ -247,3 +247,84 @@ fn julia_iter(z0r: int, z0i: int, cr: int, ci: int, max_iter: int) -> int {
247
247
  }
248
248
  return max_iter;
249
249
  }
250
+
251
+ // ─── Category 5: Geometry experiments ────────────────────────────────────────
252
+
253
+ // Angle between two 2D vectors in degrees (×1 = degrees, unsigned 0..180).
254
+ // Strategy: normalize both vectors to unit (×1000 components), then
255
+ // angle = atan2(|cross|, dot) where cross,dot are from the unit vectors.
256
+ // Unit vector components are ≤ 1000, so cross/dot ≤ 10^6 (no overflow).
257
+ // After dividing by 1000 to re-scale, pass to atan2_fixed.
258
+ //
259
+ // angle_between(1000, 0, 0, 1000) == 90
260
+ // angle_between(1000, 0, 1000, 0) == 0
261
+ // angle_between(1000, 0, -1000, 0) == 180
262
+ fn angle_between(x1: int, y1: int, x2: int, y2: int) -> int {
263
+ let nx1: int = normalize2d_x(x1, y1);
264
+ let ny1: int = normalize2d_y(x1, y1);
265
+ let nx2: int = normalize2d_x(x2, y2);
266
+ let ny2: int = normalize2d_y(x2, y2);
267
+ if (nx1 == 0 && ny1 == 0) { return 0; }
268
+ if (nx2 == 0 && ny2 == 0) { return 0; }
269
+ // dot and cross of unit vectors (components ×1000 → result ×1000000)
270
+ let d: int = dot2d(nx1, ny1, nx2, ny2) / 1000; // cos × 1000
271
+ let c: int = abs(cross2d(nx1, ny1, nx2, ny2)) / 1000; // |sin| × 1000
272
+ return atan2_fixed(c, d);
273
+ }
274
+
275
+ // Clamp a 2D point to lie within a circle of radius r centred at origin.
276
+ // r is in the same units as x, y (raw block coords, NOT fixed-point).
277
+ // Returns the clamped x component.
278
+ // Note: keep x, y < ~2000 to avoid overflow in normalize2d_x (x * 10^6).
279
+ //
280
+ // clamp_circle_x(3, 4, 10) == 3 (point at dist 5, inside r=10)
281
+ // clamp_circle_x(600, 0, 500) == 500
282
+ fn clamp_circle_x(x: int, y: int, r: int) -> int {
283
+ // length2d_fixed returns dist × 1000; compare with r × 1000
284
+ let dist: int = length2d_fixed(x, y);
285
+ if (dist <= r * 1000) { return x; }
286
+ return normalize2d_x(x, y) * r / 1000;
287
+ }
288
+
289
+ // Y component of circle clamp.
290
+ // clamp_circle_y(0, 600, 500) == 500
291
+ fn clamp_circle_y(x: int, y: int, r: int) -> int {
292
+ let dist: int = length2d_fixed(x, y);
293
+ if (dist <= r * 1000) { return y; }
294
+ return normalize2d_y(x, y) * r / 1000;
295
+ }
296
+
297
+ // Newton's method integer square root (alternative to isqrt).
298
+ // Converges quadratically; validates our while loop + division.
299
+ // newton_sqrt(25) == 5, newton_sqrt(100) == 10, newton_sqrt(2) == 1
300
+ fn newton_sqrt(n: int) -> int {
301
+ if (n <= 0) { return 0; }
302
+ if (n == 1) { return 1; }
303
+ let x: int = n / 2;
304
+ let prev: int = 0;
305
+ while (x != prev) {
306
+ prev = x;
307
+ x = (x + n / x) / 2;
308
+ }
309
+ return x;
310
+ }
311
+
312
+ // Digital root: repeatedly sum digits until single digit.
313
+ // digital_root(493) == 7 (4+9+3=16 → 1+6=7)
314
+ // digital_root(9) == 9
315
+ // digital_root(0) == 0
316
+ fn digital_root(n: int) -> int {
317
+ if (n == 0) { return 0; }
318
+ let r: int = n % 9;
319
+ if (r == 0) { return 9; }
320
+ return r;
321
+ }
322
+
323
+ // Ulam spiral ring number: which concentric square ring is n on?
324
+ // Ring 0: n=1; Ring 1: n=2..9 (3×3 square); Ring 2: n=10..25 (5×5); etc.
325
+ // Formula: floor((isqrt(n-1) + 1) / 2)
326
+ // spiral_ring(1) == 0, spiral_ring(9) == 1, spiral_ring(25) == 2
327
+ fn spiral_ring(n: int) -> int {
328
+ if (n <= 1) { return 0; }
329
+ return (isqrt(n - 1) + 1) / 2;
330
+ }