code wiki / _hdl_build / nx_ale_grade.nx
nx_ale_grade.nx source
↩ module page · 149 lines · 6295 B
1// nx_ale_grade.nx -- sovereign ALE DETERMINISTIC grader-runner core (rung ALE-R1a).
2// THE scoring engine the whole ALE-R1 harness routes through: the smallest load-bearing
3// slice (deterministic, code-graded, [0,1], no leakage). PURE function of two files:
4// argv[1] = REFERENCE path (list of grading-unit lines "unit|<name>|<value>")
5// argv[2] = ARTIFACT path (the agent-produced deliverable, same line grammar)
6// argv[3] = scratch OUT path (optional): the milli-score is written here as decimal
7// text + newline, so the gate reads back a value in 0..1000 WITHOUT hitting
8// the 0..255 process-exit-code ceiling. The score also goes to stdout.
9// SCORE (milli-units, 1000 == 1.0), data-driven, NO magic numbers:
10// total = number of "unit|" grading-unit lines in the REFERENCE (the rubric owns it)
11// matched= number of reference grading-unit lines that appear VERBATIM (name+value) as a
12// complete line in the ARTIFACT
13// raw = (matched * 1000) / total # per-unit weight = 1000/total, DERIVED
14// score = clamp(raw, 0, 1000) # BOUNDED into [0,1] inclusive
15// total == 0 -> score 0 (degenerate empty reference: nothing to credit)
16// Deterministic by construction: no clock, no rand -- only the two files decide the score.
17// No-leakage: the grader reads argv[1] (reference) ONLY; the score is independent of when/
18// where the reference existed during the (separate) agent phase. Landmines respected:
19// nested ifs (no &&/||), flat exprs, <=6 args/func, no empty-string literal, strings via
20// Write, openat_wr has no O_TRUNC (scratch file is fresh per gate run). license_tier: ORIGINAL
21import "nx_syscalls.nx"
22import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
23const K_MAGIC_262144: i64 = 262144
24
25// read whole file at path into buf (cap), return byte count (0 on open-fail)
26func ag_read(path: *u8, buf: *u8, cap: i64) -> i64 {
27 let fd: i64 = sys_openat_rd(path)
28 if fd < 0 { return 0 }
29 var n: i64 = 0
30 var go: i64 = 1
31 while go == 1 {
32 let r: i64 = sys_read(fd, (buf as i64 + n) as *u8, cap - 1 - n)
33 if r <= 0 { go = 0 } else { n = n + r }
34 if n >= cap - 1 { go = 0 }
35 }
36 sys_close(fd)
37 return n
38}
39
40// length of a C string
41func ag_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
42
43// does buf2[pos2 .. pos2+seglen) byte-equal buf1[seg .. seg+seglen)? 1/0
44func ag_seg_eq(buf1: *u8, seg: i64, seglen: i64, buf2: *u8, pos2: i64) -> i64 {
45 var k: i64 = 0
46 while k < seglen {
47 if buf1[seg + k] != buf2[pos2 + k] { return 0 }
48 k = k + 1
49 }
50 return 1
51}
52
53// is pos a line start in buf? (pos==0 or previous byte is newline)
54func ag_is_bol(buf: *u8, pos: i64) -> i64 {
55 if pos == 0 { return 1 }
56 if buf[pos - 1] == (10 as u8) { return 1 }
57 return 0
58}
59
60// index of the end of the line starting at start (the newline index, or n at EOF)
61func ag_line_end(buf: *u8, n: i64, start: i64) -> i64 {
62 var e: i64 = start
63 while e < n {
64 if buf[e] == (10 as u8) { return e }
65 e = e + 1
66 }
67 return n
68}
69
70// does the COMPLETE line buf1[seg .. segend) (segend exclusive, no newline) also appear
71// as a COMPLETE line somewhere in buf2 (length n2)? 1/0. A line is bounded by BOL and
72// by newline-or-EOF on both ends so "id" never spuriously matches inside "id,name".
73func ag_line_in(buf1: *u8, seg: i64, segend: i64, buf2: *u8, n2: i64) -> i64 {
74 let seglen: i64 = segend - seg
75 var j: i64 = 0
76 while j < n2 {
77 if ag_is_bol(buf2, j) == 1 {
78 var lineok: i64 = 0
79 if j + seglen <= n2 {
80 if ag_seg_eq(buf1, seg, seglen, buf2, j) == 1 {
81 let after: i64 = j + seglen
82 if after == n2 { lineok = 1 } else { if buf2[after] == (10 as u8) { lineok = 1 } }
83 }
84 }
85 if lineok == 1 { return 1 }
86 }
87 j = j + 1
88 }
89 return 0
90}
91
92// write decimal v (>=0) + newline to fd
93// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
94// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
95// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
96// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
97func ag_wn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
98
99func main(argc: i64, argv: *i64) -> i64 {
100 if argc < 3 { sys_exit(2); return 2 }
101 let refp: *u8 = argv[1] as *u8
102 let artp: *u8 = argv[2] as *u8
103
104 let rbuf: *u8 = sys_mmap(K_MAGIC_262144)
105 let abuf: *u8 = sys_mmap(K_MAGIC_262144)
106 let rn: i64 = ag_read(refp, rbuf, K_MAGIC_262144)
107 let an: i64 = ag_read(artp, abuf, K_MAGIC_262144)
108
109 // Count total grading-units in the REFERENCE (lines starting "unit|") and, for each,
110 // count whether that whole line appears verbatim as a complete line in the ARTIFACT.
111 let pfx: *u8 = "unit|" as *u8
112 let pl: i64 = ag_len(pfx)
113 var total: i64 = 0
114 var matched: i64 = 0
115 var i: i64 = 0
116 while i < rn {
117 if ag_is_bol(rbuf, i) == 1 {
118 var isunit: i64 = 0
119 if i + pl <= rn {
120 if ag_seg_eq(pfx, 0, pl, rbuf, i) == 1 { isunit = 1 }
121 }
122 if isunit == 1 {
123 let lineend: i64 = ag_line_end(rbuf, rn, i)
124 total = total + 1
125 if ag_line_in(rbuf, i, lineend, abuf, an) == 1 { matched = matched + 1 }
126 // advance i to the newline index; the loop's i=i+1 below steps past it
127 i = lineend
128 }
129 }
130 i = i + 1
131 }
132
133 // data-driven score: per-unit weight = 1000/total (derived from the reference rubric)
134 var raw: i64 = 0
135 if total > 0 { raw = (matched * 1000) / total }
136 // BOUNDED: clamp into [0,1000] inclusive (the [0,1] milli contract)
137 var score: i64 = raw
138 if score < 0 { score = 0 }
139 if score > 1000 { score = 1000 }
140
141 // machine-readable result: scratch file (argv[3]) if given, AND stdout
142 if argc >= 4 {
143 let out: i64 = sys_openat_wr(argv[3] as *u8, 0x1a4)
144 if out >= 0 { ag_wn(out, score); sys_close(out) }
145 }
146 ag_wn(1, score)
147 sys_exit(0)
148 return 0
149}