nx_solve_steps_lib.nx source
↩ module page · 218 lines · 9921 B
1// nx_solve_steps_lib.nx -- A SOLVE, WORKED STEP BY STEP, EACH STEP NAMING THE RULE IT APPLIED.
2//
3// WHY THIS EXISTS (measured 2026-09-03). nx_capsearch over 7,305 sources with corpus_complete=1 shows the
4// estate ALREADY HAS a solver: nx_calc_solve ships nx_calc_solve_linear and nx_calc_solve_quadratic and
5// returns roots. What it does not produce is the WORKING. The rival the operator named states its own
6// product as "get step-by-step algebra answers instantly", so the gap against it was never the arithmetic
7// -- it was the explanation. This organ is that join, and it is deliberately a SEPARATE lib rather than a
8// change to the solver, because "what is the answer" and "how would a person get there" are two questions
9// and one organ should do one thing.
10//
11// EVERY STEP CARRIES ITS RULE. A worked solution that shows lines without naming the operation is a
12// magic trick; a learner cannot tell a legal move from a wrong one. So each row is RULE then LaTeX, and
13// the LaTeX goes through nx_mathml at render time, so the page shows real typeset mathematics with no
14// third-party typesetter anywhere in the path.
15//
16// ★EXACT WHERE EXACT IS POSSIBLE, AND HONEST WHERE IT IS NOT. Rational answers are reduced by gcd and
17// printed as fractions, never as truncated decimals. When a quadratic root is irrational this says so and
18// prints the EXACT radical form rather than a rounded decimal pretending to be the answer. A solver that
19// silently rounds is teaching that mathematics is approximately true.
20//
21// license_tier: ORIGINAL
22import "syscalls.nx"
23import "nx_isqrt.nx"
24
25const SS_OK: i64 = 0
26const SS_ERR_SHORT: i64 = 0 - 1
27const SS_ERR_NOT_LINEAR: i64 = 0 - 2 // a == 0 in a*x + b = c is not an equation in x
28const SS_ERR_NOT_QUAD: i64 = 0 - 3 // a == 0 in a*x^2 + bx + c is the linear case, refuse by name
29const SS_MAXSTEPS: i64 = 16
30
31func ss_slen(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { i = i + 1 } return i }
32
33func ss_put(b: *u8, off: i64, cap: i64, s: *u8) -> i64 {
34 if off < 0 { return off }
35 let n: i64 = ss_slen(s)
36 if off + n >= cap { return SS_ERR_SHORT }
37 var i: i64 = 0
38 while i < n { b[off + i] = s[i]; i = i + 1 }
39 return off + n
40}
41
42func ss_num(b: *u8, off: i64, cap: i64, v: i64) -> i64 {
43 if off < 0 { return off }
44 var o: i64 = off
45 var m: i64 = v
46 if m < 0 { o = ss_put(b, o, cap, "-" as *u8); m = 0 - m }
47 let t: *u8 = sys_mmap(32)
48 var k: i64 = 0
49 if m == 0 { t[0] = 48 as u8; k = 1 }
50 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
51 var i: i64 = 0
52 while i < k {
53 if o < 0 { i = k } else {
54 if o + 1 >= cap { o = SS_ERR_SHORT } else { b[o] = t[k - 1 - i]; o = o + 1; i = i + 1 }
55 }
56 }
57 sys_munmap(t, 32)
58 return o
59}
60
61func ss_gcd(a: i64, b: i64) -> i64 {
62 var x: i64 = a
63 var y: i64 = b
64 if x < 0 { x = 0 - x }
65 if y < 0 { y = 0 - y }
66 while y != 0 { let t: i64 = x % y; x = y; y = t }
67 if x == 0 { return 1 }
68 return x
69}
70
71// Emit a rational as reduced LaTeX: an integer when it divides, a \frac otherwise, with the sign carried
72// on the numerator so a reader never meets a negative denominator.
73func ss_rational(b: *u8, off: i64, cap: i64, num: i64, den: i64) -> i64 {
74 var n: i64 = num
75 var d: i64 = den
76 if d < 0 { n = 0 - n; d = 0 - d }
77 let g: i64 = ss_gcd(n, d)
78 n = n / g
79 d = d / g
80 if d == 1 { return ss_num(b, off, cap, n) }
81 var o: i64 = ss_put(b, off, cap, "\\frac{" as *u8)
82 o = ss_num(b, o, cap, n)
83 o = ss_put(b, o, cap, "}{" as *u8)
84 o = ss_num(b, o, cap, d)
85 return ss_put(b, o, cap, "}" as *u8)
86}
87
88// One row of the worked solution: RULE, a tab, the LaTeX, a newline. Chosen so a caller splits on the tab
89// and hands column two straight to the typesetter with no parsing of mathematics in the page layer.
90func ss_row(b: *u8, off: i64, cap: i64, rule: *u8) -> i64 {
91 var o: i64 = ss_put(b, off, cap, rule)
92 return ss_put(b, o, cap, "\t" as *u8)
93}
94
95func ss_end(b: *u8, off: i64, cap: i64) -> i64 { return ss_put(b, off, cap, "\n" as *u8) }
96
97// Write a*x + b (with the sign of b folded in) into the buffer.
98func ss_linear_lhs(b: *u8, off: i64, cap: i64, a: i64, bb: i64) -> i64 {
99 var o: i64 = off
100 if a != 1 { if a == 0 - 1 { o = ss_put(b, o, cap, "-" as *u8) } else { o = ss_num(b, o, cap, a) } }
101 o = ss_put(b, o, cap, "x" as *u8)
102 if bb > 0 { o = ss_put(b, o, cap, " + " as *u8); o = ss_num(b, o, cap, bb) }
103 if bb < 0 { o = ss_put(b, o, cap, " - " as *u8); o = ss_num(b, o, cap, 0 - bb) }
104 return o
105}
106
107// SOLVE a*x + b = c, SHOWING THE WORK. Returns bytes written, or a negative SS_ERR_*.
108func ss_steps_linear(a: i64, b0: i64, c: i64, out: *u8, cap: i64) -> i64 {
109 // A refusal by name, because a*x + b = c with a == 0 is not an equation in x at all: it is either a
110 // contradiction or an identity, and answering it with a number would be a lie about what was asked.
111 if a == 0 { return SS_ERR_NOT_LINEAR }
112 var o: i64 = 0
113
114 o = ss_row(out, o, cap, "the equation as given" as *u8)
115 o = ss_linear_lhs(out, o, cap, a, b0)
116 o = ss_put(out, o, cap, " = " as *u8)
117 o = ss_num(out, o, cap, c)
118 o = ss_end(out, o, cap)
119
120 o = ss_row(out, o, cap, "subtract the constant from both sides, which keeps the equality true" as *u8)
121 if a != 1 { if a == 0 - 1 { o = ss_put(out, o, cap, "-" as *u8) } else { o = ss_num(out, o, cap, a) } }
122 o = ss_put(out, o, cap, "x = " as *u8)
123 o = ss_num(out, o, cap, c - b0)
124 o = ss_end(out, o, cap)
125
126 o = ss_row(out, o, cap, "divide both sides by the coefficient of x" as *u8)
127 o = ss_put(out, o, cap, "x = " as *u8)
128 o = ss_put(out, o, cap, "\\frac{" as *u8)
129 o = ss_num(out, o, cap, c - b0)
130 o = ss_put(out, o, cap, "}{" as *u8)
131 o = ss_num(out, o, cap, a)
132 o = ss_put(out, o, cap, "}" as *u8)
133 o = ss_end(out, o, cap)
134
135 o = ss_row(out, o, cap, "reduce the fraction to lowest terms, exactly and with no rounding" as *u8)
136 o = ss_put(out, o, cap, "x = " as *u8)
137 o = ss_rational(out, o, cap, c - b0, a)
138 o = ss_end(out, o, cap)
139 return o
140}
141
142// SOLVE a*x^2 + b*x + c = 0, SHOWING THE WORK, and telling the truth about irrational roots.
143func ss_steps_quadratic(a: i64, b: i64, c: i64, out: *u8, cap: i64) -> i64 {
144 if a == 0 { return SS_ERR_NOT_QUAD }
145 var o: i64 = 0
146
147 o = ss_row(out, o, cap, "the equation as given" as *u8)
148 if a != 1 { o = ss_num(out, o, cap, a) }
149 o = ss_put(out, o, cap, "x^{2}" as *u8)
150 if b > 0 { o = ss_put(out, o, cap, " + " as *u8); if b != 1 { o = ss_num(out, o, cap, b) } o = ss_put(out, o, cap, "x" as *u8) }
151 if b < 0 { o = ss_put(out, o, cap, " - " as *u8); if b != 0 - 1 { o = ss_num(out, o, cap, 0 - b) } o = ss_put(out, o, cap, "x" as *u8) }
152 if c > 0 { o = ss_put(out, o, cap, " + " as *u8); o = ss_num(out, o, cap, c) }
153 if c < 0 { o = ss_put(out, o, cap, " - " as *u8); o = ss_num(out, o, cap, 0 - c) }
154 o = ss_put(out, o, cap, " = 0" as *u8)
155 o = ss_end(out, o, cap)
156
157 o = ss_row(out, o, cap, "the quadratic formula, which solves every equation of this shape" as *u8)
158 o = ss_put(out, o, cap, "x = \\frac{-b \\pm \\sqrt{b^{2} - 4ac}}{2a}" as *u8)
159 o = ss_end(out, o, cap)
160
161 let disc: i64 = b * b - 4 * a * c
162 o = ss_row(out, o, cap, "compute the discriminant, which decides how many real roots there are" as *u8)
163 o = ss_put(out, o, cap, "b^{2} - 4ac = " as *u8)
164 o = ss_num(out, o, cap, disc)
165 o = ss_end(out, o, cap)
166
167 if disc < 0 {
168 // NOT AN ERROR, AND NOT AN EMPTY ANSWER: a negative discriminant is a real fact about the
169 // equation, and saying so is the correct result rather than a failure to produce roots.
170 o = ss_row(out, o, cap, "the discriminant is negative, so there are NO real roots" as *u8)
171 o = ss_put(out, o, cap, "x \\notin \\mathbb{R}" as *u8)
172 o = ss_end(out, o, cap)
173 return o
174 }
175
176 let r: i64 = nx_isqrt(disc)
177 if r * r == disc {
178 o = ss_row(out, o, cap, "the discriminant is a perfect square, so both roots are EXACT rationals" as *u8)
179 o = ss_put(out, o, cap, "\\sqrt{" as *u8)
180 o = ss_num(out, o, cap, disc)
181 o = ss_put(out, o, cap, "} = " as *u8)
182 o = ss_num(out, o, cap, r)
183 o = ss_end(out, o, cap)
184
185 o = ss_row(out, o, cap, "first root, reduced to lowest terms" as *u8)
186 o = ss_put(out, o, cap, "x = " as *u8)
187 o = ss_rational(out, o, cap, 0 - b + r, 2 * a)
188 o = ss_end(out, o, cap)
189
190 o = ss_row(out, o, cap, "second root, reduced to lowest terms" as *u8)
191 o = ss_put(out, o, cap, "x = " as *u8)
192 o = ss_rational(out, o, cap, 0 - b - r, 2 * a)
193 o = ss_end(out, o, cap)
194 return o
195 }
196
197 // ★THE HONEST BRANCH. The roots are irrational, so the EXACT surd is the answer. A decimal here would
198 // be a rounded number wearing the clothes of an exact one, which is precisely what an estate with no
199 // float in its path should refuse to teach.
200 o = ss_row(out, o, cap, "the discriminant is not a perfect square, so the roots are IRRATIONAL and the exact form is the answer" as *u8)
201 o = ss_put(out, o, cap, "x = \\frac{" as *u8)
202 o = ss_num(out, o, cap, 0 - b)
203 o = ss_put(out, o, cap, " \\pm \\sqrt{" as *u8)
204 o = ss_num(out, o, cap, disc)
205 o = ss_put(out, o, cap, "}}{" as *u8)
206 o = ss_num(out, o, cap, 2 * a)
207 o = ss_put(out, o, cap, "}" as *u8)
208 o = ss_end(out, o, cap)
209 return o
210}
211
212func ss_err_name(e: i64) -> *u8 {
213 if e == SS_OK { return "OK" as *u8 }
214 if e == SS_ERR_SHORT { return "OUTPUT-BUFFER-TOO-SMALL" as *u8 }
215 if e == SS_ERR_NOT_LINEAR { return "NOT-AN-EQUATION-IN-X-the-coefficient-of-x-is-zero" as *u8 }
216 if e == SS_ERR_NOT_QUAD { return "NOT-QUADRATIC-the-leading-coefficient-is-zero-solve-it-as-linear" as *u8 }
217 return "UNKNOWN-ERROR" as *u8
218}