├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── esverify.js ├── commitlint.config.js ├── examples ├── adder.js ├── arrays.js ├── cons.js ├── contradiction.js ├── counter.js ├── f.js ├── fmono.js ├── inc.js ├── map.js ├── maplen.js ├── max.js ├── msort.js ├── peano-contradiction.js ├── reverse-asc.js ├── strings.js ├── sumto.js └── twice.js ├── package-lock.json ├── package.json ├── src ├── codegen.ts ├── index.ts ├── interpreter.ts ├── javascript.ts ├── logic.ts ├── message.ts ├── model.ts ├── options.ts ├── parser.ts ├── preamble.ts ├── qi.ts ├── scopes.ts ├── smt.ts ├── util.ts ├── vcgen.ts └── verification.ts ├── tests ├── api.ts ├── arrays.ts ├── classes.ts ├── globals.ts ├── helpers.ts ├── higher-order.ts ├── interpreter.ts ├── mergesort.ts ├── mutable-state.ts ├── objects.ts ├── simple.ts ├── strings.ts ├── synthesis.ts └── transform.ts ├── tsconfig.json ├── tsconfig.module.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode 3 | build/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: '14.9' 3 | dist: xenial 4 | before_script: 5 | - wget https://github.com/Z3Prover/z3/releases/download/z3-4.6.0/z3-4.6.0-x64-ubuntu-16.04.zip -O /tmp/z3-4.6.0-x64-ubuntu-16.04.zip 6 | - unzip -d . /tmp/z3-4.6.0-x64-ubuntu-16.04.zip 7 | - export PATH=$PATH:$PWD/z3-4.6.0-x64-ubuntu-16.04/bin/ 8 | - npm prune 9 | - npm install 10 | script: 11 | - commitlint-travis 12 | - npm run lint 13 | - npm test 14 | - npm run build 15 | after_success: 16 | - npx semantic-release 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Christopher Schuster 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esverify 2 | 3 | [![Build Status](https://api.travis-ci.com/levjj/esverify.svg?branch=master)](https://travis-ci.com/levjj/esverify) 4 | [![NPM Version](https://img.shields.io/npm/v/esverify.svg)](https://www.npmjs.com/package/esverify) 5 | 6 | Program Verification for ECMAScript/JavaScript ([esverify.org](http://esverify.org/)). 7 | 8 | **Alpha: This is still a research prototype and not yet ready for production use.** 9 | 10 | ## Documentation 11 | 12 | A detailed documentation of esverify and its theoretical foundations is 13 | currently work-in-progress and will be published soon. 14 | 15 | ## Example 16 | 17 | Given a simple JavaScript `max` function, we can add pre- and post-conditions 18 | using special pseudo-calls to `requires` and `ensures` with boolean expressions. 19 | 20 | ```js 21 | function max(a, b) { 22 | requires(typeof a === "number"); 23 | requires(typeof b === "number"); 24 | ensures(res => res >= a); 25 | 26 | if (a >= b) { 27 | return a; 28 | } else { 29 | return b; 30 | } 31 | } 32 | ``` 33 | 34 | These expressions will then be statically verified with respect to the function 35 | body with an SMT solver. 36 | 37 | More examples can be found in the `tests` directory. 38 | 39 | ## Supported Features 40 | 41 | * Expressions with boolean values, strings, integer and real number arithmetic 42 | * Function pre- and post conditions as well as inline assertions and invariants 43 | * Automatically generates counter-examples for failed assertions 44 | * Runs counter-example as JavaScript code to reproduce errors in dynamic context 45 | and differentiate incorrect programs from false negatives 46 | * Mutable variables and limited temporal reasoning, e.g. `old(x) > x` 47 | * Control flow including branching, returns and while loops with manually 48 | specified invariants 49 | * Inductive reasoning with loop invariants and recursive functions 50 | * Automatic inlining of function body for external proofs of function properties 51 | (restricted to one level of inlining) 52 | * Closures 53 | * Checking of function purity 54 | * Higher-order functions 55 | * Simple proof checking using Curry-Howard correspondence 56 | * Simple immutable classes with fields, methods and class invariant (no inheritance) 57 | * Immutable JavaScript objects using string keys 58 | * Immutable arrays (no sparse arrays) 59 | * Restricted verifier preamble for global objects such as `console` and `Math` 60 | 61 | It is based on the [z3](https://github.com/Z3Prover/z3) SMT solver but avoids 62 | trigger heuristics and thereby (most) timeouts and other unpredictable results by 63 | requiring manual instantiation. Function definitions and class invariants correspond 64 | to universal quantifiers and function calls and field access act as triggers that 65 | instantiate these quantifiers in a deterministic way. 66 | 67 | ## To Do (see [GitHub issues](https://github.com/levjj/esverify/issues)) 68 | 69 | * Termination checking 70 | * Mutable objects, arrays and classes 71 | * Modules with imports and exports 72 | * Prototype and subclass inheritance 73 | * Verifier-only "ghost" variables, arguments and functions/predicates 74 | * TypeScript as input language 75 | 76 | ## Usage as Command Line Tool 77 | 78 | Simple usage without installation: 79 | 80 | ``` 81 | $ npx esverify file.js 82 | ``` 83 | 84 | Installation: 85 | 86 | ``` 87 | $ npm install -g esverify 88 | ``` 89 | 90 | Command Line Options: 91 | 92 | ``` 93 | $ esverify --help 94 | Usage: esverify [OPTIONS] FILE 95 | 96 | Options: 97 | --z3path PATH Path to local z3 executable 98 | (default path is "z3") 99 | -r, --remote Invokes z3 remotely via HTTP request 100 | --z3url URL URL to remote z3 web server 101 | --noqi Disables quantifier instantiations 102 | -t, --timeout SECS Sets timeout in seconds for z3 103 | (default timeout is 10s, 0 disables timeout) 104 | -f, --logformat FORMAT Format can be either "simple" or "colored" 105 | (default format is "colored") 106 | -q, --quiet Suppresses output 107 | -v, --verbose Prints SMT input, output and test code 108 | --logsmt PATH Path for logging SMT input in verbose mode 109 | (default path is "/tmp/vc.smt") 110 | -h, --help Prints this help text and exit 111 | --version Prints version information 112 | ``` 113 | 114 | ## Usage as Library 115 | 116 | Installation via npm: 117 | 118 | ``` 119 | $ npm install esverify --save 120 | ``` 121 | 122 | Import `verify` and invoke on source code to receive a promise of messages. 123 | 124 | ```js 125 | import { verify } from "esverify"; 126 | 127 | const opts = { }; 128 | const messages = await verify("assert(1 > 2);", opts); 129 | messages.forEach(msg => console.log(msg.status)); 130 | ``` 131 | 132 | The options and returned messages have the following structure: 133 | 134 | ```ts 135 | type opts = { 136 | filename: string, 137 | logformat: "simple" | "colored" = "colored", 138 | z3path: string = "z3", 139 | z3url: string, 140 | remote: boolean = false, 141 | quiet: true, 142 | verbose: false, 143 | logsmt: '/tmp/vc.smt' 144 | timeout: 5, 145 | qi: true 146 | } 147 | 148 | type msg = { 149 | status: "verified" | "unverified" | "timeout" | "error", 150 | loc: { file: string, start: { line: number, column: number }, 151 | end: { line: number, column: number }}, 152 | description: string 153 | } 154 | ``` 155 | 156 | ## Interactive Tools 157 | 158 | A simple [web-based editor](https://github.com/levjj/esverify-editor) 159 | is available online at [esverify.org/try](http://esverify.org/try). 160 | 161 | Additionally, there is a [Vim Plugin](https://github.com/levjj/esverify-vim) 162 | and an [Emacs Plugin](https://github.com/SohumB/flycheck-esverify) 163 | which display verification results inline. 164 | 165 | More tool support will be coming soon. 166 | 167 | ## License 168 | 169 | [MIT License](https://github.com/levjj/esverify/blob/master/LICENSE) 170 | 171 | ## Issues 172 | 173 | Please report bugs to the [GitHub Issue Tracker](https://github.com/levjj/esverify/issues). esverify is currently developed and maintained by [Christopher Schuster](https://livoris.net/). 174 | 175 | ## Acknowledgements 176 | 177 | Inspired by [Dafny](https://github.com/Microsoft/dafny) and 178 | [LiquidHaskell](https://github.com/ucsd-progsys/liquidhaskell). 179 | 180 | This project is developed by the 181 | [Software and Languages Research Group at University of California, Santa Cruz](http://slang.soe.ucsc.edu/). 182 | Thanks also to Tommy, Sohum and Cormac for support and advice. 183 | -------------------------------------------------------------------------------- /bin/esverify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var minimist = require('minimist2'); 5 | var esverify = require('../build/main/index.js'); 6 | 7 | function error (msg) { 8 | console.error(msg); 9 | process.exit(1); 10 | } 11 | 12 | function usage (err) { 13 | console.log('Usage: esverify [OPTIONS] FILE\n'); 14 | console.log('Options:'); 15 | console.log(' --z3path PATH Path to local z3 executable'); 16 | console.log(' (default path is "z3")'); 17 | console.log(' -r, --remote Invokes z3 remotely via HTTP request'); 18 | console.log(' --z3url URL URL to remote z3 web server'); 19 | console.log(' --noqi Disables quantifier instantiations'); 20 | console.log(' -t, --timeout SECS Sets timeout in seconds for z3'); 21 | console.log(' (default timeout is 10s, 0 disables timeout)'); 22 | console.log(' -f, --logformat FORMAT Format can be either "simple", "colored" or "html"'); 23 | console.log(' (default format is "colored")'); 24 | console.log(' -q, --quiet Suppresses output'); 25 | console.log(' -v, --verbose Prints SMT input, output and test code'); 26 | console.log(' --logsmt PATH Path for logging SMT input in verbose mode'); 27 | console.log(' (default path is "/tmp/vc.smt")'); 28 | console.log(' -h, --help Prints this help text and exit'); 29 | console.log(' --version Prints version information'); 30 | process.exit(err ? 1 : 0); 31 | } 32 | 33 | var opts = minimist(process.argv.slice(2), { 34 | boolean: ['noqi', 'remote', 'quiet', 'verbose', 'help', 'version'], 35 | string: ['logformat', 'z3path', 'z3url', 'logsmt', 'timeout'], 36 | alias: {r: 'remote', f: 'logformat', q: 'quiet', t: 'timeout', v: 'verbose', h: 'help' }, 37 | unknown: function(opt) { return opt[0] == '-' && opt != '-' ? usage(true) : true; } 38 | }); 39 | if (opts.version) { 40 | console.log(require('../package.json').version); 41 | process.exit(0); 42 | } 43 | if (opts._.length != 1 || opts.help) usage(!opts.help); 44 | opts.qi = !opts['noqi']; 45 | if (opts.hasOwnProperty('timeout')) { 46 | opts.timeout = parseInt(opts.timeout, 10); 47 | if (!Number.isInteger(opts.timeout) || opts.timeout < 0) usage(true); 48 | } 49 | opts.filename = opts._[0]; 50 | 51 | function run (err, js) { 52 | if (err) error('Error: ' + err.message); 53 | esverify.verify(js.toString(), opts) 54 | .then(msgs => msgs.some(msg => msg.status != 'verified') && error('failed')); 55 | } 56 | 57 | if (opts.filename !== '-') { 58 | fs.readFile(opts.filename, run); 59 | } else { 60 | var content = ''; 61 | process.stdin.resume(); 62 | process.stdin.on('data', chunk => content += chunk); 63 | process.stdin.on('error', e => error('Error: ' + e.message)); 64 | process.stdin.on('end', () => run(null, content)); 65 | } 66 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /examples/adder.js: -------------------------------------------------------------------------------- 1 | // Simple Classes: Adder 2 | class Adder { 3 | constructor (base) { 4 | this.base = base; 5 | } 6 | invariant () { 7 | return typeof this.base === 'number'; 8 | } 9 | addTo (n) { 10 | requires(typeof n === 'number'); 11 | return this.base + n; 12 | } 13 | } 14 | 15 | const adder = new Adder(5); 16 | const m = adder.addTo(3); 17 | assert(m === 8); 18 | 19 | function f (a) { 20 | requires(a instanceof Adder); 21 | ensures(res => res !== 2); // does not hold if a is "new A(1)" 22 | 23 | return a.addTo(1); 24 | } 25 | -------------------------------------------------------------------------------- /examples/arrays.js: -------------------------------------------------------------------------------- 1 | // Array Operations 2 | const a1 = [23]; 3 | assert(a1 instanceof Array); 4 | assert(a1 instanceof Object); 5 | assert('length' in a1); 6 | assert(a1.length === 1); 7 | assert(0 in a1); 8 | assert(a1[0] > 22); 9 | const p = 3 - 2 - 1; 10 | assert(a1[p] > 22); 11 | const arr = [1, 2, 3]; 12 | const sliced = arr.slice(1, 2); 13 | assert(sliced.length === 1); 14 | assert(sliced[0] === 2); 15 | -------------------------------------------------------------------------------- /examples/cons.js: -------------------------------------------------------------------------------- 1 | // Closures 2 | function cons(x) { 3 | function f () { return x; } 4 | return f; 5 | } 6 | const g = cons(1); 7 | const g1 = g(); 8 | assert(g1 === 1); 9 | const h = cons(2); 10 | const h1 = h(); 11 | assert(h1 === 2); 12 | -------------------------------------------------------------------------------- /examples/contradiction.js: -------------------------------------------------------------------------------- 1 | // Proofs: Simple contradiction 2 | function contradiction(p, p1, a3) { 3 | // p1: { x: nat | p(x) <=> 0 <= x <= 3} 4 | requires(spec(p1, (x) => Number.isInteger(x), 5 | (x) => pure() && p(x) === (0 <= x && x <= 3))); 6 | // a3: { x: nat | p(x) => p(x+1) } 7 | requires(spec(a3, (x) => Number.isInteger(x) && p(x), 8 | (x) => pure() && p(x+1))); 9 | ensures(false); 10 | 11 | // have p(3) from p1(3); 12 | p1(3); 13 | 14 | // have p(4) from a3(3); 15 | a3(3); 16 | 17 | // have 0 <= 4 <= 3 from p1(4); 18 | p1(4); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /examples/counter.js: -------------------------------------------------------------------------------- 1 | // Mutable Variables: Counter 2 | let counter = 0; 3 | invariant(Number.isInteger(counter)); 4 | invariant(counter >= 0); 5 | 6 | function increment () { 7 | ensures(counter > old(counter)); 8 | 9 | counter++; 10 | } 11 | 12 | function decrement () { 13 | ensures(old(counter) > 0 ? counter < old(counter) : counter === old(counter)); 14 | 15 | if (counter > 0) counter--; 16 | } 17 | -------------------------------------------------------------------------------- /examples/f.js: -------------------------------------------------------------------------------- 1 | // Pure Functions and Mutation 2 | let x = 0; 3 | 4 | function f() { ensures(pure()); x++; } // not actually pure 5 | function g() { ensures(pure()); return x + 1; } 6 | function h1() { /*empty*/ } 7 | function h2a() { h1(); } 8 | function h2b() { ensures(pure()); h1(); } // inlining h1 shows purity 9 | function h3a() { ensures(pure()); h2a(); } // not verified because inlining restricted to one level 10 | function h3b() { ensures(pure()); h2b(); } // verified because h2b marked as pure`, 11 | -------------------------------------------------------------------------------- /examples/fmono.js: -------------------------------------------------------------------------------- 1 | // Proofs: Fibonacci monotonous 2 | function fib (n) { 3 | requires(Number.isInteger(n)); 4 | requires(n >= 0); 5 | ensures(pure()); 6 | ensures(res => Number.isInteger(res)); 7 | 8 | if (n <= 1) { 9 | return 1; 10 | } else { 11 | return fib(n - 1) + fib(n - 2); 12 | } 13 | } 14 | 15 | function fibInc (n) { 16 | requires(Number.isInteger(n)); 17 | requires(n >= 0); 18 | ensures(fib(n) <= fib(n + 1)); 19 | ensures(pure()); 20 | 21 | fib(n); 22 | fib(n + 1); 23 | 24 | if (n > 0) { 25 | fib(n - 1); 26 | fibInc(n - 1); 27 | } 28 | 29 | if (n > 1) { 30 | fib(n - 2); 31 | fibInc(n - 2); 32 | } 33 | } 34 | 35 | function fMono (f, fInc, n, m) { 36 | requires(spec(f, x => Number.isInteger(x) && x >= 0, 37 | (x, y) => pure() && Number.isInteger(y))); 38 | requires(spec(fInc, x => Number.isInteger(x) && x >= 0, 39 | x => pure() && f(x) <= f(x + 1))); 40 | requires(Number.isInteger(n)); 41 | requires(n >= 0); 42 | requires(Number.isInteger(m)); 43 | requires(m >= 0); 44 | requires(n < m); 45 | ensures(pure()); 46 | ensures(f(n) <= f(m)); 47 | 48 | if (n + 1 === m) { 49 | fInc(n); 50 | } else { 51 | fInc(n); 52 | fMono(f, fInc, n + 1, m); 53 | } 54 | } 55 | 56 | function fibMono (n, m) { 57 | requires(Number.isInteger(n)); 58 | requires(n >= 0); 59 | requires(Number.isInteger(m)); 60 | requires(m >= 0); 61 | requires(n < m); 62 | ensures(pure()); 63 | ensures(fib(n) <= fib(m)); 64 | 65 | fMono(fib, fibInc, n, m); 66 | } 67 | -------------------------------------------------------------------------------- /examples/inc.js: -------------------------------------------------------------------------------- 1 | // Function Calls: inc 2 | function inc(n) { 3 | return n + 1; 4 | } 5 | 6 | let i = 3; 7 | let j = inc(i); // call automatically inlines function body 8 | assert(j === 4); 9 | -------------------------------------------------------------------------------- /examples/map.js: -------------------------------------------------------------------------------- 1 | // Parameterized List Class 2 | class List { 3 | constructor (head, tail, each) { 4 | this.head = head; this.tail = tail; this.each = each; 5 | } 6 | invariant () { 7 | return spec(this.each, (x) => true, (x, y) => pure() && typeof(y) === 'boolean') 8 | && (true && this.each)(this.head) // same as 'this.each(this.head)' 9 | // but avoids binding 'this' 10 | && (this.tail === null || (this.tail instanceof List && 11 | this.each === this.tail.each)); 12 | } 13 | } 14 | 15 | function map (f, lst, newEach) { 16 | requires(spec(newEach, (x) => true, (x, y) => pure() && typeof(y) === 'boolean')); 17 | requires(lst === null || spec(f, (x) => (true && lst.each)(x), 18 | (x, y) => pure() && newEach(y))); 19 | requires(lst === null || lst instanceof List); 20 | ensures(res => res === null || (res instanceof List && res.each === newEach)); 21 | ensures(pure()); // necessary for recursive calls 22 | 23 | return lst === null ? null 24 | : new List(f(lst.head), map(f, lst.tail, newEach), newEach); 25 | } 26 | -------------------------------------------------------------------------------- /examples/maplen.js: -------------------------------------------------------------------------------- 1 | // Custom List Class: map and length 2 | class List { 3 | constructor (head, tail) { this.head = head; this.tail = tail; } 4 | invariant () { return this.tail === null || this.tail instanceof List; } 5 | } 6 | 7 | function map (lst, f) { 8 | requires(lst === null || lst instanceof List); 9 | requires(spec(f, x => true, x => pure())); 10 | ensures(pure()); 11 | ensures(res => res === null || res instanceof List); 12 | 13 | if (lst === null) return null; 14 | return new List(f(lst.head), map(lst.tail, f)); 15 | } 16 | 17 | function len (lst) { 18 | requires(lst === null || lst instanceof List); 19 | ensures(pure()); 20 | ensures(res => typeof res === 'number' && res >= 0); 21 | 22 | return lst === null ? 0 : len(lst.tail) + 1; 23 | } 24 | 25 | function mapLen (lst, f) { 26 | requires(lst === null || lst instanceof List); 27 | requires(spec(f, x => true, x => pure())); 28 | ensures(pure()); 29 | ensures(len(lst) === len(map(lst, f))); 30 | 31 | const l = len(lst); 32 | const r = len(map(lst, f)); 33 | if (lst === null) { 34 | assert(l === 0); 35 | assert(r === 0); 36 | } else { 37 | const l1 = len(lst.tail); 38 | assert(l === l1 + 1); 39 | 40 | f(lst.head); 41 | const r1 = len(map(lst.tail, f)); 42 | assert(r === r1 + 1); 43 | 44 | mapLen(lst.tail, f); 45 | assert(l1 === r1); 46 | assert(l === r); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/max.js: -------------------------------------------------------------------------------- 1 | // Simple Example: max 2 | // This is a live demo, simply edit the code and click "verify" above! 3 | 4 | function max(a, b) { 5 | requires(typeof(a) === 'number'); 6 | requires(typeof(b) === 'number'); 7 | ensures(res => res >= a); 8 | ensures(res => res >= b); // this post-condition does not hold 9 | 10 | if (a >= b) { 11 | return a; 12 | } else { 13 | return a; // due to a bug in the implementation 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/msort.js: -------------------------------------------------------------------------------- 1 | // Merge Sort 2 | class IntList { 3 | constructor (head, tail) { 4 | this.head = head; 5 | this.tail = tail; 6 | } 7 | invariant () { 8 | return typeof(this.head) === 'number' && 9 | (this.tail === null || this.tail instanceof IntList); 10 | } 11 | } 12 | 13 | class IntListPartition { 14 | constructor (left, right) { 15 | this.left = left; 16 | this.right = right; 17 | } 18 | invariant () { 19 | return (this.left === null || this.left instanceof IntList) && 20 | (this.right === null || this.right instanceof IntList); 21 | } 22 | } 23 | 24 | function partition (lst, fst, snd, alternate) { 25 | requires(lst === null || lst instanceof IntList); 26 | requires(fst === null || fst instanceof IntList); 27 | requires(snd === null || snd instanceof IntList); 28 | requires(typeof(alternate) === 'boolean'); 29 | ensures(res => res instanceof IntListPartition); 30 | ensures(pure()); 31 | 32 | if (lst === null) { 33 | return new IntListPartition(fst, snd); 34 | } else if (alternate) { 35 | return partition(lst.tail, new IntList(lst.head, fst), snd, false); 36 | } else { 37 | return partition(lst.tail, fst, new IntList(lst.head, snd), true); 38 | } 39 | } 40 | 41 | function isSorted (list) { 42 | requires(list === null || list instanceof IntList); 43 | ensures(res => typeof(res) === 'boolean'); 44 | ensures(pure()); 45 | 46 | return list === null || list.tail === null || 47 | list.head <= list.tail.head && isSorted(list.tail); 48 | } 49 | 50 | function merge (left, right) { 51 | requires(left === null || left instanceof IntList); 52 | requires(isSorted(left)); 53 | requires(right === null || right instanceof IntList); 54 | requires(isSorted(right)); 55 | ensures(res => res === null || res instanceof IntList); 56 | ensures(res => isSorted(res)); 57 | ensures(res => (left === null && right === null) === (res === null)); 58 | ensures(res => !(left !== null && (right === null || right.head >= left.head)) 59 | || 60 | (res !== null && res.head === left.head)); 61 | ensures(res => !(right !== null && (left === null || right.head < left.head)) 62 | || 63 | (res !== null && res.head === right.head)); 64 | ensures(pure()); 65 | 66 | if (left === null) { 67 | return right; 68 | } else if (right === null) { 69 | return left; 70 | } else if (left.head <= right.head) { 71 | isSorted(left); 72 | isSorted(left.tail); 73 | const merged = merge(left.tail, right); 74 | const res = new IntList(left.head, merged); 75 | isSorted(res); 76 | return res; 77 | } else { 78 | isSorted(right); 79 | isSorted(right.tail); 80 | const merged = merge(left, right.tail); 81 | const res = new IntList(right.head, merged); 82 | isSorted(res); 83 | return res; 84 | } 85 | } 86 | 87 | function sort (list) { 88 | requires(list === null || list instanceof IntList); 89 | ensures(res => res === null || res instanceof IntList); 90 | ensures(res => isSorted(res)); 91 | ensures(pure()); 92 | 93 | if (list === null || list.tail === null) { 94 | isSorted(list); 95 | assert(isSorted(list)); 96 | return list; 97 | } 98 | const part = partition(list, null, null, false); 99 | return merge(sort(part.left), sort(part.right)); 100 | } 101 | -------------------------------------------------------------------------------- /examples/peano-contradiction.js: -------------------------------------------------------------------------------- 1 | // Proofs: Peano axioms 2 | function contradiction(nat, zero, s, a1, a2, p, p1, a3) { 3 | // nat: any -> Prop 4 | requires(spec(nat, (x) => true, (x,y) => pure() && typeof y === 'boolean')); 5 | 6 | // zero: nat 7 | requires(nat(zero)); 8 | 9 | // s: nat -> nat 10 | requires(spec(s, (x) => nat(x), (x, y) => pure() && nat(y))); 11 | 12 | // a1: { x: nat, y: nat | x = y <=> x+1 = y+1 } 13 | requires(spec(a1, (x, y) => nat(x) && nat(y), 14 | (x, y) => pure() && (x === y) === (s(x) === s(y)))); 15 | 16 | // a2: { x: nat | 0 !== x+1 } 17 | requires(spec(a2, (x) => nat(x), (x) => pure() && s(x) !== zero)); 18 | 19 | // p1: { x: nat | p(x) <=> 0 <= x <= 3} 20 | requires(spec(p1, (x) => nat(x), 21 | (x) => pure() && 22 | p(x) === (x === zero || x === s(zero) || 23 | x === s(s(zero)) || x === s(s(s(zero)))))); 24 | // a3: { x: nat | p(x) => p(x+1) } 25 | requires(spec(a3, (x) => nat && p(x), 26 | (x) => pure() && p(s(x)))); 27 | ensures(false); 28 | 29 | // have p(3) from p1(3); 30 | p1(s(s(s(zero)))); 31 | assert(p(s(s(s(zero))))); 32 | 33 | // have p(4) from a3(3); 34 | a3(s(s(s(zero)))); 35 | assert(p(s(s(s(s(zero)))))); 36 | 37 | // have 0 <= 4 <= 3 from p1(4); 38 | p1(s(s(s(s(zero))))); 39 | assert((s(s(s(s(zero)))) === zero || 40 | s(s(s(s(zero)))) === s(zero) || 41 | s(s(s(s(zero)))) === s(s(zero)) || 42 | s(s(s(s(zero)))) === s(s(s(zero))))); 43 | 44 | // have 4 != 0 from a2(3); 45 | a2(s(s(s(zero)))); 46 | assert(s(s(s(s(zero)))) !== zero); 47 | 48 | // have 3 != 0 from a2(2); 49 | a2(s(s(zero))); 50 | assert(s(s(s(zero))) !== zero); 51 | 52 | // have 2 != 0 from a2(1); 53 | a2(s(zero)); 54 | assert(s(s(zero)) !== zero); 55 | 56 | // have 1 != 0 from a2(0); 57 | a2(zero); 58 | assert(s(zero) !== zero); 59 | 60 | // have 4 !== 1 from a1(3, 0) 61 | a1(s(s(s(zero))), zero); 62 | assert(s(s(s(s(zero)))) !== s(zero)); 63 | 64 | // have 4 !== 2 from a1(3, 1), a1(2, 0) 65 | a1(s(s(s(zero))), s(zero)); 66 | a1(s(s(zero)), zero); 67 | assert(s(s(s(s(zero)))) !== s(s(zero))); 68 | 69 | // have 4 !== 3 from a1(3, 2), a1(2, 1), a1(1, 0) 70 | a1(s(s(s(zero))), s(s(zero))); 71 | a1(s(s(zero)), s(zero)); 72 | a1(s(zero), zero); 73 | assert(s(s(s(s(zero)))) !== s(s(s(zero)))); 74 | } 75 | 76 | -------------------------------------------------------------------------------- /examples/reverse-asc.js: -------------------------------------------------------------------------------- 1 | // Reversing an ascending list 2 | class IntList { 3 | constructor (head, tail) { 4 | this.head = head; 5 | this.tail = tail; 6 | } 7 | invariant () { 8 | return typeof(this.head) === 'number' && 9 | (this.tail === null || this.tail instanceof IntList); 10 | } 11 | } 12 | 13 | function isAscending (list) { 14 | requires(list === null || list instanceof IntList); 15 | ensures(res => typeof(res) === 'boolean'); 16 | ensures(pure()); 17 | 18 | return list === null || list.tail === null || 19 | list.head <= list.tail.head && isAscending(list.tail); 20 | } 21 | 22 | function isDescending (list) { 23 | requires(list === null || list instanceof IntList); 24 | ensures(res => typeof(res) === 'boolean'); 25 | ensures(pure()); 26 | 27 | return list === null || list.tail === null || 28 | list.head >= list.tail.head && isDescending(list.tail); 29 | } 30 | 31 | function reverseHelper (pivot, acc, list) { 32 | requires(list === null || list instanceof IntList); 33 | requires(isAscending(list)); 34 | requires(typeof pivot === 'number'); 35 | requires(list === null || pivot <= list.head); 36 | requires(acc === null || acc instanceof IntList); 37 | requires(isDescending(acc)); 38 | requires(acc === null || pivot >= acc.head); 39 | ensures(res => res === null || res instanceof IntList); 40 | ensures(res => isDescending(res)); 41 | ensures(pure()); 42 | 43 | const newList = new IntList(pivot, acc); 44 | isDescending(newList); // instantiation 45 | 46 | if (list === null) { 47 | return newList; 48 | } else { 49 | isAscending(list); // instantiation 50 | isAscending(list.tail); // instantiation 51 | return reverseHelper(list.head, newList, list.tail); 52 | } 53 | } 54 | 55 | function reverse (list) { 56 | requires(list === null || list instanceof IntList); 57 | requires(isAscending(list)); 58 | ensures(res => res === null || res instanceof IntList); 59 | ensures(res => isDescending(res)); 60 | ensures(pure()); 61 | 62 | if (list === null) { 63 | isDescending(list); // instantiation 64 | return list; 65 | } else { 66 | isAscending(list); // instantiation 67 | isAscending(list.tail); // instantiation 68 | isDescending(null); // instantiation 69 | return reverseHelper(list.head, null, list.tail); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /examples/strings.js: -------------------------------------------------------------------------------- 1 | // String Operations 2 | const s1 = 'hello'; 3 | assert(s1.length === 5); 4 | const l2 = (s1 + ' world').length; 5 | const l3 = s1.length + ' world'.length; 6 | assert(l2 === l3); 7 | assert(l2 === 11); 8 | const c1 = s1[0]; 9 | const c2 = s1[3 - 2]; 10 | assert(c1 === 'h'); 11 | assert(c2 === 'e'); 12 | const str = 'abcd'; 13 | const substr = str.substr(1, 2); 14 | assert(substr === 'bc'); 15 | -------------------------------------------------------------------------------- /examples/sumto.js: -------------------------------------------------------------------------------- 1 | // Loops: sumTo 2 | function sumTo (n) { 3 | requires(Number.isInteger(n)); 4 | requires(n >= 0); 5 | ensures(res => res === (n + 1) * n / 2); 6 | 7 | let i = 0; 8 | let s = 0; 9 | while (i < n) { 10 | invariant(i <= n); 11 | invariant(s === (i + 1) * i / 2); 12 | invariant(Number.isInteger(i)); 13 | invariant(Number.isInteger(s)); 14 | i++; 15 | s = s + i; 16 | } 17 | return s; 18 | } 19 | -------------------------------------------------------------------------------- /examples/twice.js: -------------------------------------------------------------------------------- 1 | // Higher-order Functions: twice 2 | function inc (n) { 3 | requires(Number.isInteger(n)); 4 | ensures(res => Number.isInteger(res) && res > n); 5 | return n + 1; 6 | } 7 | 8 | function twice (f) { 9 | requires(spec(f, (x) => Number.isInteger(x), 10 | (x, y) => Number.isInteger(y) && y > x)); 11 | ensures(g => spec(g, (x) => Number.isInteger(x), 12 | (x, y) => Number.isInteger(y) && y > x + 1)); 13 | return function (n) { 14 | requires(Number.isInteger(n)); 15 | ensures(res => Number.isInteger(res) && res > n + 1); 16 | return f(f(n)); 17 | }; 18 | } 19 | 20 | const incTwice = twice(inc); 21 | const y = incTwice(3); 22 | assert(y > 4); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esverify", 3 | "version": "0.0.0-development", 4 | "description": "ECMAScript program verifier based on SMT solving", 5 | "author": "Christopher Schuster ", 6 | "license": "MIT", 7 | "main": "build/main/index.js", 8 | "typings": "build/main/index.d.ts", 9 | "module": "build/module/index.js", 10 | "files": [ 11 | "bin", 12 | "build/main/*.js", 13 | "build/main/*.d.ts", 14 | "build/module/*.js", 15 | "build/module/*.d.ts" 16 | ], 17 | "bin": { 18 | "esverify": "./bin/esverify.js" 19 | }, 20 | "engines": { 21 | "node": ">=14.18" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/levjj/esverify/issues" 25 | }, 26 | "homepage": "https://esverify.org/", 27 | "scripts": { 28 | "lint": "tsc --noEmit && tslint --project .", 29 | "test": "TS_NODE_TRANSPILE_ONLY=true mocha -r ts-node/register tests/*.ts", 30 | "build": "tsc -p tsconfig.json && tsc -p tsconfig.module.json", 31 | "commit": "commit" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/levjj/esverify.git" 36 | }, 37 | "dependencies": { 38 | "@types/esprima": "^4.0.2", 39 | "@types/estree": "0.0.45", 40 | "esprima": "^4.0.1", 41 | "minimist2": "^1.0.2" 42 | }, 43 | "devDependencies": { 44 | "@commitlint/cli": "^12.0.0", 45 | "@commitlint/config-conventional": "^12.0.0", 46 | "@commitlint/prompt": "^17.6.1", 47 | "@commitlint/prompt-cli": "^17.6.1", 48 | "@commitlint/travis-cli": "^12.0.0", 49 | "@types/chai": "^4.2.18", 50 | "@types/mocha": "^8.2.2", 51 | "chai": "^4.3.4", 52 | "husky": "^4.3.8", 53 | "mocha": "^10.2.0", 54 | "ts-node": "^9.1.1", 55 | "tslint": "^6.1.3", 56 | "tslint-config-standard": "^9.0.0", 57 | "typescript": "^4.3.2" 58 | }, 59 | "husky": { 60 | "hooks": { 61 | "commit-msg": "commitlint -x @commitlint/config-conventional -e $HUSKY_GIT_PARAMS", 62 | "pre-push": "npm run lint && npm test && npm run build" 63 | } 64 | }, 65 | "volta": { 66 | "node": "14.18.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { Syntax, Visitor } from './javascript'; 2 | 3 | class Stringifier extends Visitor { 4 | 5 | depth: number = 0; 6 | 7 | formatBlock (stmts: Array): string { 8 | let res = '{\n'; 9 | this.indent(() => { 10 | for (const s of stmts) { 11 | res += typeof s === 'string' ? this.i(s) : this.visitStatement(s); 12 | } 13 | }); 14 | return res + this.i('}'); 15 | } 16 | 17 | visitIdentifierTerm (term: Syntax.Identifier): string { 18 | return term.name; 19 | } 20 | 21 | visitOldIdentifierTerm (term: Syntax.OldIdentifier): string { 22 | return `old(${term.id.name})`; 23 | } 24 | 25 | visitLiteralTerm (term: Syntax.Literal): string { 26 | return term.value === undefined ? 'undefined' : JSON.stringify(term.value); 27 | } 28 | 29 | visitUnaryTerm (term: Syntax.UnaryTerm): string { 30 | switch (term.operator) { 31 | case 'typeof': 32 | case 'void': 33 | return `${term.operator}(${this.visitTerm(term.argument)})`; 34 | default: 35 | return `${term.operator}${this.visitTerm(term.argument)}`; 36 | } 37 | } 38 | 39 | visitBinaryTerm (term: Syntax.BinaryTerm): string { 40 | return `(${this.visitTerm(term.left)} ${term.operator} ${this.visitTerm(term.right)})`; 41 | } 42 | 43 | visitLogicalTerm (term: Syntax.LogicalTerm): string { 44 | return `(${this.visitTerm(term.left)} ${term.operator} ${this.visitTerm(term.right)})`; 45 | } 46 | 47 | visitConditionalTerm (term: Syntax.ConditionalTerm): string { 48 | return `(${this.visitTerm(term.test)} ? ${this.visitTerm(term.consequent)} ` + 49 | `: ${this.visitTerm(term.alternate)})`; 50 | } 51 | 52 | visitCallTerm (term: Syntax.CallTerm): string { 53 | return `${this.visitTerm(term.callee)}(${term.args.map(a => this.visitTerm(a)).join(', ')})`; 54 | } 55 | 56 | visitMemberTerm (term: Syntax.MemberTerm): string { 57 | if (term.property.type === 'Literal' && 58 | typeof term.property.value === 'string' && 59 | /^[a-zA-Z_]+$/.test(term.property.value)) { 60 | return `${this.visitTerm(term.object)}.${term.property.value}`; 61 | } else { 62 | return `${this.visitTerm(term.object)}[${this.visitTerm(term.property)}]`; 63 | } 64 | } 65 | 66 | visitIsIntegerTerm (term: Syntax.IsIntegerTerm): string { 67 | return `Number.isInteger(${this.visitTerm(term.term)})`; 68 | } 69 | 70 | visitToIntegerTerm (term: Syntax.ToIntegerTerm): string { 71 | return `Math.trunc(${this.visitTerm(term.term)})`; 72 | } 73 | 74 | visitTermAssertion (assertion: Syntax.Term): string { 75 | return this.visitTerm(assertion); 76 | } 77 | 78 | visitPostCondition (post: Syntax.PostCondition): string { 79 | if (post.argument) { 80 | return `${this.visitTerm(post.argument)} => ${this.visitAssertion(post.expression)}`; 81 | } 82 | return this.visitAssertion(post.expression); 83 | } 84 | 85 | visitParams (params: Array): string { 86 | if (params.length === 1) return params[0]; 87 | return `(${params.join(', ')})`; 88 | } 89 | 90 | visitSpecAssertion (assertion: Syntax.SpecAssertion): string { 91 | if (assertion.post.argument) { 92 | return `spec(${this.visitTerm(assertion.callee)}, ` + 93 | `${this.visitParams(assertion.args)} => ${this.visitAssertion(assertion.pre)}, ` + 94 | `${this.visitParams([...assertion.args, assertion.post.argument.name])} => ` + 95 | `${this.visitAssertion(assertion.post.expression)})`; 96 | } 97 | return `spec(${this.visitTerm(assertion.callee)}, ` + 98 | `${this.visitParams(assertion.args)} => ${this.visitAssertion(assertion.pre)}, ` + 99 | `${this.visitParams(assertion.args)} => ${this.visitAssertion(assertion.post.expression)})`; 100 | } 101 | 102 | visitEveryAssertion (assertion: Syntax.EveryAssertion): string { 103 | if (assertion.indexArgument !== null) { 104 | return `${this.visitTerm(assertion.array)}.every(` + 105 | `(${assertion.argument.name}, ${assertion.indexArgument.name}) => ` + 106 | `${this.visitAssertion(assertion.expression)})`; 107 | } else { 108 | return `${this.visitTerm(assertion.array)}.every(` + 109 | `${assertion.argument.name} => ${this.visitAssertion(assertion.expression)})`; 110 | } 111 | } 112 | 113 | visitPureAssertion (assertion: Syntax.PureAssertion): string { 114 | return `pure()`; 115 | } 116 | 117 | visitInstanceOfAssertion (assertion: Syntax.InstanceOfAssertion): string { 118 | return `(${this.visitTerm(assertion.left)} instanceof ${assertion.right.name})`; 119 | } 120 | 121 | visitInAssertion (assertion: Syntax.InAssertion): string { 122 | return `(${this.visitTerm(assertion.property)} in ${this.visitTerm(assertion.object)})`; 123 | } 124 | 125 | visitUnaryAssertion (assertion: Syntax.UnaryAssertion) { 126 | return `!${this.visitAssertion(assertion.argument)}`; 127 | } 128 | 129 | visitBinaryAssertion (assertion: Syntax.BinaryAssertion): string { 130 | return `(${this.visitAssertion(assertion.left)} ${assertion.operator} ${this.visitAssertion(assertion.right)})`; 131 | } 132 | 133 | visitIdentifier (expr: Syntax.Identifier): string { 134 | return expr.name; 135 | } 136 | 137 | visitLiteral (expr: Syntax.Literal): string { 138 | return expr.value === undefined ? 'undefined' : JSON.stringify(expr.value); 139 | } 140 | 141 | visitUnaryExpression (expr: Syntax.UnaryExpression): string { 142 | switch (expr.operator) { 143 | case 'typeof': 144 | case 'void': 145 | return `${expr.operator}(${this.visitExpression(expr.argument)})`; 146 | default: 147 | return `${expr.operator}${this.visitExpression(expr.argument)}`; 148 | } 149 | } 150 | 151 | visitBinaryExpression (expr: Syntax.BinaryExpression): string { 152 | return `(${this.visitExpression(expr.left)} ${expr.operator} ${this.visitExpression(expr.right)})`; 153 | } 154 | 155 | visitLogicalExpression (expr: Syntax.LogicalExpression): string { 156 | return `(${this.visitExpression(expr.left)} ${expr.operator} ${this.visitExpression(expr.right)})`; 157 | } 158 | 159 | visitConditionalExpression (expr: Syntax.ConditionalExpression): string { 160 | return `(${this.visitExpression(expr.test)} ? ${this.visitExpression(expr.consequent)} ` + 161 | `: ${this.visitExpression(expr.alternate)})`; 162 | } 163 | 164 | visitAssignmentExpression (expr: Syntax.AssignmentExpression): string { 165 | return `${this.visitExpression(expr.left)} = ${this.visitExpression(expr.right)}`; 166 | } 167 | 168 | visitSequenceExpression (expr: Syntax.SequenceExpression): string { 169 | return `(${expr.expressions.map(e => this.visitExpression(e)).join(', ')})`; 170 | } 171 | 172 | visitCallExpression (expr: Syntax.CallExpression): string { 173 | return `${this.visitExpression(expr.callee)}(${expr.args.map(a => this.visitExpression(a)).join(', ')})`; 174 | } 175 | 176 | visitNewExpression (expr: Syntax.NewExpression): string { 177 | return `new ${expr.callee.name}(${expr.args.map(a => this.visitExpression(a)).join(', ')})`; 178 | } 179 | 180 | visitArrayExpression (expr: Syntax.ArrayExpression): string { 181 | return `[${expr.elements.map(a => this.visitExpression(a)).join(', ')}]`; 182 | } 183 | 184 | visitObjectExpression (expr: Syntax.ObjectExpression): string { 185 | function nameToKey (name: string): string { 186 | return /^\w+$/.test(name) ? name : `"${name}"`; 187 | } 188 | return `{ ${ 189 | expr.properties.map(({ key, value }) => `${nameToKey(key)}: ${this.visitExpression(value)}`).join(', ')} }`; 190 | } 191 | 192 | visitInstanceOfExpression (expr: Syntax.InstanceOfExpression): string { 193 | return `(${this.visitExpression(expr.left)} instanceof ${expr.right.name})`; 194 | } 195 | 196 | visitInExpression (expr: Syntax.InExpression): string { 197 | return `(${this.visitExpression(expr.property)} in ${this.visitExpression(expr.object)})`; 198 | } 199 | 200 | visitMemberExpression (expr: Syntax.MemberExpression): string { 201 | if (expr.property.type === 'Literal' && 202 | typeof expr.property.value === 'string' && 203 | /^[a-zA-Z_]+$/.test(expr.property.value)) { 204 | return `${this.visitExpression(expr.object)}.${expr.property.value}`; 205 | } else { 206 | return `${this.visitExpression(expr.object)}[${this.visitExpression(expr.property)}]`; 207 | } 208 | } 209 | 210 | visitFunctionExpression (expr: Syntax.FunctionExpression): string { 211 | if (expr.id === null && expr.body.body.length === 1 && expr.body.body[0].type === 'ReturnStatement' && 212 | expr.requires.length === 0 && expr.ensures.length === 0) { 213 | return `${this.visitParams(expr.params.map(p => p.name))} => ${this.visitExpression(expr.body.body[0].argument)}`; 214 | } 215 | const body: Array = ([] as Array) 216 | .concat(expr.requires.map(req => `requires(${this.visitAssertion(req)});`)) 217 | .concat(expr.ensures.map(ens => `ensures(${this.visitPostCondition(ens)});`)) 218 | .concat(expr.body.body); 219 | return `(function ${expr.id ? expr.id.name + ' ' : ''}(${expr.params.map(p => p.name).join(', ')}) ` + 220 | `${this.formatBlock(body)})`; 221 | } 222 | 223 | indent (f: () => void) { 224 | this.depth++; 225 | try { 226 | f(); 227 | } finally { 228 | this.depth--; 229 | } 230 | } 231 | 232 | i (s: string): string { 233 | let d = ''; 234 | for (let i = 0; i < this.depth; i++) d += ' '; 235 | return d + s; 236 | } 237 | 238 | visitVariableDeclaration (stmt: Syntax.VariableDeclaration): string { 239 | return this.i(`${stmt.kind} ${stmt.id.name} = ${this.visitExpression(stmt.init)};\n`); 240 | } 241 | 242 | visitBlockStatement (stmt: Syntax.BlockStatement): string { 243 | return this.i(this.formatBlock(stmt.body)) + '\n'; 244 | } 245 | 246 | visitExpressionStatement (stmt: Syntax.ExpressionStatement): string { 247 | return this.i(`${this.visitExpression(stmt.expression)};\n`); 248 | } 249 | 250 | visitAssertStatement (stmt: Syntax.AssertStatement): string { 251 | return this.i(`assert(${this.visitAssertion(stmt.expression)});\n`); 252 | } 253 | 254 | visitIfStatement (stmt: Syntax.IfStatement): string { 255 | if (stmt.alternate.body.length === 0) { 256 | return this.i(`if (${this.visitExpression(stmt.test)}) ` + 257 | `${this.formatBlock(stmt.consequent.body)}\n`); 258 | } else { 259 | return this.i(`if (${this.visitExpression(stmt.test)}) ` + 260 | `${this.formatBlock(stmt.consequent.body)} else ` + 261 | `${this.formatBlock(stmt.alternate.body)}\n`); 262 | } 263 | } 264 | 265 | visitReturnStatement (stmt: Syntax.ReturnStatement): string { 266 | return this.i(`return ${this.visitExpression(stmt.argument)};\n`); 267 | } 268 | 269 | visitWhileStatement (stmt: Syntax.WhileStatement): string { 270 | return this.i(`while (${this.visitExpression(stmt.test)}) ${this.formatBlock(stmt.body.body)}\n`); 271 | } 272 | 273 | visitDebuggerStatement (stmt: Syntax.DebuggerStatement): string { 274 | return this.i(`debugger;\n`); 275 | } 276 | 277 | visitFunctionDeclaration (stmt: Syntax.FunctionDeclaration): string { 278 | const body: Array = ([] as Array) 279 | .concat(stmt.requires.map(req => `requires(${this.visitAssertion(req)});`)) 280 | .concat(stmt.ensures.map(ens => `ensures(${this.visitPostCondition(ens)});`)) 281 | .concat(stmt.body.body); 282 | return this.i(`function ${stmt.id.name} (${stmt.params.map(p => p.name).join(', ')}) ` + 283 | `${this.formatBlock(body)}\n`); 284 | } 285 | 286 | visitClassDeclaration (stmt: Syntax.ClassDeclaration): string { 287 | let res = this.i(`class ${stmt.id.name} {\n`); 288 | this.depth++; 289 | res += this.i(`constructor(${stmt.fields.join(', ')}) {\n`); 290 | this.depth++; 291 | for (const f of stmt.fields) { 292 | res += this.i(`this.${f} = ${f};\n`); 293 | } 294 | this.depth--; 295 | res += this.i(`}\n`); 296 | if (stmt.invariant.type !== 'Literal' || stmt.invariant.value !== true) { 297 | res += this.i(`invariant(${stmt.fields.join(', ')}) {\n`); 298 | this.depth++; 299 | res += this.i(`return ${this.visitAssertion(stmt.invariant)};\n`); 300 | this.depth--; 301 | res += this.i(`}\n`); 302 | } 303 | stmt.methods.forEach(method => { 304 | const body: Array = ([] as Array) 305 | .concat(method.requires.map(req => `requires(${this.visitAssertion(req)});`)) 306 | .concat(method.ensures.map(ens => `ensures(${this.visitPostCondition(ens)});`)) 307 | .concat(method.body.body); 308 | res += this.i(`${method.id.name} (${method.params.map(p => p.name).join(', ')}) ` + 309 | `${this.formatBlock(body)}\n`); 310 | }); 311 | this.depth--; 312 | res += this.i(`}\n`); 313 | return res; 314 | } 315 | 316 | visitProgram (prog: Syntax.Program) { 317 | return prog.body.map(s => this.visitStatement(s)).join(''); 318 | } 319 | } 320 | 321 | export function stringifyAssertion (assertion: Syntax.Assertion): string { 322 | return (new Stringifier()).visitAssertion(assertion); 323 | } 324 | 325 | export function stringifyExpression (expr: Syntax.Expression): string { 326 | return (new Stringifier()).visitExpression(expr); 327 | } 328 | 329 | export const TEST_PREAMBLE = `function assert (p) { if (!p) throw new Error("assertion failed"); } 330 | function spec (f, id, req, ens) { 331 | if (f._mapping) { 332 | f._mapping[id] = [req, ens]; 333 | return f; 334 | } else { 335 | const mapping = { [id]: [req, ens] }; 336 | const wrapped = function (...args) { 337 | return Object.values(mapping).reduceRight(function (cont, [req, ens]) { 338 | return function (...args2) { 339 | const args3 = req.apply(this, args2); 340 | return ens.apply(this, args3.concat([cont.apply(this, args3)])); 341 | }; 342 | }, f).apply(this, args); 343 | }; 344 | wrapped._mapping = mapping; 345 | wrapped._orig = f; 346 | return wrapped; 347 | } 348 | } 349 | `; 350 | 351 | export function stringifyTestCode (body: ReadonlyArray): string { 352 | const stringifier = new Stringifier(); 353 | return `${TEST_PREAMBLE} 354 | if (typeof alert === 'undefined') global.alert = console.log; 355 | 356 | ${body.map(s => stringifier.visitStatement(s)).join('\n')}`; 357 | } 358 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageException, log, unexpected } from './message'; 2 | import { Options, getOptions, setOptions } from './options'; 3 | import { sourceAsJavaScript } from './parser'; 4 | import { resolveNames } from './scopes'; 5 | import { vcgenProgram, transformProgram } from './vcgen'; 6 | import VerificationCondition from './verification'; 7 | import { TEST_PREAMBLE } from './codegen'; 8 | 9 | export { default as VerificationCondition } from './verification'; 10 | export { Message, format as formatMessage } from './message'; 11 | export { Position, SourceLocation } from './javascript'; 12 | export { Options, setOptions } from './options'; 13 | export { JSVal, valueToString } from './model'; 14 | 15 | export function verificationConditions (src: string, opts: Partial = {}): 16 | Message | Array { 17 | setOptions(opts); 18 | try { 19 | const prog = sourceAsJavaScript(src); 20 | resolveNames(prog); 21 | return vcgenProgram(prog); 22 | } catch (e) { 23 | return e instanceof MessageException ? e.msg : unexpected(e); 24 | } 25 | } 26 | 27 | export async function verify (src: string, opts: Partial = {}): Promise> { 28 | const vcs = verificationConditions(src, opts); 29 | if (!(vcs instanceof Array)) { 30 | if (!getOptions().quiet) log(vcs); 31 | return [vcs]; 32 | } 33 | const res: Array = []; 34 | for (const vc of vcs) { 35 | let m: Message; 36 | try { 37 | m = await vc.verify(); 38 | } catch (e) { 39 | m = e instanceof MessageException ? e.msg : unexpected(e); 40 | } 41 | if (!getOptions().quiet) log(m); 42 | res.push(m); 43 | } 44 | return res; 45 | } 46 | 47 | export function testPreamble (): string { 48 | return TEST_PREAMBLE; 49 | } 50 | 51 | export function transformSourceCode (src: string, opts: Partial = {}): Message | string { 52 | setOptions(opts); 53 | try { 54 | const prog = sourceAsJavaScript(src); 55 | resolveNames(prog); 56 | return transformProgram(prog); 57 | } catch (e) { 58 | return e instanceof MessageException ? e.msg : unexpected(e); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/message.ts: -------------------------------------------------------------------------------- 1 | import { Syntax } from './javascript'; 2 | import { Model } from './model'; 3 | import { getOptions } from './options'; 4 | 5 | export interface BaseMessage { status: string; loc: Syntax.SourceLocation; description: string; } 6 | 7 | export interface Verified extends BaseMessage { status: 'verified'; } 8 | 9 | export interface Unverified extends BaseMessage { status: 'unverified'; model: Model; } 10 | export interface Unknown extends BaseMessage { status: 'unknown'; } 11 | export interface TimeOut extends BaseMessage { status: 'timeout'; } 12 | 13 | export interface BaseError extends BaseMessage { status: 'error'; type: string; } 14 | export interface Incorrect extends BaseError { type: 'incorrect'; model: Model; error: Error; } 15 | export interface ParseError extends BaseError { type: 'parse-error'; } 16 | export interface Unsupported extends BaseError { type: 'unsupported'; loc: Syntax.SourceLocation; } 17 | export interface UndefinedIdentifier extends BaseError { type: 'undefined-identifier'; } 18 | export interface AlreadyDefinedIdentifier extends BaseError { type: 'already-defined'; } 19 | export interface AssignmentToConst extends BaseError { type: 'assignment-to-const'; } 20 | export interface ReferenceInInvariant extends BaseError { type: 'reference-in-invariant'; } 21 | export interface ModelError extends BaseError { type: 'unrecognized-model'; } 22 | export interface UnexpectedError extends BaseError { type: 'unexpected'; error: Error; } 23 | export type Message = Verified | Unverified | TimeOut | Unknown | Incorrect | ParseError | Unsupported 24 | | UndefinedIdentifier | AlreadyDefinedIdentifier | AssignmentToConst | ReferenceInInvariant 25 | | ModelError | UnexpectedError; 26 | 27 | declare const console: { log: (s: string) => void, warn: (s: string) => void, error: (s: string) => void }; 28 | 29 | function formatSimple (msg: Message): string { 30 | const loc = `${msg.loc.file}:${msg.loc.start.line}:${msg.loc.start.column}`; 31 | if (msg.status === 'verified') { 32 | return `${loc}: info: verified ${msg.description}`; 33 | } else if (msg.status === 'unverified' || msg.status === 'unknown' || msg.status === 'timeout') { 34 | return `${loc}: warning: ${msg.status} ${msg.description}`; 35 | } else { 36 | return `${loc}: error: ${msg.type} ${msg.description}`; 37 | } 38 | } 39 | 40 | function formatColored (msg: Message): string { 41 | const loc = `${msg.loc.file}:${msg.loc.start.line}:${msg.loc.start.column}`; 42 | if (msg.status === 'verified') { 43 | return `[${loc}] \x1b[92mverified\x1b[0m ${msg.description}`; 44 | } else if (msg.status === 'unverified' || msg.status === 'unknown' || msg.status === 'timeout') { 45 | return `[${loc}] \x1b[94m${msg.status}\x1b[0m ${msg.description}`; 46 | } else { 47 | return `[${loc}] \x1b[91m${msg.type}\x1b[0m ${msg.description}`; 48 | } 49 | } 50 | 51 | function formatHTML (msg: Message, withLocation: boolean): string { 52 | function boldColored (color: string, text: string): string { 53 | return `${text}`; 54 | } 55 | function escape (text: string): string { 56 | const charToReplace: { [c: string]: string } = { 57 | '&': '&', 58 | '<': '<', 59 | '>': '>' 60 | }; 61 | return text.replace(/[&<>]/g, c => charToReplace[c] || c); 62 | } 63 | const loc = withLocation ? `[${msg.loc.file}:${msg.loc.start.line}:${msg.loc.start.column}] ` : ''; 64 | if (msg.status === 'verified') { 65 | return `${loc}${boldColored('32b643', 'verified')} ${escape(msg.description)}`; 66 | } else if (msg.status === 'unverified' || msg.status === 'unknown' || msg.status === 'timeout') { 67 | return `${loc}${boldColored('ffb700', msg.status)} ${escape(msg.description)}`; 68 | } else { 69 | return `${loc}${boldColored('e85600', msg.type)} ${escape(msg.description)}`; 70 | } 71 | } 72 | 73 | export function format (msg: Message, withLocation: boolean = true): string { 74 | switch (getOptions().logformat) { 75 | case 'simple': 76 | return formatSimple(msg); 77 | case 'colored': 78 | return formatColored(msg); 79 | case 'html': 80 | return formatHTML(msg, withLocation); 81 | } 82 | } 83 | 84 | export function log (msg: Message): void { 85 | if (msg.status === 'verified') { 86 | console.log(format(msg)); 87 | } else if (msg.status === 'unverified' || msg.status === 'unknown') { 88 | console.warn(format(msg)); 89 | } else { 90 | console.error(format(msg)); 91 | } 92 | } 93 | 94 | export class MessageException extends Error { 95 | readonly msg: Message; 96 | constructor (msg: Message) { super(formatSimple(msg)); this.msg = msg; } 97 | } 98 | 99 | export function unexpected (error: Error, 100 | loc: Syntax.SourceLocation = { 101 | file: getOptions().filename, 102 | start: { line: 0, column: 0 }, 103 | end: { line: 0, column: 0 }}, 104 | description?: string): Message { 105 | return { 106 | status: 'error', 107 | type: 'unexpected', 108 | loc, 109 | error, 110 | description: description !== undefined ? `${description}: ${error.message}` : error.message 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | filename: string; 3 | z3path: string; 4 | remote: boolean; 5 | z3url: string; 6 | qi: boolean; 7 | timeout: number; 8 | logformat: 'simple' | 'colored' | 'html'; 9 | quiet: boolean; 10 | verbose: boolean; 11 | logsmt: string; 12 | maxInterpreterSteps: number; 13 | } 14 | 15 | const defaultOptions: Readonly = { 16 | filename: '', 17 | z3path: 'z3', 18 | remote: false, 19 | z3url: '/z3', 20 | qi: true, 21 | timeout: 5, 22 | logformat: 'colored', 23 | quiet: true, 24 | verbose: false, 25 | logsmt: '/tmp/vc.smt', 26 | maxInterpreterSteps: 10000 27 | }; 28 | 29 | let options: Readonly = defaultOptions; // global singleton options object 30 | 31 | export function getOptions (): Readonly { 32 | return options; 33 | } 34 | 35 | export function setOptions (opts: Partial) { 36 | options = Object.assign({}, options, opts); 37 | } 38 | -------------------------------------------------------------------------------- /src/preamble.ts: -------------------------------------------------------------------------------- 1 | import { VCGenerator } from './vcgen'; 2 | import { Classes, Heap, Locs, Vars, P, tru, copy, A } from './logic'; 3 | import { Syntax, TestCode, nullLoc, id } from './javascript'; 4 | import { sourceAsJavaScript } from './parser'; 5 | import { resolveNames } from './scopes'; 6 | 7 | export type GlobalDeclaration = { type: 'Var', decl: Syntax.VariableDeclaration } 8 | | { type: 'Func', decl: Syntax.FunctionDeclaration } 9 | | { type: 'Class', decl: Syntax.ClassDeclaration }; 10 | 11 | export type Preamble = { classes: Classes, heap: Heap, locs: Locs, vars: Vars, prop: P }; 12 | 13 | function builtinClass (name: string): GlobalDeclaration { 14 | return { 15 | type: 'Class', 16 | decl: { 17 | type: 'ClassDeclaration', 18 | id: id(name), 19 | fields: [], 20 | invariant: { type: 'Literal', value: true, loc: nullLoc() }, 21 | methods: [], 22 | loc: nullLoc() 23 | } 24 | }; 25 | } 26 | 27 | function builtinConst (name: string): GlobalDeclaration { 28 | return { 29 | type: 'Var', 30 | decl: { 31 | type: 'VariableDeclaration', 32 | id: id(name), 33 | init: { type: 'Literal', value: undefined, loc: nullLoc() }, 34 | kind: 'const', 35 | loc: nullLoc() 36 | } 37 | }; 38 | } 39 | 40 | function builtinFunc (name: string, numArgs: number): GlobalDeclaration { 41 | return { 42 | type: 'Func', 43 | decl: { 44 | type: 'FunctionDeclaration', 45 | id: id(name), 46 | params: [...Array(numArgs)].map((_, i) => id(`arg${i + 1}`)), 47 | requires: [], 48 | ensures: [], 49 | body: { type: 'BlockStatement', body: [], loc: nullLoc() }, 50 | freeVars: [], 51 | loc: nullLoc() 52 | } 53 | }; 54 | } 55 | 56 | let cachedGlobalDeclarations: Array | null = null; 57 | 58 | export function globalDeclarations (): Array { 59 | if (cachedGlobalDeclarations === null) { 60 | cachedGlobalDeclarations = [ 61 | builtinClass('Object'), 62 | builtinClass('Function'), 63 | builtinClass('Array'), 64 | builtinClass('String'), 65 | builtinConst('console'), 66 | builtinFunc('parseInt', 2), 67 | builtinConst('Math'), 68 | builtinConst('Number'), 69 | builtinFunc('alert', 1) 70 | ]; 71 | } 72 | return cachedGlobalDeclarations; 73 | } 74 | 75 | class PreambleGenrator extends VCGenerator { 76 | 77 | verify (vc: P, testBody: TestCode, loc: Syntax.SourceLocation, desc: string) { 78 | /* only generate preamble, do not verify */ 79 | } 80 | 81 | createFunctionBodyInliner () { 82 | return new PreambleGenrator(this.classes, 83 | this.heap + 1, 84 | this.heap + 1, 85 | new Set([...this.locs]), 86 | new Set([...this.vars]), 87 | [], 88 | [], 89 | true, 90 | this.prop); 91 | } 92 | 93 | visitArrayExpression (expr: Syntax.ArrayExpression): [A, Syntax.Expression] { 94 | if (expr.elements.length >= 2) { 95 | const tag = expr.elements[0]; 96 | if (tag.type === 'Literal' && tag.value === '_builtin_') { 97 | const smt: Array = []; 98 | for (const element of expr.elements.slice(1)) { 99 | if (element.type === 'Literal' && typeof element.value === 'string') { 100 | smt.push(element.value); 101 | } else { 102 | const [elementA] = this.visitExpression(element); 103 | smt.push({ e: elementA }); 104 | } 105 | } 106 | return [{ type: 'RawSMTExpression', smt }, expr]; 107 | } 108 | } 109 | return super.visitArrayExpression(expr); 110 | } 111 | } 112 | 113 | const VCGEN_PREAMBLE = ` 114 | const Number = { 115 | isInteger: function (n) { 116 | ensures(pure()); 117 | return [ '_builtin_', '(jsbool (is-jsint ', n, '))']; 118 | } 119 | }; 120 | 121 | class String { 122 | constructor (_str_) { 123 | this._str_ = _str_; 124 | } 125 | invariant () { 126 | return typeof this === 'string' || typeof this._str_ === 'string'; 127 | } 128 | substr (from, len) { 129 | requires(Number.isInteger(from)); 130 | requires(Number.isInteger(len)); 131 | requires(from >= 0); 132 | requires(len >= 0); 133 | ensures(pure()); 134 | return [ 135 | '_builtin_', 136 | '(jsstr (str.substr (strv (ite (is-jsstr ', this, ') ', this, ' (String-_str_ ', this, '))) ', 137 | '(intv ', from, ') (intv ', len, ')))']; 138 | } 139 | substring (from, to) { 140 | requires(Number.isInteger(from)); 141 | requires(Number.isInteger(to)); 142 | requires(from >= 0); 143 | requires(from < this.length); 144 | requires(to >= from); 145 | requires(to < this.length); 146 | ensures(pure()); 147 | return [ 148 | '_builtin_', 149 | '(jsstr (str.substr (strv (ite (is-jsstr ', this, ') ', this, ' (String-_str_ ', this, '))) ', 150 | '(intv ', from, ') (intv ', to - from, ')))']; 151 | } 152 | } 153 | 154 | class Array { 155 | invariant () { 156 | return this.length >= 0; 157 | } 158 | slice (from, to) { 159 | requires(Number.isInteger(from)); 160 | requires(Number.isInteger(to)); 161 | requires(from >= 0); 162 | requires(from < this.length); 163 | requires(to >= from); 164 | requires(to < this.length); 165 | ensures(y => y.every((ele, idx) => ele === this[idx + from])); 166 | ensures(y => y.length === to - from); 167 | ensures(pure()); 168 | } 169 | } 170 | 171 | const Math = { 172 | max: function (n, m) { 173 | requires(typeof n === 'number'); 174 | requires(typeof m === 'number'); 175 | ensures(pure()); 176 | ensures(z => z === (n >= m ? n : m)); 177 | }, 178 | random: function () { 179 | ensures(pure()); 180 | ensures(z => typeof z === 'number' && 0 <= z && z < 1.0); 181 | }, 182 | trunc: function (n) { 183 | requires(typeof n === 'number'); 184 | return [ '_builtin_', '(jsint (_toint ', n, '))']; 185 | } 186 | }; 187 | 188 | function parseInt (s, n) { 189 | requires(typeof s === 'string'); 190 | requires(n === 10); 191 | ensures(pure()); 192 | return [ '_builtin_', '(jsint (str.to.int (strv ', s, ')))']; 193 | } 194 | 195 | const console = { 196 | log: function (arg) { 197 | ensures(y => pure() && y === undefined); 198 | } 199 | }; 200 | 201 | function alert (arg) { 202 | ensures(y => pure() && y === undefined); 203 | }`; 204 | 205 | let cachedPreamble: Preamble | null = null; 206 | 207 | export function generatePreamble (): Preamble { 208 | if (cachedPreamble === null) { 209 | let preambleProgram: Syntax.Program = sourceAsJavaScript(VCGEN_PREAMBLE); 210 | resolveNames(preambleProgram, false); 211 | const vcgen = new PreambleGenrator(new Set(), 0, 0, new Set(), new Set(), [], [], true, tru); 212 | vcgen.visitProgram(preambleProgram); 213 | const { classes, heap, locs, vars, prop } = vcgen; 214 | cachedPreamble = { classes, heap, locs, vars, prop }; 215 | } 216 | return { 217 | classes: new Set([...cachedPreamble.classes]), 218 | heap: cachedPreamble.heap, 219 | locs: new Set([...cachedPreamble.locs]), 220 | vars: new Set([...cachedPreamble.vars]), 221 | prop: copy(cachedPreamble.prop) 222 | }; 223 | } 224 | -------------------------------------------------------------------------------- /src/qi.ts: -------------------------------------------------------------------------------- 1 | import { FreeVars, Heap, Heaps, Locs, P, Reducer, Substituter, Syntax, Transformer, Traverser, Vars, and, copy, 2 | eqExpr, eqHeap, eqProp, implies, tru } from './logic'; 3 | import { getOptions } from './options'; 4 | import { propositionToSMT } from './smt'; 5 | 6 | declare const console: { log: (s: string) => void }; 7 | 8 | class TriggerEraser extends Transformer { 9 | visitCallTrigger (prop: Syntax.CallTrigger): P { 10 | return tru; 11 | } 12 | 13 | visitAccessTrigger (prop: Syntax.AccessTrigger): P { 14 | return tru; 15 | } 16 | 17 | visitForAllCalls (prop: Syntax.ForAllCalls): P { 18 | // do not erase under quantifier -> leave unchanged 19 | return copy(prop); 20 | } 21 | 22 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): P { 23 | // do not erase under quantifier -> leave unchanged 24 | return copy(prop); 25 | } 26 | 27 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): P { 28 | // do not erase under quantifier -> leave unchanged 29 | return copy(prop); 30 | } 31 | } 32 | 33 | export function eraseTriggersProp (prop: P): P { 34 | const v = new TriggerEraser(); 35 | return v.visitProp(prop); 36 | } 37 | 38 | abstract class QuantifierTransformer extends Transformer { 39 | readonly heaps: Heaps; 40 | readonly locs: Locs; 41 | readonly vars: Vars; 42 | position: boolean; 43 | 44 | constructor (heaps: Heaps, locs: Locs, vars: Vars) { 45 | super(); 46 | this.heaps = heaps; 47 | this.locs = locs; 48 | this.vars = vars; 49 | this.position = true; 50 | } 51 | 52 | freshHeap (prefered: Heap): Heap { 53 | let n = prefered; 54 | while (this.heaps.has(n)) n++; 55 | this.heaps.add(n); 56 | return n; 57 | } 58 | 59 | freshLoc (prefered: Syntax.Location): Syntax.Location { 60 | let name = prefered; 61 | if (this.locs.has(name)) { 62 | const m = name.match(/(.*)_(\d+)$/); 63 | let idx = m ? +m[2] : 0; 64 | name = m ? m[1] : name; 65 | while (this.locs.has(`${name}_${idx}`)) idx++; 66 | name = `${name}_${idx}`; 67 | } 68 | this.locs.add(name); 69 | return name; 70 | } 71 | 72 | freshVar (prefered: Syntax.Variable): Syntax.Variable { 73 | let name = prefered; 74 | if (this.vars.has(name)) { 75 | const m = name.match(/(.*)_(\d+)$/); 76 | let idx = m ? +m[2] : 0; 77 | name = m ? m[1] : name; 78 | while (this.vars.has(`${name}_${idx}`)) idx++; 79 | name = `${name}_${idx}`; 80 | } 81 | this.vars.add(name); 82 | return name; 83 | } 84 | 85 | liftExistantials (prop: Syntax.ForAllCalls, newHeap: Syntax.HeapExpression = this.freshHeap(prop.heap)): Substituter { 86 | const sub = new Substituter(); 87 | sub.replaceHeap(prop.heap, newHeap); 88 | prop.existsHeaps.forEach(h => sub.replaceHeap(h, this.freshHeap(h))); 89 | prop.existsLocs.forEach(l => sub.replaceLoc(l, this.freshLoc(l))); 90 | prop.existsVars.forEach(v => sub.replaceVar(v, this.freshVar(v))); 91 | return sub; 92 | } 93 | 94 | visitNot (prop: Syntax.Not): P { 95 | this.position = !this.position; 96 | try { 97 | return super.visitNot(prop); 98 | } finally { 99 | this.position = !this.position; 100 | } 101 | } 102 | 103 | abstract visitForAllCalls (prop: Syntax.ForAllCalls): P; 104 | abstract visitForAllAccessObject (prop: Syntax.ForAllAccessObject): P; 105 | abstract visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): P; 106 | } 107 | 108 | class QuantifierLifter extends QuantifierTransformer { 109 | 110 | freeVars: FreeVars; 111 | 112 | constructor (heaps: Heaps, locs: Locs, vars: Vars, freeVars: FreeVars) { 113 | super(heaps, locs, vars); 114 | this.freeVars = freeVars; 115 | } 116 | 117 | visitForAllCalls (prop: Syntax.ForAllCalls): P { 118 | if (this.position) return copy(prop); 119 | if (prop.existsHeaps.size + prop.existsLocs.size + prop.existsVars.size > 0) { 120 | throw new Error('Existentials in negative positions not supported'); 121 | } 122 | const thisArg: string = this.freshVar(prop.thisArg); 123 | const trigger: P = { 124 | type: 'CallTrigger', 125 | callee: prop.callee, 126 | heap: prop.heap, 127 | thisArg: prop.thisArg, 128 | args: prop.args, 129 | fuel: prop.fuel 130 | }; 131 | const sub = this.liftExistantials(prop); 132 | sub.replaceVar(prop.thisArg, thisArg); 133 | const renamedVars: Array = []; 134 | prop.args.forEach(a => { 135 | const renamedVar = this.freshVar(a); 136 | renamedVars.push(renamedVar); 137 | this.freeVars.push(renamedVar); 138 | sub.replaceVar(a, renamedVar); 139 | }); 140 | prop.liftCallback(thisArg, renamedVars); 141 | return this.visitProp(sub.visitProp(implies(trigger, prop.prop))); 142 | } 143 | 144 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): P { 145 | if (this.position) return copy(prop); 146 | const trigger: P = { 147 | type: 'AccessTrigger', 148 | object: prop.thisArg, 149 | property: this.freshVar('prop'), 150 | heap: prop.heap, 151 | fuel: prop.fuel 152 | }; 153 | const sub = new Substituter(); 154 | sub.replaceVar(prop.thisArg, this.freshVar(prop.thisArg)); 155 | return this.visitProp(sub.visitProp(implies(trigger, prop.prop))); 156 | } 157 | 158 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): P { 159 | if (this.position) return copy(prop); 160 | const trigger: P = { 161 | type: 'AccessTrigger', 162 | object: prop.object, 163 | property: prop.property, 164 | heap: prop.heap, 165 | fuel: prop.fuel 166 | }; 167 | const sub = new Substituter(); 168 | sub.replaceVar(prop.property, this.freshVar(prop.property)); 169 | return this.visitProp(sub.visitProp(implies(trigger, prop.prop))); 170 | } 171 | } 172 | 173 | class MaximumDepthFinder extends Reducer { 174 | empty (): number { return 0; } 175 | 176 | reduce (a: number, b: number): number { 177 | return Math.max(a, b); 178 | } 179 | 180 | visitForAllCalls (prop: Syntax.ForAllCalls): number { 181 | return 1 + super.visitForAllCalls(prop); 182 | } 183 | 184 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): number { 185 | return 1 + super.visitForAllAccessObject(prop); 186 | } 187 | 188 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): number { 189 | return 1 + super.visitForAllAccessProperty(prop); 190 | } 191 | } 192 | 193 | class TriggerFueler extends Traverser { 194 | 195 | fuel: number = 0; 196 | 197 | visitCallTrigger (prop: Syntax.CallTrigger): void { 198 | super.visitCallTrigger(prop); 199 | prop.fuel = this.fuel; 200 | } 201 | 202 | visitForAllCalls (prop: Syntax.ForAllCalls): void { 203 | super.visitForAllCalls(prop); 204 | prop.fuel = this.fuel; 205 | } 206 | 207 | visitAccessTrigger (prop: Syntax.AccessTrigger): void { 208 | super.visitAccessTrigger(prop); 209 | prop.fuel = this.fuel; 210 | } 211 | 212 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): void { 213 | super.visitForAllAccessObject(prop); 214 | prop.fuel = this.fuel; 215 | } 216 | 217 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): void { 218 | super.visitForAllAccessProperty(prop); 219 | prop.fuel = this.fuel; 220 | } 221 | 222 | process (prop: P): P { 223 | this.fuel = (new MaximumDepthFinder()).visitProp(prop); 224 | this.visitProp(prop); 225 | return prop; 226 | } 227 | } 228 | 229 | type Triggers = [Array, Array]; 230 | 231 | class TriggerCollector extends Reducer { 232 | position: boolean; 233 | 234 | constructor (position: boolean) { 235 | super(); 236 | this.position = position; 237 | } 238 | 239 | empty (): Triggers { return [[], []]; } 240 | 241 | reduce (a: Triggers, b: Triggers): Triggers { 242 | return [a[0].concat(b[0]), a[1].concat(b[1])]; 243 | } 244 | 245 | visitNot (prop: Syntax.Not): Triggers { 246 | this.position = !this.position; 247 | try { 248 | return super.visitNot(prop); 249 | } finally { 250 | this.position = !this.position; 251 | } 252 | } 253 | 254 | visitCallTrigger (prop: Syntax.CallTrigger): Triggers { 255 | const res = super.visitCallTrigger(prop); 256 | return this.position && prop.fuel > 0 ? this.r([[prop],[]], res) : res; 257 | } 258 | 259 | visitAccessTrigger (prop: Syntax.AccessTrigger): Triggers { 260 | const res = super.visitAccessTrigger(prop); 261 | return this.position && prop.fuel > 0 ? this.r([[],[prop]], res) : res; 262 | } 263 | 264 | visitForAllCalls (prop: Syntax.ForAllCalls): Triggers { 265 | return this.visitExpr(prop.callee); // do not collect under quantifier 266 | } 267 | 268 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): Triggers { 269 | return this.empty(); // do not collect under quantifier 270 | } 271 | 272 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): Triggers { 273 | return this.empty(); // do not collect under quantifier 274 | } 275 | } 276 | 277 | class QuantifierInstantiator extends QuantifierTransformer { 278 | triggers: Triggers = [[], []]; 279 | instantiations: number; 280 | 281 | constructor (heaps: Heaps, locs: Locs, vars: Vars) { 282 | super(heaps, locs, vars); 283 | this.instantiations = 0; 284 | } 285 | 286 | consumeFuel (prop: P, prevFuel: number): P { 287 | const fueler = new TriggerFueler(); 288 | fueler.fuel = prevFuel - 1; 289 | fueler.visitProp(prop); 290 | return prop; 291 | } 292 | 293 | instantiateCall (prop: Syntax.ForAllCalls, trigger: Syntax.CallTrigger) { 294 | const sub = this.liftExistantials(prop, trigger.heap); 295 | // substitute arguments 296 | sub.replaceVar(prop.thisArg, trigger.thisArg); 297 | prop.args.forEach((a, idx) => { 298 | sub.replaceVar(a, trigger.args[idx]); 299 | }); 300 | const replaced = sub.visitProp(prop.prop); 301 | return this.consumeFuel(replaced, trigger.fuel); 302 | } 303 | 304 | instantiateAccessObject (prop: Syntax.ForAllAccessObject, trigger: Syntax.AccessTrigger) { 305 | const sub = new Substituter(); 306 | sub.replaceVar(prop.thisArg, trigger.object); 307 | sub.replaceHeap(prop.heap, trigger.heap); 308 | const replaced = sub.visitProp(prop.prop); 309 | return this.consumeFuel(replaced, trigger.fuel); 310 | } 311 | 312 | instantiateAccessProperty (prop: Syntax.ForAllAccessProperty, trigger: Syntax.AccessTrigger) { 313 | const sub = new Substituter(); 314 | sub.replaceVar(prop.property, trigger.property); 315 | sub.replaceHeap(prop.heap, trigger.heap); 316 | const replaced = sub.visitProp(prop.prop); 317 | return this.consumeFuel(replaced, trigger.fuel); 318 | } 319 | 320 | visitForAllCalls (prop: Syntax.ForAllCalls): P { 321 | if (!this.position) return copy(prop); 322 | const clauses: Array

