Skip to content

Commit 96f47c3

Browse files
author
Leo Alt
committed
Fix smtchecker callback tests and add Eldarica
1 parent 0713a63 commit 96f47c3

File tree

6 files changed

+149
-33
lines changed

6 files changed

+149
-33
lines changed

smtchecker.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The function runs an SMT solver on each query and adjusts the input for
44
// another run.
55
// Returns null if no solving is requested.
6-
function handleSMTQueries (inputJSON, outputJSON, solver) {
6+
function handleSMTQueries (inputJSON, outputJSON, solverFunction, solver) {
77
const auxInputReq = outputJSON.auxiliaryInputRequested;
88
if (!auxInputReq) {
99
return null;
@@ -16,7 +16,7 @@ function handleSMTQueries (inputJSON, outputJSON, solver) {
1616

1717
const responses = {};
1818
for (const query in queries) {
19-
responses[query] = solver(queries[query]);
19+
responses[query] = solverFunction(queries[query], solver);
2020
}
2121

2222
// Note: all existing solved queries are replaced.
@@ -25,10 +25,10 @@ function handleSMTQueries (inputJSON, outputJSON, solver) {
2525
return inputJSON;
2626
}
2727

28-
function smtCallback (solver) {
28+
function smtCallback (solverFunction, solver) {
2929
return function (query) {
3030
try {
31-
const result = solver(query);
31+
const result = solverFunction(query, solver);
3232
return { contents: result };
3333
} catch (err) {
3434
return { error: err };

smtsolver.js

+26-13
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,48 @@ const execSync = require('child_process').execSync;
33
const fs = require('fs');
44
const tmp = require('tmp');
55

6+
// Timeout in ms.
67
const timeout = 10000;
78

89
const potentialSolvers = [
910
{
1011
name: 'z3',
12+
command: 'z3',
1113
params: '-smt2 rlimit=20000000 rewriter.pull_cheap_ite=true fp.spacer.q3.use_qgen=true fp.spacer.mbqi=false fp.spacer.ground_pobs=false'
1214
},
15+
{
16+
name: 'Eldarica',
17+
command: 'eld',
18+
params: '-horn -t:' + (timeout / 1000) // Eldarica takes timeout in seconds.
19+
},
1320
{
1421
name: 'cvc4',
22+
command: 'cvc4',
1523
params: '--lang=smt2 --tlimit=' + timeout
1624
}
1725
];
18-
const solvers = potentialSolvers.filter(solver => commandExistsSync(solver.name));
1926

20-
function solve (query) {
21-
if (solvers.length === 0) {
22-
throw new Error('No SMT solver available. Assertion checking will not be performed.');
27+
const solvers = potentialSolvers.filter(solver => commandExistsSync(solver.command));
28+
29+
function solve (query, solver) {
30+
if (solver === undefined) {
31+
if (solvers.length === 0) {
32+
throw new Error('No SMT solver available. Assertion checking will not be performed.');
33+
} else {
34+
solver = solvers[0];
35+
}
2336
}
2437

2538
const tmpFile = tmp.fileSync({ postfix: '.smt2' });
2639
fs.writeFileSync(tmpFile.name, query);
27-
// TODO For now only the first SMT solver found is used.
28-
// At some point a computation similar to the one done in
29-
// SMTPortfolio::check should be performed, where the results
30-
// given by different solvers are compared and an error is
31-
// reported if solvers disagree (i.e. SAT vs UNSAT).
3240
let solverOutput;
3341
try {
3442
solverOutput = execSync(
35-
solvers[0].name + ' ' + solvers[0].params + ' ' + tmpFile.name, {
36-
stdio: 'pipe'
43+
solver.command + ' ' + solver.params + ' ' + tmpFile.name, {
44+
encoding: 'utf8',
45+
maxBuffer: 1024 * 1024 * 1024,
46+
stdio: 'pipe',
47+
timeout: timeout // Enforce timeout on the process, since solvers can sometimes go arount it.
3748
}
3849
).toString();
3950
} catch (e) {
@@ -44,7 +55,9 @@ function solve (query) {
4455
if (
4556
!solverOutput.startsWith('sat') &&
4657
!solverOutput.startsWith('unsat') &&
47-
!solverOutput.startsWith('unknown')
58+
!solverOutput.startsWith('unknown') &&
59+
!solverOutput.startsWith('(error') && // Eldarica reports errors in an sexpr, for example: '(error "Failed to reconstruct array model")'
60+
!solverOutput.startsWith('error')
4861
) {
4962
throw new Error('Failed to solve SMT query. ' + e.toString());
5063
}
@@ -56,5 +69,5 @@ function solve (query) {
5669

5770
module.exports = {
5871
smtSolver: solve,
59-
availableSolvers: solvers.length
72+
availableSolvers: solvers
6073
};

solcjs

+9-5
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,15 @@ if (options.standardJson) {
131131
var output = reformatJsonIfRequested(solc.compile(input, callbacks));
132132

133133
try {
134-
var inputJSON = smtchecker.handleSMTQueries(JSON.parse(input), JSON.parse(output), smtsolver.smtSolver);
135-
if (inputJSON) {
136-
if (program.verbose)
137-
console.log('>>> Retrying compilation with SMT:\n' + toFormattedJson(inputJSON) + "\n")
138-
output = reformatJsonIfRequested(solc.compile(JSON.stringify(inputJSON), callbacks));
134+
if (smtsolver.availableSolvers.length === 0) {
135+
console.log('>>> Cannot retry compilation with SMT because there are no SMT solvers available.');
136+
} else {
137+
var inputJSON = smtchecker.handleSMTQueries(JSON.parse(input), JSON.parse(output), smtsolver.smtSolver, smtsolver.availableSolvers[0]);
138+
if (inputJSON) {
139+
if (program.verbose)
140+
console.log('>>> Retrying compilation with SMT:\n' + toFormattedJson(inputJSON) + "\n")
141+
output = reformatJsonIfRequested(solc.compile(JSON.stringify(inputJSON), callbacks));
142+
}
139143
}
140144
}
141145
catch (e) {

test/smtCheckerTests/loop.sol

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
function f(uint x) public pure {
3+
uint i = 0;
4+
while (i < x)
5+
++i;
6+
assert(i == x);
7+
}
8+
}

test/smtcallback.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ tape('SMTCheckerCallback', function (t) {
119119
{ smtSolver: test.cb }
120120
));
121121
const errors = collectErrors(output);
122-
st.ok(expectErrors(errors, test.expectations));
122+
st.ok(expectErrors(errors, test.expectations, false));
123123
}
124124
st.end();
125125
});
@@ -132,8 +132,10 @@ tape('SMTCheckerCallback', function (t) {
132132
return;
133133
}
134134

135-
if (smtsolver.availableSolvers === 0) {
136-
st.skip('No SMT solver available.');
135+
// For these tests we actually need z3/Spacer.
136+
const z3HornSolvers = smtsolver.availableSolvers.filter(solver => solver.command === 'z3');
137+
if (z3HornSolvers.length === 0) {
138+
st.skip('z3/Spacer not available.');
137139
st.end();
138140
return;
139141
}
@@ -167,9 +169,8 @@ tape('SMTCheckerCallback', function (t) {
167169
if (source.includes(option)) {
168170
const idx = source.indexOf(option);
169171
if (source.indexOf(option, idx + 1) !== -1) {
170-
st.skip('SMTEngine option given multiple times.');
171-
st.end();
172-
return;
172+
st.comment('SMTEngine option given multiple times.');
173+
continue;
173174
}
174175
const re = new RegExp(option + '(\\w+)');
175176
const m = source.match(re);
@@ -211,15 +212,23 @@ tape('SMTCheckerCallback', function (t) {
211212
// `pragma experimental SMTChecker;` was deprecated in 0.8.4
212213
if (semver.gt(solc.semver(), '0.8.3')) {
213214
const engine = test.engine !== undefined ? test.engine : 'all';
214-
settings = { modelChecker: { engine: engine } };
215+
settings = {
216+
modelChecker: {
217+
engine: engine,
218+
solvers: [
219+
'smtlib2'
220+
]
221+
}
222+
};
215223
}
216224
const output = JSON.parse(solc.compile(
217225
JSON.stringify({
218226
language: 'Solidity',
219227
sources: test.solidity,
220228
settings: settings
221229
}),
222-
{ smtSolver: smtchecker.smtCallback(smtsolver.smtSolver) }
230+
// This test needs z3 specifically.
231+
{ smtSolver: smtchecker.smtCallback(smtsolver.smtSolver, z3HornSolvers[0]) }
223232
));
224233
st.ok(output);
225234

test/smtchecker.js

+85-3
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,107 @@
11
const tape = require('tape');
2+
const semver = require('semver');
3+
const solc = require('../index.js');
24
const smtchecker = require('../smtchecker.js');
5+
const smtsolver = require('../smtsolver.js');
36

7+
const preamble = 'pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n';
8+
//
49
tape('SMTChecker', function (t) {
10+
// We use null for `solverFunction` and `solver` when calling `handleSMTQueries`
11+
// because theses tests do not call a solver.
12+
513
t.test('smoke test with no axuiliaryInputRequested', function (st) {
614
const input = {};
715
const output = {};
8-
st.equal(smtchecker.handleSMTQueries(input, output), null);
16+
st.equal(smtchecker.handleSMTQueries(input, output, null, null), null);
917
st.end();
1018
});
1119

1220
t.test('smoke test with no smtlib2queries', function (st) {
1321
const input = {};
1422
const output = { auxiliaryInputRequested: {} };
15-
st.equal(smtchecker.handleSMTQueries(input, output), null);
23+
st.equal(smtchecker.handleSMTQueries(input, output, null, null), null);
1624
st.end();
1725
});
1826

1927
t.test('smoke test with empty smtlib2queries', function (st) {
2028
const input = {};
2129
const output = { auxiliaryInputRequested: { smtlib2queries: { } } };
22-
st.equal(smtchecker.handleSMTQueries(input, output), null);
30+
st.equal(smtchecker.handleSMTQueries(input, output, null, null), null);
31+
st.end();
32+
});
33+
34+
t.test('smtCallback should return type function', (st) => {
35+
const response = smtchecker.smtCallback(() => {});
36+
st.equal(typeof response, 'function');
37+
st.end();
38+
});
39+
40+
t.test('smtCallback should error when passed parser fails', (st) => {
41+
const callback = smtchecker.smtCallback((content) => { throw new Error(content); });
42+
// eslint-disable-next-line node/no-callback-literal
43+
const response = callback('expected-error-message');
44+
45+
st.deepEqual(response, { error: new Error('expected-error-message') });
46+
st.end();
47+
});
48+
49+
t.test('smtCallback should return content when passed parser does not fail', (st) => {
50+
const callback = smtchecker.smtCallback((content) => { return content; });
51+
// eslint-disable-next-line node/no-callback-literal
52+
const response = callback('expected-content-message');
53+
54+
st.deepEqual(response, { contents: 'expected-content-message' });
55+
st.end();
56+
});
57+
});
58+
59+
tape('SMTCheckerWithSolver', function (t) {
60+
// In these tests we require z3 to actually run the solver.
61+
// This uses the SMT double run mechanism instead of the callback.
62+
63+
t.test('Simple test with axuiliaryInputRequested', function (st) {
64+
const z3 = smtsolver.availableSolvers.filter(solver => solver.command === 'z3');
65+
if (z3.length === 0) {
66+
st.skip('Test requires z3.');
67+
st.end();
68+
return;
69+
}
70+
71+
if (semver.lt(solc.semver(), '0.8.7')) {
72+
st.skip('This test requires Solidity 0.8.7 to enable all SMTChecker options.');
73+
st.end();
74+
return;
75+
}
76+
77+
const settings = {
78+
modelChecker: {
79+
engine: 'chc',
80+
solvers: ['smtlib2']
81+
}
82+
};
83+
84+
const source = { a: { content: preamble + '\ncontract C { function f(uint x) public pure { assert(x > 0); } }' } };
85+
86+
const input = {
87+
language: 'Solidity',
88+
sources: source,
89+
settings: settings
90+
};
91+
92+
const output = JSON.parse(solc.compile(JSON.stringify(input)));
93+
st.ok(output);
94+
95+
const newInput = smtchecker.handleSMTQueries(input, output, smtsolver.smtSolver, z3[0]);
96+
st.notEqual(newInput, null);
97+
98+
const newOutput = JSON.parse(solc.compile(JSON.stringify(newInput)));
99+
st.ok(newOutput);
100+
101+
const errors = newOutput.errors;
102+
st.equal(errors.length, 1);
103+
st.equal(errors[0].errorCode, '6328');
104+
23105
st.end();
24106
});
25107
});

0 commit comments

Comments
 (0)