nx_eqparse_lib.nx source
↩ module page · 133 lines · 6113 B
1// nx_eqparse_lib.nx -- READ AN EQUATION A PERSON ACTUALLY TYPED, AND NORMALISE IT.
2//
3// WHY THIS EXISTS (2026-09-03, and the operator is the reason). The /math surface shipped as a page whose
4// formulas were typeset at PUBLISH time and whose worked solutions were three hardcoded equations. The
5// operator's response was exact: "/math is just static output how does that match what i asked for". It
6// did not. A visitor could not type anything, and a page that demonstrates a capability is not the same
7// artifact as a page that PROVIDES it.
8//
9// The solver existed (nx_calc_solve), the worked steps existed (nx_solve_steps_lib), the typesetter
10// existed (nx_mathml_lib). The missing rung between a human and all three was this: turning the text
11// "3x+4=19" into coefficients. That is the whole gap, and it is why the page could only ever be a brochure.
12//
13// SCOPE, DECLARED: polynomials in a single variable x up to degree two, integer coefficients, both sides
14// of one equals sign, with implicit coefficients (x means 1x), unary signs, and ^2 for the square. It does
15// NOT do parentheses, division, other variables, or decimals. Anything outside that is REFUSED BY NAME so
16// a visitor is told what was not understood rather than handed a confident answer to a different question.
17//
18// license_tier: ORIGINAL
19import "syscalls.nx"
20
21const EQ_OK: i64 = 0
22const EQ_ERR_EMPTY: i64 = 0 - 1
23const EQ_ERR_NO_EQUALS: i64 = 0 - 2 // no '=' at all: an expression is not an equation
24const EQ_ERR_TWO_EQUALS: i64 = 0 - 3
25const EQ_ERR_BADCHAR: i64 = 0 - 4 // a character outside the declared scope
26const EQ_ERR_DEGREE: i64 = 0 - 5 // a power above 2
27const EQ_ERR_NO_X: i64 = 0 - 6 // nothing to solve for
28const EQ_MAXLEN: i64 = 512
29// A typed coefficient beyond this is refused rather than silently overflowing the fixed-point arithmetic
30// downstream. Named, because a bound a caller cannot see is a bound that surprises them.
31const EQ_MAX_COEF: i64 = 1000000
32
33func eq_err_name(e: i64) -> *u8 {
34 if e == EQ_OK { return "OK" as *u8 }
35 if e == EQ_ERR_EMPTY { return "EMPTY-nothing-was-typed" as *u8 }
36 if e == EQ_ERR_NO_EQUALS { return "NO-EQUALS-SIGN-an-expression-is-not-an-equation" as *u8 }
37 if e == EQ_ERR_TWO_EQUALS { return "MORE-THAN-ONE-EQUALS-SIGN" as *u8 }
38 if e == EQ_ERR_BADCHAR { return "CHARACTER-OUTSIDE-THE-SUPPORTED-SUBSET-only-digits-x-plus-minus-caret-2-and-equals" as *u8 }
39 if e == EQ_ERR_DEGREE { return "POWER-ABOVE-TWO-this-subset-solves-linear-and-quadratic-only" as *u8 }
40 if e == EQ_ERR_NO_X { return "NO-X-IN-THE-EQUATION-there-is-nothing-to-solve-for" as *u8 }
41 return "UNKNOWN" as *u8
42}
43
44func eq_is_digit(c: i64) -> i64 { if c >= 48 { if c <= 57 { return 1 } } return 0 }
45
46// Parse into coefficients of x^2, x^1, x^0 for the equation moved entirely to the left of the equals.
47// Everything on the RIGHT is subtracted, which is the same move a person makes by hand.
48func eq_parse(src: *u8, n: i64, out: *i64) -> i64 {
49 out[0] = 0
50 out[1] = 0
51 out[2] = 0
52 if n <= 0 { return EQ_ERR_EMPTY }
53 if n > EQ_MAXLEN { return EQ_ERR_BADCHAR }
54
55 var side: i64 = 1 // +1 left of '=', -1 right of it
56 var seen_eq: i64 = 0
57 var sign: i64 = 1
58 var have_digits: i64 = 0
59 var mag: i64 = 0
60 var i: i64 = 0
61 var pending: i64 = 0 // 1 once a term is open (a sign, digits, or an x has been seen)
62
63 while i <= n {
64 var c: i64 = 0
65 if i < n { c = src[i] as i64 }
66 // space and tab are ignored so "3x + 4 = 19" reads the same as "3x+4=19"
67 if c == 32 { i = i + 1 } else {
68 if c == 9 { i = i + 1 } else {
69
70 if eq_is_digit(c) == 1 {
71 mag = mag * 10 + (c - 48)
72 if mag > EQ_MAX_COEF { return EQ_ERR_BADCHAR }
73 have_digits = 1
74 pending = 1
75 i = i + 1
76 } else {
77 // A term ENDS here. Work out its degree by looking at what follows.
78 var deg: i64 = 0
79 var consumed: i64 = 0
80 if c == 120 { // 'x'
81 deg = 1
82 consumed = 1
83 // optional ^2
84 if i + 1 < n {
85 if src[i+1] == (94 as u8) { // '^'
86 if i + 2 < n {
87 let p: i64 = src[i+2] as i64
88 if p == 50 { deg = 2; consumed = 3 } else {
89 if p == 49 { deg = 1; consumed = 3 } else { return EQ_ERR_DEGREE }
90 }
91 } else { return EQ_ERR_DEGREE }
92 }
93 }
94 pending = 1
95 }
96 if deg > 0 {
97 var coef: i64 = mag
98 if have_digits == 0 { coef = 1 } // a bare x means one x
99 out[deg] = out[deg] + side * sign * coef
100 mag = 0
101 have_digits = 0
102 sign = 1
103 i = i + consumed
104 pending = 0
105 } else {
106 // Not an x: flush any pending constant, then handle the operator.
107 if pending == 1 { if have_digits == 1 {
108 out[0] = out[0] + side * sign * mag
109 } }
110 mag = 0
111 have_digits = 0
112 sign = 1
113 pending = 0
114
115 if i >= n { i = i + 1 } else {
116 if c == 43 { sign = 1; i = i + 1 } else { // '+'
117 if c == 45 { sign = 0 - 1; i = i + 1 } else { // '-'
118 if c == 61 { // '='
119 if seen_eq == 1 { return EQ_ERR_TWO_EQUALS }
120 seen_eq = 1
121 side = 0 - 1
122 i = i + 1
123 } else { return EQ_ERR_BADCHAR } } }
124 }
125 }
126 }
127 } }
128 }
129
130 if seen_eq == 0 { return EQ_ERR_NO_EQUALS }
131 if out[1] == 0 { if out[2] == 0 { return EQ_ERR_NO_X } }
132 return EQ_OK
133}