code wiki / _hdl_build / nx_proof.nx
nx_proof.nx source
↩ module page · 299 lines · 12759 B
1// nx_proof.nx -- Checks for grammatical and stylistic errors in prose without relying on external dictionaries.
2// nx_proof.nx -- GRAMMAR + STYLE PROOFING for the Nishi Office suite (office axis "Grammar and style
3// proofing", sym pf_check). DELIBERATELY DICTIONARY-FREE: every rule here is decidable from the text
4// itself, so the organ has no lexicon to ship, stale, or lie about -- spelling is a SEPARATE axis with a
5// separate derived lexicon (nx_spell). What that buys: these findings are never wrong because a word was
6// missing from a word list; a flag here is a real structural defect in the prose.
7//
8// RULES (each independently gate-proven, each with a negative control so the rule is proven able to FIRE
9// and able to STAY SILENT):
10// 1 doubled word "the the" -- case-insensitive, word-boundary exact
11// 2 double space "a b" -- inside a sentence, not leading indentation
12// 3 lowercase start ". foo" -- a sentence opening lowercase
13// 4 no terminal punct text ending bare -- last non-space char is not . ! ? : ; or a closer
14// 5 unbalanced bracket "( [ {" -- depth tracked, reports the unclosed count
15// 6 unbalanced quote '"' -- odd count of double quotes
16// 7 trailing whitespace "foo \n" -- space/tab immediately before a newline
17//
18// MARKUP IS SKIPPED: a run of <...> is not prose, so rules never fire inside a tag (an HTML/XML doc would
19// otherwise drown in false "lowercase start" and "no terminal punctuation" findings).
20// nx_proof check <file> -- report every finding as LINE:COL RULE detail
21// nx_proof (argless) -- selftest
22// expect_exit: 0 license_tier: ORIGINAL
23import "nx_syscalls.nx"
24
25const PF_CAP: i64 = 1048576
26const PF_RULE_DBL: i64 = 1
27const PF_RULE_SP2: i64 = 2
28const PF_RULE_LOW: i64 = 3
29const PF_RULE_END: i64 = 4
30const PF_RULE_BRK: i64 = 5
31const PF_RULE_QUO: i64 = 6
32const PF_RULE_TWS: i64 = 7
33
34func pf_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
35func pf_num(v: i64) -> i64 {
36 if v == 0 { pf_puts("0" as *u8); return 0 }
37 let t: *u8 = sys_mmap(32)
38 var m: i64 = v
39 var k: i64 = 0
40 if m < 0 { pf_puts("-" as *u8); m = 0 - m }
41 while m > 0 { t[k] = (48 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 }
42 let r: *u8 = sys_mmap(32)
43 var i: i64 = 0
44 while k > 0 { k = k - 1; r[i] = t[k]; i = i + 1 }
45 sys_write(1, r, i)
46 return 0
47}
48
49func pf_is_alpha(c: i64) -> i64 {
50 if c >= 65 { if c <= 90 { return 1 } }
51 if c >= 97 { if c <= 122 { return 1 } }
52 return 0
53}
54func pf_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
55func pf_is_upper(c: i64) -> i64 { if c >= 65 { if c <= 90 { return 1 } } return 0 }
56func pf_is_space(c: i64) -> i64 { if c == 32 { return 1 } if c == 9 { return 1 } return 0 }
57
58// a sentence terminator, or a closer that legitimately ends a block
59func pf_is_term(c: i64) -> i64 {
60 if c == 46 { return 1 }
61 if c == 33 { return 1 }
62 if c == 63 { return 1 }
63 if c == 58 { return 1 }
64 if c == 59 { return 1 }
65 if c == 41 { return 1 }
66 if c == 93 { return 1 }
67 if c == 125 { return 1 }
68 if c == 34 { return 1 }
69 return 0
70}
71
72func pf_rule_name(r: i64) -> *u8 {
73 if r == PF_RULE_DBL { return "doubled-word" as *u8 }
74 if r == PF_RULE_SP2 { return "double-space" as *u8 }
75 if r == PF_RULE_LOW { return "lowercase-sentence-start" as *u8 }
76 if r == PF_RULE_END { return "no-terminal-punctuation" as *u8 }
77 if r == PF_RULE_BRK { return "unbalanced-bracket" as *u8 }
78 if r == PF_RULE_QUO { return "unbalanced-quote" as *u8 }
79 if r == PF_RULE_TWS { return "trailing-whitespace" as *u8 }
80 return "unknown" as *u8
81}
82
83// THE CHECKER. Returns the number of findings; counts[r] gets the per-rule tally (r = 1..7).
84// emit=1 prints each finding as LINE:COL rule.
85func pf_check(text: *u8, n: i64, counts: *i64, emit: i64) -> i64 {
86 var total: i64 = 0
87 var line: i64 = 1
88 var col: i64 = 1
89 var depth: i64 = 0
90 var quotes: i64 = 0
91 var in_markup: i64 = 0
92 var i: i64 = 0
93 // start-of-sentence tracking: we are AT a sentence start until the first prose char of it
94 var want_upper: i64 = 1
95 var last_prose: i64 = 0
96 var any_prose: i64 = 0
97
98 while i < n {
99 let c: i64 = text[i] as i64
100
101 if c == 60 { in_markup = 1 }
102 if in_markup == 1 {
103 if c == 62 { in_markup = 0 }
104 if c == 10 { line = line + 1; col = 0 }
105 i = i + 1
106 col = col + 1
107 continue
108 }
109
110 // rule 7: whitespace immediately before a newline
111 if c == 10 {
112 if i > 0 { if pf_is_space(text[i - 1] as i64) == 1 {
113 total = total + 1
114 counts[PF_RULE_TWS] = counts[PF_RULE_TWS] + 1
115 if emit == 1 { pf_num(line); pf_puts(":" as *u8); pf_num(col); pf_puts(" " as *u8); pf_puts(pf_rule_name(PF_RULE_TWS)); pf_puts("\n" as *u8) }
116 } }
117 line = line + 1
118 col = 0
119 i = i + 1
120 col = col + 1
121 continue
122 }
123
124 // rule 2: two spaces inside a line that has already started (leading indent is not a defect)
125 if c == 32 { if i + 1 < n { if (text[i + 1] as i64) == 32 { if any_prose == 1 {
126 total = total + 1
127 counts[PF_RULE_SP2] = counts[PF_RULE_SP2] + 1
128 if emit == 1 { pf_num(line); pf_puts(":" as *u8); pf_num(col); pf_puts(" " as *u8); pf_puts(pf_rule_name(PF_RULE_SP2)); pf_puts("\n" as *u8) }
129 } } } }
130
131 // rules 5/6: bracket depth + quote parity
132 if c == 40 { depth = depth + 1 }
133 if c == 91 { depth = depth + 1 }
134 if c == 123 { depth = depth + 1 }
135 if c == 41 { depth = depth - 1 }
136 if c == 93 { depth = depth - 1 }
137 if c == 125 { depth = depth - 1 }
138 if c == 34 { quotes = quotes + 1 }
139
140 if pf_is_alpha(c) == 1 {
141 any_prose = 1
142 // rule 3: the first letter after a terminator must be uppercase
143 if want_upper == 1 {
144 if pf_is_upper(c) == 0 {
145 total = total + 1
146 counts[PF_RULE_LOW] = counts[PF_RULE_LOW] + 1
147 if emit == 1 { pf_num(line); pf_puts(":" as *u8); pf_num(col); pf_puts(" " as *u8); pf_puts(pf_rule_name(PF_RULE_LOW)); pf_puts("\n" as *u8) }
148 }
149 want_upper = 0
150 }
151 // rule 1: doubled word -- compare this word with the previous one
152 var ws: i64 = i
153 var we: i64 = i
154 while we < n { if pf_is_alpha(text[we] as i64) == 0 { break } we = we + 1 }
155 // walk back over exactly one space to the previous word
156 var ps: i64 = 0
157 var pe: i64 = 0
158 if ws >= 2 { if (text[ws - 1] as i64) == 32 {
159 pe = ws - 1
160 ps = pe
161 while ps > 0 { if pf_is_alpha(text[ps - 1] as i64) == 0 { break } ps = ps - 1 }
162 } }
163 if pe > ps {
164 let la: i64 = we - ws
165 let lb: i64 = pe - ps
166 if la == lb { if la > 1 {
167 var same: i64 = 1
168 var k: i64 = 0
169 while k < la { if pf_lower(text[ws + k] as i64) != pf_lower(text[ps + k] as i64) { same = 0; k = la } else { k = k + 1 } }
170 if same == 1 {
171 total = total + 1
172 counts[PF_RULE_DBL] = counts[PF_RULE_DBL] + 1
173 if emit == 1 { pf_num(line); pf_puts(":" as *u8); pf_num(col); pf_puts(" " as *u8); pf_puts(pf_rule_name(PF_RULE_DBL)); pf_puts("\n" as *u8) }
174 }
175 } }
176 }
177 last_prose = c
178 i = we
179 col = col + (we - ws)
180 continue
181 }
182
183 if pf_is_term(c) == 1 { if c != 34 { want_upper = 1 } }
184 if pf_is_space(c) == 0 { if c != 10 { last_prose = c; any_prose = 1 } }
185
186 i = i + 1
187 col = col + 1
188 }
189
190 // rule 4: the document must end on a terminator
191 if any_prose == 1 { if pf_is_term(last_prose) == 0 {
192 total = total + 1
193 counts[PF_RULE_END] = counts[PF_RULE_END] + 1
194 if emit == 1 { pf_num(line); pf_puts(":0 " as *u8); pf_puts(pf_rule_name(PF_RULE_END)); pf_puts("\n" as *u8) }
195 } }
196 // rule 5 tail
197 if depth != 0 {
198 total = total + 1
199 counts[PF_RULE_BRK] = counts[PF_RULE_BRK] + 1
200 if emit == 1 { pf_puts("0:0 " as *u8); pf_puts(pf_rule_name(PF_RULE_BRK)); pf_puts(" depth=" as *u8); pf_num(depth); pf_puts("\n" as *u8) }
201 }
202 // rule 6 tail
203 if quotes - (quotes / 2) * 2 != 0 {
204 total = total + 1
205 counts[PF_RULE_QUO] = counts[PF_RULE_QUO] + 1
206 if emit == 1 { pf_puts("0:0 " as *u8); pf_puts(pf_rule_name(PF_RULE_QUO)); pf_puts("\n" as *u8) }
207 }
208 return total
209}
210
211func pf_readfile(path: *u8, buf: *u8, cap: i64) -> i64 {
212 let fd: i64 = sys_openat_rd(path)
213 if fd < 0 { return 0 - 1 }
214 let n: i64 = sys_read(fd, buf, cap - 1)
215 sys_close(fd)
216 if n < 0 { return 0 - 1 }
217 buf[n] = 0 as u8
218 return n
219}
220
221func pf_zero(c: *i64) -> i64 { var i: i64 = 0; while i < 16 { c[i] = 0; i = i + 1 } return 0 }
222
223func main(argc: i64, argv: *i64) -> i64 {
224 if argc >= 3 {
225 let v: *u8 = argv[1] as *u8
226 var isc: i64 = 1
227 let want: *u8 = "check" as *u8
228 var q: i64 = 0
229 while want[q] != (0 as u8) { if v[q] != want[q] { isc = 0 } q = q + 1 }
230 if isc == 1 {
231 let buf: *u8 = sys_mmap(PF_CAP)
232 let n: i64 = pf_readfile(argv[2] as *u8, buf, PF_CAP)
233 if n < 0 { pf_puts("PROOF-READ-FAIL\n" as *u8); return 2 }
234 let counts: *i64 = sys_mmap(128) as *i64
235 pf_zero(counts)
236 let t: i64 = pf_check(buf, n, counts, 1)
237 pf_puts("NX-PROOF findings=" as *u8); pf_num(t); pf_puts("\n" as *u8)
238 return 0
239 }
240 }
241
242 // ---- selftest: every rule proven able to FIRE, and a clean text proven to stay SILENT ----
243 var pass: i64 = 0
244 var tot: i64 = 0
245 let c: *i64 = sys_mmap(128) as *i64
246
247 pf_zero(c)
248 pf_check("The the cat sat.\n" as *u8, 16, c, 0)
249 tot = tot + 1
250 if c[PF_RULE_DBL] == 1 { pass = pass + 1; pf_puts(" PASS R1 doubled word fires\n" as *u8) } else { pf_puts(" FAIL R1\n" as *u8) }
251
252 pf_zero(c)
253 pf_check("A b.\n" as *u8, 6, c, 0)
254 tot = tot + 1
255 if c[PF_RULE_SP2] == 1 { pass = pass + 1; pf_puts(" PASS R2 double space fires\n" as *u8) } else { pf_puts(" FAIL R2\n" as *u8) }
256
257 pf_zero(c)
258 pf_check("One. two.\n" as *u8, 10, c, 0)
259 tot = tot + 1
260 if c[PF_RULE_LOW] == 1 { pass = pass + 1; pf_puts(" PASS R3 lowercase sentence start fires\n" as *u8) } else { pf_puts(" FAIL R3\n" as *u8) }
261
262 pf_zero(c)
263 pf_check("No terminator here\n" as *u8, 19, c, 0)
264 tot = tot + 1
265 if c[PF_RULE_END] == 1 { pass = pass + 1; pf_puts(" PASS R4 missing terminal punctuation fires\n" as *u8) } else { pf_puts(" FAIL R4\n" as *u8) }
266
267 pf_zero(c)
268 pf_check("A (b.\n" as *u8, 6, c, 0)
269 tot = tot + 1
270 if c[PF_RULE_BRK] == 1 { pass = pass + 1; pf_puts(" PASS R5 unbalanced bracket fires\n" as *u8) } else { pf_puts(" FAIL R5\n" as *u8) }
271
272 pf_zero(c)
273 pf_check("A \"b.\n" as *u8, 6, c, 0)
274 tot = tot + 1
275 if c[PF_RULE_QUO] == 1 { pass = pass + 1; pf_puts(" PASS R6 unbalanced quote fires\n" as *u8) } else { pf_puts(" FAIL R6\n" as *u8) }
276
277 pf_zero(c)
278 pf_check("Trailing space. \n" as *u8, 17, c, 0)
279 tot = tot + 1
280 if c[PF_RULE_TWS] == 1 { pass = pass + 1; pf_puts(" PASS R7 trailing whitespace fires\n" as *u8) } else { pf_puts(" FAIL R7\n" as *u8) }
281
282 // NEGATIVE CONTROL: clean prose must produce ZERO findings, else every rule above is meaningless
283 pf_zero(c)
284 let clean: i64 = pf_check("The cat sat. It slept.\n" as *u8, 23, c, 0)
285 tot = tot + 1
286 if clean == 0 { pass = pass + 1; pf_puts(" PASS NEG clean prose -> 0 findings\n" as *u8) } else { pf_puts(" FAIL NEG clean prose flagged " as *u8); pf_num(clean); pf_puts("\n" as *u8) }
287
288 // MARKUP CONTROL: the same defects INSIDE a tag must NOT fire
289 pf_zero(c)
290 let mk: i64 = pf_check("<a href=\"x y\">Ok.</a>\n" as *u8, 23, c, 0)
291 tot = tot + 1
292 if mk == 0 { pass = pass + 1; pf_puts(" PASS MARKUP defects inside a tag are skipped\n" as *u8) } else { pf_puts(" FAIL MARKUP flagged " as *u8); pf_num(mk); pf_puts("\n" as *u8) }
293
294 pf_puts("NX-PROOF-GATE " as *u8); pf_num(pass); pf_puts("/" as *u8); pf_num(tot)
295 if pass == tot { pf_puts(" GREEN\n" as *u8); return 0 }
296 pf_puts(" RED\n" as *u8)
297 return 1
298}
299