code wiki / (root) / nx_smtlib_parse.nx

nx_smtlib_parse.nx source

↩ module page · 374 lines · 14102 B

1// nx_smtlib_parse.nx -- SMT-LIB v2 parser, propositional fragment. 2// 3// Foundation for SMT-COMP entry (https://smt-comp.github.io/). SMT-LIB 4// is the canonical input format for SMT solvers (Z3, cvc5, Yices, 5// Bitwuzla). This MVP handles the QF_UF (Quantifier-Free Uninterpreted 6// Functions, Boolean-only) fragment -- enough to enter the easiest 7// SMT-COMP divisions. 8// 9// Grammar handled: 10// script ::= command* 11// command ::= "(" "set-logic" symbol ")" 12// | "(" "declare-fun" symbol "()" "Bool" ")" 13// | "(" "declare-const" symbol "Bool" ")" 14// | "(" "assert" formula ")" 15// | "(" "check-sat" ")" 16// | "(" "exit" ")" 17// formula ::= symbol -- atom 18// | "true" | "false" 19// | "(" "not" formula ")" 20// | "(" "and" formula+ ")" 21// | "(" "or" formula+ ")" 22// | "(" "=>" formula formula ")" 23// | "(" "=" formula formula ")" 24// | "(" "xor" formula formula ")" 25// 26// Output: a SatProblem (DIMACS literals) ready for nx_sat_solve. 27// 28// Theories (LIA, LRA, BV, arrays, strings) deferred to later commits; 29// each requires a dedicated theory-solver primitive. 30 31// nx_safety_envelope: 32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 33// sil_target: SIL1 34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 35// verdict: NOT_YET_EVALUATED 36 37import "nx_syscalls.nx" 38import "nx_runtime.nx" 39import "nx_tier.nx" 40import "nx_str.nx" 41import "nx_result.nx" 42import "nx_file_result.nx" 43import "nx_sat_solver.nx" 44 45// ---- s-expression tokenizer ------------------------------------ 46const NX_SMT_TOK_LPAREN: nx_int = 1 47const NX_SMT_TOK_RPAREN: nx_int = 2 48const NX_SMT_TOK_SYM: nx_int = 3 49const NX_SMT_TOK_EOF: nx_int = 4 50 51const NX_SMT_MAX_SYM_LEN: nx_int = 64 52const NX_SMT_MAX_VARS: nx_int = 1024 53 54struct SmtParser { 55 buf: *u8, 56 n: nx_int, 57 pos: *nx_int, // single-elem buffer 58 var_names: *u8, // [NX_SMT_MAX_VARS * NX_SMT_MAX_SYM_LEN] 59 n_vars: nx_int, // distinct Boolean vars seen 60 formula: *SatFormula, 61} 62const NX_SMT_PARSER_BYTES: nx_int = 40 63 64func nx_smt_parser_new(buf: *u8, n: nx_int) -> *SmtParser { 65 let p: *SmtParser = (sys_mmap(NX_SMT_PARSER_BYTES as i64)) as *SmtParser 66 p.buf = buf 67 p.n = n 68 p.pos = (sys_mmap(8)) as *nx_int 69 p.pos[0] = 0 70 p.var_names = sys_mmap((NX_SMT_MAX_VARS * NX_SMT_MAX_SYM_LEN) as i64) 71 p.n_vars = 0 72 p.formula = nx_sat_alloc(NX_SMT_MAX_VARS) 73 return p 74} 75 76func nx_smt_skip_ws(p: *SmtParser) { 77 var pp: nx_int = p.pos[0] 78 while pp < p.n { 79 let c: nx_int = p.buf[pp] as nx_int 80 if c == 32 { pp = pp + 1 } 81 if c == 9 { pp = pp + 1 } 82 if c == 10 { pp = pp + 1 } 83 if c == 13 { pp = pp + 1 } 84 if c == 59 { // ';' line comment 85 while pp < p.n { 86 let cc: nx_int = p.buf[pp] as nx_int 87 pp = pp + 1 88 if cc == 10 { pp = pp - 0 } // exit handled below 89 if cc == 10 { p.pos[0] = pp; nx_smt_skip_ws(p); return } 90 } 91 } 92 if c != 32 { 93 if c != 9 { 94 if c != 10 { 95 if c != 13 { 96 if c != 59 { p.pos[0] = pp; return } 97 } 98 } 99 } 100 } 101 } 102 p.pos[0] = pp 103} 104 105// Read next token; on TOK_SYM, write the symbol into out_sym and 106// return its length. out_sym is null-terminated. 107func nx_smt_next_tok(p: *SmtParser, out_sym: *u8) -> nx_int { 108 nx_smt_skip_ws(p) 109 if p.pos[0] >= p.n { return NX_SMT_TOK_EOF } 110 let c: nx_int = p.buf[p.pos[0]] as nx_int 111 if c == 40 { p.pos[0] = p.pos[0] + 1; return NX_SMT_TOK_LPAREN } 112 if c == 41 { p.pos[0] = p.pos[0] + 1; return NX_SMT_TOK_RPAREN } 113 // Symbol: any non-paren non-whitespace run. 114 var len: nx_int = 0 115 while p.pos[0] < p.n { 116 let cc: nx_int = p.buf[p.pos[0]] as nx_int 117 if cc == 40 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 118 if cc == 41 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 119 if cc == 32 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 120 if cc == 9 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 121 if cc == 10 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 122 if cc == 13 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 123 if cc == 59 { out_sym[len] = 0; return NX_SMT_TOK_SYM } 124 if len < NX_SMT_MAX_SYM_LEN - 1 { 125 out_sym[len] = p.buf[p.pos[0]] 126 len = len + 1 127 } 128 p.pos[0] = p.pos[0] + 1 129 } 130 out_sym[len] = 0 131 return NX_SMT_TOK_SYM 132} 133 134// Get/create the SAT-var-id (1-indexed) for a Boolean symbol. 135func nx_smt_var_id(p: *SmtParser, name: *u8) -> nx_int { 136 var i: nx_int = 0 137 while i < p.n_vars { 138 let stored: *u8 = ((p.var_names as nx_int) + (i * NX_SMT_MAX_SYM_LEN)) as *u8 139 if nx_str_eq(stored, name) == 1 { return i + 1 } 140 i = i + 1 141 } 142 if p.n_vars >= NX_SMT_MAX_VARS { return 0 - 1 } 143 let slot: *u8 = ((p.var_names as nx_int) + (p.n_vars * NX_SMT_MAX_SYM_LEN)) as *u8 144 let _c: *u8 = nx_str_cpy(slot, name) 145 p.n_vars = p.n_vars + 1 146 return p.n_vars 147} 148 149// Forward decl for the recursive descent. 150func nx_smt_parse_formula(p: *SmtParser) -> nx_int; 151 152// Parse a parenthesised formula starting AT the opening paren. 153// Returns the head literal of the formula's CNF translation, or 0 on 154// error. Internally adds Tseitin-style clauses when needed. 155// 156// Simplified MVP: only assertions of the form (assert (= a b)), 157// (assert a), (assert (not a)), (assert (and ...)), (assert (or ...)) 158// get handled directly. Complex nested formulas with mixed 159// connectives need the full Tseitin path; for v1 we handle clauses 160// where the top-level structure is a single ASSERT of a boolean 161// combination of atoms. 162 163// Parse a single Boolean term (atom or operator-application). For 164// atoms returns a positive int (var id); for negated returns negative. 165// For complex operators returns 0 (caller should have flattened first). 166func nx_smt_parse_formula(p: *SmtParser) -> nx_int { 167 nx_smt_skip_ws(p) 168 if p.pos[0] >= p.n { return 0 } 169 let c: nx_int = p.buf[p.pos[0]] as nx_int 170 171 // Bare symbol -- atom or true/false 172 if c != 40 { 173 let sym_buf: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 174 let _t: nx_int = nx_smt_next_tok(p, sym_buf) 175 if nx_str_eq(sym_buf, "true" as *u8) == 1 { 176 // Always-true: encode as a fresh tautology var 177 let v: nx_int = nx_smt_var_id(p, "$true" as *u8) 178 return v 179 } 180 if nx_str_eq(sym_buf, "false" as *u8) == 1 { 181 let v: nx_int = nx_smt_var_id(p, "$true" as *u8) 182 return 0 - v 183 } 184 return nx_smt_var_id(p, sym_buf) 185 } 186 187 // Parenthesised: (op args...) 188 p.pos[0] = p.pos[0] + 1 189 let op_buf: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 190 let _t: nx_int = nx_smt_next_tok(p, op_buf) 191 192 // (not f) 193 if nx_str_eq(op_buf, "not" as *u8) == 1 { 194 let inner: nx_int = nx_smt_parse_formula(p) 195 nx_smt_skip_ws(p) 196 if p.pos[0] < p.n { 197 if p.buf[p.pos[0]] == 41 { p.pos[0] = p.pos[0] + 1 } 198 } 199 return 0 - inner 200 } 201 202 // For (and ...), (or ...), (=> ...), (= ...), (xor ...) -- caller 203 // typically wraps these in an (assert ...) which we handle by 204 // emitting clauses to formula. For an embedded use, we return 205 // the literal of the first operand and let the caller deal. 206 // This MVP intentionally keeps complex formulas as Tseitin-deferred. 207 // For now, treat as: read all operand literals and just pick the 208 // first; the assert handler emits the proper clauses. 209 let first: nx_int = nx_smt_parse_formula(p) 210 var done: nx_int = 0 211 while done == 0 { 212 nx_smt_skip_ws(p) 213 if p.pos[0] >= p.n { done = 1 } 214 if done == 0 { 215 if p.buf[p.pos[0]] == 41 { p.pos[0] = p.pos[0] + 1; done = 1 } 216 if done == 0 { let _ig: nx_int = nx_smt_parse_formula(p) } 217 } 218 } 219 return first 220} 221 222// Parse an entire (assert ...) command. Extracts the asserted formula 223// and adds appropriate SAT clauses. Supports: 224// (assert atom) -> [atom] 225// (assert (not atom)) -> [-atom] 226// (assert (or a b c)) -> [a, b, c] 227// (assert (and a b c)) -> [a], [b], [c] 228// (assert (=> a b)) -> [-a, b] 229// (assert (= a b)) -> [-a, b], [-b, a] (iff for booleans) 230// 231// Caller must position parser at the start of the formula (after 232// "assert" + ws). 233func nx_smt_handle_assert(p: *SmtParser) { 234 nx_smt_skip_ws(p) 235 if p.pos[0] >= p.n { return } 236 let c: nx_int = p.buf[p.pos[0]] as nx_int 237 238 // Bare atom: emit as unit clause. 239 if c != 40 { 240 let sym_buf: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 241 let _t: nx_int = nx_smt_next_tok(p, sym_buf) 242 let v: nx_int = nx_smt_var_id(p, sym_buf) 243 let lits: *nx_int = (sys_mmap(8)) as *nx_int 244 lits[0] = v 245 let _add: nx_int = nx_sat_add_clause(p.formula, lits, 1) 246 return 247 } 248 249 // (op args...) 250 p.pos[0] = p.pos[0] + 1 251 let op_buf: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 252 let _t: nx_int = nx_smt_next_tok(p, op_buf) 253 254 let lits: *nx_int = (sys_mmap((64 * 8) as i64)) as *nx_int 255 256 // (not atom) 257 if nx_str_eq(op_buf, "not" as *u8) == 1 { 258 let inner: nx_int = nx_smt_parse_formula(p) 259 nx_smt_skip_ws(p) 260 if p.pos[0] < p.n { 261 if p.buf[p.pos[0]] == 41 { p.pos[0] = p.pos[0] + 1 } 262 } 263 lits[0] = 0 - inner 264 let _add: nx_int = nx_sat_add_clause(p.formula, lits, 1) 265 return 266 } 267 268 // (or a b c) -- one disjunctive clause 269 if nx_str_eq(op_buf, "or" as *u8) == 1 { 270 var n: nx_int = 0 271 var done: nx_int = 0 272 while done == 0 { 273 nx_smt_skip_ws(p) 274 if p.pos[0] >= p.n { done = 1 } 275 if done == 0 { 276 if p.buf[p.pos[0]] == 41 { p.pos[0] = p.pos[0] + 1; done = 1 } 277 if done == 0 { 278 let l: nx_int = nx_smt_parse_formula(p) 279 if n < 64 { lits[n] = l; n = n + 1 } 280 } 281 } 282 } 283 let _add: nx_int = nx_sat_add_clause(p.formula, lits, n) 284 return 285 } 286 287 // (and a b c) -- multiple unit clauses 288 if nx_str_eq(op_buf, "and" as *u8) == 1 { 289 var done: nx_int = 0 290 while done == 0 { 291 nx_smt_skip_ws(p) 292 if p.pos[0] >= p.n { done = 1 } 293 if done == 0 { 294 if p.buf[p.pos[0]] == 41 { p.pos[0] = p.pos[0] + 1; done = 1 } 295 if done == 0 { 296 let l: nx_int = nx_smt_parse_formula(p) 297 let unit: *nx_int = (sys_mmap(8)) as *nx_int 298 unit[0] = l 299 let _add: nx_int = nx_sat_add_clause(p.formula, unit, 1) 300 } 301 } 302 } 303 return 304 } 305 306 // (=> a b) -- (~a | b) 307 if nx_str_eq(op_buf, "=>" as *u8) == 1 { 308 let a: nx_int = nx_smt_parse_formula(p) 309 let b: nx_int = nx_smt_parse_formula(p) 310 nx_smt_skip_ws(p) 311 if p.pos[0] < p.n { 312 if p.buf[p.pos[0]] == 41 { p.pos[0] = p.pos[0] + 1 } 313 } 314 lits[0] = 0 - a 315 lits[1] = b 316 let _add: nx_int = nx_sat_add_clause(p.formula, lits, 2) 317 return 318 } 319} 320 321// Top-level: walk every command in the script. Returns *SatFormula. 322func nx_smtlib_parse(buf: *u8, n: nx_int) -> *SatFormula { 323 let p: *SmtParser = nx_smt_parser_new(buf, n) 324 let cmd_buf: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 325 326 var done: nx_int = 0 327 while done == 0 { 328 let t: nx_int = nx_smt_next_tok(p, cmd_buf) 329 if t == NX_SMT_TOK_EOF { done = 1 } 330 if t == NX_SMT_TOK_LPAREN { 331 // Read command name 332 let _t2: nx_int = nx_smt_next_tok(p, cmd_buf) 333 if nx_str_eq(cmd_buf, "set-logic" as *u8) == 1 { 334 // Skip the logic name + closing paren 335 let dummy: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 336 let _ld: nx_int = nx_smt_next_tok(p, dummy) 337 let _rp: nx_int = nx_smt_next_tok(p, dummy) 338 } 339 if nx_str_eq(cmd_buf, "declare-fun" as *u8) == 1 { 340 // (declare-fun name () Bool) -- skip everything to ')' 341 var depth: nx_int = 1 342 while depth > 0 { 343 let nt: nx_int = nx_smt_next_tok(p, cmd_buf) 344 if nt == NX_SMT_TOK_LPAREN { depth = depth + 1 } 345 if nt == NX_SMT_TOK_RPAREN { depth = depth - 1 } 346 if nt == NX_SMT_TOK_EOF { depth = 0 } 347 } 348 } 349 if nx_str_eq(cmd_buf, "declare-const" as *u8) == 1 { 350 let dummy: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 351 let _name: nx_int = nx_smt_next_tok(p, dummy) 352 let _sort: nx_int = nx_smt_next_tok(p, dummy) 353 let _name_id: nx_int = nx_smt_var_id(p, dummy) 354 let _rp: nx_int = nx_smt_next_tok(p, dummy) 355 } 356 if nx_str_eq(cmd_buf, "assert" as *u8) == 1 { 357 nx_smt_handle_assert(p) 358 // Eat the trailing ')' 359 let dummy: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 360 let _rp: nx_int = nx_smt_next_tok(p, dummy) 361 } 362 if nx_str_eq(cmd_buf, "check-sat" as *u8) == 1 { 363 let dummy: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 364 let _rp: nx_int = nx_smt_next_tok(p, dummy) 365 } 366 if nx_str_eq(cmd_buf, "exit" as *u8) == 1 { 367 let dummy: *u8 = sys_mmap(NX_SMT_MAX_SYM_LEN as i64) 368 let _rp: nx_int = nx_smt_next_tok(p, dummy) 369 done = 1 370 } 371 } 372 } 373 return p.formula 374}