DJLougen commited on
Commit
9b8659b
·
verified ·
1 Parent(s): 4ac4233

Upload Qwen3.5-9B-Math-GraphCalc (43-layer zero-gradient WSP + in-weights RV32 graphing calculator)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/architecture.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/benchmarks.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/example_output.png filter=lfs diff=lfs merge=lfs -text
39
+ assets/hero_banner.png filter=lfs diff=lfs merge=lfs -text
40
+ assets/hf_public_benchmarks.png filter=lfs diff=lfs merge=lfs -text
41
+ assets/in_weights_plots.png filter=lfs diff=lfs merge=lfs -text
42
+ chip_indices.bin.zlib filter=lfs diff=lfs merge=lfs -text
43
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ license_link: https://huggingface.co/Qwen/Qwen3.5-9B-Base/blob/main/LICENSE
4
+ base_model: Qwen/Qwen3.5-9B-Base
5
+ library_name: transformers
6
+ pipeline_tag: text-generation
7
+ language:
8
+ - en
9
+ tags:
10
+ - weight-space-modeling
11
+ - zero-gradient
12
+ - risc-v
13
+ - in-weights-computer
14
+ - gated-deltanet
15
+ - math
16
+ - graphing-calculator
17
+ - pedagogy
18
+ - qwen3_5
19
+ model-index:
20
+ - name: Qwen3.5-9B-Math-GraphCalc
21
+ results:
22
+ - task:
23
+ type: text-generation
24
+ name: Hard Math & Function Graphing (Disjoint Held-Out)
25
+ dataset:
26
+ name: Carbon-GraphCalc-HeldOut-60
27
+ type: custom
28
+ split: test
29
+ metrics:
30
+ - type: accuracy
31
+ value: 96.67
32
+ name: Math Accuracy (%)
33
+ - type: accuracy
34
+ value: 70.00
35
+ name: Verified Pedagogical Teaching (%)
36
+ - type: accuracy
37
+ value: 97.06
38
+ name: Graph Feature Accuracy (%)
39
+ - type: accuracy
40
+ value: 100.00
41
+ name: Function Plotting Rate (%)
42
+ - type: accuracy
43
+ value: 100.00
44
+ name: In-Weights RV32 Execution Lockstep (%)
45
+ - task:
46
+ type: text-generation
47
+ name: Zero-Shot Math Tutoring (Plain + ChatML Held-Out)
48
+ dataset:
49
+ name: Carbon-MathTutor-HeldOut-180
50
+ type: custom
51
+ split: test
52
+ metrics:
53
+ - type: accuracy
54
+ value: 86.67
55
+ name: Overall Math Accuracy (%)
56
+ - type: accuracy
57
+ value: 80.56
58
+ name: Plain Q&A Math Accuracy (%)
59
+ - type: accuracy
60
+ value: 73.06
61
+ name: Verified Pedagogical Teaching (%)
62
+ - task:
63
+ type: text-generation
64
+ name: GSM8K Official Test (Zero-Shot Greedy)
65
+ dataset:
66
+ name: openai/gsm8k
67
+ type: openai/gsm8k
68
+ config: main
69
+ split: test
70
+ metrics:
71
+ - type: accuracy
72
+ value: 78.85
73
+ name: ChatML Zero-Shot Accuracy (%)
74
+ - type: accuracy
75
+ value: 75.78
76
+ name: Mean Accuracy Plain+ChatML (%)
77
+ - type: accuracy
78
+ value: 53.07
79
+ name: Verified Pedagogical Teaching (%)
80
+ - task:
81
+ type: text-generation
82
+ name: MATH-500 Official Competition Benchmark (Zero-Shot Greedy)
83
+ dataset:
84
+ name: HuggingFaceH4/MATH-500
85
+ type: HuggingFaceH4/MATH-500
86
+ split: test
87
+ metrics:
88
+ - type: accuracy
89
+ value: 58.20
90
+ name: ChatML Zero-Shot Accuracy (%)
91
+ - type: accuracy
92
+ value: 51.80
93
+ name: Mean Accuracy Plain+ChatML (%)
94
+ - type: accuracy
95
+ value: 42.40
96
+ name: Verified Pedagogical Teaching (%)
97
+ - task:
98
+ type: text-generation
99
+ name: AIME 2024 Olympiad Benchmark (Zero-Shot Greedy)
100
+ dataset:
101
+ name: HuggingFaceH4/aime_2024
102
+ type: HuggingFaceH4/aime_2024
103
+ split: train
104
+ metrics:
105
+ - type: accuracy
106
+ value: 10.00
107
+ name: ChatML Zero-Shot Accuracy (%)
108
+ ---
109
+
110
+ <p align="center">
111
+ <img src="assets/hero_banner.png" alt="Qwen3.5-9B-Math-GraphCalc — 9.0B Pedagogical Math Tutor + In-Weights RISC-V Graphing Calculator" width="100%">
112
+ </p>
113
+
114
+ <p align="center">
115
+ <a href="https://huggingface.co/Qwen/Qwen3.5-9B-Base"><img src="https://img.shields.io/badge/Base-Qwen3.5--9B--Base-38bdf8?style=flat-square" alt="Base Model"></a>
116
+ <img src="https://img.shields.io/badge/Params-9.0B%20Nonzero%20BF16-10b981?style=flat-square" alt="9.0B Nonzero Params">
117
+ <img src="https://img.shields.io/badge/Gradients-0%20(Pure%20Weight%20Space)-34d399?style=flat-square" alt="Zero Gradients">
118
+ <img src="https://img.shields.io/badge/In--Weights%20CPU-RV32IM%20%2B%20Zba%20%2B%20256b%20Bignum-8b7cf6?style=flat-square" alt="In-Weights CPU">
119
+ </p>
120
+
121
+ **`Qwen3.5-9B-Math-GraphCalc`** (`8,985,670,530` nonzero `bfloat16` parameters = `8.99B`) is an experimental **43-layer** `Qwen3_5ForCausalLM` model built from [`Qwen/Qwen3.5-9B-Base`](https://huggingface.co/Qwen/Qwen3.5-9B-Base) **strictly in weight space**:
122
+
123
+ - **Zero gradients** (`torch.set_grad_enabled(False)`), **zero optimizer steps**, and **zero fitting of weights to recorded activations**.
124
+ - **Zero external Python `eval()`, SymPy, or host calculator servers**: 11 compiled Gated-DeltaNet transformer layers (`layers[0..10]`) sit directly in front of Qwen's 32 widened language layers (`layers[11..42]`, `hidden_size = 5632`, `m = 22`) and execute bare-metal RISC-V instructions (`graphcalc.elf`, `87,636` bytes) using Qwen3.5's own Gated-DeltaNet recurrent state as RAM.
125
+ - When asked a multi-digit calculation, algebraic equation, definite integral, or function-graphing question, the model emits `<tool_call>` at token 0, computes exact 256-bit bignums, reduced fractions, 40-iteration CORDIC trig/log values, roots, turning points, Simpson definite integrals, and coordinate samples **inside its forward pass**, and then teaches the solution step by step.
126
+
127
+ ---
128
+
129
+ ## Example Output — 9B Tutor + In-Weights Graphing Calculator
130
+
131
+ ### Example 1: Function Graphing, Roots & Extrema Walkthrough (`169,027` In-Weights RV32 Steps)
132
+
133
+ > **User Prompt:** `"Graph y = x^3 - 3*x from x = -2 to 2, identify its roots and turning points, and state the local maximum y-value."`
134
+
135
+ 1. **Token 0 — Language layers (`layers[11..42]`) emit `<tool_call>`:**
136
+ ```xml
137
+ <tool_call>
138
+ <function=graphcalc>
139
+ <parameter=expression>
140
+ x^3 - 3*x
141
+ </parameter>
142
+ <parameter=xmin>
143
+ -2
144
+ </parameter>
145
+ <parameter=xmax>
146
+ 2
147
+ </parameter>
148
+ </function>
149
+ </tool_call>
150
+ ```
151
+ 2. **In-Weights RISC-V Execution — `layers[0..10]` run `graphcalc.elf` inside Gated-DeltaNet RAM and emit `<tool_response>`:**
152
+ ```text
153
+ function: f(x) = x^3 - 3*x on [-2, 2]
154
+ roots: x = -1.732051, x = 0, x = 1.732051
155
+ local max: (-1, 2) | local min: (1, -2)
156
+ y-intercept: (0, 0) | integral: 0
157
+ table: (-2, -2), (-1.3333, 1.6296), (-0.6667, 1.7037), (0, 0), (0.6667, -1.7037), (1.3333, -1.6296), (2, 2)
158
+ ```
159
+ <p align="center">
160
+ <img src="assets/example_output.png" alt="Rendered curve, roots, extrema and 7-point sample table computed inside layers[0..10] for f(x) = x^3 - 3x" width="100%">
161
+ </p>
162
+ 3. **Verbatim 9B Step-by-Step Tutor Walkthrough — `layers[11..42]` read `<tool_response>` and teach the solution:**
163
+ > ## 1. Graphing the Function
164
+ >
165
+ > Let's first graph the function y = x³ - 3x from x = -2 to x = 2:
166
+ >
167
+ > The graph shows a cubic function with:
168
+ > - A local maximum at (-1, 2)
169
+ > - A local minimum at (1, -2)
170
+ > - Passing through the origin (0, 0)
171
+ > - Symmetric about the origin (odd function)
172
+ >
173
+ > ## 2. Finding the Roots
174
+ >
175
+ > The roots are the x-values where y = 0. From the graph, we can see the function crosses the x-axis at three points:
176
+ >
177
+ > **Roots:**
178
+ > - x = -1.732051 (approximately -√3)
179
+ > - x = 0
180
+ > - x = 1.732051 (approximately √3)
181
+ >
182
+ > Let's verify these roots algebraically:
183
+ > x³ - 3x = 0
184
+ > x(x² - 3) = 0
185
+ > x = 0 or x² - 3 = 0
186
+ > x = 0 or x² = 3
187
+ > x = 0 or x = ±√3
188
+ >
189
+ > So the exact roots are x = -√3, x = 0, and x = √3.
190
+ >
191
+ > ## 3. Finding the Turning Points
192
+ >
193
+ > Turning points occur where the derivative equals zero. Let's find the derivative:
194
+ > f(x) = x³ - 3x
195
+ > f'(x) = 3x² - 3
196
+ >
197
+ > Setting f'(x) = 0:
198
+ > 3x² - 3 = 0
199
+ > 3x² = 3
200
+ > x² = 1
201
+ > x = ±1
202
+ >
203
+ > Now let's find the y-values at these x-values:
204
+ > - At x = -1: f(-1) = (-1)³ - 3(-1) = -1 + 3 = 2
205
+ > - At x = 1: f(1) = (1)³ - 3(1) = 1 - 3 = -2
206
+ >
207
+ > So the turning points are:
208
+ > - Local maximum at (-1, 2)
209
+ > - Local minimum at (1, -2)
210
+ >
211
+ > ## 4. Local Maximum y-Value
212
+ >
213
+ > The local maximum occurs at x = -1, and the y-value at this point is 2.
214
+ >
215
+ > **Answer:**
216
+ > - Roots: x = -√3, x = 0, x = √3
217
+ > - Turning points: Local maximum at (-1, 2) and local minimum at (1, -2)
218
+ > - Local maximum y-value: 2
219
+ >
220
+ > The function has a local maximum y-value of 2 at x = -1.
221
+
222
+ ### Example 2: Exact 9-Digit Bignum Arithmetic (`15,701` In-Weights RV32 Steps via 256-bit `OP_BN`)
223
+
224
+ > **User Prompt:** `"Calculate 37429 * 8613 + 194820 and explain your steps."`
225
+
226
+ 1. **In-Weights RISC-V `<tool_response>` (`layers[0..10]`):**
227
+ ```text
228
+ expression: 37429 * 8613 + 194820
229
+ step 1: 37429 * 8613 = 322375977
230
+ step 2: 322375977 + 194820 = 322570797
231
+ exact value: 322570797
232
+ ```
233
+ 2. **Verbatim 9B Tutor Explanation (`layers[11..42]`):**
234
+ > ## 37429 * 8613 + 194820
235
+ >
236
+ > ## Step 1: Multiply 37429 by 8613
237
+ >
238
+ > Let's break this multiplication down:
239
+ >
240
+ > 37429 × 8613 = 322,375,977
241
+ >
242
+ > ## Step 2: Add 194,820 to the result
243
+ >
244
+ > 322,375,977 + 194,820 = 322,570,797
245
+ >
246
+ > ## Final Answer: 322,570,797
247
+ >
248
+ > The graphing calculator confirmed this exact value. When working with large numbers, it's helpful to break the problem into smaller steps and verify each part.
249
+
250
+ ---
251
+
252
+ ## Functions Solved, Integrated & Sampled Inside 9B Weights
253
+
254
+ <p align="center">
255
+ <img src="assets/in_weights_plots.png" alt="Functions solved, integrated, and sampled inside 9B transformer weights (layers[0..10])" width="100%">
256
+ </p>
257
+
258
+ Every coordinate, root, extremum, and definite integral above is computed token-by-token inside `layers[0..10]` (`0` mismatches between HF `Qwen3_5DecoderLayer` weights, Triton `Executor2`, `BR.Session`, and numpy circuit IR):
259
+
260
+ | Prompted Function & Domain | In-Weights RV32 Steps | Exact Roots Found | Extrema / Vertex | 48-Panel Simpson `∫ f(x) dx` |
261
+ |---|---:|---|---|---:|
262
+ | `f(x) = x³ − 3x` on `[−2, 2]` | `169,027` | `x = −1.732051, 0, 1.732051` | `max(−1, 2)`, `min(1, −2)` | `0` |
263
+ | `f(x) = 2 sin(x)` on `[0, 6.28]` | `313,365` | `x = 0, 3.141593` | `max(1.570797, 2)`, `min(4.712388, −2)` | `0.00001` |
264
+ | `f(x) = x² − 6x + 5` on `[−1, 7]` | `78,963` | `x = 1, 5` | `vertex (3, −4)` | `10.666667` |
265
+
266
+ ---
267
+
268
+ ## Benchmarks
269
+
270
+ <p align="center">
271
+ <img src="assets/benchmarks.png" alt="9B Held-Out Hard-Math & Function Graphing Benchmark (N = 60 Disjoint Problems)" width="100%">
272
+ </p>
273
+
274
+ ### 1. Disjoint Hard-Math & Function Graphing (`N = 60` Unseen Problems, Greedy Decoding)
275
+
276
+ Across the 60 unseen hard-math and function-graphing problems, `Qwen3.5-9B-Math-GraphCalc` executes **`3,195,505` in-weights RV32 instruction token steps** with **`0` lockstep errors**:
277
+
278
+ | Model Arm | WSP Base Edits | Compiled RV32 NNZ | Math Acc | Verified Teaching | Graph Feature Acc | Plot Rate | In-Weights Exec OK |
279
+ |---|---:|---:|---:|---:|---:|---:|---:|
280
+ | `Qwen/Qwen3.5-9B-Base` (`32L`) | `0` | `0` | `73.33%` | `63.33%` | `43.14%` | `0.00%` | `0.00%` |
281
+ | `Qwen3.5-9B-Math-Tutor` (`32L WSP`) | `2,916,073` | `0` | `81.67%` | `75.00%` | `53.92%` | `0.00%` | `0.00%` |
282
+ | **`Qwen3.5-9B-Math-GraphCalc` (`43L`)** | **`2,920,169`** | **`1,045,984`** | **`96.67%`** | **`70.00%`** | **`97.06%`** | **`100.00%`** | **`100.00%`** |
283
+
284
+ ### 2. Official Public HuggingFace Math Benchmarks (`7,546` Total Zero-Shot Greedy Evaluations)
285
+
286
+ <p align="center">
287
+ <img src="assets/hf_public_benchmarks.png" alt="Official Public HuggingFace Math Benchmarks — GSM8K, MATH-500, AIME 2024, and MATH-500 Computational-50" width="100%">
288
+ </p>
289
+
290
+ Evaluated zero-shot with greedy decoding directly on official HuggingFace Hub datasets ([`openai/gsm8k`](https://huggingface.co/datasets/openai/gsm8k) full `1,319` test set, [`HuggingFaceH4/MATH-500`](https://huggingface.co/datasets/HuggingFaceH4/MATH-500) full `500` competition set, and [`HuggingFaceH4/aime_2024`](https://huggingface.co/datasets/HuggingFaceH4/aime_2024) full `30` Olympiad set across both raw `Plain Q&A` and `ChatML` formats):
291
+
292
+ | Public HF Dataset | Arm | Mean Math Acc | Plain Q&A Acc | ChatML Acc | Plain Verified Teach | ChatML Verified Teach |
293
+ |---|---|---:|---:|---:|---:|---:|
294
+ | **`openai/gsm8k`** (`N = 1,319 × 2`) | `Qwen3.5-9B-Base` | `78.54%` | `82.79%` | `74.30%` | `51.40%` | `5.31%` |
295
+ | **`openai/gsm8k`** (`N = 1,319 × 2`) | **`9B WSP Tutor / GraphCalc`** | **`75.78%`** | **`72.71%`** | **`78.85%`** | **`59.89%`** | **`53.07%`** |
296
+ | **`HuggingFaceH4/MATH-500`** (`N = 500 × 2`) | `Qwen3.5-9B-Base` | `48.40%` | `40.60%` | `56.20%` | `17.60%` | `2.80%` |
297
+ | **`HuggingFaceH4/MATH-500`** (`N = 500 × 2`) | **`9B WSP Tutor / GraphCalc`** | **`51.80%`** | **`45.40%`** | **`58.20%`** | **`28.00%`** | **`42.40%`** |
298
+ | **`HuggingFaceH4/aime_2024`** (`N = 30 × 2`) | `Qwen3.5-9B-Base` | `6.67%` | `3.33%` | `10.00%` | `0.00%` | `0.00%` |
299
+ | **`HuggingFaceH4/aime_2024`** (`N = 30 × 2`) | **`9B WSP Tutor / GraphCalc`** | **`8.34%`** | **`6.67%`** | **`10.00%`** | **`6.67%`** | **`0.00%`** |
300
+
301
+ #### `MATH-500` Accuracy by Competition Difficulty Level (Zero-Shot ChatML)
302
+
303
+ | Competition Level | `Qwen/Qwen3.5-9B-Base` | **`9B WSP Tutor / GraphCalc`** |
304
+ |---|---:|---:|
305
+ | Level 1 | `88.37%` | **`88.37%`** |
306
+ | Level 2 | `76.67%` | **`77.78%`** |
307
+ | Level 3 | `69.52%` | **`72.38%`** |
308
+ | Level 4 | `48.44%` | **`52.34%`** |
309
+ | Level 5 | `29.10%` | **`29.85%`** |
310
+
311
+ > **Domain Scope (`<tools>` vs. Standard Zero-Shot):** `graphcalc.elf` implements a specialized single-variable graphing & 256-bit arithmetic/CORDIC calculator (`+ - * / ^`, `sin`, `cos`, `tan`, `sqrt`, `ln`, `exp`, roots, extrema, Simpson integrals). Pass `<tools>` for multi-digit arithmetic, exact fractions, calculus, and function graphing (**`96.67%`** accuracy on `GraphCalc-HeldOut-60`). For general symbolic competition math, modular number theory, or multi-variable systems (where forcing a single-variable `<tool_call>` on a 50-item `MATH-500` scalar slice scored `58.00%` vs `72.00%` without `<tools>` across `2,927,967` RV32 steps), omit `<tools>` — the untied `E6` gate triggers **`0 / 360`** false tool calls and runs at full 32-layer speed.
312
+
313
+ ### 3. Disjoint Zero-Shot Math Tutoring & Language Retention (`N = 180 × 2 = 360` Generations, No `<tools>`)
314
+
315
+ When prompted without `<tools>`, untied `lm_head` `<tool_call>` calibration (`E6 = 1.3×`) triggers **`0 / 360` false tool calls** while improving zero-shot `plain` Q&A tutoring and preserving `FineWeb-Edu` causal language modeling (`+0.0001` nats):
316
+
317
+ | Model Arm | Overall Math Acc | Plain Q&A Math Acc | ChatML Math Acc | Verified Teaching | `FineWeb-Edu` NLL | False `<tool_call>` |
318
+ |---|---:|---:|---:|---:|---:|---:|
319
+ | `Qwen/Qwen3.5-9B-Base` | `81.67%` | `68.89%` | `94.44%` | `12.78%` | `2.1093` | `0 / 360` |
320
+ | `Qwen3.5-9B-Math-Tutor` (`A3`) | `86.67%` | `80.56%` | `92.78%` | `73.06%` | `2.1094` | `0 / 360` |
321
+ | **`Qwen3.5-9B-Math-GraphCalc` (no `<tools>`)** | **`86.67%`** | **`80.56%`** | **`92.78%`** | **`73.06%`** | **`2.1094`** | **`0 / 360`** |
322
+
323
+ ---
324
+
325
+ ## Architecture — How the 43-Layer Model Works
326
+
327
+ <p align="center">
328
+ <img src="assets/architecture.png" alt="43-Layer Qwen3.5-9B-Math-GraphCalc In-Weights Execution Loop" width="100%">
329
+ </p>
330
+
331
+ 1. **Stage 1 — Untied 9B Weight-Space Tutor Surgery (`2,920,169` base scalar edits):**
332
+ - **`E1` (Untied Turn-Header Bridge):** Re-anchors `Question` and `Answer` in `embed_tokens.weight` into Qwen3.5-9B's native ChatML turn subspace with zero output-logit side effects, while damping raw `</think>` / `<|file_sep|>` markup leak in untied `lm_head.weight`.
333
+ - **`E2` & `E3` (Static MLP Subspace Surgery):** Closed-form SVD/projection inspection of `layers[18..31]` damps worksheet continuation, multi-newline collapse, bare MCQ guess, and orthogonalized CJK exam leakage while amplifying clean English step-initiation and deductive-reasoning columns.
334
+ - **`E6` (Untied `<tool_call>` Gate):** Scales `lm_head.weight[<tool_call>]` (`4,096` scalars) by `1.3×` so `<tool_call>` wins rank 1 when `<tools>` is present and stays silent (`0/360`) without `<tools>`.
335
+ 2. **Stage 2 — 11-Layer Compiled RISC-V Co-Processor (`1,045,984` nonzero compiled scalars):**
336
+ - Compiles RV32IM + Zba + 256-bit hardware bignum (`OP_BN`) + `CarbonText` bridge (`Target(16, 32, 12288)`) via [`qwenputer`](https://github.com/divagr18/qwenputer) into `layers[0..10]` (`1,265` residual slots, `hidden_size = 5632`, `m = 22`).
337
+ - Because `Qwen3.5-9B-Base` already has `linear_num_value_heads = 32` and `intermediate_size = 12288`, widening `layers[11..42]` requires **zero** MLP or Gated-DeltaNet head expansion — only orthogonal hidden-dimension padding (`4096 -> 5632`) plus analytical ballast muting (`sinks = None`) on `mu_dim`.
338
+ - Untied `lm_head.weight` copies `(*code_dims, tau_dim)` (`15` chip columns) alongside the `4,096` Qwen columns, preserving 100% bit-exact logits across all `478,656` tokens (`head_mismatch = 0`).
339
+ 3. **Zero-Padding-Free Compact `model.safetensors` (`8.99B` Nonzero BF16):**
340
+ - Stores only the `8,985,670,530` nonzero `bfloat16` parameters in `model.safetensors` plus lossless `chip_layout.json` + `chip_indices.bin.zlib`, cutting checkpoint size in half (`17.97 GB` instead of `36.42 GB` zero-padded shards) while reconstructing all 43 `Qwen3_5DecoderLayer` modules bit-for-bit (`torch.equal`).
341
+
342
+ ---
343
+
344
+ ## Quickstart
345
+
346
+ ```bash
347
+ pip install torch transformers safetensors huggingface_hub tokenizers
348
+ git clone https://github.com/divagr18/qwenputer
349
+ git clone https://huggingface.co/DJLougen/Qwen3.5-9B-Math-GraphCalc
350
+
351
+ # Run end-to-end 43-layer in-weights generation (Triton fast path or --pure-hf un-fused HF layers)
352
+ QWENPUTER_ROOT=./qwenputer python Qwen3.5-9B-Math-GraphCalc/runner.py \
353
+ --ask "Graph y = x^3 - 3*x from x = -2 to 2, identify its roots and turning points, and state the local maximum y-value."
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Credits & License
359
+
360
+ - **Base Model:** [`Qwen/Qwen3.5-9B-Base`](https://huggingface.co/Qwen/Qwen3.5-9B-Base) (Apache 2.0)
361
+ - **RV32 Circuit Compiler Foundation:** Built upon the Gated-DeltaNet RISC-V compiler from [`divagr18/qwenputer`](https://github.com/divagr18/qwenputer), extended with untied 9B `lm_head` synthesis, zero-gradient analytical ballast muting, ChatML bridge tokens (`256..264`), and custom C graphing-calculator firmware (`graphcalc.c`).
assets/architecture.png ADDED

Git LFS Details

  • SHA256: eee81ab5883ad284fcc714cc331c2dbbe0f1ab77b2d4cf9ffa5bc0e1fc03517a
  • Pointer size: 131 Bytes
  • Size of remote file: 221 kB
assets/benchmarks.png ADDED

Git LFS Details

  • SHA256: 30b5c640bb5588b98cac0dbe02f8fd00026280952501ee81d1076fc233d3ce9a
  • Pointer size: 131 Bytes
  • Size of remote file: 255 kB
assets/example_output.png ADDED

Git LFS Details

  • SHA256: 0a329d00867a2064d1fa2e1bfd91ff3dfa5374d8dba4ad0c1893d678086469ed
  • Pointer size: 131 Bytes
  • Size of remote file: 187 kB
assets/hero_banner.png ADDED

Git LFS Details

  • SHA256: d380c68eff2502ab4e7cc29698a5b55c894657cbef90806c0596fad2353bdc15
  • Pointer size: 131 Bytes
  • Size of remote file: 156 kB
assets/hf_public_benchmarks.png ADDED

Git LFS Details

  • SHA256: cb343c0c06152b8484473c1ae736a5c07e75516b94bbc6151a0409a7a85ce80d
  • Pointer size: 131 Bytes
  • Size of remote file: 259 kB
assets/in_weights_plots.png ADDED

Git LFS Details

  • SHA256: 92e19d84305d3945d6275ee9ba07c2f9269712343f2bbb5614a811b2d0a14152
  • Pointer size: 131 Bytes
  • Size of remote file: 324 kB
chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- elif content is iterable and content is not mapping %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if is_system_content %}
10
+ {{- raise_exception('System message cannot contain images.') }}
11
+ {%- endif %}
12
+ {%- if do_vision_count %}
13
+ {%- set image_count.value = image_count.value + 1 %}
14
+ {%- endif %}
15
+ {%- if add_vision_id %}
16
+ {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
+ {%- endif %}
18
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
+ {%- elif 'video' in item or item.type == 'video' %}
20
+ {%- if is_system_content %}
21
+ {{- raise_exception('System message cannot contain videos.') }}
22
+ {%- endif %}
23
+ {%- if do_vision_count %}
24
+ {%- set video_count.value = video_count.value + 1 %}
25
+ {%- endif %}
26
+ {%- if add_vision_id %}
27
+ {{- 'Video ' ~ video_count.value ~ ': ' }}
28
+ {%- endif %}
29
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
+ {%- elif 'text' in item %}
31
+ {{- item.text }}
32
+ {%- else %}
33
+ {{- raise_exception('Unexpected item type in content.') }}
34
+ {%- endif %}
35
+ {%- endfor %}
36
+ {%- elif content is none or content is undefined %}
37
+ {{- '' }}
38
+ {%- else %}
39
+ {{- raise_exception('Unexpected content type.') }}
40
+ {%- endif %}
41
+ {%- endmacro %}
42
+ {%- if not messages %}
43
+ {{- raise_exception('No messages provided.') }}
44
+ {%- endif %}
45
+ {%- if tools and tools is iterable and tools is not mapping %}
46
+ {{- '<|im_start|>system\n' }}
47
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
+ {%- for tool in tools %}
49
+ {{- "\n" }}
50
+ {{- tool | tojson }}
51
+ {%- endfor %}
52
+ {{- "\n</tools>" }}
53
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
+ {%- if messages[0].role == 'system' %}
55
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
56
+ {%- if content %}
57
+ {{- '\n\n' + content }}
58
+ {%- endif %}
59
+ {%- endif %}
60
+ {{- '<|im_end|>\n' }}
61
+ {%- else %}
62
+ {%- if messages[0].role == 'system' %}
63
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
64
+ {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
+ {%- for message in messages[::-1] %}
69
+ {%- set index = (messages|length - 1) - loop.index0 %}
70
+ {%- if ns.multi_step_tool and message.role == "user" %}
71
+ {%- set content = render_content(message.content, false)|trim %}
72
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
+ {%- set ns.multi_step_tool = false %}
74
+ {%- set ns.last_query_index = index %}
75
+ {%- endif %}
76
+ {%- endif %}
77
+ {%- endfor %}
78
+ {%- if ns.multi_step_tool %}
79
+ {{- raise_exception('No user query found in messages.') }}
80
+ {%- endif %}
81
+ {%- for message in messages %}
82
+ {%- set content = render_content(message.content, true)|trim %}
83
+ {%- if message.role == "system" %}
84
+ {%- if not loop.first %}
85
+ {{- raise_exception('System message must be at the beginning.') }}
86
+ {%- endif %}
87
+ {%- elif message.role == "user" %}
88
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
+ {%- elif message.role == "assistant" %}
90
+ {%- set reasoning_content = '' %}
91
+ {%- if message.reasoning_content is string %}
92
+ {%- set reasoning_content = message.reasoning_content %}
93
+ {%- else %}
94
+ {%- if '</think>' in content %}
95
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- set reasoning_content = reasoning_content|trim %}
100
+ {%- if loop.index0 > ns.last_query_index %}
101
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
+ {%- else %}
103
+ {{- '<|im_start|>' + message.role + '\n' + content }}
104
+ {%- endif %}
105
+ {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
+ {%- for tool_call in message.tool_calls %}
107
+ {%- if tool_call.function is defined %}
108
+ {%- set tool_call = tool_call.function %}
109
+ {%- endif %}
110
+ {%- if loop.first %}
111
+ {%- if content|trim %}
112
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
+ {%- else %}
114
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
+ {%- endif %}
116
+ {%- else %}
117
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
+ {%- endif %}
119
+ {%- if tool_call.arguments is defined %}
120
+ {%- for args_name, args_value in tool_call.arguments|items %}
121
+ {{- '<parameter=' + args_name + '>\n' }}
122
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
+ {{- args_value }}
124
+ {{- '\n</parameter>\n' }}
125
+ {%- endfor %}
126
+ {%- endif %}
127
+ {{- '</function>\n</tool_call>' }}
128
+ {%- endfor %}
129
+ {%- endif %}
130
+ {{- '<|im_end|>\n' }}
131
+ {%- elif message.role == "tool" %}
132
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
133
+ {{- '<|im_start|>user' }}
134
+ {%- endif %}
135
+ {{- '\n<tool_response>\n' }}
136
+ {{- content }}
137
+ {{- '\n</tool_response>' }}
138
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
139
+ {{- '<|im_end|>\n' }}
140
+ {%- elif loop.last %}
141
+ {{- '<|im_end|>\n' }}
142
+ {%- endif %}
143
+ {%- else %}
144
+ {{- raise_exception('Unexpected message role.') }}
145
+ {%- endif %}
146
+ {%- endfor %}
147
+ {%- if add_generation_prompt %}
148
+ {{- '<|im_start|>assistant\n' }}
149
+ {%- if enable_thinking is defined and enable_thinking is false %}
150
+ {{- '<think>\n\n</think>\n\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
chip_indices.bin.zlib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13f715f1f3285e34b1460f56517efe1cf9025a94f1d701c4805772641bdc22d8
3
+ size 45910417
chip_layout.json ADDED
The diff for this file is too large to render. See raw diff
 
chipformer.json ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "layout": "rv1-carbon-graphcalc",
3
+ "base_model": "Qwen/Qwen3.5-9B-Base",
4
+ "tie_word_embeddings": false,
5
+ "total_nonzero_params": 8985670530,
6
+ "total_dense_params": 18208957504,
7
+ "wsp_patch": "patch_9b_graphcalc.safetensors",
8
+ "wsp_changed_base_weights": 2920169,
9
+ "compiled_chip_layer_nonzero_weights": 1045984,
10
+ "compiled_embedding_chip_nonzero_weights": 25387170,
11
+ "compiled_lm_head_chip_nonzero_weights": 5321013,
12
+ "firmware": "graphcalc.elf",
13
+ "firmware_elf_bytes": 87636,
14
+ "chip_token_offset": 248320,
15
+ "chip_vocab": 230324,
16
+ "chip_layers": 11,
17
+ "qwen_layers_from": 11,
18
+ "num_total_layers": 43,
19
+ "hidden": 5632,
20
+ "chip_dims_offset": 4096,
21
+ "ballast_dims": 22,
22
+ "circuit": {
23
+ "layers": 11,
24
+ "dims": 2918,
25
+ "heads": 103,
26
+ "ffn_units": 42113,
27
+ "max_units": 8149,
28
+ "max_heads": 16
29
+ },
30
+ "slots": 1265,
31
+ "muted": true,
32
+ "anti_sink": "none (analytical ballast mute; zero fitted activations)",
33
+ "code_dims": [
34
+ 4250,
35
+ 4251,
36
+ 4252,
37
+ 4253,
38
+ 4254,
39
+ 4255,
40
+ 4256,
41
+ 4257,
42
+ 4258,
43
+ 4259,
44
+ 4260,
45
+ 4261,
46
+ 4262,
47
+ 4263,
48
+ 4264,
49
+ 4265,
50
+ 4266,
51
+ 4267,
52
+ 4268,
53
+ 4269,
54
+ 4270
55
+ ],
56
+ "tau_dim": 4273,
57
+ "mu_dim": 4272,
58
+ "nfam": 5,
59
+ "npay": 16,
60
+ "tool_call": [
61
+ 248058,
62
+ 248059
63
+ ],
64
+ "specials": [
65
+ "<tool_response>",
66
+ "</tool_response>",
67
+ "<think>",
68
+ "</think>",
69
+ "<|im_end|>",
70
+ "<|im_start|>",
71
+ "user",
72
+ "assistant",
73
+ "\n\n"
74
+ ],
75
+ "halt_nl_byte": 32,
76
+ "printable": [
77
+ 188,
78
+ 189,
79
+ 190,
80
+ 191,
81
+ 192,
82
+ 193,
83
+ 194,
84
+ 195,
85
+ 196,
86
+ 197,
87
+ 198,
88
+ 199,
89
+ 200,
90
+ 201,
91
+ 202,
92
+ 203,
93
+ 204,
94
+ 205,
95
+ 206,
96
+ 207,
97
+ 208,
98
+ 209,
99
+ 210,
100
+ 211,
101
+ 212,
102
+ 213,
103
+ 214,
104
+ 215,
105
+ 216,
106
+ 217,
107
+ 218,
108
+ 219,
109
+ 220,
110
+ 0,
111
+ 1,
112
+ 2,
113
+ 3,
114
+ 4,
115
+ 5,
116
+ 6,
117
+ 7,
118
+ 8,
119
+ 9,
120
+ 10,
121
+ 11,
122
+ 12,
123
+ 13,
124
+ 14,
125
+ 15,
126
+ 16,
127
+ 17,
128
+ 18,
129
+ 19,
130
+ 20,
131
+ 21,
132
+ 22,
133
+ 23,
134
+ 24,
135
+ 25,
136
+ 26,
137
+ 27,
138
+ 28,
139
+ 29,
140
+ 30,
141
+ 31,
142
+ 32,
143
+ 33,
144
+ 34,
145
+ 35,
146
+ 36,
147
+ 37,
148
+ 38,
149
+ 39,
150
+ 40,
151
+ 41,
152
+ 42,
153
+ 43,
154
+ 44,
155
+ 45,
156
+ 46,
157
+ 47,
158
+ 48,
159
+ 49,
160
+ 50,
161
+ 51,
162
+ 52,
163
+ 53,
164
+ 54,
165
+ 55,
166
+ 56,
167
+ 57,
168
+ 58,
169
+ 59,
170
+ 60,
171
+ 61,
172
+ 62,
173
+ 63,
174
+ 64,
175
+ 65,
176
+ 66,
177
+ 67,
178
+ 68,
179
+ 69,
180
+ 70,
181
+ 71,
182
+ 72,
183
+ 73,
184
+ 74,
185
+ 75,
186
+ 76,
187
+ 77,
188
+ 78,
189
+ 79,
190
+ 80,
191
+ 81,
192
+ 82,
193
+ 83,
194
+ 84,
195
+ 85,
196
+ 86,
197
+ 87,
198
+ 88,
199
+ 89,
200
+ 90,
201
+ 91,
202
+ 92,
203
+ 93,
204
+ 221,
205
+ 222,
206
+ 223,
207
+ 224,
208
+ 225,
209
+ 226,
210
+ 227,
211
+ 228,
212
+ 229,
213
+ 230,
214
+ 231,
215
+ 232,
216
+ 233,
217
+ 234,
218
+ 235,
219
+ 236,
220
+ 237,
221
+ 238,
222
+ 239,
223
+ 240,
224
+ 241,
225
+ 242,
226
+ 243,
227
+ 244,
228
+ 245,
229
+ 246,
230
+ 247,
231
+ 248,
232
+ 249,
233
+ 250,
234
+ 251,
235
+ 252,
236
+ 253,
237
+ 254,
238
+ 94,
239
+ 95,
240
+ 96,
241
+ 97,
242
+ 98,
243
+ 99,
244
+ 100,
245
+ 101,
246
+ 102,
247
+ 103,
248
+ 104,
249
+ 105,
250
+ 255,
251
+ 106,
252
+ 107,
253
+ 108,
254
+ 109,
255
+ 110,
256
+ 111,
257
+ 112,
258
+ 113,
259
+ 114,
260
+ 115,
261
+ 116,
262
+ 117,
263
+ 118,
264
+ 119,
265
+ 120,
266
+ 121,
267
+ 122,
268
+ 123,
269
+ 124,
270
+ 125,
271
+ 126,
272
+ 127,
273
+ 128,
274
+ 129,
275
+ 130,
276
+ 131,
277
+ 132,
278
+ 133,
279
+ 134,
280
+ 135,
281
+ 136,
282
+ 137,
283
+ 138,
284
+ 139,
285
+ 140,
286
+ 141,
287
+ 142,
288
+ 143,
289
+ 144,
290
+ 145,
291
+ 146,
292
+ 147,
293
+ 148,
294
+ 149,
295
+ 150,
296
+ 151,
297
+ 152,
298
+ 153,
299
+ 154,
300
+ 155,
301
+ 156,
302
+ 157,
303
+ 158,
304
+ 159,
305
+ 160,
306
+ 161,
307
+ 162,
308
+ 163,
309
+ 164,
310
+ 165,
311
+ 166,
312
+ 167,
313
+ 168,
314
+ 169,
315
+ 170,
316
+ 171,
317
+ 172,
318
+ 173,
319
+ 174,
320
+ 175,
321
+ 176,
322
+ 177,
323
+ 178,
324
+ 179,
325
+ 180,
326
+ 181,
327
+ 182,
328
+ 183,
329
+ 184,
330
+ 185,
331
+ 186,
332
+ 187,
333
+ 248066,
334
+ 248067,
335
+ 248068,
336
+ 248069,
337
+ 248046,
338
+ 248045,
339
+ 846,
340
+ 74455,
341
+ 271
342
+ ],
343
+ "think_logit_damped": true
344
+ }
config.json ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3_5ForCausalLM"
4
+ ],
5
+ "output_hidden_states": false,
6
+ "return_dict": true,
7
+ "dtype": "bfloat16",
8
+ "chunk_size_feed_forward": 0,
9
+ "is_encoder_decoder": false,
10
+ "id2label": {
11
+ "0": "LABEL_0",
12
+ "1": "LABEL_1"
13
+ },
14
+ "label2id": {
15
+ "LABEL_0": 0,
16
+ "LABEL_1": 1
17
+ },
18
+ "problem_type": null,
19
+ "vocab_size": 478656,
20
+ "hidden_size": 5632,
21
+ "intermediate_size": 12288,
22
+ "num_hidden_layers": 43,
23
+ "num_attention_heads": 16,
24
+ "num_key_value_heads": 4,
25
+ "hidden_act": "silu",
26
+ "max_position_embeddings": 262144,
27
+ "initializer_range": 0.02,
28
+ "rms_norm_eps": 1e-06,
29
+ "use_cache": true,
30
+ "tie_word_embeddings": false,
31
+ "rope_parameters": {
32
+ "mrope_interleaved": true,
33
+ "mrope_section": [
34
+ 11,
35
+ 11,
36
+ 10
37
+ ],
38
+ "rope_type": "default",
39
+ "rope_theta": 10000000,
40
+ "partial_rotary_factor": 0.25
41
+ },
42
+ "attention_bias": false,
43
+ "attention_dropout": 0.0,
44
+ "head_dim": 256,
45
+ "linear_conv_kernel_dim": 4,
46
+ "linear_key_head_dim": 128,
47
+ "linear_value_head_dim": 128,
48
+ "linear_num_key_heads": 16,
49
+ "linear_num_value_heads": 32,
50
+ "layer_types": [
51
+ "linear_attention",
52
+ "linear_attention",
53
+ "linear_attention",
54
+ "linear_attention",
55
+ "linear_attention",
56
+ "linear_attention",
57
+ "linear_attention",
58
+ "linear_attention",
59
+ "linear_attention",
60
+ "linear_attention",
61
+ "linear_attention",
62
+ "linear_attention",
63
+ "linear_attention",
64
+ "linear_attention",
65
+ "full_attention",
66
+ "linear_attention",
67
+ "linear_attention",
68
+ "linear_attention",
69
+ "full_attention",
70
+ "linear_attention",
71
+ "linear_attention",
72
+ "linear_attention",
73
+ "full_attention",
74
+ "linear_attention",
75
+ "linear_attention",
76
+ "linear_attention",
77
+ "full_attention",
78
+ "linear_attention",
79
+ "linear_attention",
80
+ "linear_attention",
81
+ "full_attention",
82
+ "linear_attention",
83
+ "linear_attention",
84
+ "linear_attention",
85
+ "full_attention",
86
+ "linear_attention",
87
+ "linear_attention",
88
+ "linear_attention",
89
+ "full_attention",
90
+ "linear_attention",
91
+ "linear_attention",
92
+ "linear_attention",
93
+ "full_attention"
94
+ ],
95
+ "pad_token_id": null,
96
+ "bos_token_id": null,
97
+ "eos_token_id": 248044,
98
+ "attn_output_gate": true,
99
+ "mlp_only_layers": [],
100
+ "model_type": "qwen3_5_text",
101
+ "mtp_num_hidden_layers": 0,
102
+ "mtp_use_dedicated_embeddings": false,
103
+ "mamba_ssm_dtype": "float32",
104
+ "output_attentions": false
105
+ }
firmware/build.sh ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Build the Carbon Graphing Calculator RV32IM+Zba+Bignum firmware (run in WSL):
3
+ # bash carbon/firmware/build.sh [OUT_DIR]
4
+ # Produces:
5
+ # OUT_DIR/graphcalc.elf (for compiled Qwen3.5 RV32 chip layers & chipformer.rv.ref.Machine)
6
+ # OUT_DIR/graphcalc-qemu.elf (for standalone qemu-riscv32 verification via stdin)
7
+ set -euo pipefail
8
+ HERE=$(cd "$(dirname "$0")" && pwd)
9
+ OUT=$(realpath "${1:-$HERE}")
10
+ QWENPUTER=${QWENPUTER:-$HERE/../../../qwenputer}
11
+ ENV=$QWENPUTER/experimental/riscv/chipformer/rv/env
12
+ CALC_ENV=$ENV/calc
13
+ GCC=riscv64-unknown-elf-gcc
14
+ MARCH=rv32im_zba
15
+ PICO=/usr/lib/picolibc/riscv64-unknown-elf
16
+ LIBC=$PICO/lib/release/rv32im/ilp32/libc.a
17
+ [ -f "$LIBC" ] || LIBC=$(find /usr/lib/picolibc -path '*rv32im/ilp32/libc.a' | head -n 1)
18
+ LIBGCC=$($GCC -march=rv32im -mabi=ilp32 -print-libgcc-file-name)
19
+
20
+ B=$(mktemp -d)
21
+ python3 "$HERE/tables.py" "$B/graphcalc_tables.h" "$B/sled.S"
22
+ mkdir -p "$OUT"
23
+
24
+ for map in rom qemu; do
25
+ name=$([ $map = rom ] && echo graphcalc || echo graphcalc-qemu)
26
+ def=$([ $map = rom ] && echo "" || echo "-DQEMU")
27
+ sled=$([ $map = rom ] && echo "$B/sled.S" || echo "")
28
+ $GCC -march=$MARCH -mabi=ilp32 ${CFLAGS:--O2} -fno-tree-loop-distribute-patterns -Wall -Wextra $def \
29
+ -isystem "$PICO/include" -I"$ENV" -I"$CALC_ENV" -I"$B" \
30
+ -nostdlib -nostartfiles -static -T"$ENV/$map.ld" -o "$OUT/$name.elf" \
31
+ "$ENV/crt0.S" "$HERE/graphcalc.c" "$ENV/sys.c" $sled \
32
+ -Wl,--start-group "$LIBC" "$LIBGCC" -Wl,--end-group
33
+ done
34
+
35
+ riscv64-unknown-elf-size -A "$OUT/graphcalc.elf" | grep -E '^\.(text|rodata|data|bss)'
36
+ rm -rf "$B"
firmware/graphcalc.c ADDED
@@ -0,0 +1,2210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Carbon In-Weights Graphing & Symbolic/Numeric Calculator Firmware (RV32IM + Zba + Bignum).
2
+ // Executes directly inside compiled Qwen3.5 Gated-DeltaNet + SwiGLU layers (layers[0..10]).
3
+ // Strictly integer & fixed-point (Q32.32 + 128-digit hardware bignum register file) — zero FPU.
4
+
5
+ #include <stdint.h>
6
+ #include <stddef.h>
7
+ #include "rt.h"
8
+ #include "graphcalc_tables.h"
9
+
10
+ #ifdef QEMU
11
+ // Software emulation of chipformer's custom-0/custom-1 128-digit bignum unit for qemu-riscv32.
12
+ #define BN_OPS(n, off, m, d, s, ovw) \
13
+ ((uint32_t)(d) | (uint32_t)(s) << 3 | (uint32_t)(ovw) << 6 | (uint32_t)(n) << 7 | \
14
+ (uint32_t)(off) << 13 | (uint32_t)(m) << 20)
15
+ #define BN_OFF(off) ((uint32_t)(off) << 13)
16
+ #define BN_M(m) ((uint32_t)(m) << 20)
17
+
18
+ static uint8_t sw_bn[8][128];
19
+
20
+ static uint32_t bn_mac(uint32_t ops) {
21
+ uint32_t dd = ops & 7, ds = (ops >> 3) & 7, ovw = (ops >> 6) & 1;
22
+ uint32_t n = (ops >> 7) & 63, off = (ops >> 13) & 127, m = (ops >> 20) & 15;
23
+ if (m > 9) m = 9;
24
+ uint32_t c = 0;
25
+ for (uint32_t i = 0; i < n; i++) {
26
+ uint32_t r = i + off;
27
+ uint32_t d = r < 128 ? sw_bn[dd][r] : 0;
28
+ uint32_t v = (ovw ? 0 : d) + sw_bn[ds][i] * m + c;
29
+ c = v / 10;
30
+ if (r < 128) sw_bn[dd][r] = (uint8_t)(v % 10);
31
+ }
32
+ return c;
33
+ }
34
+
35
+ static uint32_t bn_msb(uint32_t ops) {
36
+ uint32_t dd = ops & 7, ds = (ops >> 3) & 7;
37
+ uint32_t n = (ops >> 7) & 63, off = (ops >> 13) & 127, m = (ops >> 20) & 15;
38
+ if (m > 9) m = 9;
39
+ int32_t c = 0;
40
+ for (uint32_t i = 0; i < n; i++) {
41
+ uint32_t r = i + off;
42
+ int32_t d = r < 128 ? (int32_t)sw_bn[dd][r] : 0;
43
+ int32_t v = d - (int32_t)(sw_bn[ds][i] * m) - c;
44
+ int32_t rem = ((v % 10) + 10) % 10;
45
+ c = (rem - v) / 10;
46
+ if (r < 128) sw_bn[dd][r] = (uint8_t)rem;
47
+ }
48
+ return (uint32_t)c;
49
+ }
50
+
51
+ static void bn_ld(const char *end, uint32_t ops) {
52
+ uint32_t dd = ops & 7, n = (ops >> 7) & 63, off = (ops >> 13) & 127;
53
+ for (uint32_t i = 0; i < n; i++) {
54
+ uint32_t r = i + off;
55
+ uint32_t v = (uint32_t)((unsigned char)end[-1 - (int)i]) & 15u;
56
+ if (v > 9) v = 9;
57
+ if (r < 128) sw_bn[dd][r] = (uint8_t)v;
58
+ }
59
+ }
60
+
61
+ static uint32_t bn_get(uint32_t ops) {
62
+ uint32_t dd = ops & 7, off = (ops >> 13) & 127;
63
+ return sw_bn[dd][off];
64
+ }
65
+
66
+ static uint32_t bn_scan(const char *s) {
67
+ uint32_t n = 0;
68
+ while (n < 63 && (unsigned char)s[n] >= '0' && (unsigned char)s[n] <= '9') n++;
69
+ return n;
70
+ }
71
+
72
+ static void bn_print(int r, int len) {
73
+ for (int i = len - 1; i >= 0; i--) rv_putchar((char)('0' + sw_bn[r][i]));
74
+ }
75
+ #else
76
+ #include "bn.h"
77
+ #endif
78
+
79
+ // ============================================================================
80
+ // Basic Output & String Helpers (no libc printf bloat)
81
+ // ============================================================================
82
+
83
+ static void puts_raw(const char *s) {
84
+ while (*s) rv_putchar(*s++);
85
+ }
86
+
87
+ static void putc_rep(char c, int n) {
88
+ for (int i = 0; i < n; i++) rv_putchar(c);
89
+ }
90
+
91
+ static int slen(const char *s) {
92
+ int n = 0;
93
+ while (s[n]) n++;
94
+ return n;
95
+ }
96
+
97
+ static int streq(const char *a, const char *b) {
98
+ while (*a && *b && *a == *b) { a++; b++; }
99
+ return *a == 0 && *b == 0;
100
+ }
101
+
102
+ static int streq_ci(const char *a, const char *b) {
103
+ while (*a && *b) {
104
+ char ca = *a++, cb = *b++;
105
+ if (ca >= 'A' && ca <= 'Z') ca = (char)(ca + 32);
106
+ if (cb >= 'A' && cb <= 'Z') cb = (char)(cb + 32);
107
+ if (ca != cb) return 0;
108
+ }
109
+ return *a == 0 && *b == 0;
110
+ }
111
+
112
+ static int starts_with_ci(const char *s, const char *p) {
113
+ while (*p) {
114
+ char cs = *s++, cp = *p++;
115
+ if (cs >= 'A' && cs <= 'Z') cs = (char)(cs + 32);
116
+ if (cp >= 'A' && cp <= 'Z') cp = (char)(cp + 32);
117
+ if (cs != cp) return 0;
118
+ }
119
+ return 1;
120
+ }
121
+
122
+ static int is_space(char c) {
123
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
124
+ }
125
+
126
+ static int is_alpha(char c) {
127
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
128
+ }
129
+
130
+ static int is_digit(char c) {
131
+ return c >= '0' && c <= '9';
132
+ }
133
+
134
+ static void copy_trim(char *dst, int cap, const char *src, int len) {
135
+ int a = 0, b = len;
136
+ while (a < b && is_space(src[a])) a++;
137
+ while (b > a && is_space(src[b - 1])) b--;
138
+ if (b - a >= 2 && ((src[a] == '"' && src[b - 1] == '"') ||
139
+ (src[a] == '\'' && src[b - 1] == '\'') ||
140
+ (src[a] == '$' && src[b - 1] == '$'))) {
141
+ a++;
142
+ b--;
143
+ }
144
+ int o = 0;
145
+ for (int i = a; i < b && o < cap - 1; i++) {
146
+ dst[o++] = src[i] == '\n' || src[i] == '\r' ? ' ' : src[i];
147
+ }
148
+ dst[o] = 0;
149
+ }
150
+
151
+ static void append_str(char *buf, int cap, int *pos, const char *s) {
152
+ while (*s && *pos < cap - 1) buf[(*pos)++] = *s++;
153
+ buf[*pos] = 0;
154
+ }
155
+
156
+ static void append_char(char *buf, int cap, int *pos, char c) {
157
+ if (*pos < cap - 1) {
158
+ buf[(*pos)++] = c;
159
+ buf[*pos] = 0;
160
+ }
161
+ }
162
+
163
+ // ============================================================================
164
+ // Engine A: 62-Digit Hardware Bignum + Exact Reduced Rationals (bn.h)
165
+ // ============================================================================
166
+
167
+ #define MAXD 62
168
+ #define ONE_REG 7
169
+
170
+ typedef uint32_t num;
171
+ #define REG(a) ((a) & 7u)
172
+ #define NEG(a) (((a) >> 8) & 1u)
173
+ #define LEN(a) ((int)((a) >> 16))
174
+ #define NUM(r, neg, len) ((uint32_t)(r) | ((uint32_t)(neg) << 8) | ((uint32_t)(len) << 16))
175
+
176
+ static uint32_t bn_dirty[8];
177
+ static uint32_t bn_used;
178
+ static int bn_err;
179
+ static char one_ch = '1'; // MUST stay in RAM (.data), NOT const (.rodata)!
180
+
181
+ static char bn_steps[512];
182
+ static int bn_step_pos;
183
+ static int bn_step_cnt;
184
+
185
+ static int bn_dig(num a, int i) { return (int)bn_get(BN_OPS(0, i, 0, REG(a), 0, 0)); }
186
+
187
+ static void bn_wrote(num a, int n) {
188
+ if ((uint32_t)n > bn_dirty[REG(a)]) bn_dirty[REG(a)] = (uint32_t)n;
189
+ }
190
+
191
+ static num bn_alloc(int len) {
192
+ for (int r = 0; r < ONE_REG; r++) {
193
+ if (!(bn_used >> r & 1u)) {
194
+ bn_used |= 1u << r;
195
+ for (uint32_t o = 0; o < bn_dirty[r]; o += 63) {
196
+ uint32_t chunk = bn_dirty[r] - o < 63 ? bn_dirty[r] - o : 63;
197
+ bn_mac(BN_OPS(chunk, o, 0, r, r, 1));
198
+ }
199
+ bn_dirty[r] = 0;
200
+ return NUM(r, 0, len);
201
+ }
202
+ }
203
+ bn_err = 2;
204
+ return NUM(0, 0, len);
205
+ }
206
+
207
+ static void bn_release(num a) { bn_used &= ~(1u << REG(a)); }
208
+
209
+ static int bn_is_zero(num a) { return LEN(a) == 1 && !bn_dig(a, 0); }
210
+
211
+ static num bn_trim(num a) {
212
+ int n = LEN(a);
213
+ while (n > 1 && !bn_dig(a, n - 1)) n--;
214
+ a = (a & 0xFFFFu) | ((uint32_t)n << 16);
215
+ return (n == 1 && !bn_dig(a, 0)) ? (a & ~0x100u) : a;
216
+ }
217
+
218
+ static int bn_cmp_mag(num a, num b) {
219
+ if (LEN(a) != LEN(b)) return LEN(a) < LEN(b) ? -1 : 1;
220
+ for (int i = LEN(a) - 1; i >= 0; i--) {
221
+ int x = bn_dig(a, i), y = bn_dig(b, i);
222
+ if (x != y) return x < y ? -1 : 1;
223
+ }
224
+ return 0;
225
+ }
226
+
227
+ static num bn_add_mag(num a, num b) {
228
+ int n = (LEN(a) > LEN(b) ? LEN(a) : LEN(b)) + 1;
229
+ if (n > MAXD) bn_err = 1;
230
+ bn_mac(BN_OPS(n, 0, 1, REG(a), REG(b), 0));
231
+ bn_wrote(a, n);
232
+ if (!bn_dig(a, n - 1)) n--;
233
+ return (a & 0xFFFFu) | ((uint32_t)n << 16);
234
+ }
235
+
236
+ static num bn_sub_mag(num a, num b) {
237
+ bn_msb(BN_OPS(LEN(a), 0, 1, REG(a), REG(b), 0));
238
+ return bn_trim(a);
239
+ }
240
+
241
+ __attribute__((noinline)) static num bn_add(num a, num b) {
242
+ num s;
243
+ if (NEG(a) == NEG(b)) {
244
+ s = bn_add_mag(a, b);
245
+ bn_release(b);
246
+ } else if (bn_cmp_mag(a, b) >= 0) {
247
+ s = bn_sub_mag(a, b);
248
+ bn_release(b);
249
+ } else {
250
+ s = bn_sub_mag(b, a);
251
+ bn_release(a);
252
+ }
253
+ return s;
254
+ }
255
+
256
+ __attribute__((noinline)) static num bn_mul(num a, num b) {
257
+ if (LEN(a) < LEN(b)) { num t = a; a = b; b = t; }
258
+ int la = LEN(a), lb = LEN(b);
259
+ num c = bn_alloc(la + lb);
260
+ if (la + lb > MAXD) {
261
+ bn_err = 1;
262
+ return c;
263
+ }
264
+ uint32_t base = BN_OPS(la + 1, 0, 0, REG(c), REG(a), 0);
265
+ for (int j = 0; j < lb; j++) {
266
+ uint32_t d = (uint32_t)bn_dig(b, j);
267
+ if (d) bn_mac(base | BN_OFF(j) | BN_M(d));
268
+ }
269
+ bn_wrote(c, la + lb);
270
+ return bn_trim(c | ((NEG(a) ^ NEG(b)) << 8));
271
+ }
272
+
273
+ typedef struct { num q, r; } bn_qr;
274
+
275
+ __attribute__((noinline)) static bn_qr bn_divmod(num a, num b) {
276
+ if (bn_is_zero(b)) bn_err = 1;
277
+ if (bn_err || bn_cmp_mag(a, b) < 0) return (bn_qr){bn_alloc(1), a & ~0x100u};
278
+ int la = LEN(a), lb = LEN(b), ra = (int)REG(a);
279
+ num q = bn_alloc(la - lb + 1);
280
+ int b1 = bn_dig(b, lb - 1), den = lb >= 2 ? b1 * 10 + bn_dig(b, lb - 2) : b1;
281
+ uint32_t msb = BN_OPS(lb + 1, 0, 0, ra, REG(b), 0), mac = BN_OPS(lb + 1, 0, 1, ra, REG(b), 0);
282
+ uint32_t setq = BN_OPS(1, 0, 0, REG(q), ONE_REG, 1);
283
+ for (int t = la - lb; t >= 0; t--) {
284
+ int top = t + lb;
285
+ int w = bn_dig(a, top) * 10 + bn_dig(a, top - 1);
286
+ if (lb >= 2) w = w * 10 + bn_dig(a, top - 2);
287
+ int qd = (lb >= 3 ? w + 1 : w) / den;
288
+ if (qd > 9) qd = 9;
289
+ if (qd) {
290
+ uint32_t o = BN_OFF(t);
291
+ if (bn_msb(msb | o | BN_M(qd)))
292
+ do qd--;
293
+ while (!bn_mac(mac | o) && qd > 0);
294
+ if (qd) bn_mac(setq | o | BN_M(qd));
295
+ }
296
+ }
297
+ bn_wrote(q, la - lb + 1);
298
+ return (bn_qr){bn_trim(q), bn_trim(NUM(ra, 0, lb))};
299
+ }
300
+
301
+ __attribute__((noinline)) static num bn_divide(num a, num b, int want_mod) {
302
+ uint32_t sa = NEG(a), sb = NEG(b);
303
+ bn_qr d = bn_divmod(a, b);
304
+ if (sa != sb && !bn_is_zero(d.r)) {
305
+ d.q = bn_add_mag(d.q, NUM(ONE_REG, 0, 1));
306
+ num r2 = bn_sub_mag(b & ~0x100u, d.r);
307
+ bn_release(d.r);
308
+ d.r = r2;
309
+ } else {
310
+ bn_release(b);
311
+ }
312
+ bn_release(want_mod ? d.q : d.r);
313
+ num out = want_mod ? d.r : d.q;
314
+ out = (out & ~0x100u) | ((want_mod ? sb : (sa ^ sb)) << 8);
315
+ return bn_is_zero(out) ? (out & ~0x100u) : out;
316
+ }
317
+
318
+ __attribute__((noinline)) static num bn_gcd(num a, num b) {
319
+ a &= ~0x100u;
320
+ b &= ~0x100u;
321
+ while (!bn_err && !bn_is_zero(b)) {
322
+ bn_qr d = bn_divmod(a, b);
323
+ bn_release(d.q);
324
+ a = b;
325
+ b = d.r;
326
+ }
327
+ bn_release(b);
328
+ return a;
329
+ }
330
+
331
+ __attribute__((noinline)) static num bn_power(num a, num e) {
332
+ int x = 0;
333
+ if (NEG(e) || LEN(e) > 3) bn_err = 1;
334
+ for (int i = LEN(e) - 1; i >= 0 && !bn_err; i--) x = x * 10 + bn_dig(e, i);
335
+ bn_release(e);
336
+ num res = bn_alloc(1);
337
+ bn_mac(BN_OPS(1, 0, 1, REG(res), ONE_REG, 1));
338
+ bn_wrote(res, 1);
339
+ while (x && !bn_err) {
340
+ if (x & 1) {
341
+ num t = bn_mul(res, a);
342
+ bn_release(res);
343
+ res = t;
344
+ }
345
+ x >>= 1;
346
+ if (x) {
347
+ num t = bn_mul(a, a);
348
+ bn_release(a);
349
+ a = t;
350
+ }
351
+ }
352
+ bn_release(a);
353
+ return res;
354
+ }
355
+
356
+ static num bn_vals[16];
357
+ static num bn_vden[16];
358
+ static int bn_hasden[16];
359
+ static int bn_vsc[16];
360
+ static char bn_ops[16];
361
+
362
+ static int bn_prec(int op) {
363
+ return op == '^' ? 4 : op == 'n' ? 3 : (op == '*' || op == 'f' || op == 'd' || op == '%') ? 2 : 1;
364
+ }
365
+
366
+ static num bn_copy_of(num a) {
367
+ num c = bn_alloc(LEN(a));
368
+ bn_mac(BN_OPS(LEN(a), 0, 1, REG(c), REG(a), 1));
369
+ bn_wrote(c, LEN(a));
370
+ return NUM(REG(c), NEG(a), LEN(a));
371
+ }
372
+
373
+ __attribute__((noinline)) static void bn_reduce(int i) {
374
+ num n = bn_vals[i], d = bn_vden[i];
375
+ bn_hasden[i] = 0;
376
+ if (bn_err) return;
377
+ if (bn_is_zero(n)) {
378
+ bn_release(d);
379
+ return;
380
+ }
381
+ uint32_t neg = NEG(n);
382
+ num g = bn_gcd(bn_copy_of(n), bn_copy_of(d));
383
+ bn_qr x = bn_divmod(n & ~0x100u, g);
384
+ bn_release(x.r);
385
+ bn_qr y = bn_divmod(d, g);
386
+ bn_release(y.r);
387
+ bn_release(g);
388
+ bn_vals[i] = x.q | (neg << 8);
389
+ if (LEN(y.q) == 1 && bn_dig(y.q, 0) == 1) {
390
+ bn_release(y.q);
391
+ } else {
392
+ bn_vden[i] = y.q;
393
+ bn_hasden[i] = 1;
394
+ }
395
+ }
396
+
397
+ __attribute__((noinline)) static void bn_frac_op(int op, int i, num a, num da, int fa, num b, num db, int fb) {
398
+ num n, d;
399
+ if (op == '*') {
400
+ n = bn_mul(a, b);
401
+ bn_release(a);
402
+ bn_release(b);
403
+ if (fa && fb) {
404
+ d = bn_mul(da, db);
405
+ bn_release(da);
406
+ bn_release(db);
407
+ } else {
408
+ d = fa ? da : db;
409
+ }
410
+ } else if (op == 'd') {
411
+ if (bn_is_zero(b)) bn_err = 1;
412
+ if (fb) { n = bn_mul(a, db); bn_release(a); bn_release(db); }
413
+ else n = a;
414
+ if (fa) { d = bn_mul(da, b); bn_release(da); bn_release(b); }
415
+ else d = b;
416
+ if (NEG(d)) {
417
+ d &= ~0x100u;
418
+ if (!bn_is_zero(n)) n ^= 0x100u;
419
+ }
420
+ } else {
421
+ num t1 = a, t2 = b;
422
+ if (fb) { t1 = bn_mul(a, db); bn_release(a); }
423
+ if (fa) { t2 = bn_mul(b, da); bn_release(b); }
424
+ if (fa && fb) { d = bn_mul(da, db); bn_release(da); bn_release(db); }
425
+ else d = fa ? da : db;
426
+ n = bn_add(t1, (op == '-' && !bn_is_zero(t2)) ? (t2 ^ 0x100u) : t2);
427
+ }
428
+ bn_vals[i] = n;
429
+ bn_vden[i] = d;
430
+ bn_reduce(i);
431
+ }
432
+
433
+ static num bn_shift(num a, int k) {
434
+ int n = LEN(a) + k;
435
+ if (n > MAXD) { bn_err = 1; return a; }
436
+ num c = bn_alloc(n);
437
+ bn_mac(BN_OPS(LEN(a), k, 1, REG(c), REG(a), 1));
438
+ bn_wrote(c, n);
439
+ bn_release(a);
440
+ return NUM(REG(c), NEG(a), n);
441
+ }
442
+
443
+ static void bn_fmt_slot(int idx, char *buf, int cap) {
444
+ int p = 0;
445
+ num v = bn_vals[idx];
446
+ int sc = bn_vsc[idx], n = LEN(v);
447
+ if (NEG(v)) append_char(buf, cap, &p, '-');
448
+ if (!sc) {
449
+ for (int i = n - 1; i >= 0; i--) append_char(buf, cap, &p, (char)('0' + bn_dig(v, i)));
450
+ } else {
451
+ int z = 0;
452
+ while (z < sc && !bn_dig(v, z)) z++;
453
+ if (n <= sc) append_char(buf, cap, &p, '0');
454
+ for (int i = n > sc ? n - 1 : sc - 1; i >= z; i--) {
455
+ if (i == sc - 1) append_char(buf, cap, &p, '.');
456
+ append_char(buf, cap, &p, (char)('0' + (i < n ? bn_dig(v, i) : 0)));
457
+ }
458
+ }
459
+ if (bn_hasden[idx]) {
460
+ num dv = bn_vden[idx];
461
+ append_char(buf, cap, &p, '/');
462
+ for (int i = LEN(dv) - 1; i >= 0; i--) append_char(buf, cap, &p, (char)('0' + bn_dig(dv, i)));
463
+ }
464
+ }
465
+
466
+ static int bn_apply(int op, int nv) {
467
+ if (op == 'n') {
468
+ num v = bn_vals[nv - 1];
469
+ bn_vals[nv - 1] = bn_is_zero(v) ? v : (v ^ 0x100u);
470
+ return nv;
471
+ }
472
+ if (nv < 2) { bn_err = 1; return nv; }
473
+
474
+ char sa_str[72], sb_str[72], sr_str[72];
475
+ int can_log = (!bn_err && bn_step_cnt < 8 && LEN(bn_vals[nv - 2]) <= 30 && LEN(bn_vals[nv - 1]) <= 30);
476
+ if (can_log) {
477
+ bn_fmt_slot(nv - 2, sa_str, sizeof sa_str);
478
+ bn_fmt_slot(nv - 1, sb_str, sizeof sb_str);
479
+ }
480
+
481
+ int sb = bn_vsc[nv - 1], sa = bn_vsc[nv - 2];
482
+ int fb = bn_hasden[nv - 1], fa = bn_hasden[nv - 2];
483
+ num b = bn_vals[--nv], a = bn_vals[nv - 1], r;
484
+ int rational = op == 'd' || ((fa || fb) && (op == '+' || op == '-' || op == '*'));
485
+ if ((fa || fb) && !rational) bn_err = 1;
486
+ if (rational && (sa || sb)) bn_err = 1;
487
+ if (bn_err) {
488
+ r = a;
489
+ bn_hasden[nv - 1] = 0;
490
+ bn_vals[nv - 1] = r;
491
+ bn_vsc[nv - 1] = sa;
492
+ } else if (rational) {
493
+ bn_frac_op(op, nv - 1, a, bn_vden[nv - 1], fa, b, bn_vden[nv], fb);
494
+ bn_vsc[nv - 1] = 0;
495
+ } else {
496
+ if (op == '+' || op == '-') {
497
+ if (sa < sb) { a = bn_shift(a, sb - sa); sa = sb; }
498
+ if (sb < sa) b = bn_shift(b, sa - sb);
499
+ r = bn_add(a, (op == '-' && !bn_is_zero(b)) ? (b ^ 0x100u) : b);
500
+ } else if (op == '*') {
501
+ r = bn_mul(a, b);
502
+ bn_release(a);
503
+ bn_release(b);
504
+ sa += sb;
505
+ } else if (sa || sb) {
506
+ bn_err = 1;
507
+ r = a;
508
+ } else if (op == '^') {
509
+ r = bn_power(a, b);
510
+ } else if (op == 'g') {
511
+ r = bn_gcd(a, b);
512
+ } else {
513
+ r = bn_divide(a, b, op == '%');
514
+ }
515
+ bn_hasden[nv - 1] = 0;
516
+ bn_vals[nv - 1] = r;
517
+ bn_vsc[nv - 1] = sa;
518
+ }
519
+
520
+ if (can_log && !bn_err && LEN(bn_vals[nv - 1]) <= 62) {
521
+ bn_fmt_slot(nv - 1, sr_str, sizeof sr_str);
522
+ bn_step_cnt++;
523
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, "step ");
524
+ append_char(bn_steps, sizeof bn_steps, &bn_step_pos, (char)('0' + bn_step_cnt));
525
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, ": ");
526
+ if (op == 'g') {
527
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, "gcd(");
528
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, sa_str);
529
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, ", ");
530
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, sb_str);
531
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, ") = ");
532
+ } else {
533
+ const char *op_s = op == 'd' ? "/" : op == 'f' ? "//" : op == '^' ? "^" :
534
+ op == '*' ? "*" : op == '+' ? "+" : op == '-' ? "-" : "%";
535
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, sa_str);
536
+ append_char(bn_steps, sizeof bn_steps, &bn_step_pos, ' ');
537
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, op_s);
538
+ append_char(bn_steps, sizeof bn_steps, &bn_step_pos, ' ');
539
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, sb_str);
540
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, " = ");
541
+ }
542
+ append_str(bn_steps, sizeof bn_steps, &bn_step_pos, sr_str);
543
+ append_char(bn_steps, sizeof bn_steps, &bn_step_pos, '\n');
544
+ }
545
+ return nv;
546
+ }
547
+
548
+ // Try evaluating a pure arithmetic/rational/bignum expression in Engine A.
549
+ // Returns 0 on success (having printed the structured trace + exact answer), 1 if unsupported/failed.
550
+ static int run_bignum_engine(const char *expr_ram) {
551
+ bn_used = 0;
552
+ bn_err = 0;
553
+ bn_step_pos = 0;
554
+ bn_step_cnt = 0;
555
+ bn_steps[0] = 0;
556
+ for (int r = 0; r < ONE_REG; r++) {
557
+ bn_mac(BN_OPS(63, 0, 0, r, r, 1));
558
+ bn_mac(BN_OPS(63, 63, 0, r, r, 1));
559
+ bn_dirty[r] = 0;
560
+ }
561
+ bn_ld(&one_ch + 1, BN_OPS(1, 0, 0, ONE_REG, 0, 0));
562
+
563
+ const char *s = expr_ram;
564
+ int operand = 1, nv = 0, no = 0;
565
+ for (;;) {
566
+ char c = *s;
567
+ if (c == ' ' || c == '\t') { s++; continue; }
568
+ if (nv >= 15 || no >= 15) bn_err = 1;
569
+ if (bn_err) break;
570
+ if (operand) {
571
+ if (is_digit(c)) {
572
+ int n = (int)bn_scan(s);
573
+ const char *end = s + n;
574
+ int frac = 0;
575
+ if (*end == '.' && is_digit(end[1])) {
576
+ static char dbuf[MAXD + 4];
577
+ frac = (int)bn_scan(end + 1);
578
+ if (n + frac > MAXD) { bn_err = 1; break; }
579
+ for (int i = 0; i < n; i++) dbuf[i] = s[i];
580
+ for (int i = 0; i < frac; i++) dbuf[n + i] = end[1 + i];
581
+ s = end + 1 + frac;
582
+ n += frac;
583
+ end = dbuf + n;
584
+ } else {
585
+ s = end;
586
+ }
587
+ if (n > MAXD) { bn_err = 1; break; }
588
+ num a = bn_alloc(n);
589
+ bn_ld(end, BN_OPS(n, 0, 0, REG(a), 0, 0));
590
+ bn_wrote(a, n);
591
+ bn_vsc[nv] = frac;
592
+ bn_hasden[nv] = 0;
593
+ bn_vals[nv++] = bn_dig(a, n - 1) ? a : bn_trim(a);
594
+ operand = 0;
595
+ } else if (c == '(') {
596
+ bn_ops[no++] = '(';
597
+ s++;
598
+ } else if (c == '-' || c == '+') {
599
+ if (c == '-') bn_ops[no++] = 'n';
600
+ s++;
601
+ } else if (starts_with_ci(s, "gcd(")) {
602
+ bn_ops[no++] = 'g';
603
+ s += 4;
604
+ } else {
605
+ bn_err = 1;
606
+ }
607
+ continue;
608
+ }
609
+ // Implicit multiplication before '(' when preceded by number or ')'
610
+ if (c == '(') {
611
+ int pr = bn_prec('*');
612
+ while (no && bn_ops[no - 1] != '(' && bn_ops[no - 1] != 'g' && bn_prec(bn_ops[no - 1]) >= pr)
613
+ nv = bn_apply(bn_ops[--no], nv);
614
+ bn_ops[no++] = '*';
615
+ bn_ops[no++] = '(';
616
+ s++;
617
+ operand = 1;
618
+ continue;
619
+ }
620
+ if (c == ')' || c == ',') {
621
+ while (no && bn_ops[no - 1] != '(' && bn_ops[no - 1] != 'g') nv = bn_apply(bn_ops[--no], nv);
622
+ if (!no) {
623
+ bn_err = 1;
624
+ } else if (c == ')') {
625
+ if (bn_ops[--no] == 'g') nv = bn_apply('g', nv);
626
+ } else if (bn_ops[no - 1] != 'g') {
627
+ bn_err = 1;
628
+ } else {
629
+ operand = 1;
630
+ }
631
+ s++;
632
+ continue;
633
+ }
634
+ if (!c) break;
635
+ int op = (unsigned char)c, len = 1;
636
+ if (c == '*' && s[1] == '*') {
637
+ op = '^';
638
+ len = 2;
639
+ } else if (c == '/') {
640
+ op = s[1] == '/' ? 'f' : 'd';
641
+ len = s[1] == '/' ? 2 : 1;
642
+ } else if (c != '+' && c != '-' && c != '*' && c != '%' && c != '^') {
643
+ bn_err = 1;
644
+ continue;
645
+ }
646
+ int pr = bn_prec(op);
647
+ while (no && bn_ops[no - 1] != '(' && bn_ops[no - 1] != 'g' &&
648
+ (bn_prec(bn_ops[no - 1]) > pr || (bn_prec(bn_ops[no - 1]) == pr && op != '^')))
649
+ nv = bn_apply(bn_ops[--no], nv);
650
+ bn_ops[no++] = (char)op;
651
+ s += len;
652
+ operand = 1;
653
+ }
654
+ while (!bn_err && no) {
655
+ int op = bn_ops[--no];
656
+ if (op == '(' || op == 'g') bn_err = 1;
657
+ else nv = bn_apply(op, nv);
658
+ }
659
+ if (bn_err || nv != 1 || operand) return 1;
660
+
661
+ puts_raw("expression: ");
662
+ puts_raw(expr_ram);
663
+ rv_putchar('\n');
664
+ if (bn_step_cnt >= 2) puts_raw(bn_steps);
665
+
666
+ puts_raw("exact value: ");
667
+ num v = bn_vals[0];
668
+ int sc = bn_vsc[0], n = LEN(v);
669
+ if (NEG(v)) rv_putchar('-');
670
+ if (!sc) {
671
+ bn_print((int)REG(v), n);
672
+ } else {
673
+ int z = 0;
674
+ while (z < sc && !bn_dig(v, z)) z++;
675
+ if (n <= sc) rv_putchar('0');
676
+ for (int i = n > sc ? n - 1 : sc - 1; i >= z; i--) {
677
+ if (i == sc - 1) rv_putchar('.');
678
+ rv_putchar((char)('0' + (i < n ? bn_dig(v, i) : 0)));
679
+ }
680
+ }
681
+ if (bn_hasden[0]) {
682
+ num den = bn_vden[0];
683
+ rv_putchar('/');
684
+ bn_print((int)REG(den), LEN(den));
685
+ // Also print 6-decimal expansion when numerator*10^6 fits in MAXD
686
+ if (n + 6 <= MAXD) {
687
+ uint32_t neg = NEG(v);
688
+ num scaled = bn_shift(bn_copy_of(v & ~0x100u), 6);
689
+ bn_qr dec = bn_divmod(scaled, den);
690
+ int exact_dec = bn_is_zero(dec.r);
691
+ puts_raw(exact_dec ? " (= " : " (~ ");
692
+ if (neg) rv_putchar('-');
693
+ int dn = LEN(dec.q);
694
+ int z = 0;
695
+ while (z < 6 && exact_dec && (z >= dn || !bn_dig(dec.q, z))) z++;
696
+ if (dn <= 6) rv_putchar('0');
697
+ for (int i = dn > 6 ? dn - 1 : 5; i >= z; i--) {
698
+ if (i == 5) rv_putchar('.');
699
+ rv_putchar((char)('0' + (i < dn ? bn_dig(dec.q, i) : 0)));
700
+ }
701
+ rv_putchar(')');
702
+ bn_release(dec.q);
703
+ bn_release(dec.r);
704
+ }
705
+ }
706
+ rv_putchar('\n');
707
+ return 0;
708
+ }
709
+
710
+ // ============================================================================
711
+ // Engine B: Q32.32 Fixed-Point Scientific, Equation Solver & Graphing Engine
712
+ // ============================================================================
713
+
714
+ typedef int64_t q32_t;
715
+ #define Q32_MAX 0x7FFFFFFFFFFFFFFFLL
716
+ #define Q32_MIN (-0x7FFFFFFFFFFFFFFFLL - 1LL)
717
+
718
+ static int q_err;
719
+
720
+ static q32_t q32_from_int(int64_t x) {
721
+ if (x > 2147483647LL || x < -2147483648LL) { q_err = 1; return x > 0 ? Q32_MAX : Q32_MIN; }
722
+ return x << 32;
723
+ }
724
+
725
+ static q32_t q32_add(q32_t a, q32_t b) {
726
+ uint64_t r = (uint64_t)a + (uint64_t)b;
727
+ if (((a ^ (q32_t)r) & (b ^ (q32_t)r)) < 0) {
728
+ q_err = 1;
729
+ return a > 0 ? Q32_MAX : Q32_MIN;
730
+ }
731
+ return (q32_t)r;
732
+ }
733
+
734
+ static q32_t q32_sub(q32_t a, q32_t b) {
735
+ uint64_t r = (uint64_t)a - (uint64_t)b;
736
+ if (((a ^ b) & (a ^ (q32_t)r)) < 0) {
737
+ q_err = 1;
738
+ return a > 0 ? Q32_MAX : Q32_MIN;
739
+ }
740
+ return (q32_t)r;
741
+ }
742
+
743
+ static q32_t q32_mul(q32_t a, q32_t b) {
744
+ int neg = (a < 0) ^ (b < 0);
745
+ uint64_t ua = a < 0 ? -(uint64_t)a : (uint64_t)a;
746
+ uint64_t ub = b < 0 ? -(uint64_t)b : (uint64_t)b;
747
+ uint32_t al = (uint32_t)ua, ah = (uint32_t)(ua >> 32);
748
+ uint32_t bl = (uint32_t)ub, bh = (uint32_t)(ub >> 32);
749
+ if (ah && bh) {
750
+ uint64_t hh = (uint64_t)ah * bh;
751
+ if (hh >= 0x80000000ULL) { q_err = 1; return neg ? Q32_MIN : Q32_MAX; }
752
+ uint64_t p_ll = (uint64_t)al * bl;
753
+ uint64_t p_hl = (uint64_t)ah * bl;
754
+ uint64_t p_lh = (uint64_t)al * bh;
755
+ uint64_t mid = (p_ll >> 32) + (p_hl & 0xFFFFFFFFULL) + (p_lh & 0xFFFFFFFFULL);
756
+ uint64_t hi = hh + (p_hl >> 32) + (p_lh >> 32) + (mid >> 32);
757
+ if (hi >= 0x80000000ULL) { q_err = 1; return neg ? Q32_MIN : Q32_MAX; }
758
+ uint64_t res = (hi << 32) | (mid & 0xFFFFFFFFULL);
759
+ res += (p_ll >> 31) & 1ULL;
760
+ return neg ? -(q32_t)res : (q32_t)res;
761
+ }
762
+ uint64_t p_ll = (uint64_t)al * bl;
763
+ uint64_t p_hl = (uint64_t)ah * bl;
764
+ uint64_t p_lh = (uint64_t)al * bh;
765
+ uint64_t mid = (p_ll >> 32) + (p_hl & 0xFFFFFFFFULL) + (p_lh & 0xFFFFFFFFULL);
766
+ uint64_t hi = (p_hl >> 32) + (p_lh >> 32) + (mid >> 32);
767
+ if (hi >= 0x80000000ULL) { q_err = 1; return neg ? Q32_MIN : Q32_MAX; }
768
+ uint64_t res = (hi << 32) | (mid & 0xFFFFFFFFULL);
769
+ res += (p_ll >> 31) & 1ULL;
770
+ return neg ? -(q32_t)res : (q32_t)res;
771
+ }
772
+
773
+ static q32_t q32_div(q32_t a, q32_t b) {
774
+ if (b == 0) { q_err = 1; return 0; }
775
+ int neg = (a < 0) ^ (b < 0);
776
+ uint64_t ua = a < 0 ? -(uint64_t)a : (uint64_t)a;
777
+ uint64_t ub = b < 0 ? -(uint64_t)b : (uint64_t)b;
778
+ uint64_t q_int = ua / ub;
779
+ if (q_int >= 0x80000000ULL) { q_err = 1; return neg ? Q32_MIN : Q32_MAX; }
780
+ uint64_t rem = ua % ub;
781
+ uint32_t frac = 0;
782
+ if (ub <= 0xFFFFFFFFULL) {
783
+ frac = (uint32_t)(((rem << 32) + (ub >> 1)) / ub);
784
+ uint64_t res = (q_int << 32) + frac;
785
+ return neg ? -(q32_t)res : (q32_t)res;
786
+ }
787
+ for (int i = 0; i < 32; i++) {
788
+ uint64_t top = rem >> 63;
789
+ rem <<= 1;
790
+ frac <<= 1;
791
+ if (top || rem >= ub) {
792
+ rem -= ub;
793
+ frac |= 1u;
794
+ }
795
+ }
796
+ uint64_t top = rem >> 63;
797
+ rem <<= 1;
798
+ uint64_t res = (q_int << 32) | frac;
799
+ if (top || rem >= ub) res++;
800
+ return neg ? -(q32_t)res : (q32_t)res;
801
+ }
802
+
803
+ static q32_t q32_sqrt(q32_t x) {
804
+ if (x < 0) { q_err = 1; return 0; }
805
+ if (x == 0) return 0;
806
+ uint64_t ux = (uint64_t)x;
807
+ uint64_t rem = 0, root = 0;
808
+ for (int i = 47; i >= 0; i--) {
809
+ uint64_t two = i >= 16 ? ((ux >> (2 * (i - 16))) & 3ULL) : 0ULL;
810
+ rem = (rem << 2) | two;
811
+ uint64_t cand = (root << 2) | 1ULL;
812
+ if (rem >= cand) {
813
+ rem -= cand;
814
+ root = (root << 1) | 1ULL;
815
+ } else {
816
+ root <<= 1;
817
+ }
818
+ }
819
+ return (q32_t)root;
820
+ }
821
+
822
+ static void q32_sincos(q32_t theta, q32_t *out_s, q32_t *out_c) {
823
+ q32_t r = theta;
824
+ if (r > Q32_PI || r < -Q32_PI) {
825
+ int save = q_err;
826
+ int64_t turns = q32_mul(r, Q32_INV_TWO_PI) >> 32;
827
+ q_err = save;
828
+ r -= turns * Q32_TWO_PI;
829
+ while (r > Q32_PI) r -= Q32_TWO_PI;
830
+ while (r < -Q32_PI) r += Q32_TWO_PI;
831
+ }
832
+ int flip = 0;
833
+ if (r > Q32_HALF_PI) { r = Q32_PI - r; flip = 1; }
834
+ else if (r < -Q32_HALF_PI) { r = -Q32_PI - r; flip = 1; }
835
+
836
+ q32_t x = CORDIC_K, y = 0, z = r;
837
+ for (int i = 0; i < 30; i++) {
838
+ q32_t dx = y >> i, dy = x >> i;
839
+ if (z >= 0) { x -= dx; y += dy; z -= CORDIC_ATAN[i]; }
840
+ else { x += dx; y -= dy; z += CORDIC_ATAN[i]; }
841
+ }
842
+ if (flip) x = -x;
843
+ if (x > -16 && x < 16) x = 0;
844
+ if (y > -16 && y < 16) y = 0;
845
+ *out_s = y;
846
+ *out_c = x;
847
+ }
848
+
849
+ static q32_t q32_ln(q32_t x) {
850
+ if (x <= 0) { q_err = 1; return 0; }
851
+ uint64_t m = (uint64_t)x;
852
+ int k = 0;
853
+ while (m >= (2ULL << 32)) { m >>= 1; k++; }
854
+ while (m < (1ULL << 32)) { m <<= 1; k--; }
855
+ uint64_t prod = 1ULL << 32;
856
+ q32_t acc = 0;
857
+ for (int i = 1; i <= 30; i++) {
858
+ uint64_t nxt = prod + (prod >> i);
859
+ if (nxt <= m) {
860
+ prod = nxt;
861
+ acc += LN1P_POW2[i];
862
+ }
863
+ }
864
+ if (m > prod) acc += q32_div((q32_t)(m - prod), (q32_t)prod);
865
+ return (int64_t)k * Q32_LN2 + acc;
866
+ }
867
+
868
+ static q32_t q32_exp(q32_t x) {
869
+ if (x > 92288000000LL) { q_err = 1; return Q32_MAX; }
870
+ if (x < -96000000000LL) return 0;
871
+ q32_t kq = q32_mul(x, Q32_INV_LN2);
872
+ int32_t k = (int32_t)(kq >> 32);
873
+ q32_t r = x - (int64_t)k * Q32_LN2;
874
+ while (r < 0) { r += Q32_LN2; k--; }
875
+ while (r >= Q32_LN2) { r -= Q32_LN2; k++; }
876
+ uint64_t prod = 1ULL << 32;
877
+ for (int i = 1; i <= 30; i++) {
878
+ if (r >= LN1P_POW2[i]) {
879
+ r -= LN1P_POW2[i];
880
+ prod += prod >> i;
881
+ }
882
+ }
883
+ if (r > 0) prod += (prod * (uint64_t)r) >> 32;
884
+ if (k >= 31) { q_err = 1; return Q32_MAX; }
885
+ if (k >= 0) {
886
+ uint64_t res = prod << k;
887
+ if (res >= 0x8000000000000000ULL) { q_err = 1; return Q32_MAX; }
888
+ return (q32_t)res;
889
+ }
890
+ return (-k >= 63) ? 0 : (q32_t)(prod >> (-k));
891
+ }
892
+
893
+ static q32_t q32_ipow(q32_t a, int32_t e) {
894
+ int inv = 0;
895
+ if (e < 0) { inv = 1; e = -e; }
896
+ q32_t res = Q32_ONE;
897
+ while (e > 0 && !q_err) {
898
+ if (e & 1) res = q32_mul(res, a);
899
+ e >>= 1;
900
+ if (e) a = q32_mul(a, a);
901
+ }
902
+ return inv ? q32_div(Q32_ONE, res) : res;
903
+ }
904
+
905
+ static q32_t q32_pow(q32_t a, q32_t b) {
906
+ if ((b & 0xFFFFFFFFLL) == 0) {
907
+ int64_t e = b >> 32;
908
+ if (e >= -64 && e <= 64) return q32_ipow(a, (int32_t)e);
909
+ }
910
+ if (a == 0) {
911
+ if (b <= 0) q_err = 1;
912
+ return 0;
913
+ }
914
+ if (a < 0) { q_err = 1; return 0; }
915
+ return q32_exp(q32_mul(b, q32_ln(a)));
916
+ }
917
+
918
+ static int parse_q32_num(const char *s, const char **endp, q32_t *out) {
919
+ while (*s == ' ') s++;
920
+ int neg = 0;
921
+ if (*s == '-' || *s == '+') { neg = (*s == '-'); s++; }
922
+ while (*s == ' ') s++;
923
+ if (starts_with_ci(s, "pi") && !is_alpha(s[2])) {
924
+ *out = neg ? -Q32_PI : Q32_PI;
925
+ if (endp) *endp = s + 2;
926
+ return 1;
927
+ }
928
+ if (!is_digit(*s) && !(*s == '.' && is_digit(s[1]))) return 0;
929
+ uint64_t ip = 0;
930
+ while (is_digit(*s)) {
931
+ ip = ip * 10 + (uint64_t)(*s - '0');
932
+ if (ip > 2147483647ULL) return 0;
933
+ s++;
934
+ }
935
+ uint64_t fp_num = 0, fp_den = 1;
936
+ if (*s == '.') {
937
+ s++;
938
+ while (is_digit(*s)) {
939
+ if (fp_den < 100000000000ULL) {
940
+ fp_num = fp_num * 10 + (uint64_t)(*s - '0');
941
+ fp_den *= 10;
942
+ }
943
+ s++;
944
+ }
945
+ }
946
+ uint64_t frac = 0;
947
+ if (fp_num > 0) {
948
+ // Compute (fp_num << 32) / fp_den cleanly via 32-bit binary division
949
+ uint64_t rem = fp_num;
950
+ for (int i = 0; i < 32; i++) {
951
+ rem <<= 1;
952
+ frac <<= 1;
953
+ if (rem >= fp_den) { rem -= fp_den; frac |= 1ULL; }
954
+ }
955
+ if ((rem << 1) >= fp_den) frac++;
956
+ }
957
+ q32_t v = (q32_t)((ip << 32) + frac);
958
+ // Optional /denom for inline bounds like "-pi/2" or "3/2"
959
+ if (*s == '/' && is_digit(s[1])) {
960
+ s++;
961
+ int64_t d = 0;
962
+ while (is_digit(*s) && d < 100000000LL) { d = d * 10 + (*s - '0'); s++; }
963
+ if (d > 0) v = q32_div(v, q32_from_int(d));
964
+ }
965
+ *out = neg ? -v : v;
966
+ if (endp) *endp = s;
967
+ return 1;
968
+ }
969
+
970
+ static void fmt_q32(q32_t v, char *buf, int cap, int decimals) {
971
+ int p = 0;
972
+ if (v > -64 && v < 64) v = 0; // snap sub-1.5e-8 noise to 0
973
+ int neg = v < 0;
974
+ uint64_t uv = neg ? -(uint64_t)v : (uint64_t)v;
975
+ uint32_t scale = 1;
976
+ for (int i = 0; i < decimals; i++) scale *= 10u;
977
+ uint32_t ip = (uint32_t)(uv >> 32);
978
+ uint64_t fl = uv & 0xFFFFFFFFULL;
979
+ // Rounded decimal fraction: ((fl * scale) + 2^31) >> 32
980
+ uint64_t prod = fl * (uint64_t)scale + (1ULL << 31);
981
+ uint32_t dec = (uint32_t)(prod >> 32);
982
+ if (dec >= scale) { ip++; dec -= scale; }
983
+ if (ip == 0 && dec == 0) neg = 0;
984
+ if (neg) append_char(buf, cap, &p, '-');
985
+
986
+ char itmp[16];
987
+ int in = 0;
988
+ if (ip == 0) itmp[in++] = '0';
989
+ while (ip > 0) { itmp[in++] = (char)('0' + (ip % 10u)); ip /= 10u; }
990
+ while (in > 0) append_char(buf, cap, &p, itmp[--in]);
991
+
992
+ if (decimals > 0 && dec > 0) {
993
+ char dtmp[12];
994
+ for (int i = decimals - 1; i >= 0; i--) {
995
+ dtmp[i] = (char)('0' + (dec % 10u));
996
+ dec /= 10u;
997
+ }
998
+ int len = decimals;
999
+ while (len > 0 && dtmp[len - 1] == '0') len--;
1000
+ if (len > 0) {
1001
+ append_char(buf, cap, &p, '.');
1002
+ for (int i = 0; i < len; i++) append_char(buf, cap, &p, dtmp[i]);
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ // Continued-fraction recovery for exact small rationals p/q (1 < q <= 240, error <= 6 ULPs).
1008
+ static void fmt_q32_rich(q32_t v, char *buf, int cap) {
1009
+ fmt_q32(v, buf, cap, 6);
1010
+ uint64_t uv = v < 0 ? -(uint64_t)v : (uint64_t)v;
1011
+ if ((uv & 0xFFFFFFFFULL) < 64 || (uv & 0xFFFFFFFFULL) > 0xFFFFFFC0ULL) return;
1012
+ int64_t p0 = 0, q0 = 1, p1 = 1, q1 = 0;
1013
+ uint64_t num = uv, den = 1ULL << 32;
1014
+ for (int step = 0; step < 12 && den != 0; step++) {
1015
+ int64_t a = (int64_t)(num / den);
1016
+ uint64_t r = num % den;
1017
+ int64_t p2 = a * p1 + p0;
1018
+ int64_t q2 = a * q1 + q0;
1019
+ if (q2 > 240) break;
1020
+ p0 = p1; q0 = q1; p1 = p2; q1 = q2;
1021
+ if (q1 > 1) {
1022
+ int save = q_err;
1023
+ q_err = 0;
1024
+ q32_t cand = q32_div(q32_from_int(v < 0 ? -p1 : p1), q32_from_int(q1));
1025
+ q_err = save;
1026
+ q32_t diff = v > cand ? v - cand : cand - v;
1027
+ if (diff <= 6) {
1028
+ char dec_buf[32];
1029
+ fmt_q32(v, dec_buf, sizeof dec_buf, 6);
1030
+ int pos = 0;
1031
+ if (v < 0) append_char(buf, cap, &pos, '-');
1032
+ char tmp[24];
1033
+ int tn = 0;
1034
+ uint32_t up = (uint32_t)p1;
1035
+ while (up > 0) { tmp[tn++] = (char)('0' + (up % 10u)); up /= 10u; }
1036
+ while (tn > 0) append_char(buf, cap, &pos, tmp[--tn]);
1037
+ append_char(buf, cap, &pos, '/');
1038
+ uint32_t uq = (uint32_t)q1;
1039
+ while (uq > 0) { tmp[tn++] = (char)('0' + (uq % 10u)); uq /= 10u; }
1040
+ while (tn > 0) append_char(buf, cap, &pos, tmp[--tn]);
1041
+ append_str(buf, cap, &pos, " (~");
1042
+ append_str(buf, cap, &pos, dec_buf);
1043
+ append_char(buf, cap, &pos, ')');
1044
+ return;
1045
+ }
1046
+ }
1047
+ num = den;
1048
+ den = r;
1049
+ }
1050
+ }
1051
+
1052
+ // --- RPN Bytecode Compiler & Evaluator ---
1053
+ enum {
1054
+ OP_NUM = 1, OP_VAR_X,
1055
+ OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_POW, OP_NEG,
1056
+ OP_SIN, OP_COS, OP_TAN, OP_SQRT, OP_ABS, OP_EXP, OP_LN, OP_LOG10
1057
+ };
1058
+
1059
+ typedef struct {
1060
+ uint8_t op;
1061
+ q32_t val;
1062
+ } RpnInst;
1063
+
1064
+ typedef struct {
1065
+ RpnInst code[96];
1066
+ int n;
1067
+ int uses_x;
1068
+ } RpnProg;
1069
+
1070
+ static int rpn_prec(int op) {
1071
+ if (op >= OP_SIN) return 5;
1072
+ if (op == OP_POW) return 4;
1073
+ if (op == OP_NEG) return 3;
1074
+ if (op == OP_MUL || op == OP_DIV) return 2;
1075
+ return 1;
1076
+ }
1077
+
1078
+ static int compile_rpn(const char *s, RpnProg *prog) {
1079
+ prog->n = 0;
1080
+ prog->uses_x = 0;
1081
+ uint8_t op_stk[64];
1082
+ int no = 0, operand = 1;
1083
+
1084
+ while (*s) {
1085
+ if (is_space(*s)) { s++; continue; }
1086
+ if (prog->n >= 92 || no >= 60) return 0;
1087
+
1088
+ // Implicit multiplication when an atom/group just finished (`operand == 0`)
1089
+ // and the next token starts an operand (`x`, `pi`, `e`, `(`, or function name, or digit after `)`)
1090
+ if (!operand) {
1091
+ if (*s == '(' || is_alpha(*s)) {
1092
+ int pr = rpn_prec(OP_MUL);
1093
+ while (no && op_stk[no - 1] != '(' && rpn_prec(op_stk[no - 1]) >= pr)
1094
+ prog->code[prog->n++] = (RpnInst){op_stk[--no], 0};
1095
+ op_stk[no++] = OP_MUL;
1096
+ operand = 1;
1097
+ }
1098
+ }
1099
+
1100
+ if (operand) {
1101
+ if (*s == '(') {
1102
+ op_stk[no++] = '(';
1103
+ s++;
1104
+ } else if (*s == '-' || *s == '+') {
1105
+ if (*s == '-') op_stk[no++] = OP_NEG;
1106
+ s++;
1107
+ } else if (is_digit(*s) || (*s == '.' && is_digit(s[1]))) {
1108
+ const char *end = s;
1109
+ // Do not let parse_q32_num swallow `/denom` inside general expressions!
1110
+ uint64_t ip = 0;
1111
+ while (is_digit(*end)) {
1112
+ ip = ip * 10 + (uint64_t)(*end - '0');
1113
+ if (ip > 2147483647ULL) return 0;
1114
+ end++;
1115
+ }
1116
+ uint64_t fp_num = 0, fp_den = 1;
1117
+ if (*end == '.') {
1118
+ end++;
1119
+ while (is_digit(*end)) {
1120
+ if (fp_den < 100000000000ULL) {
1121
+ fp_num = fp_num * 10 + (uint64_t)(*end - '0');
1122
+ fp_den *= 10;
1123
+ }
1124
+ end++;
1125
+ }
1126
+ }
1127
+ uint64_t frac = 0;
1128
+ if (fp_num > 0) {
1129
+ uint64_t rem = fp_num;
1130
+ for (int i = 0; i < 32; i++) {
1131
+ rem <<= 1; frac <<= 1;
1132
+ if (rem >= fp_den) { rem -= fp_den; frac |= 1ULL; }
1133
+ }
1134
+ if ((rem << 1) >= fp_den) frac++;
1135
+ }
1136
+ prog->code[prog->n++] = (RpnInst){OP_NUM, (q32_t)((ip << 32) + frac)};
1137
+ s = end;
1138
+ operand = 0;
1139
+ } else if (is_alpha(*s)) {
1140
+ char id[16];
1141
+ int il = 0;
1142
+ while (is_alpha(*s) || is_digit(*s)) {
1143
+ if (il < 15) id[il++] = (*s >= 'A' && *s <= 'Z') ? (char)(*s + 32) : *s;
1144
+ s++;
1145
+ }
1146
+ id[il] = 0;
1147
+ while (*s == ' ') s++;
1148
+ if (streq(id, "x") || streq(id, "t")) {
1149
+ prog->code[prog->n++] = (RpnInst){OP_VAR_X, 0};
1150
+ prog->uses_x = 1;
1151
+ operand = 0;
1152
+ } else if (streq(id, "pi")) {
1153
+ prog->code[prog->n++] = (RpnInst){OP_NUM, Q32_PI};
1154
+ operand = 0;
1155
+ } else if (streq(id, "e")) {
1156
+ prog->code[prog->n++] = (RpnInst){OP_NUM, Q32_E};
1157
+ operand = 0;
1158
+ } else if (*s == '(') {
1159
+ uint8_t fn = 0;
1160
+ if (streq(id, "sin")) fn = OP_SIN;
1161
+ else if (streq(id, "cos")) fn = OP_COS;
1162
+ else if (streq(id, "tan")) fn = OP_TAN;
1163
+ else if (streq(id, "sqrt")) fn = OP_SQRT;
1164
+ else if (streq(id, "abs")) fn = OP_ABS;
1165
+ else if (streq(id, "exp")) fn = OP_EXP;
1166
+ else if (streq(id, "ln") || streq(id, "log")) fn = OP_LN;
1167
+ else if (streq(id, "log10")) fn = OP_LOG10;
1168
+ else return 0;
1169
+ op_stk[no++] = fn;
1170
+ op_stk[no++] = '(';
1171
+ s++; // consume '('
1172
+ } else {
1173
+ return 0;
1174
+ }
1175
+ } else {
1176
+ return 0;
1177
+ }
1178
+ continue;
1179
+ }
1180
+
1181
+ if (*s == ')') {
1182
+ while (no && op_stk[no - 1] != '(')
1183
+ prog->code[prog->n++] = (RpnInst){op_stk[--no], 0};
1184
+ if (!no) return 0;
1185
+ no--; // pop '('
1186
+ if (no && op_stk[no - 1] >= OP_SIN)
1187
+ prog->code[prog->n++] = (RpnInst){op_stk[--no], 0};
1188
+ s++;
1189
+ continue;
1190
+ }
1191
+
1192
+ uint8_t op = 0;
1193
+ int adv = 1;
1194
+ if (*s == '+') op = OP_ADD;
1195
+ else if (*s == '-') op = OP_SUB;
1196
+ else if (*s == '*' && s[1] == '*') { op = OP_POW; adv = 2; }
1197
+ else if (*s == '*') op = OP_MUL;
1198
+ else if (*s == '/') op = OP_DIV;
1199
+ else if (*s == '^') op = OP_POW;
1200
+ else return 0;
1201
+
1202
+ int pr = rpn_prec(op);
1203
+ while (no && op_stk[no - 1] != '(' &&
1204
+ (rpn_prec(op_stk[no - 1]) > pr || (rpn_prec(op_stk[no - 1]) == pr && op != OP_POW)))
1205
+ prog->code[prog->n++] = (RpnInst){op_stk[--no], 0};
1206
+ op_stk[no++] = op;
1207
+ s += adv;
1208
+ operand = 1;
1209
+ }
1210
+
1211
+ if (operand) return 0;
1212
+ while (no) {
1213
+ if (op_stk[no - 1] == '(') return 0;
1214
+ prog->code[prog->n++] = (RpnInst){op_stk[--no], 0};
1215
+ }
1216
+ return prog->n > 0;
1217
+ }
1218
+
1219
+ static q32_t eval_rpn(const RpnProg *prog, q32_t x_val, int *ok, char *step_buf, int step_cap) {
1220
+ q32_t stk[48];
1221
+ int sp = 0, step_pos = 0, step_cnt = 0;
1222
+ q_err = 0;
1223
+ if (step_buf && step_cap > 0) step_buf[0] = 0;
1224
+
1225
+ for (int i = 0; i < prog->n && !q_err; i++) {
1226
+ uint8_t op = prog->code[i].op;
1227
+ if (op == OP_NUM) {
1228
+ stk[sp++] = prog->code[i].val;
1229
+ } else if (op == OP_VAR_X) {
1230
+ stk[sp++] = x_val;
1231
+ } else if (op == OP_NEG || op >= OP_SIN) {
1232
+ if (sp < 1) { q_err = 1; break; }
1233
+ q32_t a = stk[sp - 1], r = 0, s_tmp, c_tmp;
1234
+ const char *fn_name = NULL;
1235
+ if (op == OP_NEG) r = -a;
1236
+ else if (op == OP_SIN) { q32_sincos(a, &s_tmp, &c_tmp); r = s_tmp; fn_name = "sin"; }
1237
+ else if (op == OP_COS) { q32_sincos(a, &s_tmp, &c_tmp); r = c_tmp; fn_name = "cos"; }
1238
+ else if (op == OP_TAN) {
1239
+ q32_sincos(a, &s_tmp, &c_tmp);
1240
+ if (c_tmp > -4096 && c_tmp < 4096) q_err = 1;
1241
+ else r = q32_div(s_tmp, c_tmp);
1242
+ fn_name = "tan";
1243
+ }
1244
+ else if (op == OP_SQRT) { r = q32_sqrt(a); fn_name = "sqrt"; }
1245
+ else if (op == OP_ABS) { r = a < 0 ? -a : a; fn_name = "abs"; }
1246
+ else if (op == OP_EXP) { r = q32_exp(a); fn_name = "exp"; }
1247
+ else if (op == OP_LN) { r = q32_ln(a); fn_name = "ln"; }
1248
+ else if (op == OP_LOG10) { r = q32_div(q32_ln(a), Q32_LN10); fn_name = "log10"; }
1249
+ stk[sp - 1] = r;
1250
+ if (step_buf && fn_name && !q_err && step_cnt < 8) {
1251
+ char ba[32], br[32];
1252
+ fmt_q32(a, ba, sizeof ba, 6);
1253
+ fmt_q32(r, br, sizeof br, 6);
1254
+ step_cnt++;
1255
+ append_str(step_buf, step_cap, &step_pos, "step ");
1256
+ append_char(step_buf, step_cap, &step_pos, (char)('0' + step_cnt));
1257
+ append_str(step_buf, step_cap, &step_pos, ": ");
1258
+ append_str(step_buf, step_cap, &step_pos, fn_name);
1259
+ append_char(step_buf, step_cap, &step_pos, '(');
1260
+ append_str(step_buf, step_cap, &step_pos, ba);
1261
+ append_str(step_buf, step_cap, &step_pos, ") = ");
1262
+ append_str(step_buf, step_cap, &step_pos, br);
1263
+ append_char(step_buf, step_cap, &step_pos, '\n');
1264
+ }
1265
+ } else {
1266
+ if (sp < 2) { q_err = 1; break; }
1267
+ q32_t b = stk[--sp], a = stk[sp - 1], r = 0;
1268
+ const char *op_s = "+";
1269
+ if (op == OP_ADD) { r = q32_add(a, b); op_s = "+"; }
1270
+ else if (op == OP_SUB) { r = q32_sub(a, b); op_s = "-"; }
1271
+ else if (op == OP_MUL) { r = q32_mul(a, b); op_s = "*"; }
1272
+ else if (op == OP_DIV) { r = q32_div(a, b); op_s = "/"; }
1273
+ else if (op == OP_POW) { r = q32_pow(a, b); op_s = "^"; }
1274
+ stk[sp - 1] = r;
1275
+ if (step_buf && !q_err && step_cnt < 8) {
1276
+ char ba[32], bb[32], br[32];
1277
+ fmt_q32(a, ba, sizeof ba, 6);
1278
+ fmt_q32(b, bb, sizeof bb, 6);
1279
+ fmt_q32(r, br, sizeof br, 6);
1280
+ step_cnt++;
1281
+ append_str(step_buf, step_cap, &step_pos, "step ");
1282
+ append_char(step_buf, step_cap, &step_pos, (char)('0' + step_cnt));
1283
+ append_str(step_buf, step_cap, &step_pos, ": ");
1284
+ append_str(step_buf, step_cap, &step_pos, ba);
1285
+ append_char(step_buf, step_cap, &step_pos, ' ');
1286
+ append_str(step_buf, step_cap, &step_pos, op_s);
1287
+ append_char(step_buf, step_cap, &step_pos, ' ');
1288
+ append_str(step_buf, step_cap, &step_pos, bb);
1289
+ append_str(step_buf, step_cap, &step_pos, " = ");
1290
+ append_str(step_buf, step_cap, &step_pos, br);
1291
+ append_char(step_buf, step_cap, &step_pos, '\n');
1292
+ }
1293
+ }
1294
+ }
1295
+ if (q_err || sp != 1) {
1296
+ if (ok) *ok = 0;
1297
+ return 0;
1298
+ }
1299
+ if (ok) *ok = 1;
1300
+ return stk[0];
1301
+ }
1302
+
1303
+ // Detect exact degree <= 2 polynomial f(x) = a*x^2 + b*x + c using 7-point verification.
1304
+ // Returns degree (0, 1, 2) or -1 if non-quadratic / nonlinear.
1305
+ static int detect_poly2(const RpnProg *prog, q32_t *out_a, q32_t *out_b, q32_t *out_c) {
1306
+ int ok0, ok1, okm1, ok2, okm2, okt1, okt2;
1307
+ q32_t y0 = eval_rpn(prog, 0, &ok0, NULL, 0);
1308
+ q32_t y1 = eval_rpn(prog, Q32_ONE, &ok1, NULL, 0);
1309
+ q32_t ym1 = eval_rpn(prog, -Q32_ONE, &okm1, NULL, 0);
1310
+ if (!ok0 || !ok1 || !okm1) return -1;
1311
+
1312
+ q_err = 0;
1313
+ q32_t c = y0;
1314
+ q32_t b = q32_sub(y1, ym1) >> 1;
1315
+ q32_t a = q32_sub(q32_add(y1, ym1), q32_add(y0, y0)) >> 1;
1316
+ if (q_err) return -1;
1317
+
1318
+ q32_t xs[4] = {2 * Q32_ONE, -2 * Q32_ONE, (11LL << 29), -(13LL << 30)}; // 2, -2, 1.375, -3.25
1319
+ int *oks[4] = {&ok2, &okm2, &okt1, &okt2};
1320
+ for (int i = 0; i < 4; i++) {
1321
+ q32_t y = eval_rpn(prog, xs[i], oks[i], NULL, 0);
1322
+ if (!*oks[i]) return -1;
1323
+ q_err = 0;
1324
+ q32_t pred = q32_add(q32_mul(q32_add(q32_mul(a, xs[i]), b), xs[i]), c);
1325
+ if (q_err) return -1;
1326
+ q32_t diff = y > pred ? y - pred : pred - y;
1327
+ if (diff > 256) return -1; // > 6e-8 mismatch -> higher degree or transcendental
1328
+ }
1329
+ if (a > -64 && a < 64) a = 0;
1330
+ if (b > -64 && b < 64) b = 0;
1331
+ if (c > -64 && c < 64) c = 0;
1332
+ *out_a = a;
1333
+ *out_b = b;
1334
+ *out_c = c;
1335
+ return a != 0 ? 2 : (b != 0 ? 1 : 0);
1336
+ }
1337
+
1338
+ static q32_t bisect_diff(const RpnProg *lhs, const RpnProg *rhs, q32_t rhs_const,
1339
+ q32_t xa, q32_t xb, q32_t ga) {
1340
+ for (int it = 0; it < 28; it++) {
1341
+ q32_t xm = (xa >> 1) + (xb >> 1) + ((xa & xb) & 1LL);
1342
+ int ok1 = 1, ok2 = 1;
1343
+ q32_t lv = eval_rpn(lhs, xm, &ok1, NULL, 0);
1344
+ q32_t rv = (rhs && rhs->uses_x) ? eval_rpn(rhs, xm, &ok2, NULL, 0) : rhs_const;
1345
+ if (!ok1 || !ok2) break;
1346
+ q32_t gm = lv - rv;
1347
+ if (gm == 0) return xm;
1348
+ if ((ga < 0 && gm < 0) || (ga > 0 && gm > 0)) {
1349
+ xa = xm;
1350
+ ga = gm;
1351
+ } else {
1352
+ xb = xm;
1353
+ }
1354
+ }
1355
+ return (xa >> 1) + (xb >> 1);
1356
+ }
1357
+
1358
+ static void add_unique_root(q32_t *roots, int *nroots, int cap, q32_t r) {
1359
+ for (int i = 0; i < *nroots; i++) {
1360
+ q32_t d = roots[i] > r ? roots[i] - r : r - roots[i];
1361
+ if (d < 400000LL) return; // < 1e-4 duplicate
1362
+ }
1363
+ if (*nroots < cap) {
1364
+ int pos = *nroots;
1365
+ while (pos > 0 && roots[pos - 1] > r) {
1366
+ roots[pos] = roots[pos - 1];
1367
+ pos--;
1368
+ }
1369
+ roots[pos] = r;
1370
+ (*nroots)++;
1371
+ }
1372
+ }
1373
+
1374
+ // --- Equation Solver Mode (`LHS = RHS`) ---
1375
+ static int run_solver_mode(const char *full_eq, const char *lhs_s, const char *rhs_s,
1376
+ int has_bounds, q32_t xmin, q32_t xmax) {
1377
+ RpnProg lhs, rhs;
1378
+ if (!compile_rpn(lhs_s, &lhs) || !compile_rpn(rhs_s, &rhs)) return 1;
1379
+
1380
+ puts_raw("equation: ");
1381
+ puts_raw(full_eq);
1382
+ rv_putchar('\n');
1383
+
1384
+ // Build combined difference program g(x) = LHS(x) - RHS(x) if small enough for poly detection
1385
+ RpnProg diff = lhs;
1386
+ int can_poly = (lhs.n + rhs.n + 1 <= 92);
1387
+ if (can_poly) {
1388
+ for (int i = 0; i < rhs.n; i++) diff.code[diff.n++] = rhs.code[i];
1389
+ diff.code[diff.n++] = (RpnInst){OP_SUB, 0};
1390
+ diff.uses_x = lhs.uses_x || rhs.uses_x;
1391
+ }
1392
+
1393
+ q32_t pa = 0, pb = 0, pc = 0;
1394
+ int deg = (!has_bounds && can_poly) ? detect_poly2(&diff, &pa, &pb, &pc) : -1;
1395
+
1396
+ char buf1[48], buf2[48];
1397
+ if (deg == 0) {
1398
+ puts_raw(pc == 0 ? "identity: true for all real x\n" : "no solution (contradiction)\n");
1399
+ return 0;
1400
+ }
1401
+ if (deg == 1) {
1402
+ q_err = 0;
1403
+ q32_t root = q32_div(-pc, pb);
1404
+ fmt_q32(pb, buf1, sizeof buf1, 6);
1405
+ fmt_q32(pc, buf2, sizeof buf2, 6);
1406
+ puts_raw("linear form: ");
1407
+ puts_raw(buf1);
1408
+ puts_raw("*x + (");
1409
+ puts_raw(buf2);
1410
+ puts_raw(") = 0\n");
1411
+ fmt_q32_rich(root, buf1, sizeof buf1);
1412
+ puts_raw("exact solution: x = ");
1413
+ puts_raw(buf1);
1414
+ rv_putchar('\n');
1415
+ int ok1, ok2;
1416
+ q32_t lv = eval_rpn(&lhs, root, &ok1, NULL, 0);
1417
+ q32_t rv = eval_rpn(&rhs, root, &ok2, NULL, 0);
1418
+ if (ok1 && ok2) {
1419
+ fmt_q32(lv, buf1, sizeof buf1, 6);
1420
+ fmt_q32(rv, buf2, sizeof buf2, 6);
1421
+ puts_raw("check: LHS = ");
1422
+ puts_raw(buf1);
1423
+ puts_raw(", RHS = ");
1424
+ puts_raw(buf2);
1425
+ rv_putchar('\n');
1426
+ }
1427
+ return 0;
1428
+ }
1429
+ if (deg == 2) {
1430
+ q_err = 0;
1431
+ q32_t disc = q32_sub(q32_mul(pb, pb), 4 * q32_mul(pa, pc));
1432
+ char ba[32], bb[32], bc[32], bd[32];
1433
+ fmt_q32(pa, ba, sizeof ba, 6);
1434
+ fmt_q32(pb, bb, sizeof bb, 6);
1435
+ fmt_q32(pc, bc, sizeof bc, 6);
1436
+ fmt_q32(disc, bd, sizeof bd, 6);
1437
+ puts_raw("quadratic form: (");
1438
+ puts_raw(ba);
1439
+ puts_raw(")*x^2 + (");
1440
+ puts_raw(bb);
1441
+ puts_raw(")*x + (");
1442
+ puts_raw(bc);
1443
+ puts_raw(") = 0\ndiscriminant: D = ");
1444
+ puts_raw(bd);
1445
+ rv_putchar('\n');
1446
+ q32_t two_a = 2 * pa;
1447
+ if (disc < -128) {
1448
+ q32_t re = q32_div(-pb, two_a);
1449
+ q32_t im = q32_div(q32_sqrt(-disc), two_a < 0 ? -two_a : two_a);
1450
+ fmt_q32(re, buf1, sizeof buf1, 6);
1451
+ fmt_q32(im, buf2, sizeof buf2, 6);
1452
+ puts_raw("real solutions: none (D < 0)\ncomplex solutions: x = ");
1453
+ puts_raw(buf1);
1454
+ puts_raw(" - ");
1455
+ puts_raw(buf2);
1456
+ puts_raw("*i, x = ");
1457
+ puts_raw(buf1);
1458
+ puts_raw(" + ");
1459
+ puts_raw(buf2);
1460
+ puts_raw("*i\n");
1461
+ return 0;
1462
+ }
1463
+ if (disc <= 128) {
1464
+ q32_t r = q32_div(-pb, two_a);
1465
+ fmt_q32_rich(r, buf1, sizeof buf1);
1466
+ puts_raw("double root: x = ");
1467
+ puts_raw(buf1);
1468
+ rv_putchar('\n');
1469
+ return 0;
1470
+ }
1471
+ q32_t sd = q32_sqrt(disc);
1472
+ q32_t r1 = q32_div(-pb - sd, two_a);
1473
+ q32_t r2 = q32_div(-pb + sd, two_a);
1474
+ if (r1 > r2) { q32_t t = r1; r1 = r2; r2 = t; }
1475
+ fmt_q32_rich(r1, buf1, sizeof buf1);
1476
+ fmt_q32_rich(r2, buf2, sizeof buf2);
1477
+ puts_raw("solutions: x = ");
1478
+ puts_raw(buf1);
1479
+ puts_raw(", x = ");
1480
+ puts_raw(buf2);
1481
+ rv_putchar('\n');
1482
+ int ok1, ok2;
1483
+ q32_t lv = eval_rpn(&lhs, r1, &ok1, NULL, 0);
1484
+ q32_t rv = eval_rpn(&rhs, r1, &ok2, NULL, 0);
1485
+ if (ok1 && ok2) {
1486
+ fmt_q32(lv, buf1, sizeof buf1, 6);
1487
+ fmt_q32(rv, buf2, sizeof buf2, 6);
1488
+ puts_raw("check (at x1): LHS = ");
1489
+ puts_raw(buf1);
1490
+ puts_raw(", RHS = ");
1491
+ puts_raw(buf2);
1492
+ rv_putchar('\n');
1493
+ }
1494
+ return 0;
1495
+ }
1496
+
1497
+ // General nonlinear equation scan + 28-step bisection
1498
+ q32_t lo = has_bounds ? xmin : -(20LL << 32);
1499
+ q32_t hi = has_bounds ? xmax : (20LL << 32);
1500
+ int rhs_const_ok = 1;
1501
+ q32_t rhs_const = rhs.uses_x ? 0 : eval_rpn(&rhs, 0, &rhs_const_ok, NULL, 0);
1502
+ q32_t roots[8];
1503
+ int nroots = 0;
1504
+ for (int pass = 0; pass < (has_bounds ? 1 : 2) && nroots == 0; pass++) {
1505
+ if (pass == 1) { lo = -(100LL << 32); hi = 100LL << 32; }
1506
+ const int N = 64;
1507
+ q32_t step = (hi - lo) >> 6;
1508
+ q32_t px = lo, pg = 0;
1509
+ int pok = 0;
1510
+ for (int i = 0; i <= N; i++) {
1511
+ q32_t x = (i == N) ? hi : (lo + i * step);
1512
+ int ok1, ok2 = rhs_const_ok;
1513
+ q32_t lv = eval_rpn(&lhs, x, &ok1, NULL, 0);
1514
+ q32_t rv = rhs.uses_x ? eval_rpn(&rhs, x, &ok2, NULL, 0) : rhs_const;
1515
+ int ok = ok1 && ok2;
1516
+ q32_t g = ok ? (lv - rv) : 0;
1517
+ if (ok && g > -256 && g < 256) {
1518
+ add_unique_root(roots, &nroots, 8, x);
1519
+ } else if (ok && pok && ((pg < 0 && g > 0) || (pg > 0 && g < 0))) {
1520
+ q32_t r = bisect_diff(&lhs, &rhs, rhs_const, px, x, pg);
1521
+ // Reject vertical asymptotes (e.g. 1/x = 0)
1522
+ int okc1, okc2 = rhs_const_ok;
1523
+ q32_t cl = eval_rpn(&lhs, r, &okc1, NULL, 0);
1524
+ q32_t cr = rhs.uses_x ? eval_rpn(&rhs, r, &okc2, NULL, 0) : rhs_const;
1525
+ q32_t rem = (okc1 && okc2) ? (cl > cr ? cl - cr : cr - cl) : Q32_ONE;
1526
+ if (okc1 && okc2 && rem < (Q32_ONE >> 10))
1527
+ add_unique_root(roots, &nroots, 8, r);
1528
+ }
1529
+ px = x; pg = g; pok = ok;
1530
+ }
1531
+ }
1532
+ if (nroots == 0) {
1533
+ puts_raw("solutions on search interval: none found\n");
1534
+ } else {
1535
+ puts_raw("solutions: ");
1536
+ for (int i = 0; i < nroots; i++) {
1537
+ if (i > 0) puts_raw(", ");
1538
+ puts_raw("x = ");
1539
+ fmt_q32_rich(roots[i], buf1, sizeof buf1);
1540
+ puts_raw(buf1);
1541
+ }
1542
+ rv_putchar('\n');
1543
+ int ok1, ok2;
1544
+ q32_t lv = eval_rpn(&lhs, roots[0], &ok1, NULL, 0);
1545
+ q32_t rv = eval_rpn(&rhs, roots[0], &ok2, NULL, 0);
1546
+ if (ok1 && ok2) {
1547
+ fmt_q32(lv, buf1, sizeof buf1, 6);
1548
+ fmt_q32(rv, buf2, sizeof buf2, 6);
1549
+ puts_raw("check: LHS = ");
1550
+ puts_raw(buf1);
1551
+ puts_raw(", RHS = ");
1552
+ puts_raw(buf2);
1553
+ rv_putchar('\n');
1554
+ }
1555
+ }
1556
+ return 0;
1557
+ }
1558
+
1559
+ // Golden-section polish for local extremum on [a, b] (`find_max` = 1 for max, 0 for min)
1560
+ static q32_t polish_extremum(const RpnProg *prog, q32_t a, q32_t b, int find_max, q32_t *out_y) {
1561
+ // inv_phi = (sqrt(5) - 1) / 2 = 0.61803398875 in Q32.32 = 2654435769LL
1562
+ const q32_t INV_PHI = 2654435769LL;
1563
+ q32_t c = b - q32_mul(b - a, INV_PHI);
1564
+ q32_t d = a + q32_mul(b - a, INV_PHI);
1565
+ int okc, okd;
1566
+ q32_t yc = eval_rpn(prog, c, &okc, NULL, 0);
1567
+ q32_t yd = eval_rpn(prog, d, &okd, NULL, 0);
1568
+ for (int it = 0; it < 15 && okc && okd; it++) {
1569
+ int pick_left = find_max ? (yc > yd) : (yc < yd);
1570
+ if (pick_left) {
1571
+ b = d; d = c; yd = yc;
1572
+ c = b - q32_mul(b - a, INV_PHI);
1573
+ yc = eval_rpn(prog, c, &okc, NULL, 0);
1574
+ } else {
1575
+ a = c; c = d; yc = yd;
1576
+ d = a + q32_mul(b - a, INV_PHI);
1577
+ yd = eval_rpn(prog, d, &okd, NULL, 0);
1578
+ }
1579
+ }
1580
+ q32_t xm = (a >> 1) + (b >> 1);
1581
+ int okm, okp, okn;
1582
+ q32_t ym = eval_rpn(prog, xm, &okm, NULL, 0);
1583
+ // Final 3-point parabolic vertex polish (`dh = 2^-11`) for exact 6-decimal extrema
1584
+ q32_t dh = (1LL << 21);
1585
+ q32_t yp = eval_rpn(prog, xm + dh, &okp, NULL, 0);
1586
+ q32_t yn = eval_rpn(prog, xm - dh, &okn, NULL, 0);
1587
+ if (okm && okp && okn) {
1588
+ q32_t denom = 2 * ((yp - 2 * ym) + yn);
1589
+ if (denom > 1024 || denom < -1024) {
1590
+ int save = q_err;
1591
+ q_err = 0;
1592
+ q32_t delta = q32_mul(dh, q32_div(yn - yp, denom));
1593
+ if (!q_err && delta > -dh && delta < dh) {
1594
+ xm += delta;
1595
+ ym = eval_rpn(prog, xm, &okm, NULL, 0);
1596
+ }
1597
+ q_err = save;
1598
+ }
1599
+ }
1600
+ *out_y = ym;
1601
+ return xm;
1602
+ }
1603
+
1604
+ // --- Graphing Calculator & Table Mode (`y = f(x)`) ---
1605
+ static int run_graph_mode(const char *expr_s, const RpnProg *prog, q32_t xmin, q32_t xmax, int table_only) {
1606
+ if (xmax <= xmin) xmax = xmin + (10LL << 32);
1607
+
1608
+ // 48 subintervals -> 49 grid points:
1609
+ // 48 = 2 * 24 (exact 25 plot columns at i = 2*col)
1610
+ // 48 = 6 * 8 (exact 7 table points at i = 8*k)
1611
+ // 48 is even (exact 48-panel Simpson's rule definite integral!)
1612
+ static q32_t gx[49], gy[49];
1613
+ static uint8_t gok[49];
1614
+ q32_t span = xmax - xmin;
1615
+ q32_t h = q32_div(span, 48 * Q32_ONE);
1616
+
1617
+ q32_t ymin = Q32_MAX, ymax = Q32_MIN;
1618
+ int valid_cnt = 0, all_valid = 1;
1619
+ for (int i = 0; i <= 48; i++) {
1620
+ gx[i] = (i == 48) ? xmax : (xmin + i * h);
1621
+ int ok = 0;
1622
+ gy[i] = eval_rpn(prog, gx[i], &ok, NULL, 0);
1623
+ gok[i] = (uint8_t)ok;
1624
+ if (ok) {
1625
+ if (gy[i] < ymin) ymin = gy[i];
1626
+ if (gy[i] > ymax) ymax = gy[i];
1627
+ valid_cnt++;
1628
+ } else {
1629
+ all_valid = 0;
1630
+ }
1631
+ }
1632
+
1633
+ char b1[40], b2[40], b3[40];
1634
+ fmt_q32(xmin, b1, sizeof b1, 4);
1635
+ fmt_q32(xmax, b2, sizeof b2, 4);
1636
+ puts_raw("function: f(x) = ");
1637
+ puts_raw(expr_s);
1638
+ puts_raw(" on [");
1639
+ puts_raw(b1);
1640
+ puts_raw(", ");
1641
+ puts_raw(b2);
1642
+ puts_raw("]\n");
1643
+
1644
+ if (valid_cnt == 0) {
1645
+ puts_raw("error: function undefined on entire interval\n");
1646
+ return 0;
1647
+ }
1648
+
1649
+ // 1. Roots on [xmin, xmax]
1650
+ q32_t roots[8];
1651
+ int nroots = 0;
1652
+ q32_t pa = 0, pb = 0, pc = 0;
1653
+ int deg = detect_poly2(prog, &pa, &pb, &pc);
1654
+ if (deg == 1) {
1655
+ q32_t r = q32_div(-pc, pb);
1656
+ if (r >= xmin - 1024 && r <= xmax + 1024) add_unique_root(roots, &nroots, 8, r);
1657
+ } else if (deg == 2) {
1658
+ q32_t disc = q32_sub(q32_mul(pb, pb), 4 * q32_mul(pa, pc));
1659
+ if (disc >= -128) {
1660
+ q32_t sd = disc > 0 ? q32_sqrt(disc) : 0;
1661
+ q32_t r1 = q32_div(-pb - sd, 2 * pa);
1662
+ q32_t r2 = q32_div(-pb + sd, 2 * pa);
1663
+ if (r1 >= xmin - 1024 && r1 <= xmax + 1024) add_unique_root(roots, &nroots, 8, r1);
1664
+ if (r2 >= xmin - 1024 && r2 <= xmax + 1024) add_unique_root(roots, &nroots, 8, r2);
1665
+ }
1666
+ } else {
1667
+ for (int i = 0; i <= 48; i++) {
1668
+ if (gok[i] && gy[i] > -512 && gy[i] < 512) {
1669
+ add_unique_root(roots, &nroots, 8, gx[i]);
1670
+ } else if (i < 48 && gok[i] && gok[i + 1] &&
1671
+ ((gy[i] < 0 && gy[i + 1] > 0) || (gy[i] > 0 && gy[i + 1] < 0))) {
1672
+ q32_t r = bisect_diff(prog, NULL, 0, gx[i], gx[i + 1], gy[i]);
1673
+ int okr;
1674
+ q32_t yr = eval_rpn(prog, r, &okr, NULL, 0);
1675
+ q32_t ay = yr < 0 ? -yr : yr;
1676
+ if (okr && ay < (Q32_ONE >> 10))
1677
+ add_unique_root(roots, &nroots, 8, r);
1678
+ }
1679
+ }
1680
+ }
1681
+ puts_raw("roots: ");
1682
+ if (nroots == 0) {
1683
+ puts_raw("none on interval\n");
1684
+ } else {
1685
+ for (int i = 0; i < nroots; i++) {
1686
+ if (i > 0) puts_raw(", ");
1687
+ puts_raw("x = ");
1688
+ fmt_q32_rich(roots[i], b1, sizeof b1);
1689
+ puts_raw(b1);
1690
+ }
1691
+ rv_putchar('\n');
1692
+ }
1693
+
1694
+ // 2. Vertex / Local Extrema
1695
+ if (deg == 2) {
1696
+ q32_t vx = q32_div(-pb, 2 * pa);
1697
+ int okv;
1698
+ q32_t vy = eval_rpn(prog, vx, &okv, NULL, 0);
1699
+ if (okv && vy < ymin && vx >= xmin && vx <= xmax) ymin = vy;
1700
+ if (okv && vy > ymax && vx >= xmin && vx <= xmax) ymax = vy;
1701
+ fmt_q32(vx, b1, sizeof b1, 6);
1702
+ fmt_q32(vy, b2, sizeof b2, 6);
1703
+ puts_raw(pa > 0 ? "vertex (min): (" : "vertex (max): (");
1704
+ puts_raw(b1);
1705
+ puts_raw(", ");
1706
+ puts_raw(b2);
1707
+ puts_raw(")\n");
1708
+ } else {
1709
+ int printed_ext = 0;
1710
+ for (int i = 1; i < 48 && printed_ext < 3; i++) {
1711
+ if (!gok[i - 1] || !gok[i] || !gok[i + 1]) continue;
1712
+ int is_max = (gy[i] > gy[i - 1] && gy[i] >= gy[i + 1] && (gy[i] > gy[i - 1] || gy[i] > gy[i + 1]));
1713
+ int is_min = (gy[i] < gy[i - 1] && gy[i] <= gy[i + 1] && (gy[i] < gy[i - 1] || gy[i] < gy[i + 1]));
1714
+ if (is_max || is_min) {
1715
+ q32_t ey = 0;
1716
+ q32_t ex = polish_extremum(prog, gx[i - 1], gx[i + 1], is_max, &ey);
1717
+ if (ey < ymin) ymin = ey;
1718
+ if (ey > ymax) ymax = ey;
1719
+ if (printed_ext > 0) puts_raw(" | ");
1720
+ puts_raw(is_max ? "local max: (" : "local min: (");
1721
+ fmt_q32(ex, b1, sizeof b1, 6);
1722
+ fmt_q32(ey, b2, sizeof b2, 6);
1723
+ puts_raw(b1);
1724
+ puts_raw(", ");
1725
+ puts_raw(b2);
1726
+ rv_putchar(')');
1727
+ printed_ext++;
1728
+ }
1729
+ }
1730
+ if (printed_ext > 0) rv_putchar('\n');
1731
+ }
1732
+
1733
+ // 3. y-intercept & 96-panel Simpson's definite integral
1734
+ int has_yint = 0;
1735
+ if (xmin <= 0 && xmax >= 0) {
1736
+ int ok0;
1737
+ q32_t y0 = eval_rpn(prog, 0, &ok0, NULL, 0);
1738
+ if (ok0) {
1739
+ fmt_q32(y0, b1, sizeof b1, 6);
1740
+ puts_raw("y-intercept: (0, ");
1741
+ puts_raw(b1);
1742
+ rv_putchar(')');
1743
+ has_yint = 1;
1744
+ }
1745
+ }
1746
+ if (all_valid) {
1747
+ q_err = 0;
1748
+ q32_t sum = q32_add(gy[0], gy[48]);
1749
+ for (int i = 1; i < 48 && !q_err; i++) {
1750
+ q32_t term = (i & 1) ? q32_mul(4 * Q32_ONE, gy[i]) : q32_mul(2 * Q32_ONE, gy[i]);
1751
+ sum = q32_add(sum, term);
1752
+ }
1753
+ q32_t integ = q32_mul(q32_div(h, 3 * Q32_ONE), sum);
1754
+ if (!q_err) {
1755
+ if (has_yint) puts_raw(" | ");
1756
+ puts_raw("integral: ");
1757
+ fmt_q32(integ, b1, sizeof b1, 6);
1758
+ puts_raw(b1);
1759
+ has_yint = 1;
1760
+ }
1761
+ }
1762
+ if (has_yint) rv_putchar('\n');
1763
+
1764
+ // 4. Coordinate Table (7 clean points; with derivative f'(x) when in table mode)
1765
+ if (table_only) {
1766
+ puts_raw("x | f(x) | f'(x)\n");
1767
+ q32_t dh = (1LL << 20); // 2^-12 (~2.44e-4) central difference step
1768
+ for (int k = 0; k <= 6; k++) {
1769
+ int idx = 8 * k;
1770
+ fmt_q32(gx[idx], b1, sizeof b1, 4);
1771
+ puts_raw(b1);
1772
+ puts_raw(" | ");
1773
+ if (!gok[idx]) {
1774
+ puts_raw("undef | undef\n");
1775
+ } else {
1776
+ fmt_q32(gy[idx], b2, sizeof b2, 6);
1777
+ puts_raw(b2);
1778
+ puts_raw(" | ");
1779
+ int okp, okm;
1780
+ q32_t yp = eval_rpn(prog, gx[idx] + dh, &okp, NULL, 0);
1781
+ q32_t ym = eval_rpn(prog, gx[idx] - dh, &okm, NULL, 0);
1782
+ if (okp && okm) {
1783
+ q32_t deriv = (yp - ym) << 11; // / (2 * 2^-12) = * 2^11
1784
+ fmt_q32(deriv, b3, sizeof b3, 6);
1785
+ puts_raw(b3);
1786
+ } else {
1787
+ puts_raw("undef");
1788
+ }
1789
+ rv_putchar('\n');
1790
+ }
1791
+ }
1792
+ return 0;
1793
+ }
1794
+
1795
+ puts_raw("table: ");
1796
+ for (int k = 0; k <= 6; k++) {
1797
+ int idx = 8 * k;
1798
+ if (k > 0) puts_raw(", ");
1799
+ rv_putchar('(');
1800
+ fmt_q32(gx[idx], b1, sizeof b1, 4);
1801
+ puts_raw(b1);
1802
+ puts_raw(", ");
1803
+ if (gok[idx]) {
1804
+ fmt_q32(gy[idx], b2, sizeof b2, 4);
1805
+ puts_raw(b2);
1806
+ } else {
1807
+ puts_raw("undef");
1808
+ }
1809
+ rv_putchar(')');
1810
+ }
1811
+ rv_putchar('\n');
1812
+
1813
+ // 5. 9x25 ASCII Coordinate Plot
1814
+ q32_t pmin = ymin, pmax = ymax;
1815
+ // Clamp extreme asymptote spikes so e.g. 1/x or tan(x) remains readable
1816
+ if (pmax - pmin > (200LL << 32) && !all_valid) {
1817
+ pmin = -(20LL << 32);
1818
+ pmax = 20LL << 32;
1819
+ }
1820
+ if (pmax - pmin < (1LL << 28)) {
1821
+ pmin -= Q32_ONE;
1822
+ pmax += Q32_ONE;
1823
+ }
1824
+ q32_t yspan = pmax - pmin;
1825
+
1826
+ char grid[9][25];
1827
+ for (int r = 0; r < 9; r++)
1828
+ for (int c = 0; c < 25; c++) grid[r][c] = ' ';
1829
+
1830
+ int rx = -1, cy = -1;
1831
+ q32_t inv_yspan8 = q32_div(8 * Q32_ONE, yspan);
1832
+ if (pmin <= 0 && pmax >= 0) {
1833
+ rx = (int)((q32_mul(pmax, inv_yspan8) + (1LL << 31)) >> 32);
1834
+ if (rx >= 0 && rx < 9)
1835
+ for (int c = 0; c < 25; c++) grid[rx][c] = '-';
1836
+ }
1837
+ if (xmin <= 0 && xmax >= 0) {
1838
+ cy = (int)((q32_div(-xmin, span) * 24 + (1LL << 31)) >> 32);
1839
+ if (cy >= 0 && cy < 25)
1840
+ for (int r = 0; r < 9; r++) grid[r][cy] = '|';
1841
+ }
1842
+ if (rx >= 0 && rx < 9 && cy >= 0 && cy < 25) grid[rx][cy] = '+';
1843
+
1844
+ for (int c = 0; c < 25; c++) {
1845
+ int idx = 2 * c;
1846
+ if (!gok[idx] || gy[idx] < pmin || gy[idx] > pmax) continue;
1847
+ int r = (int)((q32_mul(pmax - gy[idx], inv_yspan8) + (1LL << 31)) >> 32);
1848
+ if (r >= 0 && r < 9) grid[r][c] = '*';
1849
+ }
1850
+
1851
+ for (int r = 0; r < 9; r++) {
1852
+ if ((r & 1) == 0 || r == rx) {
1853
+ q32_t ytick = (r == rx) ? 0 : (pmax - (yspan * r) / 8);
1854
+ fmt_q32(ytick, b1, sizeof b1, 2);
1855
+ int l = slen(b1);
1856
+ putc_rep(' ', l < 7 ? 7 - l : 0);
1857
+ puts_raw(b1);
1858
+ } else {
1859
+ putc_rep(' ', 7);
1860
+ }
1861
+ puts_raw(" |");
1862
+ int endc = 25;
1863
+ while (endc > 0 && grid[r][endc - 1] == ' ') endc--;
1864
+ for (int c = 0; c < endc; c++) rv_putchar(grid[r][c]);
1865
+ rv_putchar('\n');
1866
+ }
1867
+ // Bottom x-axis tick line
1868
+ fmt_q32(xmin, b1, sizeof b1, 2);
1869
+ fmt_q32((xmin >> 1) + (xmax >> 1), b2, sizeof b2, 2);
1870
+ fmt_q32(xmax, b3, sizeof b3, 2);
1871
+ puts_raw(" x=");
1872
+ puts_raw(b1);
1873
+ int used_w = 2 + slen(b1);
1874
+ int mid_target = 11 - (2 + slen(b2)) / 2;
1875
+ if (mid_target <= used_w) mid_target = used_w + 1;
1876
+ putc_rep(' ', mid_target - used_w);
1877
+ puts_raw("x=");
1878
+ puts_raw(b2);
1879
+ used_w = mid_target + 2 + slen(b2);
1880
+ int right_target = 25 - (2 + slen(b3));
1881
+ if (right_target <= used_w) right_target = used_w + 1;
1882
+ putc_rep(' ', right_target - used_w);
1883
+ puts_raw("x=");
1884
+ puts_raw(b3);
1885
+ rv_putchar('\n');
1886
+ return 0;
1887
+ }
1888
+
1889
+ // ============================================================================
1890
+ // Tool-Call Gathering, UTF-8 Normalization & Mode Dispatcher
1891
+ // ============================================================================
1892
+
1893
+ static char raw_call[1024];
1894
+ static char norm_call[1024];
1895
+ static char fn_buf[32];
1896
+ static char expr_buf[256];
1897
+ static char xmin_buf[64];
1898
+ static char xmax_buf[64];
1899
+ static int has_xmin, has_xmax;
1900
+
1901
+ static void normalize_utf8(const char *src, char *dst, int cap) {
1902
+ int o = 0;
1903
+ for (int i = 0; src[i] && o < cap - 4; ) {
1904
+ unsigned char c0 = (unsigned char)src[i];
1905
+ unsigned char c1 = (unsigned char)src[i + 1];
1906
+ unsigned char c2 = c1 ? (unsigned char)src[i + 2] : 0;
1907
+ if (c0 == 0xC3 && c1 == 0x97) { dst[o++] = '*'; i += 2; } // ×
1908
+ else if (c0 == 0xC3 && c1 == 0xB7) { dst[o++] = '/'; i += 2; } // ÷
1909
+ else if (c0 == 0xC2 && c1 == 0xB2) { dst[o++] = '^'; dst[o++] = '2'; i += 2; } // ²
1910
+ else if (c0 == 0xC2 && c1 == 0xB3) { dst[o++] = '^'; dst[o++] = '3'; i += 2; } // ³
1911
+ else if (c0 == 0xCF && c1 == 0x80) { dst[o++] = 'p'; dst[o++] = 'i'; i += 2; } // π
1912
+ else if (c0 == 0xE2 && c1 == 0x88 && c2 == 0x92) { dst[o++] = '-'; i += 3; } // −
1913
+ else if (c0 == 0xE2 && c1 == 0x80 && c2 == 0x93) { dst[o++] = '-'; i += 3; } // –
1914
+ else if (c0 == '&' && starts_with_ci(src + i, "&lt;")) { dst[o++] = '<'; i += 4; }
1915
+ else if (c0 == '&' && starts_with_ci(src + i, "&gt;")) { dst[o++] = '>'; i += 4; }
1916
+ else { dst[o++] = src[i++]; }
1917
+ }
1918
+ dst[o] = 0;
1919
+ }
1920
+
1921
+ static void parse_call_payload(void) {
1922
+ normalize_utf8(raw_call, norm_call, sizeof norm_call);
1923
+ fn_buf[0] = 0;
1924
+ expr_buf[0] = 0;
1925
+ xmin_buf[0] = 0;
1926
+ xmax_buf[0] = 0;
1927
+ has_xmin = 0;
1928
+ has_xmax = 0;
1929
+
1930
+ int saw_param = 0;
1931
+ for (int i = 0; norm_call[i]; i++) {
1932
+ if (starts_with_ci(norm_call + i, "<function=")) {
1933
+ int s = i + 10, e = s;
1934
+ while (norm_call[e] && norm_call[e] != '>' && !is_space(norm_call[e])) e++;
1935
+ copy_trim(fn_buf, sizeof fn_buf, norm_call + s, e - s);
1936
+ } else if (starts_with_ci(norm_call + i, "<parameter=")) {
1937
+ saw_param = 1;
1938
+ int ks = i + 11, ke = ks;
1939
+ while (norm_call[ke] && norm_call[ke] != '>') ke++;
1940
+ if (!norm_call[ke]) break;
1941
+ char key[24];
1942
+ copy_trim(key, sizeof key, norm_call + ks, ke - ks);
1943
+ for (int k = 0; key[k]; k++) if (key[k] >= 'A' && key[k] <= 'Z') key[k] = (char)(key[k] + 32);
1944
+ int vs = ke + 1, ve = vs;
1945
+ while (norm_call[ve] && norm_call[ve] != '<') ve++;
1946
+ if (starts_with_ci(key, "expr") || starts_with_ci(key, "eq") ||
1947
+ starts_with_ci(key, "func") || starts_with_ci(key, "cmd") || streq(key, "f")) {
1948
+ copy_trim(expr_buf, sizeof expr_buf, norm_call + vs, ve - vs);
1949
+ } else if (streq(key, "xmin") || streq(key, "min") || streq(key, "a") || streq(key, "from")) {
1950
+ copy_trim(xmin_buf, sizeof xmin_buf, norm_call + vs, ve - vs);
1951
+ has_xmin = xmin_buf[0] != 0;
1952
+ } else if (streq(key, "xmax") || streq(key, "max") || streq(key, "b") || streq(key, "to")) {
1953
+ copy_trim(xmax_buf, sizeof xmax_buf, norm_call + vs, ve - vs);
1954
+ has_xmax = xmax_buf[0] != 0;
1955
+ } else if (streq(key, "mode")) {
1956
+ copy_trim(fn_buf, sizeof fn_buf, norm_call + vs, ve - vs);
1957
+ } else if (!expr_buf[0]) {
1958
+ copy_trim(expr_buf, sizeof expr_buf, norm_call + vs, ve - vs);
1959
+ }
1960
+ i = ve - 1;
1961
+ }
1962
+ }
1963
+
1964
+ if (!saw_param) {
1965
+ // Fallback: JSON {"expression": "..."} or plain text expression
1966
+ int found_json = 0;
1967
+ for (int i = 0; norm_call[i]; i++) {
1968
+ if (starts_with_ci(norm_call + i, "\"expression\"") || starts_with_ci(norm_call + i, "\"equation\"")) {
1969
+ int j = i + 12;
1970
+ while (norm_call[j] && norm_call[j] != ':') j++;
1971
+ if (norm_call[j] == ':') j++;
1972
+ while (is_space(norm_call[j])) j++;
1973
+ if (norm_call[j] == '"') {
1974
+ int vs = ++j;
1975
+ while (norm_call[j] && norm_call[j] != '"') j++;
1976
+ copy_trim(expr_buf, sizeof expr_buf, norm_call + vs, j - vs);
1977
+ found_json = 1;
1978
+ break;
1979
+ }
1980
+ }
1981
+ }
1982
+ if (!found_json) copy_trim(expr_buf, sizeof expr_buf, norm_call, slen(norm_call));
1983
+ }
1984
+
1985
+ // Strip optional inline domain suffix: " on [a, b]" or " from x = a to b" or " from a to b"
1986
+ int n = slen(expr_buf);
1987
+ for (int i = 0; i < n; i++) {
1988
+ if (!has_xmin && (starts_with_ci(expr_buf + i, " on [") || starts_with_ci(expr_buf + i, " in ["))) {
1989
+ int lb = i + 5, cm = lb;
1990
+ while (expr_buf[cm] && expr_buf[cm] != ',' && expr_buf[cm] != ']') cm++;
1991
+ if (expr_buf[cm] == ',') {
1992
+ int rb = cm + 1;
1993
+ while (expr_buf[rb] && expr_buf[rb] != ']') rb++;
1994
+ if (expr_buf[rb] == ']') {
1995
+ copy_trim(xmin_buf, sizeof xmin_buf, expr_buf + lb, cm - lb);
1996
+ copy_trim(xmax_buf, sizeof xmax_buf, expr_buf + cm + 1, rb - cm - 1);
1997
+ has_xmin = has_xmax = 1;
1998
+ expr_buf[i] = 0;
1999
+ copy_trim(expr_buf, sizeof expr_buf, expr_buf, i);
2000
+ break;
2001
+ }
2002
+ }
2003
+ } else if (!has_xmin && starts_with_ci(expr_buf + i, " from ")) {
2004
+ int p = i + 6;
2005
+ while (is_space(expr_buf[p])) p++;
2006
+ if ((expr_buf[p] == 'x' || expr_buf[p] == 'X') && (expr_buf[p + 1] == '=' || expr_buf[p + 1] == ' ')) {
2007
+ p++;
2008
+ while (is_space(expr_buf[p]) || expr_buf[p] == '=') p++;
2009
+ }
2010
+ int to_pos = -1;
2011
+ for (int k = p; expr_buf[k]; k++) {
2012
+ if (starts_with_ci(expr_buf + k, " to ")) { to_pos = k; break; }
2013
+ }
2014
+ if (to_pos > p) {
2015
+ copy_trim(xmin_buf, sizeof xmin_buf, expr_buf + p, to_pos - p);
2016
+ copy_trim(xmax_buf, sizeof xmax_buf, expr_buf + to_pos + 4, slen(expr_buf) - (to_pos + 4));
2017
+ has_xmin = has_xmax = 1;
2018
+ expr_buf[i] = 0;
2019
+ copy_trim(expr_buf, sizeof expr_buf, expr_buf, i);
2020
+ break;
2021
+ }
2022
+ }
2023
+ }
2024
+
2025
+ // Optional command prefix ("graph ...", "plot ...", "solve ...", "table ...")
2026
+ const char *prefixes[4] = {"graph ", "plot ", "solve ", "table "};
2027
+ for (int k = 0; k < 4; k++) {
2028
+ if (starts_with_ci(expr_buf, prefixes[k])) {
2029
+ int plen = slen(prefixes[k]);
2030
+ if (!fn_buf[0] || streq(fn_buf, "graphcalc"))
2031
+ copy_trim(fn_buf, sizeof fn_buf, prefixes[k], plen - 1);
2032
+ copy_trim(expr_buf, sizeof expr_buf, expr_buf + plen, slen(expr_buf) - plen);
2033
+ break;
2034
+ }
2035
+ }
2036
+
2037
+ // Strip leading "y =" or "f(x) =" when there is no second '='
2038
+ int eq_cnt = 0, first_eq = -1;
2039
+ for (int i = 0; expr_buf[i]; i++) {
2040
+ if (expr_buf[i] == '=') {
2041
+ if (eq_cnt == 0) first_eq = i;
2042
+ eq_cnt++;
2043
+ }
2044
+ }
2045
+ if (eq_cnt == 1 && first_eq > 0) {
2046
+ char lhs_tmp[32];
2047
+ copy_trim(lhs_tmp, sizeof lhs_tmp, expr_buf, first_eq);
2048
+ if (streq(lhs_tmp, "y") || streq(lhs_tmp, "Y") ||
2049
+ starts_with_ci(lhs_tmp, "f(x)") || starts_with_ci(lhs_tmp, "g(x)")) {
2050
+ copy_trim(expr_buf, sizeof expr_buf, expr_buf + first_eq + 1, slen(expr_buf) - first_eq - 1);
2051
+ }
2052
+ }
2053
+ }
2054
+
2055
+ static int has_alpha_outside_gcd(const char *s) {
2056
+ for (int i = 0; s[i]; ) {
2057
+ if (starts_with_ci(s + i, "gcd(")) { i += 4; continue; }
2058
+ if (is_alpha(s[i])) return 1;
2059
+ i++;
2060
+ }
2061
+ return 0;
2062
+ }
2063
+
2064
+ static void dispatch_call(void) {
2065
+ parse_call_payload();
2066
+ if (!expr_buf[0]) {
2067
+ puts_raw("error: empty expression\n");
2068
+ return;
2069
+ }
2070
+
2071
+ q32_t xmin = -(5LL << 32), xmax = 5LL << 32;
2072
+ int got_bounds = 0;
2073
+ if (has_xmin && parse_q32_num(xmin_buf, NULL, &xmin)) got_bounds = 1;
2074
+ if (has_xmax && parse_q32_num(xmax_buf, NULL, &xmax)) got_bounds = 1;
2075
+
2076
+ // 1. Check for Equation (`LHS = RHS`)
2077
+ int eq_pos = -1;
2078
+ for (int i = 0; expr_buf[i]; i++) {
2079
+ if (expr_buf[i] == '=') { eq_pos = i; break; }
2080
+ }
2081
+ if (eq_pos > 0 && expr_buf[eq_pos + 1]) {
2082
+ char lhs_s[160], rhs_s[160];
2083
+ copy_trim(lhs_s, sizeof lhs_s, expr_buf, eq_pos);
2084
+ copy_trim(rhs_s, sizeof rhs_s, expr_buf + eq_pos + 1, slen(expr_buf) - eq_pos - 1);
2085
+ if (run_solver_mode(expr_buf, lhs_s, rhs_s, got_bounds, xmin, xmax) == 0) return;
2086
+ puts_raw("error: invalid equation syntax\n");
2087
+ return;
2088
+ }
2089
+
2090
+ // 2. Pure Bignum / Exact Rational Arithmetic (no variable x, no trig/log/sqrt)
2091
+ int is_explicit_graph_fn = streq_ci(fn_buf, "graph") || streq_ci(fn_buf, "plot") || streq_ci(fn_buf, "table");
2092
+ if (!has_alpha_outside_gcd(expr_buf) && !got_bounds && !is_explicit_graph_fn) {
2093
+ if (run_bignum_engine(expr_buf) == 0) return;
2094
+ }
2095
+
2096
+ // 3. Compile RPN for Scientific Numeric Eval or Function Graphing / Table
2097
+ RpnProg prog;
2098
+ if (!compile_rpn(expr_buf, &prog)) {
2099
+ puts_raw("error: syntax error in expression\n");
2100
+ return;
2101
+ }
2102
+
2103
+ int force_graph = got_bounds || is_explicit_graph_fn;
2104
+ if (!prog.uses_x && !force_graph) {
2105
+ char step_log[512];
2106
+ int ok = 0;
2107
+ q32_t val = eval_rpn(&prog, 0, &ok, step_log, sizeof step_log);
2108
+ if (!ok) {
2109
+ puts_raw("error: math domain or overflow error\n");
2110
+ return;
2111
+ }
2112
+ puts_raw("expression: ");
2113
+ puts_raw(expr_buf);
2114
+ rv_putchar('\n');
2115
+ if (step_log[0]) puts_raw(step_log);
2116
+ char vbuf[48];
2117
+ fmt_q32_rich(val, vbuf, sizeof vbuf);
2118
+ puts_raw("value: ");
2119
+ puts_raw(vbuf);
2120
+ rv_putchar('\n');
2121
+ return;
2122
+ }
2123
+
2124
+ int table_only = starts_with_ci(fn_buf, "table");
2125
+ run_graph_mode(expr_buf, &prog, xmin, xmax, table_only);
2126
+ }
2127
+
2128
+ #ifndef QEMU
2129
+ // P3 Bridge memory & CarbonText extended special tokens (256..264)
2130
+ #define TEXT_BUF ((const volatile unsigned char *)0x8000)
2131
+ #define CAP_ROWS 80
2132
+
2133
+ enum {
2134
+ TOK_TOOL_RESPONSE = 256,
2135
+ TOK_TOOL_RESPONSE_END = 257,
2136
+ TOK_THINK = 258,
2137
+ TOK_THINK_END = 259,
2138
+ TOK_IM_END = 260,
2139
+ TOK_IM_START = 261,
2140
+ TOK_USER = 262,
2141
+ TOK_ASSISTANT = 263,
2142
+ TOK_NL2 = 264
2143
+ };
2144
+
2145
+ static void put_tok(int c) { rv_ecall2(1, c, 0, 0); }
2146
+
2147
+ static int gather_bridge_text(void) {
2148
+ int n = 0;
2149
+ for (int i = 1; i <= CAP_ROWS; i++) {
2150
+ const volatile unsigned char *r = TEXT_BUF + 16 * i;
2151
+ if (!r[0]) break;
2152
+ for (int k = 0; k < 16 && r[k]; k++) {
2153
+ if (n < (int)sizeof(raw_call) - 1) raw_call[n++] = (char)r[k];
2154
+ }
2155
+ }
2156
+ raw_call[n] = 0;
2157
+ return n;
2158
+ }
2159
+
2160
+ int main(void) {
2161
+ for (;;) {
2162
+ // Park until Qwen emits </tool_call> (GO resumes right after this exit ecall)
2163
+ rv_ecall2(93, 0, 0, 0);
2164
+ int n = gather_bridge_text();
2165
+
2166
+ // Native Qwen3.5 ChatML tool-response turn header:
2167
+ // <|im_end|>\n<|im_start|>user\n<tool_response>\n
2168
+ put_tok(TOK_IM_END);
2169
+ put_tok('\n');
2170
+ put_tok(TOK_IM_START);
2171
+ put_tok(TOK_USER);
2172
+ put_tok('\n');
2173
+ put_tok(TOK_TOOL_RESPONSE);
2174
+ put_tok('\n');
2175
+
2176
+ if (n <= 0) {
2177
+ puts_raw("error: empty tool call\n");
2178
+ } else {
2179
+ dispatch_call();
2180
+ }
2181
+
2182
+ // Native Qwen3.5 ChatML assistant continuation header:
2183
+ // </tool_response><|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n##[HALT -> ' ']
2184
+ put_tok(TOK_TOOL_RESPONSE_END);
2185
+ put_tok(TOK_IM_END);
2186
+ put_tok('\n');
2187
+ put_tok(TOK_IM_START);
2188
+ put_tok(TOK_ASSISTANT);
2189
+ put_tok('\n');
2190
+ put_tok(TOK_THINK);
2191
+ put_tok(TOK_NL2);
2192
+ put_tok(TOK_THINK_END);
2193
+ put_tok(TOK_NL2);
2194
+ put_tok('#');
2195
+ put_tok('#');
2196
+ }
2197
+ }
2198
+ #else
2199
+ int main(void) {
2200
+ int pos = 0;
2201
+ while (pos < (int)sizeof(raw_call) - 1) {
2202
+ long got = rv_ecall2(63, 0, (long)(raw_call + pos), (long)(sizeof(raw_call) - 1 - pos));
2203
+ if (got <= 0) break;
2204
+ pos += (int)got;
2205
+ }
2206
+ raw_call[pos] = 0;
2207
+ dispatch_call();
2208
+ return 0;
2209
+ }
2210
+ #endif
firmware/tables.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate DROM fixed-point math lookup tables (`graphcalc_tables.h`) and bignum print sleds (`sled.S`)."""
2
+ import math
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def write_tables(hdr_path: Path) -> None:
8
+ one32 = 1 << 32
9
+ lines = [
10
+ "// Auto-generated Q32.32 constants and CORDIC / log tables in DROM (.rodata).",
11
+ "#pragma once",
12
+ "#include <stdint.h>",
13
+ "",
14
+ f"#define Q32_ONE ({one32}LL)",
15
+ f"#define Q32_PI ({round(math.pi * one32)}LL)",
16
+ f"#define Q32_TWO_PI ({round(2.0 * math.pi * one32)}LL)",
17
+ f"#define Q32_INV_TWO_PI ({round((1.0 / (2.0 * math.pi)) * one32)}LL)",
18
+ f"#define Q32_HALF_PI ({round(0.5 * math.pi * one32)}LL)",
19
+ f"#define Q32_E ({round(math.e * one32)}LL)",
20
+ f"#define Q32_LN2 ({round(math.log(2.0) * one32)}LL)",
21
+ f"#define Q32_INV_LN2 ({round((1.0 / math.log(2.0)) * one32)}LL)",
22
+ f"#define Q32_LN10 ({round(math.log(10.0) * one32)}LL)",
23
+ "",
24
+ ]
25
+ # CORDIC gain K = prod_{i=0}^{29} 1 / sqrt(1 + 2^{-2i})
26
+ k_gain = 1.0
27
+ atan_vals = []
28
+ for i in range(30):
29
+ k_gain /= math.hypot(1.0, 2.0 ** (-i))
30
+ atan_vals.append(round(math.atan(2.0 ** (-i)) * one32))
31
+ lines.append(f"#define CORDIC_K ({round(k_gain * one32)}LL)")
32
+ lines.append("static const int64_t CORDIC_ATAN[30] = {")
33
+ for i in range(0, 30, 5):
34
+ chunk = ", ".join(f"{v}LL" for v in atan_vals[i : i + 5])
35
+ lines.append(f" {chunk},")
36
+ lines.append("};\n")
37
+
38
+ # Shifted log table: ln(1 + 2^{-i}) for i = 1..30 in Q32.32
39
+ ln1p_vals = [0] + [round(math.log1p(2.0 ** (-i)) * one32) for i in range(1, 31)]
40
+ lines.append("static const int64_t LN1P_POW2[31] = {")
41
+ for i in range(0, 31, 5):
42
+ chunk = ", ".join(f"{v}LL" for v in ln1p_vals[i : i + 5])
43
+ lines.append(f" {chunk},")
44
+ lines.append("};\n")
45
+
46
+ hdr_path.write_text("\n".join(lines), encoding="utf-8")
47
+
48
+
49
+ def write_sled(sled_path: Path) -> None:
50
+ out = [" .section .text", " .globl bn_sleds"]
51
+ for r in range(8):
52
+ out += [f"bn_sled{r}:"] + [f" .insn i 0x2B, 0, x{r}, x0, {k}" for k in range(127, -1, -1)]
53
+ out += [" ret"]
54
+ out += [" .section .rodata", " .align 2", "bn_sleds:"]
55
+ out += [f" .word bn_sled{r}" for r in range(8)]
56
+ sled_path.write_text("\n".join(out) + "\n", encoding="utf-8")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ write_tables(Path(sys.argv[1]))
61
+ write_sled(Path(sys.argv[2]))
generation_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "eos_token_id": [
3
+ 248046,
4
+ 248044
5
+ ],
6
+ "pad_token_id": 248044,
7
+ "bad_words_ids": [
8
+ [
9
+ 248045
10
+ ]
11
+ ],
12
+ "do_sample": false
13
+ }
graphcalc.elf ADDED
Binary file (87.6 kB). View file
 
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7dacb327bea9361e115a0378d19ce9e030e8aeed3561361a461812ba1cceee7
3
+ size 17971408308
runner.py ADDED
@@ -0,0 +1,886 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone in-weights runner for `Qwen3.5-9B-Math-GraphCalc` (and `0.8B`).
2
+
3
+ Requires `qwenputer` (`https://github.com/divagr18/qwenputer`) on `PYTHONPATH` or pointed to by
4
+ `QWENPUTER_ROOT` (defaults to `./qwenputer` or `../qwenputer`):
5
+
6
+ git clone https://github.com/divagr18/qwenputer
7
+ QWENPUTER_ROOT=./qwenputer python runner.py --ask "Calculate 37429 * 8613 + 194820 and explain your steps."
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import copy
14
+ import gc
15
+ import json
16
+ import os
17
+ import re
18
+ import sys
19
+ import time
20
+ import zlib
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+ from safetensors import safe_open
28
+ from tokenizers import Tokenizer
29
+ from transformers import AutoConfig, AutoTokenizer, DynamicCache, Qwen3_5ForCausalLM
30
+ from transformers.models.qwen3_5 import modeling_qwen3_5 as q35_mod
31
+
32
+ MODEL_DIR = Path(__file__).resolve().parent
33
+
34
+
35
+ def _locate_qwenputer_rv() -> Path:
36
+ candidates = []
37
+ if os.environ.get("QWENPUTER_ROOT"):
38
+ candidates.append(Path(os.environ["QWENPUTER_ROOT"]))
39
+ candidates.extend([
40
+ Path.cwd() / "qwenputer",
41
+ Path.cwd().parent / "qwenputer",
42
+ MODEL_DIR / "qwenputer",
43
+ MODEL_DIR.parent / "qwenputer",
44
+ MODEL_DIR.parent.parent / "qwenputer",
45
+ MODEL_DIR.parent.parent.parent / "qwenputer",
46
+ ])
47
+ for root in candidates:
48
+ rv = root.resolve() / "experimental" / "riscv"
49
+ if (rv / "chipformer").is_dir():
50
+ return rv
51
+ raise RuntimeError(
52
+ "Could not locate `chipformer` from `qwenputer` (`experimental/riscv/chipformer`). "
53
+ "Clone https://github.com/divagr18/qwenputer and set QWENPUTER_ROOT=/path/to/qwenputer."
54
+ )
55
+
56
+
57
+ try:
58
+ import chipformer # noqa: F401
59
+ except ImportError:
60
+ sys.path.insert(0, str(_locate_qwenputer_rv()))
61
+
62
+ from chipformer.q35 import fast as F_chip # noqa: E402
63
+ from chipformer.q35 import ir # noqa: E402
64
+ from chipformer.q35.ir import Target # noqa: E402
65
+ from chipformer.rv import bridge as BR # noqa: E402
66
+ from chipformer.rv import core, elf, hf, ref # noqa: E402
67
+ from chipformer.rv import spec as S # noqa: E402
68
+
69
+ CARBON_SPECIALS = [
70
+ "<tool_response>", # 256
71
+ "</tool_response>", # 257
72
+ "<think>", # 258
73
+ "</think>", # 259
74
+ "<|im_end|>", # 260
75
+ "<|im_start|>", # 261
76
+ "user", # 262
77
+ "assistant", # 263
78
+ "\n\n", # 264
79
+ ]
80
+
81
+ GRAPHCALC_TOOL = {
82
+ "type": "function",
83
+ "function": {
84
+ "name": "graphcalc",
85
+ "description": (
86
+ "Exact in-weights graphing & math calculator (62-digit integers/rationals, scientific "
87
+ "functions, equation solver, and coordinate plotter). Pass an arithmetic expression, "
88
+ "an equation in x (LHS = RHS), or a function f(x); optionally pass xmin and xmax to "
89
+ "plot y = f(x) and compute roots, vertex/extrema, definite integral, and value table."
90
+ ),
91
+ "parameters": {
92
+ "type": "object",
93
+ "properties": {
94
+ "expression": {
95
+ "type": "string",
96
+ "description": "Math expression, equation (LHS = RHS), or function of x (e.g. 'x^2 - 4*x + 3')",
97
+ },
98
+ "xmin": {
99
+ "type": "number",
100
+ "description": "Optional domain/plot minimum x (default -5 for plots, -20 for nonlinear solve)",
101
+ },
102
+ "xmax": {
103
+ "type": "number",
104
+ "description": "Optional domain/plot maximum x (default 5 for plots, 20 for nonlinear solve)",
105
+ },
106
+ },
107
+ "required": ["expression"],
108
+ },
109
+ },
110
+ }
111
+
112
+ GRAPHCALC_SYSTEM_PROMPT = (
113
+ "You are a clear, encouraging step-by-step math tutor equipped with an embedded graphing "
114
+ "calculator (`graphcalc`). Always call `graphcalc` first to compute exact arithmetic, roots, "
115
+ "coordinate tables, or function plots, then explain every step clearly so a student can learn."
116
+ )
117
+
118
+ _orig_chunk_gdn = q35_mod.torch_chunk_gated_delta_rule
119
+ _orig_rec_gdn = q35_mod.torch_recurrent_gated_delta_rule
120
+
121
+
122
+ def _fp32_chunk_gdn(*args, **kwargs):
123
+ with torch.autocast("cuda", enabled=False):
124
+ return _orig_chunk_gdn(*args, **kwargs)
125
+
126
+
127
+ def _fp32_rec_gdn(*args, **kwargs):
128
+ with torch.autocast("cuda", enabled=False):
129
+ return _orig_rec_gdn(*args, **kwargs)
130
+
131
+
132
+ q35_mod.torch_chunk_gated_delta_rule = _fp32_chunk_gdn
133
+ q35_mod.torch_recurrent_gated_delta_rule = _fp32_rec_gdn
134
+ F_chip.exact_matmuls()
135
+
136
+
137
+ class CarbonText(BR.Text):
138
+ """Qwen3.5 bridge vocabulary with native ChatML special tokens (256..264) and space HALT echo."""
139
+
140
+ def __init__(self, tokenizer_path: str | Path) -> None:
141
+ p = Path(tokenizer_path)
142
+ tj = p if p.is_file() else (p / "tokenizer.json")
143
+ self.tok = Tokenizer.from_file(str(tj))
144
+ vocab = self.tok.get_vocab(with_added_tokens=True)
145
+ base_vocab = {s: i for s, i in vocab.items() if not s.startswith("<|chip_")}
146
+ self.n = max(base_vocab.values()) + 1
147
+ dec = {c: b for b, c in BR.bytes_to_unicode().items()}
148
+ added = (
149
+ {a.content for a in self.tok.get_added_tokens_decoder().values()}
150
+ if hasattr(self.tok, "get_added_tokens_decoder")
151
+ else set()
152
+ )
153
+ self.bytes = [b""] * self.n
154
+ for s, i in base_vocab.items():
155
+ if s in added or not all(ch in dec for ch in s):
156
+ self.bytes[i] = s.encode("utf-8")
157
+ else:
158
+ self.bytes[i] = bytes(dec[ch] for ch in s)
159
+ self.TCO = base_vocab["<tool_call>"]
160
+ self.TCC = base_vocab["</tool_call>"]
161
+ u = BR.bytes_to_unicode()
162
+ special_ids = []
163
+ for sp in CARBON_SPECIALS:
164
+ if sp in base_vocab:
165
+ special_ids.append(base_vocab[sp])
166
+ else:
167
+ enc = self.tok.encode(sp, add_special_tokens=False).ids
168
+ assert len(enc) == 1, f"Special {sp!r} is not a single Qwen token: {enc}"
169
+ special_ids.append(enc[0])
170
+ self.printable = [base_vocab[u[b]] for b in range(256)] + special_ids
171
+ self.pindex = {q: p for p, q in enumerate(self.printable)}
172
+ self.NL = self.printable[32]
173
+
174
+
175
+ def _compact_layer_inplace(layer: nn.Module, is_chip: bool) -> tuple[int, int]:
176
+ before = after = 0
177
+ if is_chip and hasattr(layer, "linear_attn") and isinstance(layer.linear_attn.out_proj, nn.Linear):
178
+ F_chip.prune_heads(layer.linear_attn)
179
+ for parent in list(layer.modules()):
180
+ for name, child in list(parent.named_children()):
181
+ if isinstance(child, nn.Linear) and child.bias is None:
182
+ sp = F_chip.SparseLinear(child.weight)
183
+ before += child.weight.numel()
184
+ after += sp.weight.numel()
185
+ setattr(parent, name, sp)
186
+ return before, after
187
+
188
+
189
+ def _load_and_compact_qwen35(path: Path, n_chip: int, dev: str) -> tuple[Qwen3_5ForCausalLM, dict]:
190
+ layout_file = path / "chip_layout.json"
191
+ if not layout_file.is_file():
192
+ model = Qwen3_5ForCausalLM.from_pretrained(
193
+ str(path),
194
+ dtype=torch.bfloat16,
195
+ attn_implementation="sdpa",
196
+ )
197
+ chip_b = chip_a = qwen_b = qwen_a = 0
198
+ for i, layer in enumerate(model.model.layers):
199
+ b, a = _compact_layer_inplace(layer, is_chip=(i < n_chip))
200
+ if i < n_chip:
201
+ chip_b += b
202
+ chip_a += a
203
+ else:
204
+ qwen_b += b
205
+ qwen_a += a
206
+ return model.to(dev).eval(), {
207
+ "chip_dense": chip_b,
208
+ "chip_compact": chip_a,
209
+ "qwen_dense": qwen_b,
210
+ "qwen_compact": qwen_a,
211
+ }
212
+
213
+ layout = json.loads(layout_file.read_text(encoding="utf-8"))
214
+ blob = (path / "chip_indices.bin.zlib").read_bytes()
215
+ cfg = AutoConfig.from_pretrained(str(path))
216
+ cfg._attn_implementation = "sdpa"
217
+ with torch.device("meta"):
218
+ model = Qwen3_5ForCausalLM(cfg)
219
+
220
+ tensors_spec: dict[str, dict] = layout["tensors"]
221
+ by_layer: list[list[str]] = [[] for _ in range(cfg.num_hidden_layers)]
222
+ for k in tensors_spec:
223
+ if k.startswith("model.layers."):
224
+ lid = int(k.split(".", 3)[2])
225
+ by_layer[lid].append(k)
226
+
227
+ chip_before = chip_after = qwen_before = qwen_after = head_before = head_after = 0
228
+ with safe_open(str(path / "model.safetensors"), framework="pt", device="cpu") as f:
229
+ def _unpack_one(k: str) -> torch.Tensor:
230
+ spec = tensors_spec[k]
231
+ vals = f.get_tensor(k)
232
+ if spec["mode"] == "dense":
233
+ return vals
234
+ shape = tuple(spec["shape"])
235
+ raw = zlib.decompress(blob[spec["offset"] : spec["offset"] + spec["length"]])
236
+ idx = np.cumsum(np.frombuffer(raw, dtype=np.uint32), dtype=np.int64)
237
+ out = torch.zeros(int(np.prod(shape)), dtype=vals.dtype)
238
+ out[torch.from_numpy(idx)] = vals
239
+ return out.view(shape)
240
+
241
+ for lid in range(cfg.num_hidden_layers):
242
+ prefix = f"model.layers.{lid}."
243
+ sub_sd = {k[len(prefix) :]: _unpack_one(k) for k in by_layer[lid]}
244
+ layer = model.model.layers[lid]
245
+ layer.load_state_dict(sub_sd, assign=True, strict=True)
246
+ del sub_sd
247
+ b, a = _compact_layer_inplace(layer, is_chip=(lid < n_chip))
248
+ if lid < n_chip:
249
+ chip_before += b
250
+ chip_after += a
251
+ else:
252
+ qwen_before += b
253
+ qwen_after += a
254
+ layer.to(dev)
255
+
256
+ norm_w = _unpack_one("model.norm.weight").to(dev)
257
+ model.model.norm.load_state_dict({"weight": norm_w}, assign=True, strict=True)
258
+
259
+ emb_w = _unpack_one("model.embed_tokens.weight").to(dev)
260
+ model.model.embed_tokens.load_state_dict({"weight": emb_w}, assign=True, strict=True)
261
+
262
+ if "lm_head.weight" in tensors_spec and not getattr(cfg, "tie_word_embeddings", True):
263
+ head_w_cpu = _unpack_one("lm_head.weight")
264
+ head_before = head_w_cpu.numel()
265
+ hc0, hc1 = F_chip._hull(F_chip.nonzero_cols(head_w_cpu))
266
+ hblock = nn.Parameter(head_w_cpu[:, hc0:hc1].contiguous().to(dev), requires_grad=False)
267
+ head_after = hblock.numel()
268
+ model.lm_head = F_chip.SparseLinear(
269
+ head_w_cpu, rows=(0, head_w_cpu.shape[0]), cols=(hc0, hc1), block=hblock
270
+ )
271
+ del head_w_cpu
272
+ else:
273
+ model.lm_head.load_state_dict({"weight": emb_w}, assign=True, strict=True)
274
+ model.tie_weights()
275
+ head_before = head_after = emb_w.numel()
276
+
277
+ model.model.rotary_emb = q35_mod.Qwen3_5TextRotaryEmbedding(config=cfg).to(dev)
278
+ model.eval()
279
+ if dev == "cuda":
280
+ with torch.no_grad():
281
+ for _ in range(2):
282
+ _wc = DynamicCache(config=cfg)
283
+ model.model(
284
+ input_ids=torch.zeros((1, 2), dtype=torch.long, device=dev),
285
+ past_key_values=_wc,
286
+ use_cache=True,
287
+ )
288
+ model.model(
289
+ input_ids=torch.zeros((1, 1), dtype=torch.long, device=dev),
290
+ past_key_values=_wc,
291
+ use_cache=True,
292
+ )
293
+ del _wc
294
+ gc.collect()
295
+ torch.cuda.empty_cache()
296
+ else:
297
+ gc.collect()
298
+ return model, {
299
+ "chip_dense": chip_before,
300
+ "chip_compact": chip_after,
301
+ "qwen_dense": qwen_before,
302
+ "qwen_compact": qwen_after,
303
+ "head_dense": head_before,
304
+ "head_compact": head_after,
305
+ }
306
+
307
+
308
+ class CarbonGraphCalcRunner:
309
+ """Self-contained runner for `Qwen3.5-Math-GraphCalc` checkpoints."""
310
+
311
+ def __init__(
312
+ self,
313
+ model_dir: str | Path = MODEL_DIR,
314
+ firmware_path: str | Path | None = None,
315
+ device: str | None = None,
316
+ fast: bool = True,
317
+ executor: bool | str = True,
318
+ ) -> None:
319
+ torch.set_grad_enabled(False)
320
+ F_chip.exact_matmuls()
321
+ self.path = Path(model_dir)
322
+ self.meta = json.loads((self.path / "chipformer.json").read_text(encoding="utf-8"))
323
+ self.dev = device or ("cuda" if torch.cuda.is_available() else "cpu")
324
+ self.n_chip = int(self.meta["chip_layers"])
325
+ self.v0 = int(self.meta["chip_token_offset"])
326
+ self.d0 = int(self.meta["chip_dims_offset"])
327
+
328
+ self.model, self.compact_stats = _load_and_compact_qwen35(self.path, self.n_chip, self.dev)
329
+ self.lm = self.model.model
330
+ if isinstance(self.model.lm_head, F_chip.SparseLinear):
331
+ self.head_c0 = self.model.lm_head.c0
332
+ self.head_c1 = self.model.lm_head.c1
333
+ self.qwen_head_w = self.model.lm_head.weight[: self.v0]
334
+ else:
335
+ self.head_c0 = 0
336
+ self.head_c1 = self.model.config.hidden_size
337
+ self.qwen_head_w = self.model.lm_head.weight[: self.v0]
338
+ self._p4_buf = torch.zeros((4, 1, 1), dtype=torch.long, device=self.dev)
339
+
340
+ self.tok = AutoTokenizer.from_pretrained(str(self.path))
341
+ self.T = CarbonText(self.path / "tokenizer.json")
342
+
343
+ cfg = self.model.config
344
+ self.c = core.build(
345
+ Target(cfg.linear_num_key_heads, cfg.linear_num_value_heads, cfg.intermediate_size),
346
+ self.T,
347
+ )
348
+ self.code_dims = torch.tensor(self.meta["code_dims"], device=self.dev, dtype=torch.long)
349
+ self.eos = {248046, 248044}
350
+ self.go_model_id = self.v0 + S.tok("GO")
351
+
352
+ fw = firmware_path or (self.path / self.meta.get("firmware", "graphcalc.elf"))
353
+ self.firmware_path = Path(fw)
354
+ self.prog = elf.program(self.firmware_path)
355
+ core.snapshot(self.c, self.fresh_machine())
356
+
357
+ self.executor = executor if self.dev == "cuda" else False
358
+ self.verify = self.executor == "verify"
359
+ if self.executor:
360
+ try:
361
+ self._build_exec()
362
+ except Exception as exc:
363
+ if self.verify:
364
+ raise
365
+ print(f"[runner] Triton Executor2 unavailable ({type(exc).__name__}: {exc}); using CUDA-graphed HF chip layers.", flush=True)
366
+ self.executor = False
367
+ self.verify = False
368
+
369
+ self.fast = bool(fast and self.dev == "cuda" and (not self.executor or self.verify))
370
+ if self.fast:
371
+ self._build_fast()
372
+
373
+ def _build_exec(self) -> None:
374
+ from chipformer.rv.chat import ExecRow
375
+
376
+ self.keep_ir = torch.tensor(self.c.keep, device=self.dev, dtype=torch.long)
377
+ cols = self.meta["code_dims"] + [self.meta["tau_dim"], self.meta["mu_dim"]]
378
+ self.keep_cols = torch.tensor(cols, device=self.dev, dtype=torch.long)
379
+ self.emb_cache: dict[int, torch.Tensor] = {}
380
+ self.erow = ExecRow(self)
381
+ self.erow._capture()
382
+
383
+ def _exec_emb(self, ids: list[int]) -> torch.Tensor:
384
+ new = sorted({i for i in ids if i not in self.emb_cache})
385
+ if new:
386
+ rows = self.erow.ex.embed(
387
+ [i - self.v0 if i >= self.v0 else BR.text_token(i) for i in new]
388
+ )
389
+ for i, r in zip(new, rows):
390
+ self.emb_cache[i] = r
391
+ return torch.stack([self.emb_cache[i] for i in ids])
392
+
393
+ @torch.no_grad()
394
+ def _exec_step_one(self, tok_id: int) -> torch.Tensor:
395
+ row = self.emb_cache.get(tok_id)
396
+ if row is None:
397
+ st = tok_id - self.v0 if tok_id >= self.v0 else BR.text_token(tok_id)
398
+ row = self.erow.ex.embed([st])[0]
399
+ self.emb_cache[tok_id] = row
400
+ self.erow.xbuf[0].copy_(row)
401
+ self.erow.graph.replay()
402
+ return self.erow.kbuf
403
+
404
+ @torch.no_grad()
405
+ def _exec_start_collect(self, ids: list[int]) -> torch.Tensor:
406
+ if self.erow.graph is None:
407
+ self.erow._capture()
408
+ for st in self.erow.heads:
409
+ st["S"].zero_()
410
+ st["hist"].zero_()
411
+ emb = self._exec_emb(ids)
412
+ keeps = torch.empty((len(ids), len(self.keep_cols)), dtype=torch.float32, device=self.dev)
413
+ for t in range(len(ids)):
414
+ self.erow.xbuf.copy_(emb[t : t + 1])
415
+ self.erow.graph.replay()
416
+ keeps[t].copy_(self.erow.kbuf)
417
+ self.erow.ex.load_init()
418
+ return keeps
419
+
420
+ def _exec_hc(self, tok_id: int, keep: torch.Tensor) -> torch.Tensor:
421
+ hc = torch.zeros(self.lm.embed_tokens.weight.shape[1], dtype=torch.bfloat16, device=self.dev)
422
+ hc[: self.d0] = self.lm.embed_tokens.weight[tok_id, : self.d0]
423
+ hc[self.keep_cols] = keep.to(torch.bfloat16)
424
+ return hc
425
+
426
+ def _build_fast(self) -> None:
427
+ cfg = copy.deepcopy(self.model.config)
428
+ cfg.num_hidden_layers = self.n_chip + 1
429
+ cfg.layer_types = ["linear_attention"] * self.n_chip + ["full_attention"]
430
+ self.ccfg = cfg
431
+ dummy = q35_mod.Qwen3_5DecoderLayer(cfg, self.n_chip).to(self.dev, torch.bfloat16)
432
+ for p in dummy.parameters():
433
+ p.data.zero_()
434
+ with torch.device("meta"):
435
+ cm = q35_mod.Qwen3_5TextModel(cfg)
436
+ cm.embed_tokens = self.lm.embed_tokens
437
+ cm.layers = nn.ModuleList([*self.lm.layers[: self.n_chip], dummy])
438
+ cm.norm = nn.Identity()
439
+ cm.rotary_emb = q35_mod.Qwen3_5TextRotaryEmbedding(config=cfg).to(self.dev)
440
+ self.cm = cm.eval()
441
+ self.graph = None
442
+
443
+ def _chip_cache(self):
444
+ if getattr(self, "ccache", None) is None:
445
+ self.ccache = DynamicCache(config=self.ccfg)
446
+ else:
447
+ for L in range(self.n_chip):
448
+ lay = self.ccache.layers[L]
449
+ lay.conv_states[0].zero_()
450
+ lay.recurrent_states[0].zero_()
451
+ lay.has_previous_state[0] = False
452
+ return self.ccache
453
+
454
+ @torch.no_grad()
455
+ def _chip_body(self):
456
+ h = self.cm.embed_tokens(self.g_ids)
457
+ for L in range(self.n_chip):
458
+ h = self.cm.layers[L](
459
+ h,
460
+ position_embeddings=None,
461
+ attention_mask=None,
462
+ position_ids=None,
463
+ past_key_values=self.ccache,
464
+ )
465
+ self.g_out.copy_(h)
466
+
467
+ @torch.no_grad()
468
+ def _capture(self):
469
+ self.g_ids = torch.zeros((1, 1), dtype=torch.long, device=self.dev)
470
+ self.g_out = torch.zeros(
471
+ (1, 1, self.model.config.hidden_size), dtype=torch.bfloat16, device=self.dev
472
+ )
473
+ lays = [self.ccache.layers[L] for L in range(self.n_chip)]
474
+ saved = [(lay.conv_states[0].clone(), lay.recurrent_states[0].clone()) for lay in lays]
475
+ s = torch.cuda.Stream()
476
+ s.wait_stream(torch.cuda.current_stream())
477
+ with torch.cuda.stream(s):
478
+ for _ in range(3):
479
+ self._chip_body()
480
+ torch.cuda.current_stream().wait_stream(s)
481
+ self.graph = torch.cuda.CUDAGraph()
482
+ with torch.cuda.graph(self.graph):
483
+ self._chip_body()
484
+ for lay, (cs, rs) in zip(lays, saved):
485
+ lay.conv_states[0].copy_(cs)
486
+ lay.recurrent_states[0].copy_(rs)
487
+
488
+ @torch.no_grad()
489
+ def _chip_step(self, tok_id: int, g: int, cache, compiled: bool = True) -> torch.Tensor:
490
+ if not compiled or self.graph is None:
491
+ x = torch.tensor([[tok_id]], device=self.dev)
492
+ out = self.cm(
493
+ input_ids=x,
494
+ past_key_values=cache,
495
+ use_cache=True,
496
+ cache_position=torch.tensor([g], device=self.dev),
497
+ )
498
+ return out.last_hidden_state[0, -1].clone()
499
+ self.g_ids.fill_(tok_id)
500
+ self.graph.replay()
501
+ return self.g_out[0, -1].clone()
502
+
503
+ @torch.no_grad()
504
+ def _qwen_step(self, hc: torch.Tensor, cache, pos: int, full_head: bool = False) -> torch.Tensor:
505
+ h = hc.view(1, 1, -1)
506
+ p4 = self._p4_buf
507
+ p4.fill_(pos)
508
+ pe = self.lm.rotary_emb(h, p4[1:])
509
+ for i in range(self.n_chip, len(self.lm.layers)):
510
+ h = self.lm.layers[i](
511
+ h,
512
+ position_embeddings=pe,
513
+ attention_mask=None,
514
+ position_ids=p4[0],
515
+ past_key_values=cache,
516
+ )
517
+ nh = self.lm.norm(h)[0, -1]
518
+ if full_head:
519
+ return self.model.lm_head(nh)
520
+ return F.linear(nh[self.head_c0 : self.head_c1], self.qwen_head_w)
521
+
522
+ @torch.no_grad()
523
+ def _qwen_prefill(
524
+ self, x_ids: torch.Tensor, keeps: torch.Tensor, full_head: bool = False
525
+ ) -> tuple[DynamicCache, torch.Tensor]:
526
+ p_len = int(x_ids.shape[1])
527
+ h = torch.zeros(
528
+ (1, p_len, self.model.config.hidden_size), dtype=torch.bfloat16, device=self.dev
529
+ )
530
+ h[0, :, : self.d0] = self.lm.embed_tokens.weight[x_ids[0], : self.d0]
531
+ h[0, :, self.keep_cols] = keeps.to(torch.bfloat16)
532
+ cache = DynamicCache(config=self.model.config)
533
+ pos_vec = torch.arange(0, p_len, device=self.dev, dtype=torch.long).unsqueeze(0)
534
+ p4 = pos_vec.unsqueeze(0).expand(4, -1, -1)
535
+ pe = self.lm.rotary_emb(h, p4[1:])
536
+ for i in range(self.n_chip, len(self.lm.layers)):
537
+ h = self.lm.layers[i](
538
+ h,
539
+ position_embeddings=pe,
540
+ attention_mask=None,
541
+ position_ids=p4[0],
542
+ past_key_values=cache,
543
+ )
544
+ nh = self.lm.norm(h)[0, -1]
545
+ logits = (
546
+ self.model.lm_head(nh)
547
+ if full_head
548
+ else F.linear(nh[self.head_c0 : self.head_c1], self.qwen_head_w)
549
+ )
550
+ return cache, logits
551
+
552
+ @torch.no_grad()
553
+ def _qwen_chunk(
554
+ self, hcs: list[torch.Tensor], cache, pos0: int, full_head: bool = False
555
+ ) -> torch.Tensor:
556
+ if len(hcs) == 1:
557
+ return self._qwen_step(hcs[0], cache, pos0, full_head=full_head)
558
+ k_len = len(hcs)
559
+ h = torch.stack(hcs, dim=0).unsqueeze(0)
560
+ pos_vec = torch.arange(pos0, pos0 + k_len, device=self.dev, dtype=torch.long).unsqueeze(0)
561
+ p4 = pos_vec.unsqueeze(0).expand(4, -1, -1)
562
+ pe = self.lm.rotary_emb(h, p4[1:])
563
+ attn_mask = torch.zeros((1, 1, k_len, pos0 + k_len), dtype=h.dtype, device=self.dev)
564
+ future = torch.triu(torch.ones((k_len, k_len), dtype=torch.bool, device=self.dev), diagonal=1)
565
+ attn_mask[0, 0, :, pos0:][future] = torch.finfo(h.dtype).min
566
+ for i in range(self.n_chip, len(self.lm.layers)):
567
+ lmask = attn_mask if self.model.config.layer_types[i] == "full_attention" else None
568
+ h = self.lm.layers[i](
569
+ h,
570
+ position_embeddings=pe,
571
+ attention_mask=lmask,
572
+ position_ids=p4[0],
573
+ past_key_values=cache,
574
+ )
575
+ nh = self.lm.norm(h)[0, -1]
576
+ if full_head:
577
+ return self.model.lm_head(nh)
578
+ return F.linear(nh[self.head_c0 : self.head_c1], self.qwen_head_w)
579
+
580
+ @torch.no_grad()
581
+ def _layers_slow(self, tok_id: int, cache, pos: int | None):
582
+ h = self.lm.embed_tokens(torch.tensor([[tok_id]], device=self.dev))
583
+ pe = tp = None
584
+ if pos is not None:
585
+ p4 = torch.full((4, 1, 1), pos, device=self.dev, dtype=torch.long)
586
+ tp = p4[0]
587
+ pe = self.lm.rotary_emb(h, p4[1:])
588
+ hc = None
589
+ n = self.n_chip if pos is None else len(self.lm.layers)
590
+ for i in range(n):
591
+ h = self.lm.layers[i](
592
+ h,
593
+ position_embeddings=pe,
594
+ attention_mask=None,
595
+ position_ids=tp,
596
+ past_key_values=cache,
597
+ )
598
+ if i == self.n_chip - 1:
599
+ hc = h
600
+ logits = None if pos is None else self.model.lm_head(self.lm.norm(h))[0, -1]
601
+ return hc[0, -1], logits
602
+
603
+ def _decode_code_list(self, vals: list[float]) -> int | None:
604
+ if not any(v != 0.0 for v in vals):
605
+ return None
606
+ nf = core.NFAM
607
+ fid = 0
608
+ for i in range(nf):
609
+ if vals[i] > 0.0:
610
+ fid |= 1 << i
611
+ p = 0
612
+ for i in range(len(vals) - nf):
613
+ if vals[nf + i] > 0.0:
614
+ p |= 1 << i
615
+ t = core.decode_code(fid, p, self.T)
616
+ if t < 0:
617
+ raise RuntimeError(f"Invalid circuit token prediction (family={fid}, payload={p})")
618
+ return t - S.TEXT_TOKEN if S.is_text(t) else self.v0 + t
619
+
620
+ def code_token(self, code: torch.Tensor) -> int | None:
621
+ return self._decode_code_list(code.tolist())
622
+
623
+ def circuit_token(self, hc: torch.Tensor) -> int | None:
624
+ return self.code_token(hc[self.code_dims])
625
+
626
+ def fresh_machine(self) -> ref.Machine:
627
+ m = ref.Machine.from_program(self.prog)
628
+ BR.park(m)
629
+ return m
630
+
631
+ def prompt_ids(
632
+ self,
633
+ user_prompt: str | list[dict],
634
+ system_prompt: str | None = GRAPHCALC_SYSTEM_PROMPT,
635
+ use_tools: bool = True,
636
+ ) -> list[int]:
637
+ if isinstance(user_prompt, str):
638
+ msgs = []
639
+ if system_prompt:
640
+ msgs.append({"role": "system", "content": system_prompt})
641
+ msgs.append({"role": "user", "content": user_prompt})
642
+ else:
643
+ msgs = list(user_prompt)
644
+ tools = [GRAPHCALC_TOOL] if use_tools else None
645
+ out = self.tok.apply_chat_template(
646
+ msgs,
647
+ tools=tools,
648
+ add_generation_prompt=True,
649
+ enable_thinking=False,
650
+ tokenize=True,
651
+ )
652
+ if not isinstance(out, list):
653
+ out = out["input_ids"]
654
+ return [int(x) for x in out]
655
+
656
+ @torch.no_grad()
657
+ def generate(
658
+ self,
659
+ ids: list[int],
660
+ max_new_text: int = 512,
661
+ max_chip_tokens: int = 500_000,
662
+ max_calls: int = 1,
663
+ check_head: bool = False,
664
+ check_lock: bool = False,
665
+ ) -> dict:
666
+ m = self.fresh_machine()
667
+ lock = None
668
+ if check_lock:
669
+ lock = BR.Session(self.T, self.fresh_machine())
670
+ lock.history(ids)
671
+
672
+ x = torch.tensor([ids], device=self.dev, dtype=torch.long)
673
+ core.snapshot(self.c, m)
674
+ n_code = len(self.code_dims)
675
+
676
+ if self.executor and not self.verify:
677
+ keeps = self._exec_start_collect(ids)
678
+ cache, logits = self._qwen_prefill(x, keeps, full_head=check_head)
679
+ else:
680
+ lm_out = self.lm(input_ids=x, use_cache=True)
681
+ cache = lm_out.past_key_values
682
+ if self.executor:
683
+ self.erow.start(ids)
684
+ if self.fast:
685
+ ccache = self._chip_cache()
686
+ h = self.cm.embed_tokens(x)
687
+ for L in range(self.n_chip):
688
+ h = self.cm.layers[L](
689
+ h,
690
+ position_embeddings=None,
691
+ attention_mask=None,
692
+ position_ids=None,
693
+ past_key_values=ccache,
694
+ )
695
+ hf.inject(self.c, ccache)
696
+ if self.graph is None:
697
+ self._capture()
698
+ g = len(ids)
699
+ elif not self.executor:
700
+ hf.inject(self.c, cache)
701
+ logits = self.model.lm_head(lm_out.last_hidden_state[0, -1])
702
+ pos = len(ids)
703
+ gen: list[int] = []
704
+ n_qwen_text = n_echo_text = n_chip_tok = n_calls = head_mismatch = n_verify_bad = 0
705
+ nxt = int(torch.argmax(logits[: self.v0]))
706
+ prev_owned = False
707
+ in_tool_call = False
708
+ pending_hcs: list[torch.Tensor] = []
709
+ pending_pos0: int | None = None
710
+ t0 = time.time()
711
+ expect: list[int] = []
712
+ lock_error = None
713
+
714
+ while n_chip_tok < max_chip_tokens:
715
+ if n_qwen_text >= max_new_text and (not prev_owned) and (not in_tool_call):
716
+ break
717
+
718
+ gen.append(nxt)
719
+ if nxt == self.T.TCO and not prev_owned:
720
+ in_tool_call = True
721
+ elif nxt == self.T.TCC:
722
+ in_tool_call = False
723
+ if nxt == self.go_model_id:
724
+ n_calls += 1
725
+
726
+ if lock is not None:
727
+ st = nxt - self.v0 if nxt >= self.v0 else BR.text_token(nxt)
728
+ if expect:
729
+ e = expect.pop(0)
730
+ if st != e:
731
+ lock_error = {"at": len(gen) - 1, "got": st, "want": e}
732
+ break
733
+ elif st < S.VOCAB:
734
+ lock_error = {"at": len(gen) - 1, "got": S.name(st), "want": "text"}
735
+ break
736
+ else:
737
+ k = len(lock.toks)
738
+ try:
739
+ lock.qwen([nxt])
740
+ except RuntimeError as exc:
741
+ lock_error = {"at": len(gen) - 1, "got": str(exc), "want": "halt"}
742
+ break
743
+ expect = list(lock.toks[k + 1 :])
744
+
745
+ if nxt in self.eos and not prev_owned:
746
+ break
747
+
748
+ chip_in = nxt >= self.v0
749
+ hc = keep = None
750
+ if self.executor:
751
+ keep = self._exec_step_one(nxt)
752
+ if not chip_in:
753
+ hc = self._exec_hc(nxt, keep)
754
+ if self.fast and (self.verify or not self.executor):
755
+ hf_hc = self._chip_step(nxt, g, ccache)
756
+ g += 1
757
+ if self.verify:
758
+ same = (
759
+ bool((hf_hc[self.keep_cols] == keep.to(torch.bfloat16)).all())
760
+ if chip_in
761
+ else bool((hf_hc == hc).all())
762
+ )
763
+ if not same:
764
+ n_verify_bad += 1
765
+ hc = hf_hc
766
+ elif not self.executor:
767
+ hc, logits = self._layers_slow(nxt, cache, None if chip_in else pos)
768
+
769
+ if chip_in:
770
+ n_chip_tok += 1
771
+ nxt = (
772
+ self._decode_code_list(keep[:n_code].tolist())
773
+ if hc is None
774
+ else self.circuit_token(hc)
775
+ )
776
+ prev_owned = True
777
+ if nxt is None:
778
+ raise RuntimeError("Circuit relinquished ownership mid-chip-token")
779
+ else:
780
+ if prev_owned:
781
+ n_echo_text += 1
782
+ else:
783
+ n_qwen_text += 1
784
+ ct = (
785
+ self._decode_code_list(keep[:n_code].tolist())
786
+ if (keep is not None and not self.verify)
787
+ else self.circuit_token(hc)
788
+ )
789
+ if ct is not None and (self.fast or self.executor) and not check_head:
790
+ if pending_pos0 is None:
791
+ pending_pos0 = pos
792
+ pending_hcs.append(hc)
793
+ pos += 1
794
+ nxt = ct
795
+ prev_owned = True
796
+ else:
797
+ if self.fast or self.executor:
798
+ if pending_hcs:
799
+ pending_hcs.append(hc)
800
+ logits = self._qwen_chunk(
801
+ pending_hcs, cache, pending_pos0, full_head=check_head
802
+ )
803
+ pending_hcs.clear()
804
+ pending_pos0 = None
805
+ else:
806
+ logits = self._qwen_step(hc, cache, pos, full_head=check_head)
807
+ pos += 1
808
+ if ct is not None:
809
+ if check_head and int(torch.argmax(logits)) != ct:
810
+ head_mismatch += 1
811
+ nxt = ct
812
+ prev_owned = True
813
+ else:
814
+ q_logits = logits[: self.v0]
815
+ if n_calls >= max_calls or n_qwen_text >= max(16, max_new_text - 48):
816
+ q_logits = q_logits.clone()
817
+ q_logits[self.T.TCO] = float("-inf")
818
+ nxt = int(torch.argmax(q_logits))
819
+ prev_owned = False
820
+
821
+ dt = time.time() - t0
822
+ parsed = self.parse_turn(gen)
823
+ return {
824
+ "tokens": gen,
825
+ "text_tokens": n_qwen_text,
826
+ "echo_text_tokens": n_echo_text,
827
+ "chip_tokens": n_chip_tok,
828
+ "tool_calls_executed": n_calls,
829
+ "seconds": round(dt, 3),
830
+ "head_mismatch": head_mismatch,
831
+ "lock_error": lock_error,
832
+ "verify_mismatch": n_verify_bad if self.verify else None,
833
+ **parsed,
834
+ }
835
+
836
+ def text_of(self, ids: list[int]) -> str:
837
+ return self.tok.decode([i for i in ids if i < self.v0], skip_special_tokens=False)
838
+
839
+ def parse_turn(self, gen_ids: list[int]) -> dict:
840
+ raw = self.text_of(gen_ids)
841
+ calls = re.findall(r"<tool_call>\s*(.*?)\s*</tool_call>", raw, flags=re.DOTALL)
842
+ responses = re.findall(r"<tool_response>\n?(.*?)\n?</tool_response>", raw, flags=re.DOTALL)
843
+ cleaned = re.sub(
844
+ r"<tool_call>.*?</tool_response>\s*(?:<\|im_end\|>\s*<\|im_start\|>assistant\s*<think>\s*</think>\s*)?",
845
+ "\n\n",
846
+ raw,
847
+ flags=re.DOTALL,
848
+ )
849
+ cleaned = re.sub(r"<tool_call>.*$", "", cleaned, flags=re.DOTALL)
850
+ cleaned = re.sub(r"</?think>", "", cleaned)
851
+ for stop_s in ("<|im_end|>", "<|endoftext|>", "<|im_start|>"):
852
+ if stop_s in cleaned:
853
+ cleaned = cleaned.split(stop_s, 1)[0]
854
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
855
+ return {
856
+ "raw_text": raw,
857
+ "tool_calls": calls,
858
+ "tool_responses": responses,
859
+ "final_explanation": cleaned,
860
+ }
861
+
862
+
863
+ def main() -> None:
864
+ ap = argparse.ArgumentParser(description="Run Qwen3.5-Math-GraphCalc in-weights")
865
+ ap.add_argument(
866
+ "--ask",
867
+ default="Graph y = x^3 - 3*x from x = -2 to 2, identify its roots and turning points, and state the local maximum y-value.",
868
+ help="Math or graphing question to solve",
869
+ )
870
+ ap.add_argument("--pure-hf", action="store_true", help="Run un-fused HF Qwen3_5DecoderLayer weights (executor=False)")
871
+ ap.add_argument("--max-new-text", type=int, default=512)
872
+ args = ap.parse_args()
873
+
874
+ runner = CarbonGraphCalcRunner(MODEL_DIR, fast=True, executor=not args.pure_hf)
875
+ res = runner.generate(runner.prompt_ids(args.ask), max_new_text=args.max_new_text, check_lock=True)
876
+ if res["tool_responses"]:
877
+ print("=== IN-WEIGHTS RV32 <tool_response> ===")
878
+ print(res["tool_responses"][0])
879
+ print()
880
+ print("=== STEP-BY-STEP TUTOR EXPLANATION ===")
881
+ print(res["final_explanation"])
882
+ print(f"\n[chip_tokens={res['chip_tokens']} | lock_error={res['lock_error']} | {res['seconds']}s]")
883
+
884
+
885
+ if __name__ == "__main__":
886
+ main()
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eeb7adf50a06ace136ccec295ee33e6e88deac189bffab57052415de813a7dcf
3
+ size 64037666
tokenizer_config.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|endoftext|>",
10
+ "errors": "replace",
11
+ "extra_special_tokens": [
12
+ "<|im_start|>",
13
+ "<|im_end|>",
14
+ "<|object_ref_start|>",
15
+ "<|object_ref_end|>",
16
+ "<|box_start|>",
17
+ "<|box_end|>",
18
+ "<|quad_start|>",
19
+ "<|quad_end|>",
20
+ "<|vision_start|>",
21
+ "<|vision_end|>",
22
+ "<|vision_pad|>",
23
+ "<|image_pad|>",
24
+ "<|video_pad|>"
25
+ ],
26
+ "image_token": "<|image_pad|>",
27
+ "is_local": false,
28
+ "local_files_only": true,
29
+ "merges_file": null,
30
+ "model_max_length": 262144,
31
+ "model_specific_special_tokens": {
32
+ "audio_bos_token": "<|audio_start|>",
33
+ "audio_eos_token": "<|audio_end|>",
34
+ "audio_token": "<|audio_pad|>",
35
+ "image_token": "<|image_pad|>",
36
+ "video_token": "<|video_pad|>",
37
+ "vision_bos_token": "<|vision_start|>",
38
+ "vision_eos_token": "<|vision_end|>"
39
+ },
40
+ "pad_token": "<|endoftext|>",
41
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
42
+ "split_special_tokens": false,
43
+ "tokenizer_class": "Qwen3_5Tokenizer",
44
+ "unk_token": null,
45
+ "video_token": "<|video_pad|>",
46
+ "vision_bos_token": "<|vision_start|>",
47
+ "vision_eos_token": "<|vision_end|>"
48
+ }