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