code wiki / _hdl_build / nx_canoncat.nx
nx_canoncat.nx source
↩ module page · 249 lines · 12307 B
1// nx_canoncat -- concatenate N canon files into one, renumbering feature part references.
2//
3// ★THE ENTIRE "PERFECT ONE UNIT, THEN AGGREGATE" MECHANISM IS THIS ORGAN (plan rung S4). A skull, a
4// hand, a torso are each authored and gated as their OWN canon; composition into a body is a cat.
5// That is only true because of one discipline enforced here:
6//
7// P and R rows pass through BYTE-VERBATIM. This organ does not parse them beyond the leading
8// letter, so when the canon grows fields (P grew mat, then rotZ, then rotY) concatenation keeps
9// working without an edit here -- an organ that re-emitted parsed fields would silently STRIP any
10// field added after it was written, which is exactly how format evolution corrupts a pipeline.
11//
12// F rows are the one exception, because they carry the ONLY cross-row reference in the format: a
13// part INDEX, where parts are numbered by order of P appearance. File k's F rows are shifted by
14// the P count of files 0..k-1. Everything after the index is passed through verbatim for the same
15// reason as above.
16//
17// Fail-closed on purpose:
18// - an F row referencing a part its OWN file never declared is a corrupt canon -> refuse (exit 5),
19// because shifting a dangling index would turn one file's corruption into another file's feature.
20// - an unknown row type -> refuse (exit 6). A row this organ cannot classify is a row whose
21// renumbering rule is UNKNOWN; passing it through silently would be a guess with a file format.
22//
23// usage: nx_canoncat <out.canon> <in1.canon> [in2.canon ...]
24// nx_canoncat selftest
25import "nx_gate_verdict.nx"
26
27const CC_MAXIN: i64 = 8388608 // 8MB per input canon -- far above any real canon, refuses beyond
28const CC_MAXOUT: i64 = 67108864
29
30func cc_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
31func cc_pn(v: i64) -> i64 {
32 let b: *u8 = sys_mmap(32); var x: i64=v; var ng: i64=0
33 if x<0 { ng=1; x=0-x }
34 var i: i64=31
35 if x==0 { b[i]=48 as u8; i=i-1 }
36 while x>0 { b[i]=(48+x%10) as u8; x=x/10; i=i-1 }
37 if ng==1 { b[i]=45 as u8; i=i-1 }
38 sys_write(1,(b as i64 + i + 1) as *u8, 31-i); return 0
39}
40func cc_streq(a: *u8, b: *u8) -> i64 {
41 var i: i64=0; var go: i64=1; var eq: i64=1
42 while go==1 { if a[i]!=b[i] { eq=0; go=0 } else { if a[i]==(0 as u8) { go=0 } else { i=i+1 } } }
43 return eq
44}
45// append v as decimal digits into out at pos
46func cc_wnum(out: *u8, pos: *i64, v: i64) -> i64 {
47 var x: i64 = v
48 if x < 0 { out[pos[0]] = 45 as u8; pos[0] = pos[0]+1; x = 0-x }
49 let t: *u8 = sys_mmap(32); var k: i64 = 0
50 if x == 0 { t[0] = 48 as u8; k = 1 }
51 while x > 0 { t[k] = (48+x%10) as u8; x = x/10; k = k+1 }
52 var q: i64 = k-1
53 while q >= 0 { out[pos[0]] = t[q]; pos[0] = pos[0]+1; q = q-1 }
54 return 0
55}
56// concatenate one input buffer into out. pbase = part offset for this file. Returns this file's own
57// P count, or negative on refusal: -5 dangling F reference, -6 unknown row type.
58func cc_one(inb: *u8, n: i64, out: *u8, opos: *i64, pbase: i64) -> i64 {
59 var pcount: i64 = 0
60 var i: i64 = 0
61 while i < n {
62 // find the line end (le = newline index, or n for an unterminated last line, which is
63 // normalised to newline-terminated on output)
64 var le: i64 = i
65 var scan: i64 = 1
66 while scan == 1 {
67 if le >= n { scan = 0 } else {
68 if inb[le] == (10 as u8) { scan = 0 } else { le = le + 1 }
69 }
70 }
71 let c: i64 = inb[i] as i64
72 if c == 80 { pcount = pcount + 1 } // 'P'
73 var handled: i64 = 0
74 if c == 70 { // 'F' -- rewrite the part index, field 2
75 handled = 1
76 var k: i64 = i + 1
77 var sk: i64 = 1
78 while sk == 1 {
79 if k >= le { sk = 0 } else { if inb[k] == (32 as u8) { k = k + 1 } else { sk = 0 } }
80 }
81 var v: i64 = 0
82 var got: i64 = 0
83 var neg: i64 = 0
84 if k < le { if inb[k] == (45 as u8) { neg = 1; k = k + 1 } }
85 var dg: i64 = 1
86 while dg == 1 {
87 if k >= le { dg = 0 } else {
88 let d: i64 = inb[k] as i64
89 if d >= 48 { if d <= 57 { v = v*10 + (d-48); got = 1; k = k + 1 } else { dg = 0 } }
90 else { dg = 0 }
91 }
92 }
93 if got == 0 { return 0-6 }
94 if neg == 1 { v = 0 - v }
95 // ★the dangling check uses THIS FILE's own part count seen SO FAR -- a feature must sit on
96 // a part already declared above it, the same implicit contract by which R rows attach to
97 // the most recent P.
98 if v < 0 { return 0-5 }
99 if v >= pcount { return 0-5 }
100 out[opos[0]] = 70 as u8; opos[0] = opos[0] + 1
101 out[opos[0]] = 32 as u8; opos[0] = opos[0] + 1
102 cc_wnum(out, opos, v + pbase)
103 var m: i64 = k
104 while m < le { out[opos[0]] = inb[m]; opos[0] = opos[0] + 1; m = m + 1 }
105 out[opos[0]] = 10 as u8; opos[0] = opos[0] + 1
106 }
107 if handled == 0 {
108 var ok: i64 = 0
109 if c == 80 { ok = 1 } // P
110 if c == 82 { ok = 1 } // R
111 if c == 10 { ok = 1 } // blank line
112 if c == 35 { ok = 1 } // '#' comment, tolerated verbatim
113 if ok == 0 { return 0-6 }
114 var m2: i64 = i
115 while m2 < le { out[opos[0]] = inb[m2]; opos[0] = opos[0] + 1; m2 = m2 + 1 }
116 out[opos[0]] = 10 as u8; opos[0] = opos[0] + 1
117 }
118 i = le + 1
119 }
120 return pcount
121}
122func cc_cat(outpath: *u8, files: *i64, nf: i64) -> i64 {
123 let out: *u8 = sys_mmap(CC_MAXOUT)
124 let opos: *i64 = sys_mmap(16) as *i64
125 opos[0] = 0
126 var pbase: i64 = 0
127 var f: i64 = 0
128 while f < nf {
129 let szp: *i64 = sys_mmap(16) as *i64
130 let inb: *u8 = sys_read_file(files[f] as *u8, szp)
131 if (inb as i64) == 0 { return 0-3 }
132 if szp[0] > CC_MAXIN { return 0-4 }
133 let pc: i64 = cc_one(inb, szp[0], out, opos, pbase)
134 if pc < 0 { return pc }
135 pbase = pbase + pc
136 f = f + 1
137 }
138 let fd: i64 = sys_openat_wr(outpath, 420)
139 if fd < 0 { return 0-7 }
140 sys_write(fd, out, opos[0])
141 sys_close(fd)
142 return pbase
143}
144func cc_gate() -> i64 {
145 let ctr: *i64 = gv_ctr()
146 gv_head("nx_canoncat selftest -- composition is a cat, and it refuses to guess" as *u8)
147 // fixture A: 2 parts, one F on part 1, P rows carrying DIFFERENT field counts (format evolution)
148 let fa: *u8 = "P 0 0 0 0 0 0\nR 470 0 0 35 45\nP 1 90 48 40 -41 0 15 30\nR 500 50 -6 38 51\nF 1 706 62 62 34 14\n" as *u8
149 // fixture B: 1 part, one F on part 0
150 let fb: *u8 = "P 1 0 0 0 0 0\nR 100 0 0 20 20\nF 0 200 30 60 20 10\n" as *u8
151 let pa: *u8 = "/tmp/cc_a.canon" as *u8
152 let pb: *u8 = "/tmp/cc_b.canon" as *u8
153 let po: *u8 = "/tmp/cc_o.canon" as *u8
154 var la: i64 = 0
155 while fa[la] != (0 as u8) { la = la + 1 }
156 var lb: i64 = 0
157 while fb[lb] != (0 as u8) { lb = lb + 1 }
158 let fd1: i64 = sys_openat_wr(pa, 420)
159 sys_write(fd1, fa, la); sys_close(fd1)
160 let fd2: i64 = sys_openat_wr(pb, 420)
161 sys_write(fd2, fb, lb); sys_close(fd2)
162 // ★★T1 IDENTITY IS BYTE-IDENTITY. cat of ONE file must reproduce it exactly -- this is the tooth
163 // that proves P/R passthrough is verbatim, and it holds for every field the format will ever grow.
164 let fl: *i64 = sys_mmap(8*8) as *i64
165 fl[0] = pa as i64
166 var t1: i64 = 0
167 if cc_cat(po, fl, 1) == 2 {
168 let szp: *i64 = sys_mmap(16) as *i64
169 let ob: *u8 = sys_read_file(po, szp)
170 if szp[0] == la {
171 var same: i64 = 1
172 var i: i64 = 0
173 while i < la { if ob[i] != fa[i] { same = 0; i = la } else { i = i + 1 } }
174 t1 = same
175 }
176 }
177 gv_check("T1 IDENTITY: cat of one file is BYTE-IDENTICAL, so passthrough survives every future field the format grows" as *u8, t1, ctr)
178 // ★★T2 THE SHIFT. A+B: B's F row must reference part 2 (0 + A's 2 parts); A's F row stays 1.
179 fl[0] = pa as i64
180 fl[1] = pb as i64
181 var t2: i64 = 0
182 if cc_cat(po, fl, 2) == 3 {
183 let szp2: *i64 = sys_mmap(16) as *i64
184 let ob2: *u8 = sys_read_file(po, szp2)
185 // count: "F 1 " present once, "F 2 " present once, "F 0 " absent
186 var f1: i64 = 0
187 var f2: i64 = 0
188 var f0: i64 = 0
189 var i2: i64 = 0
190 while i2 < szp2[0]-3 {
191 if ob2[i2] == (70 as u8) { if ob2[i2+1] == (32 as u8) {
192 if ob2[i2+2] == (49 as u8) { if ob2[i2+3] == (32 as u8) { f1 = f1 + 1 } }
193 if ob2[i2+2] == (50 as u8) { if ob2[i2+3] == (32 as u8) { f2 = f2 + 1 } }
194 if ob2[i2+2] == (48 as u8) { if ob2[i2+3] == (32 as u8) { f0 = f0 + 1 } }
195 } }
196 i2 = i2 + 1
197 }
198 if f1 == 1 { if f2 == 1 { if f0 == 0 { t2 = 1 } } }
199 }
200 gv_check("T2 THE SHIFT: the second file's feature lands on part 2, not part 0 -- the one cross-row reference is renumbered and nothing else is touched" as *u8, t2, ctr)
201 // ★T3 REFUSES a dangling feature: an F referencing a part its file never declared
202 let fc: *u8 = "P 0 0 0 0 0 0\nF 3 100 10 10 10 10\n" as *u8
203 var lc: i64 = 0
204 while fc[lc] != (0 as u8) { lc = lc + 1 }
205 let fd3: i64 = sys_openat_wr("/tmp/cc_c.canon" as *u8, 420)
206 sys_write(fd3, fc, lc); sys_close(fd3)
207 fl[0] = "/tmp/cc_c.canon" as *u8 as i64
208 var t3: i64 = 0
209 if cc_cat(po, fl, 1) == 0-5 { t3 = 1 }
210 gv_check("T3 REFUSES A DANGLING FEATURE: shifting a corrupt index would turn one file's corruption into another file's feature, so it refuses instead" as *u8, t3, ctr)
211 // ★T4 REFUSES an unknown row type -- a row with no renumbering rule is never passed through on a guess
212 let fdt: *u8 = "P 0 0 0 0 0 0\nQ 1 2 3\n" as *u8
213 var ld: i64 = 0
214 while fdt[ld] != (0 as u8) { ld = ld + 1 }
215 let fd4: i64 = sys_openat_wr("/tmp/cc_d.canon" as *u8, 420)
216 sys_write(fd4, fdt, ld); sys_close(fd4)
217 fl[0] = "/tmp/cc_d.canon" as *u8 as i64
218 var t4: i64 = 0
219 if cc_cat(po, fl, 1) == 0-6 { t4 = 1 }
220 gv_check("T4 REFUSES AN UNKNOWN ROW: a row this organ cannot classify has an UNKNOWN renumbering rule, and a silent passthrough would be a guess with a file format" as *u8, t4, ctr)
221 // ★T5 REFUSES a missing input -- composition never invents an empty contribution
222 fl[0] = "/tmp/cc_missing.canon" as *u8 as i64
223 var t5: i64 = 0
224 if cc_cat(po, fl, 1) == 0-3 { t5 = 1 }
225 gv_check("T5 REFUSES A MISSING INPUT rather than composing an empty contribution" as *u8, t5, ctr)
226 return gv_verdict("CANONCAT-GATE" as *u8, ctr, "compose canons by cat; verbatim passthrough; the one reference renumbered; refuses to guess" as *u8)
227}
228func main(argc: i64, argv: *i64) -> i64 {
229 if argc >= 2 {
230 if cc_streq(argv[1] as *u8, "selftest" as *u8) == 1 { return cc_gate() }
231 if argc >= 3 {
232 let fl: *i64 = sys_mmap(64*8) as *i64
233 var nf: i64 = 0
234 var i: i64 = 2
235 while i < argc { fl[nf] = argv[i]; nf = nf + 1; i = i + 1 }
236 let r: i64 = cc_cat(argv[1] as *u8, fl, nf)
237 if r < 0 {
238 cc_puts("{\x22organ\x22:\x22nx_canoncat\x22,\x22verdict\x22:\x22REFUSED\x22,\x22rc\x22:" as *u8); cc_pn(r)
239 cc_puts(",\x22why\x22:\x22-3 unreadable input, -5 dangling F part reference, -6 unknown row type, -4 oversized input, -7 cannot write output\x22}\n" as *u8)
240 return 0 - r
241 }
242 cc_puts("{\x22organ\x22:\x22nx_canoncat\x22,\x22parts_total\x22:" as *u8); cc_pn(r)
243 cc_puts(",\x22reads\x22:\x22one canon from many. P and R rows pass through byte-verbatim so the organ survives format growth; F part indices are shifted by the preceding files' part counts -- the format's only cross-row reference, renumbered, nothing else touched.\x22}\n" as *u8)
244 return 0
245 }
246 }
247 cc_puts("{\x22organ\x22:\x22nx_canoncat\x22,\x22usage\x22:\x22nx_canoncat <out.canon> <in1> [in2 ...] | nx_canoncat selftest\x22}\n" as *u8)
248 return 0
249}