SAT SMT by Example
SAT SMT by Example
Dennis Yurichev
SAT/SMT by Example
Dennis Yurichev
1 Introduction 11
2 Basics 14
3 Equations 36
4 Proofs 111
5 Verification 137
14 MaxSAT/MaxSMT 386
15 Synthesis 416
18 KLEE 521
1
2
28 Acronyms used 667
Contents
1 Introduction 11
1.1 What is this all about? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.2 Praise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3 As recommended reading at several universities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4 Thanks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.5 Disclaimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.6 Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.7 Latest versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.8 Proofreaders wanted! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.9 The source code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.10 Subscribe to my news . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.11 Is it hype? Yet another fad? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.12 Backward computation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.13 Is this another programming language? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.14 Hacking/tinkering plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2 Basics 14
2.1 One-hot encoding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2 SMT1 -solvers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.1 School-level system of equations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.2 Another school-level system of equations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.2.3 Why variables are declared using declare-fun? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.2.4 Connection between SAT2 and SMT solvers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.5 Referential transparency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.6 Theories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2.7 Modulo something . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.2.8 Division by 0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.2.9 List of SMT-solvers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.2.10 Z3 specific . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.3 SAT-solvers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.3.1 CNF form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.3.2 Example: 2-bit adder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.3.3 Picosat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.3.4 MaxSAT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.3.5 List of SAT-solvers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.4 Which SAT/SMT solver I should use? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5 Yet another explanation of NP-problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.1 Constant time, O(1) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.2 Linear time, O(n) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.3 Hash tables and binary trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.4 EXPTIME . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.5 NP-problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.5.6 P=NP? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.5.7 What are SAT/SMT solvers? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.6 Origins of Tetris: a hypothesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.7 Halting problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1 Satisfiability modulo theories
2 Boolean satisfiability problem
3
4
3 Equations 36
3.1 SMT-solver as a calculator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.2 Solving XKCD 287 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.2.1 Exporting the expression in SMT-LIB 2.x format . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2.2 XKCD 287 in SMT-LIB 2.x format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2.3 Other solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.2.4 A real life story . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3 Wood workshop, linear programming and Leonid Kantorovich . . . . . . . . . . . . . . . . . . . . . . . 40
3.4 Puzzle with animals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.5 Subset sum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.6 Art of problem solving . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.7 Yet another explanation of modulo inverse using SMT-solvers . . . . . . . . . . . . . . . . . . . . . . . 46
3.8 School-level equation: KLEE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.9 School-level equation: CBMC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.10 Minesweeper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.10.1 Cracking Minesweeper with SMT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.10.2 Cracking Minesweeper with SAT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
3.10.3 Optimization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.10.4 Cracking Minesweeper: Donald Knuth’s version . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
3.10.5 Cracking Minesweeper with SAT solver and sorting network . . . . . . . . . . . . . . . . . . . . 70
3.10.6 Cracking Minesweeper: by bruteforce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.10.7 More theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.10.8 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.11 LCG3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.11.1 Cracking LCG with Z3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.11.2 Can rand() generate 10 consecutive zeroes? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.11.3 Can rand() generate 100 consecutive zeroes? (SMT-LIB versions) . . . . . . . . . . . . . . . . . 79
3.11.4 Other PRNG4 s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.12 Integer factorization using Z3 SMT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.13 Integer factorization using SAT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
3.13.1 Binary adder in SAT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.13.2 Binary multiplier in SAT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.13.3 As an electrical device . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
3.13.4 Gluing all together . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
3.13.5 Division using multiplier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.13.6 Breaking RSA5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.13.7 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.14 Recalculating micro-spreadsheet using Z3Py . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.14.1 Z3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
3.14.2 Unsat core . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
3.14.3 Stress test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.14.4 The files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.15 Discrete tomography . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.16 Cribbage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
3.17 Solving Problem Euler 31: “Coin sums” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
3.18 Exercise 15 from TAOCP “7.1.3 Bitwise tricks and techniques” . . . . . . . . . . . . . . . . . . . . . . . 106
3.19 Generating de Bruijn sequences using SMT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.20 Solving the xy = 19487171 equation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
3.21 Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
4 Proofs 111
4.1 Using Z3 theorem prover to prove equivalence of some weird alternative to XOR operation . . . . . . . 111
4.1.1 In SMT-LIB form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.1.2 Using universal quantifier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.1.3 How the expression works . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
4.2 Proving bizarre XOR alternative using SAT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
4.3 Dietz’s formula . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
4.4 XOR swapping algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
3 Linear congruential generator
4 Pseudorandom number generator
5 Rivest–Shamir–Adleman cryptosystem
5
4.4.1 In SMT-LIB form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
4.5 Simplifying long and messy expressions using Z3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
4.6 Simplifying long and messy expressions using Mathematica and Z3 . . . . . . . . . . . . . . . . . . . . 119
4.7 Bit reverse function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
4.8 Proving sorting network correctness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
4.9 ITE6 example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
4.10 Branchless abs() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
4.11 Proving branchless min/max functions are correct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
4.12 Proving “Determine if a word has a zero byte” bit twiddling hack . . . . . . . . . . . . . . . . . . . . . 127
4.13 Arithmetical shift bit twiddling hack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
4.14 Proving several floor()/ceiling() properties using Z3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
4.15 Proving equivalence of two functions using CBMC and Z3 SMT-solver . . . . . . . . . . . . . . . . . . 133
4.15.1 CBMC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.15.2 Z3 SMT-solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.15.3 The files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.15.4 But bruteforce? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
5 Verification 137
5.1 Integer overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5.1.1 Signed addition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5.1.2 Arithmetic mean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
5.1.3 Allocate memory for some chunks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
5.1.4 abs() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
5.1.5 Games . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.1.6 Year 2038 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.1.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.1.8 Further work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.1.9 Some discussion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
5.1.10 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
5.2 Formal verification of population count functions using CBMC . . . . . . . . . . . . . . . . . . . . . . 143
5.2.1 CBMC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
5.2.2 SMT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
5.2.3 Files used . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
5.3 Knuth-Morris-Pratt string-searching algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
5.3.1 My homebrew algorithms formally verified using CBMC . . . . . . . . . . . . . . . . . . . . . . 154
5.3.2 The DFA7 version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
5.3.3 The DFA-less version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
5.3.4 A discussion at HN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
5.4 Boyer-Moore string search algorithm explanation and formal verification using CBMC . . . . . . . . . 165
5.4.1 Version 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
5.4.2 Version 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
5.4.3 Formal verification using CBMC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
5.4.4 But this algorithm is not finished. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
5.4.5 Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
5.4.6 Discussion at Hacker News . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
14 MaxSAT/MaxSMT 386
14.1 Making smallest possible test suite using Z3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
14.2 Making smallest possible test suite using OpenWBO . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
14.3 Fault check of digital circuit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
14.4 GCD11 and LCM12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
14.4.1 GCD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
14.4.2 Least Common Multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
14.5 Assignment problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
14.6 Find maximal clique using MaxSAT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
14.7 ”Polite customer” problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
10 Mutually Orthogonal Latin Square
11 GreatestCommon Divisor
12 Least Common Multiple
8
14.8 Packing students into a dorm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
14.9 Finding optimal beer can size using SMT solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
14.9.1 A jar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
14.10Choosing between short/long jumps in x86 assembler . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
15 Synthesis 416
15.1 Logic circuits synthesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
15.1.1 Simple logic synthesis using Z3 and Apollo Guidance Computer . . . . . . . . . . . . . . . . . . 416
15.1.2 TAOCP 7.1.1 exercises 4 and 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
15.2 Program synthesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
15.2.1 Synthesis of simple program using Z3 SMT-solver . . . . . . . . . . . . . . . . . . . . . . . . . . 438
15.2.2 Rockey dongle: finding unknown algorithm using only input/output pairs . . . . . . . . . . . . 442
15.2.3 TAOCP 7.1.3 Exercise 198, UTF-8 encoding and program synthesis by sketching . . . . . . . . 447
15.2.4 TAOCP 7.1.3 Exercise 203, MMIX MOR instruction and program synthesis by sketching . . . 449
15.2.5 Loading a constant into register using ASCII-only x86 code . . . . . . . . . . . . . . . . . . . . 452
15.2.6 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
15.3 DFA (Deterministic Finite Automaton) synthesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
15.3.1 DFA accepting a 001 substring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
15.3.2 DFA accepting a binary substring divisible by prime number . . . . . . . . . . . . . . . . . . . 457
15.3.3 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
15.3.4 Food for thought . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
15.4 Other . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
15.4.1 Graph theory: degree sequence problem / graph realization problem . . . . . . . . . . . . . . . 467
Introduction
Moshe Vardi
1.2 Praise
See here: https://smt.st.
1.4 Thanks
Armin Biere has patiently answered to my endless and boring questions.
1 Conjunctive normal form
11
12
2 3 4 5
Leonardo Mendonça de Moura , Nikolaj Bjørner , Johannes Altmanninger and Mate Soos have also helped.
Masahiro Sakai6 has helped with numberlink puzzle: 8.8.
Helped with KLEE: Cristian Cadar7 , Frank Busse8 .
Chad Brewbaker, Evan Wallace, Ben Gardiner, and Wojciech Niedbala have fixed some bugs and made improve-
ments.
Xing Shi Cai, Arseny Nerinovsky, Raphael Wimmer, Andrea Jemmett, Jason Bucata and Tom Chase have found
many bugs.
English grammar fixes: Priyanshu Jain, John Winston Garth.
Denis Fokin found a typo.
Martin Nyx Brain has helped with CBMC, etc.
1.5 Disclaimer
This collection is a non-academic reading for end-users, i.e., programmers, etc.
The author of these lines is no expert in SAT/SMT, by any means. This is not a book, rather a student’s notes.
Take it with a grain of salt...
Despite the fact there are many examples for Z3 SMT-solver, the author of these lines is not affiliated with the Z3
team in any way... Nor with any other SAT/SMT teams.
1.6 Python
Majority of code in the book is written in Python 3. Basic understanding is recommended.
2 https://www.microsoft.com/en-us/research/people/leonardo/
3 https://www.microsoft.com/en-us/research/people/nbjorner/
4 https://github.com/krobelus
5 https://www.msoos.org/
6 https://twitter.com/masahiro_sakai
7 https://github.com/ccadar
8 https://github.com/251
13
1.11 Is it hype? Yet another fad?
Some people say this is just hype. No, SAT is old and fundamental to CS9 . The reason for increased interest in it is
that computers got faster over the last couple of decades, so there are attempts to solve old problems using SAT/SMT,
which were inaccessible in past.
One significant step is GRASP SAT solver (1996) 10 , known as CDCL11 , the next is Chaff (2001) 12 .
In 1999, a new paper proposed using SAT instead of BDD for symbolic model checking: Armin Biere, Alessandro
Cimatti, Edmund Clarke, Yunshan Zhu – Symbolic Model Checking without BDDs 13 . See also: http://fmv.jku.
at/biere/talks/Biere-CAV18Award-talk.pdf.
SMT-LIB mailing list was created in 2002 14 .
Also, SAT/SMT are not special or unique. There are adjacent area like ASP: Answer Set Programming. CSP:
Constraint Satisfaction Problems. Also, Prolog programming language.
• CBMC examples. CBMC16 is easy to install. Examples are in pure C, so anybody can hack them without
additional training. CBMC exists for Windows.
• KLEE examples. Also in pure C, but KLEE is trickier to install, via docker17 ... No Windows supports, AFAIR.
• Z3Py examples. These are for Z3 Python API. Only Python knowledge is required. Grep for ”from z3 import *”
in *.py files, these are for Z3Py.
• SMT-LIB 2.x examples. Lispy language, way less handy than Python, C++, C#, etc... But this language is
distilled, and not contaminated by Python’s (or C++’s) specific features, operator overloading, etc, so there you
can see a clear border between SMT API and API’s implementation in specific PL.
• SAT examples. Low-level. Like assembly language – hard to understand, hard to use, but the most efficient way
of using SAT solvers.
9 Computer science
10 https://www.cs.cmu.edu/~emc/15-820A/reading/grasp_iccad96.pdf
11 Conflict-driven clause learning
12 http://www.princeton.edu/~chaff/publication/DAC2001v56.pdf
13 http://fmv.jku.at/papers/BiereCimattiClarkeZhu-TACAS99.pdf
14 https://cs.nyu.edu/pipermail/smt-lib/2002/
15 Programming Language
16 http://www.cprover.org/cbmc/
17 https://klee.github.io/docker/
Chapter 2
Basics
Decimal One-hot
0 00000001
1 00000010
2 00000100
3 00001000
4 00010000
5 00100000
6 01000000
7 10000000
Or in reversed form:
Decimal One-hot
0 10000000
1 01000000
2 00100000
3 00010000
4 00001000
5 00000100
6 00000010
7 00000001
2.2 SMT-solvers
2.2.1 School-level system of equations
This is school-level system of equations copy-pasted from Wikipedia 1 :
3x + 2y − z = 1
2x − 2y + 4z = −2
−x + 21 y − z = 0
Will it be possible to solve it using Z3? Here it is:
1 https://en.wikipedia.org/wiki/System_of_linear_equations
14
15
x = Real('x')
y = Real('y')
z = Real('z')
s = Solver ()
s.add (3*x + 2*y - z == 1)
s.add (2*x - 2*y + 4*z == -2)
s.add(-x + 0.5*y - z == 0)
print s.check ()
print s.model ()
If we change any equation in some way so it will have no solution, s.check() will return “unsat”.
I’ve used “Real” sort (some kind of data type in SMT-solvers) because the last expression equals to 12 , which is, of
course, a real number. For the integer system of equations, “Int” sort would work fine.
Python (and other high-level PLs like C#) interface is highly popular, because it’s practical, but in fact, there is
a standard language for SMT-solvers called SMT-LIB 2 .
Our example rewritten to it looks like this:
(declare -const x Real)
(declare -const y Real)
(declare -const z Real)
( assert (=( -(+(* 3 x) (* 2 y)) z) 1))
( assert (=(+( -(* 2 x) (* 2 y)) (* 4 z)) -2))
( assert (=( -(+ (- 0 x) (* 0.5 y)) z) 0))
(check -sat)
(get -model)
This language is very close to LISP, but is somewhat hard to read for untrained eyes.
Now we run it:
% z3 -smt2 example .smt
sat
(model
(define -fun z () Real
(- 2.0))
(define -fun y () Real
(- 2.0))
(define -fun x () Real
1.0)
)
So when you look back to my Python code, you may feel that these 3 expressions could be executed. This is not
true: Z3Py API offers overloaded operators, so expressions are constructed and passed into the guts of Z3 without
any execution 3 . I would call it “embedded DSL4 ”.
Same thing for Z3 C++ API, you may find there “operator+” declarations and many more 5 .
Z3 API6 s for Java, ML and .NET are also exist 7 .
sat
[ triangle = 1, square = 2, circle = 5]
In SMT-LIB world, you don’t declare variables, you declare argumentless functions that value is either fixed:
...
(declare -fun const1 () (_ BitVec 32))
( assert (= const1 (_ bv214013 32)))
...
... or it’s connected to another expression/function’s value/etc. This is why variables are declared using declare-fun
directive. (However, Z3 support declare-const directive, which is merely a syntactic sugar.)
One can go further: there are programming languages with no variables, but they are not very practical for our
mundane endeavours: https://en.wikipedia.org/wiki/Lambda_calculus.
(And to extreme minimalism: https://en.wikipedia.org/wiki/Iota_and_Jot, https://en.wikipedia.org/
wiki/Unlambda.)
LISP has borrowed from some features of lambda calculus, while SMT-LIB language is a very simplified LISP.
17
2.2.4 Connection between SAT and SMT solvers
SMT-solvers are frontends to SAT solvers, i.e., they translate inputted SMT expressions into CNF and feed SAT-solver
with it. Translation process is sometimes called “bit blasting”. Some SMT-solvers uses external SAT-solver: STP uses
MiniSAT or CryptoMiniSAT as backend. Some other SMT-solvers (like Z3) has their own SAT solver.
One important property that holds for all varieties of SSA, including the simplest definition above,
is referential transparency: i.e., since there is only a single definition for each variable in the program
text, a variable’s value is independent of its position in the program.
const 1 (input) x
adder
(output) x
Optimization
All this can help in optimization.
For example, in my SAT_lib.py Python library I’m using across this book in SAT examples, each gate generated
only once for the specific inputs:
def AND(self , v1:int , v2:int) -> int:
if (v1 , v2) in self. AND_already_generated :
return self. AND_already_generated [(v1 ,v2)]
out=self. create_var ()
self. AND_Tseitin (v1 , v2 , out)
struct ctx
{
...
std :: unordered_map <std :: string , struct SMT_var *> gen_cache ;
};
...
...
case EXPR_BINARY :
if ( verbose )
printf ("%s:%d (after EXPR_BINARY ) end\n",
__FUNCTION__ , __LINE__ );
rt= gen_binary (ctx , e);
rt ->e=e;
ctx -> gen_cache [tmp ]= rt;
return rt;
...
This is like common subexpression elimination. It would be way harder doing it in Algol-like programming lan-
guages.
I added this optimization to MK85 when it failed to generate the bit reverse function (4.7) – because the expression
explode on each iteration drastically.
2.2.6 Theories
QF_S – strings. You may need this to simulate strings to catch bugs like SQL injection. http://cvc4.cs.stanford.
edu/wiki/Strings.
* QF_UF ( Uninterpreted Functions ): quantifier -free formulas whose
satisfiability is to be decided modulo the empty theory . Each
benchmark may introduce its own uninterpreted function and predicate
symbols .
* QF_BV (Fixed -size Bit - vectors ): quantifier -free formulas over bit
vectors of fixed size.
( src )
...
/*
* Codes for the logic ( based on benchmarks available in June 2009)
* + one special code 'NONE ' for propositional logic
*
* 06/03/2014: More surprise logics for SMTCOMP 2014
*
* 06/18/2014: Attempt to be a bit more systematic
* -----------------------------------------------
* Added all the logics listed at smt -lib.org + a few more that should be there .
*
* The base theories are:
* AX: arrays
* BV: bitvectors
* UF: uninterpreted types and functions
* + arithmetic
*
* For arithmetic :
* - the domain can be Int or Reals or Both (mixed arithmetic )
* - the type of atoms can be
* difference logic
* linear equalities and inequalities
* non - linear equalities and inequalities ( polynomials )
* - this gives nine arithmetic variants , but the standard does not include
* mixed difference logic . So we have eight arithmetic codes :
* IDL , RDL , LIA , LRA , LIRA , NIA , NRA , NIRA
*
* AX + UF can be combined with BV and with one of the arithmetic fragments
* ( except that we don 't have AIDL and ARDL ?).
*
* Then , for each logic , we have a quantifier -free variant .
*/
typedef enum smt_logic {
NONE , // added 12/27/2012
/*
* Base logics (with quantifiers )
*/
AX , // arrays
BV , // bitvectors
IDL , // integer difference logic
LIA , // linear integer arithmetic
LRA , // linear real arithmetic
LIRA , // mixed linear arithmetic
NIA , // non - linear integer arithmetic
21
NRA , // non - linear real arithmetic
NIRA , // non - linear mixed arithmetic
RDL , // real difference logic
UF , // uninterpreted functions
/*
* Quantifier -free fragments
*/
QF_AX , // arrays
QF_BV , // bitvectors
QF_IDL , // integer difference logic
QF_LIA , // linear integer arithmetic
QF_LRA , // linear real arithmetic
QF_LIRA , // mixed linear arithmetic
QF_NIA , // non - linear integer arithmetic
QF_NRA , // non - linear real arithmetic
QF_NIRA , // non - linear mixed arithmetic
QF_RDL , // real difference logic
QF_UF , // uninterpreted functions
...
/*
* Arithmetic fragments
*/
typedef enum arith_fragment {
ARITH_IDL , // integer difference logic
ARITH_RDL , // real difference logic
ARITH_LIA , // linear integer arithmetic
ARITH_LRA , // linear real arithmetic
ARITH_LIRA , // linear mixed arithmetic
ARITH_NIA , // non - linear integer arithmetic
ARITH_NRA , // non - linear real arithmetic
ARITH_NIRA , // non - linear mixed arithmetic
ARITH_NONE , // no arithmetic
} arith_fragment_t ;
What is theory?
One can say that a SMT solver is a library of various methods on top of a SAT solver. And sometimes, one
theory/method can perform better for your problem than another.
You can first start without setting theory explicitly or setting QF_ALL. But then you can experiment trying some
of them.
It’s recommended to set a theory that has only features you need, nothing else.
And of course, SMT benchmarks for SMT solvers competition are divided by theories: http://smt-lib.loria.
fr/zip.
More recently, I learned another useful term: ”modulo errors.” This is used, for example, as ”Q.E.D.,
modulo errors.” One fellow often applied this to his blackboard proofs, meaning: ”This is a represen-
tative of an equivalence class of proofs, one of which is correct and all of which look sort of like this
one. At least one such proof is correct, but it might not be the one I wrote down.”
( https://jcdverha.home.xs4all.nl/scijokes/1_7.html )
Another example:
23
Exact inference aims at solving the tasks in an exact way, modulo errors of computer floating point
arithmetic.
( Foundations of Probabilistic Logic Programming: Languages, Semantics ... — Fabrizio Riguzzi https://books.
google.com/books?id=zk-MDwAAQBAJ )
Donald Knuth uses this:
But if anybody has a significantly better idea, I’m happy to listen — modulo the situation discussed
in [don’t send me email].
( https://www-cs-faculty.stanford.edu/~knuth/news98.html )
Also, there is a tool: “MCMT: Model Checker Modulo Theories” ( http://users.mat.unimi.it/users/ghilardi/
mcmt/ ).
Now what about “satisfiability modulo theories”?
Some thinks that means “satisfiability modulo theories [that are supported by solver]” or limited/adjusted/augmented
by them.
According to one dictionary, ”modulo” means ”correcting or adjusting for something.” This is an
appropriate definition for its use in satisfiability modulo theories:
First-order logic defines a notion of satisfiability for arbitrary formulas. In SMT, we use the same
definition of satisfiability, but *adjusted* by restricting the set of models we consider to those that
also satisfy the theories involved.
2.2.8 Division by 0
How SMT-solvers handles division by zero? They can’t throw exception, like a CPU.
It was decided to totalize division operation, i.e., make it producing an output for each input. But which output
should be for x0 ?
One idea was that x0 should be 0, as this is done in theorem-provers.
Another idea is to fix x0 to 0b11...11 or 0xff..ff:
From an implementation point of view , the simplest approach is one that
avoids special treatments for division by zero and makes things uniform .
( bvurem x y) = x - ( bvudiv x y) * y.
( src )
This is quite fun, I tried to divide by zero using my toy-blaster MK85, this resulted in 0xff..ff, since my solver
implements a primitive divisor.
See also, start of discussion.
From the SMT-LIB standard:
24
...
If you want to disable division by zero, you can still add a constraint preventing divisor to be zero. And if your
equation would require to divide by 0, you’ll get unsat.
Anyway, you can always add an ITE, that will branch depending on the value of divisor.
Python modules goes to /usr/lib/python3.x/site-packages/z3. If you have trouble with this, you can install
Python modules to your $HOME:
python3 scripts / mk_make .py --prefix = $HOME --python --pypkgdir = $HOME /. local /lib/
python3 .8/ site - packages
//cvc4.cs.stanford.edu/wiki/.
20 Grep’ed from its test suite
21 http://fmv.jku.at/boolector/
22 https://alt-ergo.ocamlpro.com/
23 http://mathsat.fbk.eu/
24 http://www.verit-solver.org/
25 https://github.com/msakai/toysolver
26 https://smt.st/MK85/
27 http://dreal.cs.cmu.edu, https://github.com/dreal
28 https://pysmt.readthedocs.io/en/latest/, https://github.com/pysmt/pysmt, http://smt2015.csl.sri.com/wp-content/
uploads/2015/06/2015-Gario-Micheli-PySMT-a-Solver-Agnostic-Library-for-Fast-Prototyping-of-SMT-Based-Algorithms.pdf
http://fmv.jku.at/avm15/slides/gario.pdf
26
Boolector
Boolector can generate CNF file instead of handing it out to SAT solver:
boolector --smt2 --dump - dimacs filename .smt > filename .cnf
Just keep in mind that if Boolector calls a SAT solver several times, all these CNFs would be dumped to stdout,
like:
c CNF dump 1 start
c Boolector version
p cnf 13325 36769
1 0
2 0
...
-13325 0
c CNF dump 1 end
c CNF dump 2 start
c Boolector version
p cnf 15421 40065
1 0
2 0
...
15421 0
c CNF dump 2 end
c CNF dump 3 start
c Boolector version
p cnf 15488 40267
1 0
2 0
...
This is useful: you can try other SAT solvers, proof checkers, etc...
2.2.10 Z3 specific
The output is not guaranteed to be random. You can randomize it by:
import time
...
s= Solver ()
set_param (" smt. random_seed ", int(time.time ()))
Or conversely, you may want to reproduce its result each time the same:
set_param (" smt. random_seed ", 1234)
2.3 SAT-solvers
SMT vs. SAT is like high level PL vs. assembly language. The latter can be much more efficient, but it’s hard to
program in it.
SAT is abbreviation of “Boolean satisfiability problem”. The problem is to find such a set of variables, which, if
plugged into boolean expression, will result in “true”.
27
2.3.1 CNF form
CNF29 is a normal form.
Any Boolean expression can be converted to normal form and CNF is one of them. The CNF expression is a bunch
of clauses (sub-expressions) consisting of terms (variables), ORs and NOTs, all of which are then glued together with
AND into a full expression. There is a way to memorize it: CNF is “AND of ORs” (or “product of sums”) and DNF30
is “OR of ANDs” (or “sum of products”).
Example is: (¬A ∨ B) ∧ (C ∨ ¬D).
∨ stands for OR (logical disjunction31 ), “+” sign is also sometimes used for OR.
∧ stands for AND (logical conjunction32 ). It is easy to memorize: ∧ looks like “A” letter. “·” is also sometimes used
for AND.
¬ is negation (NOT).
The adder in its simplest form: it has no carry-in and carry-out, and it has 3 XOR gates and one AND gate. Let’s
try to figure out, which sets of input values will force adder to set both two output bits? By doing quick memory
calculation, we can see that there are 4 ways to do so: 0 + 3 = 3, 1 + 2 = 3, 2 + 1 = 3, 3 + 0 = 3. Here is also truth
table, with these rows highlighted:
29 https://en.wikipedia.org/wiki/Conjunctive_normal_form
30 Disjunctive normal form
31 https://en.wikipedia.org/wiki/Logical_disjunction
32 https://en.wikipedia.org/wiki/Logical_conjunction
28
aH aL bH bL qH qL
3+3 = 6 ≡ 2 (mod 4) 1 1 1 1 1 0
3+2 = 5 ≡ 1 (mod 4) 1 1 1 0 0 1
3+1 = 4 ≡ 0 (mod 4) 1 1 0 1 0 0
3+0 = 3 ≡ 3 (mod 4) 1 1 0 0 1 1
2+3 = 5 ≡ 1 (mod 4) 1 0 1 1 0 1
2+2 = 4 ≡ 0 (mod 4) 1 0 1 0 0 0
2+1 = 3 ≡ 3 (mod 4) 1 0 0 1 1 1
2+0 = 2 ≡ 2 (mod 4) 1 0 0 0 1 0
1+3 = 4 ≡ 0 (mod 4) 0 1 1 1 0 0
1+2 = 3 ≡ 3 (mod 4) 0 1 1 0 1 1
1+1 = 2 ≡ 2 (mod 4) 0 1 0 1 1 0
1+0 = 1 ≡ 1 (mod 4) 0 1 0 0 0 1
0+3 = 3 ≡ 3 (mod 4) 0 0 1 1 1 1
0+2 = 2 ≡ 2 (mod 4) 0 0 1 0 1 0
0+1 = 1 ≡ 1 (mod 4) 0 0 0 1 0 1
0+0 = 0 ≡ 0 (mod 4) 0 0 0 0 0 0
In[]:=AdderQ0[aL_,bL_]=Xor[aL,bL]
Out[]:=aL ⊻ bL
In[]:=AdderQ1[aL_,aH_,bL_,bH_]=Xor[And[aL,bL],Xor[aH,bH]]
Out[]:=aH ⊻ bH ⊻ (aL && bL)
We need such expression, where both parts will generate 1’s. Let’s use Wolfram Mathematica find all instances
of such expression (I glued both parts with And):
In[]:=Boole[SatisfiabilityInstances[And[AdderQ0[aL,bL],AdderQ1[aL,aH,bL,bH]],{aL,aH,bL,bH},4]]
Out[]:={1,1,0,0},{1,0,0,1},{0,1,1,0},{0,0,1,1}
Yes, indeed, Mathematica says, there are 4 inputs which will lead to the result we need. So, Mathematica can
also be used as SAT solver.
Nevertheless, let’s proceed to CNF form. Using Mathematica again, let’s convert our expression to CNF form:
In[]:=cnf=BooleanConvert[And[AdderQ0[aL,bL],AdderQ1[aL,aH,bL,bH]],``CNF'']
Out[]:=(!aH ∥ !bH) && (aH ∥ bH) && (!aL ∥ !bL) && (aL ∥ bL)
Looks more complex. The reason of such verbosity is that CNF form doesn’t allow XOR operations.
MiniSat
For starters, we can try MiniSat33 . The standard way to encode CNF expression for MiniSat is to enumerate all OR
parts at each line. Also, MiniSat doesn’t support variable names, just numbers. Let’s enumerate our variables: 1 will
be aH, 2 – aL, 3 – bH, 4 – bL.
33 http://minisat.se/MiniSat.html
29
Here is what I’ve got when I converted Mathematica expression to the MiniSat input file:
p cnf 4 4
-1 -3 0
1 3 0
-2 -4 0
2 4 0
Two 4’s at the first lines are number of variables and number of clauses respectively. There are 4 lines then, each
for each OR clause. Minus before variable number meaning that the variable is negated. Absence of minus – not
negated. Zero at the end is just terminating zero, meaning end of the clause.
In other words, each line is OR-clause with optional negations, and the task of MiniSat is to find such set of input,
which can satisfy all lines in the input file.
That file I named as adder.cnf and now let’s try MiniSat:
% minisat -verb =0 adder .cnf results .txt
SATISFIABLE
This means, if the first two variables (aH and aL) will be false, and the last two variables (bH and bL) will be set
to true, the whole CNF expression is satisfiable. Seems to be true: if bH and bL are the only inputs set to true, both
resulting bits are also has true states.
Now how to get other instances? SAT-solvers, like SMT solvers, produce only one solution (or instance).
MiniSat uses PRNG and its initial seed can be set explicitly. I tried different values, but result is still the same.
Nevertheless, CryptoMiniSat in this case was able to show all possible 4 instances, in chaotic order, though. So this
is not very robust way.
Perhaps, the only known way is to negate solution clause and add it to the input expression. We’ve got -1 -2 3
4, now we can negate all values in it (just toggle minuses: 1 2 -3 -4) and add it to the end of the input file:
p cnf 4 5
-1 -3 0
1 3 0
-2 -4 0
2 4 0
1 2 -3 -4
This means, aH and aL must be both true and bH and bL must be false, to satisfy the input expression. Let’s
negate this clause and add it again:
p cnf 4 6
-1 -3 0
1 3 0
-2 -4 0
2 4 0
1 2 -3 -4
-1 -2 3 4 0
aH=false, aL=true, bH=true, bL=false. This is also correct, according to our truth table.
Let’s add it again:
p cnf 4 7
-1 -3 0
30
1 3 0
-2 -4 0
2 4 0
1 2 -3 -4
-1 -2 3 4 0
1 -2 -3 4 0
SAT
1 -2 -3 4 0
Now MiniSat just says “UNSATISFIABLE” without any additional information in the resulting file.
Our example is tiny, but MiniSat can work with huge CNF expressions.
CryptoMiniSat
XOR operation is absent in CNF form, but crucial in cryptographical algorithms. Simplest possible way to represent
single XOR operation in CNF form is: (¬x ∨ ¬y) ∧ (x ∨ y) – not that small expression, though, many XOR operations
in single expression can be optimized better.
One significant difference between MiniSat and CryptoMiniSat is that the latter supports clauses with XOR oper-
ations instead of ORs, because CryptoMiniSat has aim to analyze crypto algorithms34 . XOR clauses are handled by
CryptoMiniSat in a special way without translating to OR clauses.
You need just to prepend a clause with “x” in CNF file and OR clause is then treated as XOR clause by Cryp-
toMiniSat. As of 2-bit adder, this smallest possible XOR-CNF expression can be used to find all inputs where both
output adder bits are set:
(aH ⊕ bH) ∧ (aL ⊕ bL)
This is .cnf file for CryptoMiniSat:
p cnf 4 2
x1 3 0
x2 4 0
Now I run CryptoMiniSat with various random values to initialize its PRNG …
% cryptominisat4 --verb 0 --random 0 XOR_adder .cnf
s SATISFIABLE
v 1 2 -3 -4 0
% cryptominisat4 --verb 0 --random 1 XOR_adder .cnf
s SATISFIABLE
v -1 -2 3 4 0
% cryptominisat4 --verb 0 --random 2 XOR_adder .cnf
s SATISFIABLE
v 1 -2 -3 4 0
% cryptominisat4 --verb 0 --random 3 XOR_adder .cnf
s SATISFIABLE
v 1 2 -3 -4 0
% cryptominisat4 --verb 0 --random 4 XOR_adder .cnf
s SATISFIABLE
v -1 2 3 -4 0
% cryptominisat4 --verb 0 --random 5 XOR_adder .cnf
34 http://www.msoos.org/xor-clauses/
31
s SATISFIABLE
v -1 2 3 -4 0
% cryptominisat4 --verb 0 --random 6 XOR_adder .cnf
s SATISFIABLE
v -1 -2 3 4 0
% cryptominisat4 --verb 0 --random 7 XOR_adder .cnf
s SATISFIABLE
v 1 -2 -3 4 0
% cryptominisat4 --verb 0 --random 8 XOR_adder .cnf
s SATISFIABLE
v 1 2 -3 -4 0
% cryptominisat4 --verb 0 --random 9 XOR_adder .cnf
s SATISFIABLE
v 1 2 -3 -4 0
2.3.3 Picosat
At least Picosat can enumerate all possible solutions without crutches I just shown:
% picosat --all adder .cnf
s SATISFIABLE
v -1 -2 3 4 0
s SATISFIABLE
v -1 2 3 -4 0
s SATISFIABLE
v 1 2 -3 -4 0
s SATISFIABLE
v 1 -2 -3 4 0
s SOLUTIONS 4
2.3.4 MaxSAT
MaxSAT problem is a problem where as many clauses should be satisfied, as possible, but maybe not all.
(Usual) clauses which must be satisfied, called hard clauses. Clauses which should be satisfied, called soft clauses.
MaxSAT solver tries to satisfy all hard clauses and as much soft clauses, as possible.
*.wcnf files are used, the format is almost the same as in DIMACS files, like:
p wcnf 207 796 208
208 1 0
208 2 0
208 3 0
208 4 0
...
1 -152 0
1 -153 0
1 -154 0
1 155 0
1 -156 0
1 -157 0
Each clause is written as in DIMACS file, but the first number if weight. MaxSAT solver tries to maximize clauses
with bigger weights first.
32
If the weight has top weight, the clause is hard clause and must always be satisfied. Top weight is set in header. In
our case, it’s 208.
Some well-known MaxSAT solvers are Open-WBO35 , etc.
• CryptoMiniSat38 . Created by Mate Soos for cryptographical problems exploration. Supports XOR clauses,
multithreading. Has Python API.
See also: Mate Soos, Karsten Nohl, Claude Castelluccia – Extending SAT Solvers to Cryptographic Problems
(LNCS, volume 5584) 39 .
MaxSAT solvers:
Something else:
What are tautological clauses? These are clauses like “... X ... -X ...” — they can be satisfied by either X=false or
X=true, so such a clause doesn’t affect anything at all and can be removed or ignored.
( src )
They all are to be filtered via CWEB programs (CWEAVE and CTANGLE). Then you can compile both C and
TeX files.
Here I did this for all these programs: https://smt.st/current_tree/basics/SAT/Knuth.
You can see that these solvers require Stanford Graphbase library, but this is just gb_flip.h module for PRNG.
You can safely use srand()/rand().
So far, I rewrote only sat0w solver to down-to-earth C language: 23.2.
Also, D.Knuth’s solver require slightly different input file format (not DIMACS), but the converters are available
at the same Knuth’s webpage.
2.5.4 EXPTIME
Time is dependent exponentially on size of input, O(2p(n) ) or just O(2n ). This is bruteforce. When you try to break
some cryptoalgorithm by bruteforcing and trying all possible inputs. For 8 bits of input, there are 28 = 256 possible
values, etc. Bruteforce can crack any cryptoalgorithm and solve almost any problem, but this is not very interesting,
because it’s extremely slow.
34
2.5.5 NP-problems
Finding correct inputs for CNF formula is an NP-problem, but you can also find them using simple bruteforce. In fact,
SAT-solvers also do bruteforce, but the resulting search tree is extremely big. And to make search faster, SAT-solvers
prune search tree as early as possible, but in unpredictable ways. This makes execution time of SAT-solvers also
unpredictable. In fact, this is a problem, to predict how much time it will take to find a solution for CNF formula.
No SAT/SMT solvers can say you ETA45 of their work.
NP problems has no O-notation, meaning, there is no link between size (and contents) of input and execution time.
In contrast to other problems listed here (constant, linear, logarithmical, exponential), you can predict time of
work by observing input values. You can predict, how long it will take to sort items by looking at a list of items to be
sorted. This is a problem for NP-problems: you can’t predict it.
Probably, you could devise faster algorithm to solve NP-problems, maybe working 1000 times faster, but still, it
will work unpredictably.
Also, both SAT and SMT solvers are very capricious to input data. This is a rare case, when shotgun debugging
46
is justified!
2.5.6 P=NP?
One of the famous problem asking, if it’s possible to solve NP-problems fast.
In other words, a scientist (or anyone else) who will find a way to solve NP-problems in predictable time (but faster
than bruteforce/EXPTIME), will make a significant or revolutionary step in computer science.
In popular opinion, this will crack all cryptoalgorithms we currently use. In fact, this is not true. Some scientists,
like Donald Knuth, believe, that there may be a way to solve NP-problems in polynomial time, but still too slow to
be practical.
However, majority of scientists believe the humankind is not ready for such problems.
A list of many attempts to solve this problem: https://www.win.tue.nl/~gwoegi/P-versus-NP.htm.
A good example is large-scale parcel packag-ing in modern logistics systems (Figure. 1), where
parcels are mostly in regular cuboid shapes, and we would like to collectively pack them into rectangular
bins of the standard dimension. Maximizing the storage use of bins effectively reduces the cost of
inventorying, wrapping, transportation, and warehousing.
( https://arxiv.org/pdf/2006.14978.pdf )
And this is what Tetris players do all the time: they serve as a robot who take objects/figurines from a conveyor
belt and pack them into boxes. A player sees only current object, and maybe a next one.
A player may think that figurines are dropping from the top, but take another look on it: figurines may be moving
on conveyor belt from top to down. And not dropping at all, just moving slowly.
And how does the game reward you? When your packing is tight, with no voids (filled rows disappears).
45 Estimated time of arrival
46 http://www.catb.org/jargon/html/S/shotgun-debugging.html https://en.wikipedia.org/wiki/Shotgun_debugging
47 https://en.wikipedia.org/wiki/Polyominoes:_Puzzles,_Patterns,_Problems,_and_Packings
35
This is what I observe almost daily in my real life near a house where I rent my flat: a backyard of an office of a
highly popular Ukrainian delivery company “Nova Poshta”48 , a guy in a company’s uniform packs all sorts of boxes
into a van, when these boxes are coming from a hatch in a wall.
Pajitnov worked for Dorodnitsyn Computing Centre of the Soviet Academy of Sciences – not a videogame company.
Probably he was busy with such an algorithm?
You may notice that virtually all puzzle games for smartphones that are popular now, are actually NP-problems.
Someone can go even further: A human-based computation game or game with a purpose49 .
These toy CPUs and computers are in fact LBAs (Linear bounded automaton) – a Turing machine with limited
tape (or memory).
While Turing’s Halting Problem is about Turing machine with infinite tape (or RAM).
48 “New Post”
49 https://en.wikipedia.org/wiki/Human-based_computation_game
Chapter 3
Equations
from z3 import *
s= Solver ()
x,y,z=Ints('x y z')
s.add(x==2)
s.add(y==3)
s.add(z==x+y)
print (s. check ())
print (s. model ())
(check -sat)
(get - model )
sat
(model
(define -fun z () (_ BitVec 16)
#x0005 )
(define -fun y () (_ BitVec 16)
#x0003 )
(define -fun x () (_ BitVec 16)
36
37
#x0002 )
)
Think about it as about electronic circuit – you can build an adder out from full-adders and logic gates. (We will
do this soon: 23.3.1.) Then you turn on your circuit and get the result at the adder’s output.
Now the inverse calculator. Can we solve the equation like x + 3 = 17?
(declare -fun x () (_ BitVec 16))
(check -sat)
(get - model )
sat
(model
(define -fun x () (_ BitVec 16)
#x000e )
)
(0xe is 14.)
( https://www.xkcd.com/287/ )
The problem is to solve the following equation: 2.15a + 2.75b + 3.35c + 3.55d + 4.20e + 5.80f == 15.05, where a..f
are integers. So this is a linear diophantine equation.
#!/ usr/bin/ python3
s.add(a <=10)
s.add(b <=10)
s.add(c <=10)
s.add(d <=10)
s.add(e <=10)
s.add(f <=10)
Out []= {{a -> 1, b -> 0, c -> 0, d -> 2, e -> 0, f -> 1},
{a -> 7, b -> 0, c -> 0, d -> 0, e -> 0, f -> 0}}
1000 means “find at most 1000 solutions”, but only 2 are found. See also: http://reference.wolfram.com/
language/ref/FindInstance.html.
+print(s.sexpr ())
+
results =[]
[f = 0, b = 0, a = 7, c = 0, d = 0, e = 0]
[f = 1, b = 0, a = 1, c = 0, d = 2, e = 0]
results total= 2
That SMT-LIB 2.x code can be feed to another SMT solver, to check its speed, for example...
( assert
(=
(bvadd
( bvmul (_ bv215 16) a)
( bvmul (_ bv275 16) b)
( bvmul (_ bv335 16) c)
( bvmul (_ bv355 16) d)
( bvmul (_ bv420 16) e)
( bvmul (_ bv580 16) f)
)
(_ bv1505 16)
)
)
;(check -sat)
40
;(get -model )
(get -all - models )
; correct answer :
;( model
; (define -fun a () (_ BitVec 16) (_ bv7 16)) ; 0x7
; (define -fun b () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun c () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun d () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun e () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun f () (_ BitVec 16) (_ bv0 16)) ; 0x0
;)
;( model
; (define -fun a () (_ BitVec 16) (_ bv1 16)) ; 0x1
; (define -fun b () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun c () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun d () (_ BitVec 16) (_ bv2 16)) ; 0x2
; (define -fun e () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun f () (_ BitVec 16) (_ bv1 16)) ; 0x1
;)
; Model count : 2
You want to produce 800 rectangles of 4*5 size and 400 rectangles of 2*3 size:
41
To cut a piece as A/B rectangles, you can cut a 6*13 workpiece in 4 ways. Or, to put it in another way, you can
place A/B rectangles on 6*13 rectangle in 4 ways:
Now the problem. Which cuts are most efficient? You want to consume as little workpieces as possible.
This is optimization problem and I can solve it with Z3:
#!/ usr/bin/env python3
from z3 import *
s= Optimize ()
s.add(cut_A >=0)
s.add(cut_B >=0)
s.add(cut_C >=0)
s.add(cut_D >=0)
s.add(out_A ==800)
s.add(out_B ==400)
s. minimize ( workpieces_total )
sat
[cut_B = 25,
cut_D = 0,
cut_A = 250 ,
out_B = 400 ,
out_A = 800 ,
workpieces_total = 275 ,
cut_C = 0]
So you want to cut 250 workpieces in A’s way and 25 pieces in B’s way, this is the most optimal way.
Also, the problem is small enough to be solved by my toy bit-blaster MK85, (thanks to the Open-WBO MaxSAT
solver):
(declare -fun workpieces_total () (_ BitVec 16))
(declare -fun cut_A () (_ BitVec 16))
(declare -fun cut_B () (_ BitVec 16))
(declare -fun cut_C () (_ BitVec 16))
(declare -fun cut_D () (_ BitVec 16))
(declare -fun out_A () (_ BitVec 16))
(declare -fun out_B () (_ BitVec 16))
( minimize workpieces_total )
(check -sat)
(get - model )
The task I solved I’ve found in Leonid Kantorovich’s book ”The Best Uses of Economic Resources” (1959). And
these are 5 pages with the task and solution 1 (in Russian).
Leonid Kantorovich was indeed consulting plywood factory in 1939 about optimal use of materials. And this is
how linear programming (LP) and ILP2 has emerged.
See also: https://en.wikipedia.org/wiki/Guillotine_cutting#Finding_an_optimal_cutting-pattern.
This puzzle3 can be solved easily, even with my toy MK85 solver:
; https :// www. bhavinionline .com /2014/10/ whatsapp -picture -puzzle -find -weight -rabbit -
cat -dog/
(set - logic QF_BV )
(set -info :smt -lib - version 2.0)
(check -sat)
(get - model )
;(get -all - models )
sat
(model
(define -fun cat () (_ BitVec 16) (_ bv7 16)) ; 0x7
(define -fun dog () (_ BitVec 16) (_ bv17 16)) ; 0x11
(define -fun rabbit () (_ BitVec 16) (_ bv3 16)) ; 0x3
)
However, one can argue that problem like that can be easily solved with bruteforce. I’ll make it more realistic and
harder.
Let’s say, there are 4 objects, each has a mass measured in some units, in milligrams or even micrograms. Each
object’s mass is 64-bit value.
We can use a weighting scale to measure object’s mass, but as it happens in the real world, the scale is not precise.
We use two type of scales: the first can measure object down to 103 · unit, the second down to 104 · unit.
We do our measures and we set mass in some range: [xxxx...000 - xxxx...999] and [xxxx...0000 - xxxx...9990].
(set - logic QF_BV )
(set -info :smt -lib - version 2.0)
(check -sat)
(get - model )
Almost correct, considering the fact that scales are not precise to the last unit.
This is the data I used for the example:
45
Listing 3.7: Wolfram Mathematica code
In []:= a= RandomInteger [(2^62) -1]
Out []= 1679813589618336900
In []:= a+b+c
Out []= 3759470599491624897
In []:= a+b+d
Out []= 5028681613540471324
In []:= c+d
Out []= 2131460860763223681
In []:= b+c
Out []= 2079657009873287997
In computer science, the subset sum problem is an important problem in complexity theory and
cryptography. The problem is this: given a set (or multiset) of integers, is there a non-empty subset
whose sum is zero? For example, given the set {−7, −3, −2, 5, 8}, the answer is yes because the subset
{−3, −2, 5} sums to zero.
( https://en.wikipedia.org/wiki/Subset_sum_problem )
It’s expressible easily as 0-1 ILP problem:
from z3 import *
set_len =len(set)
s= Solver ()
rt =[]
# rt here is [ vars_0 *-7, vars_1 *-3, vars_2 *-2, vars_3 *5, vars_4 *8]
s.add(sum(rt)==0)
46
s.add(sum(vars) >=1) # subset must not be empty
m=s. model ()
s= Solver ()
s.add(x >0)
s.add(y >0)
s.add(x+y == 4*x*y)
print s. check ()
m=s. model ()
print "the model :"
print m
print "the answer :", m. evaluate (1/x + 1/y)
Instead of pulling values from the model and then compute the final result on Python’s side, we can evaluate an
expression ( x1 + y1 ) inside the model we’ve got:
sat
the model :
[x = 1, y = 1/3]
the answer : 4
s= Solver ()
print s.check ()
print "%x" % s. model ()[m]. as_long ()
( assert (= a b))
; without this constraint , two results would be generated (with MSB =1 and MSB =0) ,
; but we need only one indeed , MSB of m has no effect of multiplication here
; and SMT - solver offers two solutions
( assert (= ( bvand m # x8000 ) # x0000 ))
(check -sat)
(get - model )
;(get -all - models )
sat
(model
(define -fun m () (_ BitVec 16) (_ bv10923 16)) ; 0 x2aab
(define -fun a () (_ BitVec 16) (_ bv1554 16)) ; 0x612
(define -fun b () (_ BitVec 16) (_ bv1554 16)) ; 0x612
)
However, it wouldn’t work for 10, because there are no modulo inverse of 10 modulo 232 , SMT solver would give
”unsat”.
48
3.8 School-level equation: KLEE
Let’s revisit school-level system of equations from (2.2.2).
We will force KLEE to find a path, where all the constraints are satisfied:
# include <assert .h>
# include "klee.h"
int main ()
{
int circle , square , triangle ;
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ klee_eq1 .c
...
% time klee --libc= uclibc --optimize --use -query -log= solver :smt2 klee_eq1 .bc
KLEE: done: total instructions = 11794
KLEE: done: completed paths = 2
KLEE: done: partially completed paths = 1
KLEE: done: generated tests = 3
KLEE has intrinsic klee_assume() which tells KLEE to cut path if some constraint is not satisfied. So we can
rewrite our example in such cleaner way:
# include <assert .h>
# include "klee.h"
int main ()
{
int circle , square , triangle ;
Interestingly, we can dump SMT-LIB 2 file here and see its contents:
% time klee --libc= uclibc --optimize --use -query -log= solver :smt2 klee_eq2 .bc
There are 3 queries, each for each path. I can try them all separately.
Aha, here the solution in surfacing gradually. First, KLEE determines the correct value for ’circle’, then for
’square’, then for ’triangle’. Each 32-bit variable of integer type is represented by 4-bytes tuple. Maybe there is some
reason for this.
int main ()
51
{
int circle , square , triangle ;
...
** Results :
eq1.c function main
[main. assertion .1] line 15 assertion 0: FAILURE
int main ()
{
int circle , square , triangle ;
...
** Results :
eq2.c function main
[main. assertion .1] line 21 assertion 0: FAILURE
...
int main ()
{
unsigned int circle , square , triangle ;
; find_symbols
(declare -fun |main ::1:: square !0 @1 #1| () (_ BitVec 32))
; set_to true
53
( assert (= | goto_symex ::&92 ;guard #2| (not (= ( bvadd ( bvmul |main ::1:: circle !0 @1 #1| |
main ::1:: square !0 @1 #1|) |main ::1:: square !0 @1 #1|) (_ bv12 32)))))
; find_symbols
(declare -fun |main ::1:: triangle !0 @1 #1| () (_ BitVec 32))
; set_to true
( assert (= | goto_symex ::&92 ;guard #3| (not (= ( bvadd ( bvmul |main ::1:: circle !0 @1 #1| |
main ::1:: square !0 @1 #1|) ( bvneg (bvmul |main ::1:: circle !0@1 #1| |main ::1:: triangle !0
@1 #1|))) |main ::1:: circle !0 @1 #1|))))
; convert
(define -fun |B9| () Bool (= |main ::1:: circle !0 @1 #1| |main ::1:: circle !0 @1 #1|))
; convert
(define -fun |B10| () Bool (= |main ::1:: square !0 @1 #1| |main ::1:: square !0 @1 #1|))
; convert
(define -fun |B11| () Bool (= |main ::1:: triangle !0@1 #1| |main ::1:: triangle !0 @1 #1|))
; convert
(define -fun |B12| () Bool (= ( bvadd |main ::1:: circle !0 @1 #1| |main ::1:: circle !0 @1 #1|)
(_ bv10 32)))
; convert
(define -fun |B13| () Bool (= ( bvadd (bvmul |main ::1:: circle !0 @1 #1| |main ::1:: square !0
@1 #1|) |main ::1:: square !0@1 #1|) (_ bv12 32)))
; convert
(define -fun |B14| () Bool (= ( bvadd (bvmul |main ::1:: circle !0 @1 #1| |main ::1:: square !0
@1 #1|) ( bvneg (bvmul |main ::1:: circle !0@1 #1| |main ::1:: triangle !0 @1 #1|))) |main
::1:: circle !0 @1 #1|))
3.10 Minesweeper
3.10.1 Cracking Minesweeper with SMT solver
For those who are not very good at playing Minesweeper (like me), it’s possible to predict bombs’ placement without
touching debugger.
Here is a clicked somewhere and I see revealed empty cells and cells with known number of “neighbours”:
54
What we have here, actually? Hidden cells, empty cells (where bombs are not present), and empty cells with
numbers, which shows how many bombs are placed nearby.
The method
Unlike many other examples, where our goal is to find a solution, here we use the fact that an instance is unsolvable
(unsat).
Here is what we can do: we will try to place a bomb to all possible hidden cells and ask Z3 SMT solver, if it can
disprove the very fact that the bomb can be placed there.
Take a look at this fragment. ”?” mark is for hidden cell, ”.” is for empty cell, number is a number of neighbours.
C1 C2 C3
R1 ? ? ?
R2 ? 3 .
R3 ? 1 .
So there are 5 hidden cells. We will check each hidden cell by placing a bomb there. Let’s first pick top/left cell:
C1 C2 C3
R1 * ? ?
R2 ? 3 .
R3 ? 1 .
Then we will try to solve the following system of equations (RrCc is cell of row r and column c):
As it turns out, this system of equations is satisfiable, so there could be a bomb at this cell. But this information
is not interesting to us, since we want to find cells we can freely click on. And we will try another one. And if the
equation will be unsatisfiable, that would imply that a bomb cannot be there and we can click on it.
(This is so called “0-1 integer linear programming”, since unknown variables are limited to 0/1.)
The code
known =[
" 01?10001? ",
" 01?100011 ",
55
" 011100000 ",
" 000000000 ",
" 111110011 ",
" ????1001? ",
" ????3101? ",
" ?????211? ",
" ????????? "]
from z3 import *
import sys
s= Solver ()
cells =[[ Int('r%d_c%d' % (r,c)) for c in range ( WIDTH +2)] for r in range ( HEIGHT +2)]
# make border
for c in range ( WIDTH +2):
s.add(cells [0][c ]==0)
s.add(cells [ HEIGHT +1][c]==0)
for r in range ( HEIGHT +2):
s.add(cells [r ][0]==0)
s.add(cells [r][ WIDTH +1]==0)
# place bomb:
s.add( cells [row ][ col ]==1)
if s. check () == unsat :
print ("row =%d col =%d, unsat !" % (row , col))
The code is almost self-explanatory. We need a border for the same reason, why Conway’s ”Game of Life” imple-
mentations also has a border (to make calculation function simpler). Whenever we know that the cell is free of bomb,
56
we put zero there. Whenever we know number of neighbours, we add a constraint, again, just like in ”Game of Life”:
number of neighbours must be equal to the number we have seen in the Minesweeper. Then we place bomb somewhere
and check.
Based on map in the example, my script first tries to put a bomb at row=1 col=3 (first ”?”). And this system of
equations being generated:
r0_c0 + r0_c1 + r0_c2 + r1_c0 + r1_c2 + r2_c0 + r2_c1 + r2_c2 == 0
r0_c1 + r0_c2 + r0_c3 + r1_c1 + r1_c3 + r2_c1 + r2_c2 + r2_c3 == 1
r0_c3 + r0_c4 + r0_c5 + r1_c3 + r1_c5 + r2_c3 + r2_c4 + r2_c5 == 1
r0_c4 + r0_c5 + r0_c6 + r1_c4 + r1_c6 + r2_c4 + r2_c5 + r2_c6 == 0
r0_c5 + r0_c6 + r0_c7 + r1_c5 + r1_c7 + r2_c5 + r2_c6 + r2_c7 == 0
r0_c6 + r0_c7 + r0_c8 + r1_c6 + r1_c8 + r2_c6 + r2_c7 + r2_c8 == 0
r0_c7 + r0_c8 + r0_c9 + r1_c7 + r1_c9 + r2_c7 + r2_c8 + r2_c9 == 1
r1_c0 + r1_c1 + r1_c2 + r2_c0 + r2_c2 + r3_c0 + r3_c1 + r3_c2 == 0
r1_c1 + r1_c2 + r1_c3 + r2_c1 + r2_c3 + r3_c1 + r3_c2 + r3_c3 == 1
r1_c3 + r1_c4 + r1_c5 + r2_c3 + r2_c5 + r3_c3 + r3_c4 + r3_c5 == 1
r1_c4 + r1_c5 + r1_c6 + r2_c4 + r2_c6 + r3_c4 + r3_c5 + r3_c6 == 0
r1_c5 + r1_c6 + r1_c7 + r2_c5 + r2_c7 + r3_c5 + r3_c6 + r3_c7 == 0
r1_c6 + r1_c7 + r1_c8 + r2_c6 + r2_c8 + r3_c6 + r3_c7 + r3_c8 == 0
r1_c7 + r1_c8 + r1_c9 + r2_c7 + r2_c9 + r3_c7 + r3_c8 + r3_c9 == 1
r1_c8 + r1_c9 + r1_c10 + r2_c8 + r2_c10 + r3_c8 + r3_c9 + r3_c10 == 1
r2_c0 + r2_c1 + r2_c2 + r3_c0 + r3_c2 + r4_c0 + r4_c1 + r4_c2 == 0
r2_c1 + r2_c2 + r2_c3 + r3_c1 + r3_c3 + r4_c1 + r4_c2 + r4_c3 == 1
r2_c2 + r2_c3 + r2_c4 + r3_c2 + r3_c4 + r4_c2 + r4_c3 + r4_c4 == 1
r2_c3 + r2_c4 + r2_c5 + r3_c3 + r3_c5 + r4_c3 + r4_c4 + r4_c5 == 1
r2_c4 + r2_c5 + r2_c6 + r3_c4 + r3_c6 + r4_c4 + r4_c5 + r4_c6 == 0
r2_c5 + r2_c6 + r2_c7 + r3_c5 + r3_c7 + r4_c5 + r4_c6 + r4_c7 == 0
r2_c6 + r2_c7 + r2_c8 + r3_c6 + r3_c8 + r4_c6 + r4_c7 + r4_c8 == 0
r2_c7 + r2_c8 + r2_c9 + r3_c7 + r3_c9 + r4_c7 + r4_c8 + r4_c9 == 0
r2_c8 + r2_c9 + r2_c10 + r3_c8 + r3_c10 + r4_c8 + r4_c9 + r4_c10 == 0
r3_c0 + r3_c1 + r3_c2 + r4_c0 + r4_c2 + r5_c0 + r5_c1 + r5_c2 == 0
r3_c1 + r3_c2 + r3_c3 + r4_c1 + r4_c3 + r5_c1 + r5_c2 + r5_c3 == 0
r3_c2 + r3_c3 + r3_c4 + r4_c2 + r4_c4 + r5_c2 + r5_c3 + r5_c4 == 0
r3_c3 + r3_c4 + r3_c5 + r4_c3 + r4_c5 + r5_c3 + r5_c4 + r5_c5 == 0
r3_c4 + r3_c5 + r3_c6 + r4_c4 + r4_c6 + r5_c4 + r5_c5 + r5_c6 == 0
r3_c5 + r3_c6 + r3_c7 + r4_c5 + r4_c7 + r5_c5 + r5_c6 + r5_c7 == 0
r3_c6 + r3_c7 + r3_c8 + r4_c6 + r4_c8 + r5_c6 + r5_c7 + r5_c8 == 0
r3_c7 + r3_c8 + r3_c9 + r4_c7 + r4_c9 + r5_c7 + r5_c8 + r5_c9 == 0
r3_c8 + r3_c9 + r3_c10 + r4_c8 + r4_c10 + r5_c8 + r5_c9 + r5_c10 == 0
r4_c0 + r4_c1 + r4_c2 + r5_c0 + r5_c2 + r6_c0 + r6_c1 + r6_c2 == 1
r4_c1 + r4_c2 + r4_c3 + r5_c1 + r5_c3 + r6_c1 + r6_c2 + r6_c3 == 1
r4_c2 + r4_c3 + r4_c4 + r5_c2 + r5_c4 + r6_c2 + r6_c3 + r6_c4 == 1
r4_c3 + r4_c4 + r4_c5 + r5_c3 + r5_c5 + r6_c3 + r6_c4 + r6_c5 == 1
r4_c4 + r4_c5 + r4_c6 + r5_c4 + r5_c6 + r6_c4 + r6_c5 + r6_c6 == 1
r4_c5 + r4_c6 + r4_c7 + r5_c5 + r5_c7 + r6_c5 + r6_c6 + r6_c7 == 0
r4_c6 + r4_c7 + r4_c8 + r5_c6 + r5_c8 + r6_c6 + r6_c7 + r6_c8 == 0
r4_c7 + r4_c8 + r4_c9 + r5_c7 + r5_c9 + r6_c7 + r6_c8 + r6_c9 == 1
r4_c8 + r4_c9 + r4_c10 + r5_c8 + r5_c10 + r6_c8 + r6_c9 + r6_c10 == 1
r5_c4 + r5_c5 + r5_c6 + r6_c4 + r6_c6 + r7_c4 + r7_c5 + r7_c6 == 1
r5_c5 + r5_c6 + r5_c7 + r6_c5 + r6_c7 + r7_c5 + r7_c6 + r7_c7 == 0
r5_c6 + r5_c7 + r5_c8 + r6_c6 + r6_c8 + r7_c6 + r7_c7 + r7_c8 == 0
r5_c7 + r5_c8 + r5_c9 + r6_c7 + r6_c9 + r7_c7 + r7_c8 + r7_c9 == 1
r6_c4 + r6_c5 + r6_c6 + r7_c4 + r7_c6 + r8_c4 + r8_c5 + r8_c6 == 3
r6_c5 + r6_c6 + r6_c7 + r7_c5 + r7_c7 + r8_c5 + r8_c6 + r8_c7 == 1
r6_c6 + r6_c7 + r6_c8 + r7_c6 + r7_c8 + r8_c6 + r8_c7 + r8_c8 == 0
r6_c7 + r6_c8 + r6_c9 + r7_c7 + r7_c9 + r8_c7 + r8_c8 + r8_c9 == 1
r7_c5 + r7_c6 + r7_c7 + r8_c5 + r8_c7 + r9_c5 + r9_c6 + r9_c7 == 2
r7_c6 + r7_c7 + r7_c8 + r8_c6 + r8_c8 + r9_c6 + r9_c7 + r9_c8 == 1
r7_c7 + r7_c8 + r7_c9 + r8_c7 + r8_c9 + r9_c7 + r9_c8 + r9_c9 == 1
row =1 col =3, unsat !
My script tries to solve the equation with no luck (unsat). That means, no bomb can be at that cell.
57
Now let’s run it for all cells:
row =1 col =3, unsat !
row =6 col =2, unsat !
row =6 col =3, unsat !
row =7 col =4, unsat !
row =7 col =9, unsat !
row =8 col =9, unsat !
I run it again:
row =7 col =1, unsat !
row =7 col =2, unsat !
row =7 col =3, unsat !
row =8 col =3, unsat !
row =9 col =5, unsat !
row =9 col =6, unsat !
I update it again:
known =[
"01110001?" ,
"01?100011" ,
"011100000" ,
"000000000" ,
"111110011" ,
"?11?1001?" ,
"222331011" ,
"??2??2110" ,
"????22?10"]
…last result:
row =9 col =1, unsat !
row =9 col =2, unsat !
Hurrah!
The files
https://smt.st/current_tree/equations/minesweeper/1_SMT.
import subprocess
f=open("tmp.cnf", "w")
f. write ("p cnf 8 "+str(len( clauses ))+"\n")
for c in clauses :
f.write (c+" 0\n")
f. close ()
It replaces a/b/c/... variables to the variable names passed (1/2/3...), reworks syntax, etc. Here is a result:
p cnf 8 64
-1 -2 -3 0
-1 -2 -4 0
-1 -2 -5 0
-1 -2 -6 0
-1 -2 -7 0
-1 -2 -8 0
-1 -3 -4 0
-1 -3 -5 0
-1 -3 -6 0
-1 -3 -7 0
-1 -3 -8 0
-1 -4 -5 0
-1 -4 -6 0
-1 -4 -7 0
-1 -4 -8 0
-1 -5 -6 0
-1 -5 -7 0
-1 -5 -8 0
-1 -6 -7 0
-1 -6 -8 0
-1 -7 -8 0
1 2 3 4 5 6 7 0
1 2 3 4 5 6 8 0
1 2 3 4 5 7 8 0
1 2 3 4 6 7 8 0
1 2 3 5 6 7 8 0
1 2 4 5 6 7 8 0
1 3 4 5 6 7 8 0
-2 -3 -4 0
-2 -3 -5 0
-2 -3 -6 0
-2 -3 -7 0
-2 -3 -8 0
-2 -4 -5 0
-2 -4 -6 0
-2 -4 -7 0
-2 -4 -8 0
-2 -5 -6 0
-2 -5 -7 0
-2 -5 -8 0
-2 -6 -7 0
-2 -6 -8 0
-2 -7 -8 0
2 3 4 5 6 7 8 0
-3 -4 -5 0
-3 -4 -6 0
-3 -4 -7 0
-3 -4 -8 0
-3 -5 -6 0
-3 -5 -7 0
-3 -5 -8 0
-3 -6 -7 0
62
-3 -6 -8 0
-3 -7 -8 0
-4 -5 -6 0
-4 -5 -7 0
-4 -5 -8 0
-4 -6 -7 0
-4 -6 -8 0
-4 -7 -8 0
-5 -6 -7 0
-5 -6 -8 0
-5 -7 -8 0
-6 -7 -8 0
The variable name in results lacking minus sign is True. Variable name with minus sign is False. We see there are
just two variables are True: 1 and 8. This is indeed correct: MiniSat solver found a condition, for which our function
returns True. Zero at the end is just a terminal symbol which means nothing.
We can ask MiniSat for another solution, by adding current solution to the input CNF file, but with all variables
negated:
...
-5 -6 -8 0
-5 -7 -8 0
-6 -7 -8 0
-1 2 3 4 5 6 7 -8 0
In plain English language, this means “give me ANY solution which can satisfy all clauses, but also not equal to
the last clause we’ve just added”.
MiniSat, indeed, found another solution, again, with only 2 variables equal to True:
% minisat -verb =0 tst2.cnf results .txt
SATISFIABLE
By the way, population count function for 8 neighbours (POPCNT8) in CNF form is simplest:
a&&b&&c&&d&&e&&f&&g&&h
Minesweeper
Now we can use Mathematica to generate all population count functions for 0..8 neighbours.
For 9·9 Minesweeper matrix including invisible border, there will be 11·11 = 121 variables, mapped to Minesweeper
matrix like this:
1 2 3 4 5 6 7 8 9 10 11
12 13 14 15 16 17 18 19 20 21 22
23 24 25 26 27 28 29 30 31 32 33
34 35 36 37 38 39 40 41 42 43 44
...
100 101 102 103 104 105 106 107 108 109 110
111 112 113 114 115 116 117 118 119 120 121
Then we write a Python script which stacks all population count functions: each function for each known number
of neighbours (digit on Minesweeper field). Each POPCNTx() function takes list of variable numbers and outputs list
of clauses to be added to the final CNF file.
As of empty cells, we also add them as clauses, but with minus sign, which means, the variable must be False.
Whenever we try to place bomb, we add its variable as clause without minus sign, this means the variable must be
True.
Then we execute external minisat process. The only thing we need from it is exit code. If an input CNF is UNSAT,
it returns 20:
We use here the information from the previous solving of Minesweeper: 3.10.1.
#!/ usr/bin/ python3
import subprocess
WIDTH =9
HEIGHT =9
VARS_TOTAL =( WIDTH +2) *( HEIGHT +2)
known =[
" 01?10001? ",
" 01?100011 ",
" 011100000 ",
" 000000000 ",
" 111110011 ",
" ????1001? ",
" ????3101? ",
" ?????211? ",
" ????????? "]
# place a bomb
clauses . append ( coords_to_var (row ,col))
f=open("tmp.cnf", "w")
f.write ("p cnf "+str( VARS_TOTAL )+" "+str(len( clauses ))+"\n")
for c in clauses :
f. write (c+" 0\n")
f.close ()
68
child = subprocess .Popen ([" minisat ", "tmp.cnf"], stdout = subprocess .PIPE)
child .wait ()
# 10 is SAT , 20 is UNSAT
if child . returncode ==20:
print ("row =%d, col =%d, unsat !" % (row , col))
( https://smt.st/current_tree/equations/minesweeper/2_SAT/minesweeper_SAT.py )
The output CNF file can be large, up to ≈ 2000 clauses, or more, here is an example: https://smt.st/current_
tree/equations/minesweeper/2_SAT/sample.cnf.
Anyway, it works just like my previous Z3Py script:
row =1, col =3, unsat !
row =6, col =2, unsat !
row =6, col =3, unsat !
row =7, col =4, unsat !
row =7, col =9, unsat !
row =8, col =9, unsat !
…but it runs way faster, even considering overhead of executing external program. Perhaps, Z3Py version could
be optimized better?
The files, including Wolfram Mathematica notebook: https://smt.st/current_tree/equations/minesweeper/
2_SAT.
3.10.3 Optimization
A simple observation can help us: there is no need to probe hidden cells in the middle of other hidden cells. In fact,
only those hidden cells surrounding digits (known number of bombs) is to be probed: It speeds up everything a little.
...
def get_from_known_or_empty (known , r, c):
# I'm lazy to do overflow checks
try:
return known [r][c]
except IndexError :
return ""
( https://smt.st/current_tree/equations/minesweeper/3_SMT_opt/minesweeper_Z3_with_tests.py )
( https://smt.st/current_tree/libs/SAT_lib.py )
Now if you will look closely on the output of sorting network, it looks like a thermometer, isn’t it? This is indeed
unary coding, or thermometer code, where 1 is encoded as 1, 2 as 11... 4 as 1111, etc. Who need such a wasteful code?
For 9 inputs/outputs, we can afford it so far.
In other words, sorting network is a device counting input bits and giving output in unary coding.
Also, we don’t need to add 9 constraints for each variable. Only two will suffice, one False and one True, because
we are only interesting in the level of a thermometer.
72
( https://smt.st/current_tree/libs/SAT_lib.py )
And the whole source code:
#!/ usr/bin/ python3
import SAT_lib
from typing import List
WIDTH =9
HEIGHT =9
known =[
" 01?10001? ",
" 01?100011 ",
" 011100000 ",
" 000000000 ",
" 111110011 ",
" ????1001? ",
" ????3101? ",
" ?????211? ",
" ????????? "]
# place a bomb
s. fix_always_true (vars[row ][ col ])
if s. solve () == False :
print ("row =%d, col =%d, unsat !" % (row , col))
As before, this is a list of Minesweeper cells you can safely click on:
row =1, col =3, unsat !
row =6, col =2, unsat !
row =6, col =3, unsat !
row =7, col =4, unsat !
row =7, col =9, unsat !
row =8, col =9, unsat !
However, it performs several times slower than the version with Mathematica-generated POPCNT functions, which
is the fastest version so far...
Nevertheless, sorting networks has important place in SAT/SMT world. By fixing a “level” of a thermometer using
a single constraint, it’s possible to add PB (pseudo-boolean) constraints, like, “x>=10” (you need just to force a “level”
to be always higher or equal than 10).
3.11 LCG
3.11.1 Cracking LCG with Z3
There are well-known weaknesses of LCG 8 , but let’s see, if it would be possible to crack it straightforwardly, without
any special knowledge. We will define all relations between LCG states in terms of Z3. Here is a test program:
# include <stdlib .h>
# include <stdio .h>
# include <time.h>
int main ()
{
7 https://www.claymath.org/sites/default/files/minesweeper.pdf
8 http://en.wikipedia.org/wiki/Linear_congruential_generator#Advantages_and_disadvantages_of_LCGs, http://www.reteam.org/
papers/e59.pdf, http://stackoverflow.com/questions/8569113/why-1103515245-is-used-in-rand/8574774#8574774
74
int i;
srand (time(NULL));
Let’s say we are observing only 8 of these numbers (from 29 to 61) and we need to predict next one (17) and/or
previous one (37).
The program is compiled using MSVC 2013 (I choose it because its LCG is simpler than that in Glib):
.text :0040112 E rand proc near
.text :0040112 E call __getptd
.text :00401133 imul ecx , [eax +0 x14], 214013
.text :0040113 A add ecx , 2531011
.text :00401140 mov [eax +14h], ecx
.text :00401143 shr ecx , 16
.text :00401146 and ecx , 7FFFh
.text :0040114 C mov eax , ecx
.text :0040114 E retn
.text :0040114 E rand endp
s = Solver ()
print(s.check ())
print(s.model ())
URem states for unsigned remainder. It works for some time and gave us correct result!
sat
[ state3 = 2276903645 ,
state4 = 1467740716 ,
state5 = 3163191359 ,
state7 = 4108542129 ,
state8 = 2839445680 ,
state2 = 998088354 ,
state6 = 4214551046 ,
state1 = 1791599627 ,
state9 = 548002995 ,
output_next = 17,
output_prev = 37,
state10 = 1390515370]
I added ≈ 10 states to be sure result will be correct. It may be not in case of smaller set of information.
That is the reason why LCG is not suitable for any security-related task. This is why cryptographically secure
pseudorandom number generators exist: they are designed to be protected against such simple attack. Well, at least
if NSA9 don’t get involved 10 .
Security tokens like “RSA SecurID” can be viewed just as CPRNG11 with a secret seed. It shows new pseudorandom
number each minute, and the server can predict it, because it knows the seed. Imagine if such token would implement
LCG—it would be much easier to break!
from z3 import *
s = Solver ()
sat
[ state3 = 1181667981 ,
state4 = 342792988 ,
state5 = 4116856175 ,
state7 = 1741999969 ,
state8 = 3185636512 ,
state2 = 1478548498 ,
state6 = 4036911734 ,
state1 = 286227003 ,
state9 = 1700675811]
... and at some point, this piece of code can generate 8 zeroes in row, if the state will be 286227003 (decimal).
Just checked this piece of code in MSVC 2015:
// MSVC 2015 x86
int main ()
{
srand (286227003) ;
from z3 import *
77
state1 = BitVec ('state1 ', 32)
state2 = BitVec ('state2 ', 32)
state3 = BitVec ('state3 ', 32)
state4 = BitVec ('state4 ', 32)
state5 = BitVec ('state5 ', 32)
s = Solver ()
sat
[ state3 = 635704497 ,
state4 = 1644979376 ,
state2 = 1055176198 ,
state1 = 3865742399 ,
state5 = 1389375667]
However, 4 consecutive zeroes modulo 100 is impossible (given these constants at least), this gives “unsat”: https:
//smt.st/current_tree/equations/LCG/LCG100_v1.py.
... and 3 consecutive zeroes modulo 1000:
#!/ usr/bin/ python3
from z3 import *
s = Solver ()
sat
[ state3 = 1179663182 ,
state2 = 720934183 ,
state1 = 4090229556 ,
state4 = 786474201]
What if we could use rand()’s output without division? Which is in 0..0x7fff range (i.e., 15 bits)? As it can be
checked quickly, 2 zeroes at output is possible:
78
from z3 import *
s = Solver ()
sat
[ state2 = 20057 , state1 = 3385131726 , state3 = 22456]
from z3 import *
s = Solver ()
Yes:
sat
[ state3 = 2234253076 ,
state4 = 497021319 ,
state5 = 4160988718 ,
c = 3,
state2 = 333151205 ,
state6 = 46785593 ,
state1 = 1512500810 ,
state7 = 1158878744]
If srand(time(NULL)) will be executed at Tue Dec 5 21:06:50 EET 2017 (this precise second, UNIX time=1512500810),
a next 6 rand() % 10 lines will output six numbers of 3 in a row. Don’t know if it useful or not, but you’ve got the
idea.
etc:
The files: https://smt.st/current_tree/equations/LCG.
Further work: check glibc’s rand(), Mersenne Twister, etc. A simple 32-bit LCG (as described) can be checked
using simple brute-force, I think.
Fun story
The software checked protection key (dongle) randomly, from time to time. This code snippet is from a real one:
void init_all ()
{
...
srand (time(NULL));
...
};
...
void check_protection_thread ()
{
// get in 0..9 range
int t=( int)(( double )rand () /3276) ;
if (t== 5)
{
check protection
}
};
Perhaps, we can find the most optimal UNIX time to start the software, so the protection will not be checked as
long as possible...
Further reading
Breaking JavaScript’s PRNG (XorShift128+):
https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f.
( assert (= ( bvurem (bvand ( bvlshr state2 (_ bv16 32)) # x00007fff ) (_ bv1000 32)) (_
bv0 32)))
( assert (= ( bvurem (bvand ( bvlshr state3 (_ bv16 32)) # x00007fff ) (_ bv1000 32)) (_
bv0 32))) ; isn 't it redundant ?
( assert (= ( bvurem (bvand ( bvlshr state4 (_ bv16 32)) # x00007fff ) (_ bv1000 32)) (_
bv0 32))) ; isn 't it redundant ?
(check -sat)
(get - model )
Version 2: functions
We can define functions in SMT-LIB language:
(set - logic QF_BV )
(set -info :smt -lib - version 2.0)
(check -sat)
(get - model )
(check -sat)
(get - model )
Here we use the QF_ABV SMT logic: same as in QF_BV, but A means arrays.
We use arrays also in this example: 5.2.2.
Here we define relationships between various elements of array:
( assert (= ( PRNG_step ( select states #x1)) ( select states #x2)))
( assert (= ( PRNG_step ( select states #x2)) ( select states #x3)))
( assert (= ( PRNG_step ( select states #x3)) ( select states #x4)))
12 Uninterpreted Function
83
(check -sat)
(get - model )
In Plain English, this means: ”for all (32-bit) X’s, rand(x)’s value is always equals to (bvadd (bvmul X const1)
const2))))”.
This is First-Order Logic. ”For all” is an universal quantifier, usually denoted as an ”A” letter upside down: ∀.
Since quantifiers exists in the example, another SMT logic is to be used: AUFBV (A stands for Arrays, UF for UF,
BV stands for bitvector, but notice we dropped the QF_ prefix).
UF’s are then called, as in Lisp language: ”(function_name arg1 arg2 ...)”.
Performance
(Time in seconds, running on my relic Intel Core 2 Duo T9400, clocked at 2.13GHz.)
Bruteforce
But what about bruteforce? Yes, you can solve this problem using simple bruteforce. 32-bit search space is small
enough. But... this is what I can do in pure C:
# include <stdio .h>
# include <stdlib .h>
int main ()
{
for ( unsigned int seed =0; ; seed ++)
{
unsigned int t=seed;
t=t *214013 + 2531011;
unsigned int a1 =((t > >16) &0 x7fff ) % 1000;
if (a1 !=0)
continue ;
t=t *214013 + 2531011;
unsigned int a2 =((t > >16) &0 x7fff ) % 1000;
if (a2 !=0)
continue ;
t=t *214013 + 2531011;
unsigned int a3 =((t > >16) &0 x7fff ) % 1000;
if (a3 ==0)
{
printf ("seed: 0x%x\n", seed);
exit (0);
};
if (seed ==0 xffffffff )
break ;
};
};
84
If compiled with GCC -O3, it will run for 12 seconds on the same CPU. Faster than Z3 and CVC4, but slower than
STP and Boolector! Modulo division is a heavy operation, but it seems, these SMT solvers can find ways to cut the
search space!
The files
https://smt.st/current_tree/equations/LCG
s= Solver ()
s.add(out ==n)
s.add(in1*in2 == out)
# inputs cannot be negative and must be non -1:
s.add(in1 >1)
s.add(in2 >1)
if s. check () == unsat :
print (n,"is prime (unsat )")
return [n]
if s. check () == unknown :
print (n,"is probably prime ( unknown )")
return [n]
m=s. model ()
# get inputs of multiplier :
in1_n =m[in1 ]. as_long ()
in2_n =m[in2 ]. as_long ()
# infinite test:
def test ():
13 https://projecteuler.net/problem=803
85
while True:
print ( factor ( random . randrange (1000000000) ))
#test ()
( https://stackoverflow.com/questions/13898175/how-does-z3-handle-non-linear-integer-arithmetic )
Probably, this is the case: we getting ”unknown” in the case when a number cannot be factored, i.e., it’s prime.
It’s also very slow. Wolfram Mathematica can factor number around 280 in a matter of seconds. Still, I’ve written
this for demonstration.
The problem of breaking RSA is a problem of factorization of very large numbers, up to 24096 . It’s currently not
possible to do this in practice.
It’s somewhat mind-boggling to realize that numbers can be factored without using any number
theory! No greatest common divisors, no applications of Fermat’s theorems, etc., are anywhere in
sight. We’re providing no hints to the solver except for a bunch of Boolean formulas that operate
almost blindly at the bit level. Yet factors are found.
Of course we can’t expect this method to compete with the sophisticated factorization algorithms
of Section 4.5.4. But the problem of factoring does demonstrate the great versatility of clauses. And
its clauses can be combined with other constraints that go well beyond any of the problems we’ve
studied before.
A
S
B
Figure 3.6: A half-adder. (The image has been taken from Wikipedia.)
A
B
S
Cin
Carry-block
Tc
Cout
Figure 3.7: A full-adder. (The image has been taken from Wikipedia.)
X0 Y0 X1 Y1 X2 Y2 X3 Y3
X0 Y0 X1 Y1 X2 Y2 X3 Y3
What carries are? 4-bit adder can sum up two numbers up to 0b1111 (15). 15+15=30 and this is 0b11110, i.e., 5
bits. Lowest 4 bits is a sum. 5th most significant bit is not a part of sum, but is a carry bit.
If you sum two numbers on x86 CPU, CF flag is a carry bit connected to ALU14 . It is set if a resulting sum is
bigger than it can be fit into result.
Now you can also need carry-in. Again, x86 CPU has ADC instruction, it takes CF flag state. It can be said, CF
flag is connected to adder’s carry-in input. Hence, combining two ADD and ADC instructions you can sum up 128
bits on 64-bit CPU.
By the way, this is a good explanation of ”carry-ripple”. The very first full-adder’s result is depending on the
carry-out of the previous full-adder. Hence, adders cannot work in parallel. This is a problem of simplest possible
adder, other adders can solve this.
To represent full-adders in CNF form, we can use Wolfram Mathematica.
In Mathematica, I’m setting ”->1” if row is correct and ”->0” if not correct.
In [59]:= FaTbl = {{0 , 0, 0, 0, 0} -> 1, {0, 0, 0, 0, 1} ->
0, {0, 0, 0, 1, 0} -> 0, {0, 0, 0, 1, 1} -> 0, {0, 0, 1, 0, 0} ->
0, {0, 0, 1, 0, 1} -> 1, {0, 0, 1, 1, 0} -> 0, {0, 0, 1, 1, 1} ->
0, {0, 1, 0, 0, 0} -> 0, {0, 1, 0, 0, 1} -> 1, {0, 1, 0, 1, 0} ->
0, {0, 1, 0, 1, 1} -> 0, {0, 1, 1, 0, 0} -> 0, {0, 1, 1, 0, 1} ->
0, {0, 1, 1, 1, 0} -> 1, {0, 1, 1, 1, 1} -> 0, {1, 0, 0, 0, 0} ->
0, {1, 0, 0, 0, 1} -> 1, {1, 0, 0, 1, 0} -> 0, {1, 0, 0, 1, 1} ->
0, {1, 0, 1, 0, 0} -> 0, {1, 0, 1, 0, 1} -> 0, {1, 0, 1, 1, 0} ->
1, {1, 0, 1, 1, 1} -> 0, {1, 1, 0, 0, 0} -> 0, {1, 1, 0, 0, 1} ->
0, {1, 1, 0, 1, 0} -> 1, {1, 1, 0, 1, 1} -> 0, {1, 1, 1, 0, 0} ->
0, {1, 1, 1, 0, 1} -> 0, {1, 1, 1, 1, 0} -> 0, {1, 1, 1, 1, 1} -> 1}
...
In [60]:= BooleanConvert [
BooleanFunction [FaTbl , {a, b, cin , cout , s}], "CNF "]
return s, cout
If bit from Y is zero, a row is zero. If bit from Y is non-zero, a row is equal to X, but shifted each time. Then you
just sum up all rows (which are called ”partial products”.)
This is 4-bit binary multiplier. It takes 4-bit inputs and produces 8-bit output:
...
90
# bit is 0 or 1.
# i.e., if it 's 0, output is 0 (all bits)
# if it 's 1, output = input
def mult_by_bit (self , X, bit):
return [self.AND(i, bit) for i in X]
AND gate is constructed here using Tseitin transformations. This is quite popular way of encoding gates in
CNF form, by adding additional variable: https://en.wikipedia.org/wiki/Tseytin_transformation. In fact,
full-adder can be constructed without Mathematica, using logic gates, and encoded by Tseitin transformation.
# size of inputs .
# in other words , how many bits we have to allocate to store 'n '?
input_bits =int(math.ceil(math.log(n ,2)))
print (" input_bits =%d" % input_bits )
# at least one bit in each input must be set , except the lowest one.
# hence we restrict inputs to be greater than 1
if len( factor1 ) >1:
s.fix(s. OR_list ( factor1 [: -1]) , True)
if len( factor2 ) >1:
s.fix(s. OR_list ( factor2 [: -1]) , True)
91
if s. solve () == False :
print ("%d is prime ( unsat )" % n)
return [n]
# infinite test:
def test ():
while True:
print ( factor ( random . randrange (100000000000) ))
#test ()
I just connect our number to output of multiplier and ask SAT solver to find inputs. If it’s UNSAT, this is prime
number. Then we factor factors recursively.
Also, we want block input factors of 1, because obviously, we do not interesting in the fact that n*1=n. I’m using
wide OR gates for this.
Output:
% python factor_SAT .py
factoring 1234567890
input_bits =31
factors of 1234567890 are 2 and 617283945
factoring 2
input_bits =1
2 is prime (unsat )
factoring 617283945
input_bits =30
factors of 617283945 are 3 and 205761315
factoring 3
input_bits =2
3 is prime (unsat )
factoring 205761315
input_bits =28
factors of 205761315 are 3 and 68587105
factoring 3
input_bits =2
3 is prime (unsat )
factoring 68587105
input_bits =27
factors of 68587105 are 5 and 13717421
factoring 5
input_bits =3
5 is prime (unsat )
factoring 13717421
input_bits =24
factors of 13717421 are 3607 and 3803
92
factoring 3607
input_bits =12
3607 is prime (unsat )
factoring 3803
input_bits =12
3803 is prime (unsat )
[2, 3, 3, 5, 3607 , 3803]
# size of inputs .
# in other words , how many bits we have to allocate to store 'n '?
input_bits =int(math.ceil(math.log(dividend ,2)))
print (" input_bits =%d" % input_bits )
if s. solve () == False :
print (" remainder !=0 ( unsat )")
return None
As it turns out, though overkill, this can be solved using MK85 with little effort:
#!/ usr/bin/ python3
s=MK85 ()
cur_R =0
cur_C =0
if s. check ():
m=s. model ()
15 The blog post in Russian: http://thesz.livejournal.com/280784.html
94
for r in range ( HEIGHT ):
for c in range (WIDTH ):
sys. stdout .write (str(m[ coord_to_name (r, c)])+"\t")
sys. stdout . write ("\n")
else:
print (" UNSAT ")
( https://smt.st/current_tree/equations/spreadsheet/spreadsheet_MK85.py )
All we do is just creating pack of variables for each cell, named A0, B1, etc, of integer type. All of them are stored
in cells[] dictionary. Key is a string. Then we parse all the strings from cells, and add to list of constraints A0=123 (in
case of number in cell) or A0=B1+C2 (in case of expression in cell). There is a slight preparation: string like A0+B2
becomes cells[”A0”]+cells[”B2”].
Then the string is evaluated using Python eval() method, which is highly dangerous 16 : imagine if end-user could
add a string to cell other than expression? Nevertheless, it serves our purposes well, because this is a simplest way to
pass a string with expression into MK85.
3.14.1 Z3
The source code almost the same:
#!/ usr/bin/ python3
from z3 import *
import sys , re
s= Solver ()
cur_R =0
cur_C =0
( https://smt.st/current_tree/equations/spreadsheet/spreadsheet_Z3_1.py )
Two first cells of the last row (C0 and C1) are linked to each other. Our program will just tells “unsat”, meaning,
it couldn’t satisfy all constraints together. We can’t use this as error message reported to end-user, because it’s highly
unfriendly.
Our question now is: which variables ”spoiling” the whole spreadsheet and ”to be dropped” to get everything
solvable?
We can fetch unsat core, i.e., list of variables which Z3 finds conflicting.
...
s= Solver ()
s.set( unsat_core =True)
...
# add constraint :
s. assert_and_track (e, coord_to_name (cur_R , cur_C ))
...
if result ==" sat ":
...
else:
print s. unsat_core ()
( https://smt.st/current_tree/equations/spreadsheet/spreadsheet_Z3_2.py )
We must explicitly turn on unsat core support and use assert_and_track() instead of add() method, because this
feature slows down the whole process, and is turned off by default. That works:
% python 2.py test_circular
unsat
[C0 , C1]
Perhaps, these variables could be removed from the 2D array, marked as unresolved and the whole spreadsheet
could be recalculated again.
... but. Needless to say, that this is a circular dependency, but not a problem (C1+123 and C0-123):
1 0 B0+B2 A0*B0
123 10 12 11
C1 +123 C0 -123 A0 *122 A3+C2
sat
1 0 135 123
123 10 12 11
-1 -124 122 245
Funny, but Microsoft Excel can’t handle such circular (but correct) dependencies:
Error 522 is17 : “Circular reference”, “Formula refers directly or indirectly to itself and the Iterations option is not
set under Tools - Options - LibreOffice Calc - Calculate.”.
OK, I set this option on (to 0) and got Error 523:
“The calculation procedure does not converge”, “Function missed a targeted value, or iterative references do not
reach the minimum change within the maximum number of steps set.”
17 https://help.libreoffice.org/6.4/en-GB/text/scalc/05/02140000.html
97
Setting Iterations option to 1 seems to solve that problem:
Arrows will represent information flow. So a vertex (node) which has no incoming arrows to it (indegree=0), can
be set to a random number. Then we use topological sort to find dependencies between vertices. Then we assign
spreadsheet cell names to each vertex. Then we generate random expression with random operations/numbers/cells
to each cell, with the use of information from topological sorted graph.
(* Utility functions *)
In [1]:= findSublistBeforeElementByValue [lst_ , element_ ]:= lst [[ 1;; Position [lst ,
element ][[1]][[1]] -1]]
(* We omit division operation because micro - spreadsheet evaluator can 't handle
division by zero *)
In [5]:= interleaveListWithRandomOperationsAsStrings [lst_ ]:= Riffle [lst , Table [
99
RandomChoice [{"+" ," -" ,"*"}] , Length [lst ] -1]]
In [8]:= assignNumberOrExpr [g_ , vertex_ ]:= If[ VertexInDegree [g, vertex ]==0 ,
randomNumberAsString [], randomNonNumberExpression [g, vertex ]]
(* Main part *)
(* Create random graph *)
In [21]:= WIDTH =7; HEIGHT =8; TOTAL = WIDTH * HEIGHT
Out [21]= 56
...
(* Make 2D table of it *)
In [27]:= t= Partition [ expressions , WIDTH ];
Using this script, I can generate random spreadsheet of 26 · 500 = 13000 cells, which seems to be processed by Z3
in couple of seconds.
Black Box is an abstract board game for one or two players, which simulates shooting rays into a
black box to deduce the locations of ”atoms” hidden inside. It was created by Eric Solomon. The board
game was published by Waddingtons from the mid-1970s and by Parker Brothers in the late 1970s.
The game can also be played with pen and paper, and there are numerous computer implementations
for many different platforms, including one which can be run from the Emacs text editor.
Black Box was inspired by the work of Godfrey Hounsfield who was awarded the 1979 Nobel Prize
in Medicine for his invention of the CAT scanner.
( https://en.wikipedia.org/wiki/Black_Box_(game) )
How computed tomography (CT scan) actually works? A human body is bombarded by X-rays in various angles
by X-ray tube in rotating torus. X-ray detectors are also located in torus, and all the information is recorded.
Many of readers probably have seen CT scan device in dental clinics, like:
Video: https://youtu.be/0eoYdtAmr8s?t=114. It rotates around your head and then returns to initial position.
One half of device has something emitting (so far, no matter what is this: X-ray or MRI), another half has an array
of detectors.
Here is we can simulate a simple tomograph. An “i” character is rotating and will be “enlighten” at 4 angles. Let’s
imagine, character is bombarded by X-ray tube at left. All asterisks in each row is then summed and sum is ”received”
by the X-ray detector at the right.
WIDTH= 11 HEIGHT = 11
angleπ=(/4) *0
** 2
** 2
0
*** 3
** 2
** 2
** 2
** 2
** 2
**** 4
0
[2, 2, 0, 3, 2, 2, 2, 2, 2, 4, 0] ,
angleπ=(/4) *1
0
0
101
* 1
** 2
* 1
** 2
** 2
**** 4
* 1
* 1
0
[0, 0, 1, 2, 1, 2, 2, 4, 1, 1, 0] ,
angleπ=(/4) *2
0
0
0
0
* 1
** ******* 9
** ******* 9
* * 2
0
0
0
[0, 0, 0, 0, 1, 9, 9, 2, 0, 0, 0] ,
angleπ=(/4) *3
0
0
* 1
** 2
** * 3
*** 3
** 2
0
** 2
* 1
0
[0, 0, 1, 2, 3, 3, 2, 0, 2, 1, 0] ,
How do we recover the original image? We are going to represent 11*11 matrix, where sum of each row must be
equal to some value we already know. Then we rotate matrix, and do this again.
For the first matrix, the system of equations looks like that (we put there a values from the first vector):
C1 ,1 + C1 ,2 + C1 ,3 + ... + C1 ,11 = 2
C2 ,1 + C2 ,2 + C2 ,3 + ... + C2 ,11 = 2
...
vectors =[
[2, 2, 0, 3, 2, 2, 2, 2, 2, 4, 0] ,
[0, 0, 1, 2, 1, 2, 2, 4, 1, 1, 0] ,
[0, 0, 0, 0, 1, 9, 9, 2, 0, 0, 0] ,
[0, 0, 1, 2, 3, 3, 2, 0, 2, 1, 0]]
s= Solver ()
cells =[[ Int('cell_r =% d_c =%d' % (r,c)) for c in range ( WIDTH )] for r in range ( HEIGHT )]
***
**
**
**
**
**
****
* **
**
* *
**
* *
* *
****
However, the result is correct, but only 3 vectors allows too many possible “original images”, and Z3 SMT-solver
finds first.
Further reading: https://en.wikipedia.org/wiki/Discrete_tomography, https://en.wikipedia.org/wiki/
2-satisfiability#Discrete_tomography, https://en.wikipedia.org/wiki/Black_Box_(game).
3.16 Cribbage
I’ve found this problem in the Ronald L. Graham, Donald E. Knuth, Oren Patashnik – “Concrete Mathematics” book:
My solution:
#!/ usr/bin/env python3
from z3 import *
s= Solver ()
s.add(Sum(cells )==N)
if s. check () == sat:
m=s. model ()
print ("(%d terms ) %d + ... + %d == %d" % (terms , m[ cells [0]]. as_long () , m[
cells [terms -1]]. as_long () , N))
#N=15
N =1050
The result:
(3 terms ) 349 + ... + 351 == 1050
(4 terms ) 261 + ... + 264 == 1050
(5 terms ) 208 + ... + 212 == 1050
105
(7 terms ) 147 + ... + 153 == 1050
(12 terms ) 82 + ... + 93 == 1050
(15 terms ) 63 + ... + 77 == 1050
(20 terms ) 43 + ... + 62 == 1050
(21 terms ) 40 + ... + 60 == 1050
(25 terms ) 30 + ... + 54 == 1050
(28 terms ) 24 + ... + 51 == 1050
(35 terms ) 13 + ... + 47 == 1050
In the United Kingdom the currency is made up of pound (£) and pence (p). There are eight coins
in general circulation:
1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), and £2 (200p).
It is possible to make £2 in the following way:
1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p
How many different ways can £2 be made using any number of coins?
from z3 import *
while True:
if s. check () == sat:
m = s.model ()
print (m)
result . append (m)
# Create a new constraint the blocks the current model
block = []
for d in m:
# create a constant from declaration
c=d()
block . append (c != m[d])
s.add(Or(block ))
else:
print (len( result ))
break
Soltuion:
#!/ usr/bin/env python3
from z3 import *
s= Solver ()
...
[b = 7, a = 0]
[b = 6, a = 8]
[b = 7, a = 8]
[b = 6, a = 12]
[b = 7, a = 12]
[b = 12, a = 0]
[b = 13, a = 0]
[b = 12, a = 8]
[b = 13, a = 8]
[b = 12, a = 4]
107
[b = 13, a = 4]
[b = 12, a = 12]
[b = 13, a = 12]
[b = 14, a = 0]
[b = 15, a = 0]
[b = 14, a = 4]
[b = 15, a = 4]
[b = 14, a = 8]
[b = 15, a = 8]
[b = 14, a = 12]
[b = 15, a = 12]
results total= 128
(This is usually done using simpler algorithm, but it will contain conditional jumps, which is bad for CPUs starting
at RISC. There are no conditional jumps in this algorithm.)
The magic number used here is called de Bruijn sequence, and I once used bruteforce to find it (one of the results
was 0x4badf0d, which is used here). But what if we need magic number for 64-bit values? Bruteforce is not an option
here.
If you already read about these sequences in my blog or in other sources, you can see that the 32-bit magic number
is a number consisting of 5-bit overlapping chunks, and all chunks must be unique, i.e., must not be repeating.
For 64-bit magic number, these are 6-bit overlapping chunks.
To find the magic number, one can find a Hamiltonian path of a de Bruijn graph. But I’ve found that Z3 is also
can do this, though, overkill, but this is more illustrative.
from z3 import *
tmp =[]
for i in range (64):
tmp. append ((out >>i)&0 x3F)
s= Solver ()
20 https://xn--mxacd.xn--qxam/math-notes.pdf
21 https://en.wikipedia.org/wiki/Find_first_set
108
We just enumerate all overlapping 6-bit chunks and tell Z3 that they must be unique (see Distinct).
Overlapping chunks are clearly visible. So the magic number is 0x79c52dd0991abf60. Let’s check:
# include <stdint .h>
# include <stdio .h>
# include <assert .h>
int main ()
{
// construct magic table
// may be omitted in production code
for (int i=0; i <64; i++)
magic_tbl [( MAGIC /(1 ULL <<i)) & 0x3F ]=i;
// test
for (int i=0; i <64; i++)
{
printf (" input =0x%llx , result =%d\n", 1ULL <<i, bitpos (1ULL <<i));
assert ( bitpos (1ULL <<i)==i);
110
};
assert ( tzcnt (0 xFFFF0000 ) ==16) ;
assert ( tzcnt (0 xFFFF0010 )==4);
};
That works!
The problem is easy enough to be tackled MK85, although, it’s not as fast as Z3, of course: src, output.
(check -sat)
(get - model )
It is important to note that MK85 has no idea about Newton’s method of finding square/cubic/etc roots...
3.21 Exercise
As an exercise, try to encode this problem using SMT-LIB 2.0 or Z3Py API:
If a merchant buys 138 yards of cloth, some of which is black and some blue, for 540 roubles, how
many yards of each did he buy if the blue cloth cost 5 roubles a yard and the black cloth 3?
Proofs
SAT/SMT solvers can’t prove correctness of something, or if the model behaves as the author wanted.
However, it can prove equivalence of two expressions or models.
And it’s hard to say, why/where we can use it, maybe for obfuscation, I’m not sure. I would call this suboptimization
(as opposed to superoptimization). Or maybe superdeoptimization.
But my another question was also, is it possible to prove that this is correct formula at all? The Aha! checking
some input/output values against XOR operation, but of course, not all the possible values. It is 32-bit code, so it
may take very long time to try all possible 32-bit inputs to test it.
We can try Z3 theorem prover for the job. It’s called prover, after all.
So I wrote this:
#!/ usr/bin/ python
from z3 import *
In plain English language, this means “are there any case for x and y where x ⊕ y doesn’t equals to ((y&x) ∗ −2) +
(y + x)?” …and Z3 prints “unsat”, meaning, it can’t find any counterexample to the equation. So this Aha! result is
proved to be working just like XOR operation.
1 http://www.hackersdelight.org/
2 http://en.wikipedia.org/wiki/Superoptimization
111
112
Oh, I also tried to extend the formula to 64 bit:
#!/ usr/bin/ python
from z3 import *
Nope, now it says “sat”, meaning, Z3 found at least one counterexample. Oops, it’s because I forgot to extend -2
number to 64-bit value:
#!/ usr/bin/ python
from z3 import *
Now it says “unsat”, so the formula given by Aha! works for 64-bit code as well.
It returns “unsat”, meaning, Z3 couldn’t find any counterexample of the equation, i.e., it’s not exist.
Z3 also supports universal quantifier forall, which is true if the equation is true for all possible values. So we
can rewrite our SMT-LIB example as:
(declare -const x (_ BitVec 64))
(declare -const y (_ BitVec 64))
( assert
( forall ((x (_ BitVec 64)) (y (_ BitVec 64)))
(=
(bvsub
( bvadd x y)
( bvshl ( bvand x y) (_ bv1 64))
)
(bvxor x y)
)
)
)
(check -sat)
It returns “sat”, meaning, the equation is correct for all possible 64-bit x and y values, like them all were checked.
Mathematically speaking: ∀n ∈ N (x ⊕ y = (x + y − ((x&y) << 1))) 3
-2
Y
AND MUL
ADD ADD
out1
X
EQ must be true
out2
XOR
-2
Y
AND MUL
ADD ADD
out1
X
XOR OR must be 0
out2
XOR
So it has two parts: generic XOR block and a block which must be equivalent to XOR. Then we compare its
outputs using XOR and OR. If outputs of these parts are always equal to each other for all possible x and y, output
of the whole block must be 0.
I do otherwise, I’m trying to find such an input pair, for which output will be 1:
def chk1 ():
input_bits =8
if s. solve () == False :
print (" unsat ")
return
print ("sat")
print ("x=%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (x)))
print ("y=%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (y)))
print (" step1 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (step1 )))
print (" product =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution ( product )))
print (" result1 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution ( result1 )))
print (" result2 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution ( result2 )))
print (" minus_2 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution ( minus_2 )))
It’s also slow, because multiplier block is used: so we use small 8-bit x’s and y’s.
But the whole thing can be rewritten: x ⊕ y = x + y − (x&y) << 1. And subtraction is addition, but with one
negated operand. So, x ⊕ y = (−(x&y)) << 1 + (x + y) or x ⊕ y = (x&y) ∗ 2 − (x + y).
XOR out2
NEG is negation block, in two’s complement system. It just inverts all bits and adds 1:
def NEG(self , x):
# invert all bits
tmp=self. BV_NOT (x)
# add 1
one=self. alloc_BV (len(tmp))
self. fix_BV (one , n_to_BV (1, len(tmp)))
return self.adder (tmp , one)[0]
if s. solve () == False :
print (" unsat ")
return
print ("sat")
print ("x=%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (x)))
print ("y=%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (y)))
print (" step1 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (step1 )))
print (" step2 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution (step2 )))
print (" result1 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution ( result1 )))
print (" result2 =%x" % SAT_lib . BV_to_number (s. get_BV_from_solution ( result2 )))
One important thing is that we can’t operate on 64-bit values on right side, because result will overflow. So we
will zero extend inputs on right side by 1 bit (in other words, we will just 1 zero bit before each value). The result of
Dietz’s formula will also be extended by 1 bit. Hence, both sides of the equation will have a width of 65 bits:
(declare -const x (_ BitVec 64))
(declare -const y (_ BitVec 64))
( assert
( forall ((x (_ BitVec 64)) (y (_ BitVec 64)))
(=
((_ zero_extend 1)
( bvadd
( bvand x y)
( bvlshr ( bvxor x y) (_ bv1 64))
)
)
( bvlshr
( bvadd ((_ zero_extend 1) x) ((_ zero_extend 1) y))
(_ bv1 65)
)
)
)
)
(check -sat)
Z3 says “sat”.
65 bits are enough, because the result of addition of two biggest 64-bit values has width of 65 bits:
0xFF...FF + 0xFF...FF = 0x1FF...FE.
As in previous example about XOR equivalent, (not (= ... )) and exists can also be used here instead of forall.
4 http://aggregate.org/MAGIC/#Average%20of%20Integers
5 For those who interesting how it works, its mechanics is closely related to the weird XOR alternative we just saw. That’s why I placed these
Z3 gave ”unsat”, meaning, it can’t find any counterexample to the last equation (line 18). Hence, the equation is
correct and so is the whole algorithm.
; initial : X1 , Y1
;X2 := X1 XOR Y1
;Y3 := Y1 XOR X2
;X4 := X2 XOR Y3
; prove X1=Y3 and Y1=X4 for all
; needless to say that other SMT solvers may use simplification to prove this , MK85
can 't do it ,
; it " proves " on SAT level , by absence of counterexample to the expressions .
(check -sat)
; initial : X1 , Y1
;X2 := X1 ADD Y1
;Y3 := X2 SUB Y1
;X4 := X2 SUB Y3
; prove X1=Y3 and Y1=X4 for all
; needless to say that other SMT solvers may use simplification to prove this , MK85
can 't do it ,
; it " proves " on SAT level , by absence of counterexample to the expressions .
(check -sat)
x = Int('x')
y = Int('y')
print ( simplify (x + y + 2*x + 3))
print ( simplify (x < y + x + 2))
print ( simplify (And(x + 1 >= 3, x**2 + x**2 + y**2 + 2 >= 5)))
Both Mathematica and Z3 (using “simplify” command) can’t make it shorter, but I’ve got that gut feeling there is
something redundant.
Let’s take a look at the right part of the expression. If x must be less than 6 OR greater than 7, then it can hold
any value except 6 AND 7, right? So I can rewrite this manually:
if ( ( x != 7 || y!=0 ) && x != 6 && x != 7) )
{
...
};
y gets reduced.
But am I really right? And why Mathematica and Z3 didn’t simplify this at first place?
I can use Z3 to prove that these expressions are equal to each other:
#!/ usr/bin/env python3
from z3 import *
x=Int('x')
y=Int('y')
s= Solver ()
s.add(exp1 != exp2)
Z3 can’t find counterexample, so it says “unsat”, meaning, these expressions are equivalent to each other. So I’ve
rewritten this expression in my code, tests has been passed, etc.
Yes, using both Mathematica and Z3 is overkill, and this is basic boolean algebra, but after ≈ 10 hours of sitting
at a front of computer you can make really dumb mistakes, and additional proof that your piece of code is correct is
never unwanted.
6 https://reference.wolfram.com/language/ref/BooleanMinimize.html
7 https://beginners.re/: Ctrl-f: “Hex-Rays”
120
4.7 Bit reverse function
This is quite popular function. Unfortunately, such a hackish code is error-prone, an unnoticed typo can easily creep
in.
# define __constant_bitrev32 (x) \
({ \
u32 ___x = x; \
___x = (___x >> 16) | (___x << 16); \
___x = (( ___x & (u32)0 xFF00FF00UL ) >> 8) | (( ___x & (u32)0 x00FF00FFUL ) << 8);
\
___x = (( ___x & (u32)0 xF0F0F0F0UL ) >> 4) | (( ___x & (u32)0 x0F0F0F0FUL ) << 4);
\
___x = (( ___x & (u32)0 xCCCCCCCCUL ) >> 2) | (( ___x & (u32)0 x33333333UL ) << 2);
\
___x = (( ___x & (u32)0 xAAAAAAAAUL ) >> 1) | (( ___x & (u32)0 x55555555UL ) << 1);
\
___x; \
})
( https://github.com/torvalds/linux/blob/master/include/linux/bitrev.h )
While you can check all possible 32-bit values in brute-force manner, this is infeasible for 64-bit function(s).
As before, I’m not proving here the function is ”correct” in some sense, but I’m proving equivalence of two functions:
bitrev64() and bitrev64_unoptimized(), which uses bitrev32(), which in turn uses bitrev16(), etc...
#!/ usr/bin/ python3
from z3 import *
# from Henry Warren 's " Hacker 's Delight ", Chapter 7
# Or: https :// github .com/ torvalds /linux /blob/ master / include / linux / bitrev .h
# default right shift in Z3 is arithmetical , so I'm using Z3 's LShR () function here ,
which is logical shift right
# these " unoptimized " versions are constructed like a Russian doll ...
# copypasted from CADO -NFS 2.3.0 , http :// cado -nfs. gforge . inria .fr/ download .html
def bitrev64 (a):
a = LShR(a, 32) ^ (a << 32)
m = 0 x0000ffff0000ffff
a = (LShR(a, 16) & m) ^ ((a << 16) & ∼m)
m = 0 x00ff00ff00ff00ff
a = (LShR(a, 8) & m) ^ ((a << 8) & ∼m)
m = 0 x0f0f0f0f0f0f0f0f
a = (LShR(a, 4) & m) ^ ((a << 4) & ∼m)
m = 0 x3333333333333333
a = (LShR(a, 2) & m) ^ ((a << 2) & ∼m)
m = 0 x5555555555555555
a = (LShR(a, 1) & m) ^ ((a << 1) & ∼m)
return a
s= Solver ()
# tests .
# uncomment any.
# must be "unsat " in each case.
The problem is easy enough to be solved using my toy MK85 bitblaster, with only tiny modifications:
#!/ usr/bin/ python3
# MK85 uses logical shift right for Python operator >>, so here is it as is ...
# tests .
# check this:
#s.add( bitrev64_unoptimized (x)!= bitrev64 (x))
# or this:
s.add( bitrev64 (x)==y)
s.add( bitrev64 (y)!=x)
# must be False :
print (s. check ())
This is combinatorial circuit, each connection is a comparator+swapper, it swaps if one of input values is bigger
and passes output to the next level.
I copypasted it from the article: Michael Codish, Lu ́ıs Cruz-Filipe, Michael Frank, and Peter Schneider-Kamp –
“Twenty-Five Comparators is Optimal when Sorting Nine Inputs (and Twenty-Nine for Ten)”.
Another article about it: Ian Parberry – A Computer Assisted Optimal Depth Lower Bound for Nine-Input Sorting
Networks.
I don’t know (yet) how they proved it, but it’s interesting, that it’s extremely easy to prove its correctness using
Z3 SMT solver. We just construct network out of comparators/swappers and asking Z3 to find counterexample, for
which the output of the network will not be sorted. And it can’t, meaning, output’s state is always sorted, no matter
what values are plugged into inputs.
#!/ usr/bin/env python3
from z3 import *
a, b, c, d, e, f, g, h, i=Ints('a b c d e f g h i')
l=[i, h, g, f, e, d, c, b, a]
l=line(l, " ++++++++ ")
l=line(l, " + + + + ")
l=line(l, " + + ")
l=line(l, " + + ")
l=line(l, "+ + ")
l=line(l, " + + + +")
l=line(l, " + +")
l=line(l, " + + ")
l=line(l, " + + ")
l=line(l, " + +++ ")
l=line(l, "+ + ")
l=line(l, "+ + + + ")
l=line(l, "+ + ")
l=line(l, " + + ")
l=line(l, " ++++++ ++")
s= Solver ()
The first and the last are shorter than the 2nd and the 3rd, they are just min(min(min(a, b), c), d) and max(max(max(a, b), c),
Another example in this book related to sorting networks: cracking minesweeper with it (3.10.5).
!(a || b) ? h : !(a == b) ? f : g
You can assume that the variables have only one bit.
( assert (=( ite (not (or a b)) h (ite (not (= a b)) f g)) out1))
( assert (=( ite (not (or (not a) (not b))) g (ite (and (not a) (not b)) h f)) out2))
; find counterexample :
( assert ( distinct out1 out2))
; must be unsat :
(check -sat)
; unsigned version
(set - logic QF_BV )
(set -info :smt -lib - version 2.0)
; find any set of variables for which min1 != min2 or max1 != max2
( assert (or
(not (= min1 min2))
(not (= max1 max2))
))
; signed version
(set - logic QF_BV )
(set -info :smt -lib - version 2.0)
; find any set of variables for which min1 != min2 or max1 != max2
( assert (or
(not (= min1 min2))
(not (= max1 max2))
))
4.12 Proving “Determine if a word has a zero byte” bit twiddling hack
... which is:
# define haszero (v) (((v) - 0 x01010101UL ) & ∼(v) & 0 x80808080UL )
( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord )
The expression returns zero if there are no zero bytes in 32-bit word, or non-zero, if at least one is present. Here
we prove that it’s correct for all possible 32-bit words.
; checked with Z3 and MK85
( assert (= HasZeroByte
(or
128
(= ( bvand v # xff000000 ) # x00000000 )
(= ( bvand v # x00ff0000 ) # x00000000 )
(= ( bvand v # x0000ff00 ) # x00000000 )
(= ( bvand v # x000000ff ) # x00000000 )
)
)
)
; out ==0
( assert (= out # x00000000 ))
struct SMT_var * gen_shifter_real ( struct ctx* ctx , struct SMT_var * X, struct SMT_var *
cnt , bool direction , bool arith )
{
int w=X->width ;
// bit vector must have width =2^x, i.e., 8, 16, 32, 64, etc
// FIXME better func name:
assert ( popcount64c (w)==1);
in=out;
};
// if any bit is set in high part of "cnt" variable , result is 0
// i.e., if a 8-bit bitvector is shifted by cnt >8, give a zero
struct SMT_var * disable_shifter = create_internal_variable (ctx , "...", TY_BOOL ,
1);
add_Tseitin_OR_list (ctx , cnt -> SAT_var + bits_in_selector , w- bits_in_selector ,
disable_shifter -> SAT_var );
// 0x80 >>s cnt , where cnt >8, must be 0xff! so fill result with MSB(input )
struct SMT_var * default_val ;
if ( arith == false )
default_val = gen_const (ctx , 0, w);
else
default_val = gen_repeat_from_SAT_var (ctx , MSB_of_SMT_var (X), 1, w);
struct SMT_var * gen_shifter ( struct ctx* ctx , struct SMT_var * X, struct SMT_var * cnt ,
bool direction , bool arith )
{
int w=X->width ;
if ( popcount64c (w)!=1)
{
// if X is not in 2^n form
rt= gen_extract (ctx , rt , 0, w);
};
return rt;
};
struct SMT_var * gen_BVASHR ( struct ctx* ctx , struct SMT_var * X, struct SMT_var * cnt)
{
return gen_shifter (ctx , X, cnt , true , true);
};
( https://smt.st/MK85 )
In other words, we prove equivalence of the expression above and my implementation.
; tested with MK85 and Z3
; must be unsat :
(check -sat)
I can emulate real numbers using fixed point arithmetic over bitvectors. In a 16-bit variable, high 8-bit part will
be integer part and low 8-bit part is fractional part. This is also called Q8.88 .
floor() function is simple — just zero fractional part (low 8 bits). ceiling() function — if something is present in
fractional part (low 8 bits), increment high 8 bits.
from z3 import *
# Find numbers x and y such that floor (x + y) != floor (x) + floor (y) and
# ceiling (x + y) != ceiling (x) + ceiling (y).
s= Solver ()
print s. check ()
m=s. model ()
print "x=0x%04x or %f" % (m[x]. as_long () , float (m[x]. as_long ())/0 x100)
print "y=0x%04x or %f" % (m[y]. as_long () , float (m[y]. as_long ())/0 x100)
8 https://en.wikipedia.org/wiki/Fixed-point_arithmetic
131
Listing 4.2: Output
sat
x=0 x03e0 or 3.875000
y=0 x3f20 or 63.125000
In []:= y = 63.125;
s= Solver ()
print s. check ()
m=s. model ()
print "x=0x%04x or %f" % (m[x]. as_long () , float (m[x]. as_long ())/0 x100)
"""
9 If and only if: ⇐⇒
132
Prove each of the following statements about inequalities with the floor and ceiling ,
where x is a real number and n is an integer .
a. floor (x) < n iff x < n.
b. n < ceiling (x) iff n < x.
c. n <= floor (x) iff n <= x.
d. floor (x) <= n iff x <= n.
"""
s= Solver ()
"""
Prove each statement , where n is an integer .
a. ceiling (n/2) = floor ((n + 1) /2)
b. floor (n/2) = ceiling ((n - 1) /2)
"""
s= Solver ()
return false ;
}
return false ;
}
134
You see, the problem with such hackish solution is that they are prone to bugs. A small unnoticed (for a long
period of time) typo can ruin everything.
4.15.1 CBMC
I am adding this function:
void check ( unsigned int c)
{
assert ( isXMLDigit_optimized (c) == isXMLDigit (c));
};
And asking CBMC to find such an input so that assert() would stop:
$ cbmc --trace --function check isXMLdigit .c
...
** Results :
[check. assertion .1] assertion isXMLDigit_optimized (c) == isXMLDigit (c): SUCCESS
** 0 of 1 failed (1 iteration )
VERIFICATION SUCCESSFUL
Violated property :
file isXMLdigit_bug .c line 66 function check
assertion isXMLDigit_optimized (c) == isXMLDigit (c)
return_value_isXMLDigit_optimized == return_value_isXMLDigit
4.15.2 Z3 SMT-solver
But I also wanted to know if I can convert all this into propositional logic form and check equivalence using SMT
solver.
I would add two types of boolean variables. ”c”-variables for conditions. ”p”-variables are like ”points”. Each ”point”
is true if execution flow reaches this point for the corresponding input.
bool isXMLDigit ( unsigned int c)
{
/* p1.1 */ if (/* c1 */ c >=0 x0030 && c <=0 x0039 ) /* p1 .2 */ return true;/* p1.3 */
135
/* p2.1 */ if (/* c2 */ c >=0 x0660 && c <=0 x0669 ) /* p2 .2 */ return true;/* p2.3 */
...
/* p15 .1*/ if (/* c15 */c >=0 x0F20 && c <=0 x0F29 ) /* p15 .2 */ return true;/* p15 .3*/
/* p16 */
return false ;
}
p1.1 is always true (we get there for any input). p1.2 is true if p1.1 is true AND c1 condition is true. p1.3 is true if
p1.1 is true AND p1.2 is false (as if no ”return true” has been executed). On the next line, p2.1 is a synonym for p1.3.
The function returns false IFF p16==true. The function returns true IFF (p1.2 OR p2.2 OR ... OR p15.2).
Here is how I model this using SMT-LIB 2.0 LISPy language:
; /*p1 .1*/ if (/* c1 */ c >=0 x0030 && c <=0 x0039 ) /* p1 .2*/ return true ;/* p1 .3*/
; /*p2 .1*/ if (/* c2 */ c >=0 x0660 && c <=0 x0669 ) /* p2 .2*/ return true ;/* p2 .3*/
...
Yes, Z3Py could be used, but I chose not to contaminate it by Python syntax yet, to get a clearer representation
of propositional logic equation.
The optimized version of the function is a bit more complex:
bool isXMLDigit_optimized ( unsigned int c)
{
/* p1.1 */ if (/* c1 .1 */ c <0 x0030 ) /* p1 .2*/ return false ; /* p1 .3 */ if (/* c1.2 */ c <=0
x0039 ) /* p1 .4 */ return true;/*p1 .5 */
/* p2.1 */ if (/* c2 .1 */ c <0 x0660 ) /* p2 .2*/ return false ; /* p2 .3 */ if (/* c2.2 */ c <=0
x0669 ) /* p2 .4 */ return true;/*p2 .5 */
...
/* p15 .1*/ if (/* c15 .1 */c <0 x0F20 ) /* p15 .2 */ return false ; /* p15 .3 */ if (/* c15 .2 */c <=0
x0F29 ) /* p15 .4 */ return true;/* p15 .5 */
/* p16 */
return false ;
}
; /*p2 .1*/ if (/* c2 .1*/ c <0 x0660 ) /* p2 .2*/ return false ; /* p2 .3*/ if (/* c2 .2*/ c <=0
x0669 ) /* p2 .4*/ return true ;/* p2 .5*/
And at the very end, I’m asking for such an input for both function, for which their outputs would differ:
( assert (= f1_c f2_c))
( assert (or
(not (= f1_returns_false f2_returns_false ))
(not (= f1_returns_true f2_returns_true ))))
No, Z3, CVC4 and Boolector can’t find such an input, giving unsat. The problem is small enough to be tackled
by my toy-level MK85 bitblaster. Alter any constant, and SMT solver would find such an input.
Perhaps, this is what CBMC doing internally, if I understand all the things correctly.
Verification
...
};
Seems innocent? However, if a (remote) attacker can put a negative value into num, no exception is to be thrown,
and malloc() will crash on too big input value, because malloc() takes unsigned size_t on input. unsigned int
should be used instead of int for num, but many programmers use int as a generic type for everything.
In other words, you want your expression to be evaluated on both ALUs correctly, for all possible inputs, right?
Like if the result of 32-bit ALU is always fit into 32-bit register.
And now we can ask Z3 SMT solver to find such an a/b inputs, for which the final comparison will fail.
Needless to say, the default operations (+, -, comparisons, etc) in Z3’s Python API are signed, you can see this
here1 .
Also, we can find the lower bound, or minimal possible inputs, using minimize():
#!/ usr/bin/env python3
from z3 import *
1 https://github.com/Z3Prover/z3/blob/master/src/api/python/z3/z3.py
137
138
def func(a,b):
return a+b
s= Optimize ()
s. minimize (a32)
s. minimize (b32)
# from https :// stackoverflow .com/ questions /1375897/ how -to -get -the -signed -integer -
value -of -a-long -in - python
def toSigned32 (n):
n = n & 0 xffffffff
return n | (-(n & 0 x80000000 ))
print ("a32 =0x%x or %d" % (m[a32 ]. as_long () , toSigned32 (m[a32 ]. as_long ())))
print ("b32 =0x%x or %d" % (m[b32 ]. as_long () , toSigned32 (m[b32 ]. as_long ())))
print (" out32 =0x%x or %d" % (m[out32 ]. as_long () , toSigned32 (m[out32 ]. as_long ())))
print (" out32_extended =0x%x or %d" % (m[ out32_extended ]. as_long () , toSigned64 (m[
out32_extended ]. as_long ())))
print ("a64 =0x%x or %d" % (m[a64 ]. as_long () , toSigned64 (m[a64 ]. as_long ())))
print ("b64 =0x%x or %d" % (m[b64 ]. as_long () , toSigned64 (m[b64 ]. as_long ())))
print (" out64 =0x%x or %d" % (m[out64 ]. as_long () , toSigned64 (m[out64 ]. as_long ())))
a32 =0x1 or 1
b32 =0 x7fffffff or 2147483647
out32 =0 x80000000 or -2147483648
out32_extended =0 xffffffff80000000 or -2147483648
a64 =0x1 or 1
b64 =0 x7fffffff or 2147483647
out64 =0 x80000000 or 2147483648
Right, 1+0x7fffffff = 0x80000000. But the 0x80000000 value is negative already, because MSB2 is 1. However,
add this on 64-bit ALU and the result will fit in 64-bit register.
How would we fix this problem? We can devise a special function with wrapped addition:
/* Returns : a + b */
COMPILER_RT_ABI si_int
__addvsi3 ( si_int a, si_int b)
{
si_int s = ( su_int ) a + ( su_int ) b;
if (b >= 0)
{
if (s < a)
compilerrt_abort ();
}
else
{
if (s >= a)
compilerrt_abort ();
}
return s;
}
( https://github.com/llvm-mirror/compiler-rt/blob/master/lib/builtins/addvsi3.c )
Now I can simulate this function using Z3Py. I’m telling it: “find a solution, where this expression will be false”:
s.add(Not(If(b32 >=0 , a32+b32 <a32 , a32+b32 >a32)))
And it gives unsat, meaning, there is no counterexample, so the expression can be evaluated safely on both ALUs.
But is there a bug in my statement? Let’s check. Find inputs for which this piece of LLVM code will call
compilerrt_abort():
s.add(If(b32 >=0, a32+b32 <a32 , a32+b32 >a32))
a32 =0x1 or 1
b32 =0 x7fffffff or 2147483647
out32 =0 x80000000 or -2147483648
out32_extended =0 xffffffff80000000 or -2147483648
a64 =0x1 or 1
b64 =0 x7fffffff or 2147483647
out64 =0 x80000000 or 2147483648
a32 =0x1 or 1
b32 =0 x7fffffff or 2147483647
out32 =0 xc0000000 or -1073741824
out32_extended =0 xffffffffc0000000 or -1073741824
a64 =0x1 or 1
b64 =0 x7fffffff or 2147483647
out64 =0 x40000000 or 1073741824
We can fix this function using a seemingly esoteric Dietz formula, used to do the same, but without integer overflow:
def func(a,b):
return ((a^b) >>1) + (a&b)
3 https://ai.googleblog.com/2006/06/extra-extra-read-all-about-it-nearly.html, https://thebittheories.com/
the-curious-case-of-binary-search-the-famous-bug-that-remained-undetected-for-20-years-973e89fc212, also, see the “Beau-
tiful Code” book (2007), Chapter 7, That Pesky Binary Search.
140
( Its internal workings is described here: 4.3 )
Z3 gives unsat for this function, because it can’t find counterexample.
def func(a):
return a *1024
#s= Solver ()
s= Optimize ()
s.add(out32 == func(a32))
s.add(out64 == func(a64))
s. minimize (a32)
# from https :// stackoverflow .com/ questions /1375897/ how -to -get -the -signed -integer -
value -of -a-long -in - python
def toSigned32 (n):
n = n & 0 xffffffff
return n | (-(n & 0 x80000000 ))
print ("a32 =0x%x or %d" % (m[a32 ]. as_long () , toSigned32 (m[a32 ]. as_long ())))
print (" out32 =0x%x or %d" % (m[out32 ]. as_long () , toSigned32 (m[out32 ]. as_long ())))
print (" out32_extended =0x%x or %d" % (m[ out32_extended ]. as_long () , toSigned64 (m[
out32_extended ]. as_long ())))
print ("a64 =0x%x or %d" % (m[a64 ]. as_long () , toSigned64 (m[a64 ]. as_long ())))
print (" out64 =0x%x or %d" % (m[out64 ]. as_long () , toSigned64 (m[out64 ]. as_long ())))
For which a values will fail the a*1024 expression? This is a smallest a input:
a32 =0 x200000 or 2097152
out32 =0 x80000000 or -2147483648
out32_extended =0 xffffffff80000000 or -2147483648
a64 =0 x200000 or 2097152
out64 =0 x80000000 or 2147483648
s.add(a32 <100)
Still, an attacker can pass a negative a = −2147483648, and malloc() will fail.
Let’s pretend, we added a assert (a>0) before calling malloc():
s.add(a32 >0)
5.1.4 abs()
Also seemingly innocent function:
def func(a):
return If(a<0, -a, a)
This is an artifact of two’s complement system. This is INT_MIN, and -INT_MIN == INT_MIN. It can lead to nasty
bugs, and classic one is a naive implementations of itoa() or printf().
Take a look at this implementation of itoa() function from [Brian W. Kernighan, Dennis M. Ritchie, The C
Programming Language, 2ed, (1988)]:
void itoa(int n, char s[])
{
int i, sign;
if (( sign = n) < 0) /* record sign */
n = -n; /* make n positive */
i = 0;
do { /* generate digits in reverse order */
s[i++] = n % 10 + '0'; /* get next digit */
} while ((n /= 10) > 0); /* delete it */
if (sign < 0)
s[i++] = '-';
s[i] = '\0';
strrev (s);
}
Exercise 3-4. In a two’s complement number representation, our version of itoa does not handle
the largest negative number, that is, the value of n equal to −(2wordsize−1 ). Explain why not. Modify
it to print that value correctly, regardless of the machine on which it runs.
The correct answer is: the function cannot process largest negative number (INT_MIN or 0x80000000 or -2147483648)
correctly.
So, suppose, you print a signed value. And you write something like:
142
if (input <0)
{
input =- input ;
printf ("-"); // print leading minus
};
If a INT_MIN value (0x80000000) is passed, minus sign is printed, but the input variable still contain negative
value. An additional check for INT_MIN is to be added to fix this.
This is also called undefined behaviour in C/C++. The problem is that C language itself is old enough to be a witness
of old iron – computers which could represent signed numbers in other ways than two’s complement representation:
ttps://en.wikipedia.org/wiki/Signed_number_representations.
For this reason, C standard doesn’t guarantee that −1 will be 0xffffffff (all bits set) on 32-bit registers, because
the standard can’t guarantee you will run on a hardware with two’s complement representation of signed numbers.
However, almost all hardware you can currently use and buy uses two’s complement.
More about the abs() problem:
This can become a security issue . I have seen one instance in the vasprintf
implementation of libiberty ,
which is part of gcc , binutils and some other GNU software . vasprintf walks over the
format string and
tries to estimate how much space it will need to hold the formatted result string . In
format strings ,
there is a construct %.*s or %*s, which means that the actual value should be taken
from the stack .
The libiberty code did it like this:
if (*p == '*')
{
++p;
total_width += abs ( va_arg (ap , int));
}
This is actually two issues in one. The first issue is that total_width can overflow .
The second issue
is the one that is interesting in this context : abs can return a negative number ,
causing the code
to allocate not enough space and thus cause a buffer overflow .
( http://www.fefe.de/intof.html )
5.1.5 Games
A lot of video games are prone to integer overflow. Which are exploited actively by gamers. As of NetHack: https:
//nethackwiki.com/wiki/Integer_overflow, https://nethackwiki.com/wiki/Negative_gold.
5.1.7 Summary
What we did here, is we checked, if a result of an expression can fit in 32-bit register. Probably, you can use a narrower
second ALU, than a 64-bit one.
5.2.1 CBMC
So far, CBMC can prove this easily:
// time cbmc --trace --function check 1.c
These functions maybe quite naive, but I’ve been interested how CBMC handles arrays:
int popcount64_table ( uint64_t x)
{
uint64_t tbl [16]={0 ,1 ,1 ,2 ,1 ,2 ,2 ,3 ,1 ,2 ,2 ,3 ,2 ,3 ,3 ,4};
uint64_t rt =0;
return rt;
}
uint64_t rt =0;
return rt;
}
Things gets a bit harder with a function copypasted from a well-known HAKMEM. It takes 160 seconds to get
job done, despite the somewhat hard (for SAT/SMT solvers) division/remainder function with the odd divisor (63):
int hakmem169_32 ( uint32_t x)
{
x = x - ((x >> 1) & 033333333333)
145
- ((x >> 2) & 011111111111) ;
x = (x + (x >> 3)) & 030707070707 ;
return x % 63; /* casting out 63 */
}
...
Unwinding loop popcount64d .0 iteration 1584 file 1.c line 50 function popcount64d
thread 0
Unwinding loop popcount64d .0 iteration 1585 file 1.c line 50 function popcount64d
thread 0
Unwinding loop popcount64d .0 iteration 1586 file 1.c line 50 function popcount64d
thread 0
Unwinding loop popcount64d .0 iteration 1587 file 1.c line 50 function popcount64d
thread 0
146
Unwinding loop popcount64d .0 iteration 1588 file 1.c line 50 function popcount64d
thread 0
...
etc
...
...
147
if (x) {x &= x -1; count ++;}; if (x) {x &= x -1; count ++;}; if (x) {x &= x -1;
count ++;}; if (x) {x &= x -1; count ++;};
if (x) {x &= x -1; count ++;}; if (x) {x &= x -1; count ++;}; if (x) {x &= x -1;
count ++;}; if (x) {x &= x -1; count ++;};
Now it takes only 16 minutes. Please note the assert() upon the function exit. Yes, ”x” must be ”zeroed” upon the
exit for obvious reasons. However, CBMC would prove that the assert() will never throw.
What if I comment one ”if (x)...” line with 4 if’s?
SAT checker : instance is SATISFIABLE
Runtime decision procedure : 73.0928 s
** Results :
[ popcount64d_unrolled . assertion .1] assertion x==0: FAILURE
[check. assertion .1] assertion popcount64d_unrolled (c)== popcount64a (c): FAILURE
...
You see – the ”x” variable is almost ”filled” with 1’s. Yes, our modified function (with 4 of if’s removed) will run
incorrectly when ”x” has 0-3 zero bits.
Also, SAT solver gave ”SATISFIABLE”, meaning, it found a counterexample. Otherwise, it would say ”UNSATIS-
FIABLE”.
( https://www.cprover.org/SMT-LIB-LSM/ )
cbmc --smt2 --outfile dest.smt --trace --function check 1.c
148
You can get an idea what CBMC passes to a SMT solver in each case:
; SMT 2
(set -info : source " Generated by CBMC 5.10 (cbmc -5.10) ")
(set - option :produce - models true)
(set - logic QF_AUFBV )
; find_symbols
(declare -fun | nondet_symex :: nondet0 | () (_ BitVec 64))
; set_to true ( equal )
(define -fun | __CPROVER__start ::c !0#2| () (_ BitVec 64) | nondet_symex :: nondet0 |)
; find_symbols
(declare -fun | __CPROVER__start ::c !0#1| () (_ BitVec 64))
; convert
(define -fun |B0| () Bool (= | __CPROVER__start ::c!0#1| | __CPROVER__start ::c !0#1|) )
; find_symbols
(declare -fun |check :: $tmp :: return_value_popcount64a !0 @1 #1| () (_ BitVec 32))
; convert
(define -fun |B1| () Bool (= | check :: $tmp :: return_value_popcount64a !0 @1 #1| | check ::
$tmp :: return_value_popcount64a !0 @1 #1|))
; find_symbols
(declare -fun |check :: $tmp :: return_value_popcount64b !0 @1 #1| () (_ BitVec 32))
; convert
(define -fun |B2| () Bool (= | check :: $tmp :: return_value_popcount64b !0 @1 #1| | check ::
$tmp :: return_value_popcount64b !0 @1 #1|))
; set_to false
( assert (not (= | check :: $tmp :: return_value_popcount64a !0 @1 #2| | check :: $tmp ::
return_value_popcount64b !0 @1 #2|)))
; find_symbols
(declare -fun |symex ::io ::0| () (_ BitVec 64))
; set_to true
( assert (= | __CPROVER__start ::c!0#2| | symex :: io ::0|) )
(check -sat)
(exit)
; end of SMT2 file
( assert (= a_x1 ( bvadd ( bvand a_x m1) ( bvand ( bvlshr a_x (_ bv1 64)) m1))))
( assert (= a_x2 ( bvadd ( bvand a_x1 m2) (bvand ( bvlshr a_x1 (_ bv2 64)) m2))))
( assert (= a_x3 ( bvadd ( bvand a_x2 m4) (bvand ( bvlshr a_x2 (_ bv4 64)) m4))))
( assert (= a_x4 ( bvadd ( bvand a_x3 m8) (bvand ( bvlshr a_x3 (_ bv8 64)) m8))))
( assert (= a_x5 ( bvadd ( bvand a_x4 m16) ( bvand ( bvlshr a_x4 (_ bv16 64)) m16))))
( assert (= a_out ( bvadd ( bvand a_x5 m32) ( bvand ( bvlshr a_x5 (_ bv32 64)) m32))))
( assert (= naive_out
( bvadd
( bvand ( bvlshr naive_x (_ bv0 64)) (_ bv1 64))
( bvand ( bvlshr naive_x (_ bv1 64)) (_ bv1 64))
...
...
...
...
(ite (= kern_x56 c0) c0 c1) (ite (= kern_x57 c0) c0 c1) (ite (= kern_x58 c0)
c0 c1) (ite (= kern_x59 c0) c0 c1)
(ite (= kern_x60 c0) c0 c1) (ite (= kern_x61 c0) c0 c1) (ite (= kern_x62 c0)
c0 c1) (ite (= kern_x63 c0) c0 c1)
153
)))
...
Again, we can prove here that the last state of ”x” would always be zero.
Maybe I encoded the problem in a wrong way, but this time, both CVC4 and Z3 stuck and couldn’t solve anything
withing 15 minutes timeout. However, Boolector can prove it for 1 minute.
Of course, I could misunderstood something.
SMT arrays
Now what about array encoding?
;int popcount64_table ( uint64_t x)
;{
; uint64_t tbl [16]={0 ,1 ,1 ,2 ,1 ,2 ,2 ,3 ,1 ,2 ,2 ,3 ,2 ,3 ,3 ,4};
; uint64_t rt =0;
; rt=rt+tbl [(x > >(0*4))&0 xf];
; rt=rt+tbl [(x > >(1*4))&0 xf];
;...
Here we use ”store” function to populate array. At each line, tbl is reassigned. We then use ”select” function to
address elements of the array.
Arrays are implemented in SMT solvers using UF, but logically, you can think about them as about chains of ITE’s
(if-then-else). Or like a switch() construct found in many popular PLs.
Even more, Boolector during model generation (”get-model”), shows arrays as ITE chains:
(define -fun f (
(f_x0 (_ BitVec 64))) (_ BitVec 64)
(ite (= f_x0 # b0000000000000000000000000000000000000000000000000000000000000000 )
# b0000000000000000000000000000000000000000000000000000000000000000
(ite (= f_x0 # b0000000000000000000000000000000000000000000000000000000000000001 )
# b0000000000000000000000000000000000000000000000000000000000000001
...
(We see here that the default value is 0, it’s like ”default” in ”switch()” statement.)
As well as Z3:
(define -fun f ((x!0 (_ BitVec 64))) (_ BitVec 64)
(ite (= x!0 # x0000000000000003 ) # x0000000000000002
(ite (= x!0 # x0000000000000004 ) # x0000000000000001
...
(ite (= x!0 # x0000000000000002 ) # x0000000000000001
(ite (= x!0 # x0000000000000009 ) # x0000000000000002
# x0000000000000000 ))))))))))))))))
You can think about ”select” function as if it simply evaluates internal ITE chain. And ”store” function simply
prepends new ITE in front of an array (or ITE chain) returning a new array (or chain).
Anyway, this version Boolector can solve for 15 seconds, Z3 for 7 minutes and CVC4 is stuck.
Uninterpreted functions
Since my array is a constant one, I can try to implement it using UF.
Here f() acts like the ”select” function. And it’s populated like: assert f(0) is 0, f(1) is 1 ... f(15) is (4)
(declare -fun f ((_ BitVec 64)) (_ BitVec 64))
( assert (= (f (_ bv0 64)) (_ bv0 64)))
( assert (= (f (_ bv1 64)) (_ bv1 64)))
...
( assert (= (f (_ bv14 64)) (_ bv3 64)))
( assert (= (f (_ bv15 64)) (_ bv4 64)))
Boolector and Z3 can solve this for 10-15 seconds, CVC4 is stuck.
You see, there are two (cache-)memory accesses per one character of the input string (at average). Hence, the total
number of all (cache-)memory accesses can be len*2 at worst.
Is it possible to reduce that number? Yes. In the following example, we have only one single memory access per
character:
unsigned search_ok_2 (char *s, unsigned len)
{
if (len <2)
return len; // not found
bool seen_o =false ;
for ( unsigned i=0; i<len; i++)
{
char ch=s[i]; // this is single read operation
if (ch =='o')
seen_o =true;
else if ( seen_o && ch == 'k')
return i -1; // found
else
seen_o =false ; // reset
};
return len; // not found
};
...
void check ()
{
unsigned len=LEN;
char s[len ];
__CPROVER_assert ( search_eel_brute (s, len)== search_eel (s, len), " assert ");
};
** Results :
[check. assertion .1] assert : FAILURE
...
It failed for the string ”eeel”. After some thinking, we can find a problem. If a third character isn’t ’l’, but ’e’, we
are in the middle of a long string of ’e’ characters. So if seen==2 and the input character isn’t ’l’, but ’e’, we shouldn’t
advance the ’seen’ variable:
...
if (seen ==0 && ch =='e')
seen =1;
else if (seen ==1 && ch== 'e')
seen =2;
else if (seen ==2 && ch== 'l')
return i -2; // found
else if (seen ==2 && ch== 'e') // fix
seen =2; // fix
else
seen =0; // reset
...
void check ()
{
unsigned len=LEN;
char s[len ];
__CPROVER_assert ( search_cocos_brute (s, len)== search_cocos_naive (s, len), "
assert ");
};
...
...
CBMC can verify this function for all 6-character strings, OK. But it can find problematic 7-character string:
% cbmc --trace --function check -DLEN =7 kmp_cocos .c
...
** Results :
[check. assertion .1] assert : FAILURE
...
State 21 file kmp_cocos .c line 79 function check thread 0
----------------------------------------------------
s={ 'c', 'o', 'c', 'o', 'c', 'o', 's' } ({ 01100011 , 01101111 , 01100011 , 01101111 ,
01100011 , 01101111 , 01110011 })
...
Now all 7-characters strings can be tested without a fail. But not 8-character ones:
% cbmc --trace --function check -DLEN =8 kmp_cocos .c
...
** Results :
[check. assertion .1] assert : FAILURE
...
...
It fails with ”coccocos”. We have to add another check for the repeating second ’c’ character.
...
else if (seen ==3 && ch== 'o')
seen =4;
else if (seen ==3 && ch!= 'o')
{
// if input =' coccocos '
if (ch =='c')
seen =1;
else
seen =0; // reset
}
else if (seen ==4 && ch== 's')
...
Now CBMC can check it all up to 15-character strings. The whole fixed function is:
unsigned search_cocos_fixed (char *s, unsigned len)
{
if (len <5)
return len; // not found
unsigned seen =0;
for ( unsigned i=0; i<len; i++)
{
char ch=s[i]; // this is single read operation
if (seen ==0 && ch =='c')
seen =1;
else if (seen ==1 && ch== 'o')
160
seen =2;
else if (seen ==1 && ch!= 'o')
{
// we can be here if the input is 'ccocos '
if (ch =='c')
seen =1;
else
seen =0;
}
else if (seen ==2 && ch== 'c')
seen =3;
else if (seen ==3 && ch== 'o')
seen =4;
else if (seen ==3 && ch!= 'o')
{
// if input =' coccocos '
if (ch =='c')
seen =1;
else
seen =0; // reset
}
else if (seen ==4 && ch== 's')
return i -4; // found
else if (seen ==4 && ch!= 's')
{
// the input string is 'cocoX ' where X is not 's'
// ( current state of ch='X ')
// but 'X' could be 'c' if the input string is 'cococos '
if (ch =='c')
{
// if the string is 'cococos ',
// we can say that we have already seen the 'coc '
part of it:
seen =3;
}
else
{
// 'X' is not 'c', so reset
seen =0;
};
}
else
seen =0; // reset
}
return len; // not found
};
It is capable of searching for the ’cocos’ substring reading each character of the input string only once, and it is
formally verified by CBMC.
Code like that is very hard to test (can you execute these functions with all 15-characters input strings?), but
thanks to CBMC, we can be sure it’s correct, or at least, equivalent to the simple ’bruteforce’ version. I couldn’t
devise a correct version without it. In fact first versions were written in Python. I rewritten it to pure C so that I can
verify them using CBMC.
def KMP(pat):
R=256
m=len(pat)
return dfa
# https :// stackoverflow .com/ questions /3525953/ check -if -all -values -of -iterable -are -
zero
def all_elements_in_list_are_zeros (l):
return all(v==0 for v in l)
The ’cocos’:
Do you see any similarities with the C code from the previous part 5.3.1?
Yes, what we actually did, was FA, and the ’seen’ variable was used as a marker of current state. Here we use the
’j’ variable instead, but it’s almost the same! We reinvented DFA.
The KMP algorithm creates a DFA for each substring to be searched. This is preprocessing.
The DFA is then executes on the input string, in the very same fashion like regular expression matcher. (For a
DFA used instead of RE10 matcher, see also another example in this book: 6.2. And also: 15.3.2.)
if ( needle_size ==0)
return haystack ;
free(T);
return result ;
}
5.3.4 A discussion at HN
https://news.ycombinator.com/item?id=25856558
5.4.1 Version 1
If a substring would be compared in reverse order, things may be different. For example, we search for a ’CAT’
substring in ’HORSE TURTLE CAT’ string.
With naive algorithm, we will first compare ’H’ and ’C’, we see they are unequal characters and we will advance
substring one character ahead:
HORSE TURTLE CAT
CAT
^
Right, but what if we will start at the end of ’CAT’ substring proceeding back? We first compare ’R’ with ’T’
(last character of ’CAT’) and we see that they are not equal. We could advance substring one character ahead, but...
Can’t we see that ’R’ character in the middle of ’HORSE’ part is absent in the ’CAT’ substring? Can’t this tell us
something useful? Yes – since ’R’ is not in ’CAT’, we can advance ’CAT’ substring more, because we know that this
will fail for sure:
HORSE TURTLE CAT
CAT
^
This too:
HORSE TURTLE CAT
CAT
^
But this will fail too, also, the space (’ ’) character is absent in ’CAT’ too. OK, we advance 3 characters ahead:
HORSE TURTLE CAT
CAT
^
This is important. ’A’ != ’T’, but ’A’ is in the ’CAT’ substring, so this time we advance only 1 character ahead,
as in naive search algorithm:
HORSE TURTLE CAT
CAT
^
5.4.2 Version 2
Now the next observation. If we search for ’CAT’ and ’CACAT’, we see that ’C’ != ’T’, but ’C’ is in the ’CAT’
substring, so we would advance by one character.
CACAT
CAT
^
But... we can also <i>align</i> substring so that the first ’C’ character in ’CAT’ would match the next ’C’ in
’CACAT’, by advancing for 2 characters ahead:
CACAT
CAT
^
We add a rule: ”if a mismatched character in string exist in substring, <i>align</i> substring so that they would
match” or, in other words, ”if ’C’ in string isn’t equal to the last character in ’CAT’ substring, advance by 2 characters”:
if (char3 =='t')
{
...
}
else
{
// 3rd character isn 't 't'
if (( char3 != 'c') && ( char3 !='a') && ( char3 != 't'))
skip =3;
if (char3 =='c') // added
skip =2; // added
}
i=i+skip;
168
It may perform even better on some strings:
** cacat ** cacat
search_cat_brute read_ops =7 search_cat_brute read_ops =7
search_cat_BM_v1 read_ops =5 search_cat_BM_v2 read_ops =4
As of aligning ’A’ character in the middle of ’CAT’ substring – we wouldn’t bother, because advancing substring
by one character it’s the same as is done by default.
Again, our algorithm is hardcoded only for ’CAT’.
And this is the universal algorithm from the R.Sedgewick’s book, the first function construct a table of ’skips’, the
second does the actual search (Java code):
public BoyerMoore ( String pat) {
this.R = 256;
this.pat = pat;
( src )
And what if we search for ’COCOS’ substring? What if characters are repeating within search pattern? Well, we
will count only rightmost characters, ignoring two first ’CO’.
** Results :
BM_cat_v1 .c function check
[check. assertion .1] line 85 assert : SUCCESS
** 0 of 2 failed (1 iterations )
VERIFICATION SUCCESSFUL
CBMC version 5.54.0 (cbmc -5.54.0) 64- bit x86_64 linux
Parsing BM_cat_v2 .c
Converting
Type - checking BM_cat_v2
Generating GOTO Program
Adding CPROVER library ( x86_64 )
Removal of function pointers and virtual functions
Generic Property Instrumentation
Running with 9 object bits , 55 offset bits (user - specified )
Starting Bounded Model Checking
Unwinding loop search_cat_brute .0 iteration 1 file BM_cat_v2 .c line 12 function
search_cat_brute thread 0
Unwinding loop search_cat_brute .0 iteration 2 file BM_cat_v2 .c line 12 function
search_cat_brute thread 0
...
Unwinding loop search_cat_BM_v2 .0 iteration 7 file BM_cat_v2 .c line 36 function
search_cat_BM_v2 thread 0
Unwinding loop search_cat_BM_v2 .0 iteration 8 file BM_cat_v2 .c line 36 function
search_cat_BM_v2 thread 0
Not unwinding loop search_cat_BM_v2 .0 iteration 9 file BM_cat_v2 .c line 36 function
search_cat_BM_v2 thread 0
Runtime Symex: 0.0558006 s
size of program expression : 1364 steps
simple slicing removed 2 assignments
170
Generated 2 VCC(s), 2 remaining after simplification
Runtime Postprocess Equation : 0.000102357 s
Passing problem to propositional reduction
converting SSA
Runtime Convert SSA: 0.0180041 s
Running propositional reduction
Post - processing
Runtime Post - process : 2.8173e -05s
Solving with MiniSAT 2.2.1 with simplifier
14237 variables , 28199 clauses
SAT checker : instance is UNSATISFIABLE
Runtime Solver : 0.0277352 s
Runtime decision procedure : 0.0458506 s
** Results :
BM_cat_v2 .c function check
[check. assertion .1] line 87 assert : SUCCESS
** 0 of 2 failed (1 iterations )
VERIFICATION SUCCESSFUL
CBMC will still verify this algorithm and prove its correctness, because a bug here wouldn’t make the algorithm
incorrect. It will degrade its performance to a level of naive string search algorithm, as if ’skip=3’ here would be
replaced with ’skip=1’. It will still work correctly, but slower.
5.4.5 Files
https://smt.st/current_tree/verif/boyer_moore/files
Regular expressions
6.1 KLEE
I’ve always wanted to generate possible strings for given regular expression. This is not so hard if to dive into regular
expression matcher theory and details, but can we force RE matcher to do this?
I took a lightest RE engine I’ve found: https://github.com/cesanta/slre, and added this:
# include "klee.h"
int main(void)
{
# define BUF_SIZE 8
char s[ BUF_SIZE ];
klee_make_symbolic (s, sizeof s, "s");
s[BUF_SIZE -1]=0;
if ( slre_match ("(s|S)t(even| ephen |eve|evie)", s, BUF_SIZE -1, NULL , 0, 0) ==
BUF_SIZE -1)
klee_assert (0);
}
So I wanted a string that matches the Steven name, in both Steven and Stephen form, also couple diminutive forms
(Steve, Stevie). The whole string must have size of 7 characters plus zero byte.
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ slre.c
% klee --libc= uclibc --optimize --use -query -log= solver :smt2 slre.bc
...
KLEE: ERROR: slre.c :463: ASSERTION FAIL: 0
KLEE: NOTE: now ignoring this error at this location
...
This is indeed the correct string, although, not aligned with start and end of buffer.
Now this RE can match only binary strings that are divisible by 3 (15.3.2):
# include "klee.h"
int main(void)
{
# define BUF_SIZE 10
char s[ BUF_SIZE ];
klee_make_symbolic (s, sizeof s, "s");
171
172
s[BUF_SIZE -1]=0;
if ( slre_match (" ^(1(01*0) *1)*$", s, BUF_SIZE -1, NULL , 0, 0) == BUF_SIZE -1)
klee_assert (0);
}
Got one:
% ktest -tool klee -last/ test000047 . ktest | grep data
object 0: data: b '111110101\ xff '
It’s indeed so, 0b111110101=501, is divisible by 3. Last byte is supposed to be zero-terminating byte, but here is
just a noise. OK.
Now, out of whim, let’s force to find such a number that two consecutive bits before last one will be both 0:
# include "klee.h"
int main(void)
{
# define BUF_SIZE 10
char s[ BUF_SIZE ];
klee_make_symbolic (s, sizeof s, "s");
s[BUF_SIZE -1]=0;
if ( slre_match (" ^(1(01*0) *1)*$", s, BUF_SIZE -1, NULL , 0, 0) == BUF_SIZE -1)
if (s[BUF_SIZE -3]== '0' && s[BUF_SIZE -4]== '0')
klee_assert (0);
}
6.2.1 colou?r
This is a classic example in many RE tutorials – a RE that matches both U.S. ”color” and British ”colour”.
I’m going to use libfsm, a tool that can produce a DFA for a specific RE.
This is a DFA for it:
u r
c o l o
r
But we are programmers, not mathematicians. libfsm can also produce a pure C function that will behave as this
DFA:
int
fsm_main ( const char *s)
{
const char *p;
enum {
173
S0 , S1 , S2 , S3 , S4 , S5 , S6
} state ;
state = S0;
default :
; /* unreached */
}
}
/* end states */
switch ( state ) {
case S6: return 0x1; /* " colou ?r" */
174
default : return -1; /* unexpected EOT */
}
}
Now this is important part – this function can be used in place of RE matcher. It will work exactly as it. In fact,
it emulates DFA for this RE.
It can be used instead of RE matcher in practice, because it works way faster than any RE library. Of course, it
has an obvious limitation – it’s hardcoded/hardwired to a single RE.
It’s very easy to implement DFA in hardware:
Figure 6.1
6.2.2 (Net|Open|Free)BSD
... to be matched with main BSD forks.
e t
N
F r e e B S D
O n
p e
1 Read-Only Memory
2 Field-programmable gate array
3 Application-specificintegrated circuit
4 Random-access memory
5 Central processing unit
6 Graphics Processing Unit
175
int
fsm_main ( const char *s)
{
const char *p;
enum {
S0 , S1 , S2 , S3 , S4 , S5 , S6 , S7 , S8 , S9 ,
S10 , S11 , S12
} state ;
state = S0;
default :
; /* unreached */
}
}
/* end states */
switch ( state ) {
case S9: return 0x1; /* "( Net|Open|Free)BSD" */
default : return -1; /* unexpected EOT */
}
}
6.2.3 (s|S)t(even|ephen|eve|evie)
... to be matched with steven/stephen/steve/stevie, but also the first letter can be captial.
177
h e
p
n
S t e
v
s e n
i e
This is important: there are two accepting states (in double circles). How it can be handled in C? (States S6 and
S8.)
int
fsm_main ( const char *s)
{
const char *p;
enum {
S0 , S1 , S2 , S3 , S4 , S5 , S6 , S7 , S8 , S9 ,
S10
} state ;
state = S0;
default :
; /* unreached */
}
}
/* end states */
switch ( state ) {
case S6: return 0x1; /* "(s|S)t(even|ephen |eve|evie)" */
case S8: return 0x1; /* "(s|S)t(even|ephen |eve|evie)" */
default : return -1; /* unexpected EOT */
}
}
179
6.2.4 (dark|light)?(red|green|blue)(ish)?
... something more advanced. Here we see a limitation of DFA. (Well, not a limitation.) This DFA is correct. But it
will be more (visually) easier to represent it in NFA7 , but this is another story.
r
e
r d
a r k
d b l u e i s h
t g
l i g h n
r e e
b
BTW8 , libfsm can produce a JSON9 file with all transitions between all the vertices:
{
" statecount ": 22,
"start ": 9,
"end ": [ 5, 11 ],
"edges ": [
{ "src ": 0, "dst ": 2, " symbol ": "e" },
{ "src ": 1, "dst ": 17, " symbol ": "u" },
{ "src ": 2, "dst ": 11, " symbol ": "d" },
{ "src ": 3, "dst ": 4, " symbol ": "g" },
{ "src ": 4, "dst ": 16, " symbol ": "h" },
{ "src ": 6, "dst ": 1, " symbol ": "l" },
{ "src ": 7, "dst ": 6, " symbol ": "b" },
{ "src ": 7, "dst ": 15, " symbol ": "g" },
{ "src ": 7, "dst ": 0, " symbol ": "r" },
{ "src ": 8, "dst ": 3, " symbol ": "i" },
{ "src ": 9, "dst ": 6, " symbol ": "b" },
{ "src ": 9, "dst ": 13, " symbol ": "d" },
{ "src ": 9, "dst ": 15, " symbol ": "g" },
{ "src ": 9, "dst ": 8, " symbol ": "l" },
{ "src ": 9, "dst ": 0, " symbol ": "r" },
{ "src ": 10, "dst ": 21, " symbol ": "e" },
{ "src ": 11, "dst ": 20, " symbol ": "i" },
{ "src ": 12, "dst ": 14, " symbol ": "r" },
{ "src ": 13, "dst ": 12, " symbol ": "a" },
{ "src ": 14, "dst ": 7, " symbol ": "k" },
{ "src ": 15, "dst ": 10, " symbol ": "r" },
{ "src ": 16, "dst ": 7, " symbol ": "t" },
{ "src ": 17, "dst ": 11, " symbol ": "e" },
{ "src ": 18, "dst ": 5, " symbol ": "h" },
{ "src ": 19, "dst ": 11, " symbol ": "n" },
{ "src ": 20, "dst ": 18, " symbol ": "s" },
{ "src ": 21, "dst ": 19, " symbol ": "e" }
]
}
And I wrote a simple utility to enumerate all possible input strings that this DFA will accept, in Racket. It is
trivial. It just traverses a DFA recursively down to all accepting states.
#lang racket
( require json)
; knobs:
( define traverse -strlen - limit 15)
( define strings - limit 500000)
;tbl
6.2.5 (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
In NFA, we would just create 12 paths. Alas, this is DFA, so these paths are overlapped at some vertices:
v
N e
b
F u
p g
A
r
e
D
c
J u l
M a n
n
O
r
a
S y
t
c
p
6.2.6 (1|2|3|4|5|6|7|8|9|10|11|12)
’1’ is also accepting state, but can also lead to ’10’, ’11’ and ’12’:
182
0
1
1
9
183
6.2.7 (01|02|03|04|05|06|07|08|09|1|2|3|4|5|6|7|8|9|10|11|12)
1
1
2
0
9
184
6.2.8 (01|02|03|04|05|06|07|08|09|10|11|12):[0-5][0-9]:[0-5][0-9]␣(A|P)M
(Both hours and minutes are always has two digits.)
1
2 0 0
3 1 1
4 0 2 0 2
5 1 3 1 3
6
7 : 2 4 : 2 4 ␣ A M
0 3 5 3 5 P
8 4 6 4 6
9 5 7 5 7
0 8 8
1 1 9 9
2
6.2.9 [0123][0-9]-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(19|20)[0-9]{2}
For the sake of simplicity, only years 19xx and 20xx are accepted:
o
0
e v
N 0 0
1
b
F u 1 1
2
p g
A 2 2
0 3 r
e
D 3 3
1 4 - c
2 5 J u l 1 9
- 4 4
3 6 M a n 2 0 5 5
7 n 6 6
O
8 r 7 7
9 a 8 8
S y
9 9
c t
p
6.2.10 [0123][0-9]-?(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-?(19|20)[0-
9]{2}
But what if the dash is optional?
185
N o
F v
0 N e
0 0
1 O b
F c
1 1
t
2
M O r 2 2
a
0 3
M y 3 3
1 4 - 1
p r 1 9
2 5 A - 4 4
2 0 5 5
3 6 A D u g
7 c 2 6 6
8 J e l 7 7
D
9 8 8
n 9 9
S
J
u n
S p
a
6.2.11 ([0123][0-9]-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(19|20)[0-9]{2}|(1
9]{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-[0123][0-9])
Can accept dates in DD-MM-YYYY or YYYY-MM-DD format, dash must be present:
186
0
5
6
u g
0 7 p
r
3 8
r
9 A
a
y 8
0 M o
v
9
1 n 0
1 N a 0
n - 2
2 u 1 0 1
J 1
l 9
3 - O c t 2 2
4 S p 3
5 D e 3
6 c
4
7 - F e 4
b 5
2 8 6 5
9 1 e e 6
7
7
2 p 8
2 S e 2
4 9
c
3 3 D e
3
b
5 4 4
F p r
5 5 u
A g 0
6 6 -
6 - y 1
7 7 M a
8 8 J r
7 9 9 n
N
8 u
a
9 O l
0 0 n
1 o v
1
2 t
3 c
0
187
6.2.12 ([0123][0-9]-?(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-?(19|20)[0-
9]{2}|(19|20)[0-9]{2}-?(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-
?[0123][0-9])
Can accept dates in DD-MM-YYYY or YYYY-MM-DD format, a dash is optional. Now the DFA is too large:
188
0
1
2
3
4
5
6
7
0 J
8
3 9 D
A
0
1 N
1 2 u n
J a
3 l
4 F
J
5 D e n
6
7 - A D c
8 u
2 p g 6
N A
r 7
6 N o v 2
b - 8
F 2
e t 0
7 O 1 9
- O p 1 9
S c 0 0
9 F
r 1
8 1
M
S e 2
9 y 2
3
1 a 3
O 4
2 4
3 M 5 5
4 6
5 S 7
S e 2 8
3
M D 9
S e p 3
9
0 F D e c 0
0 6
b 1
1 7 O F c -
t 0
2 8 O
u g 1
3 9 A p r 2
-
4 0 l
1 J
5 A n
6 2 N u
3 J n
7
4 a v
8 N M
5 r
o
M
y
a
Note: using the standard RE, it’s possible to match only DD-MM-YYYY or DDMMYYYY strings without match-
ing DD-MMYYYY or DDMM-YYYY, but the final DFA would be bulky.
189
6.2.13 var1␣+var2
Like I stated before, RE often used for parsing. Here are more practical examples.
What it accepts? I’m going to try my utility again:
string matched : [var1 var2]
string matched : [var1 var2]
string matched : [var1 var2]
string matched : [var1 var2]
string matched : [var1 var2]
string matched : [var1 var2]
total: 6
Obviously, a set of input strings is infinite. So my utility has some limit and stopped after it is reached.
v a r 1 ␣ v a r 2
6.2.14 var1[␣\t]+var2
(Extending this RE to whitespaces, i.e., tab symbol.)
6.2.15 ␣*var1␣*=␣*var2␣*
Even more practical. A whitespace can surround input string and the equality sign. Kleene star is used here.
␣ ␣ ␣ ␣
v a r 1 = v a r 2
6.2.16 (0|(1(01*0)*1))*
This RE accepts input numbers (in binary form) that are divisible by 3 (15.3.2).
(More about it.)
See also about DFA accepting a binary substring divisible by prime number: 15.3.2.
0 1
1 0
1 0
6.2.17 [␣\t]*[a-z]+[␣\t]*=[␣\t]*[0-9]+[␣\t]*
This is the most practically useful example here. It can be used to parse config files in form option=number, but
whitespaces are also handled – around input string and around the ’equal’ sign.
According to my utility, these are (random) sample input strings that can be accepted by the RE:
string matched : [ zzzzzzzzzwt =09]
string matched : [ zzzzzzzzztkj =3]
string matched : [ zzzzzzzzzuj =5 ]
string matched : [ zzzzzzzzzy =16 ]
string matched : [ zzzzzzzzzzcr =9]
string matched : [ zzzzzzzzzzxz =2]
string matched : [ zzzzzzzzzuca =2]
string matched : [ zzzzzzzzzyd =44]
string matched : [ zzzzzzzzzuwj =5]
string matched : [ zzzzzzzzzvfj =3]
string matched : [ zzzzzzzzzs =13]
string matched : [ zzzzzzzzzt = 8]
string matched : [ zzzzzzzzzuu =5]
string matched : [ zzzzzzzzzwhi =1]
char *tmp=s;
// read option (a-z)
for (;;)
{
if ((*s >= 'a') && (*s <='z'))
{
s++;
continue ;
}
else
break ;
};
// skip spaces
while (*s== ' ')
s++;
// match '='
if (*s!= '=')
return -1;
s++;
// skip spaces
while (*s== ' ')
s++;
tmp=s;
// read option (0 -9)
for (;;)
{
if ((*s >= '0') && (*s <='9'))
{
s++;
continue ;
}
else
break ;
};
// skip spaces
while (*s== ' ')
193
s++;
// all done!
return 1;
};
# ifdef CBMC
void check ()
{
char s[LEN +1];
s[LEN ]=0;
__CPROVER_assert ( naive (s)== fsm_main (s), " assert ");
};
# endif
int main ()
{
//# define S " = "
# define S "b="
printf ("%d\n", naive (S));
printf ("%d\n", fsm_main (S));
};
int
fsm_main ( const char *s)
{
const char *p;
enum {
S0 , S1 , S2 , S3 , S4 , S5
} state ;
state = S0;
default :
; /* unreached */
}
}
/* end states */
switch ( state ) {
case S4: return 0x1; /* "[ \t]*[a-z]+[ \t]*=[ \t]*[0 -9]+[ \t]*" */
case S5: return 0x1; /* "[ \t]*[a-z]+[ \t]*=[ \t]*[0 -9]+[ \t]*" */
default : return -1; /* unexpected EOT */
}
}
Ouch, my naive parser can’t parse the string ” y=” correctly. It has no second part (a number) after the ’equality’
sign. Probably I can fix it like that:
else
break ;
196
};
+ // option must be non - empty
+ if (tmp ==s)
+ return -1;
// skip spaces
while (*s==' ')
@@ -48,6 +51 ,9 @@
else
break ;
};
+ // option must be non - empty
+ if (tmp ==s)
+ return -1;
// skip spaces
while (*s==' ')
char *tmp=s;
@@ -27,7 +27 ,7 @@
return -1;
// skip spaces
- while (*s==' ')
+ while ((*s==' ') || (*s== '\t '))
s++;
// match '='
@@ -36,7 +36 ,7 @@
s++;
// skip spaces
- while (*s==' ')
+ while ((*s==' ') || (*s== '\t '))
s++;
tmp=s;
@@ -56,7 +56 ,7 @@
return -1;
// skip spaces
- while (*s==' ')
+ while ((*s==' ') || (*s== '\t '))
s++;
Still problems:
197
<p>
-72 is 0 xffffffffffffffb8 or 0xb8 , it is non - printable character , anyway .
Fixing it:
</p>
- // skip spaces
- while ((*s==' ') || (*s== '\t '))
+ // at this point , only spaces and whitespaces are allowed
+ while (*s)
+ {
+ if ((*s!=' ') && (*s!= '\t '))
+ {
+ return -1;
+ };
s++;
+ };
// all done!
return 1;
All cool, up to strings of length 15. It should be noted: ’unwind’ parameter must be bigger by at least 1. But not
too big, otherwise it will verify your code too slow.
% cbmc --trace --function check 4.c --unwind 16 --unwinding - assertions -DCBMC -DLEN
=15
...
** Results :
[check. assertion .1] assert : SUCCESS
[naive. unwind .0] unwinding assertion loop 0: SUCCESS
[naive. unwind .1] unwinding assertion loop 1: SUCCESS
[naive. unwind .2] unwinding assertion loop 2: SUCCESS
[naive. unwind .3] unwinding assertion loop 3: SUCCESS
[naive. unwind .4] unwinding assertion loop 4: SUCCESS
[naive. unwind .5] unwinding assertion loop 5: SUCCESS
[ fsm_main . unwind .0] unwinding assertion loop 0: SUCCESS
** 0 of 8 failed (1 iteration )
VERIFICATION SUCCESSFUL
char *tmp=s;
// read option (a-z)
for (;;)
{
if ((*s >= 'a') && (*s <='z'))
{
s++;
continue ;
}
else
break ;
};
// option must be non - empty
if (tmp ==s)
return -1;
// skip spaces
while ((*s== ' ') || (*s== '\t'))
s++;
// match '='
if (*s!= '=')
return -1;
s++;
// skip spaces
while ((*s== ' ') || (*s== '\t'))
s++;
tmp=s;
// read option (0 -9)
for (;;)
{
if ((*s >= '0') && (*s <='9'))
{
s++;
continue ;
}
else
break ;
};
// option must be non - empty
if (tmp ==s)
return -1;
// all done!
return 1;
};
# ifdef CBMC
void check ()
{
char s[LEN +1];
s[LEN ]=0;
__CPROVER_assert ( naive (s)== fsm_main (s), " assert ");
};
# endif
int main ()
{
//# define S " = "
# define S "b="
printf ("%d\n", naive (S));
printf ("%d\n", fsm_main (S));
};
Obviously, without tools like CBMC, you have a lots of unchecked vulnerable code.
6.2.18 ([az]+␣*,)+[az]+␣*
Even harder RE: for CSV10 files. For the sake of simplicity, it only accepts strings consisting of ’a’ or ’z’.
, ␣
z
a
, z
a a a ␣
␣
z ␣ , z
It’s like nested or recursive RE. But probably too complicated – a more advanced parser should be used for CSV
instead of RE.
6.2.20 Homework
All this code is like ”grep”, it doesn’t return groups of parsed line, like MM, DD and YYYY separately. You can try
hacking what libfsm produces in pure C to add this feature.
Further reading: blog posts about RE by Russ Cox is what I found interesting.
A discussion at HN and reddit.
10 Comma-Separated Values
Chapter 7
Gray code
200
201
( Source: http://homepages.dordt.edu/~ddeboer//S10/304/c_at_d/304S10_RC_TRK.HTM )
Click on bigger one.
There are pins and tracks on rotating wheel. How would you do this? Easiest way is to use binary code. But it
has a problem: when a wheel is rotating, in a moment of transition from one state to another, several bits may be
changed, hence, undesirable state may be present for a short period of time. This is bad. To deal with it, Gray code
was invented: only 1 bit is changed during rotation. Like:
Decimal Binary Gray
0 0000 0000
1 0001 0001
2 0010 0011
3 0011 0010
4 0100 0110
5 0101 0111
6 0110 0101
7 0111 0100
8 1000 1100
9 1001 1101
10 1010 1111
11 1011 1110
12 1100 1010
13 1101 1011
14 1110 1001
15 1111 1000
Now the second problem. Look at the picture again. It has a lot of bit changes on the outer circles. And this is
electromechanical device. Surely, you may want to make tracks as long as possible, to reduce wearing of both tracks
and pins. This is a first problem. The second: wearing should be even across all tracks (this is balanced Gray code).
203
This is also called: ”They are listed in Gray code or minimum change order, where each subset differs in exactly
one element from its neighbors.” ( Sriram V. Pemmaraju and Steven Skiena – Computational Discrete Mathematics:
Combinatorics and Graph Theory in Mathematica )
How we can find a table for all states using Z3:
#!/ usr/bin/env python3
from z3 import *
BITS =5
# how many times a run of bits for each bit can be changed (max).
# it can be 4 for 4-bit Gray code or 8 for 5-bit code.
# 12 for 6-bit code ( maybe even less)
CHANGES_MAX =8
s= Solver ()
code =[[ Bool('code_ %d_%d' % (r,c)) for c in range (BITS)] for r in range (ROWS)]
ch =[[ Bool('ch_%d_%d' % (r,c)) for c in range (BITS)] for r in range (ROWS)]
# each rows must be different from a previous one and a next one by 1 bit:
for i in range (ROWS):
# get bits of the current row:
204
lst1 =[ code[i][ bit] for bit in range (BITS)]
# get bits of the next row.
# important : if the current row is the last one , (last +1)&MASK ==0 , so we overlap
here:
lst2 =[ code [(i+1)&MASK ][ bit] for bit in range (BITS)]
hamming1 (lst1 , lst2)
# 1 in ch [] table means that run of 1's has been changed to run of 0's, or back.
# "run" change detected using simple XOR:
for i in range (ROWS):
for bit in range (BITS):
# row overlapping works here as well:
s.add(ch[i][ bit ]== Xor(code[i][ bit],code [(i+1)&MASK ][ bit ]))
stat ={}
8 changes for 5 bits: https://smt.st/current_tree/gray_code/SMT/5.txt. 12 for 6 bits (or maybe even less):
https://smt.st/current_tree/gray_code/SMT/6.txt.
206
7.1.1 Duke Nukem 3D from 1990s
>In Duke Nukem , you often come upon panels that have four buttons in a row ,
>all in their "off" position . Each time you "push" a button , it toggles from
>one state to the other . The object is to find the unique combination that
>unlocks something in the game.
>My question is: What is the most efficient order in which to push the
>buttons so that every combination is tested with no wasted effort ?
(Oh , you wanted to know what one would be? How about :
0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
207
1010
1000
1001
1011
( https://groups.google.com/forum/#!topic/rec.puzzles/Dh2H-pGJcbI )
Obviously, using our solution, you can minimize all movements in this ancient videogame, for 4 switches, that
would be 4*4=16 switches. With our solution (balanced Gray code), wearing would be even across all 4 switches.
BITS =6
# how many times a run of bits for each bit can be changed (max).
# it can be 4 for 4-bit Gray code or 8 for 5-bit code.
# 12 for 6-bit code ( maybe even less)
# each rows must be different from a previous one and a next one by 1 bit:
for i in range (ROWS):
# get bits of the current row:
lst1 =[ code[i][ bit] for bit in range (BITS)]
# get bits of the next row.
# important : if the current row is the last one , (last +1)&MASK ==0 , so we
overlap here:
lst2 =[ code [(i+1)&MASK ][ bit] for bit in range (BITS)]
s. hamming1 (lst1 , lst2)
# 1 in ch [] table means that run of 1's has been changed to run of 0's, or back.
# "run" change detected using simple XOR:
for i in range (ROWS):
for bit in range (BITS):
# row overlapping works here as well.
# we add here "soft" constraint with weight =1:
s. fix_soft (s.EQ(ch[i][ bit], s.XOR(code[i][ bit],code [(i+1)&MASK ][ bit ])),
False , weight =1)
if s. solve () == False :
print (" unsat ")
exit (0)
# get statistics :
stat ={}
do_all ()
*
**
* **
** **
** *
* *
* **
* *
** *
stat (bit number : number of changes ):
{0: 6, 1: 4, 2: 6, 3: 6, 4: 10}
*
* *
* * *
* *
*
* *
*
**
* **
* ***
***
****
* ****
* ***
** ***
** **
** *
**
*
* *
* * *
** * *
** *
** **
* **
**
stat (bit number : number of changes ):
{5: 10, 3: 14, 4: 8, 0: 10, 2: 6, 1: 16}
1 https://yurichev.com/mirrors/Donald%20Knuth/TAOCP%202a%207.2.1.1/fasc2a.pdf
2 https://www.jjj.de/fxt/fxtbook.pdf
Chapter 8
8.1 Sudoku
Sudoku puzzle is a 9*9 grid with some cells filled with values, some are empty:
5 3
8 2
7 1 5
4 5 3
1 7 6
3 2 8
6 5 9
4 3
9 7
Unsolved Sudoku
Numbers of each row must be unique, i.e., it must contain all 9 numbers in range of 1..9 without repetition. Same
story for each column and also for each 3*3 square.
This puzzle is good candidate to try SMT solver on, because it’s essentially an unsolved system of equations.
Take each input variable, calculate 10i and sum them all. If all input values are unique, each will be settled at its
own place. Even more than that: there will be no holes, i.e., no skipped values. So, in case of Sudoku, 1111111110
number will be final result, indicating that all 9 input values are unique, in range of 1..9.
Exponentiation is heavy operation, can we use binary operations? Yes, just replace 10 with 2:
The effect is just the same, but the final value is in base 2 instead of 10.
Now a working example:
211
212
import sys
from z3 import *
"""
coordinates :
------------------------------
00 01 02 | 03 04 05 | 06 07 08
10 11 12 | 13 14 15 | 16 17 18
20 21 22 | 23 24 25 | 26 27 28
------------------------------
30 31 32 | 33 34 35 | 36 37 38
40 41 42 | 43 44 45 | 46 47 48
50 51 52 | 53 54 55 | 56 57 58
------------------------------
60 61 62 | 63 64 65 | 66 67 68
70 71 72 | 73 74 75 | 76 77 78
80 81 82 | 83 84 85 | 86 87 88
------------------------------
"""
s= Solver ()
( https://smt.st/current_tree/puzzles/sudoku/1/sudoku_plus_Z3.py )
% time python sudoku_plus_Z3 .py
1 4 5 3 2 7 6 9 8
8 3 9 6 5 4 1 2 7
6 7 2 9 1 8 5 4 3
4 9 6 1 8 5 3 7 2
2 1 8 4 7 3 9 5 6
7 5 3 2 9 6 4 8 1
3 6 7 5 4 2 8 1 9
9 8 4 7 6 1 2 3 5
5 2 1 8 3 9 7 6 4
import sys
from z3 import *
"""
coordinates :
214
------------------------------
00 01 02 | 03 04 05 | 06 07 08
10 11 12 | 13 14 15 | 16 17 18
20 21 22 | 23 24 25 | 26 27 28
------------------------------
30 31 32 | 33 34 35 | 36 37 38
40 41 42 | 43 44 45 | 46 47 48
50 51 52 | 53 54 55 | 56 57 58
------------------------------
60 61 62 | 63 64 65 | 66 67 68
70 71 72 | 73 74 75 | 76 77 78
80 81 82 | 83 84 85 | 86 87 88
------------------------------
"""
s= Solver ()
( https://smt.st/current_tree/puzzles/sudoku/1/sudoku_or_Z3.py )
Now it works much faster. Z3 handles OR operation over bit vectors better than addition?
% time python sudoku_or_Z3 .py
1 4 5 3 2 7 6 9 8
8 3 9 6 5 4 1 2 7
6 7 2 9 1 8 5 4 3
4 9 6 1 8 5 3 7 2
2 1 8 4 7 3 9 5 6
7 5 3 2 9 6 4 8 1
3 6 7 5 4 2 8 1 9
9 8 4 7 6 1 2 3 5
5 2 1 8 3 9 7 6 4
import sys
from z3 import *
"""
1 http://www.mirror.co.uk/news/weird-news/worlds-hardest-sudoku-can-you-242294
216
------------------------------
00 01 02 | 03 04 05 | 06 07 08
10 11 12 | 13 14 15 | 16 17 18
20 21 22 | 23 24 25 | 26 27 28
------------------------------
30 31 32 | 33 34 35 | 36 37 38
40 41 42 | 43 44 45 | 46 47 48
50 51 52 | 53 54 55 | 56 57 58
------------------------------
60 61 62 | 63 64 65 | 66 67 68
70 71 72 | 73 74 75 | 76 77 78
80 81 82 | 83 84 85 | 86 87 88
------------------------------
"""
s= Solver ()
# this is important , because otherwise , Z3 will report correct solutions with too big
and/or negative numbers in cells
for r in range (9):
for c in range (9):
s.add( cells [r][c] >=1)
s.add( cells [r][c] <=9)
( https://smt.st/current_tree/puzzles/sudoku/1/sudoku2_Z3.py )
% time python sudoku2_Z3 .py
1 4 5 3 2 7 6 9 8
8 3 9 6 5 4 1 2 7
6 7 2 9 1 8 5 4 3
4 9 6 1 8 5 3 7 2
2 1 8 4 7 3 9 5 6
7 5 3 2 9 6 4 8 1
3 6 7 5 4 2 8 1 9
9 8 4 7 6 1 2 3 5
5 2 1 8 3 9 7 6 4
Conclusion
SMT-solvers are so helpful, is that our Sudoku solver has nothing else, we have just defined relationships between
variables (cells).
Homework
As it seems, true Sudoku puzzle is the one which has only one solution. The piece of code I’ve included here shows
only the first one. Using the method described earlier (3.17, also called “model counting”), try to find more solutions,
or prove that the solution you have just found is the only one possible.
Further reading
http://www.norvig.com/sudoku.html
218
Sudoku as a SAT problem
It’s also possible to represent Sudoku puzzle as a huge CNF equation and use SAT-solver to find solution, but it’s just
trickier.
Some articles about it: Building a Sudoku Solver with SAT 2 , Tjark Weber, A SAT-based Sudoku Solver 3 , Ines
Lynce, Joel Ouaknine, Sudoku as a SAT Problem4 , Gihwon Kwon, Himanshu Jain, Optimized CNF Encoding for
Sudoku Puzzles5 .
SMT-solver can also use SAT-solver in its core, so it does all mundane translating work. As a “compiler”, it may
not do this in the most efficient way, though.
; ------------------------------
;00 01 02 | 03 04 05 | 06 07 08
;10 11 12 | 13 14 15 | 16 17 18
;20 21 22 | 23 24 25 | 26 27 28
; ------------------------------
;30 31 32 | 33 34 35 | 36 37 38
;40 41 42 | 43 44 45 | 46 47 48
;50 51 52 | 53 54 55 | 56 57 58
; ------------------------------
;60 61 62 | 63 64 65 | 66 67 68
;70 71 72 | 73 74 75 | 76 77 78
;80 81 82 | 83 84 85 | 86 87 88
; ------------------------------
; ..53..... - 0
; 8......2. - 1
; .7..1.5.. - 2
; 4....53.. - 3
; .1..7...6 - 4
; ..32...8. - 5
; .6.5....9 - 6
; ..4....3. - 7
; .....97.. - 8
; set input :
( assert (= ( select cells #x02) #x5))
( assert (= ( select cells #x03) #x3))
( assert (= ( select cells #x10) #x8))
( assert (= ( select cells #x17) #x2))
( assert (= ( select cells #x21) #x7))
( assert (= ( select cells #x24) #x1))
( assert (= ( select cells #x26) #x5))
( assert (= ( select cells #x30) #x4))
( assert (= ( select cells #x35) #x5))
( assert (= ( select cells #x36) #x3))
( assert (= ( select cells #x41) #x1))
( assert (= ( select cells #x44) #x7))
( assert (= ( select cells #x48) #x6))
( assert (= ( select cells #x52) #x3))
( assert (= ( select cells #x53) #x2))
( assert (= ( select cells #x57) #x8))
( assert (= ( select cells #x61) #x6))
( assert (= ( select cells #x63) #x5))
( assert (= ( select cells #x68) #x9))
( assert (= ( select cells #x72) #x4))
( assert (= ( select cells #x77) #x3))
( assert (= ( select cells #x84) #x9))
( assert (= ( select cells #x86) #x7))
(check -sat)
(get - model )
( https://smt.st/current_tree/puzzles/sudoku/2/sudoku_array.smt )
Let’s see how different SMT solvers produce results, dumping array’s content:
Listing 8.1: STP
(model
( define -fun |cells | (_ BitVec 8) (_ BitVec 4) #x70 #x5 )
( define -fun |cells | (_ BitVec 8) (_ BitVec 4) #x62 #x1 )
( define -fun |cells | (_ BitVec 8) (_ BitVec 4) #x38 #x2 )
( define -fun |cells | (_ BitVec 8) (_ BitVec 4) #x28 #x3 )
( define -fun |cells | (_ BitVec 8) (_ BitVec 4) #x85 #x4 )
...
Some SMT solvers dumps a function that create an array using store operations:
222
Listing 8.2: Z3
(
(define -fun cells () ( Array (_ BitVec 8) (_ BitVec 4))
(let ((a!1 ( store ( store ( store (( as const ( Array (_ BitVec 8) (_ BitVec 4)))
#x1)
#x07
#x7)
#x18
#x3)
#x71
#x9)))
(let ((a!2 (store ( store ( store ( store a!1 #x38 #x2) #x45 #x3) #x60 #x7)
#x65
#x8)))
(let ((a!3 (store ( store ( store ( store a!2 #x50 #x6) #x43 #x8) #x12 #x9)
#x28
#x8)))
...
(let ((a!18 (store ( store ( store ( store a!17 #x21 #x7) #x17 #x2) #x10 #x8)
#x03
#x3)))
(store a!18 #x02 #x5))))))))))))))))))))
)
Some other SMT solvers create an ITE-tree, that will behave exactly like the array filled with correct values. In
fact, you can copy and paste this output and use it as a function in your SMT code:
...
Unlike Z3, CVC5 dumps a function that constructs array without using let, but again, using nested store operations:
...
Figure 8.1
It can be solved easily with Z3. I’ve took the same piece of code I used for the usual Sudoku: 8.1.1.
... and added this:
...
"""
Subsquares :
------------------------------
| |
1,1 | 1,2 | 1,3
| |
------------------------------
| |
2,1 | 2,2 | 2,3
| |
------------------------------
| |
3,1 | 3,2 | 3,3
| |
------------------------------
"""
225
# from http :// www. killersudokuonline .com/ puzzles /2017/ puzzle - GD4hzi164344 .pdf
# subsquare 1,1:
s.add( cells [0][0] > cells [0][1])
s.add( cells [1][0] > cells [1][1])
s.add( cells [2][0] < cells [2][1])
# subsquare 1,2:
s.add( cells [0][4] > cells [1][4])
s.add( cells [1][3] > cells [2][3])
s.add( cells [1][4] > cells [2][4])
s.add( cells [1][5] > cells [2][5])
# subsquare 1,3:
s.add( cells [0][6] > cells [0][7])
s.add( cells [0][7] < cells [0][8])
s.add( cells [0][6] < cells [1][6])
s.add( cells [1][7] < cells [1][8])
s.add( cells [1][6] > cells [2][6])
s.add( cells [1][7] > cells [2][7])
s.add( cells [1][8] > cells [2][8])
# subsquare 2,1:
s.add( cells [3][0] < cells [4][0])
s.add( cells [4][0] < cells [5][0])
s.add( cells [4][1] < cells [4][2])
s.add( cells [4][0] < cells [5][0])
s.add( cells [4][1] > cells [5][1])
s.add( cells [4][2] < cells [5][2])
# subsquare 2,2:
s.add( cells [3][4] > cells [4][4])
s.add( cells [3][4] < cells [3][5])
s.add( cells [4][3] < cells [4][4])
s.add( cells [4][3] < cells [5][3])
# subsquare 2,3:
s.add( cells [3][6] > cells [3][7])
s.add( cells [3][7] > cells [3][8])
s.add( cells [3][6] > cells [4][6])
s.add( cells [4][6] < cells [4][7])
s.add( cells [4][7] < cells [4][8])
s.add( cells [5][7] < cells [5][8])
# subsquare 3,1:
s.add( cells [6][0] > cells [6][1])
s.add( cells [6][1] < cells [6][2])
s.add( cells [6][1] > cells [7][1])
s.add( cells [7][0] < cells [7][1])
s.add( cells [7][0] > cells [8][0])
s.add( cells [7][2] > cells [8][2])
# subsquare 3,2:
s.add( cells [6][3] > cells [6][4])
s.add( cells [6][4] > cells [6][5])
s.add( cells [7][3] > cells [7][4])
s.add( cells [7][4] < cells [7][5])
226
s.add( cells [8][3] > cells [8][4])
s.add( cells [8][4] < cells [8][5])
s.add( cells [7][4] > cells [8][4])
# subsquare 3,3:
s.add( cells [6][7] > cells [6][8])
s.add( cells [6][7] > cells [7][7])
s.add( cells [7][7] > cells [8][7])
s.add( cells [8][7] > cells [8][8])
...
Figure 8.2
227
There are “cages”, each cage must have distinct digits, and its sum must be equal to the number written there in
a manner of crossword. See also: https://en.wikipedia.org/wiki/Killer_sudoku.
This is also piece of cake for Z3. I’ve took the same piece of code I used for usual Sudoku (8.1.1).
...
cage =[ cells [0][7] , cells [0][8] , cells [1][7] , cells [1][8] , cells [2][7] , cells [2][8] ,
cells [3][7]]
s.add( Distinct (* cage))
s.add(Sum (* cage) ==34)
cage =[ cells [2][6] , cells [3][5] , cells [3][6] , cells [4][5] , cells [4][6]]
s.add( Distinct (* cage))
s.add(Sum (* cage) ==28)
cage =[ cells [4][2] , cells [4][3] , cells [5][2] , cells [5][3] , cells [6][2]]
s.add( Distinct (* cage))
228
s.add(Sum (* cage) ==25)
cage =[ cells [5][1] , cells [6][0] , cells [6][1] , cells [7][0] , cells [7][1] , cells [8][0] ,
cells [8][1]]
s.add( Distinct (* cage))
s.add(Sum (* cage) ==40)
...
8.1.5 KLEE
(All timings are when KLEE 3.0 running on AMD Ryzen 5 3600.)
I’ve also rewritten Sudoku example (8.1.1) for KLEE:
1 # include <stdint .h>
2 # include <assert .h>
3
4 # include "klee.h"
5
6 /*
7 coordinates :
8 ------------------------------
9 00 01 02 | 03 04 05 | 06 07 08
10 10 11 12 | 13 14 15 | 16 17 18
11 20 21 22 | 23 24 25 | 26 27 28
12 ------------------------------
13 30 31 32 | 33 34 35 | 36 37 38
14 40 41 42 | 43 44 45 | 46 47 48
15 50 51 52 | 53 54 55 | 56 57 58
16 ------------------------------
17 60 61 62 | 63 64 65 | 66 67 68
18 70 71 72 | 73 74 75 | 76 77 78
19 80 81 82 | 83 84 85 | 86 87 88
20 ------------------------------
21 */
22
23 uint8_t cells [9][9];
24
25 // http :// www. norvig .com/ sudoku .html
26 // http :// www. mirror .co.uk/news/weird -news/worlds -hardest -sudoku -can -you -242294
27 char * puzzle
="..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97..";
28
29 int main ()
30 {
31 klee_make_symbolic (cells , sizeof cells , " cells ");
32
33 // process text line:
34 for (int row =0; row <9; row ++)
35 for (int column =0; column <9; column ++)
36 {
37 char c= puzzle [row *9 + column ];
38 if (c!= '. ')
39 {
40 if ( cells [row ][ column ]!=c-'0') return 0;
41 }
42 else
43 {
44 // limit cells values to 1..9:
45 if ( cells [row ][ column ] <1) return 0;
46 if ( cells [row ][ column ] >9) return 0;
47 };
48 };
49
50 // for all 9 rows
51 for (int row =0; row <9; row ++)
52 {
230
53
54 if (((1 < < cells [row ][0]) |
55 (1<< cells [row ][1]) |
56 (1<< cells [row ][2]) |
57 (1<< cells [row ][3]) |
58 (1<< cells [row ][4]) |
59 (1<< cells [row ][5]) |
60 (1<< cells [row ][6]) |
61 (1<< cells [row ][7]) |
62 (1<< cells [row ][8]) )!=0 x3FE ) return 0; // 11 1111 1110
63 };
64
65 // for all 9 columns
66 for (int c=0; c <9; c++)
67 {
68 if (((1 < < cells [0][c]) |
69 (1<< cells [1][c]) |
70 (1<< cells [2][c]) |
71 (1<< cells [3][c]) |
72 (1<< cells [4][c]) |
73 (1<< cells [5][c]) |
74 (1<< cells [6][c]) |
75 (1<< cells [7][c]) |
76 (1<< cells [8][c]))!=0 x3FE ) return 0; // 11 1111 1110
77 };
78
79 // enumerate all 9 squares
80 for (int r=0; r <9; r+=3)
81 for (int c=0; c <9; c+=3)
82 {
83 // add constraints for each 3*3 square :
84 if ((1<< cells [r+0][c+0] |
85 1<< cells [r+0][c+1] |
86 1<< cells [r+0][c+2] |
87 1<< cells [r+1][c+0] |
88 1<< cells [r+1][c+1] |
89 1<< cells [r+1][c+2] |
90 1<< cells [r+2][c+0] |
91 1<< cells [r+2][c+1] |
92 1<< cells [r+2][c+2]) !=0 x3FE ) return 0; // 11 1111
1110
93 };
94
95 // at this point , all constraints must be satisfied
96 assert (0);
97 };
...
Now this is really slower (on AMD Ryzen 5 3600) in comparison to Z3Py solution (8.1.1).
But the answer is correct:
% ls klee -last /* err
klee -last/ test000103 . external .err
object 0: text:
.................................................................................
Character \t has code of 9 in C/C++, and KLEE prints byte array as a C/C++ string, so it shows some values
in such way. We can just keep in mind that there is 9 at the each place where we see \t. The solution, while not
properly formatted, correct indeed.
By the way, at lines 45 and 46 you may see how we tell to KLEE that all array elements must be within some
limits. If we comment these lines out, we’ve got this:
...
KLEE: ERROR: klee_sudoku_or1 .c:54: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:55: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:58: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:59: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:60: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:61: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:62: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:56: overshift error
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_sudoku_or1 .c:57: overshift error
KLEE: NOTE: now ignoring this error at this location
...
KLEE warns us that shift value at lines 54...62 is too big. Indeed, KLEE may try all byte values up to 255 (0xFF),
which are pointless to use there, and may be a symptom of error or bug, so KLEE warns about it.
Now let’s use klee_assume() again:
# include <stdint .h>
# include <assert .h>
# include "klee.h"
232
/*
coordinates :
------------------------------
00 01 02 | 03 04 05 | 06 07 08
10 11 12 | 13 14 15 | 16 17 18
20 21 22 | 23 24 25 | 26 27 28
------------------------------
30 31 32 | 33 34 35 | 36 37 38
40 41 42 | 43 44 45 | 46 47 48
50 51 52 | 53 54 55 | 56 57 58
------------------------------
60 61 62 | 63 64 65 | 66 67 68
70 71 72 | 73 74 75 | 76 77 78
80 81 82 | 83 84 85 | 86 87 88
------------------------------
*/
int main ()
{
klee_make_symbolic (cells , sizeof cells , " cells ");
};
...
KLEE: ERROR: klee_sudoku_or2 .c:96: ASSERTION FAIL: 0
KLEE: NOTE: now ignoring this error at this location
That works much faster: because only one path to explore. And, as we see, the only one path has been found (one
we actually interesting in it) instead of 161.
It’s still much slower than Z3Py solution, though.
Now we will use a POPCNT1 function to make each row in the matrix to contain only one True bit, that will preserve
consistency in encoding, since no vector can contain more than 1 True bit, or no True bits at all. Then we will use a
POPCNT1 function again to make all columns in the matrix to have only one single True bit. That will make all rows
in matrix unique, in other words, all 9 encoded numbers will always be unique.
After applying POPCNT1 function 9+9=18 times we’ll have 9 unique numbers in 1..9 range.
Using that operation we can make each row of Sudoku puzzle unique, each column unique and also each 3 · 3 = 9
box.
#!/ usr/bin/env python3
# -*- coding : utf -8 -*-
# global variables :
clauses =[]
vector_names ={}
last_var =1
BITS_PER_VECTOR =9
return solution
def neg(v):
return "-"+v
235
# 1 -> [1, 0, 0, 0, 0, 0, 0, 0, 0]
# 3 -> [0, 0, 1, 0, 0, 0, 0, 0, 0]
def number_to_vector (n):
rt =[0]*(n -1)
236
rt. append (1)
rt=rt +[0]*( BITS_PER_VECTOR -len(rt))
return rt
"""
coordinates we 're using here:
+--------+--------+--------+
|11 12 13|14 15 16|17 18 19|
|21 22 23|24 25 26|27 28 29|
|31 32 33|34 35 36|37 38 39|
+--------+--------+--------+
|41 42 43|44 45 46|47 48 49|
|51 52 53|54 55 56|57 58 59|
|61 62 63|64 65 66|67 68 69|
+--------+--------+--------+
|71 72 73|74 75 76|77 78 79|
|81 82 83|84 85 86|87 88 89|
|91 92 93|94 95 96|97 98 99|
+--------+--------+--------+
"""
def make_vec_name (row , col):
return "cell"+str(row)+str(col)
print_solution ( solution )
main ()
( https://smt.st/current_tree/puzzles/sudoku/SAT/sudoku_SAT.py )
The make_distinct_bits_in_vector() function preserves consistency of encoding.
The make_distinct_vectors() function makes 9 numbers unique.
The cvt_vector_to_number() decodes vector to number.
The number_to_vector() encodes number to vector.
The main() function has all necessary calls to make rows/columns/3 · 3 boxes unique.
That works:
% python sudoku_SAT .py
len( clauses )= 12195
1 4 5 3 2 7 6 9 8
8 3 9 6 5 4 1 2 7
6 7 2 9 1 8 5 4 3
4 9 6 1 8 5 3 7 2
2 1 8 4 7 3 9 5 6
7 5 3 2 9 6 4 8 1
3 6 7 5 4 2 8 1 9
9 8 4 7 6 1 2 3 5
5 2 1 8 3 9 7 6 4
In the interest of clarity, it must be added that each of the five houses is painted a different
color, and their inhabitants are of different national extractions, own different pets, drink different
beverages and smoke different brands of American cigarets [sic]. One other thing: in statement 6,
right means your right.
( https://en.wikipedia.org/wiki/Zebra_Puzzle )
from z3 import *
Yellow , Blue , Red , Ivory , Green =Ints('Yellow Blue Red Ivory Green ')
Norwegian , Ukrainian , Englishman , Spaniard , Japanese =Ints('Norwegian Ukrainian
Englishman Spaniard Japanese ')
Water , Tea , Milk , OrangeJuice , Coffee =Ints('Water Tea Milk OrangeJuice Coffee ')
Kools , Chesterfield , OldGold , LuckyStrike , Parliament =Ints('Kools Chesterfield
OldGold LuckyStrike Parliament ')
Fox , Horse , Snails , Dog , Zebra =Ints('Fox Horse Snails Dog Zebra ')
s = Solver ()
# so are beverages :
s.add( Distinct (Water , Tea , Milk , OrangeJuice , Coffee ))
# so are cigarettes :
s.add( Distinct (Kools , Chesterfield , OldGold , LuckyStrike , Parliament ))
# so are pets:
s.add( Distinct (Fox , Horse , Snails , Dog , Zebra ))
# 11. The man who smokes Chesterfields lives in the house next to the man with the fox
.
s.add(Or( Chesterfield == Fox +1, Chesterfield ==Fox -1)) # left or right
# 12. Kools are smoked in the house next to the house where the horse is kept.
s.add(Or( Kools == Horse +1, Kools == Horse -1)) # left or right
r=s. check ()
print (r)
if r== unsat :
exit (0)
m=s. model ()
print (m)
8.2.2 KLEE
We just define all variables and add constraints:
# include "klee.h"
int main ()
{
int Yellow , Blue , Red , Ivory , Green ;
242
int Norwegian , Ukrainian , Englishman , Spaniard , Japanese ;
int Water , Tea , Milk , OrangeJuice , Coffee ;
int Kools , Chesterfield , OldGold , LuckyStrike , Parliament ;
int Fox , Horse , Snails , Dog , Zebra ;
// limits .
if (Yellow <1 || Yellow >5) return 0;
if (Blue <1 || Blue >5) return 0;
if (Red <1 || Red >5) return 0;
if (Ivory <1 || Ivory >5) return 0;
if (Green <1 || Green >5) return 0;
// so are beverages :
if (((1 < < Water ) | (1<<Tea) | (1<< Milk) | (1<< OrangeJuice ) | (1<< Coffee ))!=0
x3E) return 0; // 111110
// so are cigarettes :
if (((1 < < Kools ) | (1<< Chesterfield ) | (1<< OldGold ) | (1<< LuckyStrike ) | (1<<
Parliament ))!=0 x3E) return 0; // 111110
// so are pets:
if (((1 < < Fox) | (1<< Horse ) | (1<< Snails ) | (1<<Dog) | (1<< Zebra ))!=0 x3E)
return 0; // 111110
// 11. The man who smokes Chesterfields lives in the house next to the man
with the fox.
if ( Chesterfield != Fox +1 && Chesterfield !=Fox -1) return 0; // left or right
// 12. Kools are smoked in the house next to the house where the horse is kept
.
if ( Kools != Horse +1 && Kools != Horse -1) return 0; // left or right
244
return 0;
};
I force KLEE to find distinct values for colors, nationalities, cigarettes, etc, in the same way as I did for Sudoku
earlier (8.1.1).
Let’s run it:
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ klee_zebra1 .c
...
It works for ≈ 7 seconds on my Intel Core i3-3110M 2.4GHz notebook. Let’s find out path, where klee_assert()
has been executed:
% ls klee -last /* err
klee -last/ test000037 . external .err
This is because KLEE found a test for each path, till each return.
How many return statement our program has, after all? Almost 43:
% cat klee_zebra1 .c| grep return | wc -l
45
int main ()
{
int Yellow , Blue , Red , Ivory , Green ;
int Norwegian , Ukrainian , Englishman , Spaniard , Japanese ;
int Water , Tea , Milk , OrangeJuice , Coffee ;
int Kools , Chesterfield , OldGold , LuckyStrike , Parliament ;
int Fox , Horse , Snails , Dog , Zebra ;
// limits .
klee_assume (Yellow >=1 && Yellow <=5);
klee_assume (Blue >=1 && Blue <=5);
klee_assume (Red >=1 && Red <=5);
klee_assume (Ivory >=1 && Ivory <=5);
klee_assume (Green >=1 && Green <=5);
// so are beverages :
klee_assume (((1 < < Water ) | (1<<Tea) | (1<< Milk) | (1<< OrangeJuice ) | (1<<
Coffee ))==0 x3E); // 111110
// so are cigarettes :
klee_assume (((1 < < Kools ) | (1<< Chesterfield ) | (1<< OldGold ) | (1<< LuckyStrike
) | (1<< Parliament ))==0 x3E); // 111110
// so are pets:
klee_assume (((1 < < Fox) | (1<< Horse ) | (1<< Snails ) | (1<<Dog) | (1<< Zebra ))==0
x3E); // 111110
247
// main constraints of the puzzle :
// 11. The man who smokes Chesterfields lives in the house next to the man
with the fox.
klee_assume ( Chesterfield == Fox +1 || Chesterfield ==Fox -1); // left or right
// 12. Kools are smoked in the house next to the house where the horse is kept
.
klee_assume ( Kools == Horse +1 || Kools == Horse -1); // left or right
% ls klee -last/
assembly .ll messages .txt run. stats test000001 . external .err test000001 .
ktest
248
info run. istats solver - queries .smt2 test000001 . kquery warnings .txt
…and this version works slightly faster (≈ 5 seconds). Faster, because KLEE has to find only one path, till the
end of the function. Hence, here is only one test generated.
8.2.3 CBMC
CBMC solution is simpler, we don’t define, which variables are subject to search.
# include <assert .h>
int check ()
{
int Yellow , Blue , Red , Ivory , Green ;
int Norwegian , Ukrainian , Englishman , Spaniard , Japanese ;
int Water , Tea , Milk , OrangeJuice , Coffee ;
int Kools , Chesterfield , OldGold , LuckyStrike , Parliament ;
int Fox , Horse , Snails , Dog , Zebra ;
// limits .
if (Yellow <1 || Yellow >5) return 0;
if (Blue <1 || Blue >5) return 0;
if (Red <1 || Red >5) return 0;
if (Ivory <1 || Ivory >5) return 0;
if (Green <1 || Green >5) return 0;
// so are beverages :
if (((1 < < Water ) | (1<<Tea) | (1<< Milk) | (1<< OrangeJuice ) | (1<< Coffee ))!=0
x3E) return 0; // 111110
249
// so are cigarettes :
if (((1 < < Kools ) | (1<< Chesterfield ) | (1<< OldGold ) | (1<< LuckyStrike ) | (1<<
Parliament ))!=0 x3E) return 0; // 111110
// so are pets:
if (((1 < < Fox) | (1<< Horse ) | (1<< Snails ) | (1<<Dog) | (1<< Zebra ))!=0 x3E)
return 0; // 111110
// 11. The man who smokes Chesterfields lives in the house next to the man
with the fox.
if ( Chesterfield != Fox +1 && Chesterfield !=Fox -1) return 0; // left or right
// 12. Kools are smoked in the house next to the house where the horse is kept
.
if ( Kools != Horse +1 && Kools != Horse -1) return 0; // left or right
return 0;
};
...
** Results :
CBMC_zebra .c function check
[ check . assertion .1] line 102 assertion 0: FAILURE
Violated property :
file CBMC_zebra .c function check line 102 thread 0
assertion 0
0 != 0
** 1 of 1 failed (2 iterations )
VERIFICATION FAILED
...
{1,1,1,1,0}->0,
{1 ,1 ,1 ,1 ,1} - >0}
...
# k=tuple : (" high - level " variable name , number of bit (0..4) )
# v= variable number in CNF
vars ={}
vars_last =1
...
add_popcnt1 (vars [( name ,0)], vars [( name ,1)], vars [( name ,2)], vars [( name ,3)],
vars [( name ,4) ])
...
alloc_distinct_variables ([" Yellow ", "Blue", "Red", " Ivory ", " Green "])
alloc_distinct_variables ([" Norwegian ", " Ukrainian ", " Englishman ", " Spaniard ", "
Japanese "])
alloc_distinct_variables ([" Water ", "Tea", "Milk", " OrangeJuice ", " Coffee "])
alloc_distinct_variables ([" Kools ", " Chesterfield ", " OldGold ", " LuckyStrike ", "
Parliament "])
alloc_distinct_variables ([" Fox", " Horse ", " Snails ", "Dog", " Zebra "])
...
Now we have 5 boolean variables for each high-level variable, and each group of variables will always have distinct
values.
Now let’s reread puzzle description: “2.The Englishman lives in the red house.”. That’s easy. In my Z3 and KLEE
examples I just wrote “Englishman==Red”. Same story here: we just add a clauses showing that 5 boolean variables
for “Englishman” must be equal to 5 booleans for “Red”.
On a lowest CNF level, if we want to say that two variables must be equal to each other, we add two clauses:
(var1 ∨ ¬var2) ∧ (¬var1 ∨ var2)
That means, both var1 and var2 values must be False or True, but they cannot be different.
def add_eq_clauses (var1 , var2):
global clauses
clauses . append (var1 + " -" + var2)
clauses . append (" -"+ var1 + " " + var2)
...
...
Now the next conditions: “9.Milk is drunk in the middle house.” (i.e., 3rd house), “10.The Norwegian lives in the
first house.” We can just assign boolean values directly:
# n=1..5
def add_eq_var_n (name , n):
global clauses
global vars
for i in range (5):
254
if i==n -1:
clauses . append (vars [( name ,i)]) # always True
else:
clauses . append (" -"+ vars [( name ,i)]) # always False
...
There is no “0 0 0 0 1” for “Ivory”, because it cannot be the last one. Now I can convert these conditions to CNF
using Wolfram Mathematica:
In []:= BooleanConvert [( a1 && !b1 &&! c1 &&! d1 &&! e1 &&! a2 && b2 &&! c2 &&! d2 &&! e2) ||
(!a1&& b1 &&! c1 &&! d1 &&! e1 &&! a2 && !b2 && c2 &&! d2 &&! e2) ||
(!a1&& !b1 &&c1 &&! d1 &&! e1 &&! a2 && !b2 &&! c2 && d2 &&! e2) ||
(!a1&& !b1 &&! c1 && d1 &&! e1 &&! a2 && !b2 &&! c2 &&! d2 && e2) ,"CNF "]
Out []= (!a1 ||! b1)&&(! a1 ||! c1)&&(! a1 ||! d1)&&( a1 ||b1 || c1 || d1)&&! a2 &&(! b1 ||! b2)&&(! b1 ||!
c1)&&
(!b1 ||! d1)&&( b1 || b2 || c1 || d1)&&(! b2 ||! c1)&&(! b2 ||! c2)&&(! b2 ||! d1)&&(! b2 ||! d2)&&(! b2 ||!
e2)&&
(b2 ||c1 ||c2||d1)&&( b2 || c2 || d1 || d2)&&( b2 || c2 || d2 || e2)&&(! c1 ||! c2)&&(! c1 ||! d1)&&(! c2 ||!
d1)&&
(!c2 ||! d2)&&(! c2 ||! e2)&&(! d1 ||! d2)&&(! d2 ||! e2)&&! e1
...
What we will do with that? “11.The man who smokes Chesterfields lives in the house next to the man with the
fox.” “12.Kools are smoked in the house next to the house where the horse is kept.”
We don’t know side, left or right, but we know that they are differ in one. Here is a clauses I would add:
Chesterfield Fox
AND (0 0 0 0 1 0 0 0 1 0)
.. OR ..
AND (0 0 0 1 0 0 0 0 0 1)
AND (0 0 0 1 0 0 0 1 0 0)
.. OR ..
AND (0 0 1 0 0 0 1 0 0 0)
AND (0 0 1 0 0 0 0 0 1 0)
.. OR ..
AND (0 1 0 0 0 1 0 0 0 0)
AND (0 1 0 0 0 0 0 1 0 0)
.. OR ..
AND (1 0 0 0 0 0 1 0 0 0)
(!a1&& b1 &&! c1 &&! d1 &&! e1 && a2 && !b2 &&! c2 &&! d2 &&! e2) ||
(!a1&& b1 &&! c1 &&! d1 &&! e1 &&! a2 && !b2 && c2 &&! d2 &&! e2) ||
(!a1&& !b1 &&c1 &&! d1 &&! e1 &&! a2 && b2 &&! c2 &&! d2 &&! e2) ||
(!a1&& !b1 &&c1 &&! d1 &&! e1 &&! a2 && !b2 &&! c2 && d2 &&! e2) ||
(!a1&& !b1 &&! c1 && d1 &&! e1 &&! a2 && !b2 && c2 &&! d2 &&! e2) ||
(!a1&& !b1 &&! c1 && d1 &&! e1 &&! a2 && !b2 &&! c2 &&! d2 && e2) ||
(!a1&& !b1 &&! c1 &&! d1 && e1 &&! a2 && !b2 &&! c2 && d2 &&! e2) ,"CNF "]
Out []= (!a1 ||! b1)&&(! a1 ||! c1)&&(! a1 ||! d1)&&(! a1 ||! e1)&&( a1 || b1 || c1 || d1 || e1)&&(! a2 || b1
)&&(! a2 ||! b2)&&
(!a2 ||! c2)&&(! a2 ||! d2)&&(! a2 ||! e2)&&( a2 || b2 || c1 || c2 || d1 || e1)&&( a2 || b2 || c2 || d1 || d2)&&(
a2||b2|| c2||d2 || e2)&&
(!b1 ||! b2)&&(! b1 ||! c1)&&(! b1 ||! d1)&&(! b1 ||! e1)&&( b1 || b2 || c1 || d1 || e1)&&(! b2 ||! c2)&&(!
b2 ||! d1)&&(! b2 ||! d2)&&
(!b2 ||! e1)&&(! b2 ||! e2)&&(! c1 ||! c2)&&(! c1 ||! d1)&&(! c1 ||! e1)&&(! c2 ||! d2)&&(! c2 ||! e1)
&&(! c2 ||! e2)&&
(!d1 ||! d2)&&(! d1 ||! e1)&&(! d2 ||! e2)
...
# 11. The man who smokes Chesterfields lives in the house next to the man with the fox
.
add_right_or_left (" Chesterfield "," Fox ") # left or right
# 12. Kools are smoked in the house next to the house where the horse is kept.
add_right_or_left (" Kools "," Horse ") # left or right
…and solved:
8.3.1 Generation
First, we need to generate it. Here is my quick idea on it. Take 8*16 array of cells. Each cell may contain some type
of block. There are joints between cells:
258
vjoints[…, 10]
vjoints[…, 11]
vjoints[…, 12]
vjoints[…, 13]
vjoints[…, 14]
vjoints[…, 15]
vjoints[…, 0]
vjoints[…, 1]
vjoints[…, 2]
vjoints[…, 3]
vjoints[…, 4]
vjoints[…, 5]
vjoints[…, 6]
vjoints[…, 7]
vjoints[…, 8]
vjoints[…, 9]
hjoints[0, …]
hjoints[1, …]
hjoints[2, …]
hjoints[3, …]
hjoints[4, …]
hjoints[5, …]
hjoints[6, …]
hjoints[7, …]
Blue lines are horizontal joints, red lines are vertical joints. We just set each joint to 0/false (absent) or 1/true
(present), randomly.
Once set, it’s now easy to find type for each cell. There are:
Dangling joints can be preset at a first stage (i.e., cell with only one joint), but they are removed recursively, these
cells are transforming into empty cells. Hence, at the end, all cells has at least two joints, and the whole plumbing
system has no connections with outer world—I hope this would make things clearer.
The C source code of generator is here: https://smt.st/current_tree/puzzles/pipe/generator. All horizontal
joints are stored in the global array hjoints[] and vertical in vjoints[].
The C program generates ANSI-colored output like it has been showed above (8.3, 8.4) plus an array of types, with
no angle information about each cell:
[
["0" , "0", "2b", "3" , "2a", "2a", "2a", "3", "3", "2a", "3", "2b", "2b", "2b", "0", "0"] ,
["2b", "2b", "3", "2b", "0" , "0", "2b", "3", "3", "3", "3", "3", "4", "2b", "0", "0"] ,
["3" , "4", "2b", "0" , "0" , "0", "3", "2b", "2b", "4" , "2b", "3", "4", "2b", "2b", "2b"],
["2b", "4", "3", "2a", "3" , "3", "3", "2b", "2b", "3" , "3", "3", "2a", "2b", "4", "3"] ,
["0" , "2b", "3", "2b", "3" , "4", "2b", "3", "3", "2b", "3", "3", "3", "0", "2a", "2a"],
["0" , "0", "2b", "2b", "0" , "3", "3", "4" , "3" , "4" , "3", "3", "3", "2b", "3", "3"] ,
["0" , "2b", "3", "2b", "0" , "3", "3", "4" , "3" , "4" , "4", "3", "0", "3", "4" , "3"] ,
["0" , "2b", "3", "3" , "2a", "3", "2b", "2b", "3", "3", "3", "3", "2a", "3", "3", "2b"],
]
259
8.3.2 Solving
First of all, we would think about 8*16 array of cells, where each has four bits: “T” (top), “B” (bottom), “L” (left), “R”
(right). Each bit represents half of joint.
[…, 10]
[…, 11]
[…, 12]
[…, 13]
[…, 14]
[…, 15]
[…, 0]
[…, 1]
[…, 2]
[…, 3]
[…, 4]
[…, 5]
[…, 6]
[…, 7]
[…, 8]
[…, 9]
T T T T T T T T T T T T T T T T
[0, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[1, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[2, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[3, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[4, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[5, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[6, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
T T T T T T T T T T T T T T T T
[7, …] L RL RL RL RL RL RL RL RL RL RL RL RL RL RL RL R
B B B B B B B B B B B B B B B B
We know that if each of half-joints is present, corresponding half-joint must be also present, and vice versa. We
define this using these constraints:
# shorthand variables for True and False :
t=True
f= False
# "top" of each cell must be equal to " bottom " of the cell above
# " bottom " of each cell must be equal to "top" of the cell below
# "left" of each cell must be equal to " right " of the cell at left
# "right" of each cell must be equal to "left" of the cell at right
for r in range( HEIGHT ):
for c in range ( WIDTH ):
if r!=0:
s.add(T[r][c]==B[r -1][c])
if r!= HEIGHT -1:
s.add(B[r][c]==T[r+1][c])
if c!=0:
s.add(L[r][c]==R[r][c -1])
if c!= WIDTH -1:
s.add(R[r][c]==L[r][c+1])
# "left" of each cell of first column shouldn 't have any connection
# so is " right " of each cell of the last column
for r in range( HEIGHT ):
s.add(L[r ][0]== f)
s.add(R[r][ WIDTH -1]== f)
260
# "top" of each cell of the first row shouldn 't have any connection
# so is " bottom " of each cell of the last row
for c in range( WIDTH ):
s.add(T[0][c]==f)
s.add(B[HEIGHT -1][c]==f)
Now we’ll enumerate all cells in the initial array (8.3.1). First two cells are empty there. And the third one has
type “2b”. This is “ ” and it can be oriented in 4 possible ways. And if it has angle 0◦ , bottom and right half-joints
are present, others are absent. If it has angle 90◦ , it looks like “ ”, and bottom and left half-joints are present, others
are absent.
In plain English: “if cell of this type has angle 0◦ , these half-joints must be present OR if it has angle 90◦ , these
half-joints must be present, OR, etc, etc.”
Likewise, we define all these rules for all types and all possible angles:
for r in range( HEIGHT ):
for c in range (WIDTH ):
ty= cells_type [r][c]
if ty =="0":
s.add(A[r][c]==f)
s.add(T[r][c]==f, B[r][c]==f, L[r][c]==f, R[r][c]==f)
if ty =="2a":
s.add(Or(And(A[r][c]==0 , L[r][c]==f, R[r][c]==f, T[r][c]==t, B[r][c]==t), #
And(A[r][c]==90 , L[r][c]==t, R[r][c]==t, T[r][c]==f, B[r][c]==f))) #
if ty =="2b":
s.add(Or(And(A[r][c]==0 , L[r][c]==f, R[r][c]==t, T[r][c]==f, B[r][c]==t), #
And(A[r][c]==90 , L[r][c]==t, R[r][c]==f, T[r][c]==f, B[r][c]==t), #
And(A[r][c]==180 , L[r][c]==t, R[r][c]==f, T[r][c]==t, B[r][c]==f), #
And(A[r][c]==270 , L[r][c]==f, R[r][c]==t, T[r][c]==t, B[r][c]==f))) #
if ty =="3":
s.add(Or(And(A[r][c]==0 , L[r][c]==f, R[r][c]==t, T[r][c]==t, B[r][c]==t), #
And(A[r][c]==90 , L[r][c]==t, R[r][c]==t, T[r][c]==f, B[r][c]==t), #
And(A[r][c]==180 , L[r][c]==t, R[r][c]==f, T[r][c]==t, B[r][c]==t), #
And(A[r][c]==270 , L[r][c]==t, R[r][c]==t, T[r][c]==t, B[r][c]==f))) #
if ty =="4":
s.add(A[r][c ]==0)
s.add(T[r][c]==t, B[r][c]==t, L[r][c]==t, R[r][c]==t) #
It worked ≈ 4 seconds on my old and slow Intel Atom N455 1.66GHz. Is it fast? I don’t know, but again, what
is really cool, we do not know about any mathematical background of all this, we just defined cells, (half-)joints and
defined relations between them.
Now the next question is, how many solutions are possible? Using method described earlier (3.17), I’ve altered
solver script 7 and solver said two solutions are possible.
Let’s compare these two solutions using gvimdiff:
Figure 8.6: gvimdiff output (pardon my red cursor at left pane at left-top corner)
4 cells in the middle can be orientated differently. Perhaps, other puzzles may produce different results.
P.S. Half-joint is defined as boolean type. But in fact, the first version of the solver has been written using integer
type for half-joints, and 0 was used for False and 1 for True. I did it so because I wanted to make source code tidier and
narrower without using long words like “False” and “True”. And it worked, but slower. Perhaps, Z3 handles boolean
data types faster? Better? Anyway, I writing this to note that integer type can also be used instead of boolean, if
needed.
7 https://smt.st/current_tree/puzzles/pipe/solver/solve_pipe_puzzle2.py
262
8.4 Eight queens problem (SAT)
Eight queens is a very popular problem and often used for measuring performance of SAT solvers. The problem is to
place 8 queens on chess board so they will not attack each other. For example:
| | | |*| | | | |
| | | | | | |*| |
| | | | |*| | | |
| |*| | | | | | |
| | | | | |*| | |
|*| | | | | | | |
| | |*| | | | | |
| | | | | | | |*|
8.4.1 make_one_hot
One important function we will (often) use is make_one_hot. This is a function which returns True if one single of
inputs is True and others are False. It will return False otherwise.
In my other examples, I’ve used Wolfram Mathematica to generate CNF clauses for it, for example: 3.10.2. What
expression Mathematica offers as make_one_hot function with 8 inputs?
(!a||!b)&&(!a||!c)&&(!a||!d)&&(!a||!e)&&(!a||!f)&&(!a||!g)&&(!a||!h)&&(a||b||c||d||e
||f||g||h)&&
(!b||!c)&&(!b||!d)&&(!b||!e)&&(!b||!f)&&(!b||!g)&&(!b||!h)&&(!c||!d)&&(!c||!e)&&(!c
||!f)&&(!c||!g)&&
(!c||!h)&&(!d||!e)&&(!d||!f)&&(!d||!g)&&(!d||!h)&&(!e||!f)&&(!e||!g)&&(!e||!h)&&(!f
||!g)&&(!f||!h)&&(!g||!h)
We can clearly see that the expression consisting of all possible variable pairs (negated) plus enumeration of all
variables (non-negated). In plain English terms, this means: “no pair can be equal to two True’s AND at least one
True must be present among all variables”.
This is how it works: if two variables will be True, negated they will be both False, and this clause will not be
evaluated to True, which is our ultimate goal. If one of variables is True, both negated will produce one True and one
False (fine). If both variables are False, both negated will produce two True’s (again, fine).
Here is how I can generate clauses for the function using itertools module from Python, which provides many
important functions from combinatorics:
# naive/ pairwise encoding
def AtMost1 (self , lst):
for pair in itertools . combinations (lst , r=2):
self. add_clause ([ self.neg(pair [0]) , self.neg(pair [1]) ])
AtMost1() function enumerates all possible pairs using itertools function combinations().
make_one_hot() function does the same, but also adds a final clause, which forces at least one True variable to be
present.
What clauses will be generated for 5 variables (1..5)?
p cnf 5 11
-2 -5 0
-2 -4 0
-4 -5 0
-2 -3 0
-1 -4 0
-1 -5 0
-1 -2 0
-1 -3 0
-3 -4 0
263
-3 -5 0
1 2 3 4 5 0
Yes, these are all possible pairs of 1..5 numbers + all 5 numbers.
We can get all solutions using Picosat:
% picosat --all popcnt1 .cnf
s SATISFIABLE
v -1 -2 -3 -4 5 0
s SATISFIABLE
v -1 -2 -3 4 -5 0
s SATISFIABLE
v -1 -2 3 -4 -5 0
s SATISFIABLE
v -1 2 -3 -4 -5 0
s SATISFIABLE
v 1 -2 -3 -4 -5 0
s SOLUTIONS 5
• zero or one queen must be present at each diagonal (empty diagonals can be present in valid solution).
These rules can be translated like that:
• make_one_hot(each row)==True
• make_one_hot(each column)==True
• AtMost1(each diagonal)==True
Now all we need is to enumerate rows, columns and diagonals and gather all clauses:
#!/ usr/bin/env python3
SIZE =8
SKIP_SYMMETRIES =True
# SKIP_SYMMETRIES =False
# print solution :
print (" solution number ", sol_n , ":")
# print 2D array :
for row in range (SIZE):
tmp =[([" ", "*"][ solution_as_2D_bool_array [row ][ col ]]+"|") for col in
range (SIZE)]
print ("|"+"".join(tmp))
265
# add 2D array as negated constraint :
add_2D_array_as_negated_constraint (s, solution_as_2D_bool_array )
sol_n =sol_n +1
main ()
( https://smt.st/current_tree/puzzles/8queens/8queens.py )
Perhaps, gen_diagonal() function is not very aesthetically appealing: it enumerates also subdiagonals of already
enumerated longer diagonals. To prevent presence of duplicate clauses, clauses global variable is not a list, rather set,
which allows only unique data to be present there.
Also, I’ve used AtMost1 for each column, this will help to produce slightly lower number of clauses. Each column
will have a queen anyway, this is implied from the first rule (make_one_hot for each row).
After running, we got CNF file with 64 variables and 736 clauses (https://smt.st/current_tree/puzzles/
8queens/8queens.cnf). Here is one solution:
% python 8 queens .py
len( clauses )= 736
| | | |*| | | | |
| | | | | | |*| |
| | | | |*| | | |
| |*| | | | | | |
| | | | | |*| | |
|*| | | | | | | |
| | |*| | | | | |
| | | | | | | |*|
How many possible solutions are there? Picosat tells 92, which is indeed correct number of solutions (https:
//oeis.org/A000170).
Performance of Picosat is not impressive, probably because it has to output all the solutions. It took 34 seconds on
my ancient Intel Atom 1.66GHz netbook to enumerate all solutions for 11 · 11 chess board (2680 solutions), which is
way slower than my straight brute-force program: https://yurichev.com/blog/8queens/. Nevertheless, it’s lighting
fast (as other SAT solvers) in finding first solution.
The SAT instance is also small enough to be easily solved by my simplest possible backtracking SAT solver: 23.1.
8.5.1 Intro
First, a bit of terminology. There are 6 colors we have: white, green, blue, orange, red, yellow. We also have 6 sides:
front, up, down, left, right, back.
This is how we will name all facelets:
U1 U2
U3 U4
-------
L1 L2 | F1 F2 | R1 R2 | B1 B2
L3 L4 | F3 F4 | R3 R4 | B3 B4
-------
D1 D2
D3 D4
There are 6 possible turns: front, left, right, back, up, down. But each turn can be clockwise, counterclockwise
and half-turn (equal to two CW or two CCW). Each CW is equal to 3 CCW and vice versa. Hence, there are 6*3=18
possible turns.
It is known, that 11 turns (including half-turns) are enough to solve any pocket cube (God’s algorithm). This
means, graph has a diameter of 11. For 3*3*3 cube one need 20 turns (http://www.cube20.org/). See also: https:
//en.wikipedia.org/wiki/Rubik%27s_Cube_group.
8.5.2 Z3
There are 6 sides and 4 facelets on each, hence, 6*4=24 variables we need to define a state.
Then we define how state is transformed after each possible turn:
FACE_F , FACE_U , FACE_D , FACE_R , FACE_L , FACE_B = 0,1,2,3,4,5
...
Then we define a function, which takes turn number and transforms a state:
# op is turn number
def rotate (turn , state , face , facelet ):
return If(op ==0 , rotate_FCW ( state )[face ][ facelet ],
If(op ==1 , rotate_FCCW ( state )[face ][ facelet ],
If(op ==2 , rotate_UCW ( state )[face ][ facelet ],
If(op ==3 , rotate_UCCW ( state )[face ][ facelet ],
If(op ==4 , rotate_DCW ( state )[face ][ facelet ],
...
# 4
init_F , init_U , init_D , init_R , init_L , init_B = set_current_state ({" FACE_F ":" RYOG", "
FACE_U ":" YRGO", " FACE_D ":" WRBO", " FACE_R ":" GYWB", " FACE_L ":" BYWG", " FACE_B ":" BOWR
"})
...
s= Solver ()
state =[[[ Int(' state %d_%d_%d' % (n, side , i)) for i in range ( FACELETS )] for side
in range( FACES )] for n in range ( TURNS +1)]
268
op=[ Int('op%d' % n) for n in range ( TURNS +1)]
# solved state
for face in range ( FACES):
for facelet in range ( FACELETS ):
s.add( state [ TURNS ][ face ][ facelet ]== face)
# turns:
for turn in range ( TURNS):
for face in range ( FACES ):
for facelet in range ( FACELETS ):
s.add( state [turn +1][ face ][ facelet ]== rotate (op[turn], state [turn],
face , facelet ))
if s. check () == sat:
print "sat"
m=s. model ()
for turn in range ( TURNS ):
print move_names [int(str(m[op[turn ]]))]
exit (0)
...but very slow. It takes up to 1 hours to find a path of 8 turns, which is not enough, we need 11.
Nevertheless, I decided to include Z3 solver as a demonstration.
You set all turns and the device ”calculates” final state.
269
Each ”blk” can be consisted of 24 multiplexers (MUX), each for each facelet. Each MUX is controlled by 5-bit
command (turn number). Depending on command, MUX takes 3-bit color from a facelet from a previous state.
Here is a table: the first column is a ”destination” facelet, then a list of ”source” facelets for each turn:
# dst , FCW FH FCCW UCW UH UCCW DCW DH DCCW RCW RH RCCW LCW LH
LCCW BCW BH BCCW
add_r ("F1" ,[" F3","F4","F2","R1","B1","L1","F1","F1","F1","F1","F1","F1","U1","B4
","D1","F1","F1","F1 "])
add_r ("F2" ,[" F1","F3","F4","R2","B2","L2","F2","F2","F2","D2","B3","U2","F2","F2
","F2","F2","F2","F2 "])
add_r ("F3" ,[" F4","F2","F1","F3","F3","F3","L3","B3","R3","F3","F3","F3","U3","B2
","D3","F3","F3","F3 "])
add_r ("F4" ,[" F2","F1","F3","F4","F4","F4","L4","B4","R4","D4","B1","U4","F4","F4
","F4","F4","F4","F4 "])
add_r ("U1" ,[" U1","U1","U1","U3","U4","U2","U1","U1","U1","U1","U1","U1","B4","D1
","F1","R2","D4","L3 "])
add_r ("U2" ,[" U2","U2","U2","U1","U3","U4","U2","U2","U2","F2","D2","B3","U2","U2
","U2","R4","D3","L1 "])
add_r ("U3" ,[" L4","D2","R1","U4","U2","U1","U3","U3","U3","U3","U3","U3","B2","D3
","F3","U3","U3","U3 "])
add_r ("U4" ,[" L2","D1","R3","U2","U1","U3","U4","U4","U4","F4","D4","B1","U4","U4
","U4","U4","U4","U4 "])
add_r ("D1" ,[" R3","U4","L2","D1","D1","D1","D3","D4","D2","D1","D1","D1","F1","U1
","B4","D1","D1","D1 "])
add_r ("D2" ,[" R1","U3","L4","D2","D2","D2","D1","D3","D4","B3","U2","F2","D2","D2
","D2","D2","D2","D2 "])
add_r ("D3" ,[" D3","D3","D3","D3","D3","D3","D4","D2","D1","D3","D3","D3","F3","U3
","B2","L1","U2","R4 "])
add_r ("D4" ,[" D4","D4","D4","D4","D4","D4","D2","D1","D3","B1","U4","F4","D4","D4
","D4","L3","U1","R2 "])
add_r ("R1" ,[" U3","L4","D2","B1","L1","F1","R1","R1","R1","R3","R4","R2","R1","R1
","R1","R1","R1","R1 "])
add_r ("R2" ,[" R2","R2","R2","B2","L2","F2","R2","R2","R2","R1","R3","R4","R2","R2
","R2","D4","L3","U1 "])
add_r ("R3" ,[" U4","L2","D1","R3","R3","R3","F3","L3","B3","R4","R2","R1","R3","R3
","R3","R3","R3","R3 "])
add_r ("R4" ,[" R4","R4","R4","R4","R4","R4","F4","L4","B4","R2","R1","R3","R4","R4
","R4","D3","L1","U2 "])
add_r ("L1" ,[" L1","L1","L1","F1","R1","B1","L1","L1","L1","L1","L1","L1","L3","L4
","L2","U2","R4","D3 "])
add_r ("L2" ,[" D1","R3","U4","F2","R2","B2","L2","L2","L2","L2","L2","L2","L1","L3
","L4","L2","L2","L2 "])
add_r ("L3" ,[" L3","L3","L3","L3","L3","L3","B3","R3","F3","L3","L3","L3","L4","L2
","L1","U1","R2","D4 "])
add_r ("L4" ,[" D2","R1","U3","L4","L4","L4","B4","R4","F4","L4","L4","L4","L2","L1
","L3","L4","L4","L4 "])
add_r ("B1" ,[" B1","B1","B1","L1","F1","R1","B1","B1","B1","U4","F4","D4","B1","B1
","B1","B3","B4","B2 "])
add_r ("B2" ,[" B2","B2","B2","L2","F2","R2","B2","B2","B2","B2","B2","B2","D3","F3
","U3","B1","B3","B4 "])
add_r ("B3" ,[" B3","B3","B3","B3","B3","B3","R3","F3","L3","U2","F2","D2","B3","B3
","B3","B4","B2","B1 "])
add_r ("B4" ,[" B4","B4","B4","B4","B4","B4","R4","F4","L4","B4","B4","B4","D1","F1
","U1","B2","B1","B3 "])
Each MUX has 32 inputs, each has width of 3 bits: colors from ”source” facelets. It has 3-bit output (color for
”destination” facelet). It has 5-bit selector, for 18 turns. Other selector values (32-18=14 values) are not used at all.
The whole problem is to build a circuit and then ask SAT solver to set ”switches” to such a state, when input and
output are determined (by us).
Now the problem is to represent MUX in CNF terms.
270
8
From EE courses we can remember about a simple if-then-else (ITE) gate, it takes 3 inputs (”selector”, ”true” and
”false”) and it has 1 output. Depending on ”selector” input it ”connects” output with ”true” or ”false” input. Using tree
of ITE gates we first can build 32-to-1 MUX, then wide 32*3-to-3 MUX.
I once have written small utility to search for shortest possible CNF formula for a specific function, in a bruteforce
manner (https://smt.st/current_tree/puzzles/rubik2/SAT/XOR_CNF_bf.c). It was inspired by ”aha! hacker
assistant” by Henry Warren. So here is a function:
bool func(bool v[ VARIABLES ])
{
// ITE:
bool tmp;
if (v [0]==0)
tmp=v[1];
else
tmp=v[2];
1st variable is ”select”, 2nd is ”false”, 3rd is ”true”, 4th is ”output”. ”output” is an additional variable, added just
like in Tseitin transformations.
Hence, CNF formula is:
(! select OR true OR ! output ) AND ( select OR false OR ! output ) AND (! select OR !true
OR output ) AND ( select OR ! false OR output )
It assures that the ”output” will always be equal to one of inputs depending on ”select”.
Now we can build a tree of ITE gates:
def create_ITE (s,f,t):
x= create_var ()
return x
This is my old MUX I wrote for 16 inputs and 4-bit selector, but you’ve got the idea: this is 4-tier tree. It has 15
ITE gates or 15*4=60 clauses.
Now the question, is it possible to make it smaller? I’ve tried to use Mathematica. First I’ve built truth table for
4-bit selector:
...
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 0 1 1 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 0 1 1 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 0 0 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 0 0 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 0 1 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 0 1 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 1 0 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 1 0 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 1 1 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 0 0 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 0 1 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 0 1 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 1 0 0 0
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 1 0 1 1
1 1 1 1 1 1 1 1 1 1 0 1 0 1 1 0 0 0 1 1 0 0
...
First 4 bits is selector, then 16 bits of input. Then the possible output and the bit, indicating, if the output bit
equals to one of the inputs.
Then I load this table to Mathematica and make CNF expression out of truth table:
arr= Import ["/ home/ dennis /P/ Rubik /TT"," Table "]
{{0 ,0 ,0 ,0,0 ,0,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0 ,0} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,1 ,1} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0 ,0 ,1} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0 ,1 ,0} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,0} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,1 ,1 ,1} ,
{0,0,0,0,0,0 ,0,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1} ,
...2097135... ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,0 ,0} ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,1 ,1} ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,1 ,0 ,0} ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,1 ,1 ,1} ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,0} ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,1 ,1} ,
{1,1,1,1,1,1 ,1,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0} ,
{1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1}}
272
BooleanConvert [ BooleanFunction [TT ,{s3 ,s2 ,s1 ,s0 , i15 ,i14 ,i13 ,i12 ,i11 ,i10 ,i9 ,i8 ,i7 ,i6 ,
i5 ,i4 ,i3 , i2 , i1 , i0 ,x}] ," CNF "]
(!i0||s0||s1 ||s2 || s3 ||x)&&( i0 || s0 || s1 || s2 || s3 ||!x)&&(! i1 ||! s0 || s1 || s2 || s3 ||x)&&( i1 ||!
s0||s1|| s2||s3 ||!x)&&(! i10 || s0 ||! s1 || s2 ||! s3 ||x)&&
(i10 ||s0 ||! s1 || s2 ||! s3 ||!x)&&(! i11 ||! s0 ||! s1 || s2 ||! s3 ||x)&&( i11 ||! s0 ||! s1 || s2 ||! s3 ||!
x)&&(! i12 || s0 || s1 ||! s2 ||! s3 ||x)&&( i12 || s0 || s1 ||! s2 ||! s3 ||!x)&&
(! i13 ||! s0|| s1 ||! s2 ||! s3 ||x)&&( i13 ||! s0 || s1 ||! s2 ||! s3 ||!x)&&(! i14 || s0 ||! s1 ||! s2 ||! s3
||x)&&( i14 || s0 ||! s1 ||! s2 ||! s3 ||!x)&&
(! i15 ||! s0 ||! s1 ||! s2 ||! s3 ||x)&&( i15 ||! s0 ||! s1 ||! s2 ||! s3 ||!x)&&(! i2 || s0 ||! s1 || s2 || s3 ||
x)&&( i2||s0 ||! s1 || s2 || s3 ||!x)&&(! i3 ||! s0 ||! s1|| s2 || s3 ||x)&&
(i3 ||! s0 ||! s1 || s2 || s3 ||!x)&&(! i4 || s0 || s1 ||! s2 || s3 ||x)&&( i4 || s0 || s1 ||! s2 || s3 ||!x)&&(!
i5 ||! s0||s1 ||! s2 || s3 ||x)&&( i5 ||! s0 || s1 ||! s2 ||s3 ||!x)&&
(!i6||s0 ||! s1 ||! s2 || s3 ||x)&&( i6 || s0 ||! s1 ||! s2 || s3 ||!x)&&(! i7 ||! s0 ||! s1 ||! s2 || s3 ||x)
&&( i7 ||! s0 ||! s1 ||! s2 || s3 ||!x)&&(! i8 || s0 || s1 ||s2 ||! s3 ||x)&&
(i8 ||s0 ||s1||s2 ||! s3 ||!x)&&(! i9 ||! s0 || s1 || s2 ||! s3 ||x)&&( i9 ||! s0 || s1 || s2 ||! s3 ||!x)
...
It has simple structure and there are 32 clauses, against 60 in my previous attempt. Will it work faster? No, as
my experience shows, it doesn’t speed up anything. Anyway, I used the latter idea to make MUX.
The following function makes pack of MUXes for each state, based on what I’ve got from Mathematica:
def create_MUX (self , ins , sels):
assert 2** len(sels)== len(ins)
x=self. create_var ()
273
for sel in range (len(ins)): # 32 for 5-bit selector
tmp =[ self. neg_if ((sel >>i)&1==1 , sels[i]) for i in range (len(sels))] # 5
for 5-bit selector
# padding : there are 18 used MUX inputs , so add 32 -18=14 for padding
for i in range (32 -18):
src_vectors . append ([t,t,t])
...
# dst , FCW FH FCCW UCW UH UCCW DCW DH DCCW RCW RH RCCW LCW LH
LCCW BCW BH BCCW
add_r ("F1" ,["F3","F4","F2","R1","B1","L1","F1","F1","F1","F1","F1","F1","U1","B4"
,"D1","F1","F1","F1"])
add_r ("F2" ,["F1","F3","F4","R2","B2","L2","F2","F2","F2","D2","B3","U2","F2","F2"
,"F2","F2","F2","F2"])
add_r ("F3" ,["F4","F2","F1","F3","F3","F3","L3","B3","R3","F3","F3","F3","U3","B2"
,"D3","F3","F3","F3"])
add_r ("F4" ,["F2","F1","F3","F4","F4","F4","L4","B4","R4","D4","B1","U4","F4","F4"
,"F4","F4","F4","F4"])
add_r ("U1" ,["U1","U1","U1","U3","U4","U2","U1","U1","U1","U1","U1","U1","B4","D1"
,"F1","R2","D4","L3"])
...
TURNS= 11
sat!
RCW
DH
274
BCW
UCW
UH
DCCW
FH
BH
BH
FH
UH
TURNS= 10
sat!
RCCW
UCCW
UCW
RCW
RCW
UH
FCW
UCCW
DCCW
DH
TURNS= 9
sat!
BCCW
LH
RH
BCW
RCW
RCCW
DCW
FH
LCW
TURNS= 8
sat!
RCW
UH
BCW
UCCW
LCW
LH
RCW
FCW
TURNS= 7
sat!
DCCW
DH
UH
UCW
BCW
UH
RCW
TURNS= 6
sat!
RCW
UCCW
DCCW
275
LCW
UH
DH
TURNS= 5
sat!
LCW
FCW
BCW
RH
LCCW
TURNS= 4
sat!
RCW
DCW
UCW
RCW
TURNS= 3
unsat!
TURNS= 11
sat!
UCW
BCW
LCCW
BH
DCW
RH
DCCW
FCW
UH
LCW
RCW
TURNS= 10
sat!
RH
BCCW
LCCW
RH
BCCW
LH
BCW
DH
BCW
LCCW
...
from z3 import *
FACES =6
FACELETS =9
...
move_names =[" FCW", "FCCW", "UCW", "UCCW", "DCW", "DCCW", "RCW", "RCCW", "LCW", "LCCW
", "BCW", "BCCW", "FH", "UH", "DH", "RH", "LH", "BH "]
init_F , init_U , init_D , init_R , init_L , init_B = set_current_state ({"F ":".... W..W.", "U
":"... W...W.", "D ":"....... W.", "R ":".. W...W.." , "L ":"...... W.." , "B ":".. W
......"})
s= Solver ()
state =[[[ Bool(' state %d_%d_%d' % (n, side , i)) for i in range ( FACELETS )] for
side in range ( FACES )] for n in range ( STEPS +1)]
# initial state
for i in range ( FACELETS ):
s.add( state [0][0][ i]== init_F [i])
s.add( state [0][1][ i]== init_U [i])
s.add( state [0][2][ i]== init_D [i])
s.add( state [0][3][ i]== init_R [i])
s.add( state [0][4][ i]== init_L [i])
s.add( state [0][5][ i]== init_B [i])
if s.check () == sat:
print "sat"
m=s. model ()
for n in range ( STEPS ):
print move_names [int(str(m[op[n]]))]
279
exit (0)
Now the fun statistics. Using random walk I collected 928 states and then I tried to solve one (white/front) face
for each state.
1 turns = 4
5 turns = 5
57 turns= 6
307 turns= 7
501 turns= 8
56 turns= 9
1 turns = 10
It seems that majority of all states can be solved for 7-8 half turns (half-turn is one of 18 turns we used here). But
there is at least one state which must be solved with 10 half turns. Maybe 10 is a ”god’s number” for one face, like 20
for all 6 faces?
8.8 Numberlink
8.8.1 Numberlink (AKA Flow Free) puzzle (Z3Py)
You probably saw Flow Free puzzle:
Figure 8.9
I’ll stick to Numberlink version of the puzzle. This is the example puzzle from Wikipedia:
280
Figure 8.10
This is solved:
Figure 8.11
from z3 import *
puzzle=[" 4 ",
" 3 25 ",
" 31 ",
" 5 ",
" ",
" 1 ",
"2 4 "]
width=len(puzzle[0])
height=len(puzzle)
# connections between cells. L means the cell has connection with cell at left, etc:
L=[[Bool('L_r%d_c%d' % (r,c)) for c in range(width)] for r in range(height)]
R=[[Bool('R_r%d_c%d' % (r,c)) for c in range(width)] for r in range(height)]
U=[[Bool('U_r%d_c%d' % (r,c)) for c in range(width)] for r in range(height)]
D=[[Bool('D_r%d_c%d' % (r,c)) for c in range(width)] for r in range(height)]
s=Solver()
# yes, I know, we have 4 bools for each cell at this point, and we can half this number,
# but anyway, for the sake of simplicity, this could be better.
for r in range(height):
for c in range(width):
t=puzzle[r][c]
if t==' ':
# puzzle has space, so degree=2, IOW, this cell must have 2 connections, no more,
no less.
# enumerate all possible L/R/U/D booleans. two of them must be True, others are
False.
t=[]
t.append(And(L[r][c], R[r][c], Not(U[r][c]), Not(D[r][c])))
t.append(And(L[r][c], Not(R[r][c]), U[r][c], Not(D[r][c])))
t.append(And(L[r][c], Not(R[r][c]), Not(U[r][c]), D[r][c]))
t.append(And(Not(L[r][c]), R[r][c], U[r][c], Not(D[r][c])))
t.append(And(Not(L[r][c]), R[r][c], Not(U[r][c]), D[r][c]))
t.append(And(Not(L[r][c]), Not(R[r][c]), U[r][c], D[r][c]))
s.add(Or(*t))
else:
# puzzle has number, add it to cells[][] as a constraint:
s.add(cells[r][c]==int(t))
# cell has degree=1, IOW, this cell must have 1 connection, no more, no less
# enumerate all possible ways:
t=[]
t.append(And(L[r][c], Not(R[r][c]), Not(U[r][c]), Not(D[r][c])))
t.append(And(Not(L[r][c]), R[r][c], Not(U[r][c]), Not(D[r][c])))
t.append(And(Not(L[r][c]), Not(R[r][c]), U[r][c], Not(D[r][c])))
t.append(And(Not(L[r][c]), Not(R[r][c]), Not(U[r][c]), D[r][c]))
s.add(Or(*t))
# if L[][]==True, cell's number must be equal to the number of cell at left, etc:
if c!=0:
s.add(If(L[r][c], cells[r][c]==cells[r][c-1], True))
if c!=width-1:
s.add(If(R[r][c], cells[r][c]==cells[r][c+1], True))
if r!=0:
s.add(If(U[r][c], cells[r][c]==cells[r-1][c], True))
if r!=height-1:
s.add(If(D[r][c], cells[r][c]==cells[r+1][c], True))
for c in range(width):
s.add(U[0][c]==False)
s.add(D[height-1][c]==False)
# print solution:
print (s.check())
m=s.model()
print ("")
for r in range(height):
for c in range(width):
282
print (m[cells[r][c]],end=' ')
print ("")
print ("")
for r in range(height):
for c in range(width):
t=""
t=t+("L" if str(m[L[r][c]])=="True" else " ")
t=t+("R" if str(m[R[r][c]])=="True" else " ")
t=t+("U" if str(m[U[r][c]])=="True" else " ")
t=t+("D" if str(m[D[r][c]])=="True" else " ")
print (t+"|",end=' ')
print ("")
print ("")
for r in range(height):
row=""
for c in range(width):
t=puzzle[r][c]
if t==' ':
tl=(True if str(m[L[r][c]])=="True" else False)
tr=(True if str(m[R[r][c]])=="True" else False)
tu=(True if str(m[U[r][c]])=="True" else False)
td=(True if str(m[D[r][c]])=="True" else False)
if tu and td:
row=row+"┃"
if tr and td:
row=row+"┏"
if tr and tu:
row=row+"┗"
if tl and td:
row=row+"┓"
if tl and tu:
row=row+"┛"
if tl and tr:
row=row+"━"
else:
row=row+t
print (row)
The solution:
sat
2 2 2 4 4 4 4
2 3 2 2 2 5 4
2 3 3 3 1 5 4
2 5 5 5 1 5 4
2 5 1 1 1 5 4
2 5 1 5 5 5 4
2 5 5 5 4 4 4
R D| LR | L D| R | LR | LR | L D|
UD| D| RU | LR | L | D| UD|
UD| RU | LR | L | D| UD| UD|
UD| R D| LR | L | UD| UD| UD|
UD| UD| R D| LR | L U | UD| UD|
UD| UD| U | R D| LR | L U | UD|
U | RU | LR | L U | R | LR | L U |
283
8.8.2 Numberlink (AKA Flow Free) puzzle as a MaxSAT problem + toy PCB router
Let’s revisit my solution for Numberlink (AKA Flow Free) puzzle written for Z3Py.
What if holes in the puzzle exist? Can we make all paths as short as possible?
I’ve rewritten the puzzle solver using my own SAT library and now I use Open-WBO MaxSAT solver, see the
source code, which is almost the same: https://smt.st/current_tree/puzzles/numberlink/MaxSAT/numberlink_
WBO.py.
But now we “maximize” number of empty cells:
Figure 8.13
This is a solution with shortest possible paths. Others are possible, but their sum wouldn’t be shorter. This is like
toy PCB routing.
What if we comment the s.fix_soft_always_true(cell_is_empty[r][c], 1) line and set maxsat=True?
284
Figure 8.14
Lines 2 and 3 “roaming” chaotically, but the solution is correct, under given constraints.
The files: https://smt.st/current_tree/puzzles/numberlink/MaxSAT.
With the use of three different weights, namely 1 lb., 3 lb., and 9 lb., how many objects of different
weights can be weighed, if the objects is to be weighed and the given weights may be placed in either
pan of the scale? (A) 15 (B) 13 (C) 11 (D) 9 (E) 7
This is fun!
from z3 import *
s= Solver ()
The Arithmetica which inspired Fermat was a Latin translation made by Claude Gaspar Bachet
de Méziriac, reputedly the most learned man in all of France. As well as being a brilliant linguist,
poet and classics scholar, Bachet had a passion for mathematical puzzles. His first publication was
a compilation of puzzles entitled Problemes plaisans et délectables qui se font par les nombres, which
included river-crossing problems, a liquid-pouring problem and several think-of-a-number tricks. One
of the questions posed was a problem about weights:
What is the least number of weights that can be used on a set of scales to weigh any
whole number of kilograms from 1 to 40
Bachet had a cunning solution which shows that it is possible to achieve this task with only four
weights...
...
In order to weigh any whole number of kilograms from 1 to 40 most people will suggest that
six weights are required: 1, 2, 4, 8, 16, 32 kg. In this way, all the weights can easily be achieved by
placing the following combinations in one pan:
1 kg = 1,
2 kg = 2,
3 kg = 2 + 1,
4 kg = 4,
5 kg = 4 + 1,
...
40 kg = 32 + 8.
However, by placing weights in both pans, such that weights are also allowed to sit alongside
the object being weighed, Bachet could complete the task with only four weights: 1, 3, 9, 27 kg. A
weight placed in the same pan as the object being weighed effectively assumes a negative value. Thus,
the weights can be achieved as follows:
1 kg = 1,
2 kg = 3 - 1,
3 kg = 3,
4 kg = 3 + 1,
5 kg = 9 - 3 - 1,
...
40 kg = 27 + 9 + 3 + 1.
287
8.10 Alphametics
According to Donald Knuth, the term “Alphametics” was coined by J. A. H. Hunter. This is a puzzle: what decimal
digits in 0..9 range must be assigned to each letter, so the following equation will be true?
SEND
+ MORE
-----
MONEY
# SEND+MORE= MONEY
D, E, M, N, O, R, S, Y = Ints('D, E, M, N, O, R, S, Y')
s= Solver ()
s.add (1000* S +100* E+10*N+D + 1000* M+100* O+10*R+E == 10000* M +1000* O+100* N+10*E+Y)
Output:
sat
[E, = 5,
S, = 9,
M, = 1,
N, = 6,
D, = 7,
R, = 8,
O, = 0,
Y = 2]
A, I, L, N, O, R, S, T, V = Ints('A, I, L, N, O, R, S, T, V')
s= Solver ()
sat
[L, = 6,
S, = 7,
N, = 2,
T, = 1,
I, = 5,
V = 3,
A, = 8,
R, = 9,
O, = 4,
TRIO = 1954 ,
SONATA , = 742818 ,
VIOLA , = 35468 ,
VIOLIN , = 354652]
# H+E+L+L+O = W+O+R+L+D = 25
s= Solver ()
s.add(H+E+L+L+O == 25)
s.add(W+O+R+L+D == 25)
9 http://pysmt.readthedocs.io/en/latest/getting_started.html
289
sat
[D = 5, R = 4, O = 3, E = 8, L = 6, W = 7, H = 2]
This is an exercise Q209 from the [Companion to the Papers of Donald Knuth]10 .
KNIFE
FORK
SPOON
SOUP
------
SUPPER
E, F, I, K, N, O, P, R, S, U = Ints('E F I K N O P R S U')
s= Solver ()
#s.add(S!=0)
KNIFE , FORK , SPOON , SOUP , SUPPER = Ints('KNIFE FORK SPOON SOUP SUPPER ')
10 http://www-cs-faculty.stanford.edu/~knuth/cp.html
290
sat
[K = 7,
N = 4,
R = 9,
I = 1,
E = 6,
S = 0,
O = 3,
F = 5,
U = 8,
P = 2,
SUPPER = 82269 ,
SOUP = 382 ,
SPOON = 2334 ,
FORK = 5397 ,
KNIFE = 74156]
S is zero, so SUPPER value is starting with leading (removed) zero. Let’s say, we don’t like it. Add this to resolve
it:
s.add(S!=0)
sat
[K = 8,
N = 4,
R = 3,
I = 7,
E = 6,
S = 1,
O = 9,
F = 2,
U = 0,
P = 5,
SUPPER = 105563 ,
SOUP = 1905 ,
SPOON = 15994 ,
FORK = 2938 ,
KNIFE = 84726]
... but. How hard it is just to bruteforce? Not so hard: 10! = 3628800 possible ways to assign 10 digits to 10
characters.
See also: Cryptarithmetic Puzzle Solver.
There are 210 = 1024 possible 10-letter strings in which each letter is either an A or a B. Find the
number of such strings that do not have more than 3 adjacent letters that are identical.
We just find all 10-bit numbers, which don’t have 4-bit runs of zeros or ones:
from z3 import *
291
a = BitVec ('a', 10)
s= Solver ()
results =[]
while True:
if s. check () == sat:
m = s. model ()
print "0x%x" % m[a]. as_long ()
It’s 548.
If the innocent told the truth and the guilty lied , who is guilty ? ( Remember that
false statements imply anything ).
I think Ed and Ted are innocent and Fred is guilty . Is it in contradiction with
statement 2.
( https://math.stackexchange.com/questions/15199/implication-of-three-statements )
And how to convert this into logic statements:
Let us write the following propositions :
Fg means Fred is guilty , and Fi means Fred is innocent , Tg and Ti for Ted and Eg and
Ei for Ed.
1. Ed says: Fg ∧ Ti
2. Fred says: Eg → Tg
3. Ted says: Ti ∧ (Fg ∨ Eg)
We know that the guilty is lying and the innocent tells the truth .
...
from z3 import *
s= Solver ()
s.add(fg !=fi)
s.add(tg !=ti)
s.add(eg !=ei)
The result:
sat
[fg = False ,
ti = False ,
tg = True ,
eg = True ,
ei = False ,
fi = True]
(check -sat)
(get - model )
293
Again, it’s small enough to be solved by MK85:
$ MK85 --dump -internal - variables fred.smt2
sat
(model
(define -fun always_false () Bool false ) ; var_no =1
(define -fun always_true () Bool true) ; var_no =2
(define -fun fg () Bool false ) ; var_no =3
(define -fun fi () Bool true) ; var_no =4
(define -fun tg () Bool true) ; var_no =5
(define -fun ti () Bool false ) ; var_no =6
(define -fun eg () Bool true) ; var_no =7
(define -fun ei () Bool false ) ; var_no =8
(define -fun internal !1 () Bool true) ; var_no =9
(define -fun internal !2 () Bool false ) ; var_no =10
(define -fun internal !3 () Bool true) ; var_no =11
(define -fun internal !4 () Bool true) ; var_no =12
(define -fun internal !5 () Bool false ) ; var_no =13
(define -fun internal !6 () Bool true) ; var_no =14
(define -fun internal !7 () Bool true) ; var_no =15
(define -fun internal !8 () Bool false ) ; var_no =16
(define -fun internal !9 () Bool true) ; var_no =17
(define -fun internal !10 () Bool false ) ; var_no =18
(define -fun internal !11 () Bool false ) ; var_no =19
(define -fun internal !12 () Bool true) ; var_no =20
(define -fun internal !13 () Bool false ) ; var_no =21
(define -fun internal !14 () Bool true) ; var_no =22
(define -fun internal !15 () Bool false ) ; var_no =23
(define -fun internal !16 () Bool true) ; var_no =24
(define -fun internal !17 () Bool true) ; var_no =25
(define -fun internal !18 () Bool false ) ; var_no =26
(define -fun internal !19 () Bool false ) ; var_no =27
(define -fun internal !20 () Bool true) ; var_no =28
)
from z3 import *
s= Solver ()
s.add(a== And(b,c,d,e,f))
s.add(b== And(Not(c),Not(d),Not(e),Not(f)))
s.add(c== And(a,b))
s.add(d== Or(And(a,Not(b),Not(c)), And(Not(a),b,Not(c)), And(Not(a),Not(b),c)))
s.add(e== And(Not(a),Not(b),Not(c),Not(d)))
s.add(f== And(Not(a),Not(b),Not(c),Not(d), Not(e)))
The answer:
sat
[f = False ,
b = False ,
a = False ,
c = False ,
d = False ,
e = True]
(check -sat)
(get - model )
Now let’s have fun and see how my toy SMT solver tackles this example. What internal variables it creates?
$ ./ MK85 --dump -internal - variables mc.smt2
sat
(model
(define -fun always_false () Bool false ) ; var_no =1
(define -fun always_true () Bool true) ; var_no =2
(define -fun a () Bool false ) ; var_no =3
(define -fun b () Bool false ) ; var_no =4
(define -fun c () Bool false ) ; var_no =5
(define -fun d () Bool false ) ; var_no =6
(define -fun e () Bool true) ; var_no =7
(define -fun f () Bool false ) ; var_no =8
(define -fun internal !1 () Bool false ) ; var_no =9
(define -fun internal !2 () Bool false ) ; var_no =10
(define -fun internal !3 () Bool false ) ; var_no =11
(define -fun internal !4 () Bool false ) ; var_no =12
(define -fun internal !5 () Bool false ) ; var_no =13
(define -fun internal !6 () Bool true) ; var_no =14
(define -fun internal !7 () Bool true) ; var_no =15
(define -fun internal !8 () Bool true) ; var_no =16
(define -fun internal !9 () Bool true) ; var_no =17
(define -fun internal !10 () Bool false ) ; var_no =18
(define -fun internal !11 () Bool false ) ; var_no =19
(define -fun internal !12 () Bool true) ; var_no =20
(define -fun internal !13 () Bool false ) ; var_no =21
(define -fun internal !14 () Bool false ) ; var_no =22
(define -fun internal !15 () Bool true) ; var_no =23
(define -fun internal !16 () Bool false ) ; var_no =24
(define -fun internal !17 () Bool false ) ; var_no =25
(define -fun internal !18 () Bool true) ; var_no =26
(define -fun internal !19 () Bool true) ; var_no =27
(define -fun internal !20 () Bool false ) ; var_no =28
(define -fun internal !21 () Bool true) ; var_no =29
(define -fun internal !22 () Bool false ) ; var_no =30
(define -fun internal !23 () Bool true) ; var_no =31
(define -fun internal !24 () Bool false ) ; var_no =32
(define -fun internal !25 () Bool true) ; var_no =33
(define -fun internal !26 () Bool false ) ; var_no =34
(define -fun internal !27 () Bool false ) ; var_no =35
(define -fun internal !28 () Bool true) ; var_no =36
(define -fun internal !29 () Bool true) ; var_no =37
298
(define -fun internal !30 () Bool true) ; var_no =38
(define -fun internal !31 () Bool false ) ; var_no =39
(define -fun internal !32 () Bool false ) ; var_no =40
(define -fun internal !33 () Bool false ) ; var_no =41
(define -fun internal !34 () Bool true) ; var_no =42
(define -fun internal !35 () Bool true) ; var_no =43
(define -fun internal !36 () Bool true) ; var_no =44
(define -fun internal !37 () Bool true) ; var_no =45
(define -fun internal !38 () Bool true) ; var_no =46
(define -fun internal !39 () Bool true) ; var_no =47
(define -fun internal !40 () Bool true) ; var_no =48
(define -fun internal !41 () Bool true) ; var_no =49
(define -fun internal !42 () Bool false ) ; var_no =50
(define -fun internal !43 () Bool true) ; var_no =51
(define -fun internal !44 () Bool true) ; var_no =52
(define -fun internal !45 () Bool true) ; var_no =53
(define -fun internal !46 () Bool true) ; var_no =54
(define -fun internal !47 () Bool true) ; var_no =55
(define -fun internal !48 () Bool true) ; var_no =56
(define -fun internal !49 () Bool true) ; var_no =57
(define -fun internal !50 () Bool true) ; var_no =58
(define -fun internal !51 () Bool false ) ; var_no =59
(define -fun internal !52 () Bool false ) ; var_no =60
(define -fun internal !53 () Bool false ) ; var_no =61
(define -fun internal !54 () Bool true) ; var_no =62
)
What is in resulting CNF file to be fed into the external picosat SAT-solver?
p cnf 62 151
-1 0
2 0
c generate_AND id1=b id2=c var1 =4 var2 =5 out id= internal !1 out var =9
-4 -5 9 0
4 -9 0
5 -9 0
c generate_AND id1= internal !1 id2=d var1 =9 var2 =6 out id= internal !2 out var =10
-9 -6 10 0
9 -10 0
6 -10 0
c generate_AND id1= internal !2 id2=e var1 =10 var2 =7 out id= internal !3 out var =11
-10 -7 11 0
10 -11 0
7 -11 0
c generate_AND id1= internal !3 id2=f var1 =11 var2 =8 out id= internal !4 out var =12
-11 -8 12 0
11 -12 0
8 -12 0
c generate_EQ id1=a, id2= internal !4, var1 =3, var2 =12
c generate_XOR id1=a id2= internal !4 var1 =3 var2 =12 out id= internal !5 out var =13
-3 -12 -13 0
3 12 -13 0
3 -12 13 0
-3 12 13 0
c generate_NOT id= internal !5 var =13 , out id= internal !6 out var =14
-14 -13 0
14 13 0
c create_assert () id= internal !6 var =14
14 0
c generate_NOT id=c var =5, out id= internal !7 out var =15
-15 -5 0
15 5 0
299
c generate_NOT id=d var =6, out id= internal !8 out var =16
-16 -6 0
16 6 0
c generate_AND id1= internal !7 id2= internal !8 var1 =15 var2 =16 out id= internal !9 out
var =17
-15 -16 17 0
15 -17 0
16 -17 0
c generate_NOT id=e var =7, out id= internal !10 out var =18
-18 -7 0
18 7 0
c generate_AND id1= internal !9 id2= internal !10 var1 =17 var2 =18 out id= internal !11 out
var =19
-17 -18 19 0
17 -19 0
18 -19 0
c generate_NOT id=f var =8, out id= internal !12 out var =20
-20 -8 0
20 8 0
c generate_AND id1= internal !11 id2= internal !12 var1 =19 var2 =20 out id= internal !13 out
var =21
-19 -20 21 0
19 -21 0
20 -21 0
c generate_EQ id1=b, id2= internal !13 , var1 =4, var2 =21
c generate_XOR id1=b id2= internal !13 var1 =4 var2 =21 out id= internal !14 out var =22
-4 -21 -22 0
4 21 -22 0
4 -21 22 0
-4 21 22 0
c generate_NOT id= internal !14 var =22 , out id= internal !15 out var =23
-23 -22 0
23 22 0
c create_assert () id= internal !15 var =23
23 0
c generate_AND id1=a id2=b var1 =3 var2 =4 out id= internal !16 out var =24
-3 -4 24 0
3 -24 0
4 -24 0
c generate_EQ id1=c, id2= internal !16 , var1 =5, var2 =24
c generate_XOR id1=c id2= internal !16 var1 =5 var2 =24 out id= internal !17 out var =25
-5 -24 -25 0
5 24 -25 0
5 -24 25 0
-5 24 25 0
c generate_NOT id= internal !17 var =25 , out id= internal !18 out var =26
-26 -25 0
26 25 0
c create_assert () id= internal !18 var =26
26 0
c generate_NOT id=b var =4, out id= internal !19 out var =27
-27 -4 0
27 4 0
c generate_AND id1=a id2= internal !19 var1 =3 var2 =27 out id= internal !20 out var =28
-3 -27 28 0
3 -28 0
27 -28 0
c generate_NOT id=c var =5, out id= internal !21 out var =29
-29 -5 0
29 5 0
c generate_AND id1= internal !20 id2= internal !21 var1 =28 var2 =29 out id= internal !22 out
300
var =30
-28 -29 30 0
28 -30 0
29 -30 0
c generate_NOT id=a var =3, out id= internal !23 out var =31
-31 -3 0
31 3 0
c generate_AND id1= internal !23 id2=b var1 =31 var2 =4 out id= internal !24 out var =32
-31 -4 32 0
31 -32 0
4 -32 0
c generate_NOT id=c var =5, out id= internal !25 out var =33
-33 -5 0
33 5 0
c generate_AND id1= internal !24 id2= internal !25 var1 =32 var2 =33 out id= internal !26 out
var =34
-32 -33 34 0
32 -34 0
33 -34 0
c generate_OR id1= internal !22 id2= internal !26 var1 =30 var2 =34 out id= internal !27 out
var =35
30 34 -35 0
-30 35 0
-34 35 0
c generate_NOT id=a var =3, out id= internal !28 out var =36
-36 -3 0
36 3 0
c generate_NOT id=b var =4, out id= internal !29 out var =37
-37 -4 0
37 4 0
c generate_AND id1= internal !28 id2= internal !29 var1 =36 var2 =37 out id= internal !30 out
var =38
-36 -37 38 0
36 -38 0
37 -38 0
c generate_AND id1= internal !30 id2=c var1 =38 var2 =5 out id= internal !31 out var =39
-38 -5 39 0
38 -39 0
5 -39 0
c generate_OR id1= internal !27 id2= internal !31 var1 =35 var2 =39 out id= internal !32 out
var =40
35 39 -40 0
-35 40 0
-39 40 0
c generate_EQ id1=d, id2= internal !32 , var1 =6, var2 =40
c generate_XOR id1=d id2= internal !32 var1 =6 var2 =40 out id= internal !33 out var =41
-6 -40 -41 0
6 40 -41 0
6 -40 41 0
-6 40 41 0
c generate_NOT id= internal !33 var =41 , out id= internal !34 out var =42
-42 -41 0
42 41 0
c create_assert () id= internal !34 var =42
42 0
c generate_NOT id=a var =3, out id= internal !35 out var =43
-43 -3 0
43 3 0
c generate_NOT id=b var =4, out id= internal !36 out var =44
-44 -4 0
44 4 0
301
c generate_AND id1= internal !35 id2= internal !36 var1 =43 var2 =44 out id= internal !37 out
var =45
-43 -44 45 0
43 -45 0
44 -45 0
c generate_NOT id=c var =5, out id= internal !38 out var =46
-46 -5 0
46 5 0
c generate_AND id1= internal !37 id2= internal !38 var1 =45 var2 =46 out id= internal !39 out
var =47
-45 -46 47 0
45 -47 0
46 -47 0
c generate_NOT id=d var =6, out id= internal !40 out var =48
-48 -6 0
48 6 0
c generate_AND id1= internal !39 id2= internal !40 var1 =47 var2 =48 out id= internal !41 out
var =49
-47 -48 49 0
47 -49 0
48 -49 0
c generate_EQ id1=e, id2= internal !41 , var1 =7, var2 =49
c generate_XOR id1=e id2= internal !41 var1 =7 var2 =49 out id= internal !42 out var =50
-7 -49 -50 0
7 49 -50 0
7 -49 50 0
-7 49 50 0
c generate_NOT id= internal !42 var =50 , out id= internal !43 out var =51
-51 -50 0
51 50 0
c create_assert () id= internal !43 var =51
51 0
c generate_NOT id=a var =3, out id= internal !44 out var =52
-52 -3 0
52 3 0
c generate_NOT id=b var =4, out id= internal !45 out var =53
-53 -4 0
53 4 0
c generate_AND id1= internal !44 id2= internal !45 var1 =52 var2 =53 out id= internal !46 out
var =54
-52 -53 54 0
52 -54 0
53 -54 0
c generate_NOT id=c var =5, out id= internal !47 out var =55
-55 -5 0
55 5 0
c generate_AND id1= internal !46 id2= internal !47 var1 =54 var2 =55 out id= internal !48 out
var =56
-54 -55 56 0
54 -56 0
55 -56 0
c generate_NOT id=d var =6, out id= internal !49 out var =57
-57 -6 0
57 6 0
c generate_AND id1= internal !48 id2= internal !49 var1 =56 var2 =57 out id= internal !50 out
var =58
-56 -57 58 0
56 -58 0
57 -58 0
c generate_NOT id=e var =7, out id= internal !51 out var =59
-59 -7 0
302
59 7 0
c generate_AND id1= internal !50 id2= internal !51 var1 =58 var2 =59 out id= internal !52 out
var =60
-58 -59 60 0
58 -60 0
59 -60 0
c generate_EQ id1=f, id2= internal !52 , var1 =8, var2 =60
c generate_XOR id1=f id2= internal !52 var1 =8 var2 =60 out id= internal !53 out var =61
-8 -60 -61 0
8 60 -61 0
8 -60 61 0
-8 60 61 0
c generate_NOT id= internal !53 var =61 , out id= internal !54 out var =62
-62 -61 0
62 61 0
c create_assert () id= internal !54 var =62
62 0
Here are comments (starting with “c ” prefix), and my SMT-solver indicate, how each low-level logical gate is added,
its inputs (variable IDs and numbers) and outputs.
Let’s filter comments:
$ cat tmp.cnf | grep "^c "
c generate_AND id1=b id2=c var1 =4 var2 =5 out id= internal !1 out var =9
c generate_AND id1= internal !1 id2=d var1 =9 var2 =6 out id= internal !2 out var =10
c generate_AND id1= internal !2 id2=e var1 =10 var2 =7 out id= internal !3 out var =11
c generate_AND id1= internal !3 id2=f var1 =11 var2 =8 out id= internal !4 out var =12
c generate_EQ id1=a, id2= internal !4, var1 =3, var2 =12
c generate_XOR id1=a id2= internal !4 var1 =3 var2 =12 out id= internal !5 out var =13
c generate_NOT id= internal !5 var =13 , out id= internal !6 out var =14
c create_assert () id= internal !6 var =14
c generate_NOT id=c var =5, out id= internal !7 out var =15
c generate_NOT id=d var =6, out id= internal !8 out var =16
c generate_AND id1= internal !7 id2= internal !8 var1 =15 var2 =16 out id= internal !9 out
var =17
c generate_NOT id=e var =7, out id= internal !10 out var =18
c generate_AND id1= internal !9 id2= internal !10 var1 =17 var2 =18 out id= internal !11 out
var =19
c generate_NOT id=f var =8, out id= internal !12 out var =20
c generate_AND id1= internal !11 id2= internal !12 var1 =19 var2 =20 out id= internal !13 out
var =21
c generate_EQ id1=b, id2= internal !13 , var1 =4, var2 =21
c generate_XOR id1=b id2= internal !13 var1 =4 var2 =21 out id= internal !14 out var =22
c generate_NOT id= internal !14 var =22 , out id= internal !15 out var =23
c create_assert () id= internal !15 var =23
c generate_AND id1=a id2=b var1 =3 var2 =4 out id= internal !16 out var =24
c generate_EQ id1=c, id2= internal !16 , var1 =5, var2 =24
c generate_XOR id1=c id2= internal !16 var1 =5 var2 =24 out id= internal !17 out var =25
c generate_NOT id= internal !17 var =25 , out id= internal !18 out var =26
c create_assert () id= internal !18 var =26
c generate_NOT id=b var =4, out id= internal !19 out var =27
c generate_AND id1=a id2= internal !19 var1 =3 var2 =27 out id= internal !20 out var =28
c generate_NOT id=c var =5, out id= internal !21 out var =29
c generate_AND id1= internal !20 id2= internal !21 var1 =28 var2 =29 out id= internal !22 out
var =30
c generate_NOT id=a var =3, out id= internal !23 out var =31
c generate_AND id1= internal !23 id2=b var1 =31 var2 =4 out id= internal !24 out var =32
c generate_NOT id=c var =5, out id= internal !25 out var =33
c generate_AND id1= internal !24 id2= internal !25 var1 =32 var2 =33 out id= internal !26 out
var =34
c generate_OR id1= internal !22 id2= internal !26 var1 =30 var2 =34 out id= internal !27 out
303
var =35
c generate_NOT id=a var =3, out id= internal !28 out var =36
c generate_NOT id=b var =4, out id= internal !29 out var =37
c generate_AND id1= internal !28 id2= internal !29 var1 =36 var2 =37 out id= internal !30 out
var =38
c generate_AND id1= internal !30 id2=c var1 =38 var2 =5 out id= internal !31 out var =39
c generate_OR id1= internal !27 id2= internal !31 var1 =35 var2 =39 out id= internal !32 out
var =40
c generate_EQ id1=d, id2= internal !32 , var1 =6, var2 =40
c generate_XOR id1=d id2= internal !32 var1 =6 var2 =40 out id= internal !33 out var =41
c generate_NOT id= internal !33 var =41 , out id= internal !34 out var =42
c create_assert () id= internal !34 var =42
c generate_NOT id=a var =3, out id= internal !35 out var =43
c generate_NOT id=b var =4, out id= internal !36 out var =44
c generate_AND id1= internal !35 id2= internal !36 var1 =43 var2 =44 out id= internal !37 out
var =45
c generate_NOT id=c var =5, out id= internal !38 out var =46
c generate_AND id1= internal !37 id2= internal !38 var1 =45 var2 =46 out id= internal !39 out
var =47
c generate_NOT id=d var =6, out id= internal !40 out var =48
c generate_AND id1= internal !39 id2= internal !40 var1 =47 var2 =48 out id= internal !41 out
var =49
c generate_EQ id1=e, id2= internal !41 , var1 =7, var2 =49
c generate_XOR id1=e id2= internal !41 var1 =7 var2 =49 out id= internal !42 out var =50
c generate_NOT id= internal !42 var =50 , out id= internal !43 out var =51
c create_assert () id= internal !43 var =51
c generate_NOT id=a var =3, out id= internal !44 out var =52
c generate_NOT id=b var =4, out id= internal !45 out var =53
c generate_AND id1= internal !44 id2= internal !45 var1 =52 var2 =53 out id= internal !46 out
var =54
c generate_NOT id=c var =5, out id= internal !47 out var =55
c generate_AND id1= internal !46 id2= internal !47 var1 =54 var2 =55 out id= internal !48 out
var =56
c generate_NOT id=d var =6, out id= internal !49 out var =57
c generate_AND id1= internal !48 id2= internal !49 var1 =56 var2 =57 out id= internal !50 out
var =58
c generate_NOT id=e var =7, out id= internal !51 out var =59
c generate_AND id1= internal !50 id2= internal !51 var1 =58 var2 =59 out id= internal !52 out
var =60
c generate_EQ id1=f, id2= internal !52 , var1 =8, var2 =60
c generate_XOR id1=f id2= internal !52 var1 =8 var2 =60 out id= internal !53 out var =61
c generate_NOT id= internal !53 var =61 , out id= internal !54 out var =62
c create_assert () id= internal !54 var =62
Now you can juxtapose list of internal variables and comments in CNF file. For example, equality gate is generated
as NOT(XOR(a,b)).
create_assert() function fixes a bool variable to be always True.
Other (internal) variables are added by SMT solver as “joints” to connect logic gates with each other.
Hence, my SMT solver constructing a digital circuit based on the input SMT file. Logic gates are then converted
into CNF form using Tseitin transformations. The task of SAT solver is then to find such an assignment, for which
CNF expression would be true. In other words, its task is to find such inputs/outputs for which this “virtual” digital
circuit would work without contradictions.
The SAT instance is also small enough to be solved using my simplest backtracking SAT solver written:
$ ./ SAT_backtrack .py tmp.cnf
SAT
-1 2 -3 -4 -5 -6 7 -8 -9 -10 -11 -12 -13 14 15 16 17 -18 -19 20 -21 -22 23 -24 -25 26
27 -28 29 -30 31 -32 33 -34 -35 36 37 38 -39 -40 -41 42 43 44 45 46 47 48 49 -50
51 52 53 54 55 56 57 58 -59 -60 -61 62 0
UNSAT
solutions = 1
304
You can juxtapose variables from solver’s result and variable numbers from MK85 listing. Therefore, MK85 +
my small SAT solver is standalone program under 3000 SLOC, which still can solve such (simple enough) system of
boolean equations, without external aid like minisat/picosat.
Among comments at the John D. Cook’s blog, there is also a solution by Aaron Meurer, using SymPy, which also
has SAT-solver inside:
7 July 2015 at 01:34
In [2]: facts = [
Equivalent (a, (b & c & d & e & f)),
Equivalent (b, (∼c & ∼d & ∼e & ∼f)),
Equivalent (c, a & b),
Equivalent (d, ((a & ∼b & ∼c) | (∼a & b & ∼c) | (∼a & ∼b & c))),
Equivalent (e, (∼a & ∼b & ∼c & ∼d)),
Equivalent (f, (∼a & ∼b & ∼c & ∼d & ∼e)),
]
So it seems e is the only answer , assuming I got the facts correct . And it is
important to use Equivalent (a bidirectional implication ) rather than just Implies
. If you only use -> ( which I guess would mean that an answer not being chosen
’doesnt necessarily mean that it ’isnt true), then ‘’none , b, and f are also
“”solutions .
Also , if I replace the d fact with Equivalent (d, a | b | c), the result is the same.
So it seems that the interpretation of “”one both in terms of choice d and in
terms of how many choices there are is irrelevant .
Thanks for the fun problem . I hope others took the time to solve this in their head
before reading the comments .
The positive integers x1 , x2 , ..., x7 satisfy x6 = 144 and xn+3 = xn+2 (xn+1 + xn ) for n = 1, 2, 3, 4.
Find the last three digits of x7 .
This is it:
from z3 import *
s= Solver ()
x1 , x2 , x3 , x4 , x5 , x6 , x7=Ints('x1 x2 x3 x4 x5 x6 x7 ')
s.add(x1 >=0)
305
s.add(x2 >=0)
s.add(x3 >=0)
s.add(x4 >=0)
s.add(x5 >=0)
s.add(x6 >=0)
s.add(x7 >=0)
s.add(x6 ==144)
results =[]
while True:
if s. check () == sat:
m = s.model ()
print m
Find the number of positive integers with three not necessarily distinct digits, abc, with a ̸= 0 and
c ̸= 0 such that both abc and cba are multiples of 4.
306
from z3 import *
a, b, c = Ints('a b c')
s= Solver ()
s.add(a >0)
s.add(b >=0)
s.add(c >0)
s.add(a <=9)
s.add(b <=9)
s.add(c <=9)
results =[]
while True:
if s. check () == sat:
m = s.model ()
print m
Let’s see:
[c = 4, b = 0, a = 4]
[b = 1, c = 2, a = 2]
[b = 6, c = 4, a = 4]
[b = 4, c = 4, a = 4]
[b = 2, c = 4, a = 4]
[b = 4, c = 4, a = 8]
[b = 8, c = 4, a = 4]
[b = 6, c = 4, a = 8]
[b = 8, c = 4, a = 8]
[b = 0, c = 4, a = 8]
[b = 2, c = 4, a = 8]
[b = 8, c = 8, a = 8]
[b = 9, c = 6, a = 6]
[b = 2, c = 8, a = 8]
[b = 2, c = 8, a = 4]
[b = 4, c = 8, a = 4]
[b = 4, c = 8, a = 8]
[b = 8, c = 8, a = 4]
[b = 6, c = 8, a = 4]
[b = 6, c = 8, a = 8]
[b = 0, c = 8, a = 4]
[b = 0, c = 8, a = 8]
[b = 7, c = 6, a = 6]
[b = 7, c = 2, a = 6]
[b = 7, c = 6, a = 2]
[b = 7, c = 2, a = 2]
307
[b = 9, c = 2, a = 6]
[b = 9, c = 2, a = 2]
[b = 9, c = 6, a = 2]
[b = 5, c = 6, a = 2]
[b = 1, c = 6, a = 2]
[b = 3, c = 6, a = 2]
[b = 5, c = 2, a = 2]
[b = 3, c = 2, a = 2]
[b = 5, c = 6, a = 6]
[b = 5, c = 2, a = 6]
[b = 3, c = 2, a = 6]
[b = 1, c = 2, a = 6]
[b = 1, c = 6, a = 6]
[b = 3, c = 6, a = 6]
total results 40
; slower :
;( assert (= ( bvurem ( bvadd ( bvmul a (_ bv100 8)) ( bvmul b (_ bv00 8)) c) #x04) #x00))
;( assert (= ( bvurem ( bvadd ( bvmul c (_ bv100 8)) ( bvmul b (_ bv00 8)) a) #x04) #x00))
; faster :
( assert (= (bvand ( bvadd ( bvmul a (_ bv100 8)) ( bvmul b (_ bv00 8)) c) #x03) #x00))
( assert (= (bvand ( bvadd ( bvmul c (_ bv100 8)) ( bvmul b (_ bv00 8)) a) #x03) #x00))
;( check -sat)
;(get -model)
(get -all - models )
;( count - models )
Faster version doesn’t find remainder, it just isolates two last bits.
from z3 import *
"""
We will keep track on numbers using row/col representation :
|0 1 2 <-col
-|- - -
0|7 8 9
1|4 5 6
2|1 2 3
^
|
row
"""
s= Solver ()
# bottom - right corner is always under left - upper corner , or equal to it , or to the
right of it:
s.add(to_r >= from_r )
s.add(to_c >= from_c )
if prove :
# prove by finding counterexample .
# find any variable state for which remainder will be non -zero:
s.add(And (( n1 %11) != 0) , (n1 %111) != 0, (n1 %1111) != 0)
s.add(And (( n2 %11) != 0) , (n2 %111) != 0, (n2 %1111) != 0)
s.add(And (( n3 %11) != 0) , (n3 %111) != 0, (n3 %1111) != 0)
s.add(And (( n4 %11) != 0) , (n4 %111) != 0, (n4 %1111) != 0)
# ... or ...
if enumerate_rectangles :
# enumerate all possible solutions :
results =[]
while True:
if s. check () == sat:
m = s. model ()
# print_model (m)
print m
print m[n1]
print m[n2]
print m[n3]
print m[n4]
results . append (m)
block = []
for d in m:
c=d()
block . append (c != m[d])
s.add(Or( block ))
else:
print " results total =", len( results )
break
...
[n1 = 5522 ,
BL = 2,
n2 = 5225 ,
to_r = 2,
LT = 5,
RT = 5,
BR = 2,
n4 = 2552 ,
from_r = 1,
n3 = 2255 ,
from_c = 1,
to_c = 1]
5522
5225
2255
2552
results total= 36
Sample result:
spur stimulated
r e c i a h e
congratulations
m u t a i s c
violation niece
s a e p e n n
rector penitent
i i o c e
accounts herald
s n g e a r o
press edinburgh
e x e n t p i
characteristics
t c l r n e a
satisfying dull
horizontal :
((0 , 0), (0, 3)) spur
((0 , 5), (0, 14)) stimulated
((2 , 0), (2, 14)) congratulations
((4 , 0), (4, 8)) violation
((4 , 10) , (4, 14)) niece
((6 , 0), (6, 5)) rector
((6 , 7), (6, 14)) penitent
((8 , 0), (8, 7)) accounts
((8 , 9), (8, 14)) herald
((10 , 0), (10 , 4)) press
((10 , 6), (10 , 14)) edinburgh
((12 , 0), (12 , 14)) characteristics
((14 , 0), (14 , 9)) satisfying
((14 , 11) , (14, 14)) dull
vertical :
((8 , 0), (14, 0)) aspects
((0 , 1), (6, 1)) promise
((10 , 2), (14 , 2)) exact
((0 , 3), (10, 3)) regulations
((10 , 4), (14 , 4)) seals
((0 , 5), (9, 5)) scattering
((10 , 6), (14 , 6)) entry
((4 , 7), (10, 7)) opposed
((0 , 8), (4, 8)) milan
((5 , 9), (14, 9)) enchanting
((0 , 10) , (4, 10)) latin
((4 , 11) , (14, 11)) interrupted
((0 , 12) , (4, 12)) those
((8 , 13) , (14, 13)) logical
((0 , 14) , (6, 14)) descent
Unsat is possible if the dictionary is too small or have no words of length present in pattern.
312
Constructing a crossword puzzle by bruteforce is not feasible, especially in the presence of big dictionary.
The source code:
#!/ usr/bin/env python3
from z3 import *
import sys
"""
# https :// commons . wikimedia .org/wiki/File:Khachbar -1. jpg
pattern =[
"*****" ,
"* * *",
"* ***" ,
"*** *",
"* ***" ,
"* * *",
"*****"]
"""
"""
# https :// commons . wikimedia .org/wiki/File:Khachbar -4. jpg
pattern =[
"*******" ,
"* * * *",
"*******" ,
"* * * *",
"*******" ,
"* * * *",
"*******"]
"""
#"""
# https :// commons . wikimedia .org/wiki/File: British_crossword .svg
pattern =[
"**** ********** ",
" * * * * * * *",
" *************** ",
" * * * * * * *",
" ********* ***** ",
" * * * * * * *",
" ****** ******** ",
" * * * * * ",
" ******** ****** ",
"* * * * * * * ",
" ***** ********* ",
"* * * * * * * ",
" *************** ",
"* * * * * * * ",
" ********** ****"]
#"""
# scan pattern [] and find all " sticks " longer than 1 and collect its coordinates :
horizontal =[]
in_the_middle =False
for r in range ( HEIGHT ):
313
for c in range ( WIDTH ):
if pattern [r][c]== '*' and in_the_middle == False :
in_the_middle =True
start =(r,c)
elif pattern [r][c]== ' ' and in_the_middle == True:
if c-start [1] >1:
horizontal . append (( start , (r, c -1)))
in_the_middle = False
if in_the_middle :
if c- start [1] >1:
horizontal . append (( start , (r, c)))
in_the_middle = False
vertical =[]
in_the_middle =False
for c in range ( WIDTH ):
for r in range ( HEIGHT ):
if pattern [r][c]== '*' and in_the_middle == False :
in_the_middle =True
start =(r,c)
elif pattern [r][c]== ' ' and in_the_middle == True:
if r-start [0] >1:
vertical . append (( start , (r-1, c)))
in_the_middle = False
if in_the_middle :
if r- start [0] >1:
vertical . append (( start , (r, c)))
in_the_middle = False
# for the first simple pattern , we will have such coordinates of " sticks ":
# horizontal =[((0 , 0), (0, 4)), ((2 , 2) , (2, 4)), ((3 , 0) , (3, 2)), ((4 , 2) , (4, 4)),
((6, 0), (6, 4))]
# vertical =[((0 , 0) , (6, 0)), ((0 , 2) , (6, 2)), ((0 , 4), (6, 4))]
# the list in this file is assumed to not have duplicates , otherwise duplicates can
be present in the final resulting crossword :
with open("words .txt") as f:
content = f. readlines ()
words = [x.strip () for x in content ]
s= Solver ()
set_param (" parallel . enable ", True)
314
# this function takes coordinates , word length and word itself
# for " hello ", it returns array like:
# [chars [0][0]== ord('h '), chars [0][1]== ord('e '), chars [0][2]== ord('l '), chars [0][3]==
ord('l '), chars [0][4]== ord('o ')]
def form_H_expr (r, c, l, w):
return [ chars [r][c+i]== ord(w[i]) for i in range (l)]
# now we find all horizonal " sticks ", we find all possible words of corresponding
length ...
for i in range (len( horizontal )):
h= horizontal [i]
_from =h[0]
_to=h[1]
l=_to [1] - _from [1]+1
list_of_ANDs =[]
for idx , word in find_words_len (l):
# at this point , we form expression like:
# And(chars [0][0]== ord('h '), chars [0][1]== ord('e '), chars [0][2]== ord('l '),
chars [0][3]== ord('l '), chars [0][4]== ord('o '), horizontal_idx []==...)
list_of_ANDs . append (And( form_H_expr ( _from [0] , _from [1] , l, word)+[
horizontal_idx [i]== idx ]))
# at this point , we form expression like:
# Or(And( chars ...== word1 ), And( chars ...== word2 ), And( chars ...== word3 ))
s.add(Or (* list_of_ANDs ))
# we collected indices of horizonal / vertical words to make sure they will not be
duplicated on resulting crossword :
s.add( Distinct (*( horizontal_idx + vertical_idx )))
N=10
# get 10 results :
results =[]
for i in range (N):
if s. check () == sat:
m = s.model ()
print_model (m)
results . append (m)
block = []
for d in m:
c=d()
block . append (c != m[d])
s.add(Or(block ))
else:
print (" total results ", len( results ))
break
Fill ? with -, + or * operation, and find such a sequence, so that the equation would be true. This is tricky, because
of operator precedence, multiplication must be handled at first. Brackets are also possible.
See also: https://www.dcode.fr/missing-operators-equation-solver,
https://brilliant.org/wiki/arithmetic-puzzles-operator-search/.
To solve this, I just enumerate all possible ordered binary search tree of 9 elements, which are almost the same as
expression with 9 terms and all possible variations of brackets.
Then we try each expression...
#!/ usr/bin/env python3
from z3 import *
CONST =1234
TOTAL =9
# prepare a list in form: [(1 ,"1") ,(2 ,"2") ,(3 ,"3") ...]
# rationale : enum_ordered () yields both expression tree and expression as a string ...
input_values =[]
for i in range ( TOTAL ):
input_values . append ((i+1, str(i+1)))
OPS=TOTAL -1
# expression may have form like ( please note , many constants are optimized / folded ):
# If(op_1 == 0,
# 1 +
# If(op_0 == 0, 5, If(op_0 == 1, -1, If(op_0 == 2, 6, 0))),
# If(op_1 == 1,
# 1 -
# If(op_0 == 0,
# 5,
# If(op_0 == 1, -1, If(op_0 == 2, 6, 0))),
# If(op_1 == 2,
# 1*
# If(op_0 == 0,
# 5,
# If(op_0 == 1, -1, If(op_0 == 2, 6, 0))),
# 0)))
# string is like "(1 op1 (2 op0 3))", opX substring will be replaced by operation
name after (-, +, *)
For 9 terms, there are 1430 binary trees, or expressions (9th Catalan number).
For 0, there are 1391 solutions, some of them:
(1 + (2 + (3 + (4 + (5 * (6 - (7 - (8 - 9)))))))) = 0
(1 + (2 + (3 + (4 + (5 * (6 - ((7 - 8) + 9))))))) = 0
(1 + (2 + (3 + (4 + (5 * ((6 - 7) + (8 - 9))))))) = 0
(1 + (2 + (3 - (4 + (5 - ((6 * (7 - 8)) + 9)))))) = 0
(1 + (2 + (3 + (4 + (5 * (((6 - 7) + 8) - 9)))))) = 0
(1 - (2 + (3 + (4 + ((5 - 6) + (7 * (8 - 9))))))) = 0
(1 - (2 + (3 + (4 + ((5 - 6) * ((7 - 8) + 9)))))) = 0
317
...
Surely, several of these expressions are equivalent to each other, due to associative property of multiplication and
addition.
For 1234:
(1 * (2 * (((3 - (4 - ((5 + 6) * 7))) * 8) + 9))) = 1234
(1 + (2 + ((((3 * ((4 + 5) * 6)) - 7) * 8) - 9))) = 1234
(1 + (2 + (((((3 * (4 + 5)) * 6) - 7) * 8) - 9))) = 1234
(1 + ((2 + (3 * ((4 + 5) * (6 + (7 - 8))))) * 9)) = 1234
(1 + ((2 + (3 * ((4 + 5) * ((6 + 7) - 8)))) * 9)) = 1234
(1 + ((2 + (3 * ((4 + (5 - 6)) * (7 + 8)))) * 9)) = 1234
(1 + ((2 + (3 * (((4 + 5) - 6) * (7 + 8)))) * 9)) = 1234
(1 + ((2 + (3 * (((4 * (5 + 6)) - 7) + 8))) * 9)) = 1234
(1 + ((2 + ((3 * (4 + 5)) * (6 + (7 - 8)))) * 9)) = 1234
(1 - ((2 - (((3 + 4) * 5) + ((6 + 7) * 8))) * 9)) = 1234
(1 + ((2 + ((3 * (4 + (5 - 6))) * (7 + 8))) * 9)) = 1234
(1 + ((2 + ((3 * ((4 + 5) - 6)) * (7 + 8))) * 9)) = 1234
(1 + ((2 - (((3 * (4 - 5)) - 6) * (7 + 8))) * 9)) = 1234
(1 - ((2 + ((3 * ((4 - (5 + 6)) * 7)) + 8)) * 9)) = 1234
(1 - ((2 + ((3 * (((4 - 5) - 6) * 7)) + 8)) * 9)) = 1234
(1 - ((2 + (((3 * (4 - (5 + 6))) * 7) + 8)) * 9)) = 1234
(1 - ((2 + (((3 * ((4 - 5) - 6)) * 7) + 8)) * 9)) = 1234
(1 - ((2 - ((((3 * (4 + 5)) - 6) * 7) - 8)) * 9)) = 1234
(1 - (((2 - ((3 + 4) * 5)) - ((6 + 7) * 8)) * 9)) = 1234
(1 - (((2 - ((3 + (4 * 5)) * 6)) + (7 - 8)) * 9)) = 1234
(1 + (((2 + (3 - (4 * (5 - (6 * 7))))) * 8) + 9)) = 1234
(1 + (((2 + (3 + (4 * ((5 * 6) + 7)))) * 8) + 9)) = 1234
(1 - (((2 + (3 * ((4 - (5 + 6)) * 7))) + 8) * 9)) = 1234
(1 - (((2 + (3 * (((4 - 5) - 6) * 7))) + 8) * 9)) = 1234
(1 - (((2 + ((3 * (4 - (5 + 6))) * 7)) + 8) * 9)) = 1234
(1 - (((2 + ((3 * ((4 - 5) - 6)) * 7)) + 8) * 9)) = 1234
(1 - (((2 - (((3 + (4 * 5)) * 6) - 7)) - 8) * 9)) = 1234
(1 + ((((2 + 3) - (4 * (5 - (6 * 7)))) * 8) + 9)) = 1234
(1 - ((((2 - (3 * ((4 + 5) * 6))) + 7) * 8) - 9)) = 1234
(1 - ((((2 - ((3 + (4 * 5)) * 6)) + 7) - 8) * 9)) = 1234
((1 + 2) + ((((3 * ((4 + 5) * 6)) - 7) * 8) - 9)) = 1234
((1 + 2) + (((((3 * (4 + 5)) * 6) - 7) * 8) - 9)) = 1234
((1 + (2 + (((3 * ((4 + 5) * 6)) - 7) * 8))) - 9) = 1234
((1 + (2 + ((((3 * (4 + 5)) * 6) - 7) * 8))) - 9) = 1234
((1 + ((2 + (3 - (4 * (5 - (6 * 7))))) * 8)) + 9) = 1234
((1 + ((2 + (3 + (4 * ((5 * 6) + 7)))) * 8)) + 9) = 1234
((1 - ((2 - ((3 * ((4 + 5) * 6)) - 7)) * 8)) + 9) = 1234
((1 - ((2 - (((3 * (4 + 5)) * 6) - 7)) * 8)) + 9) = 1234
((1 + (((2 + 3) - (4 * (5 - (6 * 7)))) * 8)) + 9) = 1234
318
((1 - (((2 - (3 * ((4 + 5) * 6))) + 7) * 8)) + 9) = 1234
((1 - (((2 - ((3 * (4 + 5)) * 6)) + 7) * 8)) + 9) = 1234
(((1 + 2) + (((3 * ((4 + 5) * 6)) - 7) * 8)) - 9) = 1234
(((1 + 2) + ((((3 * (4 + 5)) * 6) - 7) * 8)) - 9) = 1234
n=-1
CONST =13
# TOTAL =9
TOTAL =7
BIT_WIDTH =8
input_values =[]
for i in range ( TOTAL ):
input_values . append ((s. BitVecConst (i+1, BIT_WIDTH ), str(i+1)))
OPS=TOTAL -1
if s. check ():
m=s. model ()
# print (" sat", tree [1])
tmp=tree [1]
for i in range (OPS):
op_s =["+", "-", "*"][m['op_%d' % i]]
319
tmp=tmp. replace ("op"+str(i), op_s)
print (tmp , "=", eval(tmp))
# show only first solution ...
exit (0)
WIDTH =len(cols)
HEIGHT =len(rows)
s= Solver ()
#
------------------------------------------------------------------------------------------
# ... or ...
The result:
sat
******** ******* ***** *******
***** **** *** ***
*** *** ** ***
**** *** ** **
*** *** ** **
*** **** ** **
**** ***** **
*** ***** *
**** *** **
*** **** **
**** **** **
*** ****** **
*** ** *** *
**** *** **** **
*** ** *** **
****** *****
**** *****
*** ***
*** ***
* *
How it works (briefly). Given a row of width 8 and input (or clue) like [3,2], we create two islands of two bitstrings
of corresponding lengths:
00000111
00000011
11100000
00001100
->
11101100
( von Nora Hartsfield, Gerhard Ringel – Pearls in Graph Theory: A Comprehensive Introduction )
There are only 3 ways to allocate food to kids. The problem is also small enough to be solved using my toy-level
MK85 SMT-solver...
(set - logic QF_BV )
(set -info :smt -lib - version 2.0)
; apples = 0
; bananas = 1
; cherries = 2
; dates = 3
( assert
(or
(= E (_ bv2 2))
(= E (_ bv3 2))
)
)
( assert
(or
(= F (_ bv0 2))
(= F (_ bv2 2))
)
)
( assert
324
(or
(= G (_ bv1 2))
(= G (_ bv2 2))
)
)
( assert
(or
(= H (_ bv0 2))
(= H (_ bv2 2))
(= H (_ bv3 2))
)
)
;( model
; (define -fun E () (_ BitVec 2) (_ bv2 2)) ; 0x2
; (define -fun F () (_ BitVec 2) (_ bv0 2)) ; 0x0
; (define -fun G () (_ BitVec 2) (_ bv1 2)) ; 0x1
; (define -fun H () (_ BitVec 2) (_ bv3 2)) ; 0x3
;)
;( model
; (define -fun E () (_ BitVec 2) (_ bv3 2)) ; 0x3
; (define -fun F () (_ BitVec 2) (_ bv2 2)) ; 0x2
; (define -fun G () (_ BitVec 2) (_ bv1 2)) ; 0x1
; (define -fun H () (_ BitVec 2) (_ bv0 2)) ; 0x0
;)
;( model
; (define -fun E () (_ BitVec 2) (_ bv3 2)) ; 0x3
; (define -fun F () (_ BitVec 2) (_ bv0 2)) ; 0x0
; (define -fun G () (_ BitVec 2) (_ bv1 2)) ; 0x1
; (define -fun H () (_ BitVec 2) (_ bv2 2)) ; 0x2
;)
; Model count : 3
# apples = 0
# bananas = 1
# cherries = 2
# dates = 3
E, F, G, H = Ints('E F G H')
s= Solver ()
[G = 1, F = 2, E = 3, H = 0]
[G = 2, F = 0, E = 3, H = 1]
[G = 1, F = 0, E = 2, H = 3]
results total = 3
"""
What is the minimum number of transfers of water between buckets ? The challenge is to
solve this as
a planning problem ( encoded into satisfiability or constraint satisfaction ) with an
efficiency approaching
(or exceeding ) a simple enumeration .
s= Solver ()
# " columns ": If(And(op ==... , preconditions ), And(what next state will have), ...)
for cur in range (STATES -1):
next=cur +1
s.add(If(And(op[cur ]==0 , A[cur]>0, B[cur]< jug_B ), And(B[next ]== Z3_min (jug_B ,
B[cur ]+A[cur ]) , A[next ]==A[cur ]-(B[next]-B[cur ]) , C[next ]==C[cur ]) ,
If(And(op[cur ]==1 , A[cur]>0, C[cur]<jug_C ), And(C[next ]== Z3_min (jug_C , C[
cur ]+A[cur ]) , A[next ]==A[cur ]-(C[next]-C[cur ]) , B[next ]==B[cur ]) ,
If(And(op[cur ]==2 , B[cur ] >0) , And(A[next ]==A[cur ]+B[cur], C[next ]==C[cur
], B[next ]==0) ,
If(And(op[cur ]==3 , B[cur]>0, C[cur]<jug_C ), And(A[next ]==A[cur], C[next
]== Z3_min (jug_C , C[cur ]+B[cur ]) , B[next ]==B[cur ]-(C[next]-C[cur ])),
If(And(op[cur ]==4 , C[cur ] >0) , And(A[next ]==A[cur ]+C[cur], B[next ]==B[cur
], C[next ]==0) ,
If(And(op[cur ]==5 , C[cur ] >0) , And(A[next ]==A[cur], B[next ]== Z3_min (jug_B ,
B[cur ]+C[cur ]) , C[next ]==C[cur ]-(B[next]-B[cur ])),
False )))))))
if s. check () == unsat :
return
m=s. model ()
#_try (7)
#exit (0)
state 0, 8-0-0
op_0 = A->B
state 1, 3-5-0
op_1 = B->C
state 2, 3-2-3
op_2 = C->A
state 3, 6-2-0
op_3 = B->C
state 4, 6-0-2
op_4 = A->B
327
state 5, 1-5-2
op_5 = B->C
state 6, 1-4-3
op_6 = C->A
state 7, 4-4-0
Graph coloring
borders ={" Albania ": [" Greece ", " Kosovo ", " Macedonia ", " Montenegro "],
" Andorra ": [" France ", "Spain "],
" Austria ": [" CzechRepublic ", " Germany ", " Hungary ", " Italy ", " Liechtenstein ", "
Slovakia ", " Slovenia ", " Switzerland "],
" Belarus ": [" Latvia ", " Lithuania ", " Poland ", " Ukraine "],
" Belgium ": [" France ", " Germany ", " Luxembourg ", " Netherlands "],
" BosniaHerzegovina ": [" Croatia ", " Montenegro ", " Serbia "],
" Bulgaria ": [" Greece ", " Macedonia ", " Romania ", " Serbia "],
" Croatia ": [" BosniaHerzegovina ", " Hungary ", " Montenegro ", " Serbia ", " Slovenia "],
" Cyprus ": [],
" CzechRepublic ": [" Austria ", " Germany ", " Poland ", " Slovakia "],
" Denmark ": [" Germany "],
" Estonia ": [" Latvia "],
" Finland ": [" Norway ", " Sweden "],
" France ": [" Andorra ", " Belgium ", " Germany ", "Italy ", " Luxembourg ", " Monaco ", " Spain ",
" Switzerland "],
" Germany ": [" Austria ", " Belgium ", " CzechRepublic ", " Denmark ", " France ", " Luxembourg ",
" Netherlands ", " Poland ", " Switzerland "],
" Greece ": [" Albania ", " Bulgaria ", " Macedonia "],
" Hungary ": [" Austria ", " Croatia ", " Romania ", " Serbia ", " Slovakia ", " Slovenia ", "
Ukraine "],
" Iceland ": [],
" Ireland ": [" UnitedKingdom "],
" Italy ": [" Austria ", " France ", " SanMarino ", " Slovenia ", " Switzerland ", " VaticanCity "
],
" Kosovo ": [" Albania ", " Macedonia ", " Montenegro ", " Serbia "],
" Latvia ": [" Belarus ", " Estonia ", " Lithuania "],
" Liechtenstein ": [" Austria ", " Switzerland "],
" Lithuania ": [" Belarus ", " Latvia ", " Poland "],
" Luxembourg ": [" Belgium ", " France ", " Germany "],
" Macedonia ": [" Albania ", " Bulgaria ", " Greece ", " Kosovo ", " Serbia "],
" Malta ": [],
" Moldova ": [" Romania ", " Ukraine "],
" Monaco ": [" France "],
328
329
" Montenegro ": [" Albania ", " BosniaHerzegovina ", " Croatia ", " Kosovo ", " Serbia "],
" Netherlands ": [" Belgium ", " Germany "],
" Norway ": [" Finland ", " Sweden "],
" Poland ": [" Belarus ", " CzechRepublic ", " Germany ", " Lithuania ", " Slovakia ", " Ukraine "
],
" Portugal ": ["Spain "],
" Romania ": [" Bulgaria ", " Hungary ", " Moldova ", " Serbia ", " Ukraine "],
" SanMarino ": [" Italy "],
" Serbia ": [" BosniaHerzegovina ", " Bulgaria ", " Croatia ", " Hungary ", " Kosovo ", "
Macedonia ", " Montenegro ", " Romania "],
" Slovakia ": [" Austria ", " CzechRepublic ", " Hungary ", " Poland ", " Ukraine "],
" Slovenia ": [" Austria ", " Croatia ", " Hungary ", " Italy "],
" Spain ": [" Andorra ", " France ", " Portugal "],
" Sweden ": [" Finland ", " Norway "],
" Switzerland ": [" Austria ", " France ", " Germany ", " Italy ", " Liechtenstein "],
" Ukraine ": [" Belarus ", " Hungary ", " Moldova ", " Poland ", " Romania ", " Slovakia "],
" UnitedKingdom ": [" Ireland "],
" VaticanCity ": ["Italy "]}
s= Solver ()
GeoGraphics [{ EdgeForm [
Directive [Thin ,
Black ]], { GeoStyling [#2] , Tooltip [ Polygon [#1] , #1[[2]]]} & @@@
coloring }]
331
...
s. maximize (Sum (*[ If( country_color [i]==0 , 1, 0) for i in range ( countries_total )]))
Suppose some people form committees to do various tasks. The problem is to schedule the com-
mittee meetings in as few time slots as possible. To simplify the discussion, we’ll represent each person
with a number. For example, let S = 1, 2, 3, 4, 5, 6, 7 represent a set of seven people, and suppose
they have formed six three-person committees as follows:
S1 = 1, 2, 3, S2 = 2, 3, 4, S3 = 3, 4, 5, S4 = 4, 5, 6, S5 = 5, 6, 7, S6 = 1, 6, 7.
We can model the problem with the graph pictured in Figure 1.4.4, where the committee names
are the vertices and an edge connects two vertices if a person belongs to both committees represented
by the vertices. If each committee meets for one hour, what is the smallest number of hours needed
for the committees to do their work? From the graph, it follows that an edge between two committees
means that they have at least one member in common. Thus, they cannot meet at the same time.
No edge between committees means that they can meet at the same time. For example, committees
S1 and S4 can meet the first hour. Then committees S2 and S5 can meet the second hour. Finally,
committees S3 and S6 can meet the third hour. Can you see why three hours is the smallest number
of hours that the six committees can meet?
import itertools
from z3 import *
# 7 peoples , 6 committees
S={}
S[1]= set ([1, 2, 3])
S[2]= set ([2, 3, 4])
S[3]= set ([3, 4, 5])
S[4]= set ([4, 5, 6])
S[5]= set ([5, 6, 7])
S[6]= set ([1, 6, 7])
committees =len(S)
s= Solver ()
schedule ={}
for t in schedule :
print ("hour:", t, " committees :", schedule [t])
The result:
hour: 0 committees : [1, 4]
hour: 1 committees : [2, 5]
hour: 2 committees : [3, 6]
Suppose that you are organizing housing accommodations for a group of four hundred university
students. Space is limited and only one hundred of the students will receive places in the dormitory.
To complicate matters, the Dean has provided you with a list of pairs of incompatible students, and
requested that no pair from this list appear in your final choice.
( https://www.claymath.org/millennium-problems/p-vs-np-problem )
What if we want to divide our community/company/university by groups. There are 16 persons and, which must
be divided by 4 groups, 4 persons in each. However, several persons hate each other, maybe, for personal reasons.
Can we group all them so the ”haters” would be separated?
#!/ usr/bin/env python3
336
from z3 import *
# 16 peoples , 4 groups
PERSONS =16
GROUPS =4
s= Solver ()
# each pair of persons can 't be in the same group , because they hate each other .
# IOW , we add an edge between vertices .
s.add( person [0] != person [7])
s.add( person [0] != person [8])
s.add( person [0] != person [9])
s.add( person [2] != person [9])
s.add( person [9] != person [14])
s.add( person [11] != person [15])
s.add( person [11] != person [1])
s.add( person [11] != person [2])
s.add( person [11] != person [9])
s.add( person [10] != person [1])
If( person_0 == g, 1, 0) +
If( person_1 == g, 1, 0) +
If( person_2 == g, 1, 0) +
...
If( person_15 == g, 1, 0)
"""
return Sum (*[ If( person [i]==g, 1, 0) for i in range ( PERSONS )])
groups ={}
for i in range ( PERSONS ):
g=m[ person [i]]. as_long ()
if g not in groups :
groups [g]=[]
groups [g]. append (i)
for g in groups :
print (" group %d, persons :" % g, groups [g])
The result:
337
group 0, persons : [1, 2, 5, 8]
group 1, persons : [4, 7, 9, 12]
group 2, persons : [0, 3, 11, 13]
group 3, persons : [6, 10, 14, 15]
int64_t T [1024];
if ( needle_size ==0)
return haystack ;
// free(T);
return result ;
}
int main ()
{
printf ("%s\n", helper (" hello world ", " world "));
printf ("%s\n", helper (" hello world ", "ell"));
};
... as you can see, I simplified it a bit, there are no more calls to malloc/free and T[] array is now global.
Then I compiled it using GCC 7.3 x64 and reworked assembly listing a little, now there are no registers, but rather
vXX variables, each one is assigned only once, in a SSA manner. No variable assigned more than once. This is AT&T
syntax.
#RDI - haystack v1
#RSI - haystack_size v2
#RDX - needle v3
#RCX - needle_size v4
.text
.globl kmp_search
.type kmp_search , @function
kmp_search :
# v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16
testq %v4 , %v4 # | | | U
movq %v1 , %rax # U | | |
je .exit # | | | |
leaq (%v4 ,% v3), %v7 # | | U U D
movq %v3 , %v5 # | | U | D |
leaq 8+T(% rip), %v9 # | | | | | | D
movq $-1, T(% rip) # | | | | | | |
cmpq %v5 , %v7 # | | | | U U |
leaq -8(% v9), %v8 # | | | | | | D U
je .L20 # | | | | | | | |
.L6: # | | | | | | | |
movq -8(% v9), %v14 # | | | | | | | U D
addq $1 , %v14 # | | | | | | | | U
testq %v14 , %v14 # | | | | | | | | U
movq %v14 , (% v9) # | | | | | | | U U
jle .L4 # | | | | | | | | |
movzbl (% v5), %v11 # | | | | U | | | D |
cmpb %v11_byte , -1(%v3 ,% v14) # | | U | | | | | U U
jne .L5 # | | | | | | | | |
jmp .L4 # | | | | | | | | |
.L21: # | | | | | | | | |
movzbl -1(%v3 ,% v14), %v12 # | | U | | | | | D U
cmpb %v12_byte , (% v5) # | | | | U | | | U |
je .L4 # | | | | | | | | |
.L5: # | | | | | | | | |
movq -8(%v8 ,%v14 ,8) , %v15 # | | | | | | U | U D
addq $1 , %v15 # | | | | | | | U
testq %v15 , %v15 # | | | | | | | U
movq %v15 , (% v9) # | | | | | | U U
jg .L21 # | | | | | | |
.L4: # | | | | | | |
addq $1 , %v5 # | | | | U | |
addq $8 , %v9 # | | | | | | U
cmpq %v5 , %v7 # | | | | U U |
jne .L6 # | | | | | | |
.L20:
# v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16
# | | | |
leaq T(% rip), %v6 # | | | | D
xorq %v16 , %v16 # | | | | | D
xorq %v10 , %v10 # | | | | | D |
.L7: # | | | | | | |
cmpq %v10 , %v2 # | U | | | U |
jbe .L22 # | | | | | | |
.L11: # | | | | | | |
testq %v16 , %v16 # | | | | | | U
js .L8 # | | | | | | |
movzbl (%v3 ,% v16), %v13 # | | U | | | D U
cmpb %v13_byte , (%v1 ,% v10) # U | | | U U |
je .L8 # | | | | | |
339
cmpq %v10 , %v2 # | U | | U |
movq (%v6 ,%v16 ,8) , %v16 # | | U | U
ja .L11 # | | | |
.L22: # | | | |
xorq %rax , %rax # | | | |
ret # | | | |
.L8: # | | | |
addq $1 , %v16 # | | | U
addq $1 , %v10 # | | U |
cmpq %v16 , %v4 # | U | U
jne .L7 # | | |
subq %v4 , %v10 # | U U
leaq (%v1 ,% v10), %rax # U U
.exit:
ret
.comm T ,8192 ,32
Dangling ”noodles” you see at right are ”live ranges” of each vXX variable. ”D” means ”defined”, ”U” - ”used” or
”used and then defined again”. Whenever live range is started, we need to allocate variable (in a register or a local
stack). When it’s ending, we do not need to keep it somewhere in storage (in a register or a local stack).
As you can see, the function has two parts: preparation and processing. You can clearly see how live ranges are
divided by two groups, except of first 4 variables, which are function arguments.
You see, there are 16 variables. But we want to use as small number of registers, as possible. If several live ranges
are present at some address or point of time, these variables cannot be allocated in the same register.
This is how we can assign a register to each live range using Z3 SMT-solver:
#!/ usr/bin/env python3
from z3 import *
s= Solver ()
registers =["RDI", "RSI", "RDX", "RCX", "R8", "R9", "R10", "R11", "R12", "R13", "
R14", "R15"]
340
# first 4 variables are function arguments and they are always linked to rdi/rsi/
rdx/rcx:
s.add(v [1]==0)
s.add(v [2]==1)
s.add(v [3]==2)
s.add(v [4]==3)
if s. check () == sat:
print ("* colors =", colors )
m=s. model ()
for i in range (1, 17):
print ("v%d=%s" % (i, registers [m[v[i]]. as_long () ]))
.text
.globl kmp_search
.type kmp_search , @function
kmp_search :
# v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16
testq %rcx , %rcx # | | | U
movq %rdi , %rax # U | | |
je .exit # | | | |
leaq (%rcx ,% rdx), %r8 # | | U U D
movq %rdx , %r10 # | | U | D |
leaq 8+T(% rip), %r11 # | | | | | | D
movq $-1, T(% rip) # | | | | | | |
cmpq %r10 , %r8 # | | | | U U |
leaq -8(% r11), %r13 # | | | | | | D U
je .L20 # | | | | | | | |
.L6: # | | | | | | | |
movq -8(% r11), %r9 # | | | | | | | U D
addq $1 , %r9 # | | | | | | | | U
testq %r9 , %r9 # | | | | | | | | U
movq %r9 , (% r11) # | | | | | | | U U
jle .L4 # | | | | | | | | |
movzbl (% r10), %r12d # | | | | U | | | D |
cmpb %r12b , -1(%rdx ,% r9) # | | U | | | | | U U
jne .L5 # | | | | | | | | |
jmp .L4 # | | | | | | | | |
.L21: # | | | | | | | | |
movzbl -1(%rdx ,% r9), %r12d # | | U | | | | | D U
cmpb %r12b , (% r10) # | | | | U | | | U |
je .L4 # | | | | | | | | |
.L5: # | | | | | | | | |
movq -8(%r13 ,%r9 ,8) , %r12 # | | | | | | U | U D
addq $1 , %r12 # | | | | | | | U
testq %r12 , %r12 # | | | | | | | U
movq %r12 , (% r11) # | | | | | | U U
jg .L21 # | | | | | | |
.L4: # | | | | | | |
addq $1 , %r10 # | | | | U | |
addq $8 , %r11 # | | | | | | U
cmpq %r10 , %r8 # | | | | U U |
jne .L6 # | | | | | | |
.L20:
# v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16
# | | | |
leaq T(% rip), %r11 # | | | | D
xorq %r9 , %r9 # | | | | | D
xorq %r10 , %r10 # | | | | | D |
.L7: # | | | | | | |
cmpq %r10 , %rsi # | U | | | U |
jbe .L22 # | | | | | | |
.L11: # | | | | | | |
testq %r9 , %r9 # | | | | | | U
js .L8 # | | | | | | |
movzbl (%rdx ,% r9), %r8d # | | U | | | D U
cmpb %r8b , (%rdi ,% r10) # U | | | U U |
je .L8 # | | | | | |
cmpq %r10 , %rsi # | U | | U |
movq (%r11 ,%r9 ,8) , %r9 # | | U | U
ja .L11 # | | | |
.L22: # | | | |
342
xorq %rax , %rax # | | | |
ret # | | | |
.L8: # | | | |
addq $1 , %r9 # | | | U
addq $1 , %r10 # | | U |
cmpq %r9 , %rcx # | U | U
jne .L7 # | | |
subq %rcx , %r10 # | U U
leaq (%rdi ,% r10), %rax # U U
.exit:
ret
.comm T ,8192 ,32
4 https://xn--mxacd.xn--qxam/math-notes.pdf
Chapter 10
Knapsack problems
10.1 Popsicles
Found this problem at http://artofproblemsolving.com/wiki/index.php?title=2017_AMC_12A_Problems/Problem_
1:
Pablo buys popsicles for his friends. The store sells single popsicles for $1 each, 3-popsicle boxes
for $2, and 5-popsicle boxes for $3. What is the greatest number of popsicles that Pablo can buy with
$8?
s= Optimize ()
s.add(box1pop >=0)
s.add(box3pop >=0)
s.add(box5pop >=0)
s. maximize ( pop_total )
The result:
sat
[ box3pop = 1,
box5pop = 2,
cost_total = 8,
pop_total = 13,
box1pop = 0]
343
344
10.1.1 SMT-LIB 2.x
( assert (=
((_ zero_extend 16) pop_total )
( bvadd
((_ zero_extend 16) box1pop )
(bvmul ((_ zero_extend 16) box3pop ) # x00000003 )
(bvmul ((_ zero_extend 16) box5pop ) # x00000005 )
)))
( assert (=
((_ zero_extend 16) cost_total )
( bvadd
((_ zero_extend 16) box1pop )
(bvmul ((_ zero_extend 16) box3pop ) # x00000002 )
(bvmul ((_ zero_extend 16) box5pop ) # x00000003 )
)))
( maximize pop_total )
(check -sat)
(get - model )
; correct solution :
;( model
; (define -fun box1pop () (_ BitVec 16) (_ bv0 16)) ; 0x0
; (define -fun box3pop () (_ BitVec 16) (_ bv1 16)) ; 0x1
; (define -fun box5pop () (_ BitVec 16) (_ bv2 16)) ; 0x2
; (define -fun pop_total () (_ BitVec 16) (_ bv13 16)) ; 0xd
; (define -fun cost_total () (_ BitVec 16) (_ bv8 16)) ; 0x8
;)
storages =[700 , 30, 100 , 800 , 100 , 800 , 300 , 150 , 60, 500 , 1000]
files =[18 , 57, 291 , 184 , 167 , 496 , 45, 368 , 144 , 428 , 15, 100 , 999]
345
print (" trying to fit all the files into storages :", storages_to_be_used ,end=' ')
s= Solver ()
... in plain English - if a file is in storage , add its size to the final sum
"""
s.add( storage_occupied [i]== Sum ([ If( file_in_storage [f]==i, files [f], 0) for f
in range ( files_n )]))
# ... but sum of all files in each storage must be lower than what we have in
the storage :
s.add(And( storage_occupied [i]>=0, storage_occupied [i]<= storages [
storages_to_be_used [i]]))
if s. check () == sat:
print ("sat")
print ("* solution (%d storages ):" % t)
m=s. model ()
# print (m)
for i in range (t):
346
print (" storage %d (total =%d):" % ( storages_to_be_used [i], storages [
storages_to_be_used [i]]))
for f in range ( files_n ):
if m[ file_in_storage [f]]. as_long () ==i:
print (" file%d (%d)" % (f, files [f]))
print (" allocated on storage =%d" % (m[ storage_occupied [i]]. as_long ()),end
=' ')
print ("free on storage =%d" % ( storages [ storages_to_be_used [i]] - m[
storage_occupied [i]]. as_long ()))
print (" total in all storages =%d" % storage_t )
print (" allocated on all storages =%d%%" % (( float ( files_t )/ float ( storage_t ))
*100) )
print ("")
return True
else:
print (" unsat ")
return False
( https://smt.st/current_tree/knapsack/backup/backup.py )
Choose any solution you like:
trying to fit all the files into storages : (0, 3, 5, 7, 10) sat
* solution (5 storages ):
storage0 (total =700) :
file0 (18)
file3 (184)
file9 (428)
allocated on storage =630 free on storage =70
storage3 (total =800) :
file2 (291)
file5 (496)
allocated on storage =787 free on storage =13
storage5 (total =800) :
file1 (57)
file4 (167)
file6 (45)
file7 (368)
file8 (144)
allocated on storage =781 free on storage =19
storage7 (total =150) :
file10 (15)
file11 (100)
347
allocated on storage =115 free on storage =35
storage10 ( total =1000) :
file12 (999)
allocated on storage =999 free on storage =1
total in all storages =3450
allocated on all storages =96%
trying to fit all the files into storages : (0, 3, 5, 8, 10) sat
* solution (5 storages ):
storage0 (total =700) :
file3 (184)
file5 (496)
allocated on storage =680 free on storage =20
storage3 (total =800) :
file1 (57)
file4 (167)
file8 (144)
file9 (428)
allocated on storage =796 free on storage =4
storage5 (total =800) :
file0 (18)
file2 (291)
file7 (368)
file11 (100)
allocated on storage =777 free on storage =23
storage8 (total =60):
file6 (45)
file10 (15)
allocated on storage =60 free on storage =0
storage10 ( total =1000) :
file12 (999)
allocated on storage =999 free on storage =1
total in all storages =3360
allocated on all storages =98%
Now something practical. You may want to store each file twice. And no pair must reside on a single storage. Not
a problem, just make two arrays of variables:
...
...
...
...
( https://smt.st/current_tree/knapsack/backup/backup_twice.py )
The result:
storage total we need 3570
trying to fit all the files into storages : (0, 3, 5, 6, 10) sat
* solution (5 storages ):
storage0 (total =700) :
file4 (1st copy) (167)
file7 (1st copy) (368)
file8 (1st copy) (144)
file9 (2nd copy) (15)
allocated on storage =694 free on storage =6
storage3 (total =800) :
file0 (2nd copy) (18)
file3 (1st copy) (184)
file4 (2nd copy) (167)
file6 (2nd copy) (45)
file7 (2nd copy) (368)
file9 (1st copy) (15)
allocated on storage =797 free on storage =3
storage5 (total =800) :
file0 (1st copy) (18)
file1 (2nd copy) (57)
file3 (2nd copy) (184)
file5 (2nd copy) (496)
file6 (1st copy) (45)
allocated on storage =800 free on storage =0
storage6 (total =300) :
file2 (1st copy) (291)
allocated on storage =291 free on storage =9
storage10 ( total =1000) :
file1 (1st copy) (57)
file2 (2nd copy) (291)
file5 (1st copy) (496)
file8 (2nd copy) (144)
allocated on storage =988 free on storage =12
total in all storages =3600
allocated on all storages =99%
trying to fit all the files into storages : (0, 3, 5, 9, 10) sat
* solution (5 storages ):
storage0 (total =700) :
file0 (1st copy) (18)
file3 (2nd copy) (184)
file5 (2nd copy) (496)
allocated on storage =698 free on storage =2
storage3 (total =800) :
file1 (2nd copy) (57)
file2 (2nd copy) (291)
file4 (2nd copy) (167)
file8 (1st copy) (144)
file9 (2nd copy) (15)
allocated on storage =674 free on storage =126
storage5 (total =800) :
file4 (1st copy) (167)
file6 (2nd copy) (45)
file7 (1st copy) (368)
file8 (2nd copy) (144)
allocated on storage =724 free on storage =76
storage9 (total =500) :
349
file2 (1st copy) (291)
file3 (1st copy) (184)
allocated on storage =475 free on storage =25
storage10 ( total =1000) :
file0 (2nd copy) (18)
file1 (1st copy) (57)
file5 (1st copy) (496)
file6 (1st copy) (45)
file7 (2nd copy) (368)
file9 (1st copy) (15)
allocated on storage =999 free on storage =1
total in all storages =3800
allocated on all storages =93%
The problem: use as small number of servers, as possible. Fit VMs into them in the most efficient way, so that the
free RAM/storage would be minimal.
This is like knapsack problem. But the classic knapsack problem is about only one dimension (weight or size).
We’ve two dimensions here: RAM and storage. This is called multidimensional knapsack problem.
Another problem we will solve here is a bin packing problem.
#!/ usr/bin/env python3
from z3 import *
import itertools
# RAM , storage
vms =[(1 , 100) ,
(16 , 900) ,
(4, 710) ,
(2, 800) ,
(4, 7000) ,
(8, 4000) ,
(2, 800) ,
(4, 2500) ,
(16 , 450) ,
(16 , 3700) ,
(12 , 1300)]
vms_total =len(vms)
print (" trying to fit VMs into servers :", servers_to_be_used ,end=' ')
s= Solver ()
If( VM0_in_srv == 3, 1, 0) +
If( VM1_in_srv == 3, 16, 0) +
If( VM2_in_srv == 3, 4, 0) +
351
If( VM3_in_srv == 3, 2, 0) +
If( VM4_in_srv == 3, 4, 0) +
If( VM5_in_srv == 3, 8, 0) +
If( VM6_in_srv == 3, 2, 0) +
If( VM7_in_srv == 3, 4, 0) +
If( VM8_in_srv == 3, 16, 0) +
If( VM9_in_srv == 3, 16, 0) +
If( VM10_in_srv == 3, 12, 0)
"""
# RAM
s.add( RAM_allocated [i]== Sum ([ If( VM_in_srv [v]==i, vms[v][0] , 0) for v in range
( vms_total )]))
# storage
s.add( storage_allocated [i]== Sum ([If( VM_in_srv [v]==i, vms[v][1] , 0) for v in
range ( vms_total )]))
# ... but sum of all RAM/ storage occupied in each server must be lower than
what we have in the server :
s.add(And( RAM_allocated [i]>=0, RAM_allocated [i]<= servers [ servers_to_be_used [i
]][0]) )
s.add(And( storage_allocated [i]>=0, storage_allocated [i]<= servers [
servers_to_be_used [i ]][1]) )
if s. check () == sat:
print ("sat")
print ("* solution (%d servers ):" % t)
m=s. model ()
( https://smt.st/current_tree/knapsack/VM_packing/VM_pack.py )
The result:
RAM total we need 85
storage total we need 22260
trying to fit VMs into servers : (3, 4, 5, 6, 7) unsat
trying to fit VMs into servers : (3, 4, 5, 7, 9) sat
* solution (5 servers ):
srv3 ( total =16/8000) : VM2 (4/710) VM3 (2/800) VM5 (8/4000) VM6 (2/800) allocated on
srv =16/6310 free on srv =0/1690
srv4 ( total =8/3000) : VM0 (1/100) VM7 (4/2500) allocated on srv =5/2600 free on srv
=3/400
srv5 ( total =16/6000) : VM9 (16/3700) allocated on srv =16/3700 free on srv =0/2300
srv7 ( total =32/2000) : VM1 (16/900) VM8 (16/450) allocated on srv =32/1350 free on srv
=0/650
srv9 ( total =16/10000) : VM4 (4/7000) VM10 (12/1300) allocated on srv =16/8300 free on
srv =0/1700
total in all servers =88/29000
allocated on all servers =96%/76%
Twenty golfers wish to play in foursomes for 5 days. Is it possible for each golfer to play no more
than once with any other golfer?
...
Event organizers for bowling, golf, bridge, or tennis frequently tackle problems of this sort, unaware
of the problem complexity. In general, it is an unsolved problem. A table of known results is maintained
by Harvey.
( http://mathworld.wolfram.com/SocialGolferProblem.html )
It’s also problem 010 in CSPLIB1 .
This is the solution that was ported from Markus Triska’s solution written in Prolog2 .
This is an example solution for 7 golfers, 3 players in group, 9 weeks/days (7-3-9):
* week 1
[[1 , 2, 4], [3, 5, 13] , [6, 8, 19] , [7, 16, 20] , [9, 11, 14] , [10 , 15, 18] , [12 , 17,
21]]
* week 2
[[1 , 3, 10], [2, 11, 18] , [4, 13, 16] , [5, 14, 19] , [6, 9, 12] , [7, 15, 17] , [8, 20,
21]]
* week 3
[[1 , 6, 14], [2, 7, 9], [3, 15, 21] , [4, 10, 11] , [5, 12, 20] , [8, 13, 18] , [16 , 17,
19]]
* week 4
[[1 , 7, 11], [2, 5, 17] , [3, 6, 16] , [4, 9, 18] , [8, 12, 15] , [10 , 19, 20] , [13 , 14,
21]]
* week 5
[[1 , 9, 20], [2, 12, 13] , [3, 7, 8], [4, 6, 15] , [5, 16, 18] , [10 , 14, 17] , [11 , 19,
21]]
* week 6
[[1 , 12, 19], [2, 16, 21] , [3, 9, 17] , [4, 7, 14] , [5, 8, 10] , [6, 18, 20] , [11 , 13,
15]]
* week 7
[[1 , 13, 17], [2, 6, 10] , [3, 11, 12] , [4, 5, 21] , [7, 18, 19] , [8, 9, 16] , [14 , 15,
20]]
* week 8
[[1 , 15, 16], [2, 3, 20] , [4, 8, 17] , [5, 6, 11] , [7, 10, 21] , [9, 13, 19] , [12 , 14,
18]]
* week 9
[[1 , 18, 21], [2, 8, 14] , [3, 4, 19] , [5, 9, 15] , [6, 7, 13] , [10 , 12, 16] , [11 , 17,
20]]
1 https://csplib.github.io/csplib-builds/ghmeta/Problems/prob010/results/
2 https://www.metalevel.at/sgpsat.pdf, https://www.metalevel.at/sgp/satgolf.pl
354
355
I also wanted to check timings when symmertry breaking constraints present and when not. These are my results.
All checked on Intel(R) Xeon(R) CPU E3-1270 v3 @ 3.50GHz. Kissat 2.0.1 (2021) SAT solver was used. Time in
seconds.
• Ian P. Gent and Inês Lynce – A SAT Encoding for the Social Golfer Problem.
• Markus Triska: Webpage, Solution Methods for the Social Golfer Problem.
• Warwick Harvey: Warwick’s Results Page for the Social Golfer Problem, Solutions to various Social Golfer
configurations.
• Ed Pegg Jr: Social Golfer Problem.
• https://demonstrations.wolfram.com/SocialGolferProblem/.
• Ke Liu, Sven Löffler and Petra Hofstedt: Solving the Social Golfers Problems by Constraint Programming in
Sequential and Parallel.
Chapter 12
Latin squares
Magic/Latin square is a square filled with numbers/letters, which are all distinct in each row and column. Sudoku is
9*9 magic square with additional constraints (for each 3*3 subsquare).
1 Latin Square
2A Normalized/reduced Latin square has first row and column consisting of ascending numbers/symbols (0,1,2,3...)
357
358
By looking from all three sides, you’ll see all 6 · 6 = 36 cubelets, but only one in each slot:
359
Also, see the video for L(6) LS: https://www.youtube.com/watch?v=Fk-ysMqcO98
See the video for L(10): https://www.youtube.com/watch?v=zT04G60VKD8. Notice the distinctive “V” shape,
which is the result of normalization (0..9 in first row and column).
Also L(16): https://www.youtube.com/watch?v=Lovt41KVkwM, L(25): https://www.youtube.com/watch?v=
1eVsFy_gLaY, L(36): https://www.youtube.com/watch?v=IgYuqRXry0A.
Adding constraints for such a cube is easy:
def latin_add_constraints (SIZE , s, ar):
# 'cube '
( https://oeis.org/A002860 )
A number of normalized/reduced Latin squares:
3 On-Line Encyclopedia of Integer Sequences
360
( https://oeis.org/A000315 )
And these is symmetry breaking constraints that makes Latin square to be normalized/reduced. Sometimes they
make SAT solver work faster, but sometimes not. As they say, your mileage may vary, they can help for your specific
problem, or may not.
I’m fixing solution’s column to be in ascending order (0,1,2,3...) like in D.Knuth’s solutions: solutions count would
be multiple of 10! otherwise (number of isomorphic squares).
my_time .py ./ MOLS2_par .py --solver gimsatul --all - solutions --normalize --first
3145926870281976350494523071686208451793836409521759812740364627530981057614832917306894527093812645
# 6 solutions
# Knuth_c
="0572164938605129847348670392151439807652832475609172039415865610473829914862530727953801643986512740"
my_time .py ./ MOLS2_par .py --solver gimsatul --all - solutions --normalize --first
0572164938605129847348670392151439807652832475609172039415865610473829914862530727953801643986512740
362
# 12 ,265 ,168 solutions
# Knuth_e
="7823456019823406719523401789563401289567401239567856789123406789523401019563478219567408239567801234"
my_time .py ./ MOLS2_par .py --solver gimsatul --all - solutions --normalize --first
7823456019823406719523401789563401289567401239567856789123406789523401019563478219567408239567801234
12.2.3 Proofs
Thirty-six officers problem is a well-known classic problem. Citing Wikipedia:
A problem similar to the card problem above was circulating in St. Petersburg in the late 1700s
and, according to folklore, Catherine the Great asked Euler to solve it, since he was residing at her
court at the time.[7] This problem is known as the thirty-six officers problem,[8] and Euler introduced
it as follows:[9][10]
A very curious question, which has exercised for some time the ingenuity of many people, has
involved me in the following studies, which seem to open a new field of analysis, in particular the
study of combinations. The question revolves around arranging 36 officers to be drawn from 6 different
regiments so that they are ranged in a square so that in each line (both horizontal and vertical) there
are 6 officers of different ranks and different regiments. — Leonhard Euler
( https://en.wikipedia.org/wiki/Mutually_orthogonal_Latin_squares )
It was proved by Gaston Tarry in 1901. But we can prove it as well.
In other words, we will try to find a 2-MOLS(6), but this is impossible, my script would report UNSAT. But can
we create a proof?
my_time .py ./ MOLS2_par .py --solver gimsatul --normalize --proof --size 6
(L(10) (e) from TAOCP has 5504 transversals 7 . Here is also a video where these transversals are combined:
https://www.youtube.com/watch?v=ifvKt_ts8DE.)
Then add the following constraint: second square (or mate): each transversal in mate must have same number/sym-
bol.
This divides the task to 3 subtasks: 1) find all transversals in LS (fast); 2) combine order transversals using exact
cover problem (toughest step); 3) find mate for transversals set (order! permutations).
That method was used in 1959 by E.T.Parker. Citing Wikipedia:
( https://en.wikipedia.org/wiki/Mutually_orthogonal_Latin_squares )
The famous magazine cover:
7 By the way, that square visualized as a cube: https://www.youtube.com/watch?v=gnNq-J5kcl4. Clearly, you can see here some symme-
tries/regularities.
365
However, finding 3-MOLS(n) using this method is not as good as my naive method, described previously. The
naive method can find 3-MOLS(8) easily, but this method is not competitive.
Probably because the naive method find triple in parallel.
366
While this method do this sequentially: it finds a first LS, then tries to find a LS that is MOLS for the first LS,
then tries to find a third LS which is MOLS for first and second LS. That is 3 stages.
For example, it turns out that orthogonal latin squares are enormously useful, particularly in the
design of experiments. Already in 1788, Francois Crette de Palluel used a 4x4 latin square to study
what happens when sixteen sheep - four each from four different breeds - were fed four different diets
and harvested at four different times. [Memoires d’Agriculture (Paris: Societe Royale d’Agriculture,
trimestre d’ete, 1788), 17-23.] The latin square allowed him to do this with 16 sheep instead of 64;
with a Greco-Latin square he could also have varied another parameter by trying, say, four different
quantities of food or four different grazing paradigms.
But if we had focused our discussion on his approach to animal husbandry, we might well have gotten
bogged down in details about breeding, about root vegetables versus grains and the costs of growing
them, etc. Readers who aren’t farmers might therefore have decided to skip the whole topic, even
though latin square designs apply to a wide range of studies. (Think about testing five kinds of pills,
on patients in five stages of some disease, five age brackets, and five weight groups.) Moreover, a
concentration on experimental design could lead readers to miss the fact that latin squares also have
important applications to coding and cryptography (see exercises 18-24).
After testing, you’ll find a best pair of compiler and optimization options.
This is a Latin square. You saw it often in Sudoku form, which is also Latin square, but with additional constraints
(9 3*3 boxes).
Thanks to Latin square, you could test all pairs in 4 days using only 4 computers. Even if you have no idea about
Latin squares and will try to find this arrangement manually, you’ll come with a solution similar to that, because there
is no smaller solution.
Now the problem is harder: you have 4 frameworks/libraries/APIs to test. Finally, you want to find a best triple:
compiler + optimization options + framework.
For the problem like that it would be hard to find a good arrangement manually. I’m using my small utility to
find two Latin squares, that are mutually orthogonal to each other:
first: | mate:
0 1 2 3 | 0 3 1 2
1 0 3 2 | 1 2 0 3
2 3 0 1 | 2 1 3 0
3 2 1 0 | 3 0 2 1
concatenated :
00 13 21 32
11 02 30 23
22 31 03 10
33 20 12 01
8 https://yurichev.com/mirrors/Donald%20Knuth/TAOCP%200a%207/fasc0a.pdf
367
Each pair in the final ’concatenated’ (or superimposed) table is unique. Now add this ’concatenated’ table to ours:
Each pair of digits would reflect optimization options + framework. Thanks to MOLS (Mutually Orthogonal Latin
Squares) you can test all triplets (compiler + optimization options + framework) (again) in 4 days using 4 computers.
Now the next problem: you also have 4 different operating systems. macOS, Windows, and maybe two flavours of
Unix. 3 MOLS would help.
first: | mate 1: | mate 2:
0 1 2 3 | 0 2 3 1 | 0 3 1 2
1 0 3 2 | 1 3 2 0 | 1 2 0 3
2 3 0 1 | 2 0 1 3 | 2 1 3 0
3 2 1 0 | 3 1 0 2 | 3 0 2 1
first+mate 1+ mate2 :
000 123 231 312
111 032 320 203
222 301 013 130
333 210 102 021
These are 3 Latin squares where any pair of these 3 squares are orthogonal to each other. Hence, in the final table
you see only unique triplets. Let’s add this to our table:
Each unique triplet is: optimization options + framework + OS. Again you need only 4 days and 4 computers to
test each pair of these parameters and pick the best set of compiler + optimization options + framework + OS.
Unfortunately, MOLS generation has its limits too. Only small squares of order less than 10 are easy to generate.
Generation of larger is a very hard problem. Despite of that, small MOLS are very useful.
• Combinatorics: Ancient & Modern (2013), Lars Døvling Andersen – Latin Squares, pp 251-283.
• Hantao Zhang – Combinatorial Designs by SAT Solvers10 .
9 https://yurichev.com/mirrors/Donald%20Knuth/TAOCP%200a%207/fasc0a.pdf
10 https://www.researchgate.net/publication/242029072_Combinatorial_Designs_by_SAT_Solvers, https://ebooks.iospress.nl/
volumearticle/5002, https://ebooks.iospress.nl/volumearticle/56529.
Chapter 13
# include "klee.h"
crc = ∼crc;
while (len --)
{
crc ^= *buf ++;
for (k = 0; k < 8; k++)
crc = crc & 1 ? (crc >> 1) ^ 0 xC96C5795D7870F42 : crc >> 1;
}
return ∼crc;
}
int main ()
{
# define HEAD_STR "Hello , world !.. "
# define HEAD_SIZE strlen ( HEAD_STR )
# define TAIL_STR " ... and goodbye "
# define TAIL_SIZE strlen ( TAIL_STR )
# define MID_SIZE 14 // stuff
//# define MID_SIZE 13 // ( failed ) stuff
1 https://xn--mxacd.xn--qxam/math-notes.pdf
2 Cyclic redundancy check
3 There are several slightly different CRC64 implementations, the one I use here can also be different from popular ones.
368
369
# define BUF_SIZE HEAD_SIZE + TAIL_SIZE + MID_SIZE
...
% ls klee -last /*
klee -last/ assembly .ll klee -last/solver - queries .smt2 klee -last/ test000002 . ktest
klee -last/info klee -last/ test000001 . kquery klee -last/ test000002 .user.err
klee -last/ messages .txt klee -last/ test000001 . ktest klee -last/ test000003 . ktest
klee -last/run. istats klee -last/ test000001 .user.err klee -last/ warnings .txt
klee -last/run.stats klee -last/ test000002 . kquery
Since our code uses memcmp() standard C/C++ function, we need to add --libc=uclibc switch, so KLEE will
use its own uClibc implementation.
Maybe it’s slow, but definitely faster than bruteforce. Indeed, log2 2614 ≈ 65.8 which is close to 64 bits. In other
words, one need ≈ 14 latin characters to encode 64 bits. And KLEE + SMT solver needs 64 bits at some place it can
alter to make final CRC64 value equal to what we defined.
I tried to reduce length of the middle block to 13 characters: no luck for KLEE then, it has no space enough.
370
13.1.2 Buffer alteration case #2
I went sadistic: what if the buffer must contain the CRC64 value which, after calculation of CRC64, will result in the
same value? Fascinatingly, KLEE can solve this. The buffer will have the following format:
Hello , world !.. <8 bytes (64 - bit value )> <6 more bytes > ... and goodbye
It works:
% time klee --libc= uclibc --optimize klee_CRC64_v2 .bc
...
KLEE: done: total instructions = 16901
KLEE: done: completed paths = 1
KLEE: done: partially completed paths = 32
KLEE: done: generated tests = 3
% ls klee -last /*
klee -last/ assembly .ll klee -last/solver - queries .smt2 klee -last/ test000002 . ktest
klee -last/info klee -last/ test000001 . kquery klee -last/ test000002 .user.err
klee -last/ messages .txt klee -last/ test000001 . ktest klee -last/ test000003 . ktest
klee -last/run. istats klee -last/ test000001 .user.err klee -last/ warnings .txt
klee -last/run.stats klee -last/ test000002 . kquery
8 bytes between two strings is 64-bit value which equals to CRC64 of this whole block itself: 0xef58d2051498c60d.
Also, there are 14-8=6 bytes after that 64-bit value which seems random, but it’s OK. Again, it’s faster than brute-
force way to find it. If to decrease last spare 6-byte buffer to 4 bytes or less, KLEE works so long so I’ve stopped
it.
371
13.1.3 Recovering input data for given CRC64 value of it
I’ve always wanted to do so, but everyone knows this is impossible for input buffers larger than 8 bytes. As my
experiments show, it’s still possible for tiny input buffers of data, which is constrained in some way.
The CRC64 value of 13-byte “undisciplined” string is known: 0x811265a32d6ac13a. KLEE can find this 13-byte
string, if it knows that each byte of input buffer is in a..z limits:
1 ...
2
3 int main ()
4 {
5 # define BUF_SIZE 13
6
7 char buf[ BUF_SIZE ];
8
9 klee_make_symbolic (buf , sizeof buf , "buf");
10
11 for (int i=0; i< BUF_SIZE ; i++)
12 klee_assume (buf[i]>='a' && buf[i]<='z');
13
14 klee_assume ( crc64 (0, buf , BUF_SIZE )==0 x811265a32d6ac13a );
15 }
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ klee_CRC64_undisciplined .c
...
% ls klee -last /*
klee -last/ assembly .ll klee -last/ messages .txt klee -last/run. stats klee -
last/ test000001 . ktest
klee -last/info klee -last/run. istats klee -last/solver - queries .smt2 klee -
last/ warnings .txt
Still, it’s no magic: if to remove condition at line 30 (i.e., if to relax constraint), KLEE will produce some other
string, which will be still correct for the CRC64 value given.
It works, because 13 Latin characters in a..z limits contain ≈ 61 bits: log2 2613 ≈ 61, which is even smaller value
than 64. In other words, the final CRC64 value holds enough bits to recover ≈ 61 bits of input.
The input buffer can be even bigger, if each byte of it will be in even tighter constraints (decimal digits, binary
digits, etc).
int main ()
{
//# define BUF_SIZE 13 // fail
# define BUF_SIZE 14
So CRC64("adnwsskbroggni")==0, cool.
Buf shorter string (13 character) isn’t possible, alas.
Every tool has its limitations, so is KLEE. I couldn’t find such string with characters in 0..9 range.
crc = ∼crc;
while (len --)
{
crc ^= *buf ++;
for (k = 0; k < 8; k++)
crc = crc & 1 ? (crc >> 1) ^ 0 xC96C5795D7870F42 : crc >> 1;
}
return ∼crc;
}
# define SIZE 13
void main ()
{
char str[SIZE ];
int i=0;
for (i=0; i<SIZE; i++)
{
if (str[i]<'a')
return ;
if (str[i]>'z')
return ;
};
** Results :
CBMC_crc64_find .c function main
[main. assertion .1] line 35 assertion 0: FAILURE
...
Violated property :
file CBMC_crc64_find .c function main line 35 thread 0
assertion 0
( __CPROVER_bool )0
** 1 of 1 failed (2 iterations )
VERIFICATION FAILED
# define SIZE 14
void main ()
{
char str[SIZE ];
int i=0;
for (i=0; i<SIZE; i++)
{
if (str[i]<'a')
return ;
if (str[i]>'z')
return ;
};
** Results :
CBMC_crc64_find_2 .c function main
[main. assertion .1] line 36 assertion 0: FAILURE
# define SIZE 16
void main ()
{
char str[SIZE ];
int i=0;
for (i=0; i<SIZE; i++)
{
if ( valid_char (str[i]) ==0)
return ;
};
** Results :
CBMC_crc64_find_3 .c function main
[main. assertion .1] line 45 assertion 0: FAILURE
...
Violated property :
file CBMC_crc64_find_3 .c function main line 45 thread 0
assertion 0
0 != 0
** 1 of 1 failed (2 iterations )
VERIFICATION FAILED
['cbmc ', '--trace ', '--function ', 'main ', '--z3 ', 'CBMC_crc64_find_3 .c ']
['/ home/i/ dotfiles /bin/ my_time .py ', 'cbmc ', '--trace ', '--function ', 'main ', '--z3 ',
'CBMC_crc64_find_3 .c ']
seconds : 510
or: 8m30s
375
Yes, CRC64("8493a5648600c495")==0, and this is very predictable: we just need to find a 64-bit input for CRC64
so that it’s output would be zero. No magic here.
Again, CBMC can’t find a string consisting only of 0..9 characters, like KLEE.
"""
rows with dots are partial products :
aaaa
b ___ ....
b __ ...._
b _.... __
b .... ___
"""
# partial products
p=[ BitVec ('p_%d' % i, OUTPUT_SIZE ) for i in range ( INPUT_SIZE )]
s= Solver ()
4 https://github.com/diffblue/cbmc/issues/5099
376
# Concat () is for glueling together bitvectors (of different widths )
# BitVecVal () is constant of specific width
if i==0:
s.add(p[i] == If ((b>>i)&1==1 , Concat ( BitVecVal (0, OUTPUT_SIZE -i-
INPUT_SIZE ), a), 0))
else:
s.add(p[i] == If ((b>>i)&1==1 , Concat ( BitVecVal (0, OUTPUT_SIZE -i-
INPUT_SIZE ), a, BitVecVal (0, i)), 0))
if s. check () == unsat :
print (" prime factor : 0x%x or %s" % (poly , my_utils . poly_to_str (poly)))
return
m=s. model ()
# print ("sat , a=0x%x, b=0x%x" % (m[a]. as_long () , m[b]. as_long ()))
# factor results recursively :
factor (m[a]. as_long ())
factor (m[b]. as_long ())
print (" starting . poly =0x%x or %s" % (poly , my_utils . poly_to_str (poly)))
factor (poly)
See also almost the same solution for my SAT library: https://smt.st/current_tree/CRC/factor/gf2_factor_
SAT.py
gf2_factor_test ()
{
echo "* poly $1"
echo "** running for SAT"
./ gf2_factor_SAT .py $1
echo "** running for Z3"
./ gf2_factor_z3 .py $1
}
13.3.1 SageMath
... but.
SageMath can do this way more quickly, and can factor 64-bit polynomials on which my solution stuck. Also, it
can factor 128-bit polynomial used in AES-GCM, with no effort.
https://smt.st/current_tree/CRC/factor/sagemath/check_irreducible.html.
(SageMath notebooks I used: https://smt.st/current_tree/CRC/factor/sagemath/.)
Alas! This is a professional mathematical tool. It probably uses a specially tailored algorithm, which may be way
more complex.
Yet again, I wrote my tool for SAT/SMT with no special knowledge, as a newbie. And it was fun and it helped
me to learn Galois fields.
from z3 import *
import struct
# knobs :
# CRC -16 ( Modbus ) on https :// www. lammertbies .nl/comm/info/crc - calculation .html
# width =16
# samples =["\ x01", "\ x02 "]
# must_be =[0 x807E , 0 x813E ]
# sample_len =1
# crc64_jones .c (redis ):
# width =64
# samples =["\ x01", "\ x02 "]
# must_be =[0 x7ad870c830358979 , 0 xf5b0e190606b12f2 ]
# sample_len =1
# crc64_xz .c:
# width =64
# samples =["\ x01", "\ x02 "]
# must_be =[0 xac83edcd67c06036 , 0 xeb299724cc279f02 ]
# sample_len =1
# these " unoptimized " versions are constructed like a Russian doll ...
states =[[[ BitVec ('state_ %d_%d_%d' % (sample , i, bit), width ) for bit in range (8+1) ]
for i in range ( sample_len +1)] for sample in range (len( samples ))]
s= Solver ()
print ("poly =0x%x, init =0x%x, ReflectIn =%s, %s" % (m[poly ]. as_long () , m[
states [0][0][8]]. as_long () , str(m[ reflect_in ]) , outparams ))
Sometimes, we have no enough information, but still can get something. This is for CRC-16:
383
poly =0 x96bc , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 x7cfc , init =0x0 , ReflectIn =False , XORout =-1, ReflectOut = False
poly =0 x5814 , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 x6182 , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 xa287 , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 x726b , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 x41c1 , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 xe83d , init =0x0 , ReflectIn =True , XORout =-1, ReflectOut =True
poly =0 xa001 , init =0x0 , ReflectIn =True , XORout =0, ReflectOut =True
total results 9
poly =0 xd800000000000000 , init =0x0 , ReflectIn =True , XORout =0, ReflectOut =True
poly =0 xfe4fffffffffffff , init =0x0 , ReflectIn =False , XORout =-1, ReflectOut =True
poly =0 xfffffffffffff27f , init =0x0 , ReflectIn =False , XORout =-1, ReflectOut = False
poly =0xd80 , init =0x0 , ReflectIn =False , XORout ==0 , ReflectOut = False
poly =0 x1b0000000000000 , init =0x0 , ReflectIn =False , XORout =0, ReflectOut =True
poly =0 xa7ffffffffffffbf , init =0 xffffffffffffffff , ReflectIn =False , XORout =0,
ReflectOut =True
poly =0 x6c000 , init =0x0 , ReflectIn =True , XORout ==0 , ReflectOut = False
poly =0 xb40b40b40b4816 , init =0 xffffffffffffffff , ReflectIn =True , XORout ==0 , ReflectOut
=False
poly =0 xe73cf3cf3cf3cf3c , init =0 xffffffffffffffff , ReflectIn =False , XORout =-1,
ReflectOut =True
poly =0 x53ffffffffffffff , init =0 xffffffffffffffff , ReflectIn =True , XORout =0,
ReflectOut =True
poly =0 x7ffffffffff93fbf , init =0 xffffffffffffffff , ReflectIn =False , XORout ==0 ,
ReflectOut =False
poly =0 xcc2055dc9e9bc60f , init =0 xffffffffffffffff , ReflectIn =False , XORout =-1,
ReflectOut =True
poly =0 x3ffffffffffc9fff , init =0 xffffffffffffffff , ReflectIn =True , XORout ==0 ,
ReflectOut =False
poly =0 x9fd82e25acc7fcf3 , init =0 xffffffffffffffff , ReflectIn =False , XORout =-1,
ReflectOut =False
poly =0 x367a57106cf7678 , init =0 xffffffffffffffff , ReflectIn =False , XORout ==0 ,
ReflectOut =False
poly =0 x92492492492e924 , init =0 xffffffffffffffff , ReflectIn =True , XORout ==0 ,
ReflectOut =False
poly =0 x1d24924924924924 , init =0 xffffffffffffffff , ReflectIn =True , XORout =0,
ReflectOut =True
poly =0 x31ba , init =0x0 , ReflectIn =True , XORout ==0, ReflectOut = False
poly =0 x153225b1d0d61af , init =0 xffffffffffffffff , ReflectIn =False , XORout ==0 ,
ReflectOut =False
poly =0 x461861861861861 , init =0 xffffffffffffffff , ReflectIn =True , XORout =0, ReflectOut
=True
poly =0 x30c30c30c30f861 , init =0 xffffffffffffffff , ReflectIn =True , XORout ==0 ,
ReflectOut =False
total results 21
• http://www.cosc.canterbury.ac.nz/greg.ewing/essays/CRC-Reverse-Engineering.html
• http://reveng.sourceforge.net/crc-catalogue/1-15.htm
• http://reveng.sourceforge.net/crc-catalogue/16.htm
• http://reveng.sourceforge.net/crc-catalogue/17plus.htm
from z3 import *
import copy , random
width =32
s= Solver ()
no_call =0
Problem: at least this one. CRC must be able to detect errors in very long buffers, up to 232 for CRC32. We can’t
feed that huge buffers to SMT solver. I had success only with samples up to ≈ 32 bytes.
MaxSAT/MaxSMT
A leftmost number is an execution count for each line. ##### means the line of code hasn’t been executed at all.
The second column is a line number.
Now the Z3Py script, which will parse all these 1000 gcov results and produce minimal hitting set:
#!/ usr/bin/env python3
1 https://en.wikipedia.org/wiki/Set_cover_problem
2 Lempel–Ziv–Storer–Szymanski
3 https://github.com/opensource-apple/kext_tools/blob/master/compression.c
4 https://smt.st/current_tree/MaxSxT/min_test_Z3/gen_gcov_tests.sh
386
387
import re , sys
from z3 import *
TOTAL_TESTS =1000
while True:
l=f. readline (). rstrip ()
m = re. search ('^ *([0 -9]+): ([0 -9]+) :.*$', l)
if m!= None:
lines . append (int(m.group (2)))
if len(l)==0:
break
f.close ()
return lines
# we need to find a such solution , where minimal number of all " test_X " variables
will have 1
# "* tests " unfolds to a list of arguments : [test_1 , test_2 , test_3 ,...]
# "Sum (* tests )" unfolds to the following expression : "Sum(test_1 , test_2 , ...)"
388
# the sum of all " test_X " variables should be as minimal as possible :
h=opt. minimize (Sum (* tests ))
And what it produces (~19s on my old Intel Quad-Core Xeon E3-1220 3.10GHz):
% time python set_cover .py
sat
test_7
test_48
test_134
python set_cover .py 18.95 s user 0.03s system 99% cpu 18.988 total
We need just these 3 tests to execute (almost) all lines in the code: looks impressive, given the fact, that it would
be notoriously hard to pick these tests by hand! The result can be checked easily, again, using gcov utility.
This is sometimes also called MaxSAT/MaxSxT — the problem is to find solution, but the solution where some
variable/expression is maximal as possible, or minimal as possible.
Also, the code gives incorrect results on Z3 4.4.1, but working correctly on Z3 4.5.0 (so please upgrade). This is
relatively fresh feature in Z3, so probably it was not stable in previous versions?
The files: https://smt.st/current_tree/MaxSxT/min_test_Z3.
Further reading: https://en.wikipedia.org/wiki/Set_cover_problem, https://en.wikipedia.org/wiki/Maximum_
satisfiability_problem, https://en.wikipedia.org/wiki/Optimization_problem.
...
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x0F\x58\
xF1", 3, " ADDSS XMM6 , XMM1");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x0F\x59\
xC0", 3, " MULSS XMM0 , XMM0");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x0F\x5C\
xD7", 3, " SUBSS XMM2 , XMM7");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x0F\x5E\
xF8", 3, " DIVSS XMM7 , XMM0");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x42\x0F\
x11\x74\x1D\x98", 3, " MOVSS [RBP+R11 -68h], XMM6");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x48\x0F\
x2A\xC0", 3, " CVTSI2SS XMM0 , RAX");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x48\xAB",
3, "REP STOSQ ");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF7\xED", 0
x300 , "IMUL EBP");
if ( test_all || line == __LINE__ ) disas_test2_1op (Fuzzy_True , ( const byte *)"\xFF\xD0",
0x300 , "CALL RAX", 64);
if ( test_all || line == __LINE__ ) disas_test2_2op (Fuzzy_True , ( const byte *)"\x00\x4C\
x8B\x94", 0x300 , "ADD [RBX+RCX *4 -6 ch], CL", 8, 8);
...
Also any disassembler, including mine, has a mega-table or mother-table or master-table that contains all opcodes,
flags, instruction names, etc.
And I added a debugging statement: if a table entry is loaded, its number is being printed. And you’ll see why.
I’m compiling the code so that it will generate GCOV-statistics:
gcc -fprofile -arcs -ftest - coverage -g -DX86_DISASM_PRINT_INS_TBL_ENTRY
x86_disasm_tests .c x86_disas .c x86_register .c -I../ octothorpe ../ octothorpe /
octothorpe .a
echo $i
rm *gcda*
tbl_entry =$(./a.out $i | tail -1)
echo $tbl_entry
gcov x86_disas
mv x86_disas .c.gcov gcovs /$i. $tbl_entry
done
12531 files have been created. Filename consists of number1.number2, where number1 is a test number (or line
number in tests.h) and number2 is a table entry loaded during testing. The contents is a typical GCOV’s output:
...
1: 651: if ( IS_SET (p->new_flags ,
F_REG32_IS_LOWEST_PART_OF_1ST_BYTE ))
#####: 652: mask =0 xF8;
1: 653: if ( IS_SET (p->new_flags ,
F_REG64_IS_LOWEST_PART_OF_1ST_BYTE ))
#####: 654: mask =0 xF8;
-: 655:
1: 656: if (( opc & mask) != ins_tbl [p-> tbl_p ]. opc)
-: 657: {
#####: 658: p-> tbl_p ++;
#####: 659: continue ;
-: 660: };
390
-: 661:
-: 662: // load second opcode if needed
1: 663: if ( IS_SET (p->new_flags , F_OPC2 ))
-: 664: {
-: 665: uint8_t opc2;
#####: 666: if ( Da_stage1_get_next_byte (p, &opc2)== false )
-: 667: {
#####: 668: if ( dbg_print )
#####: 669: printf ("%s() line %d\n",
__func__ , __LINE__ );
#####: 670: return false ;
-: 671: };
...
This is how many times each line was executed during run. ##### means never. 1 means one.
Now the goal: to execute all lines at least once, with the help of as few tests as possible. This is how the problem
can be stated in plain English language:
for the line X, the test Y OR the test Z or the test M must be ran.
We generate such (hard) clauses for each lines.
Also, we say to MaxSAT solver to find such a solution, where as few tests would be True, as possible. And this is
my Python program to do so:
#!/ usr/bin/env python3
import re , sys , os
while True:
l=f. readline (). rstrip ()
m = re. search ('^ *([0 -9]+): ([0 -9]+) :.*$', l)
if m!= None:
lines . append (int(m.group (2)))
if len(l)==0:
break
f.close ()
return lines
max_test_n =0
print ("p wcnf "+str( max_test_n )+" "+str(len(lines )+ max_test_n -1)+" 10000 ")
I run it:
python minimize_tests1 .py > 1. wcnf
”c” is comment. I’m putting line (of code) number here. So for line 768, any tests among 3432 6250 9716 9034 2020
4138 1308 5546 3433 4137 2728 7661 must be present, at least one. For line 375, a (single) test 1126 must be present.
(By the way, it’s quite interesting to see what line of code is triggered by a single test. Surely, they are must be
present in minimized test suite.)
All test numbers are then enumerated as soft constraints.
Now I’m running the Open-WBO MaxSAT solver on the given WCNF file:
open -wbo 1. wcnf > result
c
c Open -WBO: a Modular MaxSAT Solver -- based on Glucose4 .1 (core version )
...
s OPTIMUM FOUND
v -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 ...
-1125 1126 -1127 ...
...
5 Weighted Conjunctive normal form
392
The output consists of all variables. But since a test number is mapped to a line number, this number also mapped
to a variable’s number. Efficiently, Open-WBO reports, which tests are to be picked (like 1126th).
I wrote an utility for that:
#!/ usr/bin/env python3
import sys
And we found that to cover (almost) all lines of code in my disassembler, only 11 tests are enough!
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_False , ( const byte *)"\x64\xA3\x12\
x34\x56\x78", 0x100 , "FS: MOV [78563412 h], EAX");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_False , ( const byte *)"\xD9\xEE", 0
x123456 , "FLDZ");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_False , ( const byte *)"\xF0\x66\x0F\
xB1\x0B", 0x1234 , "LOCK CMPXCHG [EBX], CX");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\x44\xAB", 0
x4f5b , " STOSD ");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\x44\xBC\x90\
x90\x90\x90", 0x507c , "MOV ESP , 90909090 h");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\x45\xDB\x90\
x90\x90\x90\x90", 0x638b , "FIST [R8 -6 f6f6f70h ]");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\x4B\xC8\x90\
x90\x90", 0xc848 , " ENTER 0 FFFFFFFFFFFF9090h , 90h");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\x4F\xBF\x90\
x90\x90\x90\x90\x90\x90\x90", 0x10baf , "MOV R15 , 9090909090909090 h");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\x66\x0F\x38\
x3D\xD0", 3, " PMAXSD XMM2 , XMM0");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF2\x0F\x5A\
xCE", 0, " CVTSD2SS XMM1 , XMM6");
if ( test_all || line == __LINE__ ) disas_test1 ( Fuzzy_True , ( const byte *)"\xF3\x42\x0F\
x11\x74\x1D\x98", 3, " MOVSS [RBP+R11 -68h], XMM6");
(It’s important to know that my ”bloated” tests was not perfect, some lines of code like error reporting are like
”dead code” now, but it’s OK.)
At this point, my readers perhaps could stop reading and reuse my ideas for their own code and tests.
But... As I mentioned, a disassembler has a mega-table. And we want to touch each its entry during tests at least
once, like each line of code. And this is a second version of my program:
#!/ usr/bin/env python3
import re , sys , os
while True:
l=f. readline (). rstrip ()
m = re. search ('^ *([0 -9]+): ([0 -9]+) :.*$', l)
393
if m!= None:
lines . append (int(m.group (2)))
if len(l)==0:
break
f.close ()
return lines
max_test_n =0
print ("p wcnf "+str( max_test_n )+" "+str(len( tbl_entries )+len(lines )+ max_test_n -1)+"
10000 ")
Now only 301 tests are enough to cover (almost) all lines in my disassembler and to touch (almost) all entries in
the mega-table. Much better than 12531.
Also, Open-WBO seems to be a better tool for the job, it works faster than Z3. Or maybe Z3 can be tuned?
Find a way to check, if it was soldered correctly, with no wires stuck at ground (always 0) or current (always 1).
You can just enumerate all possible inputs (5) and this will be a table of correct inputs/outputs, 32 pairs. But you
want to make fault check as fast as possible and minimize test set.
This is almost a problem I’ve been writing before: 14.1.
We want such a test set, so that all gates’ outputs will output 0 and 1, at least once. And the test set should be
as small, as possible.
The source code is very close to my previous example...
#!/ usr/bin/env python3
from z3 import *
OUT_Z1 , OUT_Z2 , OUT_Z3 , OUT_Z4 , OUT_Z5 , OUT_A2 , OUT_A3 , OUT_B1 , OUT_B2 , OUT_B3 ,
OUT_C1 , OUT_C2 , OUT_P , OUT_Q , OUT_S = range ( OUTPUTS_TOTAL )
out_false_if ={}
out_true_if ={}
outs ={}
s= Solver ()
# this generates expression like (tests [0]==1 OR tests [1]==1 OR tests [X ]==1) :
for o in range ( OUTPUTS_TOTAL ):
opt.add(Or (*[ tests [i]==1 for i in out_false_if [o]]))
opt.add(Or (*[ tests [i]==1 for i in out_true_if [o]]))
This is it, you can test this circuit using just 3 test vectors: 01111, 10001 and 11011.
However, Donald Knuth’s test set is bigger: 5 test vectors, but his algorithm also checks ”fanout” gates (one input,
multiple outputs), which also may be faulty. I’ve omitted this for simplification.
14.4.1 GCD
To compute GCD, one of the oldest algorithms is used: Euclidean algorithm. But, I can demonstrate how to make
things much less efficient, but more spectacular.
To find GCD of 14 and 8, we are going to solve this system of equations:
x*GCD =14
y*GCD =8
Then we drop x and y, we don’t need them. This system can be solved using a piece of paper and pencil, but GCD
must be as big as possible. Here we can use Z3 in MaxSMT mode:
#!/ usr/bin/env python
from z3 import *
opt = Optimize ()
opt.add(x*GCD ==14)
opt.add(y*GCD ==8)
That works:
sat
[y = 4, x = 7, GCD = 2]
What if we need to find GCD for 3 numbers? Maybe we are going to fill a space with biggest possible cubes?
#!/ usr/bin/env python
from z3 import *
opt = Optimize ()
opt.add(x*GCD ==300)
opt.add(y*GCD ==333)
opt.add(z*GCD ==900)
6 https://xn--mxacd.xn--qxam/math-notes.pdf
399
h=opt. maximize (GCD)
This is 3:
sat
[z = 300, y = 111 , x = 100 , GCD = 3]
In SMT-LIB form:
; checked with Z3 and MK85
; must be 21
; see also: https :// www. wolframalpha .com/ input /?i=GCD [861 ,3969 ,840]
( assert (= ( bvmul ((_ zero_extend 16) x) ((_ zero_extend 16) GCD)) (_ bv861 32)))
( assert (= ( bvmul ((_ zero_extend 16) y) ((_ zero_extend 16) GCD)) (_ bv3969 32)))
( assert (= ( bvmul ((_ zero_extend 16) z) ((_ zero_extend 16) GCD)) (_ bv840 32)))
( maximize GCD)
(check -sat)
(get - model )
; correct result :
;( model
; (define -fun x () (_ BitVec 16) (_ bv41 16)) ; 0x29
; (define -fun y () (_ BitVec 16) (_ bv189 16)) ; 0xbd
; (define -fun z () (_ BitVec 16) (_ bv40 16)) ; 0x28
; (define -fun GCD () (_ BitVec 16) (_ bv21 16)) ; 0x15
;)
from z3 import *
opt = Optimize ()
opt.add(x*4== LCM)
opt.add(y*6== LCM)
opt.add(LCM >0)
As in my previous examples, Z3 and SMT-solver may be overkill for the task. Simpler algorithm exists for this
task (Hungarian algorithm/method) 7 .
But again, I use it to demonstrate the problem + as SMT-solvers demonstration.
Here is what I do:
#!/ usr/bin/env python3
from z3 import *
# find such a ( distinct ) choice []'s, so that the final_sum would be minimal :
s. minimize ( final_sum )
In plain English this means choose such columns, so that their sum would be as small as possible.
Result is seems to be correct:
sat
[ choice_0 = 1,
choice_1 = 2,
choice_2 = 0,
z3name !12 = 0,
z3name !7 = 1,
z3name !10 = 2,
z3name !8 = 0,
z3name !11 = 0,
z3name !9 = 0,
final_sum = 950 ,
row_value_2 = 200 ,
row_value_1 = 350 ,
row_value_0 = 400]
(However, I’ve no idea what “z3name” variables mean, perhaps, some internal variables?)
The problem can also be stated in SMT-LIB 2.0 format, and solved using MK85:
; checked with Z3 and MK85
( assert (= row_value1
(ite (= choice1 (_ bv0 2)) (_ bv250 16)
(ite (= choice1 (_ bv1 2)) (_ bv400 16)
(ite (= choice1 (_ bv2 2)) (_ bv250 16)
(_ bv999 16))))))
( assert (= row_value2
(ite (= choice2 (_ bv0 2)) (_ bv400 16)
(ite (= choice2 (_ bv1 2)) (_ bv600 16)
(ite (= choice2 (_ bv2 2)) (_ bv350 16)
403
(_ bv999 16))))))
( assert (= row_value3
(ite (= choice3 (_ bv0 2)) (_ bv200 16)
(ite (= choice3 (_ bv1 2)) (_ bv400 16)
(ite (= choice3 (_ bv2 2)) (_ bv250 16)
(_ bv999 16))))))
( minimize final_sum )
(check -sat)
(get - model )
VERTICES =50
edges =set ()
while True:
raw=f. readline ()
l=raw. rstrip ()
if len(l)==0:
break
8 https://xn--mxacd.xn--qxam/math-notes.pdf
404
f. close ()
Run:
python3 1.py edges .txt
...
# we don 't use coins for simplicity , but it 's not a problem to add them
s= Optimize ()
# sum:
s.add( from_cash_register ==
from_cash_register_1 *1 +
from_cash_register_3 *3 +
from_cash_register_5 *5 +
from_cash_register_10 *10 +
from_cash_register_25 *25 +
from_cash_register_50 *50 +
from_cash_register_100 *100)
s.add( from_customer ==
from_customer_wallet_1 *1 +
from_customer_wallet_3 *3 +
from_customer_wallet_5 *5 +
from_customer_wallet_10 *10 +
from_customer_wallet_25 *25 +
from_customer_wallet_50 *50 +
407
from_customer_wallet_100 *100)
print ("")
sat
from_cash_register_1 = 0
from_cash_register_3 = 0
from_cash_register_5 = 0
from_cash_register_10 = 2
from_cash_register_25 = 0
from_cash_register_50 = 0
from_cash_register_100 = 0
from_cash_register = 20
from_customer_wallet_1 = 2
from_customer_wallet_3 = 1
from_customer_wallet_5 = 0
from_customer_wallet_10 = 0
from_customer_wallet_25 = 0
from_customer_wallet_50 = 11
408
from_customer_wallet_100 = 16
from_customer = 2155
So that the cashier have to pull only 2 tenners from cash desk.
Now what if we don’t care about small banknotes, but want to make transaction as paperless as possible:
...
...
sat
from_cash_register_1 = 0
from_cash_register_3 = 0
from_cash_register_5 = 1
from_cash_register_10 = 1
from_cash_register_25 = 0
from_cash_register_50 = 0
from_cash_register_100 = 0
from_cash_register = 15
from_customer_wallet_1 = 0
from_customer_wallet_3 = 0
from_customer_wallet_5 = 0
from_customer_wallet_10 = 0
from_customer_wallet_25 = 0
from_customer_wallet_50 = 3
from_customer_wallet_100 = 20
from_customer = 2150
Likewise, you can minimize banknotes from cashier only, or from customer only.
STUDENTS =15
CEILING_LOG2_STUDENTS =4 # 4 bits to hold a number in 0..14 range
POSSIBLE_INTERESTS =8
# each variable is " grounded " to the bitmask from interests []:
interests_vars =[s. alloc_BV ( POSSIBLE_INTERESTS ) for i in range (2**
CEILING_LOG2_STUDENTS )]
for st in range (2** CEILING_LOG2_STUDENTS ):
s. fix_BV ( interests_vars [st], SAT_lib . n_to_BV ( interests [st], POSSIBLE_INTERESTS ))
assert s. solve ()
total_in_all_groups =0
# print solution :
for group in range (int( STUDENTS /3)):
print ("* group ", group )
st1= SAT_lib . BV_to_number (s. get_BV_from_solution ( students_positions [ group *3]))
st2= SAT_lib . BV_to_number (s. get_BV_from_solution ( students_positions [ group *3+1]) )
st3= SAT_lib . BV_to_number (s. get_BV_from_solution ( students_positions [ group *3+2]) )
total =c12+c13+c23
print (" total =", total )
total_in_all_groups = total_in_all_groups + total
...
( https://smt.st/current_tree/libs/SAT_lib.py )
... and then Open-WBO MaxSAT searches such a solution, for which as many soft clauses as possible would be
satisfied, i.e., as many hobbies shared, as possible.
And the result:
* group 0
students : 7 12 1
common interests between 1 and 2: 1
common interests between 1 and 3: 1
common interests between 2 and 3: 0
total= 2
* group 1
students : 13 2 10
common interests between 1 and 2: 0
common interests between 1 and 3: 1
common interests between 2 and 3: 0
total= 1
* group 2
students : 3 4 14
common interests between 1 and 2: 1
common interests between 1 and 3: 1
common interests between 2 and 3: 0
total= 2
* group 3
students : 8 5 11
common interests between 1 and 2: 0
common interests between 1 and 3: 0
common interests between 2 and 3: 1
total= 1
* group 4
students : 9 0 6
common interests between 1 and 2: 1
common interests between 1 and 3: 1
common interests between 2 and 3: 1
total= 3
* total between all groups = 9
Surely, you can group any other objects with each other based on multiple preferences.
; unknowns
(declare -fun Radius () (_ BitVec 16))
(declare -fun Height () (_ BitVec 16))
; There are may not be solutions for Volume =10000 , since this we work on integers
; So let 's ask for solution for Volume somewhere in between 10000 and 10500...
412
(declare -fun Volume () (_ BitVec 16))
( assert ( bvuge Volume (_ bv10000 16)))
( assert ( bvule Volume (_ bv10500 16)))
; r*r
(declare -fun Radius2 () (_ BitVec 16))
( assert (= Radius2 ( bvmul_no_overflow Radius Radius )))
; pi*r*r
(declare -fun AreaOfBase () (_ BitVec 16))
( assert (= AreaOfBase ( bvmul_no_overflow Radius2 AlmostPi )))
; 2*pi*r
(declare -fun Circumference () (_ BitVec 16))
( assert (= Circumference ( bvmul_no_overflow ( bvmul_no_overflow Radius AlmostPi ) (_
bv2 16))))
( minimize SurfaceOfCylinder )
(check -sat)
(get - model )
The solution:
sat
(model
(define -fun AlmostPi () (_ BitVec 16) (_ bv3 16)) ; 0x3
(define -fun AreaOfBase () (_ BitVec 16) (_ bv507 16)) ; 0x1fb
(define -fun Circumference () (_ BitVec 16) (_ bv78 16)) ; 0x4e
(define -fun Height () (_ BitVec 16) (_ bv20 16)) ; 0x14
(define -fun Radius () (_ BitVec 16) (_ bv13 16)) ; 0xd
(define -fun Radius2 () (_ BitVec 16) (_ bv169 16)) ; 0xa9
(define -fun SurfaceOfCylinder () (_ BitVec 16) (_ bv2574 16)) ; 0xa0e
(define -fun Volume () (_ BitVec 16) (_ bv10140 16)) ; 0 x279c
)
For Volume=10140, Radius=13, Height=20. Hard to believe, but this is almost correct. I can check it with Wolfram
Mathematica:
In []:= FindMinimum [{2* Pi*r*h + Pi*r*r*2, h*Pi*r*r == 10000 }, {r, h}]
Out []= {2569.5 , {r -> 11.6754 , h -> 23.3509}}
Error is 3.
Using my toy solver and π fixed to 3, perhaps, my results cannot be used in practice, however, we can deduce a
general rule: height must be the same as diameter, so that you’ll spent minimum tin (or other material) to make it.
Probably, the can will not be suitable for stacking, packing, transporting, or holding in hand, but this is the most
economical way to produce them.
14.9.1 A jar
What about a jar (a can without top (or bottom))?
...
; surface of jar = Circumference * Height + AreaOfBase
(declare -fun SurfaceOfJar () (_ BitVec 16))
413
( assert (= SurfaceOfJar ( bvadd ( bvmul_no_overflow Circumference Height ) AreaOfBase )))
( minimize SurfaceOfJar )
...
sat
(model
(define -fun AlmostPi () (_ BitVec 16) (_ bv3 16)) ; 0x3
(define -fun AreaOfBase () (_ BitVec 16) (_ bv675 16)) ; 0x2a3
(define -fun Circumference () (_ BitVec 16) (_ bv90 16)) ; 0x5a
(define -fun Height () (_ BitVec 16) (_ bv15 16)) ; 0xf
(define -fun Radius () (_ BitVec 16) (_ bv15 16)) ; 0xf
(define -fun Radius2 () (_ BitVec 16) (_ bv225 16)) ; 0xe1
(define -fun SurfaceOfJar () (_ BitVec 16) (_ bv2025 16)) ; 0x7e9
(define -fun Volume () (_ BitVec 16) (_ bv10125 16)) ; 0 x278d
)
For Volume=10125, Radius=15 and Height=15. We can see that for jar, the height must be equal to the radius.
Let’s recheck in Mathematica:
In []:= FindMinimum [{2* Pi*r*h + Pi*r*r, h*Pi*r*r == 10000 }, {r, h}]
Out []= {2039.41 , {r -> 14.7101 , h -> 14.7101}}
Correct!
Yes, you’ve been probably taught in school to solve this using paper and pencil, but... The fun thing is that I never
knew calculus at all, but I could write my toy bit-blaster, which can give such answers. And thanks to Open-WBO,
which is used in my MK85 as external MaxSAT solver.
Since I’m using non-standard SMT-LIB function bvmul_no_overflow, this will not work on other SMT solvers.
For Z3, bvumul_noovfl is to be used: https://github.com/Z3Prover/z3/issues/574
See also: https://demonstrations.wolfram.com/MinimizingTheSurfaceAreaOfACylinderWithAFixedVolume/
from z3 import *
# this is simplification , "back" offsets are limited by 127 bytes , " forward " ones by
128 bytes , but OK , let 's say ,
# all of them are 128:
def JMP_size ( offset ):
return If(offset >128 , 5, 2)
block1_size =64
block2_size =81
block3_size =12
block4_size =50
block5_size =60
s= Optimize ()
# calculate all JMPs offsets , these are block sizes and also other 's JMPs sizes
between the current address
# and destination address :
s.add( JMP_1_offset == block2_size + JMP_2_size )
s.add( JMP_2_offset == block3_size + JMP_3_size + block4_size + JMP_4_size )
s.add( JMP_3_offset == block2_size + JMP_2_size + block3_size )
s.add( JMP_4_offset == block1_size + JMP_1_size + block2_size + JMP_2_size + block3_size +
JMP_3_size + block4_size )
s.add( JMP_5_offset == block3_size + JMP_3_size + block4_size + JMP_4_size + block5_size )
print s. check ()
print s. model ()
I.e., JMP_4 and JMP_5 JMPs must be long ones, others can be short ones.
Other simplification I made for the sake of example: short conditional Jcc’s can also be encoded using 2 bytes,
long ones using 6 bytes rather than 5 (5 is unconditional JMP).
Chapter 15
Synthesis
from z3 import *
import sys
# 1-bit NOT
"""
INPUTS =[0 b10]
OUTPUTS =[0 b01]
BITS =2
add_always_false =False
add_always_true =True
avail =[ I_XOR ]
# avail =[ I_NOR3 ]
"""
# 2- input AND
"""
INPUTS =[0 b1100 , 0b1010 ]
OUTPUTS =[0 b1000 ]
BITS =4
add_always_false =False
add_always_true = False
avail =[I_OR , I_NOT ]
# avail =[ I_NOR3 ]
"""
# 2- input XOR
#"""
INPUTS =[0 b1100 , 0 b1010 ]
OUTPUTS =[0 b0110 ]
BITS =4
add_always_false =False
# add_always_false =True
416
417
add_always_true = False
# add_always_true =True
# avail =[ I_NOR3 ]
# avail =[ I_AND , I_NOT ]
avail =[I_OR , I_NOT ]
#"""
# full - adder
"""
INPUTS =[0 b11110000 , 0b11001100 , 0 b10101010 ]
OUTPUTS =[0 b11101000 , 0 b10010110 ] # carry -out , sum
BITS =8
add_always_false =False
add_always_true = False
avail =[ I_AND , I_OR , I_XOR , I_NOT ]
# avail =[ I_AND , I_OR , I_NOT ]
# avail =[ I_NOR3 ]
"""
# popcnt
""" TT for popcnt
in HL
000 00
001 01
010 01
011 10
100 01
101 10
110 10
111 11
"""
"""
INPUTS =[0 b11110000 , 0b11001100 , 0 b10101010 ]
OUTPUTS =[0 b11101000 , 0 b10010110 ] # high , low
BITS =8
add_always_false =False
418
add_always_true = False
# avail =[ I_AND , I_OR , I_XOR , I_NOT ]
avail =[ I_NOR3 ]
"""
# 2-bit adder :
"""
INPUTS =[0 b1111111100000000 , 0 b1111000011110000 , 0 b1100110011001100 , 0
b1010101010101010 ]
OUTPUTS =[0 b1001001101101100 , 0 b0101101001011010 ] # high , low
BITS =16
add_always_false =True
add_always_true =True
# add_always_false = False
# add_always_true =False
avail =[ I_AND , I_OR , I_XOR , I_NOT ]
# avail =[ I_NOR3 ]
"""
"""
#7- segment display
INPUTS =[0 b1111111100000000 , 0 b1111000011110000 , 0 b1100110011001100 , 0
b1010101010101010 ]
# "g" segment , like here: http :// www. nutsvolts .com/ uploads / wygwam /
NV_0501_Marston_Figure02 .jpg
OUTPUTS =[0 b1110111101111100 ] # g
BITS =16
add_always_false =False
add_always_true = False
avail =[ I_AND , I_OR , I_XOR , I_NOT ]
# avail =[ I_NOR3 ]
"""
MAX_STEPS =20
else:
raise AssertionError
return R
"""
selector () functions generates expression like:
If( op1_reg_s5 == 0,
S_s0 ,
If( op1_reg_s5 == 1,
S_s1 ,
If( op1_reg_s5 == 2,
S_s2 ,
If( op1_reg_s5 == 3,
S_s3 ,
If( op1_reg_s5 == 4,
S_s4 ,
If( op1_reg_s5 == 5,
S_s5 ,
420
If( op1_reg_s5 == 6,
S_s6 ,
If( op1_reg_s5 == 7,
S_s7 ,
If( op1_reg_s5 == 8,
S_s8 ,
If( op1_reg_s5 == 9,
S_s9 ,
If( op1_reg_s5 == 10,
S_s10 ,
If( op1_reg_s5 == 11,
S_s11 ,
0))))))))))))
# fill inputs :
for i in range (len( INPUTS )):
sl.add(R[i]== INPUTS [i])
# fill outputs , "must be 's"
for o in range (len( OUTPUTS )):
sl.add(R[STEPS -(o+1) ]== list( reversed ( OUTPUTS ))[o])
tmp=sl.check ()
if tmp == sat:
print ("sat!")
m=sl. model ()
# print (m)
print_model (m, R, STEPS , op , op1_reg , op2_reg , op3_reg )
selftest (m, STEPS , op , op1_reg , op2_reg , op3_reg )
exit (0)
else:
print (tmp)
I could generate only small circuits maybe up to ≈ 10 gates, but this is interesting nonetheless.
Also, I’ve always wondering how you can do something usable for Apollo Guidance Computer, which had only one
single gate: NOR3? See also its schematics: http://klabs.org/history/ech/agc_schematics/. The answer is De
Morgan’s laws, but this is not obvious.
INPUTS[] has all possible bit combinations for all inputs, or all possible truth tables. OUTPUTS[] has truth
table for each output. All the rest is processed in bitsliced manner. Given that, the resulting program will work on
4/8/16-bit CPU and will generate defined OUTPUTS for defined INPUTS. Or, this program can be treated just like
a logic circuit.
AND gate
How to build 2-input AND gate using only OR’s and NOT’s?
INPUTS =[0 b1100 , 0 b1010 ]
OUTPUTS =[0 b1000]
BITS =4
avail =[I_OR , I_NOT ]
...
r0=input 1100
r1=input 1010
422
r2=NOT r1 0101
r3=NOT r0 0011
r4=OR r3 , r2 0111
r5=NOT r4 1000
This is indeed like stated in De Morgan’s laws: x ∧ y is equivalent to ¬(¬x ∨ ¬y). Can be used for obfuscation?
Now using only NOR3 gate?
avail =[ I_NOR3 ]
...
r0=input 1100
r1=input 1010
r2=NOR3 r1 , r1 , r1 0101
r3=NOR3 r2 , r0 , r0 0010
r4=NOR3 r3 , r2 , r2 1000
XOR gate
How to build 2-input XOR using only OR’s and NOT’s?
INPUTS =[0 b1100 , 0 b1010 ]
OUTPUTS =[0 b0110]
BITS =4
avail =[I_OR , I_NOT ]
...
7 instructions
r0=input 1100
r1=input 1010
r2=OR r1 , r0 1110
r3=NOT r2 0001
r4=OR r0 , r3 1101
r5=NOT r4 0010
r6=OR r1 , r3 1011
r7=NOT r6 0100
r8=OR r5 , r7 0110
Full-adder
According to Wikipedia, full-adder can be constructed using two XOR gates, two AND gates and one OR gate. But
I had no idea 3 XORs and 2 ANDs can be used instead:
INPUTS =[0 b11110000 , 0b11001100 , 0 b10101010 ]
OUTPUTS =[0 b11101000 , 0 b10010110 ] # carry -out , sum
BITS =8
avail =[ I_AND , I_OR , I_XOR , I_NOT ]
...
5 instructions
r0=input 11110000
r1=input 11001100
r2=input 10101010
r3=XOR r2 , r1 01100110
r4=AND r0 , r3 01100000
r5=AND r2 , r1 10001000
r6=XOR r4 , r5 11101000
r7=XOR r0 , r3 10010110
POPCNT
Smallest circuit to count bits in 3-bit input, producing 2-bit output:
INPUTS =[0 b11110000 , 0b11001100 , 0 b10101010 ]
OUTPUTS =[0 b11101000 , 0 b10010110 ] # high , low
BITS =8
avail =[ I_AND , I_OR , I_XOR , I_NOT ]
...
5 instructions
r0=input 11110000
r1=input 11001100
r2=input 10101010
r3=XOR r2 , r1 01100110
r4=AND r0 , r3 01100000
r5=AND r2 , r1 10001000
r6=XOR r4 , r5 11101000
r7=XOR r0 , r3 10010110
avail =[ I_NOR3 ]
...
8 instructions
r0=input 11110000
r1=input 11001100
r2=input 10101010
r3=NOR3 r0 , r0 , r1 00000011
r4=NOR3 r2 , r3 , r1 00010000
r5=NOR3 r3 , r2 , r0 00000100
r6=NOR3 r3 , r0 , r5 00001000
r7=NOR3 r5 , r2 , r4 01000001
r8=NOR3 r3 , r4 , r1 00100000
r9=NOR3 r3 , r5 , r4 11101000
r10=NOR3 r8 , r7 , r6 10010110
Y
s+i−1
jo · m
j=i
Figure 15.1: The general formula. j = how many inputs available to an instruction at a step; i = total number of
inputs of a program (4 in my case); s = total number of instructions in program (steps); o = total number of operands
each instruction has; m = how many instructions available to choose from.
I’m not clever enough to solve this manually (yet), but I could try using logic synthesis, as I did before. As they
say, “machines should work; people should think”.
The modified Z3Py script:
#!/ usr/bin/env python3
from z3 import *
import sys
# 2- input function
BITS =4
add_always_false =False
428
add_always_true = False
# add_always_false =True
# add_always_true =True
avail =[ I_NAND ]
# avail =[ I_ANDN ]
MAX_STEPS =10
else:
raise AssertionError
return R
"""
selector () function generates expression like:
If( op1_reg_s5 == 0,
S_s0 ,
If( op1_reg_s5 == 1,
S_s1 ,
If( op1_reg_s5 == 2,
S_s2 ,
If( op1_reg_s5 == 3,
S_s3 ,
If( op1_reg_s5 == 4,
S_s4 ,
If( op1_reg_s5 == 5,
S_s5 ,
If( op1_reg_s5 == 6,
S_s6 ,
If( op1_reg_s5 == 7,
S_s7 ,
If( op1_reg_s5 == 8,
S_s8 ,
If( op1_reg_s5 == 9,
S_s9 ,
If( op1_reg_s5 == 10,
S_s10 ,
If( op1_reg_s5 == 11,
S_s11 ,
0))))))))))))
# fill inputs :
for i in range (len( INPUTS )):
sl.add(R[i]== INPUTS [i])
# fill outputs , "must be's"
for o in range (len( OUTPUTS )):
sl.add(R[STEPS -(o+1) ]== list( reversed ( OUTPUTS ))[o])
tmp=sl. check ()
431
if tmp == sat:
print ("sat!")
m=sl.model ()
# print (m)
print_model (m, R, STEPS , op , op1_reg , op2_reg , op3_reg )
selftest (m, STEPS , op , op1_reg , op2_reg , op3_reg )
return True
else:
print (tmp)
return False
My solutions are slightly different: I haven’t ”pass through” instruction, so sometimes a value is copied from the
input to the output using NAND/ANDN. Also, my versions are sometimes different, but correct and has the same
length.
Each block takes previous state of registers and produces new states. There are two chains. First chain takes 0 as
state of R0 at the very beginning, and the chain is supposed to produce 0 at the end (since zero multiplied by any
value is still zero). The second chain takes 1 and must produce multiplier as the state of very last register (since 1
multiplied by multiplier must equal to multiplier).
Each block is “controlled” by operation type, operands, etc. For each column, there is each own set.
Now you can view these two chains as two equations. The ultimate goal is to find such state of all operation types
and operands, so the first chain will equal to 0, and the second to multiplier.
Let’s also take a look into “magic block” inside:
op1 reg
selector1
registers
selector2
op2 reg ADD/SUB/SHL result
op
op2 imm
This is very important to understand: if the operation is ADD/SUB, op2_imm’s value is just ignored. Otherwise,
if operation is SHL, value of op2_reg is ignored. Just like in case of digital circuit.
The code: https://smt.st/current_tree/synth/pgm/mult/mult.py
Now let’s see how it works:
% ./ mult.py 12
multiplier = 12
attempt , STEPS= 2
unsat
attempt , STEPS= 3
unsat
441
attempt , STEPS= 4
sat!
r1=SHL r0 , 2
r2=SHL r1 , 1
r3=ADD r1 , r2
tests are OK
The first step is always a step containing 0/1, or, r0. So when our solver reporting about 4 steps, this means 3
instructions.
Something harder:
% ./ mult.py 123
multiplier = 123
attempt , STEPS= 2
unsat
attempt , STEPS= 3
unsat
attempt , STEPS= 4
unsat
attempt , STEPS= 5
sat!
r1=SHL r0 , 2
r2=SHL r1 , 5
r3=SUB r2 , r1
r4=SUB r3 , r0
tests are OK
Looks great, but it took ≈ 23 seconds to find it on my Intel Xeon CPU E3-1220 @ 3.10GHz. I agree, this is far
from practical usage. Also, I’m not quite sure that this piece of code will work faster than a single multiplication
instruction. But anyway, it’s a good demonstration of SMT solvers capabilities.
The code multiplying by 12345 (≈ 150 seconds):
r1=SHL r0 , 5
r2=SHL r0 , 3
r3=SUB r2 , r1
r4=SUB r1 , r3
r5=SHL r3 , 9
r6=SUB r4 , r5
r7=ADD r0 , r6
What multipliers are possible? I tried all multipliers in the [1..2530] range with success. Further work: try more.
Few notes
I’ve removed SHR instruction support, simply because the code multiplying by a constant makes no use of it. Even
more: it’s not a problem to add support of constants as second operand for all instructions, but again, you wouldn’t
442
find a piece of code which does this job and uses some additional constants. Or maybe I wrong?
Of course, for another job you’ll need to add support of constants and other operations. But at the same time, it
will work slower and slower. So I had to keep ISA1 of this toy CPU as compact as possible.
The code
https://smt.st/current_tree/synth/pgm/mult
See also
Multiplication using a series of additions also called addition chain. [See Alexander A. Stepanov, Daniel E. Rose –
From Mathematics to Generic Programming, section 2.1 Egyptian Multiplication.]
15.2.2 Rockey dongle: finding unknown algorithm using only input/output pairs
Some smartcards can execute Java or .NET code - that’s the way to hide your sensitive algorithm into chip that is
very hard to break (decapsulate). For example, one may encrypt/decrypt data files by hidden crypto algorithm ren-
dering software piracy of such software close to impossible—an encrypted date file created on software with connected
smartcard would be impossible to decrypt on cracked version of the same software. (This leads to many nuisances,
though.)
That’s what is called black box.
Some software protection dongles offers this functionality too. One example is Rockey 42 .
This is a small dongle connected via USB. Is contain some user-defined memory but also memory for user algorithms.
The virtual (toy) CPU for these algorithms is very simple: it offer only 8 16-bit registers (however, only 4 can be
set and read) and 8 operations (addition, subtraction, cyclic left shifting, multiplication, OR, XOR, AND, negation).
Second instruction argument can be a constant (from 0 to 63) instead of register.
Each algorithm is described by string like
A=A+B, B=C*13, D=D^A, C=B*55, C=C&A, D=D|A, A=A*9, A=A&B.
There are no memory, stack, conditional/unconditional jumps, etc.
Each algorithm, obviously, can’t have side effects, so they are actually pure functions and their results can be
memoized.
By the way, as it has been mentioned in Rockey 4 manual, first and last instruction cannot have constants. Maybe
that’s because these fields used for some internal data: each algorithm start and end should be marked somehow
internally anyway.
Would it be possible to reveal hidden impossible-to-read algorithm only by recording input/output dongle traffic?
Common sense tell us “no”. But we can try anyway.
Since, my goal wasn’t to break into some Rockey-protected software, I was interesting only in limits (which
algorithms could we find), so I make some things simpler: we will work with only 4 16-bit registers, and there will be
only 6 operations (add, subtract, multiply, OR, XOR, AND).
Let’s first calculate, how much information will be used in brute-force case.
There are 384 of all possible instructions in reg=reg,op,reg format for 4 registers and 6 operations, and also 6144
instructions in reg=reg,op,constant format. Remember that constant limited to 63 as maximal value? That help
us for a little.
So, there are 6528 of all possible instructions. This mean, there are ≈ 1.1 · 1019 5-instruction algorithms. That’s
too much.
1 Instruction Set Architecture
2 http://www.rockey.nl/en/rockey.html
443
How can we express each instruction as system of equations? While remembering some school mathematics, I
wrote this:
Function one\_step ()=
So the question now is to find 5 · 23 Boolean values satisfying known input/output pairs.
I wrote small utility to probe Rockey 4 algorithm with random numbers, it produce results in form:
RY_CALCULATE1 : ( input ) p1 =30760 p2 =18484 p3 =41200 p4 =61741 ( output ) p1 =49244 p2 =11312
p3 =27587 p4 =12657
RY_CALCULATE1 : ( input ) p1 =51139 p2 =7852 p3 =53038 p4 =49378 ( output ) p1 =58991 p2 =34134
p3 =40662 p4 =9869
RY_CALCULATE1 : ( input ) p1 =60086 p2 =52001 p3 =13352 p4 =45313 ( output ) p1 =46551 p2 =42504
p3 =61472 p4 =1238
RY_CALCULATE1 : ( input ) p1 =48318 p2 =6531 p3 =51997 p4 =30907 ( output ) p1 =54849 p2 =20601
p3 =31271 p4 =44794
• constraint definitions (like, output_1 should be n for input_1=m, constant cannot be greater than 63, etc);
• functions constructing system of equations.
This piece of code define some kind of structure consisting of 4 named 16-bit variables, each represent register in
our toy CPU.
Registers_State = Datatype (' Registers_State ')
Registers_State . declare ('cons ', ('A', BitVecSort (16)), ('B', BitVecSort (16)), ('C',
BitVecSort (16)), ('D', BitVecSort (16)))
444
Registers_State = Registers_State . create ()
These enumerations define two new types (or sorts in Z3’s terminology):
Operation , (OP_MULT , OP_MINUS , OP_PLUS , OP_XOR , OP_OR , OP_AND ) = EnumSort ('Operation
', ('OP_MULT ', 'OP_MINUS ', 'OP_PLUS ', 'OP_XOR ', 'OP_OR ', 'OP_AND '))
Register , (A, B, C, D) = EnumSort ('Register ', ('A', 'B', 'C', 'D '))
This part is very important, it defines all variables in our system of equations. op_step is type of operation in
instruction. reg_or_constant is selector between register and constant in second argument — False if it’s a register
and True if it’s a constant. reg_step is a destination register of this instruction. reg1_step and reg2_step are just
registers at arg1 and arg2. constant_step is constant (in case it’s used in instruction instead of arg2).
op_step =[ Const (' op_step %s' % i, Operation ) for i in range ( STEPS )]
reg_or_constant_step =[ Bool(' reg_or_constant_step %s' % i) for i in range ( STEPS )]
reg_step =[ Const(' reg_step %s' % i, Register ) for i in range ( STEPS )]
reg1_step =[ Const (' reg1_step %s' % i, Register ) for i in range ( STEPS )]
reg2_step =[ Const (' reg2_step %s' % i, Register ) for i in range ( STEPS )]
constant_step = [ BitVec (' constant_step %s' % i, 16) for i in range ( STEPS )]
Adding constraints is very simple. Remember, I wrote that each constant cannot be larger than 63?
# according to Rockey 4 dongle manual , arg2 in first and last instructions cannot be
a constant
s.add ( reg_or_constant_step [0]== False )
s.add ( reg_or_constant_step [STEPS -1]== False )
...
This function returning corresponding register value from structure. Needless to say, the code above is not executed.
If() is Z3Py function. The code only declares the function, which will be used in another. Expression declaration
resembling LISP PL in some way.
Here is another function where register_selector() is used:
# Bool , Register , Registers_State , int -> int
def register_or_constant_selector ( register_or_constant , register , input_registers ,
constant ):
return If( register_or_constant == False , register_selector (register ,
input_registers ), constant )
The code here is never executed too. It only constructs one small piece of very big expression. But for the sake
of simplicity, one can think all these functions will be called during bruteforce search, many times, at fastest possible
speed.
# Operation , Bool , Register , Register , Int , Registers_State -> int
def one_op (op , register_or_constant , reg1 , reg2 , constant , input_registers ):
arg1= register_selector (reg1 , input_registers )
445
arg2= register_or_constant_selector ( register_or_constant , reg2 , input_registers ,
constant )
return If(op == OP_MULT , arg1*arg2 ,
If(op == OP_MINUS , arg1 -arg2 ,
If(op == OP_PLUS , arg1+arg2 ,
If(op == OP_XOR , arg1^arg2 ,
If(op == OP_OR , arg1|arg2 ,
If(op == OP_AND , arg1&arg2 ,
0)))))) # default
Here is the expression describing each instruction. new_val will be assigned to destination register, while all other
registers’ values are copied from input registers’ state:
# Bool , Register , Operation , Register , Register , Int , Registers_State ->
Registers_State
def one_step ( register_or_constant , register_assigned_in_this_step , op , reg1 , reg2 ,
constant , input_registers ):
new_val = one_op (op , register_or_constant , reg1 , reg2 , constant , input_registers )
return If ( register_assigned_in_this_step ==A, Registers_State .cons (new_val ,
Registers_State .B( input_registers ),
Registers_State .C( input_registers ),
Registers_State .D( input_registers )),
If ( register_assigned_in_this_step ==B, Registers_State .cons (
Registers_State .A( input_registers ),
new_val ,
Registers_State .C( input_registers ),
Registers_State .D( input_registers )),
If ( register_assigned_in_this_step ==C, Registers_State .cons (
Registers_State .A( input_registers ),
Registers_State .B( input_registers ),
new_val ,
Registers_State .D( input_registers )),
If ( register_assigned_in_this_step ==D, Registers_State .cons (
Registers_State .A( input_registers ),
Registers_State .B( input_registers ),
Registers_State .C( input_registers ),
new_val ),
Registers_State .cons (0 ,0 ,0 ,0))))) #
default
Again, for the sake of simplicity, it can be said, now Z3 will try each possible registers/operations/constants against
this expression to find such combination which satisfy all input/output pairs. Sounds absurd, but this is close to reality.
SAT/SMT-solvers indeed tries them all. But the trick is to prune search tree as early as possible, so it will work for
some reasonable time. And this is hardest problem for solvers.
Now let’s start with very simple 3-step algorithm: B=A^D, C=D*D, D=A*C. Please note: register A left unchanged.
I programmed Rockey 4 dongle with the algorithm, and recorded algorithm outputs are:
RY_CALCULATE1 : ( input ) p1 =8803 p2 =59946 p3 =36002 p4 =44743 ( output ) p1 =8803 p2 =36004
p3 =7857 p4 =24691
RY_CALCULATE1 : ( input ) p1 =5814 p2 =55512 p3 =52155 p4 =55813 ( output ) p1 =5814 p2 =52403
p3 =33817 p4 =4038
RY_CALCULATE1 : ( input ) p1 =25206 p2 =2097 p3 =55906 p4 =22705 ( output ) p1 =25206 p2 =15047
p3 =10849 p4 =43702
RY_CALCULATE1 : ( input ) p1 =10044 p2 =14647 p3 =27923 p4 =7325 ( output ) p1 =10044 p2 =15265
p3 =47177 p4 =20508
446
RY_CALCULATE1 : ( input ) p1 =15267 p2 =2690 p3 =47355 p4 =56073 ( output ) p1 =15267 p2 =57514
p3 =26193 p4 =53395
It took about one second and only 5 pairs above to find algorithm (on my quad-core Xeon E3-1220 3.1GHz, however,
Z3 solver working in single-thread mode):
B = A ^ D
C = D * D
D = C * A
Note the last instruction: C and A registers are swapped comparing to version I wrote by hand. But of course, this
instruction is working in the same way, because multiplication is commutative operation.
Now if I try to find 4-step program satisfying to these values, my script will offer this:
B = A ^ D
C = D * D
D = A * C
A = A | A
…and that’s really fun, because the last instruction do nothing with value in register A, it’s like NOP3 —but still,
algorithm is correct for all values given.
Here is another 5-step algorithm: B=B^D, C=A*22, A=B*19, A=A&42, D=B&C and values:
RY_CALCULATE1 : ( input ) p1 =61876 p2 =28737 p3 =28636 p4 =50362 ( output ) p1 =32 p2 =46331 p3
=50552 p4 =33912
RY_CALCULATE1 : ( input ) p1 =46843 p2 =43355 p3 =39078 p4 =24552 ( output ) p1 =8 p2 =63155 p3
=47506 p4 =45202
RY_CALCULATE1 : ( input ) p1 =22425 p2 =51432 p3 =40836 p4 =14260 ( output ) p1 =0 p2 =65372 p3
=34598 p4 =34564
RY_CALCULATE1 : ( input ) p1 =44214 p2 =45766 p3 =19778 p4 =59924 ( output ) p1 =2 p2 =22738 p3
=55204 p4 =20608
RY_CALCULATE1 : ( input ) p1 =27348 p2 =49060 p3 =31736 p4 =59576 ( output ) p1 =0 p2 =22300 p3
=11832 p4 =1560
A=A&42 was correctly deduced (look at these five p1’s at output (assigned to output A register): 32,8,0,2,0)
6-step algorithm A=A+B, B=C*13, D=D^A, C=C&A, D=D|B, A=A&B and values:
RY_CALCULATE1 : ( input ) p1 =4110 p2 =35411 p3 =54308 p4 =47077 ( output ) p1 =32832 p2 =50644
p3 =36896 p4 =60884
RY_CALCULATE1 : ( input ) p1 =12038 p2 =7312 p3 =39626 p4 =47017 ( output ) p1 =18434 p2 =56386
p3 =2690 p4 =64639
RY_CALCULATE1 : ( input ) p1 =48763 p2 =27663 p3 =12485 p4 =20563 ( output ) p1 =10752 p2 =31233
p3 =8320 p4 =31449
RY_CALCULATE1 : ( input ) p1 =33174 p2 =38937 p3 =54005 p4 =38871 ( output ) p1 =4129 p2 =46705
p3 =4261 p4 =48761
RY_CALCULATE1 : ( input ) p1 =46587 p2 =36275 p3 =6090 p4 =63976 ( output ) p1 =258 p2 =13634 p3
=906 p4 =48966
Conclusion
This is in fact an exercise in program synthesis.
Some short algorithms for tiny CPUs are really possible to find using so small set set of data. Of course it’s still
not possible to reveal some complex algorithm, but this method definitely should not be ignored.
The files
Rockey 4 dongle programmer and reader, Rockey 4 manual, Z3Py script for finding algorithms, input/output pairs:
https://smt.st/current_tree/synth/pgm/rockey
Further work
Perhaps, constructing LISP-like S-expression can be better than a program for toy-level CPU.
It’s also possible to start with smaller constants and then proceed to bigger. This is somewhat similar to increasing
password length in password brute-force cracking.
Exercise
https://challenges.re/25/.
15.2.3 TAOCP 7.1.3 Exercise 198, UTF-8 encoding and program synthesis by sketch-
ing
Found this exercise in TAOCP 7.1.3 (Bitwise Tricks and Techniques):
This is like program synthesis by sketching: you give a sketch with several “holes” missing and ask some automated
software to fill the “holes”. In our case, a, b and c are “holes”.
Let’s find them using Z3:
#!/ usr/bin/env python3
from z3 import *
s= Solver ()
print ("a,b,c = 0x%x 0x%x 0x%x" % (m[a]. as_long () , m[b]. as_long () , m[c]. as_long ()
))
I tried various bit widths for a, b and c and found that 22 bits are enough. I’ve lots of results like:
...
...
It seems that several least significant bits of a and c are not used. After little experimentation, I’ve come to this:
...
...
Pick any.
But how it works? Its operation is very similar to the bitwise trick related to leading/trailing zero bits counting
based on De Bruijn sequences. Read more about it in Untitled collection of math notes4 .
The problem is small enough to be tackled by MK85: https://smt.st/current_tree/synth/pgm/TAOCP_713_
198/TAOCP_713_198_MK85.py
Bruteforce is feasible here.
15.2.4 TAOCP 7.1.3 Exercise 203, MMIX MOR instruction and program synthesis
by sketching
Found this exercise in TAOCP 7.1.3 (Bitwise Tricks and Techniques):
( http://mmix.cs.hm.edu/doc/mmix-doc.pdf )
Let’s try to solve. We create two functions. First has MOR instructions simulation + the program from TAOCP.
The second is a naive implementation. Then we add “forall” quantifier: for all inputs, both functions must produce
the same result. But, we don’t know a/b/c/d/e/f and ask Z3 SMT-solver to
#!/ usr/bin/env python3
from z3 import *
s= Solver ()
set_param (" parallel . enable ", True)
$x_i_j = y_0_j z_i_0 \vee y_1_j z_i_1 \vee \cdots \vee y_7_j z_i_7$
https :// latexbase .com/d/bf2243f8 -5d0b -4231 -8891 -66 fb47d846f0
IOW:
x<byte ><bit > = (y<0><bit > AND z<byte ><0>) OR (y<1><bit > AND z<byte ><1>) OR ... OR
(y<7><bit > AND z<byte ><7>)
"""
return rt
#"""
# new version .
# for all possible 32- bit x's, find such a/b/c/d/e, so that these two parts would be
equal to each other
# zero extend x to 64- bit value in both cases
x= BitVec ('x', 32)
s.add( ForAll ([x], simulate_pgm ( ZeroExt (32 , x))== method2 ( ZeroExt (32 , x))))
#"""
"""
# previous version :
for i in range (5):
x= random . getrandbits (32)
t ="%08 x" % x
y=int(''. join ("%02 X" % ord(c) for c in t), 16)
print ("%x %x" % (x, y))
Very slow, it takes 30m on Intel Quad-Core Xeon E3-1270 v3 3.50GHz but found at least one solution:
a,b,c,d,e = 8000400020001 f0f0f0f0f0f0f0f 56 d656d616969616 411 a00000000
bf3fbf3fff7f8000
...
Will it be possible to generate such a sequence of instructions, so that the arbitrary 32-bit constant would be
loaded into EAX register? Given the fact that the initial value of EAX is unknown, because, let’s say, we can’t reset
it? Surely, all 32-bit operands must have ASCII-only bytes as well.
The answer is... using Z3 SMT-solver:
#!/ usr/bin/env python3
from z3 import *
453
import sys , random
BIT_WIDTH =32
MAX_STEPS =20
# CONST =0
# CONST =0 x12345678
# CONST =0 x0badf00d
# CONST =0 xffffffff
CHAINS =30
R=[[ BitVec ('S_s%d_c%d' % (s, c), BIT_WIDTH ) for s in range ( MAX_STEPS )] for c in
range ( CHAINS )]
op =[ Int('op_s%d' % s) for s in range ( MAX_STEPS )]
op2_imm =[ BitVec ('op2_imm_s %d' % s, BIT_WIDTH ) for s in range ( MAX_STEPS )]
"""
# or use 0..9 , a..z, A..Z:
for shift_cnt in [0 ,8 ,16 ,24]:
sl.add(Or(
And (( op2_imm [s]>> shift_cnt )&0xff >= ord ('0') ,( op2_imm [s]>> shift_cnt )&0
xff <= ord ('9')),
And (( op2_imm [s]>> shift_cnt )&0xff >= ord('a ') ,( op2_imm [s]>> shift_cnt )&0
xff <= ord('z ')),
And (( op2_imm [s]>> shift_cnt )&0xff >= ord('A ') ,( op2_imm [s]>> shift_cnt )&0
xff <= ord('Z '))))
"""
454
tmp=sl.check ()
if tmp == sat:
print ("sat!")
m=sl. model ()
print_model (m, STEPS , op , op2_imm )
exit (0)
else:
print (tmp)
These two instructions clears EAX. You can understand how it works if you’ll see these operands in binary form:
0 x3e5a3e28 = 111110010110100011111000101000
0 x40214040 = 1000000001000010100000001000000
It’s best to have a zero bit for both operands, but this is not always possible, because each of 4 bytes in 32-bit
operand must be in [0x21..0x7e] range, so Z3 solver find a way to reset other bits using second instruction.
Running it again:
AND EAX , 0 x3c5e3621
AND EAX , 0 x42214850
First two AND instruction clears EAX, 3th and 4th makes 0x0badf00d value.
Now 0x12345678:
AND EAX , 0 x41212230
XOR EAX , 0 x292f2224
AND EAX , 0 x365e4048
XOR EAX , 0 x323a5678
Further work: use ForAll quantifier instead of randomly generated test inputs... also, we could try INC EAX/DEC
EAX instructions.
What about bruteforce? Each instruction is at least 32 bits... Not feasible, unless you can find heuristics...
This example shows how to design a finite automaton E_2 to recognize the regular
language of all strings that contain the string 001 as a substring . For example ,
0010 , 1001 , 001 , and 11111110011111 are all in the language , but 11 and 0000
are not. How would you recognize this language if you were pretending to
be E_2?
state =[[ Int('state_ %d_%d' % (s, b)) for b in range (2)] for s in range (100) ]
INITIAL_STATE =0
INVALID_STATE =999
# construct FA for z3
456
def transition (STATES , s, i):
# this is like switch ()
# construct FA for z3
def FA(STATES , input_string ):
s= IntVal ( INITIAL_STATE )
for i in input_string :
s= transition (STATES , s, int(i))
return s
FA ={}
for s in range ( STATES ):
f. write ("\tS_%d -> S_%d [ label = \"0\" ];\n" % (s, m[state [s ][0]]. as_long ())
)
f. write ("\tS_%d -> S_%d [ label = \"1\" ];\n" % (s, m[state [s ][1]]. as_long ())
)
FA[s]=(m[state [s ][0]]. as_long () , m[ state [s ][1]]. as_long ())
f.write ("}\n")
f.close ()
os. system ("dot -Tpng 1.gv > 1. png") # run GraphViz
ACCEPTING_STATE =STATES -1
result =[]
457
if sl. check () == unsat :
return
m = sl.model ()
print_model (STATES , m)
exit (0)
• https://stackoverflow.com/questions/7974655/regex-for-binary-multiple-of-3
• https://stackoverflow.com/questions/844867/check-if-a-number-is-divisible-by-3
• https://stackoverflow.com/questions/867279/regular-expression-to-define-some-binary-sequence
• https://www.regextester.com/96234
state =[[ Int('state_ %d_%d' % (s, b)) for b in range (2)] for s in range (100) ]
INITIAL_STATE =0
INVALID_STATE =999
# construct FA for z3
def transition (STATES , s, i):
# this is like switch ()
# construct FA for z3
def FA(STATES , input_string ):
s= IntVal ( INITIAL_STATE )
for i in input_string :
s= transition (STATES , s, int(i))
return s
458
# simulate FA for testing purpose :
def simulate_FA (input_ , FA):
s= INITIAL_STATE
for i in input_ :
if i== '0':
s=FA[s][0]
else:
s=FA[s][1]
return s
FA ={}
for s in range ( STATES ):
f. write ("\tS_%d -> S_%d [ label = \"0\" ];\n" % (s, m[state [s ][0]]. as_long ())
)
f. write ("\tS_%d -> S_%d [ label = \"1\" ];\n" % (s, m[state [s ][1]]. as_long ())
)
FA[s]=(m[state [s ][0]]. as_long () , m[ state [s ][1]]. as_long ())
f.write ("}\n")
f.write ("// STATES =%d\n" % STATES )
f.close ()
os. system ("dot -Tpng "+ fname_base +".gv > "+ fname_base +".png") # run GraphViz
test_FA (DIVISOR , STATES , FA)
ACCEPTING_STATE =STATES -1
result =[]
As you can see, it has testing procedure, which is, in turn, can be used instead of RE matcher, if you really need
to match numbers divisible by 3.
States in double circles — initial (S_0) and accepting:
... is almost like the one someone posted here, but my solution has two separate states as initial and accepting.
Some of solutions are hard to find manually.
DFAs are bigger for prime numbers, of course, and smaller for even numbers.
460
Figure 15.19: DFA for numbers divisible by 11 (12 states) (may be not minimal)
May not be minimal again. Search failed for 11, 12, 13 states (one hour for each), but found for 14 states:
Figure 15.21: DFA for numbers divisible by 13 (14 states) (may be not minimal)
464
May be not be minimal. Search failed for 10..15 states, but found for 16 states:
Figure 15.23: DFA for numbers divisible by 15 (16 states) (may be not minimal)
Further work
Convert DFAs to RE...
465
What about bruteforce?
For 10 vertices, you have to enumerate (10 · 10)10 = 1020 DFAs, or log2 (10 · 10)10 ≈ 66 bits.
The files
https://smt.st/current_tree/synth/DFA
466
A problem from the “Digital Logic RTL & Verilog Interview Questions” book
Figure 15.25: A problem from the “Digital Logic RTL & Verilog Interview Questions” book
467
5
I’ve found the problem here (Russian Facebook post). The book is available at amazon . More information on its
website.
15.4 Other
15.4.1 Graph theory: degree sequence problem / graph realization problem
The degree sequence of an undirected graph is the non-increasing sequence of its vertex degrees;[2]
for the above graph it is (5, 3, 3, 2, 2, 1, 0). The degree sequence is a graph invariant so isomorphic
graphs have the same degree sequence. However, the degree sequence does not, in general, uniquely
identify a graph; in some cases, non-isomorphic graphs have the same degree sequence.
The degree sequence problem is the problem of finding some or all graphs with the degree sequence
being a given non-increasing sequence of positive integers. (Trailing zeroes may be ignored since they
are trivially realized by adding an appropriate number of isolated vertices to the graph.) A sequence
which is the degree sequence of some graph, i.e. for which the degree sequence problem has a solution,
is called a graphic or graphical sequence. As a consequence of the degree sum formula, any sequence
with an odd sum, such as (3, 3, 1), cannot be realized as the degree sequence of a graph. The converse
is also true: if a sequence has an even sum, it is the degree sequence of a multigraph. The construction
of such a graph is straightforward: connect vertices with odd degrees in pairs by a matching, and fill
out the remaining even degree counts by self-loops. The question of whether a given degree sequence
can be realized by a simple graph is more challenging. This problem is also called graph realization
problem and can either be solved by the Erdős–Gallai theorem or the Havel–Hakimi algorithm. The
problem of finding or estimating the number of graphs with a given degree sequence is a problem from
the field of graph enumeration.
( https://en.wikipedia.org/wiki/Degree_(graph_theory) )
The graph realization problem is a decision problem in graph theory . Given a finite
sequence ( d 1 , … , d n )
{\ displaystyle (d_ {1} ,\ dots ,d_{n})} {\ displaystyle (d_ {1} ,\ dots ,d_{n})} of natural
numbers , the problem asks whether there is
labeled simple graph such that ( d 1 , … , d n ) {\ displaystyle (d_ {1} ,\ dots ,d_{n})}
{\ displaystyle (d_ {1} ,\ dots ,d_{n})}
is the degree sequence of this graph .
( https://en.wikipedia.org/wiki/Graph_realization_problem )
I can solve this using Z3 SMT solver, however, isomorphic graphs are not being weed out... the result is then
rendered using GraphViz.
#!/ usr/bin/env python3
# "The degree sequence problem is the problem of finding some or all graphs with
# the degree sequence being a given non - increasing sequence of positive integers ."
# ( https :// en. wikipedia .org/wiki/ Degree_ ( graph_theory ) )
from z3 import *
import subprocess
5 https://www.amazon.com/Digital-Logic-Verilog-Interview-Questions/dp/1512021466
468
BV_WIDTH =8
seq =[8 ,8 ,7 ,7 ,6 ,6 ,4 ,3 ,2 ,1 ,1 ,1] # https :// math. stackexchange .com/ questions /1074651/
check -if -sequence -is -graphic -8 -8 -7 -7 -6 -6 -4 -3 -2 -1 -1 -1
vertices =len(seq)
if (sum(seq) & 1) == 1:
print ("not a graphical sequence ")
exit (0)
edges =int(sum(seq)/2)
print (" edges =", edges )
# for each edge , edges_begin [] and edges_end [] pair defines a vertex numbers , which
they connect :
edges_begin =[ BitVec ('edges_begin_ %d' % i, BV_WIDTH ) for i in range ( edges )]
edges_end =[ BitVec ('edges_end_ %d' % i, BV_WIDTH ) for i in range ( edges )]
s= Solver ()
gv_no =0
Exercise
... from the “Pearls in Graph Theory” book:
1.1.1. Seven students go on vacations. They decide that each will send a postcard to three of the
others. Is it possible that every student receives postcards from precisely the three to whom he sent
postcards?
No, it’s not possible, because 7*3 is a odd number. However, if you reduce 7 students to 6, this is solvable, the
sequence is [3,3,3,3,3,3].
Now the graph of mutual exchanging of postcards between 6 students:
471
Toy decompiler
16.1 Introduction
A modern-day compiler is a product of hundreds of developer/year. At the same time, toy compiler can be an exercise
for a student for a week (or even weekend).
Likewise, commercial decompiler like Hex-Rays can be extremely complex, while toy decompiler like this one, can
be easy to understand and remake.
The following decompiler written in Python, supports only short basic blocks, with no jumps. Memory is also not
supported.
* 9
- 5
INPUT 32
How to store it in memory? We see here 3 types of nodes: 1) numbers (or values); 2) arithmetical operations; 3)
symbols (like “INPUT”).
Many developers with OOP1 in their mind will create some kind of class. Other developer maybe will use “variant
type”.
I’ll use simplest possible way of representing this structure: a Python tuple. First element of tuple can be a string:
either “EXPR_OP” for operation, “EXPR_SYMBOL” for symbol or “EXPR_VALUE” for value. In case of symbol
or value, it follows the string. In case of operation, the string followed by another tuples.
Node type and operation type are stored as plain strings—to make debugging output easier to read.
There are constructors in our code, in OOP sense:
1 Object-oriented programming
472
473
"EXPR_OP"
"/"
"EXPR_OP" "EXPR_VALUE"
"*" 9
"EXPR_OP" "EXPR_VALUE"
"-" 5
"EXPR_SYMBOL" "EXPR_VALUE"
"arg1" 32
At start, these symbols are assigned to registers: RAX=initial_RAX, RBX=initial_RBX, RDI=arg1, RSI=arg2,
RDX=arg3, RCX=arg4.
When we handle MOV instruction, we just copy expression from RDI to RAX. When we handle IMUL instruction,
we create a new expression, adding together expressions from RAX and RSI and putting result into RAX again.
I can feed this to decompiler and we will see how register’s state is changed through processing:
python td.py --show - registers --python -expr tests /mul.s
...
...
result =(' EXPR_OP ', '*', (' EXPR_SYMBOL ', 'arg1 ') , (' EXPR_SYMBOL ', 'arg2 '))
IMUL instruction is mapped to “*” string, and then new expression is constructed in handle_binary_op(), which
puts result into RAX.
In this output, the data structures are dumped using Python str() function, which does mostly the same, as
print().
Output is bulky, and we can turn off Python expressions output, and see how this internal data structure can be
rendered neatly using our internal expr_to_string() function:
python td.py --show - registers tests /mul.s
...
...
...
...
result =(' EXPR_OP ', '+', ('EXPR_OP ', '*', (' EXPR_SYMBOL ', 'arg1 ') , (' EXPR_SYMBOL ', '
arg2 ')), (' EXPR_SYMBOL ', 'arg3 '))
...
...
...
result =(' EXPR_OP ', '+', ('EXPR_OP ', '*', (' EXPR_SYMBOL ', 'arg1 ') , (' EXPR_VALUE ',
1234)), ('EXPR_OP ', '*', (' EXPR_SYMBOL ', 'arg2 ') , (' EXPR_VALUE ', 5678) ))
...
You can see, how register’s state is changed over execution (or parsing).
Raw:
python td.py --show - registers --python -expr tests / fahr_to_celsius .s
...
...
result =(' EXPR_OP ', '/', ('EXPR_OP ', '*', ('EXPR_OP ', '-', (' EXPR_SYMBOL ', 'arg1 ') , ('
EXPR_VALUE ', 32)), (' EXPR_VALUE ', 5)), (' EXPR_VALUE ', 9))
Neat:
python td.py --show - registers tests / fahr_to_celsius .s
...
...
It is interesting to note that IDIV instruction also calculates reminder of division, and it is placed into RDX register.
It’s not used, but is available for use.
This is how quotient and remainder are stored in registers:
def handle_unary_DIV_IDIV (registers , op1):
op1_expr = register_or_number_in_string_to_expr (registers , op1)
current_RAX = registers [" rax "]
registers [" rax "]= create_binary_expr ("/" , current_RAX , op1_expr )
registers [" rdx "]= create_binary_expr ("%" , current_RAX , op1_expr )
; rdi=i
; rsi=grain
sub rsi , 1
add rdi , rsi
not rsi
and rdi , rsi
mov rax , rdi
...
...
…will be transformed into (arg1 + arg1) expression. It can be reduced to (arg1 * 2). Our toy decompiler can
identify patterns like such and rewrite them.
# X+X -> X*2
def reduce_ADD1 (expr):
if is_expr_op (expr) and get_op (expr) =="+" and get_op1 (expr)== get_op2 (expr):
return dbg_print_reduced_expr (" reduce_ADD1 ", expr , create_binary_expr ("*" ,
get_op1 (expr), create_val_expr (2)))
This function will just test, if the current node has EXPR_OP type, operation is “+” and both children are equal
to each other. By the way, since our data structure is just tuple of tuples, Python can compare them using plain
“==” operation. If the testing is finished successfully, current node is then replaced with a new expression: we take
one of children, we construct a node of EXPR_VALUE type with “2” number in it, and then we construct a node of
EXPR_OP type with “*”.
dbg_print_reduced_expr() serving solely debugging purposes—it just prints the old and the new (reduced)
expressions.
Decompiler is then traverse expression tree recursively in deep-first search fashion.
def reduce_step (e):
if is_expr_op (e)== False :
480
return e # expr isn 't EXPR_OP , nothing to reduce (we don 't reduce EXPR_SYMBOL
and EXPR_VAL )
...
...
...
We take input expression, and we also construct pattern to be matched. Matcher works recursively over both
expressions synchronously. Pattern is also expression, but can use two additional node types: EXPR_WILDCARD
and EXPR_WILDCARD_VALUE. These nodes are supplied with keys (stored as strings). When matcher encoun-
ters EXPR_WILDCARD in pattern, it just stashes current expression and will return it. If matcher encounters
EXPR_WILDCARD_VALUE, it does the same, but only in case the current node has EXPR_VALUE type.
bind_expr() and bind_value() are functions which create nodes with the types we have seen.
All this means, reduce_MUL1() function will search for the expression in form (X*A)*B, where A and B are
numbers. In other cases, matcher will return input expression untouched, so these reducing function can be chained.
Now when reduce_MUL1() encounters (sub)expression we are interesting in, it will return dictionary with keys and
expressions. Let’s add print m call somewhere before return and rerun:
python td.py tests /add2.s
...
going to reduce ((( arg1 + arg1) + (arg1 + arg1)) + (( arg1 + arg1) + (arg1 + arg1)))
reduction in reduce_ADD1 () (arg1 + arg1) -> (arg1 * 2)
reduction in reduce_ADD1 () (arg1 + arg1) -> (arg1 * 2)
reduction in reduce_ADD1 () (( arg1 * 2) + (arg1 * 2)) -> (( arg1 * 2) * 2)
{'A': 2, 'X': (' EXPR_SYMBOL ', 'arg1 ') , 'B ': 2}
reduction in reduce_MUL1 () (( arg1 * 2) * 2) -> (arg1 * 4)
reduction in reduce_ADD1 () (arg1 + arg1) -> (arg1 * 2)
reduction in reduce_ADD1 () (arg1 + arg1) -> (arg1 * 2)
reduction in reduce_ADD1 () (( arg1 * 2) + (arg1 * 2)) -> (( arg1 * 2) * 2)
{'A': 2, 'X': (' EXPR_SYMBOL ', 'arg1 ') , 'B ': 2}
reduction in reduce_MUL1 () (( arg1 * 2) * 2) -> (arg1 * 4)
reduction in reduce_ADD1 () (( arg1 * 4) + (arg1 * 4)) -> (( arg1 * 4) * 2)
{'A': 4, 'X': (' EXPR_SYMBOL ', 'arg1 ') , 'B ': 2}
reduction in reduce_MUL1 () (( arg1 * 4) * 2) -> (arg1 * 8)
going to reduce (arg1 * 8)
483
...
result =( arg1 * 8)
The dictionary has keys we supplied plus expressions matcher found. We then can use them to create new expression
and return it. Numbers are just summed while forming second operand to “*” operation.
Now a real-world optimization technique—optimizing GCC replaced multiplication by 31 by shifting and subtrac-
tion operations:
mov rax , rdi
sal rax , 5
sub rax , rdi
Without reduction functions, our decompiler will translate this into ((arg1 « 5) - arg1). We can replace shifting
left by multiplication:
# X<<n -> X*(2^n)
def reduce_SHL1 (expr):
m= match (expr , create_binary_expr ("<<", bind_expr ("X") , bind_value ("Y")))
if m== None:
return expr # no match
Now we getting ((arg1 * 32) - arg1). We can add another reduction function:
# (X*n)-X -> X*(n -1)
def reduce_SUB3 (expr):
m= match (expr , create_binary_expr ("-",
create_binary_expr ("*" , bind_expr (" X1 ") , bind_value ("N")),
bind_expr (" X2 ")))
Matcher will return two X’s, and we must be assured that they are equal. In fact, in previous versions of this toy
decompiler, I did comparison with plain “==”, and it worked. But we can reuse match() function for the same purpose,
because it will process commutative operations better. For example, if X1 is “Q+1” and X2 is “1+Q”, expressions are
equal, but plain “==” will not work. On the other side, match() function, when encounter “+” operation (or another
commutative operation), and it fails with comparison, it will also try swapped operand and will try to compare again.
However, to understand it easier, for a moment, you can imagine there is “==” instead of the second match().
Anyway, here is what we’ve got:
working out tests / mul31_GCC .s
going to reduce (( arg1 << 5) - arg1)
reduction in reduce_SHL1 () (arg1 << 5) -> (arg1 * 32)
reduction in reduce_SUB3 () (( arg1 * 32) - arg1) -> (arg1 * 31)
going to reduce (arg1 * 31)
...
result =( arg1 * 31)
Another optimization technique is often seen in ARM thumb code: AND-ing a value with a value like 0xFFFFFFF0,
is implemented using shifts:
mov rax , rdi
shr rax , 4
shl rax , 4
This code is quite common in ARM thumb code, because it’s a headache to encode 32-bit constants using couple
of 16-bit thumb instructions, while single 16-bit instruction can shift by 4 bits left or right.
Also, the expression (x»4)«4 can be jokingly called as “twitching operator”: I’ve heard the “--i++” expression was
called like this in Russian-speaking social networks, it was some kind of meme (“operator podergivaniya”).
Anyway, these reduction functions will be used:
484
...
...
# FIXME: slow
# returns True if n=2^x or popcnt (n)=1
def is_2n (n):
return bin(n). count ("1") ==1
result = x
3
=x· 1
3
=x· 1·M agicN umber
3·M agicN umber
Given the fact that division by 2n is very fast, we now should find that M agicN umber, for which the following
equation will be true: 2n = 3 · M agicN umber.
This code performing division by 10:
mov rax , rdi
movabs rdx , 0 cccccccccccccccdh
mul rdx
shr rdx , 3
mov rax , rdx
Division by 264 is somewhat hidden: lower 64-bit of product in RAX is not used (dropped), only higher 64-bit of
product (in RDX) is used and then shifted by additional 3 bits.
RDX register is set during processing of MUL/IMUL like this:
def handle_unary_MUL_IMUL (registers , op1):
op1_expr = register_or_number_in_string_to_expr (registers , op1)
result = create_binary_expr ("*" , registers [" rax "], op1_expr )
registers [" rax "]= result
registers [" rdx "]= create_binary_expr (">>", result , create_val_expr (64))
0cccccccccccccccdh
In other words, the assembly code we have just seen multiplication operations by 264+3 , or divides
64+3
2
by 0cccccccccccccccdh . To find divisor we just have to divide numerator by denominator.
# n = magic number
# m = shifting coefficient
# return = 1 / (n / 2^m) = 2^m / n
def get_divisor (n, m):
return (2** float (m))/ float (n)
This works, but we have a problem: this rule takes (arg1 * 0xcccccccccccccccd) » 64 expression first and finds divisor
to be equal to 1.25. This is correct: result is shifted by 3 bits after (or divided by 8), and 1.25 · 8 = 10. But our toy
decompiler doesn’t support real numbers.
We can solve this problem in the following way: if divisor has fractional part, we postpone reducing, with a hope,
that two subsequent right shift operations will be reduced into single one:
# (X*n)>>m, where m >=64 -> X/...
def reduce_div_by_MUL (expr):
m= match (expr , create_binary_expr (">>", create_binary_expr ("*" , bind_expr ("X") ,
bind_value ("N")), bind_value ("M")))
if m== None:
return expr # no match
That works:
working out tests / div_by_mult10_unsigned .s
going to reduce ((( arg1 * 0 xcccccccccccccccd ) >> 64) >> 3)
reduce_div_by_MUL (): postponing reduction , because divisor = 1.25
reduction in reduce_SHR1 () ((( arg1 * 0 xcccccccccccccccd ) >> 64) >> 3) -> (( arg1 * 0
xcccccccccccccccd ) >> 67)
going to reduce (( arg1 * 0 xcccccccccccccccd ) >> 67)
reduction in reduce_div_by_MUL () (( arg1 * 0 xcccccccccccccccd ) >> 67) -> (arg1 / 10)
going to reduce (arg1 / 10)
result =( arg1 / 10)
I don’t know if this is best solution. In early version of this decompiler, it processed input expression in two passes:
first pass for everything except division by multiplication, and the second pass for the latter. I don’t know which way
is better. Or maybe we could support real numbers in expressions?
Couple of words about better understanding division by multiplication. Many people miss “hidden” division by 232
or 264 , when lower 32-bit part (or 64-bit part) of product is not used (or just dropped). Also, there is misconception
that modulo inverse is used here. This is close, but not the same thing. Extended Euclidean algorithm is usually used
to find magic coefficient, but in fact, this algorithm is rather used to solve the equation. You can solve it using any
other method. Also, needless to mention, the equation is unsolvable for some divisors, because this is diophantine
equation (i.e., equation allowing result to be only integer), since we work on integer CPU registers, after all.
16.5 Obfuscation/deobfuscation
Despite simplicity of our decompiler, we can see how to deobfuscate (or optimize) using several simple tricks.
For example, this piece of code does nothing:
mov rax , rdi
xor rax , 12345678 h
xor rax , 0 deadbeefh
xor rax , 12345678 h
xor rax , 0 deadbeefh
if m!= None:
return dbg_print_reduced_expr (" reduce_XOR4 ", expr , create_binary_expr ("^" ,
m["X"],
create_val_expr (m["N"]^m["M"])))
else:
return expr # no match
...
# default :
487
return expr # no match
I also used aha! 4 superoptimizer to find weird piece of code which does nothing.
Aha! is so called superoptimizer, it tries various piece of codes in brute-force manner, in attempt to find shortest
possible alternative for some mathematical operation. While sane compiler developers use superoptimizers for this
task, I tried it in opposite way, to find oddest pieces of code for some simple operations, including NOP operation. In
past, I’ve used it to find weird alternative to XOR operation (4.1).
So here is what aha! can find for NOP:
; do nothing (as found by aha)
# X & X -> X
def reduce_AND3 (expr):
m= match (expr , create_binary_expr ("&" , bind_expr (" X1 ") , bind_expr (" X2 ")))
if m!= None and match (m[" X1"], m[" X2 "]) != None:
return dbg_print_reduced_expr (" reduce_AND3 ", expr , m[" X1 "])
else:
return expr # no match
...
# X | X -> X
def reduce_OR1 (expr):
m= match (expr , create_binary_expr ("|" , bind_expr (" X1 ") , bind_expr (" X2 ")))
if m!= None and match (m[" X1"], m[" X2 "]) != None:
return dbg_print_reduced_expr (" reduce_OR1 ", expr , m[" X1 "])
else:
return expr # no match
4 http://www.hackersdelight.org/aha/aha.pdf
488
This is weirder:
; do nothing (as found by aha)
Rules added (I used “NEG” string to represent sign change and to be different from subtraction operation, which
is just minus (“-”)):
# (op(op X)) -> X, where both ops are NEG or NOT
def reduce_double_NEG_or_NOT (expr):
# try each:
for op in [" NEG", "∼"]:
m=match (expr , create_unary_expr (op , create_unary_expr (op , bind_expr ("X")))
)
if m!= None:
return dbg_print_reduced_expr (" reduce_double_NEG_or_NOT ", expr , m["X"])
# default :
return expr # no match
...
I also forced aha! to find piece of code which adds 2 with no addition/subtraction operations allowed:
; arg1 +2, without add/sub allowed , as found by aha:
489
;Found a 4- operation program :
; not r1 ,rx
; neg r2 ,r1
; not r3 ,r2
; neg r4 ,r3
; Expr: -(∼(-(∼(x))))
Rule:
# (- (∼X)) -> X+1
def reduce_NEG_NOT (expr):
m= match (expr , create_unary_expr (" NEG", create_unary_expr ("∼", bind_expr ("X")))
)
if m== None:
return expr # no match
This is artifact of two’s complement system of signed numbers representation. Same can be done for subtraction
(just swap NEG and NOT operations).
Now let’s add some fake luggage to Fahrenheit-to-Celsius example:
; celsius = 5 * (fahr -32) / 9
; fake luggage :
mov rbx , 12345 h
mov rax , rdi
sub rax , 32
; fake luggage :
add rbx , rax
imul rax , 5
mov rbx , 9
idiv rbx
; fake luggage :
sub rdx , rax
It’s not a problem for our decompiler, because the noise is left in RDX register, and not used at all:
working out tests / fahr_to_celsius_obf1 .s
line =[ mov rbx , 12345 h]
rcx=arg4
rsi=arg2
rbx =0 x12345
rdx=arg3
rdi=arg1
rax= initial_RAX
…but in fact, it’s all reduced by reduce_AND2() function we already saw (16.5):
working out tests / fahr_to_celsius_obf2 .s
going to reduce (((( arg1 - 32) * 5) / 9) & (((( arg1 - 32) * 5) / 9) | (((( arg1 - 32)
* 5) % 9) - ((( arg1 - 32) * 5) / 9))))
reduction in reduce_AND2 () (((( arg1 - 32) * 5) / 9) & (((( arg1 - 32) * 5) / 9) | ((((
arg1 - 32) * 5) % 9) - ((( arg1 - 32) * 5)
/ 9)))) -> ((( arg1 - 32) * 5) / 9)
going to reduce ((( arg1 - 32) * 5) / 9)
result =((( arg1 - 32) * 5) / 9)
We can see that deobfuscation is in fact the same thing as optimization used in compilers. We can try this function
in GCC:
int f(int a)
{
return -(∼a);
};
GCC has its own rewriting rules, some of which are, probably, close to what we use here.
16.6 Tests
Despite simplicity of the decompiler, it’s still error-prone. We need to be sure that original expression and reduced
one are equivalent to each other.
In fact, this is very close to what LISP EVAL function does, or even LISP interpreter. However, not all symbols are
set. If the expression is using initial values from RAX or RBX (to which symbols “initial_RAX” and “initial_RBX”
are assigned, decompiler will stop with exception, because no random values assigned to these registers, and these
symbols are absent in symbols dictionary.
Using this test, I’ve suddenly found a bug here (despite simplicity of all these reduction rules). Well, no-one
protected from eye strain. Nevertheless, the test has a serious problem: some bugs can be revealed only if one of
arguments is 0, or 1, or −1. Maybe there are even more special cases exists.
Mentioned above aha! superoptimizer tries at least these values as arguments while testing: 1, 0, -1, 0x80000000,
0x7FFFFFFF, 0x80000001, 0x7FFFFFFE, 0x01234567, 0x89ABCDEF, -2, 2, -3, 3, -64, 64, -5, -31415.
Still, you cannot be sure.
Using toy decompiler, I’ve found that this piece is reduced to arg1 expression:
working out tests / t5_obf .s
going to reduce (((( - arg1) - 3) - (-arg1)) - ((- arg1) - 3))
reduction in reduce_SUB2 () ((- arg1) - 3) -> (-( arg1 + 3))
reduction in reduce_SUB5 () (( -( arg1 + 3)) - (-arg1)) -> (( -( arg1 + 3)) + arg1)
reduction in reduce_SUB2 () ((- arg1) - 3) -> (-( arg1 + 3))
reduction in reduce_ADD_SUB () ((( -( arg1 + 3)) + arg1) - (-( arg1 + 3))) -> arg1
going to reduce arg1
result =arg1
But is it correct? I’ve added a function which can output expression(s) to SMT-LIB-format, it’s as simple as a
function which converts expression to string.
And this is SMT-LIB-file for Z3:
( assert
( forall (( arg1 (_ BitVec 64)) (arg2 (_ BitVec 64)) (arg3 (_ BitVec 64)) (arg4 (_
BitVec 64)))
(=
( bvsub ( bvsub ( bvsub ( bvneg arg1) # x0000000000000003 ) ( bvneg arg1)) (
bvsub ( bvneg arg1) # x0000000000000003 ))
arg1
)
)
)
(check -sat)
In plain English terms, what we asking it to be sure, that forall four 64-bit arguments, two expressions are equivalent
(second is just arg1).
The syntax maybe hard to understand, but in fact, this is very close to LISP, and arithmetical operations are
named “bvsub”, “bvadd”, etc, because “bv” stands for bit vector.
While running, Z3 shows “sat”, meaning “satisfiable”. In other words, Z3 couldn’t find counterexample for this
expression.
In fact, I can rewrite this expression in the following form: expr1 != expr2, and we would ask Z3 to find at least
one set of input arguments, for which expressions are not equal to each other:
(declare -const arg1 (_ BitVec 64))
(declare -const arg2 (_ BitVec 64))
(declare -const arg3 (_ BitVec 64))
(declare -const arg4 (_ BitVec 64))
( assert
(not
(=
( bvsub ( bvsub ( bvsub ( bvneg arg1) # x0000000000000003 ) ( bvneg arg1)) (
bvsub ( bvneg arg1) # x0000000000000003 ))
arg1
)
)
494
)
(check -sat)
Z3 says “unsat”, meaning, it couldn’t find any such counterexample. In other words, for all possible input arguments,
results of these two expressions are always equal to each other.
Nevertheless, Z3 is not omnipotent. It fails to prove equivalence of the code which performs division by multipli-
cation. First of all, I extended it so both results will have size of 128 bit instead of 64:
(declare -const x (_ BitVec 64))
( assert
( forall ((x (_ BitVec 64)))
(=
((_ zero_extend 64) ( bvudiv x (_ bv17 64)))
( bvlshr ( bvmul ((_ zero_extend 64) x) #
x0000000000000000f0f0f0f0f0f0f0f1 ) (_ bv68 128))
)
)
)
(check -sat)
(get -model)
(bv17 is just 64-bit number 17, etc. “bv” stands for “bit vector”, as opposed to integer value.)
Z3 works too long without any answer, and I had to interrupt it.
As Z3 developers mentioned, such expressions are hard for Z3 so far: https://github.com/Z3Prover/z3/issues/
514.
Still, division by multiplication can be tested using previously described brute-force check.
( lib/Analysis/InstructionSimplify.cpp )
// (A | B) | C and A | (B | C) -> bswap if possible .
// (A >> B) | (C << D) and (A << B) | (B >> C) -> bswap if possible .
if (match(Op0 , m_Or( m_Value () , m_Value ())) ||
match (Op1 , m_Or( m_Value () , m_Value ())) ||
(match(Op0 , m_LogicalShift ( m_Value () , m_Value ())) &&
match(Op1 , m_LogicalShift ( m_Value () , m_Value ())))) {
if ( Instruction * BSwap = MatchBSwap (I))
return BSwap ;
( lib/Transforms/InstCombine/InstCombineAndOrXor.cpp )
As you can see, my matcher tries to mimic LLVM. What I call reduction is called folding in LLVM. Both terms
are popular.
I have also a blog post about LLVM obfuscator, in which LLVM matcher is mentioned: https://yurichev.com/
blog/llvm/.
Python version of toy decompiler uses strings in place where enumerate data type is used in C version (like
OP_AND, OP_MUL, etc) and symbols used in Racket version6 (like ’OP_DIV, etc). This may be seen as inefficient,
nevertheless, thanks to strings interning, only address of strings are compared in Python version, not strings as a
whole. So strings in Python can be seen as possible replacement for LISP symbols.
5 https://smt.st/current_tree/toy_decompiler/files/C
6 Racket is Scheme (which is, in turn, LISP dialect) dialect. https://smt.st/current_tree/toy_decompiler/files/Racket
495
16.7.1 Even simpler toy decompiler
Knowledge of LISP makes you understand all these things naturally, without significant effort. But when I had no
knowledge of it, but still tried to make a simple toy decompiler, I made it using usual text strings which hold expressions
for each registers (and even memory).
So when MOV instruction copies value from one register to another, we just copy string. When arithmetical
instruction occurred, we do string concatenation:
std :: string registers [ TOTAL ];
...
Now you’ll have long expressions for each register, represented as strings. For reducing them, you can use plain
simple regular expression matcher.
For example, for the rule (X*n)+(X*m) -> X*(n+m), you can match (sub)string using the following regular expres-
sion:
((.*)*(.*))+((.*)*(.*)) 7 . If the string is matched, you’re getting 4 groups (or substrings). You then just compare
1st and 3rd using string comparison function, then you check if the 2nd and 4th are numbers, you convert them to
numbers, sum them and you make new string, consisting of 1st group and sum, like this: (" + X + "*" + (int(n)
+ int(m)) + ").
It was naïve, clumsy, it was source of great embarrassment, but it worked correctly.
• C data types: arrays, structures, pointers, etc. This problem is virtually non-existent for JVM9 (Java, etc) and
.NET decompilers, because type information is present in binary files.
10
• Basic blocks, C/C++ statements. Mike Van Emmerik in his thesis shows how this can be tackled using SSA
forms (which are also used heavily in compilers).
• Memory support, including local stack. Keep in mind pointer aliasing problem. Again, decompilers of JVM and
.NET files are simpler here.
As I’ve mentioned, LISP knowledge can help to understand this all much easier. Here is well-known micro-
interpreter of LISP by Peter Norvig, also written in Python: https://web.archive.org/web/20161116133448/http:
//www.norvig.com/lispy.html, https://web.archive.org/web/20160305172301/http://norvig.com/lispy2.html.
The gradual rewriting I’ve shown here is also available in Mathematica (”Trace” command): https://reference.
wolfram.com/language/ref/Trace.html.
I also enjoyed reading “The Elements of Artificial Intelligence” book by Steve Tanimoto 14 , chapter 3: “Production
Systems and Pattern Matching”.
See also: Nuno Lopes – Verifying Optimizations using SMT Solvers 15 .
Symbolic execution
class Expr:
def __init__ (self ,s):
self.s=s
It works, because Python is dynamically typed PL, so the function doesn’t care what to operate on, numerical
values, or on objects of Expr() class.
Here is result:
new_X ((X^Y)^(Y^(X^Y)))
new_Y (Y^(X^Y))
You can remove double variables in your mind (since XORing by a value twice will result in nothing). At new_X
we can drop two X-es and two Y-es, and single Y will left. At new_Y we can drop two Y-es, and single X will left.
1 https://xn--mxacd.xn--qxam/math-notes.pdf
497
498
17.1.2 Change endianness
What does this code do?
mov eax , ecx
mov edx , ecx
shl edx , 16
and eax , 0000 ff00H
or eax , edx
mov edx , ecx
and edx , 00 ff0000H
shr ecx , 16
or edx , ecx
shl eax , 8
shr edx , 8
or eax , edx
In fact, many reverse engineers play shell game a lot, keeping track of what is stored where, at each point of time.
Again, we can build equivalent function which can take both numerical variables and Expr() objects. We also
extend Expr() class to support many arithmetical and boolean operations. Also, Expr() methods would take both
Expr() objects on input and integer values.
#!/ usr/bin/env python3
class Expr:
def __init__ (self ,s):
self.s=s
# change endianness
ecx=Expr(" initial_ECX ") # 1st argument
eax=ecx # mov eax , ecx
edx=ecx # mov edx , ecx
edx=edx <<16 # shl edx , 16
eax=eax &0 xff00 # and eax , 0000 ff00H
eax=eax|edx # or eax , edx
edx=ecx # mov edx , ecx
edx=edx &0 x00ff0000 # and edx , 00 ff0000H
ecx=ecx >>16 # shr ecx , 16
edx=edx|ecx # or edx , ecx
eax=eax <<8 # shl eax , 8
edx=edx >>8 # shr edx , 8
eax=eax|edx # or eax , edx
print (eax)
I run it:
(((( initial_ECX &65280) |( initial_ECX < <16)) <<8) |((( initial_ECX &16711680) |( initial_ECX
> >16)) >>8))
Now this is something more readable, however, a bit LISPy at first sight. In fact, this is a function which change
endianness in 32-bit word.
By the way, my Toy Decompiler can do this job as well, but operates on AST instead of plain strings: 16.
def FFT(X):
n = len(X)
w = exp (-2* pi *1j/n)
if n > 1:
X = FFT(X [::2]) + FFT(X [1::2])
for k in range (int(n/2)):
xk = X[k]
X[k] = xk + w**k*X[int(k+n/2)]
X[int(k+n/2)] = xk - w**k*X[int(k+n/2)]
return X
class Expr:
def __init__ (self ,s):
self.s=s
def FFT(X):
n = len(X)
# cast complex value to string , and then to Expr
w = Expr(str(exp (-2* pi *1j/n)))
if n > 1:
X = FFT(X [::2]) + FFT(X [1::2])
for k in range (int(n/2)):
xk = X[k]
X[k] = xk + w**k*X[int(k+n/2)]
X[int(k+n/2)] = xk - w**k*X[int(k+n/2)]
return X
FFT() function left almost intact, the only thing I added: complex value is converted into string and then Expr()
object is constructed.
0 : ((( input_0 +((( -1 -1.22464679915e -16j)**0)* input_4 )) +(((6.12323399574e -17 -1j)**0) *(
input_2 +((( -1 -1.22464679915e -16j)**0)* input_6 )))) +(((0.707106781187 -0.707106781187
j)**0) *(( input_1 +((( -1 -1.22464679915e -16j)**0)* input_5 )) +(((6.12323399574e -17 -1j)
**0) *( input_3 +((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
1 : ((( input_0 -((( -1 -1.22464679915e -16j)**0)* input_4 )) +(((6.12323399574e -17 -1j)**1) *(
input_2 -((( -1 -1.22464679915e -16j)**0)* input_6 )))) +(((0.707106781187 -0.707106781187
j)**1) *(( input_1 -((( -1 -1.22464679915e -16j)**0)* input_5 )) +(((6.12323399574e -17 -1j)
**1) *( input_3 -((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
2 : ((( input_0 +((( -1 -1.22464679915e -16j)**0)* input_4 )) -(((6.12323399574e -17 -1j)**0) *(
input_2 +((( -1 -1.22464679915e -16j)**0)* input_6 )))) +(((0.707106781187 -0.707106781187
j)**2) *(( input_1 +((( -1 -1.22464679915e -16j)**0)* input_5 )) -(((6.12323399574e -17 -1j)
**0) *( input_3 +((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
3 : ((( input_0 -((( -1 -1.22464679915e -16j)**0)* input_4 )) -(((6.12323399574e -17 -1j)**1) *(
input_2 -((( -1 -1.22464679915e -16j)**0)* input_6 )))) +(((0.707106781187 -0.707106781187
j)**3) *(( input_1 -((( -1 -1.22464679915e -16j)**0)* input_5 )) -(((6.12323399574e -17 -1j)
**1) *( input_3 -((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
4 : ((( input_0 +((( -1 -1.22464679915e -16j)**0)* input_4 )) +(((6.12323399574e -17 -1j)**0) *(
input_2 +((( -1 -1.22464679915e -16j)**0)* input_6 )))) -(((0.707106781187 -0.707106781187
j)**0) *(( input_1 +((( -1 -1.22464679915e -16j)**0)* input_5 )) +(((6.12323399574e -17 -1j)
**0) *( input_3 +((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
5 : ((( input_0 -((( -1 -1.22464679915e -16j)**0)* input_4 )) +(((6.12323399574e -17 -1j)**1) *(
input_2 -((( -1 -1.22464679915e -16j)**0)* input_6 )))) -(((0.707106781187 -0.707106781187
j)**1) *(( input_1 -((( -1 -1.22464679915e -16j)**0)* input_5 )) +(((6.12323399574e -17 -1j)
**1) *( input_3 -((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
6 : ((( input_0 +((( -1 -1.22464679915e -16j)**0)* input_4 )) -(((6.12323399574e -17 -1j)**0) *(
input_2 +((( -1 -1.22464679915e -16j)**0)* input_6 )))) -(((0.707106781187 -0.707106781187
j)**2) *(( input_1 +((( -1 -1.22464679915e -16j)**0)* input_5 )) -(((6.12323399574e -17 -1j)
**0) *( input_3 +((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
7 : ((( input_0 -((( -1 -1.22464679915e -16j)**0)* input_4 )) -(((6.12323399574e -17 -1j)**1) *(
input_2 -((( -1 -1.22464679915e -16j)**0)* input_6 )))) -(((0.707106781187 -0.707106781187
j)**3) *(( input_1 -((( -1 -1.22464679915e -16j)**0)* input_5 )) -(((6.12323399574e -17 -1j)
**1) *( input_3 -((( -1 -1.22464679915e -16j)**0)* input_7 ))))))
We can see subexpressions in form like x0 and x1 . We can eliminate them, since x0 = 1 and x1 = x. Also, we can
reduce subexpressions like x · 1 to just x.
def __mul__ (self , other ):
op1=self.s
op2=self. convert_to_Expr_if_int ( other ).s
if op1 =="1":
return Expr(op2)
if op2 =="1":
return Expr(op1)
0 : ((( input_0 + input_4 )+( input_2 + input_6 ))+(( input_1 + input_5 )+( input_3 + input_7 )))
502
1 : ((( input_0 - input_4 ) +((6.12323399574e -17 -1j)*( input_2 - input_6 )))
+((0.707106781187 -0.707106781187 j)*(( input_1 - input_5 ) +((6.12323399574e -17 -1j)*(
input_3 - input_7 )))))
2 : ((( input_0 + input_4 ) -( input_2 + input_6 )) +(((0.707106781187 -0.707106781187 j)**2) *((
input_1 + input_5 ) -( input_3 + input_7 ))))
3 : ((( input_0 - input_4 ) -((6.12323399574e -17 -1j)*( input_2 - input_6 )))
+(((0.707106781187 -0.707106781187 j)**3) *(( input_1 - input_5 ) -((6.12323399574e -17 -1j)
*( input_3 - input_7 )))))
4 : ((( input_0 + input_4 )+( input_2 + input_6 )) -(( input_1 + input_5 )+( input_3 + input_7 )))
5 : ((( input_0 - input_4 ) +((6.12323399574e -17 -1j)*( input_2 - input_6 )))
-((0.707106781187 -0.707106781187 j)*(( input_1 - input_5 ) +((6.12323399574e -17 -1j)*(
input_3 - input_7 )))))
6 : ((( input_0 + input_4 ) -( input_2 + input_6 )) -(((0.707106781187 -0.707106781187 j)**2) *((
input_1 + input_5 ) -( input_3 + input_7 ))))
7 : ((( input_0 - input_4 ) -((6.12323399574e -17 -1j)*( input_2 - input_6 )))
-(((0.707106781187 -0.707106781187 j)**3) *(( input_1 - input_5 ) -((6.12323399574e -17 -1j)
*( input_3 - input_7 )))))
import sys
class Expr:
def __init__ (self ,s):
self.s=s
BYTES =1
buf =[[ Expr("in_%d_%d" % (byte , bit)) for bit in range (8)] for byte in range (BYTES )]
crc32 (buf)
Here are expressions for each CRC32 bit for 1-byte buffer:
state 0=(1^( in_0_2 ^1))
state 1=((1^( in_0_0 ^1))^( in_0_3 ^1))
state 2=(((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_4 ^1))
state 3=(((1^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_5 ^1))
state 4=(((1^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))
state 5=(((1^( in_0_3 ^1))^( in_0_4 ^1))^( in_0_7 ^(1^( in_0_1 ^1))))
state 6=((1^( in_0_4 ^1))^( in_0_5 ^1))
state 7=((1^( in_0_5 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))
state 8=(((1^( in_0_0 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))^( in_0_7 ^(1^( in_0_1 ^1))))
state 9=((1^( in_0_1 ^1))^( in_0_7 ^(1^( in_0_1 ^1))))
state 10=(1^( in_0_2 ^1))
state 11=(1^( in_0_3 ^1))
state 12=((1^( in_0_0 ^1))^( in_0_4 ^1))
state 13=(((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_5 ^1))
state 14=((((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))
state 15=((((1^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_7 ^(1^( in_0_1 ^1))))
state 16=((((1^( in_0_0 ^1))^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_4 ^1))
state 17=(((((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_3 ^1))^( in_0_4 ^1))^( in_0_5 ^1))
state 18=(((((1^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_4 ^1))^( in_0_5 ^1))^( in_0_6 ^(1^( in_0_0 ^1)
)))
state 19=((((((1^( in_0_0 ^1))^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_5 ^1))^( in_0_6 ^(1^( in_0_0
^1))))^( in_0_7 ^(1^( in_0_1 ^1))))
state 20=((((((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_3 ^1))^( in_0_4 ^1))^( in_0_6 ^(1^( in_0_0
^1))))^( in_0_7 ^(1^( in_0_1 ^1))))
state 21=(((((1^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_4 ^1))^( in_0_5 ^1))^( in_0_7 ^(1^( in_0_1 ^1)
)))
state 22=(((((1^( in_0_0 ^1))^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_5 ^1))^( in_0_6 ^(1^( in_0_0 ^1)
504
)))
state 23=((((((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_3 ^1))^( in_0_4 ^1))^( in_0_6 ^(1^( in_0_0
^1))))^( in_0_7 ^(1^( in_0_1 ^1))))
state 24=(((((( in_0_0 ^1) ^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_4 ^1))^( in_0_5 ^1))^( in_0_7 ^(1^(
in_0_1 ^1))))
state 25=((((( in_0_1 ^1) ^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_5 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))
state 26=((((( in_0_2 ^1) ^( in_0_3 ^1))^( in_0_4 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))^( in_0_7 ^(1^(
in_0_1 ^1))))
state 27=(((( in_0_3 ^1) ^( in_0_4 ^1))^( in_0_5 ^1))^( in_0_7 ^(1^( in_0_1 ^1))))
state 28=((( in_0_4 ^1) ^( in_0_5 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))
state 29=((( in_0_5 ^1) ^( in_0_6 ^(1^( in_0_0 ^1))))^( in_0_7 ^(1^( in_0_1 ^1))))
state 30=(( in_0_6 ^(1^( in_0_0 ^1)))^( in_0_7 ^(1^( in_0_1 ^1))))
state 31=( in_0_7 ^(1^( in_0_1 ^1)))
For larger buffer, expressions gets increasing exponentially. This is 0th bit of the final state for 4-byte buffer:
state 0=(((((((((((((( in_0_0 ^1) ^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_4 ^1))^( in_0_5 ^1))^(
in_0_7 ^(1^( in_0_1 ^1))))^
( in_1_0 ^(1^( in_0_2 ^1))))^( in_1_2 ^(((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_4 ^1))))^( in_1_3
^(((1^( in_0_1 ^1))^
( in_0_2 ^1))^( in_0_5 ^1))))^( in_1_4 ^(((1^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_6 ^(1^( in_0_0 ^1))
))))^( in_2_0 ^((((1^
( in_0_0 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))^( in_0_7 ^(1^( in_0_1 ^1))))^( in_1_2 ^(((1^( in_0_0 ^1)
)^( in_0_1 ^1))^( in_0_4 ^
1))))))^( in_2_6 ^(((((((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))
^( in_1_4 ^(((1^( in_0_2 ^1))^
( in_0_3 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))))^( in_1_5 ^(((1^( in_0_3 ^1))^( in_0_4 ^1))^( in_0_7
^(1^( in_0_1 ^1))))))^
( in_2_0 ^((((1^( in_0_0 ^1))^( in_0_6 ^(1^( in_0_0 ^1))))^( in_0_7 ^(1^( in_0_1 ^1))))^( in_1_2
^(((1^( in_0_0 ^1))^( in_0_1 ^1))^
( in_0_4 ^1))))))))^( in_2_7 ^(((((((1^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_3 ^1))^( in_0_7 ^(1^(
in_0_1 ^1))))^( in_1_5 ^(((1^
( in_0_3 ^1))^( in_0_4 ^1))^( in_0_7 ^(1^( in_0_1 ^1))))))^( in_1_6 ^(((1^( in_0_4 ^1))^( in_0_5
^1))^( in_1_0 ^(1^( in_0_2 ^
1))))))^( in_2_1 ^((((1^( in_0_1 ^1))^( in_0_7 ^(1^( in_0_1 ^1))))^( in_1_0 ^(1^( in_0_2 ^1))))^(
in_1_3 ^(((1^( in_0_1 ^1))^
( in_0_2 ^1))^( in_0_5 ^1))))))))^( in_3_2 ^(((((((((1^( in_0_1 ^1))^( in_0_2 ^1))^( in_0_4 ^1))
^( in_0_5 ^1))^( in_0_6 ^(1^
( in_0_0 ^1))))^( in_1_2 ^(((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_4 ^1))))^( in_2_0 ^((((1^(
in_0_0 ^1))^( in_0_6 ^(1^( in_0_0 ^
1))))^( in_0_7 ^(1^( in_0_1 ^1))))^( in_1_2 ^(((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_4 ^1))))))^(
in_2_1 ^((((1^( in_0_1 ^1))^
( in_0_7 ^(1^( in_0_1 ^1))))^( in_1_0 ^(1^( in_0_2 ^1))))^( in_1_3 ^(((1^( in_0_1 ^1))^( in_0_2 ^1)
)^( in_0_5 ^1))))))^( in_2_4 ^
(((((1^( in_0_0 ^1))^( in_0_4 ^1))^( in_1_2 ^(((1^( in_0_0 ^1))^( in_0_1 ^1))^( in_0_4 ^1))))^(
in_1_3 ^(((1^( in_0_1 ^1))^
( in_0_2 ^1))^( in_0_5 ^1))))^( in_1_6 ^(((1^( in_0_4 ^1))^( in_0_5 ^1))^( in_1_0 ^(1^( in_0_2 ^1))
))))))))
Expression for the 0th bit of the final state for 8-byte buffer has length of ≈ 350KiB, which is, of course, can be
reduced significantly (because this expression is basically XOR tree), but you can feel the weight of it.
Now we can process this expressions somehow to get a smaller picture on what is affecting what. Let’s say, if we
can find “in_2_3” substring in expression, this means that 3rd bit of 2nd byte of input affects this expression. But
even more than that: since this is XOR tree (i.e., expression consisting only of XOR operations), if some input variable
is occurring twice, it’s annihilated, since x ⊕ x = 0. More than that: if a variable occurred even number of times (2,
4, 8, etc), it’s annihilated, but left if it’s occurred odd number of times (1, 3, 5, etc).
for i in range (32):
# print " state %d=%s" % (i, state [31 -i])
sys. stdout . write (" state %02d: " % i)
for byte in range ( BYTES ):
for bit in range (8):
s=" in_%d_%d" % (byte , bit)
505
if str( state [31 -i]). count (s) & 1:
sys. stdout . write ("*")
else:
sys. stdout . write (" ")
sys. stdout . write ("\n")
( https://smt.st/current_tree/symbolic/exec/4_CRC/2.py )
Now this how each bit of 1-byte input buffer affects each bit of the final CRC32 state:
state 00: *
state 01: * *
state 02: ** *
state 03: ** *
state 04: * ** *
state 05: * ** *
state 06: **
state 07: * **
state 08: * **
state 09: *
state 10: *
state 11: *
state 12: * *
state 13: ** *
state 14: ** *
state 15: ** *
state 16: * ***
state 17: ** ***
state 18: *** ***
state 19: *** ***
state 20: ** **
state 21: * ** *
state 22: ** **
state 23: ** **
state 24: * * ** *
state 25: **** **
state 26: ***** **
state 27: * *** *
state 28: * ***
state 29: ** ***
state 30: ** **
state 31: * *
class Expr:
def __init__ (self ,s):
self.s=s
Now if we once got several values from this PRNG, like 4583, 16304, 14440, 32315, 28670, 12568..., how would we
recover the initial seed? The problem in fact is solving a system of equations:
(((( initial_seed *1103515245) +12345) >>16) &32767) ==4583
(((((( initial_seed *1103515245) +12345) *1103515245) +12345) >>16) &32767) ==16304
(((((((( initial_seed *1103515245) +12345) *1103515245) +12345) *1103515245) +12345) >>16)
&32767) ==14440
(((((((((( initial_seed *1103515245) +12345) *1103515245) +12345) *1103515245) +12345)
*1103515245) +12345) >>16) &32767) ==32315
As it turns out, Z3 can solve this system correctly using only two equations:
#!/ usr/bin/env python3
from z3 import *
s= Solver ()
a =1103515245
c =12345
s.add ((((x*a)+c) >>16) &32767==4583)
s.add (((((( x*a)+c)*a)+c) >>16) &32767==16304)
#s.add (((((((( x*a)+c)*a)+c)*a)+c) >>16) &32767==14440)
#s.add (((((((((( x*a)+c)*a)+c)*a)+c)*a)+c) >>16) &32767==32315)
s. check ()
print (s. model ())
[x = 11223344]
input =...
SECS_DAY =24*60*60
dayno = input / SECS_DAY
wday = (dayno + 4) % 7
if wday ==5:
print " Thanks God , it 's Friday !"
Let’s say, we should find a way to run the block with print() call in it. What input value should be?
First, let’s build expression of wday variable:
#!/ usr/bin/env python3
class Expr:
def __init__ (self ,s):
self.s=s
from z3 import *
s= Solver ()
509
x=Int("x")
s.add(x >0)
s. check ()
print (s. model ())
[x = 86400 , dayno = 1]
Though the date back in year 1970, but it’s still correct!
This is also called “path constraint”, i.e., what constraint must be satisfied to execute specific block? Several tools
has “path” in their names, like “pathgrind”, Symbolic PathFinder, CodeSurfer Path Inspector, etc.
Like the shell game, this task is also often encounters in practice. You can see that something dangerous can be
executed inside some basic block and you’re trying to deduce, what input values can cause execution of it. It may be
buffer overflow, etc. Such input values are sometimes also called “inputs of death”.
Many crackmes are solved in this way, all you need is find a path into block which prints “key is correct” or
something like that.
We can extend this tiny example:
input =...
SECS_DAY =24*60*60
dayno = input / SECS_DAY
wday = (dayno + 4) % 7
print wday
if wday ==5:
print " Thanks God , it 's Friday !"
else:
print "Got to wait a little "
input
Now we have two blocks: for the first we should solve this equation: (( 86400 + 4) ≡ 5 mod 7. But for the second
input
we should solve inverted equation: (( 86400 + 4) ̸≡ 5 mod 7. By solving these equations, we will find two paths into
both blocks.
KLEE (or similar tool) tries to find path to each [basic] block and produces “ideal” unit test. Hence, KLEE can
find a path into the block which crashes everything, or reporting about correctness of the input key/license, etc.
Surprisingly, KLEE can find backdoors in the very same manner.
KLEE is also called “KLEE Symbolic Virtual Machine” – by that its creators mean that the KLEE is VM3 which
executes a code symbolically rather than numerically (like usual CPU).
How KLEE would handle this example?
# include <stdint .h>
# include <assert .h>
int main ()
{
uint32_t input ;
klee_make_symbolic (& input , sizeof input , " input ");
if ( is_it_friday ( input ))
klee_assert (0);
};
KLEE found two inputs, the one for ’return 1’ and the second for ’return 0’:
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ TGIF_KLEE1 .c
% time klee --libc= uclibc --optimize --use -query -log= solver :smt2 TGIF_KLEE1 .bc
...
KLEE: ERROR: TGIF_KLEE1 .c:20: failed external call: klee_assert
KLEE: NOTE: now ignoring this error at this location
% ls klee -last /*
klee -last/ assembly .ll klee -last/run. stats klee -last/ test000001 . ktest
klee -last/info klee -last/solver - queries .smt2 klee -last/ test000002 . ktest
klee -last/ messages .txt klee -last/ test000001 . external .err klee -last/ warnings .txt
klee -last/run. istats klee -last/ test000001 . kquery
So two inputs: 0 and 86400. One for not-Friday, another is for Friday:
% date -d @86400 -u "+%a %Y -%m -%d %H:%M:%S"
Fri 1970 -01 -02 00:00:00
KLEE can produce SMT-LIB file for SMT-solver, if the ’–use-query-log=solver:smt2’ option supplied: ’solver-
queries.smt2’. This is excerpt:
; Query 1 -- Type: InitialValues , Instructions : 11658
(set - option :produce - models true)
(set -logic QF_AUFBV )
(declare -fun input () ( Array (_ BitVec 32) (_ BitVec 8) ) )
( assert (= (_ bv5 32) ( bvurem ( bvadd (_ bv4 32) ( bvudiv ( concat ( select input (
_ bv3 32) ) ( concat ( select input (_ bv2 32) ) ( concat ( select input (_ bv1
32) ) ( select input (_ bv0 32) ) ) ) ) (_ bv86400 32) )
) (_ bv7 32) ) ) )
(check -sat)
(get -value ( ( select input (_ bv0 32) ) ) )
(get -value ( ( select input (_ bv1 32) ) ) )
(get -value ( ( select input (_ bv2 32) ) ) )
(get -value ( ( select input (_ bv3 32) ) ) )
0xc841c680=3359753856.
It’s still friday, but in future:
% date -d @3359753856 -u "+%a %Y -%m -%d %H:%M:%S"
Fri 2076 -06 -19 00:57:36
And CBMC?
# include <stdint .h>
# include <assert .h>
I’ll ask CBMC to find such an input so that assert() would trigger:
% cbmc --trace --function check TGIF_CBMC1 .c
...
...
Violated property :
file TGIF_CBMC1 .c function check line 17 thread 0
assertion is_it_friday ( input )==0
return_value_is_it_friday == 0
CBMC can also produce an SMT-LIB 2.x file for a SMT solver, with this problem converted to SMT-LIB 2.x:
% cbmc --smt2 --outfile TGIF_CBMC1_dest .smt --trace --function check TGIF_CBMC1 .c
It’s not very verbose, thanks to the tiny size of our problem:
( https://smt.st/current_tree/symbolic/exec/6_TGIF/TGIF_CBMC1_dest.smt.result )
This is the computed input value:
...
((| __CPROVER__start :: input !0 @1 #2| # x00000000b1796100 ))
...
0xb1796100=2977521920
Still, this is Friday, but the Year is 2064:
% date -d @2977521920 -u "+%a %Y -%m -%d %H:%M:%S"
Fri 2064 -05 -09 01:25:20
By the way, if the ’–z3’ option used with CBMC, it will spawn Z3 with that file.
Example 2
Let’s make our example more realistic. We can go to beer pub within some time range, say, between 18:00 and 00:00.
#!/ usr/bin/env python3
class Expr:
def __init__ (self ,s):
self.s=s
from z3 import *
s= Solver ()
x=Int("x")
s.add(x >0)
# 1st constraint :
dayno =Int("dayno ")
s.add(dayno ==x /86400)
s.add (( dayno +4) %7==5) # must be Friday
# 2nd constraint :
hour=Int("hour")
s.add(hour ==(x %86400) /3600) # get hour (UTC time)
s.add(hour >=18)
s.add(hour <=23)
s. check ()
print (s. model ())
int main ()
{
uint32_t input ;
klee_make_symbolic (& input , sizeof input , " input ");
if ( lets_party ( input ))
klee_assert (0);
};
...
KLEE: done: total instructions = 11820
KLEE: done: completed paths = 2
KLEE: done: partially completed paths = 1
KLEE: done: generated tests = 3
...
% ls klee -last /*
klee -last/ assembly .ll klee -last/run. stats klee -last/ test000001 . ktest
klee -last/info klee -last/solver - queries .smt2 klee -last/ test000002 . ktest
klee -last/ messages .txt klee -last/ test000001 . external .err klee -last/ test000003 . ktest
klee -last/run. istats klee -last/ test000001 . kquery klee -last/ warnings .txt
• 0 (not friday)
• 86400 (just friday)
• 159744 (friday plus hour in needed range)
515
What SMT file KLEE prepares? Now it consists of two SMT queries. First for first ’if’ statement, second is for
second:
; Query 1 -- Type: InitialValues , Instructions : 11656
(set - option :produce - models true)
(set -logic QF_AUFBV )
(declare -fun input () ( Array (_ BitVec 32) (_ BitVec 8) ) )
( assert (= (_ bv5 32) ( bvurem ( bvadd (_ bv4 32) ( bvudiv ( concat ( select input (
_ bv3 32) ) ( concat ( select input (_ bv2 32) ) ( concat ( select input (_ bv1
32) ) ( select input (_ bv0 32) ) ) ) ) (_ bv86400 32) ) ) (_ bv7 32) ) ) )
(check -sat)
(get -value ( ( select input (_ bv0 32) ) ) )
(get -value ( ( select input (_ bv1 32) ) ) )
(get -value ( ( select input (_ bv2 32) ) ) )
(get -value ( ( select input (_ bv3 32) ) ) )
(exit)
; OK -- Elapsed : 2.303087e -02s
; Solvable : true
; input = [128 ,81 ,1 ,0]
...
** Results :
TGIF_CBMC2 .c function check
[check. assertion .1] line 27 assertion lets_party ( input )==0: FAILURE
Summary
In short, this is how KLEE and CBMC work. They construct a system of equations for each basic block and then ask
SMT solver to find correct inputs.
class Expr:
def __init__ (self ,s):
self.s=s
"""
x
------------
2y + 4z - 12
"""
This equation is easy to solve, let’s try Wolfram Mathematica this time:
In []:= FindInstance [{(y*2 + z*4) - 12 == 0}, {y, z}, Integers ]
Out []= {{y -> 0, z -> 3}}
class Expr:
def __init__ (self ,s,i):
self.s=s
self.i=i
if len(m) <= 1:
print (tabs( indent )+" merge_sort () end. returning single element ")
return m
middle = len(m) // 2
left = m[: middle ]
right = m[ middle :]
But here is a function which compares elements. Obviously, it wouldn’t work correctly without it.
So we can track both expression for each element and numerical value. Both will be printed finally. But whenever
values are to be compared, only numerical parts will be used.
Result:
merge_sort () begin . input :
input1 (22)
input2 (7)
input3 (2)
input4 (1)
input5 (8)
input6 (4)
merge_sort () begin . input :
input1 (22)
519
input2 (7)
input3 (2)
merge_sort () begin . input :
input1 (22)
merge_sort () end. returning single element
merge_sort () begin . input :
input2 (7)
input3 (2)
merge_sort () begin . input :
input2 (7)
merge_sort () end. returning single element
merge_sort () begin . input :
input3 (2)
merge_sort () end. returning single element
merge_sort () end. returning :
input3 (2)
input2 (7)
merge_sort () end. returning :
input3 (2)
input2 (7)
input1 (22)
merge_sort () begin . input :
input4 (1)
input5 (8)
input6 (4)
merge_sort () begin . input :
input4 (1)
merge_sort () end. returning single element
merge_sort () begin . input :
input5 (8)
input6 (4)
merge_sort () begin . input :
input5 (8)
merge_sort () end. returning single element
merge_sort () begin . input :
input6 (4)
merge_sort () end. returning single element
merge_sort () end. returning :
input6 (4)
input5 (8)
merge_sort () end. returning :
input4 (1)
input6 (4)
input5 (8)
merge_sort () end. returning :
input4 (1)
input3 (2)
input6 (4)
input2 (7)
input5 (8)
input1 (22)
17.3 Tools
• http://angr.io - static and dynamic symbolic (”concolic”) analysis.
17.4 Examples
• https://fevral.github.io/2017/08/13/flareon2015-2.html – using angr.io.
• Breaking Kryptonite’s obfuscation: a static analysis approach relying on symbolic execution 7 .
4 https://classes.soe.ucsc.edu/cmps290g/Fall09/Papers/AssigningMeanings1967.pdf
5 https://yurichev.com/mirrors/king76symbolicexecution.pdf
6 https://github.com/enzet/symbolic-execution
7 https://doar-e.github.io/blog/2013/09/16/breaking-kryptonites-obfuscation-with-symbolic-execution/
8 https://sean.heelan.io/2012/03/23/anatomy-of-a-symbolic-emulator-part-1-trace-generation/, https://sean.heelan.
io/2012/03/23/anatomy-of-a-symbolic-emulator-part-2-introducing-symbolic-data/, https://sean.heelan.io/2012/03/23/
anatomy-of-a-symbolic-emulator-part-3-processing-symbolic-data-generating-new-inputs/
Chapter 18
KLEE
18.1 Installation
KLEE building from source is tricky. Easiest way to use KLEE is to install docker and then to run KLEE docker
image1 . To copy files to/from docker, use the “docker cp” command.
# include "klee.h"
// abbreviated hexadecimal
// like "#123"
if ((R> >4) ==(R&0 xF) && (G > >4) ==(G&0 xF) && (B > >4) ==(B&0 xF))
return 5;
// last resort
// like "#123456"
return 6;
};
1 http://klee.github.io/docker/
521
522
int main ()
{
uint8_t R, G, B;
klee_make_symbolic (&R, sizeof R, "R");
klee_make_symbolic (&G, sizeof R, "G");
klee_make_symbolic (&B, sizeof R, "B");
// without passing return value out of main () , KLEE wouldn 't work correctly
// perhaps , this is how we show that the return value is used somehow
// instead of dangling as void
return HTML_color (R, G, B);
};
There are 6 possible paths in function, and let’s see, if KLEE could find them all? It’s indeed so:
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ color.c
while (1)
{
ret = *( unsigned char *) s1 - *( unsigned char *) s2;
if (ret !=0)
break ;
if ((* s1 ==0) || (* s2)==0)
break ;
s1 ++;
s2 ++;
};
if (ret < 0)
{
return -1;
} else if (ret > 0)
{
return 1;
}
return 0;
}
int main ()
{
char input1 [2];
char input2 [2];
klee_assume (( input1 [0] >= 'a ') && ( input1 [0] <= 'z '));
klee_assume (( input2 [0] >= 'a ') && ( input2 [0] <= 'z '));
// without passing return value out of main () , KLEE wouldn 't work correctly
524
// perhaps , this is how we show that the return value is used somehow
// instead of dangling as void
return my_strcmp (input1 , input2 );
};
Let’s find out, if KLEE is capable of finding all three paths? I intentionally made things simpler for KLEE by
limiting input arrays to two 2 bytes or to 1 character + terminal zero byte.
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ strcmp .c
% ls klee -last
assembly .ll messages .txt run. stats test000002 . ktest warnings .txt
info run. istats test000001 . ktest test000003 . ktest
These are the input values for each path inside of my implementation of strcmp():
% ktest -tool klee -last/ test000001 . ktest | grep data
object 0: data: b'a\x00 '
object 1: data: b'c\x00 '
1st is if first argument (“a”) is lesser than the second (“c”), 2nd is if they are equal (“a” and “a”), 3rd is fi first
argument (“c”) is greater than the second (“a”).
Using these 3 test cases, we’ve got full coverage of our (toy) implementation of strcmp().
gmtime.c
525
14
15 # define YEAR0 1900
16 # define EPOCH_YR 1970
17 # define SECS_DAY (24L * 60L * 60L)
18 # define YEARSIZE (year) ( LEAPYEAR (year) ? 366 : 365)
19
20 const int _ytab [2][12] =
21 {
22 { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
23 { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
24 };
25
26 const char *_days [] =
27 {
28 " Sunday ", " Monday ", " Tuesday ", " Wednesday ",
29 " Thursday ", " Friday ", " Saturday "
30 };
31
32 const char * _months [] =
33 {
34 " January ", " February ", " March ",
35 "April", "May", "June",
36 "July", " August ", " September ",
37 " October ", " November ", " December "
38 };
39
40 # define LEAPYEAR (year) (!(( year) % 4) && ((( year) % 100) || !(( year) % 400)))
41
42 void decode_UNIX_time ( const time_t time)
43 {
44 unsigned int dayclock , dayno ;
45 int year = EPOCH_YR ;
46
47 dayclock = ( unsigned long)time % SECS_DAY ;
48 dayno = ( unsigned long)time / SECS_DAY ;
49
50 int seconds = dayclock % 60;
51 int minutes = ( dayclock % 3600) / 60;
52 int hour = dayclock / 3600;
53 int wday = ( dayno + 4) % 7;
54 while ( dayno >= YEARSIZE (year))
55 {
56 dayno -= YEARSIZE (year);
57 year ++;
58 }
59
60 year = year - YEAR0 ;
61
62 int month = 0;
63
64 while ( dayno >= _ytab [ LEAPYEAR (year)][ month ])
65 {
66 dayno -= _ytab [ LEAPYEAR (year)][ month ];
67 month ++;
68 }
69
70 char *s;
71 switch ( month )
72 {
73 case 0: s=" January "; break ;
74 case 1: s=" February "; break ;
526
75 case 2: s=" March "; break ;
76 case 3: s=" April "; break ;
77 case 4: s=" May "; break ;
78 case 5: s=" June "; break ;
79 case 6: s=" July "; break ;
80 case 7: s=" August "; break ;
81 case 8: s=" September "; break ;
82 case 9: s=" October "; break ;
83 case 10: s=" November "; break ;
84 case 11: s=" December "; break ;
85 default :
86 assert (0);
87 };
88
89 printf ("%04d -%s -%02d %02d:%02d:%02d\n", YEAR0 +year , s, dayno +1, hour ,
minutes , seconds );
90 printf (" week day: %s\n", _days [wday ]);
91 }
92
93 int main ()
94 {
95 uint32_t time;
96
97 klee_make_symbolic (& time , sizeof time , "time ");
98
99 decode_UNIX_time (time);
100
101 return 0;
102 }
...
Wow, assert() at line 86 has been triggered, why? Let’s see a value of UNIX time which triggers it:
% ls klee -last
assembly .ll run. istats test000001 .exec.err test000002 . assert .err
warnings .txt
info run. stats test000001 . kquery test000002 . kquery
messages .txt solver - queries .smt2 test000001 . ktest test000002 . ktest
After my investigation, I’ve found that month variable can hold incorrect value of 12 (while 11 is maximal, for
December), because LEAPYEAR() macro must receive year number as 2000, not as 100.
Correct fix would be moving year substraction expression down:
% diff -u klee_time1 .c klee_time1_fixed .c
--- klee_time1 .c 2023 -12 -04 18:25:34.077200826 +0200
+++ klee_time1_fixed .c 2023 -12 -04 18:48:28.352408590 +0200
@@ -57,8 +57 ,6 @@
year ++;
}
So I’ve introduced a bug during rewriting this function, and KLEE found it! Or was it bug in the original code?
Just interesting, what would be if I’ll replace switch() to array of strings, like it usually happens in concise C/C++
code?
% diff -u klee_time1 .c klee_time2 .c
--- klee_time1 .c 2023 -12 -04 18:25:34.077200826 +0200
+++ klee_time2 .c 2023 -12 -04 18:34:34.388646376 +0200
@@ -67,24 +67 ,7 @@
month ++;
}
- char *s;
- switch ( month )
- {
- case 0: s=" January "; break ;
- case 1: s=" February "; break ;
- case 2: s=" March "; break ;
- case 3: s=" April "; break ;
- case 4: s=" May "; break ;
- case 5: s=" June "; break ;
- case 6: s=" July "; break ;
- case 7: s=" August "; break ;
- case 8: s=" September "; break ;
- case 9: s=" October "; break ;
- case 10: s=" November "; break ;
- case 11: s=" December "; break ;
- default :
- assert (0);
- };
+ const char *s= _months [ month ];
So, if this piece of code can be triggered on remote computer, with this input value (input of death), it’s possible
to crash the process (with some luck, though).
OK, now I’m fixing a bug by moving year subtracting expression down, and let’s find, what UNIX time value
corresponds to some fancy date like 2022-February-2?
1 ...
2
3 while ( dayno >= _ytab [ LEAPYEAR (year)][ month ])
4 {
5 dayno -= _ytab [ LEAPYEAR (year)][ month ];
6 month ++;
7 }
8 year = year - YEAR0 ;
9
10 if (YEAR0 +year ==2022 && month ==1 && dayno +1==22)
11 assert (0);
12 }
Success, but hours/minutes/seconds are seems random—they are random indeed, because KLEE satisfied all
constraints we’ve put, nothing else.
We didn’t ask it to set hours/minutes/seconds to zeroes.
Let’s add constraints to hours/minutes/seconds as well:
1 if (YEAR0 +year ==2022 && month ==1 && dayno +1==22 && hour ==22 && minutes ==22 &&
seconds ==22)
2 klee_assert (0);
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ klee_base64 .c
...
% klee --libc= uclibc --optimize --use -query -log= solver :smt2 klee_base64 .bc
KLEE: ERROR: klee_base64 .c:84: invalid klee_assume call ( provably false )
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_base64 .c:89: ASSERTION FAIL: 0
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_base64 .c:70: memory error : out of bound pointer
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_base64 .c:66: memory error : out of bound pointer
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: klee_base64 .c:50: memory error : out of bound pointer
KLEE: NOTE: now ignoring this error at this location
We’re interesting in the second error, where klee_assert() has been triggered:
% ls klee -last | grep err
test000001 .user.err
test000002 . assert .err
test000003 .ptr.err
test000004 .ptr.err
test000005 .ptr.err
This is indeed a real base64 string, terminated with the zero byte, just as it’s requested by C/C++ standards. The
final zero byte at 31th byte (starting at zeroth byte) is our deed: so that KLEE would report lesser number of errors.
The base64 string is indeed correct:
% echo AAECAwQFBgcICQoLDA0OD + | base64 -d | xxd -g 1
base64 : invalid input
00000000: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................
base64 decoder Linux utility I’ve just run blaming for “invalid input”—it means the input string is not properly
padded. Now let’s pad it manually, and decoder utility will no complain anymore:
532
The reason our generated base64 string is not padded is because base64 decoders are usually discards padding
symbols (“=”) at the end. In other words, they are not require them, so is the case of our decoder. Hence, padding
symbols are left unnoticed to KLEE.
So we again made antipode or inverse function of base64 decoder.
int
decompress_lzss ( uint8_t *dst , uint8_t *src , uint32_t srclen )
{
/* ring buffer of size N, with extra F -1 bytes to aid string comparison */
uint8_t * dststart = dst;
uint8_t * srcend = src + srclen ;
int i, j, k, r, c;
unsigned int flags ;
uint8_t text_buf [N + F - 1];
dst = dststart ;
srcend = src + srclen ;
for (i = 0; i < N - F; i++)
text_buf [i] = ' ';
r = N - F;
flags = 0;
for ( ; ; ) {
if ((( flags >>= 1) & 0x100) == 0) {
if (src < srcend ) c = *src ++; else break ;
flags = c | 0 xFF00 ; /* uses higher byte cleverly */
} /* to count eight */
if (flags & 1) {
if (src < srcend ) c = *src ++; else break ;
*dst ++ = c;
text_buf [r++] = c;
r &= (N - 1);
} else {
if (src < srcend ) i = *src ++; else break ;
if (src < srcend ) j = *src ++; else break ;
i |= ((j & 0xF0) << 4);
533
j = (j & 0x0F) + THRESHOLD ;
for (k = 0; k <= j; k++) {
c = text_buf [(i + k) & (N - 1) ];
*dst ++ = c;
text_buf [r++] = c;
r &= (N - 1);
}
}
}
int main ()
{
# define COMPRESSED_LEN 15
//# define COMPRESSED_LEN 14 -- fail
uint8_t input [ COMPRESSED_LEN ];
uint8_t plain [ PLAIN_TEXT_LEN ];
uint32_t size= COMPRESSED_LEN ;
klee_assert (0);
return 0;
}
I decreased COMPRESSED_LEN gradually to check, whether KLEE would find compressed piece of data, and it did:
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ klee_lzss1 .c
...
KLEE consumed ≈ 2GB of RAM and worked for ≈ 6 minutes (on AMD Ryzen 5 3600).
And here is it, a 15 bytes which, if decompressed by our copypasted algorithm, will result in desired plain text!
So how LZSS works? Without peeking into Wikipedia, we can say that: if LZSS compressor observes some data
it already had, it replaces the data with a link to some place in past with size. If it observes something yet unseen, it
puts data as is. This is theory. This is indeed what we’ve got. Desired text is three “Buffalo” words, the first and the
last are equivalent, but the second is almost equivalent, differing with first by one character.
That’s what we see (better formatted C-string):
"\ xff" " Buffalo " "\ x09" "b" "\ xef\xf4\xee\xf5"
Here is some control byte (0xff), “Buffalo” word is placed as is, then another control byte (0x09), then we see
beginning of the second word (“b”) and more control bytes, perhaps, links to the beginning of the buffer. These are
command to decompressor, like, in plain English, “copy data from the buffer we’ve already done, from that place to
that place”, etc.
What we’ve got here is KLEE acting like a LZSS compressor – very slow, complex and expensive, but compressor.
Interesting, is it possible to meddle into this piece of compressed data? Out of whim, can we force KLEE to find
a compressed data, where not just “b” character has been placed as is, but also the second character of the word, i.e.,
“bu”?
I’ve modified main() function by adding klee_assume(): now the 11th byte of input (compressed) data (right
after “b” byte) must have “u”. I has no luck with 15 byte of compressed data, so I increased it to 16 bytes:
int main ()
{
# define COMPRESSED_LEN 16
uint8_t input [ COMPRESSED_LEN ];
uint8_t plain [ PLAIN_TEXT_LEN ];
uint32_t size= COMPRESSED_LEN ;
klee_assert (0);
return 0;
}
…et voilà: KLEE found a compressed piece of data which satisfied our whimsical constraint:
% time klee --libc= uclibc --optimize klee_lzss2 .bc
535
...
So now we found a piece of compressed data where two strings are placed as is: “Buffalo” and “bu”.
Both pieces of compressed data, if feeded into our copypasted function, produce “Buffalo buffalo Buffalo” text
string.
Please note, I still have no access to LZSS compressor code, and I didn’t get into LZSS decompressor details yet.
(However, yes, it must be very simple.)
Unfortunately, things are not that cool: larger texts are out of reach for KLEE.
Nevertheless, it’s very impressive, taking into account the fact that we’re not getting into internals of this specific
LZSS decompressor. Once more time, we’ve created antipode of decompressor, or inverse function.
Also, as it seems, KLEE isn’t very good so far with decompression algorithms (but who’s good then?).
( https://smt.st/current_tree/KLEE/strtodx.c )
Interestingly, KLEE cannot handle floating-point arithmetic, but nevertheless, found something 4 :
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ strtodx .c
...
...
As it seems, the “.0f-79” string does out of bounds array access (read) at the line 204. During further investigation,
I’ve found that powersOf10[] array is too short: 6th element (started at 0th) has been accessed. And here we see a
part of array commented (line 81)! Probably someone’s mistake? Unfixed bug?
( https://smt.st/current_tree/KLEE/calc.c )
I reworked it by commenting (f)printf() calls. Not needed anyway.
KLEE found two expression strings which leads to division error:
% clang -emit -llvm -c -g -O0 -Xclang -disable -O0 - optnone -I /tmp/ klee_src / include /
klee/ calc.c
...
Maybe this is not an impressive result, nevertheless, it’s yet another reminder that division and remainder operations
must be wrapped somehow in production code to avoid possible crash.
18.10 Exercise
Here is my crackme/keygenme, which may be tricky, but easy to solve using KLEE: http://challenges.re/74/.
Chapter 19
(Amateur) cryptography
class Expr:
def __init__ (self ,s):
self.s=s
# reworked from:
546
547
# Released under the BSD License .
def MX():
return ((z> >5) ^(y < <2)) + ((y > >3) ^(z <<4))^( sum^y) + (k[( Expr(str(p)) & 3)^e]^z
)
y = v[0]
sum = Expr("0")
DELTA = 0 x9e3779b9
# Encoding only
z = v[n -1]
# number of rounds :
#q = 6 + 52 / n
q=1
while q > 0:
q -= 1
sum = sum + DELTA
e = (sum >> 2) & 3
p = 0
while p < n - 1:
y = v[p+1]
z = v[p] = v[p] + MX()
p += 1
y = v[0]
z = v[n -1] = v[n -1] + MX()
return 0
v=[ Expr(" input1 "), Expr(" input2 "), Expr(" input3 "), Expr(" input4 ")]
k=Expr("key")
raw_xxtea (v, 4, k)
A key is chosen according to input data, and, obviously, we can’t know it during symbolic execution, so we leave
expression like k[...].
Now results for just one round, for each of 4 outputs:
0 : ( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^
input2 )+
(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 ))))
1 : ( input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^
input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) >>5)^( input3 < <2))+(( input3
>>3) ^(( input1 +
(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key
[((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+(( key [((1&3)
^(((0+2654435769) >>2)&
3))])^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769)
^ input2 )+
(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 ))))))))
2 : ( input3 +((((( input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4
<<4)))^
548
(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2) &3))])^ input4 )))) >>5)^(
input3 <<2))+
(( input3 > >3) ^(( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^
input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^
input3 )+
(( key [((1&3) ^(((0+2654435769) >>2)&3))]) ^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2
>>3)^( input4 < <4)))
^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))))))) >>5)
^( input4 <<2))+
(( input4 > >3) ^(( input2 +((((( input1 +(((( input4 > >5)^( input2 < <2))+(( input2 > >3) ^( input4
<<4))) ^(((0+2654435769) ^
input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) >>5)^( input3 < <2))+(( input3
>>3) ^(( input1 +
(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key
[((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+(( key [((1&3)
^(((0+2654435769) >>2)&3))])^
( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^
input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))))))) <<4))) ^(((0+2654435769) ^ input4 )+(( key [((2&3)
^(((0+2654435769) >>2)&
3))])^( input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^
input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) >>5)^( input3 < <2))+(( input3
>>3) ^(( input1 +
(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key
[((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+(( key [((1&3)
^(((0+2654435769) >>2)&
3))])^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769)
^ input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 ))))))))))))
3 : ( input4 +((((( input3 +((((( input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2
>>3)^( input4 < <4)))^
(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2) &3))])^ input4 )))) >>5)^(
input3 <<2))+(( input3 > >3)^
(( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^
input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+(( key [((1&3)
^(((0+2654435769) >>2)&3))])^
( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^
input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))))))) >>5)^( input4 < <2))+(( input4 > >3) ^(( input2
+((((( input1 +(((( input4 > >5)^
(input2 <<2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3)
^(((0+2654435769) >>2)&3))])^
input4 )))) >>5)^( input3 < <2))+(( input3 > >3) ^(( input1 +(((( input4 > >5) ^( input2 < <2))+((
input2 >>3)^( input4 < <4)))^
(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2) &3))])^ input4 )))) <<4)))
^(((0+2654435769) ^ input3 )+
(( key [((1&3) ^(((0+2654435769) >>2)&3))]) ^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2
>>3)^( input4 < <4)))^
(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2) &3))])^ input4 )))))))) <<4)))
^(((0+2654435769) ^
input4 )+(( key [((2&3) ^(((0+2654435769) >>2)&3))]) ^( input2 +((((( input1 +(((( input4 > >5) ^(
input2 <<2))+(( input2 > >3)^
(input4 <<4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4
)))) >>5)^( input3 < <2))+
(( input3 > >3) ^(( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
549
^(((0+2654435769) ^ input2 )+
(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+((
key [((1&3) ^(((0+2654435769) >>
2) &3))])^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))))))))))) >>5)^(( input1 +(((( input4 > >5) ^( input2 < <2)
)+(( input2 >>3) ^( input4 <<
4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) <<2))
+((( input1 +(((( input4 > >5)^
(input2 <<2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3)
^(((0+2654435769) >>2)&3))])^
input4 )))) >>3) ^(( input3 +((((( input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2
>>3)^( input4 < <4))) ^(((0+
2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) >>5)^( input3
<<2))+(( input3 > >3) ^(( input1 +
(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key
[((0&3) ^(((0+2654435769) >>
2) &3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+(( key [((1&3) ^(((0+2654435769) >>2)
&3))])^( input1 +(((( input4 >>
5)^( input2 <<2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3)
^(((0+2654435769) >>2)&3))])^
input4 )))))))) >>5)^( input4 < <2))+(( input4 > >3) ^(( input2 +((((( input1 +(((( input4 > >5) ^(
input2 <<2))+(( input2 > >3)^
(input4 <<4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4
)))) >>5)^( input3 < <2))+
(( input3 > >3) ^(( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+
(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^ input3 )+((
key [((1&3) ^(((0+2654435769) >>
2) &3))])^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))))))) <<4))) ^(((0+2654435769) ^ input4 )+(( key [((2&3)
^(((0+2654435769) >>2)&3))])^
( input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))) >>5)^( input3 < <2))+(( input3 > >3) ^(( input1 +((((
input4 >>5)^( input2 < <2))+
(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)
&3))])^ input4 )))) <<4)))^(((
0+2654435769) ^ input3 )+(( key [((1&3) ^(((0+2654435769) >>2)&3))]) ^( input1 +(((( input4 > >5)
^( input2 <<2))+(( input2 > >3) ^(
input4 <<4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )
))))))))))) <<4))) ^(((0+
2654435769) ^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))))+(( key [((3&3) ^(((0+2654435769) >>2)&3))]) ^(
input3 +((((( input2 +((((( input1 +
(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key
[((0&3) ^(((0+2654435769) >>
2) &3))])^ input4 )))) >>5)^( input3 < <2))+(( input3 > >3) ^(( input1 +(((( input4 > >5) ^( input2 < <2)
)+(( input2 >>3) ^( input4 <<
4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) <<4))
) ^(((0+2654435769) ^ input3 )+
(( key [((1&3) ^(((0+2654435769) >>2)&3))]) ^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2
>>3)^( input4 < <4))) ^(((0+
2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))))))) >>5)^(
input4 <<2))+(( input4 > >3) ^((
input2 +((((( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+(( key [((0&3) ^
(((0+2654435769) >>2)&3))])^ input4 )))) >>5)^( input3 < <2))+(( input3 > >3) ^(( input1 +((((
input4 >>5)^( input2 < <2))+((
550
input2 >>3)^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)
&3))])^ input4 )))) <<4))) ^(((0+
2654435769) ^ input3 )+(( key [((1&3) ^(((0+2654435769) >>2)&3))]) ^( input1 +(((( input4 > >5) ^(
input2 <<2))+(( input2 > >3)^
(input4 <<4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4
)))))))) <<4))) ^(((0+
2654435769) ^ input4 )+(( key [((2&3) ^(((0+2654435769) >>2)&3))]) ^( input2 +((((( input1 +((((
input4 >>5)^( input2 < <2))+
(( input2 > >3) ^( input4 < <4))) ^(((0+2654435769) ^ input2 )+(( key [((0&3) ^(((0+2654435769) >>2)
&3))])^ input4 )))) >>5)^
(input3 <<2))+(( input3 > >3) ^(( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4
<<4))) ^(((0+2654435769) ^
input2 )+(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 )))) <<4))) ^(((0+2654435769) ^
input3 )+(( key [((1&3) ^(((0+
2654435769) >>2)&3))]) ^( input1 +(((( input4 > >5) ^( input2 < <2))+(( input2 > >3) ^( input4 < <4)))
^(((0+2654435769) ^ input2 )+
(( key [((0&3) ^(((0+2654435769) >>2)&3))])^ input4 ))))))))))))))))
Somehow, the size of the expression for each subsequent output is bigger. I hope I haven’t been mistaken? And
this is just for 1 round. For 2 rounds, size of all 4 expression is ≈ 970KB. For 3 rounds, this is ≈ 115M B. For 4
rounds, I have not enough RAM on my computer. Expressions exploding exponentially. And there are 19 rounds. You
can weigh it.
Perhaps, you can simplify these expressions: there are a lot of excessive parenthesis, but I’m highly pessimistic,
cryptoalgorithms constructed in such a way to not have any spare operations.
In order to crack it, you can use these expressions as system of equation and try to solve it using SMT-solver. This
is called “algebraic attack”.
In other words, theoretically, you can build a system of equation like this: M D5(x) = 12341234..., but expressions
are so huge so it’s impossible to solve them. Yes, cryptographers are fully aware of this and one of the goals of the
successful cipher is to make expressions as big as possible, using reasonable time and size of algorithm.
Nevertheless, you can find numerous papers about breaking these cryptosystems with reduced number of rounds:
when expression isn’t exploded yet, sometimes it’s possible. This cannot be applied in practice, but such an experience
has some interesting theoretical uses.
• Algebraic Attacks on the Crypto-1 Stream Cipher in MiFare Classic and Oyster Cards
• Attacking Bivium Using SAT Solvers
• Extending SAT Solvers to Cryptographic Problems
• Applications of SAT Solvers to Cryptanalysis of Hash Functions
int main ()
{
uint32_t buf [4];
klee_make_symbolic (buf , sizeof buf);
megahash (buf);
if (buf [0]==0 x18f71ce6 // or whatever
&& buf [1]==0 xf37c2fc9
&& buf [2]==0 x1cfe96fe
&& buf [3]==0 x8c02c75e )
klee_assert (0);
};
KLEE can break it with little effort. Functions of such complexity is common in shareware, which checks license
keys, etc.
552
Here is how we can make its work harder by making rotations dependent of inputs, and this makes number of
possible inputs much, much bigger:
void megahash ( uint32_t buf [4])
{
for (int i=0; i <16; i++)
{
uint32_t t0=buf [0]^0 x12345678 ^buf [1];
uint32_t t1=buf [1]^0 xabcdef01 ^buf [2];
uint32_t t2=buf [2]^0 x23456789 ^buf [3];
uint32_t t3=buf [3]^0 x0abcdef0 ^buf [0];
Addition (or modular addition2 , as cryptographers say) can make things even harder:
void megahash ( uint32_t buf [4])
{
for (int i=0; i <4; i++)
{
uint32_t t0=buf [0]^0 x12345678 ^buf [1];
uint32_t t1=buf [1]^0 xabcdef01 ^buf [2];
uint32_t t2=buf [2]^0 x23456789 ^buf [3];
uint32_t t3=buf [3]^0 x0abcdef0 ^buf [0];
Heavy operations for SAT/SMT are shifts/rotates by a variable, division, remainder. Easy operations: shifts/rotates
by constant, bit twiddling.
As en exercise, you can try to make a block cipher which KLEE wouldn’t break. This is quite sobering experience.
Another exercise is to make an algorithm that converts a plain text in English or your native language to the block
of the same length, but with highest possible entropy. It’s not an easy task. (Data compression is cheating.)
An easy way to test your algorithm: encrypt numbers starting at 0 and feed the resulting ciphertext to diehard
test 4 (like in Counter/CTR encryption mode). These tests are designed to check PRNGs. In other words, these tests
shouldn’t find any regularities in a list of random numbers, as well as in a ciphertext.
Summary: if you deal with amateur cryptography, you can always give KLEE and SMT solver a try. Even more:
sometimes you have only decryption function, and if algorithm is simple enough, KLEE or SMT solver can reverse
things back.
If a SAT/SMT solver can find a key faster than bruteforce, this is usually a very bad symptom for the algorithm.
One amusing thing to mention: if you try to implement amateur cryptoalgorithm in Verilog/VHDL language to
run it on FPGA, maybe in brute-force way, you can find that EDA5 tools can optimize many things during synthesis
(this is the word they use for “compilation”) and can leave cryptoalgorithm much smaller/simpler than it was. Even if
you try to implement DES6 algorithm in bare metal with a fixed key, Altera Quartus can optimize first round of DES
and make it slightly smaller/faster than others.
In []:= 256/130// N
Out []= 1.96923
19.2.2 Bugs
Another prominent feature of amateur cryptography is bugs. Bugs here often left uncaught because output of en-
crypting function visually looked “good enough” or “obfuscated enough”, so a developer stopped to work on it.
This is especially feature of hash functions, because when you work on block cipher, you have to do two functions
(encryption/decryption), while hashing function is single.
Weirdest ever amateur encryption algorithm I once saw, encrypted only odd bytes of input block, while even bytes
left untouched, so the input plain text has been partially seen in the resulting encrypted block. It was encryption
routine used in license key validation. Hard to believe someone did this on purpose. Most likely, it was just an
unnoticed bug.
Not uncommon thing is when both encryption and decryption functions are laying side-by-side in the code: probably
a programmer just wrote two functions and they are residing in the same source code file/object file. So if you see a
license decryption function in code, try to inspect functions near it.
Also, not uncommon bug is when a encryption key(s) are present in the executable file. Even, (RSA) private keys
that can be used to sign license.
This is a real snippet from some software. This function converts MAC address of your network card or HDD’s
serial number to a license key that is to be compared with what user is supplied.
Note usage of srand()/rand() functions as blackbox functions.
# include <stdio .h>
v4 = MAC_or_HDD_SN ;
if ( MAC_or_HDD_SN <= 1000000 )
{
if ( MAC_or_HDD_SN <= 1000 )
v4 = 1001001 * MAC_or_HDD_SN ;
else
v4 = 1001 * MAC_or_HDD_SN ;
}
v5 = 112 + v4 % (532123 + 916245078) ;
v6 = ( signed int)(112 + v4 % (532123 + 916245078) ) % 1000000;
v2 = v5 % 1000;
srand (v5);
v1 = rand () % 1000;
srand (v6);
v7 = rand () % 1000;
srand (v2);
v8 = rand ();
sprintf (
out_str ,
( const char *)L"%03d%03d%03d%03d",
v1 ,
v7 ,
8 Message authentication code
9 Elliptic curve cryptography
555
v8 % 1000 ,
(v8 % 1000 + 1000 * (v7 + 1000 * v1)) % 719);
}
int main ()
{
char tmp [128];
sub_832EC0 (0 x29d989d4 , tmp);
printf ("%s\n", tmp);
};
Manual decompiling
Here its assembly language listing in IDA:
sub_401510 proc near
; ECX = input
mov rdx , 5 D7E0D1F2E0F1F84h
mov rax , rcx ; input
imul rax , rdx
mov rdx , 388 D76AEE8CB1500h
mov ecx , eax
and ecx , 0Fh
ror rax , cl
xor rax , rdx
mov rdx , 0 D2E9EE7E83C4285Bh
mov ecx , eax
and ecx , 0Fh
rol rax , cl
lea r8 , [rax+rdx]
mov rdx , 8888888888888889 h
mov rax , r8
mul rdx
shr rdx , 5
mov rax , rdx
lea rcx , [r8+rdx *4]
shl rax , 6
sub rcx , rax
mov rax , r8
rol rax , cl
; EAX = output
retn
sub_401510 endp
The example was compiled by GCC, so the first argument is passed in ECX.
10 https://0xec.blogspot.com/2016/04/reversing-petya-ransomware-with.html
11 https://yurichev.com/mirrors/SAT_SMT_crypto/solution.pdf
12 This example was also used by Murphy Berzish in his lecture about SAT and SMT: http://mirror.csclub.uwaterloo.ca/csclub/
mtrberzi-sat-smt-slides.pdf, http://mirror.csclub.uwaterloo.ca/csclub/mtrberzi-sat-smt.mp4
556
If you don’t have Hex-Rays or Ghidra, or if you don’t trust to it, you can try to reverse this code manually. One
method is to represent the CPU registers as local C variables and replace each instruction by a one-line equivalent
expression, like:
uint64_t f( uint64_t input )
{
uint64_t rax , rbx , rcx , rdx , r8;
ecx=input ;
rdx =0 x5D7E0D1F2E0F1F84 ;
rax=rcx;
rax *= rdx;
rdx =0 x388D76AEE8CB1500 ;
rax= _lrotr (rax , rax &0 xF); // rotate right
rax ^= rdx;
rdx =0 xD2E9EE7E83C4285B ;
rax= _lrotl (rax , rax &0 xF); // rotate left
r8=rax+rdx;
rdx =0 x8888888888888889 ;
rax=r8;
rax *= rdx;
rdx=rdx > >5;
rax=rdx;
rcx=r8+rdx *4;
rax=rax < <6;
rcx=rcx -rax;
rax=r8
rax= _lrotl (rax , rcx &0 xFF); // rotate left
return rax;
};
If you are careful enough, this code can be compiled and will even work in the same way as the original.
Then, we are going to rewrite it gradually, keeping in mind all registers usage. Attention and focus is very important
here—any tiny typo may ruin all your work!
Here is the first step:
uint64_t f( uint64_t input )
{
uint64_t rax , rbx , rcx , rdx , r8;
ecx=input ;
rdx =0 x5D7E0D1F2E0F1F84 ;
rax=rcx;
rax *= rdx;
rdx =0 x388D76AEE8CB1500 ;
rax= _lrotr (rax , rax &0 xF); // rotate right
rax ^= rdx;
rdx =0 xD2E9EE7E83C4285B ;
rax= _lrotl (rax , rax &0 xF); // rotate left
r8=rax+rdx;
rdx =0 x8888888888888889 ;
rax=r8;
rax *= rdx;
// RDX here is a high part of multiplication result
rdx=rdx > >5;
// RDX here is division result !
rax=rdx;
rcx=r8+rdx *4;
557
rax=rax < <6;
rcx=rcx -rax;
rax=r8
rax= _lrotl (rax , rcx &0 xFF); // rotate left
return rax;
};
Next step:
uint64_t f( uint64_t input )
{
uint64_t rax , rbx , rcx , rdx , r8;
ecx=input ;
rdx =0 x5D7E0D1F2E0F1F84 ;
rax=rcx;
rax *= rdx;
rdx =0 x388D76AEE8CB1500 ;
rax= _lrotr (rax , rax &0 xF); // rotate right
rax ^= rdx;
rdx =0 xD2E9EE7E83C4285B ;
rax= _lrotl (rax , rax &0 xF); // rotate left
r8=rax+rdx;
rdx =0 x8888888888888889 ;
rax=r8;
rax *= rdx;
// RDX here is a high part of multiplication result
rdx=rdx > >5;
// RDX here is division result !
rax=rdx;
We can spot the division using multiplication. Indeed, let’s calculate the divider in Wolfram Mathematica:
We get this:
uint64_t f( uint64_t input )
{
uint64_t rax , rbx , rcx , rdx , r8;
ecx=input ;
rdx =0 x5D7E0D1F2E0F1F84 ;
rax=rcx;
rax *= rdx;
rdx =0 x388D76AEE8CB1500 ;
rax= _lrotr (rax , rax &0 xF); // rotate right
rax ^= rdx;
rdx =0 xD2E9EE7E83C4285B ;
rax= _lrotl (rax , rax &0 xF); // rotate left
r8=rax+rdx;
558
rax=rdx=r8 /60;
rax=input ;
rax *=0 x5D7E0D1F2E0F1F84 ;
rax= _lrotr (rax , rax &0 xF); // rotate right
rax ^=0 x388D76AEE8CB1500 ;
rax= _lrotl (rax , rax &0 xF); // rotate left
r8=rax +0 xD2E9EE7E83C4285B ;
By simple reducing, we finally see that it’s calculating the remainder, not the quotient:
uint64_t f( uint64_t input )
{
uint64_t rax , rbx , rcx , rdx , r8;
rax=input ;
rax *=0 x5D7E0D1F2E0F1F84 ;
rax= _lrotr (rax , rax &0 xF); // rotate right
rax ^=0 x388D76AEE8CB1500 ;
rax= _lrotl (rax , rax &0 xF); // rotate left
r8=rax +0 xD2E9EE7E83C4285B ;
# define C1 0 x5D7E0D1F2E0F1F84
# define C2 0 x388D76AEE8CB1500
# define C3 0 xD2E9EE7E83C4285B
int main ()
{
printf ("% llu\n", hash (...) );
};
Since we are not cryptanalysts we can’t find an easy way to generate the input value for some specific output value.
The rotate instruction’s coefficients look frightening—it’s a warranty that the function is not bijective, it is rather
surjective, it has collisions, or, speaking more simply, many inputs may be mapped to one output.
Brute-force is not solution because values are 64-bit ones, that’s beyond reality.
This can be automated. Each found result can be added as a constraint and then the next result will be searched
for. Here is a slightly more sophisticated example:
1 #!/ usr/bin/env python3
2
3 from z3 import *
4
5 C1 =0 x5D7E0D1F2E0F1F84
6 C2 =0 x388D76AEE8CB1500
7 C3 =0 xD2E9EE7E83C4285B
8
9 inp , i1 , i2 , i3 , i4 , i5 , i6 , outp = BitVecs ('inp i1 i2 i3 i4 i5 i6 outp ', 64)
10
11 s = Solver ()
561
12 s.add(i1 == inp*C1)
13 s.add(i2 == RotateRight (i1 , i1 & 0xF))
14 s.add(i3 ==i2 ^ C2)
15 s.add(i4 == RotateLeft (i3 , i3 & 0xF))
16 s.add(i5 ==i4 + C3)
17 s.add(outp == RotateLeft (i5 , URem(i5 , 60)))
18
19 s.add(outp ==10816636949158156260)
20
21 # copypasted from http :// stackoverflow .com/ questions /11867611/ z3py -checking -all -
solutions -for - equation
22 result =[]
23 while True:
24 if s. check () == sat:
25 m = s.model ()
26 print (m[inp ])
27 result . append (m)
28 # Create a new constraint the blocks the current model
29 block = []
30 for d in m:
31 # d is a declaration
32 if d.arity () > 0:
33 raise Z3Exception (" uninterpreted functions are not supported ")
34 # create a constant from declaration
35 c=d()
36 if is_array (c) or c.sort ().kind () == Z3_UNINTERPRETED_SORT :
37 raise Z3Exception (" arrays and uninterpreted sorts are not supported ")
38 block . append (c != m[d])
39 s.add(Or(block ))
40 else:
41 print (" results total =",len( result ))
42 break
We got:
1364123924608584563
1234567890
9223372038089343698
4611686019661955794
13835058056516731602
3096040143925676201
12319412180780452009
7707726162353064105
16931098199207839913
1906652839273745429
11130024876128521237
15741710894555909141
6518338857701133333
5975809943035972467
15199181979890748275
10587495961463360371
results total= 16
It is indeed so:
sat
[i1 = 14869545517796235860 ,
i3 = 8388171335828825253 ,
i5 = 6918262285561543945 ,
inp = 1370377541658871093 ,
outp = 14543180351754208565 ,
i4 = 10167065714588685486 ,
i2 = 5541032613289652645]
inp =0 x13048F1D12C00535
outp =0 xC9D3C17A12C00535
Let’s be more sadistic and add another constraint: last 16 bits must be 0x1234:
1 #!/ usr/bin/env python3
2
3 from z3 import *
4
5 C1 =0 x5D7E0D1F2E0F1F84
6 C2 =0 x388D76AEE8CB1500
7 C3 =0 xD2E9EE7E83C4285B
8
9 inp , i1 , i2 , i3 , i4 , i5 , i6 , outp = BitVecs ('inp i1 i2 i3 i4 i5 i6 outp ', 64)
10
11 s = Solver ()
12 s.add(i1 == inp*C1)
13 s.add(i2 == RotateRight (i1 , i1 & 0xF))
14 s.add(i3 ==i2 ^ C2)
15 s.add(i4 == RotateLeft (i3 , i3 & 0xF))
16 s.add(i5 ==i4 + C3)
17 s.add(outp == RotateLeft (i5 , URem(i5 , 60)))
18
19 s.add(outp & 0 xFFFFFFFF == inp & 0 xFFFFFFFF )
20 s.add(outp & 0 xFFFF == 0 x1234 )
21
22 print (s. check ())
23 m=s. model ()
24 print (m)
25 print (" inp =0x%X" % m[inp ]. as_long ())
26 print ("outp =0x%X" % m[outp ]. as_long ())
563
Z3 works very fast and it implies that the algorithm is weak, it is not cryptographic at all (like the most of the
amateur cryptography).
# encrypt it back:
The author saw this in shareware checking license keys, etc, when a key is encrypted (yes) to get information from
it. Perhaps, their key generator decrypted data when putting it to key file. Hopefully, they swapped functions just by
mistake, but it worked, so they left it all as is.
There is also a case of 3DES or Triple DES. Several variants are available, and the most popular encryption
procedure (DES-EDE3) is: 1) encrypt with key1; 2) decrypt with key2; 3) encrypt with key3. The decryption
procedure is inverted. This procedure is as secure as triple encryption using 3 keys (DES-EEE3).
Chapter 20
First-Order Logic
For exists/forall/forall:
( assert
( exists ((x Bool)) ( forall ((y Bool)) ( forall ((z Bool))
(and
(or x y)
(or (not x) z)
(or y (not z))
)))
)
)
(check -sat)
unsat
unsat
564
565
unsat
sat
unsat
unsat
sat
sat
For (a):
( assert
( forall ((x Bool) (y Bool) (z Bool))
(=
(or (xor x y) z)
(xor (or x z) (or y z))
)
)
)
(check -sat)
For (b):
( assert
( forall ((x Bool) (y Bool) (z Bool) (w Bool))
(=
(or (xor w x y) z)
(xor (or w z) (or x z) (or y z))
)
)
)
(check -sat)
For (c):
( assert
( forall ((x Bool) (y Bool) (z Bool))
(=
(or (xor x y) (xor y z))
(or (xor x z) (xor y z))
)
)
)
(check -sat)
Results:
% z3 -smt2 Knuth_a .smt
unsat
% z3 -smt2 Knuth_b .smt
sat
% z3 -smt2 Knuth_c .smt
sat
Chapter 21
Cellular automata
…where “center” is state of a center cell, “neighbours” are 8 neighbouring cells, popcnt2 is a function which returns
True if it has exactly 2 bits on input, popcnt3 is the same, but for 3 bits (just like these were used in my “Minesweeper”
example (3.10.2)).
Using Wolfram Mathematica, I first create all helper functions and truth table for the function, which returns true,
if a cell must be present in the next state, or false if not:
In [1]:= popcount [ n_Integer ]:= IntegerDigits [n ,2] // Total
566
567
{1,0,0,0,0,0,0,1,1}->1,
...
...
Also, we need a second function, inverted one, which will return true if the cell must be absent in the next state,
or false otherwise:
In [15]:= NewCellIsFalse = Flatten [ Table [Join [{ center }, PadLeft [ IntegerDigits [ neighbours
,2] ,8]] ->
Boole[Not[ newcell [center , neighbours ]]] ,{ neighbours ,0 ,255} ,{ center ,0 ,1}]]
Out [15]= {{0,0,0 ,0 ,0 ,0 ,0 ,0 ,0} - >1 ,
{1,0,0,0,0,0,0,0,0}->1,
{0,0,0,0,0,0,0,0,1}->1,
{1,0,0,0,0,0,0,0,1}->1,
{0,0,0,0,0,0,0,1,0}->1,
...
...
Using the very same way as in my “Minesweeper” example (3.10.2), I can convert CNF expression to list of clauses:
def mathematica_to_CNF (s:str , d:Dict[str , str ]) -> List[List[str ]]:
for k in d.keys ():
s=s. replace (k, d[k])
s=s. replace ("!" , " -"). replace ("||" , " "). replace ("(" , ""). replace (")", "")
lst=s. split ("&&")
rt =[]
for l in lst:
rt. append (l. split (" "))
return rt
And again, as in “Minesweeper”, there is an invisible border, to make processing simpler. SAT variables are also
numbered as in previous example:
568
1 2 3 4 5 6 7 8 9 10 11
12 13 14 15 16 17 18 19 20 21 22
23 24 25 26 27 28 29 30 31 32 33
34 35 36 37 38 39 40 41 42 43 44
...
100 101 102 103 104 105 106 107 108 109 110
111 112 113 114 115 116 117 118 119 120 121
Also, there is a visible border, always fixed to False, to make things simpler.
Now the working source code. Whenever we encounter “*” in final_state[], we add clauses generated by
cell_is_true() function, or cell_is_false() if otherwise. When we get a solution, it is negated and added to
the list of clauses, so when minisat is executed next time, it will skip solution which was already printed.
...
...
( https://smt.st/current_tree/CA/GoL/GoL_SAT_utils.py )
#!/ usr/bin/ python3
import os
from GoL_SAT_utils import *
final_state =[
" * ",
"* *",
" * "]
assert s. solve ()
571
tmp= SAT_solution_to_grid (grid , VAR_FALSE , s.solution , H, W)
print_grid (tmp)
write_RLE (tmp)
return tmp
while True:
try_again ()
if s. fetch_next_solution () == False :
break
print ("")
( https://smt.st/current_tree/CA/GoL/reverse1.py )
Here is the result:
HEIGHT = 3 WIDTH= 3
.*.
*.*
.*.
1. rle written
.**
*..
*.*
2. rle written
**.
..*
*.*
3. rle written
*.*
*..
.**
4. rle written
*.*
..*
**.
5. rle written
*.*
.*.
*.*
6. rle written
unsat!
The first result is the same as initial state. Indeed: this is “still life”, i.e., state which will never change, and it is
correct solution. The last solution is also valid.
Now the problem: 2nd, 3rd, 4th and 5th solutions are equivalent to each other, they just mirrored or rotated.
In fact, this is reflectional1 (like in mirror) and rotational2 symmetries. We can solve this easily: we will take each
solution, reflect and rotate it and add them negated to the list of clauses, so minisat will skip them during its work:
...
while True:
solution = try_again ()
1 https://en.wikipedia.org/wiki/Reflection_symmetry
2 https://en.wikipedia.org/wiki/Rotational_symmetry
572
if solution == None:
break
...
( https://smt.st/current_tree/CA/GoL/reverse2.py )
Functions reflect_vertically(), reflect_horizontally and rotate_squarearray() are simple array manip-
ulation routines.
Now we get just 3 solutions:
HEIGHT = 3 WIDTH= 3
.*.
*.*
.*.
1. rle written
.**
*..
*.*
2. rle written
*.*
.*.
*.*
3. rle written
unsat!
_PRE_BEGIN
HEIGHT = 3 WIDTH= 3
...
***
...
1. rle written
unsat!
final_state =[
" * ",
" ",
" ** ",
" * ",
" * ",
" *** "]
.*.*.
..*..
.**.*
..*..
*.*.*
.**..
2. rle written
..*.*
..**.
.**..
....*
*.*.*
.**..
3. rle written
..*.*
..**.
.**..
*...*
..*.*
.**..
4. rle written
...
HEIGHT = 10 WIDTH = 13
..*.*.**.....
.....*****...
574
....**..*....
......*...*..
..**...*.*...
.*..*.*.**..*
*....*....*.*
..*.*..*.....
..*.....*.*..
....**..*.*..
1. rle written
*.*.*.**.....
.....*****...
....**..*....
......*...*..
..**...*.*...
.*..*.*.**..*
*....*....*.*
..*.*..*.....
..*.....*.*..
....**..*.*..
2. rle written
..*.*.**.....
*....*****...
....**..*....
......*...*..
..**...*.*...
.*..*.*.**..*
*....*....*.*
..*.*..*.....
..*.....*.*..
....**..*.*..
3. rle written
...
I don’t know how many possible states can lead to “space invader”, perhaps, too many. Had to stop it. And it
slows down during execution, because number of clauses is increasing (because of negating solutions addition).
All solutions are also exported to RLE files, which can be opened by Golly3 .
Garden of Eden
As they say, “A Garden of Eden is a pattern that has no parents and thus can only occur in generation 0.”4 .
We can check if these are really gardens:
#!/ usr/bin/ python3
if s. solve () == False :
return None
return tmp
solution = try_again ()
if solution == None:
print ("Eden ( UNSAT )")
else:
print ("not Eden (SAT)")
...
...
W=3 # WIDTH
577
H=3 # HEIGHT
s= SAT_lib . SAT_lib ()
VAR_FALSE =s. const_false
grid =[[s. create_var () for w in range (W)] for h in range (H)]
return t
while True:
solution = try_again ()
if solution == None:
break
( https://smt.st/current_tree/CA/GoL/SL1.py )
578
What we’ve got for 2 · 2?
..
..
1. rle written
**
**
2. rle written
unsat!
Both solutions are correct: empty square will progress into empty square (no cells are born). 2 · 2 box is also known
“still life”.
What about 3 · 3 square?
...
...
...
1. rle written
.**
.**
...
2. rle written
.**
*.*
**.
3. rle written
.*.
*.*
**.
4. rle written
.*.
*.*
.*.
5. rle written
unsat!
Here is a problem: we see familiar 2 · 2 box, but shifted. This is indeed correct solution, but we don’t interested in
it, because it has been already seen.
What we can do is add another condition. We can force minisat to find solutions with no empty rows and columns.
This is easy. These are SAT variables for 5 · 5 square:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
...
That means that each row must have at least one True value somewhere. We can also do this for each column as
well.
579
...
...
( https://smt.st/current_tree/CA/GoL/SL2.py )
Now we can see that 3 · 3 square has 3 possible “still lives”:
.*.
*.*
**.
1. rle written
.*.
*.*
.*.
2. rle written
.**
*.*
**.
3. rle written
unsat!
4 · 4 has 7:
..**
...*
***.
*...
1. rle written
..**
..*.
*.*.
**..
2. rle written
..**
.*.*
*.*.
**..
3. rle written
..*.
.*.*
*.*.
**..
4. rle written
.**.
*..*
*.*.
580
.*..
5. rle written
..*.
.*.*
*.*.
.*..
6. rle written
.**.
*..*
*..*
.**.
7. rle written
unsat!
When I try large squares, like 20 · 20, funny things happen. First of all, minisat finds solutions not very pleasing
aesthetically, but still correct, like:
....**.**.**.**.**.*
**..*.**.**.**.**.**
*...................
.*..................
**..................
*...................
.*..................
**..................
*...................
.*..................
**..................
*...................
.*..................
**..................
*...................
.*..................
..*.................
...*................
***.................
*...................
1. rle written
...
Indeed: all rows and columns has at least one True value.
Then minisat begins to add smaller “still lives” into the whole picture:
.**....**...**...**.
.**...*..*.*.*...*..
.......**...*......*
..................**
...**............*..
...*.*...........*..
....*.*........**...
**...*.*...**..*....
*.*...*....*....*...
.*..........****.*..
................*...
..*...**..******....
.*.*..*..*..........
*..*...*.*..****....
***.***..*.*....*...
....*..***.**..**...
581
**.*..*.............
.*.**.**..........**
*..*..*..*......*..*
**..**..**......**..
43. rle written
In other words, result is a square consisting of smaller “still lives”. It then altering these parts slightly, shifting
back and forth. Is it cheating? Anyway, it does it in a strict accordance to rules we defined.
However, when I switch to Parallel Lingeling, things are slightly different:
...............**.**
...............*...*
....**..**.**...**..
.....*...*.*..*.*...
.....*.*.*.*.**.*...
...**.*.*...*..*....
..*.*.*......**.....
..**...*............
........*...........
.........*..........
..........*.........
...........*........
............*...**..
.............*..*...
..............*.*...
...............*....
.**.................
..*.................
*...................
**..................
2. rle written
...
...
( https://smt.st/current_tree/CA/GoL/SL3.py )
This is indeed denser:
..**.**......*.*.*..
...*.*.....***.**.*.
...*..*...*.......*.
....*.*..*.*......**
...**.*.*..*...**.*.
..*...*.***.....*.*.
...*.*.*......*..*..
****.*..*....*.**...
*....**.*....*.*....
582
...**..*...**..*....
..*..*....*....*.**.
.*.*.**....****.*..*
..*.*....*.*..*..**.
....*.****..*..*.*..
....**....*.*.**..*.
*.**...****.*..*.**.
**...**.....**.*....
...**..*..**..*.**.*
***.*.*..*.*..*.*.**
*....*....*....*....
1. rle written
..**.**......*.*.*..
...*.*.....***.**.*.
...*..*...*.......*.
....*.*..*.*......**
...**.*.*..*...**.*.
..*...*.***.....*.*.
...*.*.*......*..*..
****.*..*....*.**...
*....**.*....*.*....
...**..*...**..*....
..*..*....*....*.**.
.*.*.**....****.*..*
..*.*....*.*..*..**.
....*.****..*..*.*..
....**....*.*.**..*.
*.**...****.*..*.**.
**...**.....**.*....
...**..*.***..*.**.*
***.*..*.*..*.*.*.**
*.......*..**.**....
2. rle written
...
Let’s try more dense, one mandatory true cell per each 4-cell chunk:
.**.**...*....**..**
*.*.*...*.*..*..*..*
*....*...*.*..*.**..
.***.*.....*.**.*...
..*.*.....**...*..*.
*......**..*...*.**.
**.....*...*.**.*...
...**...*...**..*...
**.*..*.*......*...*
.*...**.**..***.****
.*....*.*..*..*.*...
**.***...*.**...*.**
.*.*..****.....*..*.
*....*.....**..**.*.
*.***.*..**.*.....**
.*...*..*......**...
...*.*.**......*.***
..**.*.....**......*
*..*.*.**..*.*..***.
**....*.*...*...*...
1. rle written
.**.**...*....**..**
583
*.*.*...*.*..*..*..*
*....*...*.*..*.**..
.***.*.....*.**.*...
..*.*.....**...*..*.
*......**..*...*.**.
**.....*...*.**.*...
...**...*...**..*...
**.*..*.*......*...*
.*...**.**..***.****
.*....*.*..*..*.*...
**.***...*.**...*.**
.*.*..****.....*..*.
*....*.....**..**.*.
*.***.*..**.*.....**
.*...*..*......**..*
...*.*.**......*.**.
..**.*.....**....*..
*..*.*.**..*.*...*.*
**....*.*...*.....**
2. rle written
...
**.*..**...**.**....
*.**..*.*...*.*.*.**
....**..*...*...*.*.
.**..*.*.**.*.*.*.*.
..**.*.*...*.**.*.**
*...*.*.**.*....*.*.
**.*..*...*.*.***..*
.*.*.*.***..**...**.
.*.*.*.*..**...*.*..
**.**.*..*...**.*..*
..*...*.**.**.*.*.**
..*.**.*..*.*.*.*...
**.*.*...*..*.*.*...
.*.*...*.**..*..***.
.*..****.*....**...*
584
..*.*...*..*...*..*.
.**..**.*.**...*.*..
*..*.*..*.*...**.**.
*..*.*.*..*..*..*..*
.**...*...**..**..**
2. rle written
...
This is most dense. Unfortunately, it’s impossible to construct “still life” with one mandatory true cell per each
2-cell chunk.
(Open-WBO has been switched to the fastest algorithm for the task, ”LinearSU” (1st one).
I just maximize all cells, adding them as soft clauses:
...
for r in range(H):
for c in range (W):
s. add_soft_clause ([ GoL_SAT_utils . coords_to_var (grid , VAR_FALSE , r, c, H, W)],
1)
...
( https://smt.st/current_tree/CA/GoL/SL_MaxSAT.py )
See also:
• CSPLIB Problem 032: Maximum density still life
• Maximum Density Still Life
• Also, an interesting paper: Chu, G., & Stuckey, P. J. (2012). A complete solution to the Maximum Density Still
Life Problem. Artificial Intelligence, 184-185, 1–16. doi:10.1016/j.artint.2012.02.001
Further work: count solutions, eliminating symmetrical. Search for symmetrical still lives, like: https://www.
conwaylife.com/wiki/Clips, https://www.conwaylife.com/wiki/Inflected_clips.
from z3 import *
WIDTH =15
rules =[]
for i in range (8):
if (( RULE >>i)&1) ==1:
rules . append (True)
else:
rules . append ( False )
S=[[ Bool("%d_%d" % (s, i)) for i in range ( WIDTH )] for s in range ( STATES )]
s= Solver ()
if WRAPPED == False :
for st in range ( STATES ):
s.add(S[st ][0]== False )
s.add(S[st][ WIDTH -1]== False )
if WRAPPED == False :
for st in range (1, STATES ):
for i in range (1, WIDTH -1):
s.add(S[st ][i] == f(S[st -1][i -1] , S[st -1][i], S[st -1][i+1]))
else:
for st in range (1, STATES ):
for i in range ( WIDTH ):
s.add(S[st ][i] == f(S[st -1][(i -1) % WIDTH ], S[st -1][i], S[st -1][( i+1)
% WIDTH ]))
if s. check () == unsat :
return
# print " unsat "
#exit (0)
m=s. model ()
print ("RULE =%d STATES =%d, WRAPPED =%s, SHIFTED =%d" % (RULE , STATES , str( WRAPPED ),
SHIFTED ))
for st in range ( STATES ):
t=""
for i in range (WIDTH ):
if str(m[S[st ][i]]) =="False ":
t=t+"."
else:
t=t+"*"
print (t)
SHIFTED=0 means oscillator, SHIFTED=1 means glider slipping left, SHIFTED=2 — slipping right.
Chapter 22
Everything else
1 http://www.cs.utsa.edu/~wagner/knuth/fasc0b.pdf#page=18&zoom=200,-6,389
595
596
I’m assigning 1..7 SAT variables to t,u,v,w,x,y,z variables and translating this to a CNF file (lines prefixed with ”c
” are comments, of course):
p cnf 7 16
c variables :
c 1 t
c 2 u
c 3 v
c 4 w
c 5 x
c 6 y
c 7 z
c clauses .
c first line ( commented ) is clause number and variable names .
c second line: CNF clause
c #1 -t -w
-1 -4 0
c #2 -u -z
-2 -7 0
c #3 u y
2 -6 0
c #4 u z
2 7 0
c #5 -y z
-6 7 0
c #6 t -x
1 -5 0
c #7 t z
1 7 0
c #8 -x z
597
-5 7 0
c #9 -t -z
-1 -7 0
c #10 -v y
-3 6 0
c #11 v -w
3 -4 0
c #12 v -y
3 -6 0
c #13 -w -y
-4 -6 0
c #14 y x
2 5 0
c #15 -u v
-2 3 0
c #16 -v -x
-3 -5 0
The instance is UNSAT, of course. Now I’m running PicoMUS utility from Picosat SAT solver, that extracts
MUS2 :
c [ picomus ] WARNING : no output file given
c [ picomus ] WARNING : PicoSAT compiled without trace generation
c [ picomus ] WARNING : core extraction disabled
s UNSATISFIABLE
c [ picomus ] computed MUS of size 8 out of 16 (50%)
v 2
v 4
v 5
v 6
v 9
v 10
v 14
v 15
v 0
This is a list of clauses. If all of them are removed from your instances, it will be transformed from UNSAT to
SAT. Let’s see, what are these clauses?
-2 -7 0 # 2 -u -z
2 7 0 # 4 u z
-6 7 0 # 5 -y z
1 -5 0 # 6 t -x
-1 -7 0 # 9 -t -z
-3 6 0 # 10 -v y
-2 3 0 # 15 -u v
These are indeed clauses with variables that are in the vicious cycle3 according to D.Knuth.
Note that the MUS is not the smallest possible (sub)set, but just a set that you can use for diagnostics. Minimal in
MUS means that is cannot be reduced in its form. However, smallest MUSes are still possible to exist, but PicoMUS
isn’t guaranteed to find a smallest MUS.
Now there is another tool in Picosat: PicoMCS, that extracts MCS4 . What can it say?
s UNSATISFIABLE
v 10 0
v 14 0
v 15 0
v 2 0
v 5 0
v 6 0
2 Minimal Unsatisfiable Subformula
3 https://en.wikipedia.org/wiki/Virtuous_circle_and_vicious_circle
4 Minimal Correction Subset
598
v 8 4 0
v 9 0
Each line in list is a list of clauses. If a list of clauses is removed from your instances, it transforms from UNSAT
to SAT. Indeed, 10th clause is an element of vicious cycle. And so is 15th, etc. But 14th clause is not. But removing
it from the instance will solve the problem. Also, if 8th and 4th clauses are both to be removed from the instance, it
will be SAT, but not one-by-one.
Why not always using PicoMCS instead of PicoMUS? It’s way slower. And in practice, you can be satisfied with
larger MUS.
Now what if we can represent each clause as weighted and use MaxSAT solver? (The same Open-WBO, I use so
often.)
I prepend each clause with ’1’, that is a (smallest) weight of each clause:
p wcnf 7 16 1000
1 -1 -4 0
1 -2 -7 0
1 2 -6 0
1 2 7 0
1 -6 7 0
1 1 -5 0
1 7 0
1 -5 7 0
1 -1 -7 0
1 -3 6 0
1 3 -4 0
1 3 -6 0
1 -4 -6 0
1 2 5 0
1 -2 3 0
1 -3 -5 0
... and asking Open-WBO MaxSAT solver to find such an assignment, that can satisfy as many clauses, as possible:
s OPTIMUM FOUND
v -1 2 3 -4 -5 6 7
Let’s see, which clauses gets satisfied with this assignment? I manually added comments to the WCNF instance:
p wcnf 7 16 1000
1 -1 -4 0 # -t -w OK
1 -2 -7 0 # -u -z UNSAT
1 2 -6 0 # u y OK
1 2 7 0 # u z OK
1 -6 7 0 # -y z OK
1 1 -5 0 # t -x OK
1 1 7 0 # t z OK
1 -5 7 0 # -x z OK
1 -1 -7 0 # -t -z OK
1 -3 6 0 # -v y OK
1 3 -4 0 # v -w OK
1 3 -6 0 # v -y OK
1 -4 -6 0 # -w -y OK
1 2 5 0 # y x OK
1 -2 3 0 # -u v OK
1 -3 -5 0 # -v -x OK
22.1.1 MUSer2
This is a tool5 acting as a front-end MUS extractor on top of a SAT-solver like MiniSat, PicoSat, Glucose, UBCSAT,
etc. Unlike PicoMUS, it produces a CNF file for our example, but it contains 8 clauses as well:
5 https://github.com/meelgroup/muser
599
To match successfully, the second group must coincide with the first, like <b>test</b>, but not <a>test</b>.
Another practical usage I’ve heard: match "string" or 'string', but not "string'.
Also, you can find two longest repeating substrings in an input
√ string 7 .
Some other uses are very arcane: divide input number by 28 , detecting factorial number9 .
This text has been inspired by “Reduction of 3-CNF-SAT to Perl Regular Expression Matching”10 , please read
it first. However the author incorrectly states that only 3SAT problems are solvable. In fact, any SAT instance is
solvable, consisting of clauses of arbitrary sizes.
Also, since my SAT/CNF instances usually has more variables than 9 and I can’t use backreferences like \1 ... \9,
I use different method (Python):
The syntax for backreferences in an expression such as (...) \1 refers to the number
of the group . ’Theres naturally a variant that uses the group name instead of the
number . This is another Python extension : (?P=name) indicates that the contents of
the group called name should again be matched at the current point . The regular
expression for finding doubled words , \b(\w+)\s+\1\b can also be written as \b(?P<
word >\w+)\s+(?P=word)\b
( https://docs.python.org/3/howto/regex.html )
See also: https://www.regular-expressions.info/replacebackref.html.
Now let’s take this small CNF instance:
p cnf 5 11
-2 -5 0
-2 -4 0
-4 -5 0
-2 -3 0
-1 -4 0
-1 -5 0
-1 -2 0
-1 -3 0
-3 -4 0
-3 -5 0
1 2 3 4 5 0
This is what I call popcnt1: only 1 variable must be true, all the rest are always false.
6 https://www.regular-expressions.info/backref.html
7 https://stackoverflow.com/questions/9177647/regular-epxressions-that-matches-the-longest-repeating-sequence https:
//stackoverflow.com/questions/44465284/match-longest-repeating-sequence-that-is-not-made-of-a-repeating-sequence
8 https://codegolf.stackexchange.com/questions/198427/shift-right-by-half-a-bit/198428#198428
9 https://codegolf.stackexchange.com/questions/121731/is-this-number-a-factorial/178979#178979
10 https://perl.plover.com/NPC/NPC-3SAT.html
600
s SATISFIABLE
v -1 -2 -3 -4 5 0
s SATISFIABLE
v -1 -2 -3 4 -5 0
s SATISFIABLE
v -1 -2 3 -4 -5 0
s SATISFIABLE
v -1 2 -3 -4 -5 0
s SATISFIABLE
v 1 -2 -3 -4 -5 0
s SOLUTIONS 5
You see: an input string has as many x’s as many variables the CNF instance has. Then, as many x, substrings,
as many clauses the instance has.
Take the first part of the pattern: ^(?P<a_1>x?)(?P<a_2>x?)(?P<a_3>x?)(?P<a_4>x?)(?P<a_5>x?).*; ...
Roughly, it means that the group can match x, or may not. Let matcher decide on its own. The second part of
the pattern are clauses. For the 1 2 3 4 5 clause we have (?:(?P=a_1)|(?P=a_2)|(?P=a_3)|(?P=a_4)|(?P=a_5)),
there. That means, that the whole group must match x, but we don’t tell how: one of 5 subgroups may match, and
each subgroup is actually backreference to the first part of pattern. But these terms are all positive. What about
negative terms?
For the -2 -5 clause, we have (?:(?P=a_2)x|(?P=a_5)x), there. That means that the whole group must match
x, and again, we don’t tell how, but both backreferences a_2 and a_5 are prohibited, if present simultaneously. It’s
OK for a_2 to match x, but then the second part of the union would match. It’s OK for a_5 to match x, and then
the first part of the union would match. Also, it’s OK for both a_2 and a_5 match nothing: either part of union will
match x then.
If regex can’t match, this means the input instance is unsatisfiable.
Likewise, I can run my popcnt4 instance, which commands to be 4 of input 8 variables be true:
string = xxxxxxxx ;x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x
,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x
,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,
pattern =^(?P<a_1 >x?) (?P<a_2 >x?) (?P<a_3 >x?) (?P<a_4 >x?) (?P<a_5 >x?) (?P<a_6 >x?) (?P<a_7 >x
?)(?P<a_8 >x?) .*;(?:(? P=a_1)x|(?P=a_2)x|(?P=a_3)x|(?P=a_4)x|(?P=a_5)x) ,(?:(?P=a_1)x
|(?P=a_2)x|(?P=a_3)x|(?P=a_4)x|(?P=a_6)x) ,(?:(?P=a_1)x|(?P=a_2)x|(?P=a_3)x|(?P=a_4
)x|(?P=a_7)x) ,(?:(?P=a_1)x|(?P=a_2)x|(?P=a_3)x|(?P=a_4)x|(?P=a_8)x) ,(?:(?P=a_1)x
|(?P=a_2)x|(?P=a_3)x|(?P=a_5)x|(?P=a_6)x) ,(?:(?P=a_1)x|(?P=a_2)x|(?P=a_3)x|(?P=a_5
)x|(?P=
...
import re
#n =12300 # composite
#n =123001 # prime , 27s
#n =12300200 # composite
n =123023 # composite , one factor : 43
#n =123027 # composite , one factor : 3
#n =223099 # prime , 87s
It can find factors for small numbers. And here is how it works. In plain English, we asking regex matcher to
find such a string, that consists of some number (≥ 2) of ”1” ’s ((11+?)), which is glueled with the same string (\1)
arbitrary number of times (+).
Of course it’s extremely slow, and even worse than bruteforce. For 87 seconds on my old 2GHz CPU I found out
that 223099 is a prime.
But again, this is like a thought experiment. A reduction from one problem (integer factorization) to another (find
equal substrings in a string).
Hi Dennis ,
I was curious to see how much perf could be gained from using the PCRE
JIT with your regex SAT solver , so I reimplemented it in PHP (I don 't
know Perl and PHP has native PCRE support ).
Time comparison below . It 's about 50% constant speedup . Thought you
might be interested .
Cheers ,
Allan
Some of them:
eel
oops
ooze
cocoa
cocos
kokos
mimic
cocoon
I couldn’t manage sed to find repeated suffixes, so I wrote a Racket program to do that (each suffix must have at
least two characters):
#lang racket
( define (f s)
(regexp -match r s))
603
( define result
(sort
( filter f (file -> lines " words_alpha .txt "))
( lambda (x y)
(< (string - length x) (string - length y)))))
Some of these:
ceded
crisis
rococo
cantata
I couln’d stand the itch and tried to find all words with 3 repeated suffixes:
( define r ( pregexp "^(.+) (.+) \\2\\2 $"))
That includes both words with 3 repeated characters at the end and the rare term ’ratatat’ – thricely repeated ’at’
suffix:
brrr
ieee
mmmm
oooo
viii
xiii
xviii
xxiii
ratatat
earlesss
12, 96, 3120 , 115200 , 5836320 , 382072320 , 31488549120 , ... ( sequence A059375 in
the OEIS).
( Wikipedia. )
We can count it using Z3, but also get actual men/women allocations:
#!/ usr/bin/env python3
from z3 import *
COUPLES =3
# m m m
# w w w
# i.e., women [0] is placed between men [0] and men [1]
# the last women [COUPLES -1] is between men[COUPLES -1] and men [0] ( wrapping )
s= Solver ()
s.add( Distinct (men))
s.add( Distinct ( women ))
# a pair , each woman belong to , cannot be the same as men 's located at left and right
.
# "% COUPLES " is wrapping , so that the last woman is between the last man and the
first man.
for i in range ( COUPLES ):
s.add(And(women [i]!= men[i], women [i]!= men [(i+1) % COUPLES ]))
results =[]
( https://smt.st/current_tree/other/menage/menage.py )
For 3 couples:
men 0 2 1
women 1 0 2
men 1 2 0
women 0 1 2
men 0 1 2
women 2 0 1
men 2 1 0
women 0 2 1
men 2 0 1
women 1 2 0
men 1 0 2
women 2 1 0
results total= 6
however , according to https :// oeis.org/ A059375 : 12
We are getting “half” of results because men and women can be then swapped (their sex swapped (or reassigned))
and you’ve got another 6 results. 6+6=12 in total. This is kind of symmetry.
For 4 couples:
...
men 3 0 2 1
women 1 3 0 2
men 3 0 1 2
women 2 3 0 1
men 1 0 2 3
women 3 1 0 2
men 2 0 1 3
women 3 2 0 1
results total= 48
however , according to https :// oeis.org/ A059375 : 96
For 5 couples:
...
men 0 4 1 2 3
women 1 3 0 4 2
men 0 3 1 2 4
women 1 4 0 3 2
606
men 0 3 1 2 4
women 1 0 4 3 2
men 4 3 1 0 2
women 0 2 4 1 3
Figure 22.1
Each arrow shows that an item is needed by an item arrow pointing to, i.e., if “a -> b”, then item “a” must be first
processed, because “b” needs it, or “b” depends on “a”.
How Mathematica would “sort” the dependency graph?
In []:= TopologicalSort [g]
Out []= {7, 3, 0, 5, 1, 4, 6, 2}
Now I’ll use Z3 SMT-solver for topological sort, which is overkill, but quite spectacular: all we need to do is to
add constraint for each edge (or “connection”) in graph, if “a -> b”, then “a” must be less then “b”, where each variable
reflects ordering.
#!/ usr/bin/env python3
from z3 import *
TOTAL =8
s= Solver ()
s.add( Distinct ( order ))
for i in range ( TOTAL ):
s.add(And(order [i]>=0, order [i]< TOTAL ))
m=s. model ()
order_to_print =[ None ]*( TOTAL )
for i in range ( TOTAL ):
order_to_print [m[ order [i]]. as_long () ]=i
print ( order_to_print )
The lorder command is essentially obsolete. Use the following command in its place: % ar -ts file.a
The lorder command reads one or more object or library archive files, looks for external references,
and writes a list of paired filenames to standard output. The first of each pair of files contains references
to identifiers that are defined in the second file. You can send this list to the tsort command to find
an ordering of a library member file suitable for 1-pass access by ld.
( https://www.unix.com/man-page/osf1/1/lorder/ )
from z3 import *
s= Optimize ()
libA=Int('libA ')
# libA 's version is 1..5 or 999 ( which means library will not be installed ):
s.add(Or(And(libA >=1 , libA <=5) ,libA ==999) )
libB=Int('libB ')
# libB 's version is 1, 4, 5 or 999:
s.add(Or(libB ==1 , libB ==4 , libB ==5 , libB ==999) )
libC=Int('libC ')
# libC 's version is 10, 11, 14 or 999:
s.add(Or(libC ==10 , libC ==11 , libC ==14 , libC ==999) )
libD=Int('libD ')
# libD 's version is 1..10
s.add(Or(And(libD >=1 , libD <=10) ,libD ==999) )
999 means that there is no need to install libD, it’s not required by other packages.
Change version of ProgramB to v8 and it will says “unsat”, meaning, there is a conflict: ProgramA requires libA
v2, but ProgramB v8 eventually requires newer libA.
Still, there is a work to do: “unsat” message is somewhat useless to end user, some information about conflicting
items should be printed.
Here is my another optimization problem example: 14.1.
More about using SAT/SMT solvers in package managers: https://research.swtch.com/version-sat, https:
//cseweb.ucsd.edu/~lerner/papers/opium.pdf.
Now in the opposite direction: forcing aptitude package manager to solve Sudoku:
http://web.archive.org/web/20160326062818/http://algebraicthunk.net/~dburrows/blog/entry/package-managemen
Some readers may ask, how to order libraries/programs/packages to be installed? This is simpler problem, which
is often solved by topological sorting. The algorithm reorders graph in such a way so that vertices not depended on
anything will be on the top of queue. Next, there will be vertices depended on vertices from the previous layer. And
so on.
make UNIX utility does this while constructing order of items to be processed. Even more: older make utilities
offloaded the job to the external utility tsort, which is included in POSIX standard11 .
22.5.2 Toy package manager under 200 SLOC on top of SAT solver
Ubuntu Linux
Many Ubuntu Linux users see this often:
% sudo apt install katomic
Reading package lists ... Done
Building dependency tree
Reading state information ... Done
11 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tsort.html
610
The following packages were automatically installed and are no longer required :
a2jmidid libsamplerate0 :i386 pulseaudio -module -jack python3 -cffi python3 -jack -
client python3 -ply python3 - pycparser qasmixer
qastools - common zita - ajbridge
Use 'sudo apt autoremove ' to remove them.
The following additional packages will be installed :
kdoctools5 khelpcenter libgrantlee - templates5 libkf5kdegames -data libkf5kdegames7
libkf5newstuff -data libkf5newstuff5
libkf5newstuffcore5 qml -module -org -kde - newstuff
The following NEW packages will be installed :
katomic kdoctools5 khelpcenter libgrantlee - templates5 libkf5kdegames -data
libkf5kdegames7 libkf5newstuff -data libkf5newstuff5
libkf5newstuffcore5 qml -module -org -kde - newstuff
0 upgraded , 10 newly installed , 0 to remove and 20 not upgraded .
Need to get 7 ,175 kB of archives .
After this operation , 28.3 MB of additional disk space will be used.
Do you want to continue ? [Y/n]
During installation of a package, other packages are to be installed - libraries, optional packages, etc. Also, ’apt’
utility tries to find the most fresh packages.
How an Ubuntu package is defined?
Format : 3.0 (quilt )
Source : katomic
Binary : katomic
Architecture : any
Version : 4:20.08.0 -1
...
Build - Depends : cmake (>= 3.5∼), debhelper - compat (= 13) , extra -cmake - modules (>=
5.30.0∼), gettext , libkf5config -dev (>= 5.30.0∼), libkf5coreaddons -dev (>= 5.30.0∼
), libkf5crash -dev (>= 5.30.0∼), libkf5dbusaddons -dev (>= 5.30.0∼), libkf5doctools
-dev (>= 5.30.0∼), libkf5i18n -dev (>= 5.30.0∼), libkf5kdegames -dev (>= 4.9.0∼),
libkf5newstuff -dev (>= 5.30.0∼), libkf5widgetsaddons -dev (>= 5.30.0∼),
libkf5xmlgui -dev (>= 5.30.0∼), pkg -kde - tools (>> 0.15.15) , qtbase5 -dev (>= 5.7.0∼)
Package -List:
katomic deb games optional arch=any
...
( src )
Here you see that the ’katomic’ package requires the ’debhelper-compat’ package of exact version 13, ’pkg-kde-tools’
newer than version 0.15.15, ’gettext’ package of unspecific version, etc...
More on .dsc files file format: 1, 2, 3, 4.
Almost all Linux distributions has some kind of this system. Haskell has ’Cabal’ system example package.
This task is a good specimen of CSP, which is NP-problem, of course.
It defines a list of dependencies for each package/version. Each dependency is a list of triplets. Each triple is:
package number, minimal version allowed, maximal version allowed. 0 and 9999 are allowed for min/max versions.
And this a test100/conflicts.json file, that lists of conflicting packages:
[
[[40 , 2013 , 2017] , [68 , 2015 , 2020]] ,
[[76 , 2000 , 2010] , [32 , 2000 , 2012]] ,
[[40 , 2013 , 2017] , [71 , 2002 , 2002]] ,
[[72 , 2004 , 2007] , [15 , 2001 , 2016]] ,
[[36 , 2001 , 2007] , [21 , 2000 , 2010]] ,
[[98 , 2007 , 2010] , [14 , 2004 , 2012]] ,
...
This means, package number 80 in versions [2011..2012] cannot be installed with package 45 in versions [2005..2017].
However, other versions may be allowed.
Why JSON? To make hand editing possible.
In my previous implementation of my toy package manager in Racket, I used S-expressions, but they are less
familiar to a general audience. Another option is XML, but it’s too verbose...
First, only one version per package is allowed, so I’m using one-hot encoding and ’AtMost1’ constraint for each
package. However, a bit may be absent at all. It will leave version value in undefined state.
IMPLY constraint
Second, for each dependency, I’m adding an IMPLY constraint. If a package 1 version 2004 requires package 2
versions 2000..2002, the following constraint is to be added:
pkg -1-ver -2004 => pkg -2-ver -2000 OR pkg -2-ver -2001 OR pkg -2-ver -2002
Handling conflicts
For example, package 1 versions [2000..2001] cannot be installed simultaneously with package 2 versions [2005..2007].
I’ll add this constraint:
AND(pkg -1-ver -2000 OR pkg -1-ver -2001 , pkg -2-ver -2005 OR pkg -2-ver -2006 OR pkg -2-ver
-2007) = False
...or...
NOT(AND(pkg -1-ver -2000 OR pkg -1-ver -2001 , pkg -2-ver -2005 OR pkg -2-ver -2006 OR pkg -2-
ver -2007) )== True
NAND gate can also be called ”not both”. In my example, in plain English language, that means: ”pkg-1-ver-2000
OR pkg-1-ver-2001 cannot be true if pkg-2-ver-2005 OR pkg-2-ver-2006 OR pkg-2-ver-2007 is also true and vice versa”.
In other words, these two OR expressions cannot be both true, but can be both false.
Getting solution
You’ll get a correct solution, but other package/versions may also be turned on. Because you can’t ’ground’ them
to false by default. ’Unconnected packages’ are ’dangling’ chaotically each time you run SAT solver.
My idea is to collect information from solution recursively, starting at the initial list of desired packages supplied
by an end-user. See the collect_from_solution_recursively() function.
Test
Let’s take a test data from ’test100’ folder and run it:
% python3 v1.py test100 0 1 5 10 50
get_first_solution
initial_pkgs : [0, 1, 5, 10, 50]
going to run solver
SAT
first solution :
0:2018; 1:2020; 2:2019; 3:2019; 4:2020; 5:2015; 6:2016; 7:2020; 8:2014; 10:2015;
11:2018; 12:2019; 13:2020; 14:2012; 15:2020; 16:2017; 17:2019; 18:2020; 19:2018;
21:2020; 22:2000; 23:2001; 28:2001; 29:2020; 34:2020; 35:2014; 36:2015; 50:2014;
The solution is correct, but versions are ’random’ (in allowed limits), not maximized.
# the code is a bit Lispy , because it was rewritten from Racket ...
path=sys.argv [1]
# add conflicts
for conflict in conflicts :
c1 , c2 = conflict [0] , conflict [1]
vars1 = version_range_to_list_of_vars (c1 [0] , c1 [1] , c1 [2])
vars2 = version_range_to_list_of_vars (c2 [0] , c2 [1] , c2 [2])
s.fix(s.NAND(s. OR_list (vars1 ), s. OR_list ( vars2 )), True) # not both
Circular dependencies
As a by-product, my toy package manager can support circular dependencies.
Open this example (deps.json) in the test_circ folder:
{
"0":
{
"2016" : [[2 ,2005 ,2015]]
},
"1":
{
"2018" : [[0 ,2001 ,2016]]
},
"2":
{
"2015" : [[1 ,2006 ,2018]]
}
}
Indeed, there are 3 implications: 0 =⇒ 1; 1 =⇒ 2; 2 =⇒ 0. By the rules of boolean logic, that means, all 3
boolean variables would be simultaneously false or true.
I’m not sure if they are handled in a real package manager like Ubuntu’s ’apt’. But this feature is possible.
Example trace:
% python3 v2.py test100 5
get_first_solution
initial_pkgs : [5]
going to run solver
SAT
first solution :
0:2018; 5:2006;
trying version 2015 for package 5
SAT
solution : 0:2018; 1:2020; 4:2020; 5:2015;
trying version 2020 for package 4
fixed_versions : {5: 2015}
SAT
solution : 0:2018; 1:2020; 4:2020; 5:2015;
trying version 2020 for package 1
fixed_versions : {5: 2015 , 4: 2020}
SAT
solution : 0:2018; 1:2020; 4:2020; 5:2015;
trying version 2018 for package 0
fixed_versions : {5: 2015 , 4: 2020 , 1: 2020}
SAT
solution : 0:2018; 1:2020; 4:2020; 5:2015;
final solution : 0:2018; 1:2020; 4:2020; 5:2015;
Now here I’m trying to ’install’ package 4 and 11. My toy package manager tries to raise versions of all packages,
including dependent ones:
% python3 v2.py test100 4 11
get_first_solution
initial_pkgs : [4, 11]
going to run solver
SAT
first solution :
4:2020; 11:2007;
trying version 2018 for package 11
SAT
solution : 0:2018; 1:2006; 4:2020; 5:2015; 11:2018;
trying version 2015 for package 5
fixed_versions : {11: 2018}
SAT
solution : 0:2018; 1:2006; 4:2020; 5:2015; 11:2018;
trying version 2020 for package 4
fixed_versions : {11: 2018 , 5: 2015}
SAT
solution : 0:2018; 1:2006; 4:2020; 5:2015; 11:2018;
trying version 2020 for package 1
fixed_versions : {11: 2018 , 5: 2015 , 4: 2020}
SAT
solution : 0:2018; 1:2020; 4:2020; 5:2015; 11:2018;
trying version 2018 for package 0
fixed_versions : {11: 2018 , 5: 2015 , 4: 2020 , 1: 2020}
SAT
solution : 0:2018; 1:2020; 4:2020; 5:2015; 11:2018;
final solution : 0:2018; 1:2020; 4:2020; 5:2015; 11:2018;
By the way, it’s important to note that the first solution can coincide with the ’best’ solution. But may not.
616
Listing 22.3: The source code
#!/ usr/bin/env python3
# the code is a bit Lispy , because it was rewritten from Racket ...
path=sys.argv [1]
# add conflicts
for conflict in conflicts :
c1 , c2 = conflict [0] , conflict [1]
vars1 = version_range_to_list_of_vars (c1 [0] , c1 [1] , c1 [2])
vars2 = version_range_to_list_of_vars (c2 [0] , c2 [1] , c2 [2])
s.fix(s.NAND(s. OR_list (vars1 ), s. OR_list ( vars2 )), True) # not both
fixed_versions ={}
res=s.solve ()
if res == False :
print ("UNSAT ")
continue
else:
# new solution
solution ={}
[ collect_from_solution_recursively (s, vars , solution , p) for p in
packages_to_install ]
print ("SAT")
t="; ".join ([ str(s)+":"+str( solution [s]) for s in sorted ( solution .keys ())
])+"; "
print (" solution : "+t)
# we stop here
found =True
fixed_versions [pkg ]=v
s.fix(SAT_var , True) # fix this package / version until the end
return solution
618
assert found
while True:
# find a package that present in solution , but not in fixed_versions
# (by computing set difference )
in_solution_but_not_in_fixed_versions =set( solution .keys ()) - set( fixed_versions .
keys ())
# if we can 't find one , finish
if len( in_solution_but_not_in_fixed_versions )==0:
s="; ".join ([ str(s)+":"+str( solution [s]) for s in sorted ( solution .keys ())])+"
; "
print (" final solution : "+s)
exit (0)
else:
# found
# pick a package with highest number
# (we start enumerating all versions at the highest package number )
pkg=max( in_solution_but_not_in_fixed_versions )
solution = find_max_version_for_package (solution , s, vars , pkg , fixed_versions )
SAT assumptions
I’m using SAT assumptions. What is this?
Imagine you play Doom/Quake/another shooter videogame. You gone far and you stumbled before the fork in the
road. You know you may get killed by a monster. So you save the state of the game, you take a first path, you get
killed by a monster, you restore a state, you take a second path, etc, etc. You want to save the state because you
don’t want to lose what you’ve achieved up to this point.
It takes a time for SAT solver to process input clauses, to ’warm up’, like an engine.
Here I’m using SAT assumption after all the constraints processed, and I ’assume’ that a package X has version
Y. I execute sat() call of picosat, it returns SAT or UNSAT, and all ’assumptions’ are reset after each sat() call, but
the picosat SAT solver is still ’warmed up’, and all constraints are still there. And then I ’assume’ that a package X
has version Y-1, and execute sat() call again, etc, etc.
It’s impossible to add assumptions running picosat executable, so I have to load the libpicosat.so shared library
and call all these functions one after another.
It’s important to note that I could run picosat executable instead, without any assumptions, but then the whole
process would be longer: because you’ll have to generate CNF file each time, picosat SAT solver have to process all
the clauses from the beginning, ’warm its engine’, etc.
And when PicoMUS utility returns list of clauses, we get a list of packages from our records, that are connected
to these clauses:
print (" running picomus ")
MUS_clauses , MUS_vars =s. get_MUS_vars ()
...
IMPLY_clauses_packages_out =set ()
...
for c in MUS_clauses :
...
if c in IMPLY_clauses_packages .keys ():
IMPLY_clauses_packages_out . update ( IMPLY_clauses_packages [c])
...
print (" conflicted packages (IMPLY ):", sorted (list( IMPLY_clauses_packages_out
)))
We also keep tabs on clauses added when fixing packages user wants to install and clauses generated during
conflict.json file processing.
Now a small example.
Now you see that packages 1 and 2 cannot be installed with each other.
Another example (test6). Package 1 requires package 0 of version 2014 (only this version) and package 2 requires
package 0 version 2011 only:
{
"0":
{
"2011" : [],
"2014" : []
},
"1":
{
"2018" : [[0 ,2014 ,2014]]
},
"2":
{
"2015" : [[0 ,2011 ,2011]]
}
}
Unfortunately, PicoMUS is slow and usable only for debugging. It may take minutes to find a MUS.
On my Intel Core 2 Duo CPU clocked at 2.13GHz:
python3 v3.py test200 187 102.24 s user 0.15s system 99% cpu 1:43.05 total
python3 v3.py test500 492 101.44 s user 0.17s system 99% cpu 1:41.92 total
python3 v3.py test1000 980 38.11 s user 0.17s system 99% cpu 38.602 total
621
It’s unlikely, an end user would wait this long. But my tests are much more complex than real-world cases, such
as Ubuntu packages, etc. So it can be usable.
# the code is a bit Lispy , because it was rewritten from Racket ...
path=sys.argv [1]
if get_MUS :
s= SAT_lib . SAT_lib ( SAT_solver =" picomus ")
else:
s= SAT_lib . SAT_lib ( SAT_solver =" libpicosat ")
if get_MUS :
fix_clauses_packages = defaultdict (set)
IMPLY_clauses_packages = defaultdict (set)
conflicts_clauses_packages = defaultdict (set)
# add conflicts
for conflict in conflicts :
if get_MUS :
clause_start =s. CNF_next_idx +1
c1 , c2 = conflict [0] , conflict [1]
vars1 = version_range_to_list_of_vars (c1 [0] , c1 [1] , c1 [2])
vars2 = version_range_to_list_of_vars (c2 [0] , c2 [1] , c2 [2])
s.fix(s.NAND(s. OR_list (vars1 ), s. OR_list ( vars2 )), True) # not both
if get_MUS :
clause_stop =s. CNF_next_idx -1+1
# print (" conflict between ", c1 , c2 , " clauses =[" , clause_start ,
clause_stop , "]")
for c in range ( clause_start , clause_stop +1):
conflicts_clauses_packages [c]. add(c1 [0])
conflicts_clauses_packages [c]. add(c2 [0])
if get_MUS :
print (" running picomus ")
MUS_clauses , MUS_vars =s. get_MUS_vars ()
fix_clauses_packages_out =set ()
IMPLY_clauses_packages_out =set ()
conflicts_clauses_packages_out =set ()
for c in MUS_clauses :
if c in fix_clauses_packages .keys ():
fix_clauses_packages_out . update ( fix_clauses_packages [c])
if c in IMPLY_clauses_packages .keys ():
IMPLY_clauses_packages_out . update ( IMPLY_clauses_packages [c])
if c in conflicts_clauses_packages .keys ():
conflicts_clauses_packages_out . update ( conflicts_clauses_packages [c])
623
print (" conflicted packages (set by user):", sorted (list(
fix_clauses_packages_out )))
print (" conflicted packages (IMPLY ):", sorted (list( IMPLY_clauses_packages_out
)))
print (" conflicted packages (from conflicts .json):", sorted (list(
conflicts_clauses_packages_out )))
exit (0)
fixed_versions ={}
res=s.solve ()
if res == False :
print ("UNSAT ")
continue
else:
# new solution
solution ={}
[ collect_from_solution_recursively (s, vars , solution , p) for p in
packages_to_install ]
print ("SAT")
t="; ".join ([ str(s)+":"+str( solution [s]) for s in sorted ( solution .keys ())
])+"; "
print (" solution : "+t)
# we stop here
found =True
fixed_versions [pkg ]=v
s.fix(SAT_var , True) # fix this package / version until the end
return solution
624
assert found
while True:
# find a package that present in solution , but not in fixed_versions
# (by computing set difference )
in_solution_but_not_in_fixed_versions =set( solution .keys ()) - set( fixed_versions .
keys ())
# if we can 't find one , finish
if len( in_solution_but_not_in_fixed_versions )==0:
s="; ".join ([ str(s)+":"+str( solution [s]) for s in sorted ( solution .keys ())])+"
; "
print (" final solution : "+s)
exit (0)
else:
# found
# pick a package with highest number
# (we start enumerating all versions at the highest package number )
pkg=max( in_solution_but_not_in_fixed_versions )
solution = find_max_version_for_package (solution , s, vars , pkg , fixed_versions )
Future work
’Remove’ operation.
Further reading
Russ Cox: 1, 2.
Several attempts exists on adding SAT solvers to package managers in major *NIX distributions:
Fedora-specific: 1, 2.
Debian-specific: 1, 2.
SUSE-specific: 1, 2, 3, 4, 5.
FreeBSD: 1. Has picosat SAT solver: 1.
More comments: Fedora, OpenSUSE, Windows 7, PubGrub and Dart, PHP, Anaconda.
A paper: OPIUM: Optimal Package Install/Uninstall Manager.
The Internet infrastructure company Akamai, cofounded by Tom Leighton, also uses a variation of
the Mating Ritual to assign web traffic to its servers.
In the early days, Akamai used other combinatorial optimization algorithms that got to be too
slow as the number of servers (over 65,000 in 2010) and requests (over 800 billion per day) increased.
Akamai switched to a Ritual-like approach, since a Ritual is fast and can be run in a distributed
manner. In this case, web requests correspond to women and web servers correspond to men. The
web requests have preferences based on latency and packet loss, and the web servers have preferences
based on cost of bandwidth and co-location.
( Eric Lehman, F Thomson Leighton, Albert R Meyer - Mathematics for Computer Science )
My solution is much less efficient, because much simpler/better algorithm exists (Gale/Shapley algorithm), but I
did it to demonstrate the essence of the problem plus as a yet another SMT-solvers and Z3 demonstration.
See comments:
625
from z3 import *
SIZE =10
# names and preferences has been copypasted from https :// rosettacode .org/wiki/
Stable_marriage_problem
# males :
abe , bob , col , dan , ed , fred , gav , hal , ian , jon = 0,1,2,3,4,5,6,7,8,9
MenStr =["abe", "bob", "col", "dan", "ed", "fred", "gav", "hal", "ian", "jon"]
# females :
abi , bea , cath , dee , eve , fay , gay , hope , ivy , jan = 0,1,2,3,4,5,6,7,8,9
WomenStr =["abi", "bea", "cath", "dee", "eve", "fay", "gay", "hope", "ivy", "jan"]
s= Solver ()
# " inverted index ", make sure all men and women are " connected " to each other , i.e.,
form pairs .
# FIXME : only work for SIZE =10
for i in range (SIZE):
s.add( WomanChoice [i]==
If( ManChoice [0]==i, 0,
626
If( ManChoice [1]==i, 1,
If( ManChoice [2]==i, 2,
If( ManChoice [3]==i, 3,
If( ManChoice [4]==i, 4,
If( ManChoice [5]==i, 5,
If( ManChoice [6]==i, 6,
If( ManChoice [7]==i, 7,
If( ManChoice [8]==i, 8,
If( ManChoice [9]==i, 9, -1)))))))))))
# this is like ManChoice [] value , but " inverted index ". it reflects wife 's rating in
man 's own rating system .
# 0 if he married best women , 1 if there is 1 women who he would prefer (if there is
a chance ):
ManChoiceInOwnRating =[ Int('ManChoiceInOwnRating_ %d' % i) for i in range (SIZE)]
# same for all women :
WomanChoiceInOwnRating =[ Int('WomanChoiceInOwnRating_ %d' % i) for i in range (SIZE)]
# set values in " inverted " indices according to values in ManPrefer []/ WomenPrefer [].
# FIXME : only work for SIZE =10
for m in range (SIZE):
s.add ( ManChoiceInOwnRating [m]==
If( ManChoice [m]== ManPrefer [m][0] ,0 ,
If( ManChoice [m]== ManPrefer [m][1] ,1 ,
If( ManChoice [m]== ManPrefer [m][2] ,2 ,
If( ManChoice [m]== ManPrefer [m][3] ,3 ,
If( ManChoice [m]== ManPrefer [m][4] ,4 ,
If( ManChoice [m]== ManPrefer [m][5] ,5 ,
If( ManChoice [m]== ManPrefer [m][6] ,6 ,
If( ManChoice [m]== ManPrefer [m][7] ,7 ,
If( ManChoice [m]== ManPrefer [m][8] ,8 ,
If( ManChoice [m]== ManPrefer [m][9] ,9 , -1)))))))))))
# this is 2D bool array . "true" if a ( married or already connected ) man would prefer
another women over his wife.
ManWouldPrefer =[[ Bool('ManWouldPrefer_ %d_%d' % (m, w)) for w in range (SIZE)] for m in
range (SIZE)]
# same for all women :
WomanWouldPrefer =[[ Bool('WomanWouldPrefer_ %d_%d' % (w, m)) for m in range (SIZE)] for
w in range (SIZE)]
# set "true" in ManWouldPrefer [][] table for all women who are better than the wife a
man currently has.
# all others can be " false "
# if the man married best women , all entries would be " false "
for m in range (SIZE):
627
for w in range (SIZE):
s.add( ManWouldPrefer [m][w] == ( ManPrefer [m]. index (w) < ManChoiceInOwnRating [m
]))
print ("")
print ("")
ManChoice :
abe <-> ivy
bob <-> cath
col <-> dee
dan <-> fay
ed <-> jan
fred <-> bea
gav <-> gay
hal <-> eve
ian <-> hope
jon <-> abi
WomanChoice :
abi <-> jon
bea <-> fred
cath <-> bob
dee <-> col
eve <-> hal
fay <-> dan
gay <-> gav
628
hope <-> ian
ivy <-> abe
jan <-> ed
This is what we did in plain English language. “Connect men and women somehow, we don’t care how. But no
pair must exist of those who prefer each other (simultaneously) over their current spouses”. Gale/Shapley algorithm
uses “steps” to “stabilize” marriage. There are no “steps”, all pairs are married couples already.
Another important thing to notice: only one solution must exist.
...
results =[]
...
...
...
Figure 22.2
630
Now the classic: 12 pentominos and ”mutilated” chess board, several solutions:
Figure 22.3
631
Figure 22.4
(a) A guide
(b) 3 cardboards
Now the question: can we put all the parts needed on smaller plates? To save some cardboard material?
634
I digitized all parts using usual notebook:
I don’t know a real size of a square in notebook, probably, 5mm. I would call it one [square] unit.
Then I took the same piece of Python code I used before: 22.7.2.
It was easy: there are just (a big) pack of Boolean variables and AMO1/ALO1 constraints, or, as I called them
before, POPCNT1.
Thanks to parallelized Plingeling SAT-solver, I could find a solution for a 43*34 [units] plate in 10 minutes14 :
14 4 threads on Intel Xeon CPU E3-1270 v3 @ 3.50GHz
635
Probably this is smallest plate possible. When I try smaller dimensions, SAT solver stuck. But if you wish, you
can decrease dimensions and run it again and again...
Now the question: the toy factory wants to ship all parts in several (smaller) plates. Like, 3 of them. Because one
plate is impractical for shipping, handling, etc.
To put all parts on 3 plates, I can just add 2 borders between them:
board =["*"* BOARD_SMALL_WIDTH + " " + "*"* BOARD_SMALL_WIDTH + " " + "*"*
BOARD_SMALL_WIDTH ]* BOARD_SMALL_HEIGHT
This is slightly better than what was produced by the toy factory (20*30 [units], as measured by my notebook).
Now you can see there are two 2 · 1 notches at each plate:
636
I don’t know why they been cut. Probably for easier stacking at factory? Who knows. Anyway, I think I can add
this constraint to my solver. These are 3 initial plates:
board =[" ***************** ***************** ***************** ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ****************** ****************** ****************** ",
" ****************** ****************** ****************** ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* ",
" ******************* ******************* ******************* "]
637
Keep in mind, how coarse my units are ( 5mm). You can digitize better if you use a millimeter paper15 , but such
a problem would be more hard for SAT solver, of course.
What I also did: this problem required huge AMO1/ALO1 constraints (several thousands Boolean variables).
Naive quadratic encoding can’t manage this, also, CNF instances growing greatly.
I used commander encoding this time. For example, you need to add AMO1/ALO1 constraint to 100 variables.
Divide them by 10 parts. Add naive/quadratic AMO1/ALO1 for each of these 10 parts. Add OR for each parts.
Then you get 10 OR result. Each OR result is a commander, like, a commander of a squad. Join them together with
quadratic AMO1/ALO1 constraint again.
I do this recursively, so it looks like a multi-tiered tree of commanders. Also, changing these constants (5 and 10)
influences SAT solver’s performance significantly, probably, tuning is required for each type of task...
(The constants defines breadth and depth of a tree.)
# naive / pairwise / quadratic encoding
def AtMost1_pairwise (self , lst:List[str ]):
for pair in itertools . combinations (lst , r=2):
self. add_clause ([ self.neg(pair [0]) , self.neg(pair [1]) ])
( https://smt.st/current_tree/libs/SAT_lib.py )
The files: https://smt.st/current_tree/other/tiling/kangaroo.
jobs =[]
"""
# from https :// developers . google .com/ optimization / scheduling / job_shop
jobs. append ([(0 , 3) , (1, 2), (2, 2)])
jobs. append ([(0 , 2) , (2, 1), (1, 4)])
jobs. append ([(1 , 4) , (2, 3)])
machines =3
makespan =11
"""
#"""
# from http :// support .sas.com/ documentation /cdl/en/ orcpug /63973/ HTML/ default / viewer .
htm# orcpug_clp_sect048 .htm
jobs. append ([(2 , 44) , (3, 5) , (5, 58) , (4, 97) , (0, 9) , (7, 84) , (8,
77) , (9, 96) , (1, 58) , (6, 89) ])
jobs. append ([(4 , 15) , (7, 31) , (1, 87) , (8, 57) , (0, 77) , (3, 85) , (2,
81) , (5, 39) , (9, 73) , (6, 21) ])
jobs. append ([(9 , 82) , (6, 22) , (4, 10) , (3, 70) , (1, 49) , (0, 40) , (8,
34) , (2, 48) , (7, 80) , (5, 71) ])
jobs. append ([(1 , 91) , (2, 17) , (7, 62) , (5, 75) , (8, 47) , (4, 11) , (3,
7), (6, 72) , (9, 35) , (0, 55) ])
jobs. append ([(6 , 71) , (1, 90) , (3, 75) , (0, 64) , (2, 94) , (8, 15) , (4,
12) , (7, 67) , (9, 20) , (5, 50) ])
jobs. append ([(7 , 70) , (5, 93) , (8, 77) , (2, 29) , (4, 58) , (6, 93) , (3,
68) , (1, 57) , (9, 7) , (0, 52) ])
jobs. append ([(6 , 87) , (1, 63) , (4, 26) , (5, 6) , (2, 82) , (3, 27) , (7,
56) , (8, 48) , (9, 36) , (0, 95) ])
jobs. append ([(0 , 36) , (5, 15) , (8, 41) , (9, 78) , (3, 76) , (6, 84) , (4,
30) , (7, 76) , (2, 36) , (1, 8) ])
jobs. append ([(5 , 88) , (2, 81) , (3, 13) , (6, 82) , (4, 54) , (7, 13) , (8,
29) , (9, 40) , (1, 78) , (0, 75) ])
jobs. append ([(9 , 88) , (4, 54) , (6, 64) , (7, 32) , (0, 52) , (2, 6) , (8,
54) , (5, 82) , (3, 6) , (1, 26) ])
machines =10
makespan =842
639
#"""
s= Solver ()
text_result =[]
It takes 20s on my venerable Intel Xeon E3-1220 3.10GHz to solve 10*10 (10 jobs and 10 machines) problem from
sas.com: https://smt.st/current_tree/other/job_shop/r2.txt.
Further work: makespan can be decreased gradually, or maybe binary search can be used...
machines =10
-makespan =842
+# makespan =842
#"""
+ makespan = Int('makespan ')
-s= Solver ()
+s = Optimize ()
+s.add(makespan >0)
+h = s. minimize ( makespan )
+
if s.check ()== unsat :
print (" unsat ")
exit (0)
+s.lower(h)
m=s.model ()
text_result =[]
Toy-level solvers
... which has been written by me and serves as a demonstration and playground.
SAT
1 2 3 -4 -5 6 -7 -8 0
SAT
1 2 3 -4 5 -6 -7 -8 0
SAT
1 2 3 4 -5 -6 -7 -8 0
UNSAT
solutions = 70
It was also tested on my SAT-based Minesweeper cracker (3.10.2), and finishes in reasonable time (though, slower
than MiniSat by a factor of ~10).
On bigger CNF instances, it gets stuck, though.
The source code:
#!/ usr/bin/env python3
count_solutions =True
# count_solutions =False
import sys
1 Davis–Putnam–Logemann–Loveland
2 https://smt.st/current_tree/solvers/backtrack/POPCNT4.cnf
642
643
def read_DIMACS (fname ):
content = read_text_file ( fname )
solutions =0
As you can see, all it does is enumerate all possible solutions, but prunes search tree as early as possible. This is
backtracking.
Worst time of execution is EXPTIME.
The files: https://smt.st/current_tree/solvers/backtrack.
Some comments: https://www.reddit.com/r/compsci/comments/6jn3th/simplest_sat_solver_in_120_lines/.
645
23.2 SAT solvers with watched literals/lists
These are couple of my remakes of Donald Knuth’s SAT0W SAT solver3 , CWEB file on his website which is very
basic and only serves as a demonstration of watch lists. Read more about it in TAOCP 7.2.2.226.2 (algorithm B, pp.
30-31).
In short:
• A variable is what you see in the DIMACS CNF file. A number. Can be positive or negative. 1234 and −1234
are the same variable.
• Literal is a variable plus sign. 1234 and −1234 are two different literals for the same variable. A clause consists
of literals. A CNF instance consists of clauses.
• We need to find an assignment for all SAT variables so that all clauses in CNF would be satisfied.
• We don’t need to take into account all variables in each clause. Only one literal in each clause must be true to
satisfy the whole CNF instance — keep in mind that fact.
• A literal of our current focus in each clause is called watched literal or watchee. Each watchee is connected to a
literal in a database, forming a watch list. Default watchee is a first literal of a clause.
• Assignment is a goal of a SAT solver: a list of false/true variables. Partial assignment is an assignment of several
variables, not all of them. During solving, each watchee is either connected to a literal in a partial assignment,
or to (yet) unassigned literal.
• When we switch a variable from false to true (or back) in a partial assignment, a watch list connected to a false
literal is to be disemboweled: all clauses in watch list are to be reconnected to other literals, either under partial
assignment, or shoved into yet unassigned literals (in other words, postponed to future). Reconnecting involves
finding another watchee to be picked from literals in a clause. If you can’t find another watchee, either switch to
another alternative for this variable (false/true) or backtrack.
• Several books and articles says that in this scheme, all clauses are always satisfied, this is like invariant. In my
opinion, this is not correct. All clauses connected to watch lists under partial assignment during solving are
satisfied, so far. While we can’t say this about other clauses connected to watch lists behind partial assignment:
they are to be processed in future.
• Essentially, the whole process of SAT solving in this tiny SAT solver is moving clauses from one watch list to
another.
; find a, b:
(get -all - models )
...
(model
(define -fun a () (_ BitVec 4) (_ bv9 4)) ; 0x9
(define -fun b () (_ BitVec 4) (_ bv11 4)) ; 0xb
)
Model count: 16
( https://smt.st/MK85 )
|add_Tseitin*()| functions makes logic gates in CNF form: https://en.wikipedia.org/wiki/Tseytin_transformation.
Then I connect logic gates to make full-adder.
Then I connect full-adders to create a n-bit adder:
void generate_adder ( struct variable * a, struct variable * b, struct variable *carry_in
, // inputs
struct variable ** sum , struct variable ** carry_out ) // outputs
{
...
// the first full - adder could be half -adder , but we make things simple here
for (int i=0; i<a->width ; i++)
{
* carry_out = create_internal_variable (" adder_carry ", TY_BOOL , 1);
add_FA (a-> var_no +i, b-> var_no +i, carry , (* sum)->var_no +i, (* carry_out
)->var_no );
// newly created carry_out is a carry_in for the next full - adder :
carry =(* carry_out )->var_no ;
};
};
( https://smt.st/MK85 )
Let’s take a look on output CNF file:
p cnf 40 114
c always false
-1 0
c always true
2 0
c generate_adder
c add_FA inputs =3, 7, cin =1, s=11 , cout =15
c add_Tseitin_XOR 16=3^7
-3 -7 -16 0
3 7 -16 0
3 -7 16 0
-3 7 16 0
c add_Tseitin_XOR 11=16^1
-16 -1 -11 0
16 1 -11 0
16 -1 11 0
-16 1 11 0
648
c add_Tseitin_AND 17=16&1
-16 -1 17 0
16 -17 0
1 -17 0
c add_Tseitin_AND 18=3&7
-3 -7 18 0
3 -18 0
7 -18 0
c add_Tseitin_OR2 15=17|18
17 18 -15 0
-17 15 0
-18 15 0
c add_FA inputs =4, 8, cin =15 , s=12 , cout =19
c add_Tseitin_XOR 20=4^8
-4 -8 -20 0
4 8 -20 0
4 -8 20 0
-4 8 20 0
c add_Tseitin_XOR 12=20^15
-20 -15 -12 0
20 15 -12 0
20 -15 12 0
-20 15 12 0
c add_Tseitin_AND 21=20&15
-20 -15 21 0
20 -21 0
15 -21 0
c add_Tseitin_AND 22=4&8
-4 -8 22 0
4 -22 0
8 -22 0
c add_Tseitin_OR2 19=21|22
21 22 -19 0
-21 19 0
-22 19 0
c add_FA inputs =5, 9, cin =19 , s=13 , cout =23
c add_Tseitin_XOR 24=5^9
-5 -9 -24 0
5 9 -24 0
5 -9 24 0
-5 9 24 0
c add_Tseitin_XOR 13=24^19
-24 -19 -13 0
24 19 -13 0
24 -19 13 0
-24 19 13 0
c add_Tseitin_AND 25=24&19
-24 -19 25 0
24 -25 0
19 -25 0
c add_Tseitin_AND 26=5&9
-5 -9 26 0
5 -26 0
9 -26 0
c add_Tseitin_OR2 23=25|26
25 26 -23 0
-25 23 0
-26 23 0
c add_FA inputs =6, 10, cin =23 , s=14 , cout =27
c add_Tseitin_XOR 28=6^10
-6 -10 -28 0
649
6 10 -28 0
6 -10 28 0
-6 10 28 0
c add_Tseitin_XOR 14=28^23
-28 -23 -14 0
28 23 -14 0
28 -23 14 0
-28 23 14 0
c add_Tseitin_AND 29=28&23
-28 -23 29 0
28 -29 0
23 -29 0
c add_Tseitin_AND 30=6&10
-6 -10 30 0
6 -30 0
10 -30 0
c add_Tseitin_OR2 27=29|30
29 30 -27 0
-29 27 0
-30 27 0
c generate_const (val =4, width =4). var_no =[31..34]
-31 0
-32 0
33 0
-34 0
c generate_EQ for two bitvectors , v1 =[11...14] , v2 =[31...34]
c generate_BVXOR v1 =[11...14] v2 =[31...34]
c add_Tseitin_XOR 35=11^31
-11 -31 -35 0
11 31 -35 0
11 -31 35 0
-11 31 35 0
c add_Tseitin_XOR 36=12^32
-12 -32 -36 0
12 32 -36 0
12 -32 36 0
-12 32 36 0
c add_Tseitin_XOR 37=13^33
-13 -33 -37 0
13 33 -37 0
13 -33 37 0
-13 33 37 0
c add_Tseitin_XOR 38=14^34
-14 -34 -38 0
14 34 -38 0
14 -34 38 0
-14 34 38 0
c generate_OR_list (var =35 , width =4) var out =39
c add_Tseitin_OR_list (var =35 , width =4, var_out =39)
35 36 37 38 -39 0
-35 39 0
-36 39 0
-37 39 0
-38 39 0
c generate_NOT id= internal !8 var =39 , out id= internal !9 out var =40
-40 -39 0
40 39 0
c create_assert () id= internal !9 var =40
40 0
c always false
c always true
c generate_adder
c add_FA inputs =3, 7, cin =1, s=11 , cout =15
c add_Tseitin_XOR 16=3^7
c add_Tseitin_XOR 11=16^1
c add_Tseitin_AND 17=16&1
c add_Tseitin_AND 18=3&7
c add_Tseitin_OR2 15=17|18
c add_FA inputs =4, 8, cin =15 , s=12 , cout =19
c add_Tseitin_XOR 20=4^8
c add_Tseitin_XOR 12=20^15
c add_Tseitin_AND 21=20&15
c add_Tseitin_AND 22=4&8
c add_Tseitin_OR2 19=21|22
c add_FA inputs =5, 9, cin =19 , s=13 , cout =23
c add_Tseitin_XOR 24=5^9
c add_Tseitin_XOR 13=24^19
c add_Tseitin_AND 25=24&19
c add_Tseitin_AND 26=5&9
c add_Tseitin_OR2 23=25|26
c add_FA inputs =6, 10, cin =23 , s=14 , cout =27
c add_Tseitin_XOR 28=6^10
c add_Tseitin_XOR 14=28^23
c add_Tseitin_AND 29=28&23
c add_Tseitin_AND 30=6&10
c add_Tseitin_OR2 27=29|30
c generate_const (val =4, width =4). var_no =[31..34]
c generate_EQ for two bitvectors , v1 =[11...14] , v2 =[31...34]
c generate_BVXOR v1 =[11...14] v2 =[31...34]
c add_Tseitin_XOR 35=11^31
c add_Tseitin_XOR 36=12^32
c add_Tseitin_XOR 37=13^33
c add_Tseitin_XOR 38=14^34
c generate_OR_list (var =35 , width =4) var out =39
c add_Tseitin_OR_list (var =35 , width =4, var_out =39)
c generate_NOT id= internal !8 var =39 , out id= internal !9 out var =40
c create_assert () id= internal !9 var =40
I make these functions add variable numbers to comments. And you can see how all the signals are routed inside
each full-adder.
|generate_EQ()| function makes two bitvectors equal by XOR-ing two bitvectors. Resulting bitvector is then
OR-ed, and result must be zero.
Again, this SAT instance is small enough to be handled by my simple SAT backtracking solver:
SAT
-1 2 -3 -4 -5 -6 -7 -8 9 -10 -11 -12 13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 24
-25 -26 -27 -28 -29 -30 -31 -32 33 -34 -35 -36
-37 -38 -39 40 0
SAT
-1 2 -3 -4 -5 6 -7 -8 9 10 -11 -12 13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 24 -25
-26 27 -28 -29 30 -31 -32 33 -34 -35 -36 -37
-38 -39 40 0
...
SAT
-1 2 3 4 5 -6 7 -8 9 10 -11 -12 13 -14 15 -16 -17 18 19 20 21 -22 23 -24 -25 26 27 28
29 -30 -31 -32 33 -34 -35 -36 -37 -38 -39 40 0
SAT
-1 2 3 4 5 6 7 -8 9 -10 -11 -12 13 -14 15 -16 -17 18 19 20 21 -22 23 -24 -25 26 27 28
651
29 -30 -31 -32 33 -34 -35 -36 -37 -38 -39 40 0
UNSAT
solutions = 16
; must be 21
; see also: https :// www. wolframalpha .com/ input /?i=GCD [861 ,3969 ,840]
( assert (= ( bvmul ((_ zero_extend 16) x) ((_ zero_extend 16) GCD)) (_ bv861 32)))
( assert (= ( bvmul ((_ zero_extend 16) y) ((_ zero_extend 16) GCD)) (_ bv3969 32)))
( assert (= ( bvmul ((_ zero_extend 16) z) ((_ zero_extend 16) GCD)) (_ bv840 32)))
( maximize GCD)
(check -sat)
(get - model )
; correct result :
;( model
; (define -fun x () (_ BitVec 16) (_ bv41 16)) ; 0x29
; (define -fun y () (_ BitVec 16) (_ bv189 16)) ; 0xbd
; (define -fun z () (_ BitVec 16) (_ bv40 16)) ; 0x28
; (define -fun GCD () (_ BitVec 16) (_ bv21 16)) ; 0x15
;)
We are going to find such an assignment, for which GCD variable will be as big as possible (that would not break
hard constraints, of course).
Whenever my MK85 encounters minimize/maximize command, the following function is called:
void create_min_max ( struct expr* e, bool min_max )
{
...
...
};
( https://smt.st/MK85 )
Lowest bit of variable to maximize receives weight 1. Second bit receives weight 2. Then 4, 8, 16, etc. Hence,
MaxSAT solver, in order to maximize weights of soft clauses, would maximize the binary variable as well!
What is in the WCNF file for the GCD example?
...
return rt;
};
It can be said, the |cnt| variable would be set during SAT instance creation, but it cannot be changed during
solving.
Now let’s create a real shifter. Now for 8-bit left shifter, I’m generating the following (long) expression:
X=ITE(cnt &1, X<<1, X)
X=ITE ((cnt > >1)&1, X<<2, X)
654
X=ITE ((cnt > >2)&1, X<<4, X)
I.e., if a specific bit is set in |cnt|, shift X by that number of bits, or do nothing otherwise. ITE() is a if-then-else
gate, works for bitvectors as well.
Glueing all this together:
// direction = false for shift left
// direction =true for shift right
struct SMT_var * generate_shifter ( struct SMT_var * X, struct SMT_var * cnt ,
bool direction )
{
int w=X-> width ;
// bit vector must have width =2^x, i.e., 8, 16, 32, 64, etc
assert ( popcount64c (w)==1);
in=out;
};
Now the puzzle. a>>b must be equal to 0x12345678, while several bits in a must be reset, like (a&0xf1110100)==0.
Find a, b:
(declare -fun a () (_ BitVec 32))
(declare -fun b () (_ BitVec 32))
(check -sat)
655
(get -model )
The solution:
sat
( model
(define -fun a () (_ BitVec 32) (_ bv38177487 32)) ; 0 x2468acf
(define -fun b () (_ BitVec 32) (_ bv3 32)) ; 0x3
)
You know , like bank robbers in movies rotating a wheel on a safe (I don 't know how it
's called correctly )
and find all digits consecutively .
This is like brute - fource .
We do here the same: we try 0/1 for each bit of index value .
We start at 1, and if the array value at this index is too large , we clear the bit to
0 and proceed to the next ( lower ) bit.
The array here has 32 numbers . The array index has 5 bits ( log2 (32) ==5 ).
And you can clearly see that one need only 5 steps to find a value in an array of
sorted numbers .
The result :
==========================================
testing idx =0 x10 or 16
bit 4 is incorrect , it 's 0, so we clear it
testing idx =0 x8 or 8
bit 3 is correct , it 's 1
testing idx =0 xc or 12
bit 2 is correct , it 's 1
testing idx =0 xe or 14
bit 1 is incorrect , it 's 0, so we clear it
testing idx =0 xd or 13
found , idx =0 xd or 13
==========================================
*/
int main ()
{
binsearch (2722) ;
};
You wouldn’t use it, but I wrote it because it’s fits so nicely on SAT.
To solve an optimization problem, you want to find some optimum variable, that is minimized or maximized. Like
in my implementation of binary search, I can try bit by bit, but these are added as assumptions.
// poor man 's MaxSMT
// if we minimize , first try false , then true
// if we maximize , do contrariwise
bool run_poor_mans_MaxSMT ( struct ctx* ctx)
{
if (verbose >0)
printf ("%s() begin \n", __FUNCTION__ );
assert (ctx -> maxsat == true);
struct PicoSAT *p= picosat_init ();
( https://smt.st/MK85 )
For the popsicle problem (10.1):
idx =15 trying true
got UNSAT
idx =15 trying false
got SAT
idx =14 trying true
got UNSAT
idx =14 trying false
got SAT
idx =13 trying true
got UNSAT
idx =13 trying false
got SAT
idx =12 trying true
got UNSAT
idx =12 trying false
got SAT
idx =11 trying true
got UNSAT
idx =11 trying false
got SAT
idx =10 trying true
got UNSAT
idx =10 trying false
got SAT
idx =9 trying true
got UNSAT
idx =9 trying false
got SAT
idx =8 trying true
got UNSAT
idx =8 trying false
got SAT
659
idx =7 trying true
got UNSAT
idx =7 trying false
got SAT
idx =6 trying true
got UNSAT
idx =6 trying false
got SAT
idx =5 trying true
got UNSAT
idx =5 trying false
got SAT
idx =4 trying true
got UNSAT
idx =4 trying false
got SAT
idx =3 trying true
got SAT
idx =2 trying true
got SAT
idx =1 trying true
got UNSAT
idx =1 trying false
got SAT
idx =0 trying true
got SAT
...
run_poor_mans_MaxSMT () begin -> true , val =13
It works slower than if using Open-WBO, but sometimes even faster than Z3!
This is as well: 14.9.
And this is close to LEXSAT, see exercise and solution from the TAOCP, section 7.2.2.2:
Glossary (SAT)
• clause - disjunction of one or more literals. For example: var1 OR -var2 OR var3 ... - at least one literal must
be satisfied in each clause.
• CNF (conjunctive normal form) formula, conjunction of one or more clauses. Is a list of clauses, all of which
must be satisfied.
• literal/term - can be variable and -variable, these are different literals.
• pure literal - present only as x or -x. Can be eliminated at start.
1 http://archive.dimacs.rutgers.edu/Challenges/
660
Chapter 25
Glossary (SMT)
• sort - datatype.
661
Chapter 26
Further reading
1
• Julien Vanegue, Sean Heelan, Rolf Rolles – SMT Solvers for Software Security
• Armin Biere, Marijn Heule, Hans van Maaren, Toby Walsh – Handbook of Satisfiability (2009). The second
edition came out in 2021.
• Rui Reis – Practical Symbolic Execution and SATisfiability Module Theories (SMT) 101 2 .
• Daniel Kroening and Ofer Strichman – Decision Procedures – An Algorithmic Point of View 3 .
26.1 Z3-specific
4
• Z3 API in Python
5
• Z3 Strategies
• Nikolaj Bjørner – Recent Trends in SMT and Z3: An interactive taste of SMT with Z3 6 .
• Nikolaj Bjørner, Leonardo de Moura, Lev Nachmanson, Christoph Wintersteiger – Programming Z3 7 .
• Questions tagged [z3] on Stack Overflow 8 .
26.2 SAT-specific
Instead of epigraph: “There is a 350+ book from 2015 by Donald Knuth in the art of programming series on SAT. So
the nerdiness of SAT has highest blessing.”9 .
• Donald Knuth – TAOCP 7.2.2.2. Satisfiability 10 . Since the Postscript file is freely available at Donald Knuth’s
website, I converted it to PDF and put it to my website: Download here.
11
• Niklas Een, Niklas Sorensson – Translating Pseudo-Boolean Constraints into SAT .
662
663
15
• Martin Finke – Equisatisfiable SAT Encodings of Arithmetical Operations .
16
• Satisfiability / Suggested Format (1993) .
26.3 SMT-specific
• Important website: http://smtlib.cs.uiowa.edu/.
• Theories: http://smtlib.cs.uiowa.edu/logics.shtml, https://cs.nyu.edu/pipermail/smt-lib/2005/000046.
html.
• SMT-COMP mailing list: https://cs.nyu.edu/mailman/listinfo/smt-comp.
• SMT-LIB mailing list: https://cs.nyu.edu/pipermail/smt-lib/, https://groups.google.com/forum/#!
forum/smt-lib.
• SMT-LIB standard: http://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2017-07-18.pdf.
• David R. Cok – The SMT-LIBv2 Language and Tools: A Tutorial: http://smtlib.github.io/jSMTLIB/
SMTLIBTutorial.pdf.
26.3.1 Benchmarks
SAT/SMT is a young field (yet). Not much documentation exist.
So I’ve found SMT benchmark collection as an invaluable source of information. Benchmarks are typically generated
by a SMT applications, so you can get into better understanding, how SMT is used.
Using gitlab at uiowa.edu17 :
sudo apt -get install git -lfs
git clone https :// clc - gitlab .cs. uiowa .edu :2443/ mpreiner /smt -lib -non - incremental .git
cd smt -lib -non - incremental
git submodule update --init
cd ..
git clone https :// clc - gitlab .cs. uiowa .edu :2443/ mpreiner /smt -lib - incremental .git
cd smt -lib - incremental
git submodule update --init
cd ..
26.4 et cetera
Many puzzles from Martin Gardner’s books can be solved effortlessly using SAT/SMT. And it is such a fun, because
SAT/SMT solvers can help understanding all the math theory behind them.
# ifndef CONSTANT_TIME_INCREMENT
for (i = len - 1; i >= 0; i--)
if (++ ctr[i]) /* continue on overflow */
return ;
#else
u8 x, add = 1;
Some applications
• All sorts of theorem provers, including (but not limited to) Isabelle1 , HOL...
• Dafny (Microsoft Research)2 , uses Z3.
665
666
16
• Musketeer – A static analysis approach to automatic [memory] fence insertion .
17
• Averest is a framework for the specification, verification, and implementation of reactive systems .
• OpenJML – a program verification tool for Java programs that allows you to check the specifications of programs
annotated in the Java Modeling Language 18 . See also: ESC/Java.
16 https://arxiv.org/pdf/1312.1411.pdf, http://www.cprover.org/wmm/musketeer/
17 http://www.averest.org/
18 http://www.openjml.org/
19 https://arxiv.org/abs/1711.04422, https://github.com/google/souper, https://news.ycombinator.com/item?id=10463312, https:
//blog.regehr.org/archives/1252.
20 https://github.com/stp/stp/blob/master/docs/index.rst#use-cases
21 https://cvc4.github.io/third-party-applications.html
22 http://web.ist.utl.pt/nuno.lopes/pubs/alive-cacm18.pdf
23 http://www.cs.utah.edu/~regehr/papers/pldi15.pdf
24 https://llvm.org/devmtg/2013-11/slides/Lopes-SMT.pdf
25 https://codingnest.com/modern-sat-solvers-fast-neat-and-underused-part-2-of-n/, https://codingnest.com/files/thesis.
pdf
Chapter 28
Acronyms used
PL Programming Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
667
668
AST Abstract syntax tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474
CS Computer science . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
ITE If-Then-Else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
UF Uninterpreted Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82