= [prop]; 323 | for (const t of this.triggers[0]) { 324 | // continue if already instantiated with same trigger 325 | if (prop.args.length !== t.args.length || prop.instantiations.some(trigger => eqProp(t, trigger))) { 326 | continue; 327 | } 328 | const instantiated: P = this.instantiateCall(prop, t); 329 | clauses.push(instantiated); 330 | prop.instantiations.push(t); 331 | this.instantiations++; 332 | if (getOptions().verbose && !getOptions().quiet) { 333 | console.log('trigger: ' + propositionToSMT(t)); 334 | } 335 | } 336 | return and(...clauses); 337 | } 338 | 339 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): P { 340 | if (!this.position) return copy(prop); 341 | const clauses: Array

= [prop]; 342 | for (const t of this.triggers[1]) { 343 | // continue if already instantiated with same trigger, ignoring trigger.property 344 | if (prop.instantiations.some(trigger => eqHeap(t.heap, trigger.heap) && eqExpr(t.object, trigger.object))) { 345 | continue; 346 | } 347 | const instantiated: P = this.instantiateAccessObject(prop, t); 348 | clauses.push(instantiated); 349 | prop.instantiations.push(t); 350 | this.instantiations++; 351 | if (getOptions().verbose && !getOptions().quiet) { 352 | console.log('trigger: ' + propositionToSMT(t)); 353 | } 354 | } 355 | return and(...clauses); 356 | } 357 | 358 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): P { 359 | if (!this.position) return copy(prop); 360 | const clauses: Array

= [prop]; 361 | for (const t of this.triggers[1]) { 362 | // continue if already instantiated with same trigger 363 | if (prop.instantiations.some(trigger => eqProp(t, trigger))) { 364 | continue; 365 | } 366 | const instantiated: P = this.instantiateAccessProperty(prop, t); 367 | clauses.push(instantiated); 368 | prop.instantiations.push(t); 369 | this.instantiations++; 370 | if (getOptions().verbose && !getOptions().quiet) { 371 | console.log('trigger: ' + propositionToSMT(t)); 372 | } 373 | } 374 | return and(...clauses); 375 | } 376 | 377 | process (prop: P) { 378 | this.triggers = (new TriggerCollector(true)).visitProp(prop); 379 | return this.visitProp(prop); 380 | } 381 | } 382 | 383 | class QuantifierEraser extends Transformer { 384 | visitCallTrigger (prop: Syntax.CallTrigger): P { 385 | return tru; 386 | } 387 | 388 | visitForAllCalls (prop: Syntax.ForAllCalls): P { 389 | return tru; 390 | } 391 | 392 | visitAccessTrigger (prop: Syntax.AccessTrigger): P { 393 | return tru; 394 | } 395 | 396 | visitForAllAccessObject (prop: Syntax.ForAllAccessObject): P { 397 | return tru; 398 | } 399 | 400 | visitForAllAccessProperty (prop: Syntax.ForAllAccessProperty): P { 401 | return tru; 402 | } 403 | } 404 | 405 | export function instantiateQuantifiers (heaps: Heaps, locs: Locs, vars: Vars, freeVars: FreeVars, p: P): P { 406 | const initialFuel = new TriggerFueler(); 407 | const lifter = new QuantifierLifter(heaps, locs, vars, freeVars); 408 | const instantiator = new QuantifierInstantiator(heaps, locs, vars); 409 | let prop = initialFuel.process(lifter.visitProp(p)); 410 | let num = -1; 411 | while (instantiator.instantiations > num) { 412 | num = instantiator.instantiations; 413 | prop = instantiator.process(prop); 414 | prop = lifter.visitProp(prop); 415 | } 416 | prop = (new QuantifierEraser()).visitProp(prop); 417 | return prop; 418 | } 419 | -------------------------------------------------------------------------------- /src/scopes.ts: -------------------------------------------------------------------------------- 1 | import { Syntax, Visitor, id } from './javascript'; 2 | import { MessageException, unexpected } from './message'; 3 | import { globalDeclarations } from './preamble'; 4 | 5 | function unsupportedLoc (loc: Syntax.SourceLocation, description: string = '') { 6 | return new MessageException({ status: 'error', type: 'unsupported', loc, description }); 7 | } 8 | 9 | function undefinedId (loc: Syntax.SourceLocation) { 10 | return new MessageException({ status: 'error', type: 'undefined-identifier', loc, description: '' }); 11 | } 12 | 13 | function alreadyDefined (loc: Syntax.SourceLocation, decl: Syntax.Declaration) { 14 | if (decl.type === 'Unresolved') { 15 | throw unexpected(new Error('decl should be resolved')); 16 | } 17 | const { file, start } = decl.decl.loc; 18 | return new MessageException({ 19 | status: 'error', 20 | type: 'already-defined', 21 | loc, 22 | description: `at ${file}:${start.line}:${start.column}` 23 | }); 24 | } 25 | 26 | function assignToConst (loc: Syntax.SourceLocation) { 27 | return new MessageException({ status: 'error', type: 'assignment-to-const', loc, description: '' }); 28 | } 29 | 30 | function refInInvariant (loc: Syntax.SourceLocation) { 31 | return new MessageException({ status: 'error', type: 'reference-in-invariant', loc, description: '' }); 32 | } 33 | 34 | export function isMutable (idOrDecl: Syntax.Identifier | Syntax.Declaration): boolean { 35 | const decl = idOrDecl.type === 'Identifier' ? idOrDecl.decl : idOrDecl; 36 | return decl.type === 'Var' && decl.decl.kind === 'let'; 37 | } 38 | 39 | class Scope { 40 | 41 | funcOrLoop: Syntax.Function | Syntax.WhileStatement | null; 42 | ids: { [varname: string]: Syntax.Declaration } = {}; 43 | parent: Scope | null; 44 | 45 | constructor (parent: Scope | null = null, fw: Syntax.Function | Syntax.WhileStatement | null = null) { 46 | this.parent = parent; 47 | this.funcOrLoop = fw; 48 | } 49 | 50 | lookupDef (sym: Syntax.Identifier) { 51 | if (sym.name in this.ids) throw alreadyDefined(sym.loc, this.ids[sym.name]); 52 | if (this.parent) this.parent.lookupDef(sym); 53 | } 54 | 55 | defSymbol (sym: Syntax.Identifier, decl: Syntax.Declaration) { 56 | // TODO enable shadowing 57 | this.lookupDef(sym); 58 | this.ids[sym.name] = decl; 59 | } 60 | 61 | lookupUse (sym: Syntax.Identifier, clz: boolean): Syntax.Declaration { 62 | let decl: Syntax.Declaration | null = null; 63 | if (sym.name in this.ids) { 64 | decl = this.ids[sym.name]; 65 | } else if (this.parent) { 66 | decl = this.parent.lookupUse(sym, clz); 67 | if (this.funcOrLoop && !this.funcOrLoop.freeVars.some(fv => fv.name === sym.name) && isMutable(decl)) { 68 | this.funcOrLoop.freeVars.push(sym); // a free variable 69 | } 70 | } 71 | if (!decl || decl.type === 'Unresolved') { 72 | throw undefinedId(sym.loc); 73 | } 74 | if (clz && (decl.type !== 'Class')) throw unsupportedLoc(sym.loc, 'expected class'); 75 | if (!clz && (decl.type === 'Class')) throw unsupportedLoc(sym.loc, 'did not expect class'); 76 | return decl; 77 | } 78 | 79 | useSymbol (sym: Syntax.Identifier, write: boolean = false, clz: boolean = false, allowRef: boolean = true) { 80 | const decl = this.lookupUse(sym, clz); 81 | sym.decl = decl; 82 | switch (decl.type) { 83 | case 'Var': 84 | decl.decl.id.refs.push(sym); 85 | if (!allowRef) { 86 | throw refInInvariant(sym.loc); 87 | } 88 | if (write) { 89 | if (decl.decl.kind === 'const') { 90 | throw assignToConst(sym.loc); 91 | } 92 | decl.decl.id.isWrittenTo = true; 93 | } 94 | break; 95 | case 'Func': 96 | if (!decl.decl.id) throw unsupportedLoc(sym.loc, 'function should have name'); 97 | decl.decl.id.refs.push(sym); 98 | if (write) { 99 | throw assignToConst(sym.loc); 100 | } 101 | break; 102 | case 'Param': 103 | decl.decl.refs.push(sym); 104 | if (write) { 105 | throw assignToConst(sym.loc); 106 | } 107 | break; 108 | case 'Class': 109 | if (write) { 110 | throw assignToConst(sym.loc); 111 | } 112 | break; 113 | } 114 | } 115 | } 116 | 117 | class NameResolver extends Visitor { 118 | 119 | scope: Scope = new Scope(); 120 | allowOld: boolean = false; 121 | allowRef: boolean = true; 122 | 123 | scoped (action: () => void, allowsOld: boolean = this.allowOld, allowsRef: boolean = this.allowRef, 124 | fn: Syntax.Function | Syntax.WhileStatement | null = this.scope.funcOrLoop) { 125 | const { scope, allowOld, allowRef } = this; 126 | try { 127 | this.scope = new Scope(scope, fn); 128 | this.allowOld = allowsOld; 129 | this.allowRef = allowsRef; 130 | action(); 131 | } finally { 132 | this.scope = scope; 133 | this.allowOld = allowOld; 134 | this.allowRef = allowRef; 135 | } 136 | } 137 | 138 | visitIdentifierTerm (term: Syntax.Identifier) { 139 | this.scope.useSymbol(term, false, false, this.allowRef); 140 | } 141 | 142 | visitOldIdentifierTerm (term: Syntax.OldIdentifier) { 143 | if (!this.allowOld) throw unsupportedLoc(term.loc, 'old() not allowed in this context'); 144 | this.scope.useSymbol(term.id); 145 | } 146 | 147 | visitLiteralTerm (term: Syntax.Literal) { /* empty */ } 148 | 149 | visitUnaryTerm (term: Syntax.UnaryTerm) { 150 | this.visitTerm(term.argument); 151 | } 152 | 153 | visitBinaryTerm (term: Syntax.BinaryTerm) { 154 | this.visitTerm(term.left); 155 | this.visitTerm(term.right); 156 | } 157 | 158 | visitLogicalTerm (term: Syntax.LogicalTerm) { 159 | this.visitTerm(term.left); 160 | this.visitTerm(term.right); 161 | } 162 | 163 | visitConditionalTerm (term: Syntax.ConditionalTerm) { 164 | this.visitTerm(term.test); 165 | this.visitTerm(term.consequent); 166 | this.visitTerm(term.alternate); 167 | } 168 | 169 | visitCallTerm (term: Syntax.CallTerm) { 170 | term.args.forEach(e => this.visitTerm(e)); 171 | this.visitTerm(term.callee); 172 | } 173 | 174 | visitMemberTerm (term: Syntax.MemberTerm) { 175 | this.visitTerm(term.object); 176 | this.visitTerm(term.property); 177 | } 178 | 179 | visitIsIntegerTerm (term: Syntax.IsIntegerTerm) { 180 | this.visitTerm(term.term); 181 | } 182 | 183 | visitToIntegerTerm (term: Syntax.ToIntegerTerm) { 184 | this.visitTerm(term.term); 185 | } 186 | 187 | visitTermAssertion (term: Syntax.Term) { 188 | this.visitTerm(term); 189 | } 190 | 191 | visitPureAssertion (assertion: Syntax.PureAssertion) { /* empty */ } 192 | 193 | visitPostCondition (post: Syntax.PostCondition) { 194 | if (post.argument) { 195 | // scoped at the surrounding context (spec or function body) 196 | this.scope.defSymbol(post.argument, { type: 'PostArg', decl: post }); 197 | } 198 | this.visitAssertion(post.expression); 199 | } 200 | 201 | visitSpecAssertion (assertion: Syntax.SpecAssertion) { 202 | this.visitTerm(assertion.callee); 203 | this.scoped(() => { 204 | assertion.args.forEach((a, argIdx) => { 205 | this.scope.defSymbol(id(a), { type: 'SpecArg', decl: assertion, argIdx }); 206 | }); 207 | this.visitAssertion(assertion.pre); 208 | }, false); 209 | this.scoped(() => { 210 | assertion.args.forEach((a, argIdx) => { 211 | this.scope.defSymbol(id(a), { type: 'SpecArg', decl: assertion, argIdx }); 212 | }); 213 | this.visitPostCondition(assertion.post); 214 | }, true); 215 | } 216 | 217 | visitEveryAssertion (assertion: Syntax.EveryAssertion) { 218 | this.visitTerm(assertion.array); 219 | this.scoped(() => { 220 | this.scope.defSymbol(assertion.argument, { type: 'EveryArg', decl: assertion }); 221 | if (assertion.indexArgument !== null) { 222 | this.scope.defSymbol(assertion.indexArgument, { type: 'EveryIdxArg', decl: assertion }); 223 | } 224 | this.visitAssertion(assertion.expression); 225 | }, false); 226 | } 227 | 228 | visitInstanceOfAssertion (assertion: Syntax.InstanceOfAssertion) { 229 | this.visitTerm(assertion.left); 230 | this.scope.useSymbol(assertion.right, false, true); 231 | } 232 | 233 | visitInAssertion (assertion: Syntax.InAssertion) { 234 | this.visitTerm(assertion.property); 235 | this.visitTerm(assertion.object); 236 | } 237 | 238 | visitUnaryAssertion (assertion: Syntax.UnaryAssertion) { 239 | this.visitAssertion(assertion.argument); 240 | } 241 | 242 | visitBinaryAssertion (assertion: Syntax.BinaryAssertion) { 243 | this.visitAssertion(assertion.left); 244 | this.visitAssertion(assertion.right); 245 | } 246 | 247 | visitIdentifier (expr: Syntax.Identifier) { 248 | this.scope.useSymbol(expr, false, false, this.allowRef); 249 | } 250 | 251 | visitLiteral (expr: Syntax.Literal) { /* empty */ } 252 | 253 | visitUnaryExpression (expr: Syntax.UnaryExpression) { 254 | this.visitExpression(expr.argument); 255 | } 256 | 257 | visitBinaryExpression (expr: Syntax.BinaryExpression) { 258 | this.visitExpression(expr.left); 259 | this.visitExpression(expr.right); 260 | } 261 | 262 | visitLogicalExpression (expr: Syntax.LogicalExpression) { 263 | this.visitExpression(expr.left); 264 | this.visitExpression(expr.right); 265 | } 266 | 267 | visitConditionalExpression (expr: Syntax.ConditionalExpression) { 268 | this.visitExpression(expr.test); 269 | this.visitExpression(expr.consequent); 270 | this.visitExpression(expr.alternate); 271 | } 272 | 273 | visitAssignmentExpression (expr: Syntax.AssignmentExpression) { 274 | this.visitExpression(expr.right); 275 | if (expr.left.type !== 'Identifier') throw unsupportedLoc(expr.loc); 276 | this.scope.useSymbol(expr.left, true); 277 | } 278 | 279 | visitSequenceExpression (expr: Syntax.SequenceExpression) { 280 | expr.expressions.forEach(e => this.visitExpression(e)); 281 | } 282 | 283 | visitCallExpression (expr: Syntax.CallExpression) { 284 | expr.args.forEach(e => this.visitExpression(e)); 285 | this.visitExpression(expr.callee); 286 | } 287 | 288 | visitNewExpression (expr: Syntax.NewExpression) { 289 | this.scope.useSymbol(expr.callee, false, true); 290 | expr.args.forEach(e => this.visitExpression(e)); 291 | } 292 | 293 | visitArrayExpression (expr: Syntax.ArrayExpression) { 294 | expr.elements.forEach(e => this.visitExpression(e)); 295 | } 296 | 297 | visitObjectExpression (expr: Syntax.ObjectExpression) { 298 | expr.properties.forEach(p => this.visitExpression(p.value)); 299 | } 300 | 301 | visitInstanceOfExpression (expr: Syntax.InstanceOfExpression) { 302 | this.visitExpression(expr.left); 303 | this.scope.useSymbol(expr.right, false, true); 304 | } 305 | 306 | visitInExpression (expr: Syntax.InExpression) { 307 | this.visitExpression(expr.property); 308 | this.visitExpression(expr.object); 309 | } 310 | 311 | visitMemberExpression (expr: Syntax.MemberExpression) { 312 | this.visitExpression(expr.object); 313 | this.visitExpression(expr.property); 314 | } 315 | 316 | visitFunctionExpression (expr: Syntax.FunctionExpression) { 317 | this.scoped(() => { 318 | if (expr.id) this.scope.defSymbol(expr.id, { type: 'Func', decl: expr }); 319 | expr.params.forEach(p => this.scope.defSymbol(p, { type: 'Param', func: expr, decl: p })); 320 | expr.requires.forEach(r => this.visitAssertion(r)); 321 | expr.ensures.forEach(s => { 322 | this.scoped(() => this.visitPostCondition(s), true); 323 | }); 324 | expr.body.body.forEach(s => this.visitStatement(s)); 325 | }, false, this.allowRef, expr); 326 | } 327 | 328 | visitVariableDeclaration (stmt: Syntax.VariableDeclaration) { 329 | this.scope.defSymbol(stmt.id, { type: 'Var', decl: stmt }); 330 | this.visitExpression(stmt.init); 331 | } 332 | 333 | visitBlockStatement (stmt: Syntax.BlockStatement) { 334 | this.scoped(() => { 335 | stmt.body.forEach(s => this.visitStatement(s)); 336 | }); 337 | } 338 | 339 | visitExpressionStatement (stmt: Syntax.ExpressionStatement) { 340 | this.visitExpression(stmt.expression); 341 | } 342 | 343 | visitAssertStatement (stmt: Syntax.AssertStatement) { 344 | this.visitAssertion(stmt.expression); 345 | } 346 | 347 | visitIfStatement (stmt: Syntax.IfStatement) { 348 | this.visitExpression(stmt.test); 349 | this.scoped(() => { 350 | stmt.consequent.body.forEach(s => this.visitStatement(s)); 351 | }); 352 | this.scoped(() => { 353 | stmt.alternate.body.forEach(s => this.visitStatement(s)); 354 | }); 355 | } 356 | 357 | visitReturnStatement (stmt: Syntax.ReturnStatement) { 358 | this.visitExpression(stmt.argument); 359 | } 360 | 361 | visitWhileStatement (stmt: Syntax.WhileStatement) { 362 | this.scoped(() => { 363 | this.visitExpression(stmt.test); 364 | stmt.invariants.forEach(i => this.visitAssertion(i)); 365 | stmt.body.body.forEach(s => this.visitStatement(s)); 366 | }, false, true, stmt); 367 | } 368 | 369 | visitDebuggerStatement (stmt: Syntax.DebuggerStatement) { /* empty */ } 370 | 371 | visitFunctionDeclaration (stmt: Syntax.FunctionDeclaration) { 372 | this.scope.defSymbol(stmt.id, { type: 'Func', decl: stmt }); 373 | this.scoped(() => { 374 | stmt.params.forEach(p => this.scope.defSymbol(p, { type: 'Param', func: stmt, decl: p })); 375 | stmt.requires.forEach(r => this.visitAssertion(r)); 376 | stmt.ensures.forEach(s => { 377 | this.scoped(() => this.visitPostCondition(s), true); 378 | }); 379 | stmt.body.body.forEach(s => this.visitStatement(s)); 380 | }, false, true, stmt); 381 | } 382 | 383 | visitMethodDeclaration (stmt: Syntax.MethodDeclaration, cls: Syntax.ClassDeclaration) { 384 | this.scoped(() => { 385 | this.scope.defSymbol(id('this'), { type: 'This', decl: cls }); 386 | stmt.params.forEach(p => this.scope.defSymbol(p, { type: 'Param', func: stmt, decl: p })); 387 | stmt.requires.forEach(r => this.visitAssertion(r)); 388 | stmt.ensures.forEach(s => { 389 | this.scoped(() => this.visitPostCondition(s), true); 390 | }); 391 | stmt.body.body.forEach(s => this.visitStatement(s)); 392 | }, false, true, stmt); 393 | } 394 | 395 | visitClassDeclaration (stmt: Syntax.ClassDeclaration) { 396 | this.scope.defSymbol(stmt.id, { type: 'Class', decl: stmt }); 397 | this.scoped(() => { 398 | this.scope.defSymbol(id('this'), { type: 'This', decl: stmt }); 399 | this.visitAssertion(stmt.invariant); 400 | }, false, false); 401 | stmt.methods.forEach(method => this.visitMethodDeclaration(method, stmt)); 402 | } 403 | 404 | visitPreamble () { 405 | for (const decl of globalDeclarations()) { 406 | this.scope.defSymbol(decl.decl.id, decl); 407 | } 408 | } 409 | 410 | visitProgram (prog: Syntax.Program) { 411 | prog.body.forEach(stmt => this.visitStatement(stmt)); 412 | prog.invariants.forEach(inv => this.visitAssertion(inv)); 413 | } 414 | } 415 | 416 | export function resolveNames (program: Syntax.Program, preamble: boolean = true): void { 417 | const resolver = new NameResolver(); 418 | if (preamble) { 419 | resolver.visitPreamble(); 420 | } 421 | resolver.visitProgram(program); 422 | } 423 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export type SExpr = string | SExprList; 2 | export interface SExprList extends Array {} 3 | 4 | export function flatMap (a: ReadonlyArray, f: (a: A) => ReadonlyArray): Array { 5 | return a.map(f).reduce((a: Array, b: ReadonlyArray): Array => a.concat(b), []); 6 | } 7 | 8 | export function parseSExpr (input: string): SExpr { 9 | let idx = 0; 10 | 11 | function skipWS () { 12 | while (true) { 13 | if (input[idx] === ';') { 14 | while (input[idx] !== '\n') idx++; 15 | } 16 | if (input[idx] !== ' ' && input[idx] !== '\t' && input[idx] !== '\n') break; 17 | idx++; 18 | } 19 | } 20 | 21 | function sexpr (): SExpr | null { 22 | skipWS(); 23 | if (input[idx] === '(') { 24 | idx++; 25 | const list: SExprList = []; 26 | for (let next = sexpr(); next !== null; next = sexpr()) { 27 | list.push(next); 28 | } 29 | skipWS(); 30 | if (input[idx++] !== ')') { 31 | throw new Error(`expected s-expression: ${input.substring(idx - 10, idx + 10)}`); 32 | } 33 | return list; 34 | } 35 | const m = input.substr(idx).match(/^("[^"]*")|^[a-zA-Z0-9_\-!#=.\/]+/); 36 | if (m) { 37 | idx += m[0].length; 38 | return m[0]; 39 | } 40 | return null; 41 | } 42 | 43 | const result = sexpr(); 44 | if (result === null) { 45 | throw new Error(`expected s-expression: ${input}`); 46 | } else { 47 | return result; 48 | } 49 | } 50 | 51 | export type SExprPattern = string | { name: string } | { group: string } | { expr: string } | SExprPatternList; 52 | export interface SExprPatternList extends Array {} 53 | 54 | export function matchSExpr (expr: SExpr, pattern: SExprPattern): { [name: string]: SExpr } | null { 55 | const bindings: { [name: string]: SExpr } = {}; 56 | function process (expr: SExpr, pattern: SExprPattern): boolean { 57 | if (typeof pattern === 'string') { 58 | if (expr !== pattern) return false; 59 | } else if (pattern instanceof Array) { 60 | if (!(expr instanceof Array) || pattern.length !== expr.length) return false; 61 | for (let i = 0; i < pattern.length; i++) { 62 | if (!process(expr[i], pattern[i])) return false; 63 | } 64 | } else if ('name' in pattern) { 65 | if (typeof expr !== 'string') return false; 66 | bindings[pattern.name] = expr; 67 | } else if ('group' in pattern) { 68 | if (!(expr instanceof Array)) return false; 69 | bindings[pattern.group] = expr; 70 | } else { 71 | bindings[pattern.expr] = expr; 72 | } 73 | return true; 74 | } 75 | return process(expr, pattern) ? bindings : null; 76 | } 77 | -------------------------------------------------------------------------------- /src/verification.ts: -------------------------------------------------------------------------------- 1 | import { stringifyTestCode } from './codegen'; 2 | import { Substituter, Syntax, nullLoc, TestCode, eqSourceLocation, compEndPosition, id, 3 | replaceVarAssertion, replaceVarExpr } from './javascript'; 4 | import { Classes, FreeVars, Heap, Heaps, Locs, P, Vars, not, and } from './logic'; 5 | import { Message, MessageException, unexpected } from './message'; 6 | import { Model, valueToJavaScript, JSVal } from './model'; 7 | import { getOptions } from './options'; 8 | import { SMTInput, SMTOutput, vcToSMT } from './smt'; 9 | import { sourceAsJavaScriptAssertion, sourceAsJavaScriptExpression } from './parser'; 10 | import { VCGenerator } from './vcgen'; 11 | import { Interpreter, interpret } from './interpreter'; 12 | import { generatePreamble } from './preamble'; 13 | 14 | export type Assumption = Readonly<{source: string, prop: P, canBeDeleted: boolean}>; 15 | 16 | declare const console: { log: (s: string) => void }; 17 | declare const require: (s: string) => any; 18 | declare const fetch: (s: string, opts: any) => Promise; 19 | 20 | let checkedLocalZ3Version: boolean = false; 21 | 22 | export default class VerificationCondition { 23 | private classes: Classes; 24 | private heaps: Heaps; 25 | private locs: Locs; 26 | private vars: Vars; 27 | private prop: P; 28 | private assumptions: Array; 29 | private assertion: P; 30 | private loc: Syntax.SourceLocation; 31 | private freeVars: FreeVars; 32 | private testBody: TestCode; 33 | private testAssertion: TestCode; 34 | private description: string; 35 | private heapHints: Array<[Syntax.SourceLocation, Heap]>; 36 | private aliases: { [from: string]: string }; 37 | private watches: Array<[string, Syntax.Expression]>; 38 | 39 | private model: Model | null; 40 | private interpreter: Interpreter | null; 41 | private result: Message | null; 42 | 43 | constructor (classes: Classes, heap: Heap, locs: Locs, vars: Vars, prop: P, assumptions: Array, 44 | assertion: P, loc: Syntax.SourceLocation, description: string, freeVars: FreeVars, 45 | testBody: TestCode, testAssertion: TestCode, heapHints: Array<[Syntax.SourceLocation, Heap]>, 46 | aliases: { [from: string]: string }) { 47 | this.classes = new Set([...classes]); 48 | this.heaps = new Set([...Array(heap + 1).keys()]); 49 | this.locs = new Set([...locs]); 50 | this.vars = new Set([...vars]); 51 | this.prop = prop; 52 | this.assumptions = assumptions; 53 | this.assertion = assertion; 54 | this.loc = loc; 55 | this.description = description; 56 | this.freeVars = [...freeVars]; 57 | this.testBody = testBody; 58 | this.testAssertion = testAssertion; 59 | this.heapHints = heapHints; 60 | this.aliases = aliases; 61 | this.watches = []; 62 | 63 | this.model = null; 64 | this.interpreter = null; 65 | this.result = null; 66 | } 67 | 68 | async verify (): Promise { 69 | try { 70 | this.model = null; 71 | this.interpreter = null; 72 | this.result = null; 73 | const smtin = this.prepareSMT(); 74 | const smtout = await (getOptions().remote ? this.solveRemote(smtin) : this.solveLocal(smtin)); 75 | const modelOrMessage = this.processSMTOutput(smtout); 76 | if (modelOrMessage instanceof Model) { 77 | this.model = modelOrMessage; 78 | return this.result = this.runTest(); 79 | } else { 80 | return this.result = modelOrMessage; 81 | } 82 | } catch (error) { 83 | if (error instanceof MessageException) return this.result = error.msg; 84 | return this.result = unexpected(error, this.loc, this.description); 85 | } 86 | } 87 | 88 | getDescription (): string { 89 | return this.description; 90 | } 91 | 92 | getLocation (): Syntax.SourceLocation { 93 | return this.loc; 94 | } 95 | 96 | setDescription (description: string): void { 97 | this.description = description; 98 | } 99 | 100 | hasModel (): boolean { 101 | return this.model !== null; 102 | } 103 | 104 | runTest (): Message { 105 | const code = this.testSource(); 106 | try { 107 | /* tslint:disable:no-eval */ 108 | eval(code); 109 | return { status: 'unverified', description: this.description, loc: this.loc, model: this.getModel() }; 110 | } catch (e) { 111 | if (e instanceof Error && (e instanceof TypeError || e.message === 'assertion failed')) { 112 | return { 113 | status: 'error', 114 | type: 'incorrect', 115 | description: this.description, 116 | loc: this.loc, 117 | model: this.getModel(), 118 | error: e 119 | }; 120 | } else { 121 | return unexpected(e, this.loc, this.description); 122 | } 123 | } 124 | } 125 | 126 | runWithInterpreter (): Message { 127 | const interpreter = this.getInterpreter(); 128 | try { 129 | interpreter.run(); 130 | this.result = { status: 'unverified', description: this.description, loc: this.loc, model: this.getModel() }; 131 | return this.result; 132 | } catch (e) { 133 | if (e instanceof Error && (e instanceof TypeError || e.message === 'assertion failed')) { 134 | return this.result = { 135 | status: 'error', 136 | type: 'incorrect', 137 | description: this.description, 138 | loc: this.loc, 139 | model: this.getModel(), 140 | error: e 141 | }; 142 | } else { 143 | return this.result = unexpected(e, this.loc, this.description); 144 | } 145 | } 146 | } 147 | 148 | addAssumption (source: string): void { 149 | let assumption = sourceAsJavaScriptAssertion(source); 150 | for (const aliasedVar in this.aliases) { 151 | const replacement = this.aliases[aliasedVar]; 152 | assumption = replaceVarAssertion(aliasedVar, id(replacement), id(replacement), assumption); 153 | } 154 | const maxHeap = Math.max(...this.heaps.values()); 155 | const assumptions = this.assumptions.map(({ source }) => source); 156 | const vcgen = new VCGenerator(new Set([...this.classes]), maxHeap, maxHeap, 157 | new Set([...this.locs]), new Set([...this.vars]), assumptions, this.heapHints, 158 | true, this.prop); 159 | const [assumptionP] = vcgen.assume(assumption); 160 | this.assumptions = this.assumptions.concat([{ source, prop: assumptionP, canBeDeleted: true }]); 161 | } 162 | 163 | getAssumptions (): Array<[string, boolean]> { 164 | return this.assumptions.map(({ source, canBeDeleted }): [string, boolean] => [source, canBeDeleted]); 165 | } 166 | 167 | removeAssumption (idx: number): void { 168 | const assumptionToRemove = idx < this.assumptions.length ? this.assumptions[idx] : undefined; 169 | if (assumptionToRemove === undefined) throw new Error('no such assumption'); 170 | if (!assumptionToRemove.canBeDeleted) throw new Error('cannot remove built-in assumptions'); 171 | this.assumptions = this.assumptions.filter(a => a !== assumptionToRemove); 172 | } 173 | 174 | assert (source: string): VerificationCondition { 175 | let assertion = sourceAsJavaScriptAssertion(source); 176 | for (const aliasedVar in this.aliases) { 177 | const replacement = this.aliases[aliasedVar]; 178 | assertion = replaceVarAssertion(aliasedVar, id(replacement), id(replacement), assertion); 179 | } 180 | const maxHeap = Math.max(...this.heaps.values()); 181 | const assumptions = this.assumptions.map(({ source }) => source); 182 | const vcgen = new VCGenerator(new Set([...this.classes]), maxHeap, maxHeap, 183 | new Set([...this.locs]), new Set([...this.vars]), assumptions, 184 | this.heapHints, true, this.prop); 185 | const [assertionP, , assertionT] = vcgen.assert(assertion); 186 | return new VerificationCondition(this.classes, maxHeap, this.locs, this.vars, this.prop, this.assumptions, 187 | assertionP, this.loc, source, this.freeVars, this.testBody, assertionT, 188 | this.heapHints, this.aliases); 189 | } 190 | 191 | steps (): number { 192 | return this.getInterpreter().steps; 193 | } 194 | 195 | pc (): Syntax.SourceLocation { 196 | return this.getInterpreter().loc(); 197 | } 198 | 199 | iteration (): number { 200 | return this.getInterpreter().iteration(); 201 | } 202 | 203 | callstack (): Array<[string, Syntax.SourceLocation, number]> { 204 | return this.getInterpreter().callstack(); 205 | } 206 | 207 | getScopes (frameIndex: number): Array> { 208 | const heap = this.guessCurrentHeap(); 209 | return this.getInterpreter().scopes(frameIndex).map(scope => 210 | scope.map(([varname, dynamicValue]): [string, JSVal, JSVal | undefined] => { 211 | const staticValue = this.modelValue(varname, heap); 212 | return [varname, this.getInterpreter().asValue(dynamicValue), staticValue]; 213 | }) 214 | ); 215 | } 216 | 217 | getWatches (): Array<[string, JSVal | undefined, JSVal | undefined]> { 218 | return this.watches.map(([src, expr]): [string, JSVal | undefined, JSVal | undefined] => { 219 | let dynamicValue: JSVal | undefined = undefined; 220 | let staticValue: JSVal | undefined = undefined; 221 | try { 222 | dynamicValue = this.getInterpreter().asValue(this.getInterpreter().evalExpression(expr, [])); 223 | staticValue = this.getInterpreter().asValue( 224 | this.getInterpreter().evalExpression(expr, this.currentBindingsFromModel())); 225 | } catch (e) { /* ignore errors */ } 226 | return [src, dynamicValue, staticValue]; 227 | }); 228 | } 229 | 230 | addWatch (source: string): void { 231 | let expr = sourceAsJavaScriptExpression(source); 232 | for (const aliasedVar in this.aliases) { 233 | const replacement = this.aliases[aliasedVar]; 234 | expr = replaceVarExpr(aliasedVar, id(replacement), id(replacement), expr); 235 | } 236 | this.watches.push([source, expr]); 237 | } 238 | 239 | removeWatch (idx: number): void { 240 | const watchToRemove = idx < this.watches.length ? this.watches[idx] : undefined; 241 | if (watchToRemove === undefined) throw new Error('no such watch'); 242 | this.watches = this.watches.filter(w => w !== watchToRemove); 243 | } 244 | 245 | restart (): void { 246 | this.getInterpreter().restart(); 247 | this.stepToSource(); 248 | } 249 | 250 | goto (pos: Syntax.Position, iteration: number = 0): void { 251 | this.getInterpreter().goto(pos, iteration); 252 | this.stepToSource(); 253 | } 254 | 255 | stepInto (): void { 256 | this.getInterpreter().stepInto(); 257 | this.stepToSource(); 258 | } 259 | 260 | stepOver (): void { 261 | this.getInterpreter().stepOver(); 262 | this.stepToSource(); 263 | } 264 | 265 | stepOut (): void { 266 | this.getInterpreter().stepOut(); 267 | this.stepToSource(); 268 | } 269 | 270 | getAnnotations (): Array<[Syntax.SourceLocation, Array, JSVal | undefined]> { 271 | return this.getInterpreter().annotations 272 | .filter(annotation => annotation.location.file === getOptions().filename) 273 | .map((annotation): [Syntax.SourceLocation, Array, JSVal | undefined] => { 274 | const heap = this.guessCurrentHeap(annotation.location); 275 | const staticValue = this.modelValue(annotation.variableName, heap); 276 | return [ 277 | annotation.location, 278 | annotation.values.map((v: any): JSVal => this.getInterpreter().asValue(v)), 279 | staticValue 280 | ]; 281 | }); 282 | } 283 | 284 | getResult (): Message | null { 285 | return this.result; 286 | } 287 | 288 | private prepareSMT (): SMTInput { 289 | const prop = and(this.prop, ...this.assumptions.map(({ prop }) => prop), not(this.assertion)); 290 | const smt = vcToSMT(this.classes, this.heaps, this.locs, this.vars, this.freeVars, prop); 291 | if (getOptions().verbose) { 292 | console.log('SMT Input:'); 293 | console.log('------------'); 294 | console.log(smt); 295 | console.log('------------'); 296 | } 297 | return smt; 298 | } 299 | 300 | private solveLocal (smt: SMTInput): Promise { 301 | if (!getOptions().quiet && getOptions().verbose) { 302 | console.log(`${this.description}: solving locally with ${getOptions().z3path}`); 303 | } 304 | let p = Promise.resolve(''); 305 | if (!checkedLocalZ3Version) { 306 | p = p.then(() => new Promise((resolve, reject) => { 307 | const exec = require('child_process').exec; 308 | exec(getOptions().z3path + ' -version', (err: Error, out: string) => { 309 | if (err) { 310 | reject(new Error('cannot invoke z3: ' + String(err))); 311 | } else { 312 | const vstr = out.toString().match(/(\d+)\.(\d+)\.\d+/); 313 | if (!vstr || +vstr[1] !== 4 || +vstr[2] !== 6) { 314 | reject(new Error('esverify requires z3 verison 4.6')); 315 | } else { 316 | checkedLocalZ3Version = true; 317 | resolve(''); 318 | } 319 | } 320 | }); 321 | })); 322 | } 323 | if (!getOptions().quiet && getOptions().verbose) { 324 | p = p.then(() => new Promise((resolve, reject) => { 325 | const writeFile = require('fs').writeFile; 326 | writeFile(getOptions().logsmt, smt, (err: Error, out: string) => { 327 | if (err) { 328 | reject(new Error('cannot write: ' + String(err))); 329 | } else { 330 | resolve(''); 331 | } 332 | }); 333 | })); 334 | } 335 | p = p.then(() => new Promise((resolve, reject) => { 336 | const spawn = require('child_process').spawn; 337 | const p = spawn(getOptions().z3path, 338 | [`-T:${getOptions().timeout}`, '-smt2', '-in'], 339 | { stdio: ['pipe', 'pipe', 'ignore'] }); 340 | let result: string = ''; 341 | p.stdout.on('data', (data: Object) => { result += data.toString(); }); 342 | p.on('exit', (code: number) => { 343 | if (!getOptions().quiet && getOptions().verbose) { 344 | console.log('SMT Output:'); 345 | console.log('------------'); 346 | console.log(result); 347 | console.log('------------'); 348 | } 349 | return resolve(result); 350 | }); 351 | p.on('error', reject); 352 | p.stdin.write(smt); 353 | p.stdin.end(); 354 | })); 355 | return p; 356 | } 357 | 358 | private async solveRemote (smt: SMTInput): Promise { 359 | if (!getOptions().quiet && getOptions().verbose) { 360 | console.log(`${this.description}: sending request to ${getOptions().z3url}`); 361 | } 362 | const req = await fetch(getOptions().z3url, { method: 'POST', body: smt }); 363 | const smtout = await req.text(); 364 | if (!getOptions().quiet && getOptions().verbose) { 365 | console.log('SMT Output:'); 366 | console.log('------------'); 367 | console.log(smtout); 368 | console.log('------------'); 369 | } 370 | return smtout; 371 | } 372 | 373 | private processSMTOutput (out: SMTOutput): Model | Message { 374 | if (out && out.startsWith('sat')) { 375 | return new Model(out); 376 | } else if (out && out.startsWith('unsat')) { 377 | return { status: 'verified', description: this.description, loc: this.loc }; 378 | } else if (out && out.startsWith('unknown')) { 379 | return { status: 'unknown', description: this.description, loc: this.loc }; 380 | } else if (out && out.startsWith('timeout')) { 381 | return { status: 'timeout', description: this.description, loc: this.loc }; 382 | } else { 383 | return unexpected(new Error('unexpected: ' + out), this.loc); 384 | } 385 | } 386 | 387 | private getModel (): Model { 388 | if (!this.model) throw new Error('no model available'); 389 | return this.model; 390 | } 391 | 392 | private testCode (): TestCode { 393 | const sub: Substituter = new Substituter(); 394 | this.freeVars.forEach(freeVar => { 395 | const expr = valueToJavaScript(this.getModel().valueOf(freeVar)); 396 | const und: Syntax.Literal = { type: 'Literal', value: undefined, loc: nullLoc() }; 397 | sub.replaceVar(`__free__${typeof freeVar === 'string' ? freeVar : freeVar.name}`, und, expr); 398 | }); 399 | const testCode = this.testBody.concat(this.testAssertion); 400 | return testCode.map(s => sub.visitStatement(s)); 401 | } 402 | 403 | private testSource (): string { 404 | const code = stringifyTestCode(this.testCode()); 405 | if (!getOptions().quiet && getOptions().verbose) { 406 | console.log('Test Code:'); 407 | console.log('------------'); 408 | console.log(code); 409 | console.log('------------'); 410 | } 411 | return code; 412 | } 413 | 414 | private getInterpreter (): Interpreter { 415 | if (!this.interpreter) { 416 | const prog: Syntax.Program = { 417 | body: [...this.testCode()], 418 | invariants: [] 419 | }; 420 | this.interpreter = interpret(prog); 421 | this.stepToSource(); 422 | } 423 | return this.interpreter; 424 | } 425 | 426 | private stepToSource (): void { 427 | const interpreter = this.getInterpreter(); 428 | while (interpreter.canStep() && interpreter.loc().file !== getOptions().filename) { 429 | interpreter.stepInto(); 430 | } 431 | } 432 | 433 | private guessCurrentHeap (loc: Syntax.SourceLocation = this.pc()): Heap { 434 | // find index of heap hint 435 | const idx = this.heapHints.findIndex(([loc2]) => eqSourceLocation(loc, loc2)); 436 | if (idx >= 0) { 437 | return this.heapHints[idx][1]; 438 | } else { 439 | // no exact match found in heap hints 440 | // find index of first heap hint that is not earlier 441 | const idx = this.heapHints.findIndex(([loc2]) => !compEndPosition(loc, loc2)); 442 | if (idx >= 0) { 443 | return this.heapHints[idx][1]; 444 | } else if (this.heapHints.length > 0) { 445 | // no heap hint found that is later, so use last 446 | return this.heapHints[this.heapHints.length - 1][1]; 447 | } else { 448 | throw new Error('unable to guess current heap'); 449 | } 450 | } 451 | } 452 | 453 | private modelValue (varname: string, currentHeap: Heap): JSVal | undefined { 454 | const model = this.getModel(); 455 | if (model.mutableVariables().has(varname)) { 456 | try { 457 | return model.valueOf({ name: varname, heap: currentHeap }); 458 | } catch (e) { 459 | return undefined; 460 | } 461 | } else { 462 | return model.valueOf(varname); 463 | } 464 | } 465 | 466 | private currentBindingsFromModel (): Array<[string, any]> { 467 | const model = this.getModel(); 468 | const heap = this.guessCurrentHeap(); 469 | const bindings: Array<[string, any]> = []; 470 | for (const varname of model.variables()) { 471 | if (generatePreamble().vars.has(varname) || generatePreamble().locs.has(varname)) { 472 | continue; 473 | } 474 | const jsval = this.modelValue(varname, heap); 475 | if (jsval !== undefined) { 476 | bindings.push([varname, this.getInterpreter().fromValue(jsval)]); 477 | } 478 | } 479 | return bindings; 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /tests/api.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { code, vcs } from './helpers'; 4 | import { valueToString } from '../src'; 5 | 6 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 7 | declare function requires (x: boolean): void; 8 | 9 | const example = () => { 10 | return () => { 11 | function f (x) { 12 | requires(Number.isInteger(x)); 13 | ensures(y => y > 3); 14 | return x; 15 | } 16 | }; 17 | }; 18 | 19 | describe('description', () => { 20 | 21 | code(example()); 22 | 23 | it('included in verification conditions', () => { 24 | const verificationConditions = vcs(); 25 | expect(verificationConditions).to.have.length(1); 26 | const vc = verificationConditions[0]; 27 | expect(vc.getDescription()).to.be.eql('f: (y > 3)'); 28 | }); 29 | }); 30 | 31 | describe('verification', () => { 32 | 33 | code(example()); 34 | 35 | it('returns message', async () => { 36 | const vc = vcs()[0]; 37 | const message = await vc.verify(); 38 | expect(message.status).to.be.eql('error'); 39 | if (message.status !== 'error') { throw new Error(); } 40 | expect(message.type).to.be.eql('incorrect'); 41 | expect(message.description).to.be.eql('f: (y > 3)'); 42 | expect(message.description).to.be.eql(vc.getDescription()); 43 | expect(message.loc).to.be.deep.eq({ 44 | file: '', 45 | start: { line: 3, column: 20 }, 46 | end: { line: 3, column: 30 } 47 | }); 48 | }); 49 | }); 50 | 51 | describe('assumptions', () => { 52 | 53 | code(example(), true); 54 | 55 | it('are added automatically', async () => { 56 | const vc = vcs()[0]; 57 | expect(vc.getAssumptions()).to.have.length(1); 58 | expect(vc.getAssumptions()[0]).to.be.deep.eq(['Number.isInteger(x)', false]); 59 | }); 60 | 61 | it('can be added', async () => { 62 | const vc = vcs()[0]; 63 | vc.addAssumption('x > 4'); 64 | expect(vc.getAssumptions()).to.have.length(2); 65 | expect(vc.getAssumptions()[1]).to.be.deep.eq(['x > 4', true]); 66 | const message = await vc.verify(); 67 | expect(message.status).to.be.eql('verified'); 68 | }); 69 | 70 | it('can be removed', async () => { 71 | const vc = vcs()[0]; 72 | vc.addAssumption('x > 4'); 73 | let message = await vc.verify(); 74 | expect(message.status).to.be.eql('verified'); 75 | vc.removeAssumption(1); 76 | expect(vc.getAssumptions()).to.have.length(1); 77 | message = await vc.verify(); 78 | expect(message.status).to.be.eql('error'); 79 | if (message.status !== 'error') { throw new Error(); } 80 | expect(message.type).to.be.eql('incorrect'); 81 | }); 82 | 83 | it('cannot be removed if non-existing', async () => { 84 | const vc = vcs()[0]; 85 | expect(() => { 86 | vc.removeAssumption(2); 87 | }).to.throw('no such assumption'); 88 | }); 89 | 90 | it('cannot be removed if builtin', async () => { 91 | const vc = vcs()[0]; 92 | expect(() => { 93 | vc.removeAssumption(0); 94 | }).to.throw('cannot remove built-in assumptions'); 95 | }); 96 | }); 97 | 98 | describe('assertion', () => { 99 | 100 | code(example()); 101 | 102 | it('can be changed', async () => { 103 | const vc = vcs()[0]; 104 | const vc2 = vc.assert('x >= 1 || x < 1'); 105 | expect(vc2.getDescription()).to.be.eql('x >= 1 || x < 1'); 106 | expect(vc2.getAssumptions()).to.have.length(1); 107 | const message = await vc2.verify(); 108 | expect(message.status).to.be.eql('verified'); 109 | }); 110 | }); 111 | 112 | describe('trace', () => { 113 | 114 | code(example()); 115 | 116 | it('can be obtained', async () => { 117 | const vc = vcs()[0]; 118 | await vc.verify(); 119 | const message = vc.runWithInterpreter(); 120 | expect(message.status).to.be.eql('error'); 121 | if (message.status !== 'error') { throw new Error(); } 122 | expect(message.type).to.be.eql('incorrect'); 123 | expect(message.description).to.be.eql('f: (y > 3)'); 124 | expect(vc.steps()).to.be.eq(19); 125 | expect(vc.pc()).to.be.deep.eq({ 126 | file: '', 127 | start: { line: 3, column: 25 }, 128 | end: { line: 3, column: 30 } 129 | }); 130 | expect(vc.iteration()).to.be.eq(0); 131 | expect(vc.callstack()).to.be.deep.eq([[' (:3:25)', { 132 | file: '', 133 | start: { line: 3, column: 25 }, 134 | end: { line: 3, column: 30 } 135 | }, 0]]); 136 | }); 137 | }); 138 | 139 | describe('scopes', () => { 140 | 141 | code(example(), true); 142 | 143 | it('can be queried', async () => { 144 | const vc = vcs()[0]; 145 | await vc.verify(); 146 | vc.runWithInterpreter(); 147 | const scopes = vc.getScopes(0); 148 | expect(scopes).to.have.length(1); 149 | const globalScope = scopes[0]; 150 | 151 | const fBinding = globalScope.find(([varname]) => varname === 'f'); 152 | expect(fBinding).to.be.an('array'); 153 | expect(fBinding).to.have.length(3); 154 | expect(fBinding[0]).to.be.eq('f'); 155 | expect(fBinding[1].type).to.be.eq('fun'); 156 | expect(fBinding[2].type).to.be.eq('fun'); 157 | expect(valueToString(fBinding[1])).to.be.eq('function f (x) {\n return x;\n}'); 158 | expect(valueToString(fBinding[2])).to.be.eq( 159 | 'function (x_0) {\n if ((x_0 === 0)) {\n return 0;\n }\n return 0;\n}'); 160 | 161 | const xBinding = globalScope.find(([varname]) => varname === 'x'); 162 | expect(xBinding).to.be.deep.eq(['x', { type: 'num', v: 0 }, { type: 'num', v: 0 }]); 163 | expect(valueToString(xBinding[1])).to.be.eq('0'); 164 | }); 165 | }); 166 | 167 | describe('watches', () => { 168 | 169 | code(example(), true); 170 | 171 | it('can be queried', async () => { 172 | const vc = vcs()[0]; 173 | await vc.verify(); 174 | vc.runWithInterpreter(); 175 | const watches = vc.getWatches(); 176 | expect(watches).to.have.length(0); 177 | }); 178 | 179 | it('can be added', async () => { 180 | const vc = vcs()[0]; 181 | await vc.verify(); 182 | vc.runWithInterpreter(); 183 | vc.addWatch('x + 1'); 184 | const watches = vc.getWatches(); 185 | expect(watches).to.have.length(1); 186 | expect(watches).to.be.deep.eq([ 187 | ['x + 1', { type: 'num', v: 1 }, { type: 'num', v: 1 }] 188 | ]); 189 | }); 190 | 191 | it('can be removed', async () => { 192 | const vc = vcs()[0]; 193 | await vc.verify(); 194 | vc.runWithInterpreter(); 195 | vc.addWatch('x + 1'); 196 | let watches = vc.getWatches(); 197 | expect(watches).to.have.length(1); 198 | vc.removeWatch(0); 199 | watches = vc.getWatches(); 200 | expect(watches).to.have.length(0); 201 | }); 202 | 203 | it('cannot be removed if non-existing', async () => { 204 | const vc = vcs()[0]; 205 | expect(() => { 206 | vc.removeWatch(2); 207 | }).to.throw('no such watch'); 208 | }); 209 | }); 210 | 211 | describe('execution', () => { 212 | 213 | code(example(), true); 214 | 215 | it('can be stepped', async () => { 216 | const vc = vcs()[0]; 217 | await vc.verify(); 218 | vc.runWithInterpreter(); 219 | vc.restart(); 220 | expect(vc.pc()).to.be.deep.eq({ 221 | file: '', 222 | start: { line: 1, column: 2 }, 223 | end: { line: 5, column: 9 } 224 | }); 225 | vc.stepInto(); 226 | expect(vc.pc()).to.be.deep.eq({ 227 | file: '', 228 | start: { line: 1, column: 13 }, 229 | end: { line: 1, column: 14 } 230 | }); 231 | vc.stepInto(); 232 | expect(vc.pc()).to.be.deep.eq({ 233 | file: '', 234 | start: { line: 4, column: 19 }, 235 | end: { line: 4, column: 20 } 236 | }); 237 | vc.stepInto(); 238 | expect(vc.pc()).to.be.deep.eq({ 239 | file: '', 240 | start: { line: 4, column: 12 }, 241 | end: { line: 4, column: 21 } 242 | }); 243 | }); 244 | 245 | it('allows navigation', async () => { 246 | const vc = vcs()[0]; 247 | await vc.verify(); 248 | vc.runWithInterpreter(); 249 | vc.goto({ line: 4, column: 19 }); 250 | expect(vc.pc()).to.be.deep.eq({ 251 | file: '', 252 | start: { line: 4, column: 19 }, 253 | end: { line: 4, column: 20 } 254 | }); 255 | }); 256 | }); 257 | 258 | describe('annotations', () => { 259 | 260 | code(example()); 261 | 262 | it('are added automatically', async () => { 263 | const vc = vcs()[0]; 264 | await vc.verify(); 265 | vc.runWithInterpreter(); 266 | expect(vc.getAnnotations()).to.be.deep.eq([ 267 | [ 268 | { file: '', start: { line: 1, column: 13 }, end: { line: 1, column: 14 } }, 269 | [{ type: 'num', v: 0 }], 270 | { type: 'num', v: 0 } 271 | ] 272 | ]); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /tests/arrays.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { code, incorrect, verified, unverified } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 6 | declare function requires (x: boolean): void; 7 | 8 | describe('simple arrays', () => { 9 | /* tslint:disable:no-unused-expression */ 10 | 11 | code(() => { 12 | function f (a: Array) { 13 | requires(a instanceof Array); 14 | requires(a.length >= 1); 15 | 16 | return a[0]; 17 | } 18 | 19 | function g (a: Array) { 20 | requires(a instanceof Array); 21 | requires(a.length >= 1); 22 | requires(a.length < 5); 23 | ensures(res => res > 3); 24 | 25 | return a[0]; 26 | } 27 | 28 | const a0 = []; 29 | assert(a0 instanceof Array); 30 | assert(a0 instanceof Object); 31 | assert('length' in a0); 32 | assert(a0.length === 0); 33 | 34 | const a1 = [23]; 35 | assert(a1 instanceof Array); 36 | assert(a1 instanceof Object); 37 | assert('length' in a1); 38 | assert(a1.length === 1); 39 | assert(0 in a1); 40 | assert(a1[0] > 22); 41 | const p = 3 - 2 - 1; 42 | assert(a1[p] > 22); 43 | f(a1); 44 | 45 | const a2 = [23, 42]; 46 | assert(a2.length === 2); 47 | assert(!(2 in a2)); 48 | f(a2); 49 | a2[2]; 50 | 51 | function ff (a: Array) { 52 | requires(a instanceof Array); 53 | requires(a.every(e => e > 3)); 54 | requires(a.length >= 2); 55 | assert(a[0] > 2); // holds 56 | assert(a[1] > 4); // does not hold 57 | assert(a[2] > 0); // out of bounds 58 | } 59 | 60 | ff([5, 4]); 61 | ff([5]); // precondition violated 62 | }); 63 | 64 | verified('f: a has property 0'); 65 | verified('g: a has property 0'); 66 | incorrect('g: (res > 3)', ['a', [false, false]]); 67 | 68 | verified('assert: (a0 instanceof Array)'); 69 | verified('assert: (a0 instanceof Object)'); 70 | verified('assert: ("length" in a0)'); 71 | verified('assert: (a0.length === 0)'); 72 | 73 | verified('assert: (a1 instanceof Array)'); 74 | verified('assert: (a1 instanceof Object)'); 75 | verified('assert: ("length" in a1)'); 76 | verified('assert: (a1.length === 1)'); 77 | verified('assert: (0 in a1)'); 78 | verified('assert: (a1[0] > 22)'); 79 | verified('assert: (a1[p] > 22)'); 80 | verified('precondition f(a1)'); 81 | 82 | verified('assert: (a2.length === 2)'); 83 | verified('assert: !(2 in a2)'); 84 | verified('precondition f(a2)'); 85 | incorrect('a2 has property 2'); 86 | }); 87 | 88 | describe('array invariants', () => { 89 | 90 | code(() => { 91 | function f_1 () { 92 | ensures(res => res.every(e => e > 23)); 93 | 94 | return [42, 69]; 95 | } 96 | 97 | function f_2 (a: Array) { 98 | requires(a.every(e => e > 23)); 99 | requires(a.length >= 1); 100 | ensures(res => res > 12); 101 | 102 | return a[0]; 103 | } 104 | 105 | function f_3 (a: Array) { 106 | requires(a.every(e => e > 23)); 107 | requires(a.length >= 3); 108 | ensures(a[2] > 12); 109 | } 110 | 111 | function f_4 () { 112 | ensures(res => res.every((e, i) => e > i)); 113 | 114 | return [1, 2, 3]; 115 | } 116 | 117 | function g_1 () { 118 | ensures(res => res.every(e => e > 23)); 119 | 120 | return [42, 69, 4]; 121 | } 122 | 123 | function g_2 (a: Array) { 124 | requires(a.every(e => e > 23)); 125 | requires(a.length >= 1); 126 | requires(a.length < 6); 127 | ensures(res => res > 42); 128 | 129 | return a[0]; 130 | } 131 | 132 | function g_3 (a: Array) { 133 | requires(a.every(e => e > 23)); 134 | requires(a.length === 0); 135 | ensures(a[2] > 12); 136 | } 137 | 138 | function g_4 () { 139 | ensures(res => res.every((e, i) => e > i)); 140 | 141 | return [1, 2, 2]; 142 | } 143 | }); 144 | 145 | verified('f_1: res.every(e => (e > 23))'); 146 | verified('f_2: a has property 0'); 147 | verified('f_2: (res > 12)'); 148 | verified('f_3: (a[2] > 12)'); 149 | verified('f_4: res.every((e, i) => (e > i))'); 150 | incorrect('g_1: res.every(e => (e > 23))'); 151 | verified('g_2: a has property 0'); 152 | incorrect('g_2: (res > 42)', ['a', [24]]); 153 | incorrect('g_3: (a[2] > 12)', ['a', []]); 154 | incorrect('g_4: res.every((e, i) => (e > i))'); 155 | }); 156 | 157 | describe('array constructor', () => { 158 | 159 | code(() => { 160 | const a = new Array(3, 2); 161 | assert(a.length === 2); 162 | assert(a[1] === 2); 163 | 164 | const b = Array(3, 2); 165 | assert(b.length === 2); 166 | assert(b[1] === 2); 167 | }); 168 | 169 | verified('assert: (a.length === 2)'); 170 | verified('assert: (a[1] === 2)'); 171 | verified('assert: (b.length === 2)'); 172 | verified('assert: (b[1] === 2)'); 173 | }); 174 | 175 | describe('array slice', () => { 176 | 177 | code(() => { 178 | const arr = [1, 2, 3]; 179 | const sliced = arr.slice(1, 2); 180 | assert(sliced[0] === 2); 181 | 182 | function f (a) { 183 | requires(a instanceof Array); 184 | requires(a.length === 6); 185 | ensures(y => y.length === 2); 186 | ensures(y => y[1] === a[3]); 187 | 188 | return a.slice(2, 4); 189 | } 190 | 191 | function g (a) { 192 | requires(a instanceof Array); 193 | requires(a.length === 6); 194 | ensures(y => y[1] !== a[3]); 195 | 196 | return a.slice(2, 4); 197 | } 198 | 199 | const d = [1, 2, 3]; 200 | d.slice(1, 4); 201 | }); 202 | 203 | verified('precondition arr.slice(1, 2)'); 204 | verified('assert: (sliced[0] === 2)'); 205 | verified('f: a has property "slice"'); 206 | verified('f: precondition a.slice(2, 4)'); 207 | verified('f: (y.length === 2)'); 208 | verified('f: (y[1] === a[3])'); 209 | verified('g: a has property "slice"'); 210 | verified('g: precondition a.slice(2, 4)'); 211 | incorrect('g: (y[1] !== a[3])').timeout(4000); 212 | unverified('precondition d.slice(1, 4)'); 213 | }); 214 | -------------------------------------------------------------------------------- /tests/classes.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { verificationConditions } from '../src'; 4 | import { code, incorrect, verified, unverified } from './helpers'; 5 | 6 | declare function assert (x: boolean): void; 7 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 8 | declare function requires (x: boolean): void; 9 | declare function pure (): boolean; 10 | declare function spec (f: any, r: (rx: any) => boolean, s: (sx: any, sy: any) => boolean): boolean; 11 | 12 | describe('simple class invariant', () => { 13 | 14 | code(() => { 15 | function greaterThree (y: number) { 16 | return y > 3; 17 | } 18 | 19 | class A { 20 | readonly x: number; 21 | constructor (x: number) { 22 | this.x = x; 23 | } 24 | invariant () { 25 | return greaterThree(this.x); 26 | } 27 | } 28 | 29 | function greaterTwo (a: A) { 30 | requires(a instanceof A); 31 | ensures(a.x > 2); 32 | greaterThree(a.x); 33 | } 34 | }); 35 | 36 | verified('greaterTwo: a has property "x"'); 37 | verified('greaterTwo: (a.x > 2)'); 38 | }); 39 | 40 | describe('class invariant with reference to mutable variable', () => { 41 | 42 | const code = () => { 43 | let x = 23; 44 | 45 | class A { 46 | readonly x: number; 47 | constructor (x: number) { 48 | this.x = x; 49 | } 50 | invariant () { 51 | return x > 4; 52 | } 53 | } 54 | }; 55 | 56 | it('gets rejected', () => { 57 | const src = code.toString(); 58 | const t = verificationConditions(src.substring(14, src.length - 2)); 59 | expect(t).to.have.property('status', 'error'); 60 | expect(t).to.have.property('type', 'reference-in-invariant'); 61 | }); 62 | }); 63 | 64 | describe('mapLen internal', () => { 65 | 66 | code(() => { 67 | class List { 68 | head: any; 69 | tail: List; 70 | constructor (head, tail) { 71 | this.head = head; this.tail = tail; 72 | } 73 | invariant () { return this.tail === null || this.tail instanceof List; } 74 | } 75 | 76 | function len (lst) { 77 | requires(lst === null || lst instanceof List); 78 | ensures(res => typeof(res) === 'number'); 79 | ensures(pure()); 80 | return lst === null ? 0 : 1 + len(lst.tail); 81 | } 82 | 83 | function map (f, lst) { 84 | requires(lst === null || lst instanceof List); 85 | requires(spec(f, x => true, x => pure())); 86 | ensures(pure()); 87 | ensures(res => res === null || res instanceof List); 88 | ensures(res => len(lst) === len(res)); 89 | len(lst); 90 | const res = lst === null ? null : new List(f(lst.head), map(f, lst.tail)); 91 | len(res); 92 | return res; 93 | } 94 | }); 95 | 96 | verified('len: lst has property "tail"'); 97 | verified('len: precondition len(lst.tail)'); 98 | verified('len: (typeof(res) === "number")'); 99 | verified('len: pure()'); 100 | verified('map: precondition len(lst)'); 101 | verified('map: lst has property "head"'); 102 | verified('map: precondition f(lst.head)'); 103 | verified('map: lst has property "tail"'); 104 | verified('map: precondition map(f, lst.tail)'); 105 | verified('map: class invariant List'); 106 | verified('map: precondition len(res)'); 107 | verified('map: pure()'); 108 | verified('map: ((res === null) || (res instanceof List))'); 109 | verified('map: (len(lst) === len(res))'); 110 | }); 111 | 112 | describe('mapLen external', () => { 113 | 114 | code(() => { 115 | 116 | class List { 117 | head: any; 118 | tail: List; 119 | constructor (head, tail) { this.head = head; this.tail = tail; } 120 | invariant () { return this.tail === null || this.tail instanceof List; } 121 | } 122 | 123 | function map (lst, f) { 124 | requires(lst === null || lst instanceof List); 125 | requires(spec(f, x => true, x => pure())); 126 | ensures(pure()); 127 | ensures(res => res === null || res instanceof List); 128 | 129 | if (lst === null) return null; 130 | return new List(f(lst.head), map(lst.tail, f)); 131 | } 132 | 133 | function len (lst) { 134 | requires(lst === null || lst instanceof List); 135 | ensures(pure()); 136 | ensures(res => typeof res === 'number' && res >= 0); 137 | 138 | return lst === null ? 0 : len(lst.tail) + 1; 139 | } 140 | 141 | function mapLen (lst, f) { 142 | requires(lst === null || lst instanceof List); 143 | requires(spec(f, x => true, x => pure())); 144 | ensures(pure()); 145 | ensures(len(lst) === len(map(lst, f))); 146 | 147 | const l = len(lst); 148 | const r = len(map(lst, f)); 149 | if (lst === null) { 150 | assert(l === 0); 151 | assert(r === 0); 152 | } else { 153 | const l1 = len(lst.tail); 154 | assert(l === l1 + 1); 155 | 156 | f(lst.head); 157 | const r1 = len(map(lst.tail, f)); 158 | assert(r === r1 + 1); 159 | 160 | mapLen(lst.tail, f); 161 | assert(l1 === r1); 162 | assert(l === r); 163 | } 164 | } 165 | }); 166 | 167 | verified('map: lst has property "head"'); 168 | verified('map: precondition f(lst.head)'); 169 | verified('map: lst has property "tail"'); 170 | verified('map: precondition map(lst.tail, f)'); 171 | verified('map: class invariant List'); 172 | verified('map: pure()'); 173 | verified('map: ((res === null) || (res instanceof List))'); 174 | verified('len: lst has property "tail"'); 175 | verified('len: precondition len(lst.tail)'); 176 | verified('len: pure()'); 177 | verified('len: ((typeof(res) === "number") && (res >= 0))'); 178 | verified('mapLen: precondition len(lst)'); 179 | verified('mapLen: precondition map(lst, f)'); 180 | verified('mapLen: precondition len(map(lst, f))'); 181 | verified('mapLen: assert: (l === 0)'); 182 | verified('mapLen: assert: (r === 0)'); 183 | verified('mapLen: lst has property "tail"'); 184 | verified('mapLen: precondition len(lst.tail)'); 185 | verified('mapLen: assert: (l === (l1 + 1))'); 186 | verified('mapLen: lst has property "head"'); 187 | verified('mapLen: precondition f(lst.head)'); 188 | verified('mapLen: precondition map(lst.tail, f)'); 189 | verified('mapLen: precondition len(map(lst.tail, f))'); 190 | verified('mapLen: assert: (r === (r1 + 1))'); 191 | verified('mapLen: precondition mapLen(lst.tail, f)'); 192 | verified('mapLen: assert: (l1 === r1)'); 193 | verified('mapLen: assert: (l === r)'); 194 | verified('mapLen: pure()'); 195 | verified('mapLen: (len(lst) === len(map(lst, f)))'); 196 | 197 | }); 198 | 199 | describe('map invariant', () => { 200 | 201 | code(() => { 202 | class List { 203 | head: any; 204 | tail: List; 205 | each: (element: any) => boolean; 206 | constructor (head, tail, each) { 207 | this.head = head; this.tail = tail; this.each = each; 208 | } 209 | invariant () { 210 | return spec(this.each, x => true, (x, y) => pure() && typeof(y) === 'boolean') && 211 | (true && this.each)(this.head) && 212 | (this.tail === null || this.tail instanceof List && this.each === this.tail.each); 213 | } 214 | } 215 | 216 | function map (f, lst, newEach) { 217 | requires(spec(newEach, x => true, (x, y) => pure() && typeof(y) === 'boolean')); 218 | requires(lst === null || spec(f, x => (true && lst.each)(x), (x, y) => pure() && newEach(y))); 219 | requires(lst === null || lst instanceof List); 220 | ensures(res => res === null || (res instanceof List && res.each === newEach)); 221 | ensures(pure()); 222 | if (lst === null) { 223 | return null; 224 | } else { 225 | return new List(f(lst.head), map(f, lst.tail, newEach), newEach); 226 | } 227 | } 228 | }); 229 | 230 | verified('map: lst has property "head"'); 231 | verified('map: lst has property "tail"'); 232 | verified('map: precondition f(lst.head)'); 233 | verified('map: precondition map(f, lst.tail, newEach)'); 234 | verified('map: class invariant List'); 235 | verified('map: pure()'); 236 | verified('map: ((res === null) || ((res instanceof List) && (res.each === newEach)))'); 237 | }); 238 | 239 | describe('map invariant method', () => { 240 | code(() => { 241 | // custom linked list class with predicate and map function 242 | class List { 243 | head: any; 244 | tail: List; 245 | each: (element: any) => boolean; 246 | constructor (head, tail, each) { 247 | this.head = head; this.tail = tail; this.each = each; 248 | } 249 | 250 | invariant () { 251 | // this.each is a predicate that is true for each element 252 | return spec(this.each, (x) => true, 253 | (x, y) => pure() && typeof(y) === 'boolean') && 254 | (true && this.each)(this.head) && 255 | (this.tail === null || 256 | this.tail instanceof List && this.each === this.tail.each); 257 | } 258 | 259 | map (f, newEach) { 260 | // new each neeeds to be a predicate 261 | // (a pure function without precondition that returns a boolean) 262 | requires(spec(newEach, (x) => true, 263 | (x, y) => pure() && typeof(y) === 'boolean')); 264 | // the current predicate 'this.each' must satisfy the precondition of 'f' 265 | // and the output of 'f' needs to satisfy the new predicate 266 | requires(spec(f, (x) => (true && this.each)(x), 267 | (x, y) => pure() && newEach(y))); 268 | ensures(res => res instanceof List && res.each === newEach); 269 | ensures(pure()); 270 | 271 | return new List( 272 | f(this.head), 273 | this.tail === null ? null : this.tail.map(f, newEach), 274 | newEach); 275 | } 276 | } 277 | }); 278 | 279 | verified('map: this has property "head"'); 280 | verified('map: precondition f(this.head)'); 281 | verified('map: this has property "tail"'); 282 | verified('map: this.tail has property "map"'); 283 | verified('map: precondition this.tail.map(f, newEach)'); 284 | verified('map: class invariant List'); 285 | verified('map: ((res instanceof List) && (res.each === newEach))'); 286 | verified('map: pure()'); 287 | }); 288 | 289 | describe('promise', () => { 290 | 291 | code(() => { 292 | class Promise { 293 | value: any; 294 | constructor (value) { 295 | this.value = value; 296 | } 297 | } 298 | 299 | function resolve (fulfill) { 300 | // fulfill is value, promise or then-able 301 | requires(!('then' in fulfill) || spec(fulfill.then, () => true, () => true)); 302 | 303 | if (fulfill instanceof Promise) { 304 | return fulfill; 305 | } else if ('then' in fulfill) { 306 | return new Promise(fulfill.then()); 307 | } else { 308 | return new Promise(fulfill); 309 | } 310 | } 311 | 312 | function then (promise, fulfill) { 313 | // fulfill returns value or promise 314 | requires(promise instanceof Promise); 315 | requires(spec(fulfill, x => true, (x, res) => true)); 316 | 317 | const res = fulfill(promise.value); 318 | if (res instanceof Promise) { 319 | return res; 320 | } else { 321 | return new Promise(res); 322 | } 323 | } 324 | 325 | const p = resolve(0); 326 | const p2 = then(p, n => { 327 | return n + 2; 328 | }); 329 | const p3 = then(p2, n => { 330 | return new Promise(n + 5); 331 | }); 332 | }); 333 | 334 | verified('resolve: fulfill has property "then"'); 335 | verified('resolve: precondition fulfill.then()'); 336 | verified('resolve: class invariant Promise'); 337 | verified('resolve: class invariant Promise'); 338 | verified('then: promise has property "value"'); 339 | verified('then: precondition fulfill(promise.value)'); 340 | verified('then: class invariant Promise'); 341 | verified('precondition resolve(0)'); 342 | verified('precondition then(p, n => (n + 2))'); 343 | verified('func: class invariant Promise'); 344 | verified('precondition then(p2, n => new Promise((n + 5)))'); 345 | }); 346 | 347 | describe('simple class instance access', () => { 348 | 349 | code(() => { 350 | class A { 351 | b: number; 352 | constructor (b) { 353 | this.b = b; 354 | } 355 | invariant () { 356 | return this.b >= 0; 357 | } 358 | } 359 | 360 | function f (a: A) { 361 | requires(a instanceof A); 362 | ensures(res => res >= 0); 363 | 364 | return a.b; 365 | } 366 | 367 | function g (a: A) { 368 | requires(a instanceof A); 369 | ensures(res => res < 0); 370 | 371 | return a.b; 372 | } 373 | 374 | const a = new A(23); 375 | assert(a instanceof A); 376 | assert(a instanceof Object); 377 | assert('b' in a); 378 | assert(a.b > 22); 379 | assert(a['b'] > 22); 380 | const p = 'b'; 381 | assert(a[p] > 22); 382 | }); 383 | 384 | verified('f: a has property "b"'); 385 | verified('f: (res >= 0)'); 386 | verified('g: a has property "b"'); 387 | incorrect('g: (res < 0)', ['a', { _cls_: 'A', _args_: [0] }]); 388 | verified('class invariant A'); 389 | verified('assert: (a instanceof A)'); 390 | verified('assert: (a instanceof Object)'); 391 | verified('assert: ("b" in a)'); 392 | verified('assert: (a.b > 22)'); 393 | verified('assert: (a[p] > 22)'); 394 | }); 395 | 396 | describe('static methods', () => { 397 | 398 | code(() => { 399 | class A { 400 | constructor () { /* emtpy */ } 401 | method () { 402 | return 23; 403 | } 404 | } 405 | 406 | const a = new A(); 407 | const m = a.method(); 408 | assert(m === 23); 409 | }); 410 | 411 | verified('a has property "method"'); 412 | verified('precondition a.method()'); 413 | verified('assert: (m === 23)'); 414 | }); 415 | 416 | describe('methods', () => { 417 | 418 | code(() => { 419 | class Adder { 420 | base: number; 421 | constructor (base) { 422 | this.base = base; 423 | } 424 | invariant () { 425 | return typeof this.base === 'number'; 426 | } 427 | addTo (n) { 428 | requires(typeof n === 'number'); 429 | return this.base + n; 430 | } 431 | } 432 | 433 | const adder = new Adder(5); 434 | const m = adder.addTo(3); 435 | assert(m === 8); 436 | 437 | function f (a: Adder) { 438 | requires(a instanceof Adder); 439 | ensures(res => res !== 2); 440 | 441 | return a.addTo(1); 442 | } 443 | }); 444 | 445 | verified('addTo: this has property "base"'); 446 | verified('class invariant Adder'); 447 | verified('adder has property "addTo"'); 448 | verified('precondition adder.addTo(3)'); 449 | verified('assert: (m === 8)'); 450 | incorrect('f: (res !== 2)', ['a', { _cls_: 'Adder', _args_: [1] }]); 451 | }); 452 | 453 | describe('methods calling other methods', () => { 454 | 455 | code(() => { 456 | class A { 457 | b: number; 458 | constructor (b) { 459 | this.b = b; 460 | } 461 | invariant () { 462 | return typeof this.b === 'number'; 463 | } 464 | m (x) { 465 | requires(x >= 4); 466 | ensures(y => y > 4); 467 | return this.n(x) + x; 468 | } 469 | n (x) { 470 | requires(x > 3); 471 | ensures(y => y >= 1); 472 | return 1; 473 | } 474 | o () { 475 | return this.n(7); 476 | } 477 | p () { 478 | requires(this.b >= 0); 479 | ensures(y => y >= 0); 480 | return this.b + this.n(4); 481 | } 482 | q () { 483 | this.n(2); 484 | (this as any).x(); 485 | } 486 | r () { 487 | return this.n(2); 488 | } 489 | } 490 | 491 | const a = new A(5); 492 | const m = a.m(4); 493 | assert(m > 4); 494 | const o = a.o(); 495 | assert(o === 1); 496 | a.n(0); 497 | const n = a.n(5); 498 | assert(n === 1); 499 | const p = a.p(); 500 | assert(p >= 0); 501 | (new A(-1)).p(); 502 | }); 503 | 504 | verified('m: this has property "n"'); 505 | verified('m: precondition this.n(x)'); 506 | verified('m: (y > 4)'); 507 | verified('n: (y >= 1)'); 508 | verified('o: this has property "n"'); 509 | verified('o: precondition this.n(7)'); 510 | verified('p: this has property "b"'); 511 | verified('p: this has property "n"'); 512 | verified('p: precondition this.n(4)'); 513 | verified('p: (y >= 0)'); 514 | verified('q: this has property "n"'); 515 | incorrect('q: precondition this.n(2)', ['_this_14', { _cls_: 'A', _args_: [0] }]); 516 | incorrect('q: this has property "x"', ['_this_14', { _cls_: 'A', _args_: [0] }]); 517 | incorrect('q: precondition this.x()', ['_this_14', { _cls_: 'A', _args_: [-1] }]); 518 | verified('r: this has property "n"'); 519 | incorrect('r: precondition this.n(2)', ['_this_15', { _cls_: 'A', _args_: [0] }]); 520 | verified('class invariant A'); 521 | verified('a has property "m"'); 522 | verified('precondition a.m(4)'); 523 | verified('assert: (m > 4)'); 524 | verified('a has property "n"'); 525 | verified('a has property "o"'); 526 | verified('precondition a.o()'); 527 | unverified('assert: (o === 1)'); 528 | incorrect('precondition a.n(0)'); 529 | verified('a has property "n"'); 530 | verified('precondition a.n(5)'); 531 | verified('assert: (n === 1)'); 532 | verified('a has property "p"'); 533 | verified('precondition a.p()'); 534 | verified('assert: (p >= 0)'); 535 | verified('class invariant A'); 536 | verified('new A(-1) has property "p"'); 537 | incorrect('precondition new A(-1).p()'); 538 | }); 539 | 540 | describe('access method as function', () => { 541 | 542 | code(() => { 543 | class A { 544 | constructor () { /* emtpy */ } 545 | method () { 546 | return 23; 547 | } 548 | } 549 | 550 | const a = new A(); 551 | const m = a.method; 552 | m(); 553 | }); 554 | 555 | verified('a has property "method"'); 556 | incorrect('precondition m()'); 557 | }); 558 | -------------------------------------------------------------------------------- /tests/globals.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { code, incorrect, unverified, verified } from './helpers'; 4 | import { verificationConditions } from '../src'; 5 | 6 | declare function assert (x: boolean): void; 7 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 8 | declare function requires (x: boolean): void; 9 | 10 | describe('undefined identifier', () => { 11 | 12 | const code = () => { 13 | // @ts-ignore: intentionally using undefined variable 14 | monsole.log('hello'); 15 | }; 16 | 17 | it('gets rejected', () => { 18 | const src = code.toString(); 19 | const t = verificationConditions(src.substring(14, src.length - 2)); 20 | expect(t).to.have.property('status', 'error'); 21 | expect(t).to.have.property('type', 'undefined-identifier'); 22 | }); 23 | }); 24 | 25 | describe('console', () => { 26 | 27 | code(() => { 28 | function f (x) { 29 | ensures(y => y === undefined); 30 | 31 | return console.log(x); 32 | } 33 | 34 | console.log(); 35 | // @ts-ignore: intentionally using wrong method name 36 | console.mog('hello'); 37 | }); 38 | 39 | verified('f: console has property "log"'); 40 | verified('f: precondition console.log(x)'); 41 | verified('f: (y === undefined)'); 42 | verified('console has property "log"'); 43 | unverified('precondition console.log()'); 44 | incorrect('console has property "mog"'); 45 | incorrect('precondition console.mog("hello")'); 46 | }); 47 | 48 | describe('parseInt', () => { 49 | 50 | code(() => { 51 | function f (x) { 52 | requires(typeof x === 'string'); 53 | ensures(y => typeof y === 'number'); 54 | ensures(y => Number.isInteger(y)); 55 | 56 | return parseInt(x, 10); 57 | } 58 | 59 | function g (x) { 60 | requires(typeof x === 'string'); 61 | ensures(y => y !== 12); 62 | 63 | return parseInt(x, 10); 64 | } 65 | 66 | const z = parseInt('23', 10); 67 | assert(z === 23); 68 | parseInt('23', 16); 69 | }); 70 | 71 | verified('f: precondition parseInt(x, 10)'); 72 | verified('f: (typeof(y) === "number")'); 73 | verified('f: Number.isInteger(y)'); 74 | verified('g: precondition parseInt(x, 10)'); 75 | incorrect('g: (y !== 12)', ['x', '12']); 76 | verified('precondition parseInt("23", 10)'); 77 | verified('assert: (z === 23)'); 78 | unverified('precondition parseInt("23", 16)'); 79 | }); 80 | 81 | describe('Math', () => { 82 | 83 | code(() => { 84 | function f (x) { 85 | requires(typeof x === 'number'); 86 | ensures(y => y >= 4); 87 | 88 | return Math.max(x, 4); 89 | } 90 | 91 | function g (x) { 92 | requires(typeof x === 'number'); 93 | ensures(y => y !== 5); 94 | 95 | return Math.max(x, 4); 96 | } 97 | 98 | const z = Math.max(12, 44); 99 | assert(z === 44); 100 | // @ts-ignore: intentionally using wrong type 101 | Math.max('abc', 16); 102 | }); 103 | 104 | verified('f: Math has property "max"'); 105 | verified('f: precondition Math.max(x, 4)'); 106 | verified('f: (y >= 4)'); 107 | verified('g: Math has property "max"'); 108 | verified('g: precondition Math.max(x, 4)'); 109 | incorrect('g: (y !== 5)', ['x', 5]); 110 | verified('Math has property "max"'); 111 | verified('precondition Math.max(12, 44)'); 112 | verified('assert: (z === 44)'); 113 | verified('Math has property "max"'); 114 | unverified('precondition Math.max("abc", 16)'); 115 | }); 116 | 117 | describe('Number', () => { 118 | 119 | code(() => { 120 | function f (x) { 121 | requires(typeof x === 'number'); 122 | requires(Number.isInteger(x)); 123 | ensures(y => Number.isInteger(y)); 124 | 125 | return x + 1; 126 | } 127 | 128 | function g (x) { 129 | requires(typeof x === 'number'); 130 | requires(x > 0); 131 | requires(x < 1); 132 | ensures(y => Number.isInteger(y)); 133 | 134 | return x; 135 | } 136 | 137 | const y = Number.isInteger(12); 138 | assert(y === true); 139 | // @ts-ignore: intentionally using wrong type 140 | const z = Number.isInteger('abc'); 141 | assert(z === false); 142 | }); 143 | 144 | verified('f: Number.isInteger(y)'); 145 | incorrect('g: Number.isInteger(y)', ['x', 0.5]); 146 | verified('Number has property "isInteger"'); 147 | verified('precondition Number.isInteger(12)'); 148 | verified('assert: (y === true)'); 149 | verified('Number has property "isInteger"'); 150 | verified('precondition Number.isInteger("abc")'); 151 | verified('assert: (z === false)'); 152 | }); 153 | 154 | describe('Random dice roll', () => { 155 | 156 | code(() => { 157 | const d6 = Math.trunc(Math.random() * 6 + 1); 158 | assert(typeof d6 === 'number'); 159 | assert(d6 >= 1); 160 | assert(d6 <= 6); 161 | assert(Number.isInteger(d6)); 162 | }); 163 | 164 | verified('Math has property "trunc"'); 165 | verified('Math has property "random"'); 166 | verified('precondition Math.random()'); 167 | verified('precondition Math.trunc(((Math.random() * 6) + 1))'); 168 | verified('assert: (typeof(d6) === "number")'); 169 | verified('assert: (d6 >= 1)'); 170 | verified('assert: (d6 <= 6)'); 171 | verified('assert: Number.isInteger(d6)'); 172 | }); 173 | 174 | describe('alert', () => { 175 | 176 | code(() => { 177 | function f (x) { 178 | ensures(y => y === undefined); 179 | 180 | return alert(x); 181 | } 182 | 183 | alert(); 184 | }); 185 | 186 | verified('f: precondition alert(x)'); 187 | verified('f: (y === undefined)'); 188 | unverified('precondition alert()'); 189 | }); 190 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { verificationConditions } from '../src'; 4 | import { FreeVar } from '../src/logic'; 5 | import { log } from '../src/message'; 6 | import { plainToJSVal } from '../src/model'; 7 | import { setOptions } from '../src/options'; 8 | import { sourceAsJavaScript } from '../src/parser'; 9 | import { interpret } from '../src/interpreter'; 10 | import VerificationCondition from '../src/verification'; 11 | import { TEST_PREAMBLE } from '../src/codegen'; 12 | 13 | let savedVCs: Array; 14 | 15 | export function vcs (): Array { 16 | return savedVCs; 17 | } 18 | 19 | export function codeToString (fn: () => any) { 20 | const code = fn.toString(); 21 | return code.substring(14, code.length - 2); 22 | } 23 | 24 | export function code (fn: () => any, each: boolean = false) { 25 | const setUp = () => { 26 | const t = verificationConditions(codeToString(fn)); 27 | if (!(t instanceof Array)) { 28 | log(t); 29 | if (t.status === 'error' && t.type === 'unexpected') console.log(t.error); 30 | throw new Error('failed to find verification conditions'); 31 | } 32 | savedVCs = t; 33 | }; 34 | if (each) { 35 | beforeEach(setUp); 36 | } else { 37 | before(setUp); 38 | } 39 | } 40 | 41 | function helper (expected: 'verified' | 'unverified' | 'incorrect' | 'timeout', description: string, 42 | debug: boolean, expectedModel: Map): Mocha.ITest { 43 | const body = async () => { 44 | /* tslint:disable:no-unused-expression */ 45 | if (debug) { 46 | setOptions({ quiet: false, verbose: true, timeout: 60 }); 47 | console.log(savedVCs.map(vc => vc.getDescription()).join('\n')); 48 | } 49 | const vc = savedVCs.find(v => v.getDescription() === description); 50 | expect(vc).to.be.ok; 51 | const res = await vc.verify(); 52 | if (res.status === 'error' && debug) console.log(res); 53 | if (expected === 'verified' || expected === 'unverified') { 54 | const st = res.status === 'error' && res.type === 'incorrect' ? res.type : res.status; 55 | expect(st).to.be.eql(expected); 56 | if (res.status === 'unverified') { 57 | for (const v of expectedModel.keys()) { 58 | expect(res.model.variables()).to.include(typeof v === 'string' ? v : v.name); 59 | expect(res.model.valueOf(v)).to.eql(plainToJSVal(expectedModel.get(v))); 60 | } 61 | } 62 | } else if (expected === 'timeout') { 63 | expect(res.status).to.eql('timeout'); 64 | } else { 65 | expect(res.status).to.equal('error'); 66 | if (res.status === 'error') { 67 | expect(res.type).to.equal(expected); 68 | if (res.type === 'incorrect') { 69 | for (const v of expectedModel.keys()) { 70 | expect(res.model.variables()).to.include(typeof v === 'string' ? v : v.name); 71 | expect(res.model.valueOf(v)).to.eql(plainToJSVal(expectedModel.get(v))); 72 | } 73 | } 74 | } 75 | } 76 | }; 77 | if (debug) { 78 | return it.only(description + ' ' + expected, body).timeout(60000); 79 | } else { 80 | return it(description + ' ' + expected, body); 81 | } 82 | } 83 | 84 | export function skip (description: string) { return it.skip(description); } 85 | 86 | interface VerifiedFun { 87 | (description: string): Mocha.ITest; 88 | debug: (description: string) => Mocha.ITest; 89 | } 90 | interface UnverifiedFun { 91 | (description: string, ...expectedVariables: Array<[FreeVar, any]>): Mocha.ITest; 92 | debug: (description: string, ...expectedVariables: Array<[FreeVar, any]>) => Mocha.ITest; 93 | } 94 | 95 | export const verified: VerifiedFun = (() => { 96 | const f: any = (description: string): Mocha.ITest => helper('verified', description, false, new Map()); 97 | f.debug = (description: string): Mocha.ITest => helper('verified', description, true, new Map()); 98 | return f; 99 | })(); 100 | 101 | export const unverified: UnverifiedFun = (() => { 102 | const f: any = (description: string, ...expectedVariables: Array<[FreeVar, any]>): Mocha.ITest => 103 | helper('unverified', description, false, new Map(expectedVariables)); 104 | f.debug = (description: string, ...expectedVariables: Array<[FreeVar, any]>): Mocha.ITest => 105 | helper('unverified', description, true, new Map(expectedVariables)); 106 | return f; 107 | })(); 108 | 109 | export const incorrect: UnverifiedFun = (() => { 110 | const f: any = (description: string, ...expectedVariables: Array<[FreeVar, any]>): Mocha.ITest => 111 | helper('incorrect', description, false, new Map(expectedVariables)); 112 | f.debug = (description: string, ...expectedVariables: Array<[FreeVar, any]>): Mocha.ITest => 113 | helper('incorrect', description, true, new Map(expectedVariables)); 114 | return f; 115 | })(); 116 | 117 | export const timeout: VerifiedFun = (() => { 118 | const f: any = (description: string): Mocha.ITest => helper('timeout', description, false, new Map()).timeout(6000); 119 | f.debug = (description: string): Mocha.ITest => helper('timeout', description, true, new Map()); 120 | return f; 121 | })(); 122 | 123 | function bisimulationHelper (description: string, fn: () => any, steps: number, debug: boolean) { 124 | const testBody = () => { 125 | const code = fn.toString().substring(14, fn.toString().length - 2); 126 | const program = sourceAsJavaScript(code, false); 127 | const interpreterOutputs: Array = []; 128 | let interpreterError: { error: any } | undefined; 129 | const evalOutputs: Array = []; 130 | let evalError: { error: any } | undefined; 131 | function log (out: any): void { 132 | evalOutputs.push(out); 133 | if (debug) console.log(out); 134 | } 135 | if (debug) console.log('\eval outputs: '); 136 | try { 137 | /* tslint:disable:no-eval */ 138 | eval(TEST_PREAMBLE + code); 139 | } catch (error) { 140 | evalError = { error }; 141 | } 142 | if (debug) console.log('\eval error: ', evalError); 143 | const interpreter = interpret(program); 144 | interpreter.define('log', (out: any): void => { 145 | interpreterOutputs.push(out); 146 | if (debug) console.log(out); 147 | }, 'const'); 148 | if (debug) console.log('\ninterpreter outputs:'); 149 | try { 150 | interpreter.run(); 151 | } catch (error) { 152 | interpreterError = { error }; 153 | } 154 | if (debug) { 155 | console.log('\ninterpreter error: ', interpreterError); 156 | console.log('\ninterpreter steps: ', interpreter.steps, `(expected ${steps})`); 157 | } 158 | /* tslint:disable:no-unused-expression */ 159 | expect(interpreterOutputs).to.have.length(evalOutputs.length); 160 | expect(interpreterOutputs).to.deep.eq(evalOutputs); 161 | if (evalError === undefined) { 162 | expect(interpreterError).to.be.undefined; 163 | } else { 164 | expect(typeof evalError.error).to.be.eq(typeof interpreterError.error); 165 | expect(evalError.error.message).to.be.eq(interpreterError.error.message); 166 | } 167 | expect(interpreter.steps).to.eq(steps, 'expected number of steps'); 168 | }; 169 | if (debug) { 170 | return it.only(`interprets ${description}`, testBody).timeout(60000); 171 | } else { 172 | return it(`interprets ${description}`, testBody); 173 | } 174 | } 175 | 176 | interface BisimulationFun { 177 | (description: string, code: () => any, steps: number): Mocha.ITest; 178 | debug: (description: string, code: () => any, steps: number) => Mocha.ITest; 179 | } 180 | 181 | export const bisumulate: BisimulationFun = (() => { 182 | const f: any = (description: string, code: () => any, steps: number): Mocha.ITest => 183 | bisimulationHelper(description, code, steps, false); 184 | f.debug = (description: string, code: () => any, steps: number): Mocha.ITest => 185 | bisimulationHelper(description, code, steps, true); 186 | return f; 187 | })(); 188 | -------------------------------------------------------------------------------- /tests/higher-order.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { code, incorrect, verified } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 6 | declare function requires (x: boolean): void; 7 | declare function pure (): boolean; 8 | declare function spec (f: any, r: (rx: any) => boolean, s: (sx: any, sy: any) => boolean): boolean; 9 | 10 | describe('fibonacci increasing (external proof)', () => { 11 | 12 | code(() => { 13 | function fib (n) { 14 | ensures(pure()); 15 | 16 | if (n <= 1) return 1; 17 | return fib(n - 1) + fib(n - 2); 18 | } 19 | 20 | function fibInc (n) { 21 | requires(Number.isInteger(n)); 22 | requires(n >= 0); 23 | ensures(fib(n) >= n); 24 | ensures(pure()); 25 | 26 | fib(n); 27 | if (n >= 2) { 28 | fibInc(n - 1); fib(n - 1); 29 | fibInc(n - 2); fib(n - 2); 30 | } 31 | } 32 | }); 33 | 34 | verified('fib: pure()'); 35 | verified('fibInc: precondition fibInc((n - 1))'); 36 | verified('fibInc: precondition fibInc((n - 2))'); 37 | verified('fibInc: (fib(n) >= n)'); 38 | }); 39 | 40 | describe('simple higher-order functions', () => { 41 | 42 | code(() => { 43 | function inc (n) { 44 | requires(Number.isInteger(n)); 45 | ensures(res => Number.isInteger(res) && res > n); 46 | 47 | return n + 1; 48 | } 49 | 50 | function twice (f, n) { 51 | requires(spec(f, x => Number.isInteger(x), (x, y) => Number.isInteger(y) && y > x)); 52 | requires(Number.isInteger(n)); 53 | ensures(res => res > n + 1); 54 | 55 | return f(f(n)); 56 | } 57 | 58 | const x = 3; 59 | const y = twice(inc, x); 60 | assert(y > 4); 61 | }); 62 | 63 | verified('inc: (Number.isInteger(res) && (res > n))'); 64 | verified('twice: precondition f(n)'); 65 | verified('twice: precondition f(f(n))'); 66 | verified('twice: (res > (n + 1))'); 67 | verified('precondition twice(inc, x)'); 68 | verified('assert: (y > 4)'); 69 | }); 70 | 71 | describe('higher-order proofs', () => { 72 | 73 | code(() => { 74 | function fib (n) { 75 | requires(Number.isInteger(n)); 76 | requires(n >= 0); 77 | ensures(pure()); 78 | ensures(res => Number.isInteger(res)); 79 | 80 | if (n <= 1) { 81 | return 1; 82 | } else { 83 | return fib(n - 1) + fib(n - 2); 84 | } 85 | } 86 | 87 | function fibInc (n) { 88 | requires(Number.isInteger(n)); 89 | requires(n >= 0); 90 | ensures(fib(n) <= fib(n + 1)); 91 | ensures(pure()); 92 | 93 | fib(n); 94 | fib(n + 1); 95 | 96 | if (n > 0) { 97 | fib(n - 1); 98 | fibInc(n - 1); 99 | } 100 | 101 | if (n > 1) { 102 | fib(n - 2); 103 | fibInc(n - 2); 104 | } 105 | } 106 | 107 | function fMono (f, fInc, n, m) { 108 | requires(spec(f, x => Number.isInteger(x) && x >= 0, 109 | (x, y) => pure() && Number.isInteger(y))); 110 | requires(spec(fInc, x => Number.isInteger(x) && x >= 0, 111 | x => pure() && f(x) <= f(x + 1))); 112 | requires(Number.isInteger(n)); 113 | requires(n >= 0); 114 | requires(Number.isInteger(m)); 115 | requires(m >= 0); 116 | requires(n < m); 117 | ensures(pure()); 118 | ensures(f(n) <= f(m)); 119 | 120 | if (n + 1 === m) { 121 | fInc(n); 122 | } else { 123 | fInc(n); 124 | fMono(f, fInc, n + 1, m); 125 | } 126 | } 127 | 128 | function fibMono (n, m) { 129 | requires(Number.isInteger(n)); 130 | requires(n >= 0); 131 | requires(Number.isInteger(m)); 132 | requires(m >= 0); 133 | requires(n < m); 134 | ensures(pure()); 135 | ensures(fib(n) <= fib(m)); 136 | 137 | fMono(fib, fibInc, n, m); 138 | } 139 | 140 | }); 141 | 142 | verified('fib: precondition fib((n - 1))'); 143 | verified('fib: precondition fib((n - 2))'); 144 | verified('fib: pure()'); 145 | verified('fib: Number.isInteger(res)'); 146 | verified('fibInc: precondition fib(n)'); 147 | verified('fibInc: precondition fib((n + 1))'); 148 | verified('fibInc: precondition fib((n - 1))'); 149 | verified('fibInc: precondition fibInc((n - 1))'); 150 | verified('fibInc: precondition fib((n - 2))'); 151 | verified('fibInc: precondition fibInc((n - 2))'); 152 | verified('fibInc: (fib(n) <= fib((n + 1)))'); 153 | verified('fibInc: pure()'); 154 | verified('fMono: precondition fInc(n)'); 155 | verified('fMono: precondition fInc(n)'); 156 | verified('fMono: precondition fMono(f, fInc, (n + 1), m)'); 157 | verified('fMono: pure()'); 158 | verified('fMono: (f(n) <= f(m))'); 159 | verified('fibMono: precondition fMono(fib, fibInc, n, m)'); 160 | verified('fibMono: pure()'); 161 | verified('fibMono: (fib(n) <= fib(m))'); 162 | }); 163 | 164 | describe('nested function bug', () => { 165 | 166 | code(() => { 167 | function f (x) { 168 | function g (y) { 169 | return x; 170 | } 171 | return g; 172 | } 173 | f(1)(2); 174 | assert(f(1)(2) === f(1)); 175 | }); 176 | 177 | incorrect('assert: (f(1)(2) === f(1))'); 178 | }); 179 | 180 | describe('functions returning functions', () => { 181 | 182 | code(() => { 183 | function twice (f: any) { 184 | requires(spec(f, (x) => typeof(x) === 'number', 185 | (x,y) => typeof(y) === 'number' && y > x)); 186 | ensures(res => spec(res, (x) => typeof(x) === 'number', 187 | (x,y) => typeof(y) === 'number' && y > x + 1)); 188 | return function (x) { 189 | requires(typeof(x) === 'number'); 190 | ensures(y => typeof(y) === 'number' && y > x + 1); 191 | return f(f(x)); 192 | }; 193 | } 194 | }); 195 | 196 | verified('twice: spec(res, x => (typeof(x) === "number"), ' + 197 | '(x, y) => ((typeof(y) === "number") && (y > (x + 1))))'); 198 | }); 199 | 200 | describe('function subtyping with same type', () => { 201 | 202 | code(() => { 203 | function f (g) { 204 | requires(spec(g, x => x > 3, (x, y) => y > x)); 205 | ensures(spec(g , x => x > 3, (x, y) => y > x)); 206 | } 207 | }); 208 | 209 | verified('f: spec(g, x => (x > 3), (x, y) => (y > x))'); 210 | }); 211 | 212 | describe('function subtyping with stronger pre', () => { 213 | 214 | code(() => { 215 | function f (g) { 216 | requires(spec(g, x => x > 3, (x, y) => y > x)); 217 | ensures(spec(g , x => x > 4, (x, y) => y > x)); 218 | } 219 | }); 220 | 221 | verified('f: spec(g, x => (x > 4), (x, y) => (y > x))'); 222 | }); 223 | 224 | describe('function subtyping with weaker pre', () => { 225 | 226 | code(() => { 227 | function f (g) { 228 | requires(spec(g, x => x > 3, (x, y) => y > x)); 229 | ensures(spec(g , x => x > 2, (x, y) => y > x)); 230 | } 231 | }); 232 | 233 | incorrect('f: spec(g, x => (x > 2), (x, y) => (y > x))', ['x', 3]); 234 | }); 235 | 236 | describe('function subtyping with stronger post', () => { 237 | 238 | code(() => { 239 | function f (g) { 240 | requires(spec(g, x => x > 3, (x, y) => y > x)); 241 | ensures(spec(g , x => x > 3, (x, y) => y > x + 1)); 242 | } 243 | }); 244 | 245 | incorrect('f: spec(g, x => (x > 3), (x, y) => (y > (x + 1)))', ['x', 3.5]); 246 | }); 247 | 248 | describe('function subtyping with weaker post', () => { 249 | 250 | code(() => { 251 | function f (g) { 252 | requires(spec(g, x => x > 3, (x, y) => y > x)); 253 | ensures(spec(g , x => x > 3, (x, y) => y >= x)); 254 | } 255 | }); 256 | 257 | verified('f: spec(g, x => (x > 3), (x, y) => (y >= x))'); 258 | }); 259 | -------------------------------------------------------------------------------- /tests/interpreter.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { bisumulate } from './helpers'; 3 | 4 | declare function log (out: any): void; 5 | declare function assert (x: boolean): void; 6 | declare function spec (f: (X) => Y, id: number, r: (rx: X) => [X], s: (sx: X, sy: Y) => Y): (X) => Y; 7 | 8 | describe('interpreter', () => { 9 | 10 | bisumulate('trivial program', () => { 11 | /* tslint:disable:no-unused-expression */ 12 | 0; 13 | }, 1); 14 | 15 | bisumulate('output', () => { 16 | log(0); 17 | }, 3); 18 | 19 | bisumulate('global', () => { 20 | log(Math); 21 | }, 3); 22 | 23 | bisumulate('literals', () => { 24 | log(undefined); 25 | log(null); 26 | log(true); 27 | log(23); 28 | log('foo'); 29 | }, 15); 30 | 31 | bisumulate('unary operators', () => { 32 | log(-4); 33 | log(!false); 34 | log(typeof 23); 35 | log(void(0)); 36 | }, 16); 37 | 38 | bisumulate('binary operators', () => { 39 | log((1 as any) === 2); 40 | log(('foo' as any) !== 'bar'); 41 | log(12 < 23); 42 | log(32 >> 2); 43 | log(1 + 2); 44 | log('foo' + 'bar'); 45 | log(3 * 5); 46 | log(0xFF & 0x80); 47 | }, 40); 48 | 49 | bisumulate('logical expressions', () => { 50 | log(23 && 42); 51 | log(0 && false); 52 | true && log(2); 53 | false && log(3); 54 | log(23 || false); 55 | log(0 || 42); 56 | true || log(2); 57 | false || log(3); 58 | }, 24); 59 | 60 | bisumulate('conditional expressions', () => { 61 | /* tslint:disable:no-constant-condition */ 62 | log(true ? 23 : 42); 63 | log(false ? 'foo' : 'bar'); 64 | true ? log(2) : log(4); 65 | false ? log('f') : log('b'); 66 | }, 16); 67 | 68 | bisumulate('assignment', () => { 69 | let x = 0; 70 | log(x); 71 | x = 23; 72 | log(x); 73 | }, 10); 74 | 75 | bisumulate('sequence expressions', () => { 76 | log((log(0), log(1))); 77 | }, 8); 78 | 79 | bisumulate('calling native functions', () => { 80 | log(Math.max(4, 5)); 81 | log(Number.isInteger(44)); 82 | }, 13); 83 | 84 | bisumulate('calling native methods', () => { 85 | log('hello'.substring(1)); 86 | }, 6); 87 | 88 | bisumulate('calling custom functions', () => { 89 | function inc (x) { 90 | log(x); 91 | return x + 1; 92 | } 93 | log(inc(12)); 94 | }, 13); 95 | 96 | bisumulate('calling custom methods', () => { 97 | const o = { 98 | m: function (x) { 99 | log(x); 100 | return this.p + x; 101 | }, 102 | p: 5 103 | }; 104 | log(o.m(10)); 105 | }, 19); 106 | 107 | bisumulate('new expressions', () => { 108 | class A { x: any; 109 | constructor (x) { 110 | this.x = x; 111 | } 112 | } 113 | const a = new A(23); 114 | log(a.constructor.name); 115 | log(a.x); 116 | }, 17); 117 | 118 | bisumulate('array expressions', () => { 119 | const a = [1, 2, 3, log(4), 5]; 120 | log(a); 121 | log(a[2]); 122 | }, 17); 123 | 124 | bisumulate('object expressions', () => { 125 | const o = { 126 | a: 12, 127 | b: log(24) 128 | }; 129 | log(o); 130 | log(o.a); 131 | log(o.b); 132 | }, 19); 133 | 134 | bisumulate('instanceof expressions', () => { 135 | log([] instanceof Array); 136 | log((23 as any) instanceof Array); 137 | }, 10); 138 | 139 | bisumulate('in expressions', () => { 140 | log(0 in [12]); 141 | log(4 in [12]); 142 | log('a' in { a: 23 }); 143 | log('b' in { a: 23 }); 144 | }, 24); 145 | 146 | bisumulate('member expressions', () => { 147 | log(Math.max); 148 | log(['abc'][0]); 149 | log(['abc'][2]); 150 | log({ a: 23 }.a); 151 | log({ a: 23 }['a']); 152 | log({ a: 23 }['b']); 153 | log((23 as any)['b']); 154 | }, 40); 155 | 156 | bisumulate('function expressions', () => { 157 | log((function f (x) { return x; }).name); 158 | }, 5); 159 | 160 | bisumulate('variable declarations', () => { 161 | const a = 23; 162 | log(a); 163 | let b; 164 | log(b); 165 | const c = a + 1; 166 | log(c); 167 | b = c; 168 | log(b); 169 | let d = b; 170 | log(d); 171 | { 172 | const e = 23; 173 | log(e + a); 174 | } 175 | // @ts-ignore: intentional error 176 | log(e); 177 | }, 36); 178 | 179 | bisumulate('closures', () => { 180 | function adder (x) { 181 | let s = 0; 182 | return y => x + y; 183 | } 184 | 185 | const add2 = adder(2); 186 | log(add2(3)); 187 | log(add2(4)); 188 | 189 | function counter () { 190 | let s = 0; 191 | return () => { s += 1; return s; }; 192 | } 193 | 194 | const counter1 = counter(); 195 | log(counter1()); 196 | log(counter1()); 197 | const counter2 = counter(); 198 | log(counter1()); 199 | log(counter2()); 200 | log(counter1()); 201 | }, 92); 202 | 203 | bisumulate('if statements', () => { 204 | if (true) { 205 | log(12); 206 | } 207 | if (false) { 208 | // @ts-ignore: intentionally unreachable 209 | log(20); 210 | } 211 | if (true) { 212 | log('foo'); 213 | } else { 214 | // @ts-ignore: intentionally unreachable 215 | log('bar'); 216 | } 217 | if ('str') { 218 | log('f'); 219 | } else { 220 | // @ts-ignore: intentionally unreachable 221 | log('b'); 222 | } 223 | if (0) { 224 | // @ts-ignore: intentionally unreachable 225 | log([1, 2]); 226 | } else { 227 | log([2, 1]); 228 | } 229 | }, 19); 230 | 231 | bisumulate('return statements', () => { 232 | function f (x) { 233 | log(x); 234 | if (x === 0) { 235 | log('hello'); 236 | return 23; 237 | } 238 | log('ret'); 239 | return x; 240 | } 241 | log(f(0)); 242 | log(f(1)); 243 | }, 33); 244 | 245 | bisumulate('while statements', () => { 246 | let i = 0; 247 | let s = 0; 248 | while (log(0), i < 4) { 249 | i++; 250 | s += i; 251 | log(i); 252 | log(s); 253 | } 254 | log(i); 255 | log(s); 256 | }, 108); 257 | 258 | bisumulate('class declarations', () => { 259 | class A { 260 | b: number; 261 | constructor (b) { 262 | this.b = b; 263 | } 264 | invariant () { 265 | return typeof this.b === 'number'; 266 | } 267 | m (x) { 268 | log('A.m'); 269 | log(x); 270 | return this.n(x + 1) + x; 271 | } 272 | n (x) { 273 | log('A.n'); 274 | log(x); 275 | return 1; 276 | } 277 | } 278 | 279 | const a = new A(5); 280 | log(a.m(4)); 281 | log(a.n(0)); 282 | log(a.n(5)); 283 | log((new A(-1)).m(-4)); 284 | 285 | class B { 286 | constructor () { /* emtpy */ } 287 | method () { 288 | log('B.method'); 289 | return 23; 290 | } 291 | } 292 | 293 | const b = new B(); 294 | const bm = b.method; 295 | log(bm()); 296 | }, 112); 297 | 298 | bisumulate('assert', () => { 299 | assert(1 < 3); 300 | assert(Number.isInteger(23)); 301 | assert(0 > 23); 302 | }, 15); 303 | 304 | bisumulate('spec, unwrapped call', () => { 305 | function f (x) { log(x); return x; } 306 | const f2 = spec(f, 23, x => { assert(x > 0); return [x]; }, (x, y) => { assert(y > 0); return y; }); 307 | log(f(-2)); 308 | }, 19); 309 | 310 | bisumulate('spec, valid call', () => { 311 | function f (x) { log(x); return x; } 312 | const f2 = spec(f, 23, x => { assert(x > 0); return [x]; }, (x, y) => { assert(y > 0); return y; }); 313 | log(f2(2)); 314 | }, 37); 315 | 316 | bisumulate('spec, violate pre', () => { 317 | function f (x) { log(x); return 23; } 318 | const f2 = spec(f, 23, x => { assert(x > 0); return [x]; }, (x, y) => { assert(y > 0); return y; }); 319 | log(f2(-2)); 320 | }, 17); 321 | 322 | bisumulate('spec, violate post', () => { 323 | function f (x) { log(x); return -4; } 324 | const f2 = spec(f, 23, x => { assert(x > 0); return [x]; }, (x, y) => { assert(y > 0); return y; }); 325 | log(f2(2)); 326 | }, 32); 327 | }); 328 | -------------------------------------------------------------------------------- /tests/mergesort.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { code, verified } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 6 | declare function requires (x: boolean): void; 7 | declare function pure (): boolean; 8 | 9 | describe('merge sort', () => { 10 | 11 | code(() => { 12 | class IntList { 13 | head: number; 14 | tail: null | IntList; 15 | constructor (head, tail) { 16 | this.head = head; this.tail = tail; 17 | } 18 | invariant () { 19 | return typeof(this.head) === 'number' && (this.tail === null || this.tail instanceof IntList); 20 | } 21 | } 22 | 23 | class IntListPartition { 24 | left: IntList; 25 | right: IntList; 26 | constructor (left, right) { 27 | this.left = left; this.right = right; 28 | } 29 | invariant () { 30 | return (this.left === null || this.left instanceof IntList) && 31 | (this.right === null || this.right instanceof IntList); 32 | } 33 | } 34 | 35 | function partition (lst, fst, snd, alternate) { 36 | requires(lst === null || lst instanceof IntList); 37 | requires(fst === null || fst instanceof IntList); 38 | requires(snd === null || snd instanceof IntList); 39 | requires(typeof(alternate) === 'boolean'); 40 | ensures(res => res instanceof IntListPartition); 41 | ensures(pure()); 42 | 43 | if (lst === null) { 44 | return new IntListPartition(fst, snd); 45 | } else if (alternate) { 46 | return partition(lst.tail, new IntList(lst.head, fst), snd, false); 47 | } else { 48 | return partition(lst.tail, fst, new IntList(lst.head, snd), true); 49 | } 50 | } 51 | 52 | function isSorted (list) { 53 | requires(list === null || list instanceof IntList); 54 | ensures(res => typeof(res) === 'boolean'); 55 | ensures(pure()); 56 | 57 | return list === null || list.tail === null || 58 | list.head <= list.tail.head && isSorted(list.tail); 59 | } 60 | 61 | function merge (left, right) { 62 | requires(left === null || left instanceof IntList); 63 | requires(isSorted(left)); 64 | requires(right === null || right instanceof IntList); 65 | requires(isSorted(right)); 66 | ensures(res => res === null || res instanceof IntList); 67 | ensures(res => isSorted(res)); 68 | ensures(res => (left === null && right === null) === (res === null)); 69 | ensures(res => !(left !== null && (right === null || right.head >= left.head)) 70 | || 71 | (res !== null && res.head === left.head)); 72 | ensures(res => !(right !== null && (left === null || right.head < left.head)) 73 | || 74 | (res !== null && res.head === right.head)); 75 | ensures(pure()); 76 | 77 | if (left === null) { 78 | return right; 79 | } else if (right === null) { 80 | return left; 81 | } else if (left.head <= right.head) { 82 | isSorted(left); 83 | isSorted(left.tail); 84 | const merged = merge(left.tail, right); 85 | const res = new IntList(left.head, merged); 86 | isSorted(res); 87 | return res; 88 | } else { 89 | isSorted(right); 90 | isSorted(right.tail); 91 | const merged = merge(left, right.tail); 92 | const res = new IntList(right.head, merged); 93 | isSorted(res); 94 | return res; 95 | } 96 | } 97 | 98 | function sort (list) { 99 | requires(list === null || list instanceof IntList); 100 | ensures(res => res === null || res instanceof IntList); 101 | ensures(res => isSorted(res)); 102 | ensures(pure()); 103 | 104 | if (list === null || list.tail === null) { 105 | isSorted(list); 106 | assert(isSorted(list)); 107 | return list; 108 | } 109 | const part = partition(list, null, null, false); 110 | return merge(sort(part.left), sort(part.right)); 111 | } 112 | }); 113 | 114 | verified('partition: class invariant IntListPartition'); 115 | verified('partition: lst has property "head"'); 116 | verified('partition: lst has property "tail"'); 117 | verified('partition: class invariant IntList'); 118 | verified('partition: precondition partition(lst.tail, new IntList(lst.head, fst), snd, false)'); 119 | verified('partition: precondition partition(lst.tail, fst, new IntList(lst.head, snd), true)'); 120 | verified('partition: (res instanceof IntListPartition)'); 121 | verified('partition: pure()'); 122 | verified('isSorted: list has property "head"'); 123 | verified('isSorted: list has property "tail"'); 124 | verified('isSorted: precondition isSorted(list.tail)'); 125 | verified('isSorted: (typeof(res) === "boolean")'); 126 | verified('isSorted: pure()'); 127 | verified('merge: left has property "head"'); 128 | verified('merge: left has property "tail"'); 129 | verified('merge: right has property "head"'); 130 | verified('merge: right has property "tail"'); 131 | verified('merge: precondition isSorted(left)'); 132 | verified('merge: precondition isSorted(left.tail)'); 133 | verified('merge: precondition merge(left.tail, right)'); 134 | verified('merge: class invariant IntList'); 135 | verified('merge: precondition isSorted(res)'); 136 | verified('merge: precondition isSorted(right)'); 137 | verified('merge: precondition isSorted(right.tail)'); 138 | verified('merge: precondition merge(left, right.tail)'); 139 | verified('merge: class invariant IntList'); 140 | verified('merge: precondition isSorted(res)'); 141 | verified('merge: ((res === null) || (res instanceof IntList))'); 142 | verified('merge: isSorted(res)'); 143 | verified('merge: (((left === null) && (right === null)) === (res === null))'); 144 | verified('merge: (!((left !== null) && ((right === null) || (right.head >= left.head))) || ' + 145 | '((res !== null) && (res.head === left.head)))'); 146 | verified('merge: (!((right !== null) && ((left === null) || (right.head < left.head))) || ' + 147 | '((res !== null) && (res.head === right.head)))'); 148 | verified('merge: pure()'); 149 | verified('sort: list has property "tail"'); 150 | verified('sort: precondition isSorted(list)'); 151 | verified('sort: assert: isSorted(list)'); 152 | verified('sort: precondition partition(list, null, null, false)'); 153 | verified('sort: part has property "left"'); 154 | verified('sort: precondition sort(part.left)'); 155 | verified('sort: part has property "right"'); 156 | verified('sort: precondition sort(part.right)'); 157 | verified('sort: precondition merge(sort(part.left), sort(part.right))'); 158 | verified('sort: ((res === null) || (res instanceof IntList))'); 159 | verified('sort: isSorted(res)'); 160 | verified('sort: pure()'); 161 | }); 162 | -------------------------------------------------------------------------------- /tests/mutable-state.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { code, incorrect, unverified, verified } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function invariant (x: boolean): void; 6 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 7 | declare function requires (x: boolean): void; 8 | declare function pure (): boolean; 9 | declare function old (x: T): T; 10 | 11 | describe('counter', () => { 12 | 13 | code(() => { 14 | let counter = 0; 15 | invariant(Number.isInteger(counter)); 16 | invariant(counter >= 0); 17 | 18 | function increment () { 19 | ensures(counter > old(counter)); 20 | 21 | counter++; 22 | } 23 | 24 | function decrement () { 25 | ensures(old(counter) > 0 ? counter < old(counter) : counter === old(counter)); 26 | 27 | if (counter > 0) counter--; 28 | } 29 | }); 30 | 31 | verified('initially: Number.isInteger(counter)'); 32 | verified('initially: (counter >= 0)'); 33 | verified('increment: (counter > old(counter))'); 34 | verified('increment: Number.isInteger(counter)'); 35 | verified('increment: (counter >= 0)'); 36 | verified('decrement: ((old(counter) > 0) ? (counter < old(counter)) : (counter === old(counter)))'); 37 | verified('decrement: Number.isInteger(counter)'); 38 | verified('decrement: (counter >= 0)'); 39 | }); 40 | 41 | describe('simple steps', () => { 42 | 43 | code(() => { 44 | let i = 0; 45 | assert(i < 1); 46 | i = 3; 47 | assert(i < 2); 48 | }); 49 | 50 | verified('assert: (i < 1)'); 51 | incorrect('assert: (i < 2)', [{ name: 'i', heap: 2 }, 3]); 52 | }); 53 | 54 | describe('loop', () => { 55 | 56 | code(() => { 57 | let i = 0; 58 | 59 | while (i < 5) { 60 | invariant(i <= 5); 61 | invariant(Number.isInteger(i)); 62 | i++; 63 | } 64 | 65 | assert(i === 5); 66 | }); 67 | 68 | verified('invariant on entry: (i <= 5)'); 69 | verified('invariant on entry: Number.isInteger(i)'); 70 | verified('invariant maintained: (i <= 5)'); 71 | verified('invariant maintained: Number.isInteger(i)'); 72 | verified('assert: (i === 5)'); 73 | }); 74 | 75 | describe('loop with missing invariant', () => { 76 | 77 | code(() => { 78 | let i = 0; 79 | 80 | while (i < 5) { 81 | i++; 82 | } 83 | 84 | assert(i === 5); 85 | }); 86 | 87 | unverified('assert: (i === 5)', [{ name: 'i', heap: 2 }, false]); 88 | }); 89 | 90 | describe('sum', () => { 91 | 92 | code(() => { 93 | function sumTo (n) { 94 | requires(Number.isInteger(n)); 95 | requires(n >= 0); 96 | ensures(res => res === (n + 1) * n / 2); 97 | 98 | let i = 0; 99 | let s = 0; 100 | while (i < n) { 101 | invariant(i <= n); 102 | invariant(s === (i + 1) * i / 2); 103 | invariant(Number.isInteger(i)); 104 | invariant(Number.isInteger(s)); 105 | i++; 106 | s = s + i; 107 | } 108 | return s; 109 | } 110 | }); 111 | 112 | verified('sumTo: invariant on entry: (i <= n)'); 113 | verified('sumTo: invariant on entry: (s === (((i + 1) * i) / 2))'); 114 | verified('sumTo: invariant on entry: Number.isInteger(i)'); 115 | verified('sumTo: invariant on entry: Number.isInteger(s)'); 116 | verified('sumTo: invariant maintained: (i <= n)'); 117 | verified('sumTo: invariant maintained: (s === (((i + 1) * i) / 2))'); 118 | verified('sumTo: invariant maintained: Number.isInteger(i)'); 119 | verified('sumTo: invariant maintained: Number.isInteger(s)'); 120 | verified('sumTo: (res === (((n + 1) * n) / 2))'); 121 | }); 122 | 123 | describe('mutable variables', () => { 124 | 125 | code(() => { 126 | let x = 2; 127 | const y = 3; 128 | function f (z) { 129 | requires(x < y); 130 | } 131 | f(0); 132 | x = 4; 133 | f(1); 134 | }); 135 | 136 | verified('precondition f(0)'); 137 | incorrect('precondition f(1)'); 138 | }); 139 | 140 | describe('pure functions', () => { 141 | 142 | code(() => { 143 | let x = 0; 144 | 145 | function f () { ensures(pure()); x++; } 146 | function g () { ensures(pure()); return x + 1; } 147 | function h1 (y) { /*empty*/ } 148 | function h2a () { h1(3); } 149 | function h2b () { ensures(pure()); h1(4); } 150 | function h3a () { ensures(pure()); h2a(); } 151 | function h3b () { ensures(pure()); h2b(); } 152 | }); 153 | 154 | unverified('f: pure()'); // not pure 155 | verified('g: pure()'); // pure 156 | verified('h2b: pure()'); // inlined h1 pure 157 | unverified('h3a: pure()'); // pure, but only one level inlining 158 | verified('h3b: pure()'); // calling other pure function 159 | }); 160 | 161 | describe('global mutable variable with missing invariant', () => { 162 | 163 | code(() => { 164 | let x = 23; 165 | let y = 42; 166 | let z = 69; 167 | 168 | invariant(typeof y === 'number'); 169 | 170 | invariant(typeof z === 'number' && z > 22); 171 | 172 | function f () { 173 | ensures(res => res > 22); 174 | 175 | return x; 176 | } 177 | 178 | function g () { 179 | ensures(res => res > 22); 180 | 181 | return y; 182 | } 183 | 184 | function h () { 185 | ensures(res => res > 22); 186 | 187 | return z; 188 | } 189 | }); 190 | 191 | incorrect('f: (res > 22)', [{ name: 'x', heap: 3 }, 23], [{ name: 'x', heap: 4 }, false]); 192 | incorrect('g: (res > 22)', [{ name: 'y', heap: 3 }, 42], [{ name: 'y', heap: 4 }, 22]); 193 | verified('h: (res > 22)'); 194 | }); 195 | -------------------------------------------------------------------------------- /tests/objects.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { code, incorrect, verified } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 6 | declare function requires (x: boolean): void; 7 | 8 | describe('simple object access', () => { 9 | 10 | code(() => { 11 | function f (a) { 12 | requires('b' in a); 13 | requires(a.b >= 1); 14 | ensures(res => res >= 0); 15 | 16 | return a.b; 17 | } 18 | 19 | function g (a) { 20 | requires('b' in a); 21 | requires(a.b >= 1); 22 | ensures(res => res < 0); 23 | 24 | return a.b; 25 | } 26 | 27 | const a = { b: 23 }; 28 | assert(a instanceof Object); 29 | assert('b' in a); 30 | assert(a.b > 22); 31 | assert(a['b'] > 22); 32 | const p = 'b'; 33 | assert(a[p] > 22); 34 | }); 35 | 36 | verified('f: a has property "b"'); 37 | verified('f: (res >= 0)'); 38 | verified('g: a has property "b"'); 39 | incorrect('g: (res < 0)', ['a', { _str_: 3, b: 1, length: 0 }]); 40 | verified('assert: (a instanceof Object)'); 41 | verified('assert: ("b" in a)'); 42 | verified('assert: (a.b > 22)'); 43 | verified('assert: (a[p] > 22)'); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/simple.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { code, incorrect, unverified, verified, vcs } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 6 | declare function requires (x: boolean): void; 7 | declare function spec (f: any, r: (rx: any) => boolean, s: (sx: any, sy: any) => boolean): boolean; 8 | 9 | describe('max()', () => { 10 | 11 | code(() => { 12 | function max (a, b) { 13 | requires(typeof(a) === 'number'); 14 | requires(typeof(b) === 'number'); 15 | ensures(res => res >= a); 16 | 17 | if (a >= b) { 18 | return a; 19 | } else { 20 | return b; 21 | } 22 | } 23 | }); 24 | 25 | it('finds a verification conditions', () => { 26 | expect(vcs()).to.have.length(4); 27 | }); 28 | 29 | it('has a description', async () => { 30 | expect(vcs()[3].getDescription()).to.be.eql('max: (res >= a)'); 31 | }); 32 | 33 | verified('max: (res >= a)'); 34 | }); 35 | 36 | describe('max() with missing pre', () => { 37 | 38 | code(() => { 39 | function max (a, b) { 40 | requires(typeof(a) === 'number'); 41 | ensures(res => res >= a); 42 | 43 | if (a >= b) { 44 | return a; 45 | } else { 46 | return b; 47 | } 48 | } 49 | }); 50 | 51 | unverified('max: (res >= a)', ['b', false]); 52 | }); 53 | 54 | describe('global call', () => { 55 | 56 | code(() => { 57 | function inc (n) { 58 | requires(typeof(n) === 'number'); 59 | ensures(res => res > n); 60 | 61 | return n + 1; 62 | } 63 | 64 | let i = 3; 65 | let j = inc(i); 66 | assert(j > 3); 67 | }); 68 | 69 | verified('precondition inc(i)'); 70 | verified('assert: (j > 3)'); 71 | verified('inc: (res > n)'); 72 | }); 73 | 74 | describe('assert within function', () => { 75 | 76 | code(() => { 77 | function f (n) { 78 | requires(typeof(n) === 'number'); 79 | ensures(res => res > 3); 80 | 81 | assert(n > 3); 82 | return n; 83 | } 84 | }); 85 | 86 | incorrect('f: assert: (n > 3)', ['n', 3]); 87 | verified('f: (res > 3)'); 88 | }); 89 | 90 | describe('inline global call', () => { 91 | 92 | code(() => { 93 | function inc (n) { 94 | return n + 1; 95 | } 96 | function inc2 (n) { 97 | return inc(inc(n)); 98 | } 99 | 100 | let i = 3; 101 | let j = inc(i); 102 | assert(j === 4); 103 | let k = inc2(i); 104 | assert(k === 5); 105 | }); 106 | 107 | verified('assert: (j === 4)'); 108 | unverified('assert: (k === 5)', [{ name: 'k', heap: 4 }, 8]); // only inline one level 109 | }); 110 | 111 | describe('post conditions global call', () => { 112 | 113 | code(() => { 114 | function inc (n) { 115 | requires(typeof(n) === 'number'); 116 | ensures(res => res > n); 117 | 118 | return n + 1; 119 | } 120 | function inc2 (n) { 121 | return inc(inc(n)); 122 | } 123 | 124 | let i = 3; 125 | let j = inc(i); 126 | assert(j >= 4); 127 | let k = inc2(i); 128 | assert(k >= 5); 129 | }); 130 | 131 | verified('inc: (res > n)'); 132 | incorrect('inc2: precondition inc(n)', ['n', true]); 133 | incorrect('inc2: precondition inc(inc(n))'); 134 | verified('precondition inc(i)'); 135 | verified('assert: (j >= 4)'); 136 | verified('precondition inc2(i)'); 137 | unverified('assert: (k >= 5)', [{ name: 'k', heap: 4 }, 9]); 138 | // only inline one level, so post-cond of inc(inc(i)) not available 139 | }); 140 | 141 | describe('closures', () => { 142 | 143 | code(() => { 144 | function cons (x) { 145 | function f () { return x; } 146 | return f; 147 | } 148 | const g = cons(1); 149 | const g1 = g(); 150 | assert(g1 === 1); 151 | const h = cons(2); 152 | const h1 = h(); 153 | assert(h1 === 2); 154 | }); 155 | 156 | verified('assert: (g1 === 1)'); 157 | verified('assert: (h1 === 2)'); 158 | }); 159 | 160 | describe('fibonacci', () => { 161 | 162 | code(() => { 163 | function fib (n) { 164 | requires(Number.isInteger(n)); 165 | requires(n >= 0); 166 | ensures(res => res >= n); 167 | ensures(res => res >= 1); 168 | 169 | if (n <= 1) return 1; 170 | return fib(n - 1) + fib(n - 2); 171 | } 172 | }); 173 | 174 | verified('fib: precondition fib((n - 1))'); 175 | verified('fib: precondition fib((n - 2))'); 176 | verified('fib: (res >= n)'); 177 | verified('fib: (res >= 1)'); 178 | }); 179 | 180 | describe('buggy fibonacci', () => { 181 | 182 | code(() => { 183 | function fib (n) { 184 | requires(Number.isInteger(n)); 185 | requires(n >= 0); 186 | ensures(res => res >= n); 187 | 188 | if (n <= 1) return n; 189 | return fib(n - 1) + fib(n - 2); 190 | } 191 | }); 192 | 193 | verified('fib: precondition fib((n - 1))'); 194 | verified('fib: precondition fib((n - 2))'); 195 | incorrect('fib: (res >= n)', ['n', 2]); 196 | }); 197 | 198 | describe('function expressions', () => { 199 | 200 | code(() => { 201 | const x = (function (z: number) { return z; })(3); 202 | assert(x === 3); 203 | const y = ((z: number) => z)(4); 204 | assert(y === 4); 205 | }); 206 | 207 | verified('assert: (x === 3)'); 208 | verified('assert: (y === 4)'); 209 | }); 210 | 211 | describe('function bug', () => { 212 | 213 | code(() => { 214 | function f (x) { 215 | ensures(!spec(f, y => true, y => y !== x)); 216 | f(x); 217 | } 218 | }); 219 | 220 | verified('f: !spec(f, y => true, y => (y !== x))'); 221 | }); 222 | 223 | describe('integers', () => { 224 | 225 | code(() => { 226 | function f (i) { 227 | requires(Number.isInteger(i)); 228 | ensures(res => Number.isInteger(res) && res > i && res <= i + 1); 229 | 230 | return i + 1; 231 | } 232 | 233 | function g (i) { 234 | requires(Number.isInteger(i)); 235 | requires(i === 9 / 2); 236 | ensures(1 + 1 === 1); 237 | } 238 | 239 | function h (i) { 240 | requires(Number.isInteger(i) && i > 1 && i < 4); 241 | ensures(res => res > 2); 242 | } 243 | }); 244 | 245 | verified('f: ((Number.isInteger(res) && (res > i)) && (res <= (i + 1)))'); 246 | verified('g: ((1 + 1) === 1)'); 247 | incorrect('h: (res > 2)', ['i', 2]); 248 | }); 249 | 250 | describe('reals', () => { 251 | 252 | code(() => { 253 | function f (i) { 254 | requires(typeof i === 'number'); 255 | ensures(res => typeof res === 'number' && res > i && res < i + 1); 256 | 257 | return i + 0.5; 258 | } 259 | 260 | function g (i) { 261 | requires(typeof i === 'number'); 262 | requires(i === 9 / 2); 263 | ensures(1 + 1 === 1); 264 | } 265 | 266 | function h (i) { 267 | requires(typeof i === 'number' && i > 1 && i < 2); 268 | ensures(res => res >= 1.5); 269 | } 270 | }); 271 | 272 | verified('f: (((typeof(res) === "number") && (res > i)) && (res < (i + 1)))'); 273 | incorrect('g: ((1 + 1) === 1)', ['i', 4.5]); 274 | incorrect('h: (res >= 1.5)', ['i', 1.5]); 275 | }); 276 | 277 | describe('plus operator', () => { 278 | 279 | code(() => { 280 | const a = 23; 281 | const b = 42; 282 | const c = a + b; 283 | assert(c === 65); 284 | 285 | const s1 = 'Hello,'; 286 | const s2 = 'world!'; 287 | const s3 = s1 + s2; 288 | assert(s3 === 'Hello,world!'); 289 | 290 | const x = a + s1; 291 | const y = s2 + b; 292 | // @ts-ignore: intentional error 293 | const z = [] + { }; 294 | }); 295 | 296 | verified('operator + requires number or string: b'); 297 | verified('operator + requires number or string: b'); 298 | verified('assert: (c === 65)'); 299 | verified('operator + requires number or string: s1'); 300 | verified('operator + requires number or string: s2'); 301 | verified('assert: (s3 === "Hello,world!")'); 302 | verified('operator + requires number or string: a'); 303 | verified('operator + requires number or string: s1'); 304 | verified('operator + requires number or string: s2'); 305 | verified('operator + requires number or string: b'); 306 | incorrect('operator + requires number or string: []'); 307 | incorrect('operator + requires number or string: { }'); 308 | }); 309 | -------------------------------------------------------------------------------- /tests/strings.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { code, incorrect, verified, unverified } from './helpers'; 3 | 4 | declare function assert (x: boolean): void; 5 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 6 | declare function requires (x: boolean): void; 7 | 8 | describe('string concatenation', () => { 9 | code(() => { 10 | function f (x, y) { 11 | requires(typeof x === 'string'); 12 | requires(typeof y === 'string'); 13 | requires(x + y === 'ab'); 14 | ensures(z => z === 'abc'); 15 | 16 | return x + y; 17 | } 18 | 19 | const s1 = 'hello'; 20 | const s2 = 'world'; 21 | const s3 = s1 + ' ' + s2; 22 | assert(s3 === 'hello world'); 23 | assert(s1 + ' ' + s2 === 'hello world'); 24 | assert(s2 + s1 === 'helloworld'); 25 | }); 26 | 27 | incorrect('f: (z === "abc")', ['x', 'ab'], ['y', '']); 28 | verified('assert: (s3 === "hello world")'); 29 | verified('assert: (((s1 + " ") + s2) === "hello world")'); 30 | incorrect('assert: ((s2 + s1) === "helloworld")'); 31 | }); 32 | 33 | describe('string length', () => { 34 | code(() => { 35 | function f (x) { 36 | requires(typeof x === 'string'); 37 | ensures(z => z.length !== 3); 38 | return x; 39 | } 40 | 41 | const s1 = 'hello'; 42 | const l1 = s1.length; 43 | assert(l1 === 5); 44 | assert(s1.length === 5); 45 | const l2 = (s1 + ' world').length; 46 | const l3 = s1.length + ' world'.length; 47 | assert(l2 === l3); 48 | assert(l2 === 11); 49 | assert(l2 > 11); 50 | 51 | }); 52 | 53 | incorrect('f: (z.length !== 3)', ['x', '\u0000\u0000\u0000']); 54 | verified('s1 has property "length"'); 55 | verified('assert: (l1 === 5)'); 56 | verified('assert: (s1.length === 5)'); 57 | verified('(s1 + " world") has property "length"'); 58 | verified('s1 has property "length"'); 59 | verified('" world" has property "length"'); 60 | verified('assert: (l2 === l3)'); 61 | verified('assert: (l2 === 11)'); 62 | incorrect('assert: (l2 > 11)'); 63 | }); 64 | 65 | describe('string access character by index', () => { 66 | code(() => { 67 | function f (x) { 68 | requires(typeof x === 'string'); 69 | requires(x[0] === 'a'); 70 | ensures(z => z.length !== 2); 71 | return x; 72 | } 73 | 74 | const s1 = 'hello'; 75 | const c1 = s1[0]; 76 | const c2 = s1[3 - 2]; 77 | assert(s1[0] === 'h'); 78 | assert(c1 === 'h'); 79 | assert(c2 === 'e'); 80 | const c3 = s1[6]; 81 | assert(c3 === undefined); 82 | }); 83 | 84 | incorrect('f: (z.length !== 2)', ['x', 'a\u0000']); 85 | verified('s1 has property 0'); 86 | verified('s1 has property (3 - 2)'); 87 | verified('assert: (s1[0] === "h")'); 88 | verified('assert: (c1 === "h")'); 89 | verified('assert: (c2 === "e")'); 90 | incorrect('s1 has property 6'); 91 | verified('assert: (c3 === undefined)'); 92 | }); 93 | 94 | describe('string substr', () => { 95 | 96 | code(() => { 97 | const str = 'abcd'; 98 | const substr = str.substr(1, 2); 99 | assert(substr === 'bc'); 100 | 101 | function f (a) { 102 | requires(typeof a === 'string'); 103 | requires(a.length === 6); 104 | ensures(y => y.length === 2); 105 | ensures(y => y[1] === a[3]); 106 | 107 | return a.substr(2, 2); 108 | } 109 | 110 | function g (a) { 111 | requires(typeof a === 'string'); 112 | requires(a.length === 6); 113 | ensures(y => y[1] !== a[3]); 114 | 115 | return a.substr(2, 2); 116 | } 117 | 118 | const d = 'abc'; 119 | d.substr(-1, 4); 120 | }); 121 | 122 | verified('precondition str.substr(1, 2)'); 123 | verified('assert: (substr === "bc")'); 124 | verified('f: a has property "substr"'); 125 | verified('f: precondition a.substr(2, 2)'); 126 | verified('f: (y.length === 2)'); 127 | verified('f: (y[1] === a[3])'); 128 | verified('g: a has property "substr"'); 129 | verified('g: precondition a.substr(2, 2)'); 130 | incorrect('g: (y[1] !== a[3])', ['a', '\u0000\u0000\u0000\u0000\u0000\u0000']).timeout(4000); 131 | unverified('precondition d.substr(-1, 4)'); 132 | }); 133 | 134 | describe('string substring', () => { 135 | 136 | code(() => { 137 | const str = 'abc'; 138 | const substr = str.substring(1, 2); 139 | assert(substr === 'b'); 140 | 141 | function f (a) { 142 | requires(typeof a === 'string'); 143 | requires(a.length === 6); 144 | ensures(y => y.length === 2); 145 | ensures(y => y[1] === a[3]); 146 | 147 | return a.substring(2, 4); 148 | } 149 | 150 | function g (a) { 151 | requires(typeof a === 'string'); 152 | requires(a.length === 6); 153 | ensures(y => y[1] !== a[3]); 154 | 155 | return a.substring(2, 4); 156 | } 157 | 158 | const d = 'abc'; 159 | d.substring(1, 4); 160 | }); 161 | 162 | verified('precondition str.substring(1, 2)'); 163 | verified('assert: (substr === "b")'); 164 | verified('f: a has property "substring"'); 165 | verified('f: precondition a.substring(2, 4)'); 166 | verified('f: (y.length === 2)'); 167 | verified('f: (y[1] === a[3])'); 168 | verified('g: a has property "substring"'); 169 | verified('g: precondition a.substring(2, 4)'); 170 | incorrect('g: (y[1] !== a[3])', ['a', '\u0000\u0000\u0000\u0000\u0000\u0000']); 171 | unverified('precondition d.substring(1, 4)'); 172 | }); 173 | 174 | describe('string class', () => { 175 | 176 | code(() => { 177 | const str = new String('abc'); 178 | assert(str instanceof String); 179 | const l1 = str.length; 180 | assert(l1 === 5); 181 | const c1 = str[0]; 182 | assert(c1 === 'a'); 183 | 184 | const substr = str.substring(1, 2); 185 | assert(substr[0] === 'b'); 186 | 187 | function f (a) { 188 | requires(a instanceof String); 189 | requires(a.length === 6); 190 | ensures(y => y.length === 2); 191 | ensures(y => y[1] === a[3]); 192 | 193 | return a.substring(2, 4); 194 | } 195 | }); 196 | 197 | verified('precondition str.substring(1, 2)'); 198 | }); 199 | -------------------------------------------------------------------------------- /tests/synthesis.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { code, incorrect, unverified, verified, vcs } from './helpers'; 4 | 5 | declare function assert (x: boolean): void; 6 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 7 | declare function requires (x: boolean): void; 8 | declare function spec (f: any, r: (rx: any) => boolean, s: (sx: any, sy: any) => boolean): boolean; 9 | 10 | describe('function counter examples', () => { 11 | 12 | code(() => { 13 | function g1 (f) { 14 | requires(spec(f, () => true, y => y === 0)); 15 | ensures(res => res === 1); 16 | 17 | return f(); 18 | } 19 | 20 | function g2 (f) { 21 | requires(spec(f, x => true, (x, y) => true)); 22 | requires(f(1) === 0); 23 | ensures(res => res === 1); 24 | 25 | return f(1); 26 | } 27 | 28 | function g3 (f) { 29 | requires(spec(f, x => x > 4, (x, y) => y === 23)); 30 | ensures(res => res < 20); 31 | 32 | return f(5); 33 | } 34 | 35 | }); 36 | 37 | verified('g1: precondition f()'); 38 | incorrect('g1: (res === 1)'); 39 | it('g1: (res === 1) returns counterexample', async () => { 40 | const m = await vcs()[1].verify(); 41 | expect(m.description).to.eql('g1: (res === 1)'); 42 | expect(m.status).to.equal('error'); 43 | if (m.status !== 'error') throw new Error(); 44 | expect(m.type).to.equal('incorrect'); 45 | if (m.type !== 'incorrect') throw new Error(); 46 | expect(m.model.variables()).to.include('f'); 47 | const f = m.model.valueOf('f'); 48 | expect(f.type).to.eql('fun'); 49 | if (f.type !== 'fun') throw new Error(); 50 | expect(f.body.id === null); 51 | expect(f.body.params).to.have.length(0); 52 | expect(f.body.body.body).to.have.length(1); 53 | const retStmt = f.body.body.body[0]; 54 | expect(retStmt.type).to.eql('ReturnStatement'); 55 | if (retStmt.type !== 'ReturnStatement') throw new Error(); 56 | const arg = retStmt.argument; 57 | expect(arg.type).to.eql('Literal'); 58 | if (arg.type !== 'Literal') throw new Error(); 59 | expect(arg.value).to.eql(0); 60 | }); 61 | verified('g2: precondition f(1)'); 62 | incorrect('g2: (res === 1)'); 63 | it('g2: (res === 1) returns counterexample', async () => { 64 | const m = await vcs()[3].verify(); 65 | expect(m.description).to.eql('g2: (res === 1)'); 66 | expect(m.status).to.equal('error'); 67 | if (m.status !== 'error') throw new Error(); 68 | expect(m.type).to.equal('incorrect'); 69 | if (m.type !== 'incorrect') throw new Error(); 70 | expect(m.model.variables()).to.include('f'); 71 | const f = m.model.valueOf('f'); 72 | expect(f.type).to.eql('fun'); 73 | if (f.type !== 'fun') throw new Error(); 74 | expect(f.body.id === null); 75 | expect(f.body.params).to.have.length(1); 76 | expect(f.body.params[0].name).to.eql('x_0'); 77 | expect(f.body.body.body).to.have.length(2); 78 | 79 | const ifStmt = f.body.body.body[0]; 80 | expect(ifStmt.type).to.eql('IfStatement'); 81 | if (ifStmt.type !== 'IfStatement') throw new Error(); 82 | const cond = ifStmt.test; 83 | expect(cond.type).to.eql('BinaryExpression'); 84 | if (cond.type !== 'BinaryExpression') throw new Error(); 85 | expect(cond.operator).to.eql('==='); 86 | const condLeft = cond.left; 87 | expect(condLeft.type).to.eql('Identifier'); 88 | if (condLeft.type !== 'Identifier') throw new Error(); 89 | expect(condLeft.name).to.eql('x_0'); 90 | const condRight = cond.right; 91 | expect(condRight.type).to.eql('Literal'); 92 | if (condRight.type !== 'Literal') throw new Error(); 93 | expect(condRight.value).to.eql(1); 94 | 95 | expect(ifStmt.consequent.body).to.have.length(1); 96 | const thenStmt = ifStmt.consequent.body[0]; 97 | expect(thenStmt.type).to.eql('ReturnStatement'); 98 | if (thenStmt.type !== 'ReturnStatement') throw new Error(); 99 | const thenArg = thenStmt.argument; 100 | expect(thenArg.type).to.eql('Literal'); 101 | if (thenArg.type !== 'Literal') throw new Error(); 102 | expect(thenArg.value).to.eql(0); 103 | 104 | expect(ifStmt.alternate.body).to.have.length(0); 105 | 106 | const retStmt = f.body.body.body[1]; 107 | expect(retStmt.type).to.eql('ReturnStatement'); 108 | if (retStmt.type !== 'ReturnStatement') throw new Error(); 109 | const arg = retStmt.argument; 110 | expect(arg.type).to.eql('Literal'); 111 | if (arg.type !== 'Literal') throw new Error(); 112 | expect(arg.value).to.eql(undefined); 113 | }); 114 | verified('g3: precondition f(5)'); 115 | incorrect('g3: (res < 20)'); 116 | it('g3: (res < 20) returns counterexample', async () => { 117 | const m = await vcs()[5].verify(); 118 | expect(m.description).to.eql('g3: (res < 20)'); 119 | expect(m.status).to.equal('error'); 120 | if (m.status !== 'error') throw new Error(); 121 | expect(m.type).to.equal('incorrect'); 122 | if (m.type !== 'incorrect') throw new Error(); 123 | expect(m.model.variables()).to.include('f'); 124 | const f = m.model.valueOf('f'); 125 | expect(f.type).to.eql('fun'); 126 | if (f.type !== 'fun') throw new Error(); 127 | expect(f.body.id === null); 128 | expect(f.body.params).to.have.length(1); 129 | expect(f.body.params[0].name).to.eql('x_0'); 130 | expect(f.body.body.body).to.have.length(2); 131 | 132 | const ifStmt = f.body.body.body[0]; 133 | expect(ifStmt.type).to.eql('IfStatement'); 134 | if (ifStmt.type !== 'IfStatement') throw new Error(); 135 | const cond = ifStmt.test; 136 | expect(cond.type).to.eql('BinaryExpression'); 137 | if (cond.type !== 'BinaryExpression') throw new Error(); 138 | expect(cond.operator).to.eql('==='); 139 | const condLeft = cond.left; 140 | expect(condLeft.type).to.eql('Identifier'); 141 | if (condLeft.type !== 'Identifier') throw new Error(); 142 | expect(condLeft.name).to.eql('x_0'); 143 | const condRight = cond.right; 144 | expect(condRight.type).to.eql('Literal'); 145 | if (condRight.type !== 'Literal') throw new Error(); 146 | expect(condRight.value).to.eql(5); 147 | 148 | expect(ifStmt.consequent.body).to.have.length(1); 149 | const thenStmt = ifStmt.consequent.body[0]; 150 | expect(thenStmt.type).to.eql('ReturnStatement'); 151 | if (thenStmt.type !== 'ReturnStatement') throw new Error(); 152 | const thenArg = thenStmt.argument; 153 | expect(thenArg.type).to.eql('Literal'); 154 | if (thenArg.type !== 'Literal') throw new Error(); 155 | expect(thenArg.value).to.eql(23); 156 | 157 | expect(ifStmt.alternate.body).to.have.length(0); 158 | 159 | const retStmt = f.body.body.body[1]; 160 | expect(retStmt.type).to.eql('ReturnStatement'); 161 | if (retStmt.type !== 'ReturnStatement') throw new Error(); 162 | const arg = retStmt.argument; 163 | expect(arg.type).to.eql('Literal'); 164 | if (arg.type !== 'Literal') throw new Error(); 165 | expect(arg.value).to.eql(undefined); 166 | }); 167 | }); 168 | 169 | describe('function spec enforced in test', () => { 170 | 171 | code(() => { 172 | function g1 (f) { 173 | requires(spec(f, x => x > 4, (x, y) => true)); 174 | f(4); 175 | } 176 | 177 | function g2 (f) { 178 | requires(spec(f, x => true, (x, y) => y > 4)); 179 | const z = f(23); 180 | assert(z > 5); 181 | } 182 | 183 | g2(() => 3); 184 | }); 185 | 186 | incorrect('g1: precondition f(4)'); 187 | verified('g2: precondition f(23)'); 188 | incorrect('g2: assert: (z > 5)'); 189 | incorrect('precondition g2(() => 3)'); 190 | }); 191 | 192 | describe('higher-order function spec enforced in test', () => { 193 | 194 | code(() => { 195 | function g (f) { 196 | requires(spec(f, () => true, y => spec(y, x => x > 0, (x, z) => true))); 197 | 198 | const y = f(); 199 | y(0); 200 | } 201 | }); 202 | 203 | verified('g: precondition f()'); 204 | incorrect('g: precondition y(0)'); 205 | }); 206 | 207 | describe('asserting function spec generates call', () => { 208 | 209 | code(() => { 210 | function f (x) { 211 | requires(x > 2); 212 | } 213 | assert(spec(f, x => x > 1, (x, y) => true)); 214 | }); 215 | 216 | incorrect('assert: spec(f, x => (x > 1), (x, y) => true)', ['x', 2]); 217 | }); 218 | -------------------------------------------------------------------------------- /tests/transform.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { transformSourceCode } from '../src'; 4 | import { sourceAsJavaScript } from '../src/parser'; 5 | import { stringifyTestCode } from '../src/codegen'; 6 | import { codeToString } from './helpers'; 7 | 8 | declare function ensures (x: boolean | ((y: any) => boolean)): void; 9 | declare function requires (x: boolean): void; 10 | declare function assert (x: boolean): void; 11 | 12 | function expectTransformation (originalCode: () => any, expectedTransformedSource: string) { 13 | const originalSource = codeToString(originalCode); 14 | const expectedSource = stringifyTestCode(sourceAsJavaScript(expectedTransformedSource).body); 15 | const transformedSource = transformSourceCode(originalSource); 16 | expect(transformedSource).to.be.eql(expectedSource); 17 | } 18 | 19 | describe('source transformation', () => { 20 | 21 | it('retains assertions', () => { 22 | expectTransformation(() => { 23 | const x = 23; 24 | const y = 42; 25 | assert(x < y); 26 | }, ` 27 | let x = 23; 28 | let y = 42; 29 | assert(x < y);`); 30 | }); 31 | 32 | it('retains assertions in functions', () => { 33 | expectTransformation(() => { 34 | function f (x) { 35 | assert(x > 0); 36 | } 37 | }, ` 38 | function f (x) { 39 | assert(x > 0); 40 | }`); 41 | }); 42 | 43 | it('changes preconditions to assertions', () => { 44 | expectTransformation(() => { 45 | function f (x) { 46 | requires(Number.isInteger(x)); 47 | return x; 48 | } 49 | }, ` 50 | function f (x) { 51 | return x; 52 | } 53 | f = spec(f, 10371, (function (x) { 54 | assert(Number.isInteger(x)); 55 | return [x]; 56 | }), (x, _tmp_24438) => _tmp_24438);`); 57 | }); 58 | 59 | it('changes postconditions to assertions', () => { 60 | expectTransformation(() => { 61 | function f (x) { 62 | ensures(y => y > 3); 63 | return x; 64 | } 65 | }, ` 66 | function f (x) { 67 | return x; 68 | } 69 | f = spec(f, 10371, x => [x], (function (x, _tmp_24438) { 70 | assert(_tmp_24438 > 3); 71 | return _tmp_24438; 72 | }));`); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "outDir": "build/main", 5 | "rootDir": "src", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "strictNullChecks": true, 12 | "noUnusedLocals": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "traceResolution": false, 18 | "listEmittedFiles": false, 19 | "listFiles": false, 20 | "pretty": true, 21 | "preserveConstEnums": true, 22 | "removeComments": false, 23 | "sourceMap": false, 24 | "lib": ["es2017"], 25 | "types": [], 26 | "typeRoots": ["node_modules/@types"] 27 | }, 28 | "include": [ 29 | "src/*.ts" 30 | ], 31 | "compileOnSave": false 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "build/module", 6 | "module": "esnext" 7 | } 8 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "indent": [true, "spaces"], 5 | "semicolon": [true, "always"], 6 | "no-var-keyword": true, 7 | "no-debugger": true, 8 | "no-parameter-reassignment": true, 9 | "max-line-length": [true, 120] 10 | } 11 | } 12 | --------------------------------------------------------------------------------