contracts-js
Advanced tools
Comparing version 0.4.2 to 0.4.3
@@ -169,2 +169,51 @@ % Contracts.js Documentation | ||
### Dependent Contracts | ||
You can also write a function contract who's result depends on the | ||
value of its arguments. | ||
```js | ||
@ (x: Pos) -> res: Num | res <= x | ||
function square_root(x) { return Math.sqrt(x); } | ||
``` | ||
Name each argument and result with the notation `<name>: <contract>` | ||
and then each name can be referred to in the dependency guard | ||
following the `|`. The guard is an expression the must evaluate to a | ||
boolean. If the guard evaluates to `true` the dependent function | ||
contract will pass otherwise it fails. | ||
If you need more than a single boolean expression you can wrap it in | ||
curlies: | ||
```js | ||
@ (x: Pos) -> res: Num | { | ||
var fromlib = Math.sqrt(x); | ||
return res <= x && fromlib === res; | ||
} | ||
function square_root(x) { return Math.sqrt(x); } | ||
``` | ||
Note that guards in a dependent contract could potentially violate | ||
a contract on one of the arguments: | ||
```js | ||
@ (f: (Num) -> Num) -> res: Num | f("foo") > 10 | ||
function foo(f) { return f(24) } | ||
``` | ||
In a case like this, the contract itself will be blamed: | ||
<pre style="color:red"> | ||
expected: Num | ||
given: 'foo' | ||
in: the 1st argument of | ||
the 1st argument of | ||
(f: (Num) -> Num) -> res: Num | f (foo) > 10 | ||
function foo guarded at line: 2 | ||
blaming: the contract of foo | ||
</pre> | ||
If you are familiar with contract research, this is the [indy](http://www.ccs.neu.edu/racket/pubs/popl11-dfff.pdf) semantics. | ||
## Object Contracts | ||
@@ -171,0 +220,0 @@ |
@@ -30,2 +30,12 @@ // returns the log array | ||
}, | ||
{ | ||
id: 4, | ||
file: "dependent.js", | ||
title: "Dependent Contracts" | ||
}, | ||
{ | ||
id: 4, | ||
file: "dependent-indy.js", | ||
title: "Dependent Contracts with Indy Blame" | ||
} | ||
]; | ||
@@ -32,0 +42,0 @@ var contractModulePromise = Ember.$.ajax("macros/index.js", { |
@@ -55,2 +55,5 @@ var _c; | ||
}; | ||
BlameObj.prototype.setNeg = function (neg) { | ||
return Blame.clone(this, { neg: neg }); | ||
}; | ||
function assert(cond, msg) { | ||
@@ -122,4 +125,8 @@ if (!cond) { | ||
function fun(dom, rng, options) { | ||
var domName = '(' + dom.join(', ') + ')'; | ||
var contractName = domName + ' -> ' + rng; | ||
var domStr = dom.map(function (d, idx) { | ||
return options && options.namesStr ? options.namesStr[idx] + ': ' + d : d; | ||
}).join(', '); | ||
var domName = '(' + domStr + ')'; | ||
var rngStr = options && options.namesStr ? options.namesStr[options.namesStr.length - 1] + ': ' + rng : rng; | ||
var contractName = domName + ' -> ' + rngStr + (options && options.dependencyStr ? ' | ' + options.dependencyStr : ''); | ||
var c = new Contract(contractName, 'fun', function (blame) { | ||
@@ -133,2 +140,3 @@ return function (f) { | ||
var checkedArgs = []; | ||
var depArgs = []; | ||
for (var i = 0; i < dom.length; i++) { | ||
@@ -138,4 +146,9 @@ if (dom[i].type === 'optional' && args[i] === undefined) { | ||
} else { | ||
var domProj = dom[i].proj(blame.swap().addLocation('the ' + addTh(i + 1) + ' argument of')); | ||
var location = 'the ' + addTh(i + 1) + ' argument of'; | ||
var domProj = dom[i].proj(blame.swap().addLocation(location)); | ||
checkedArgs.push(domProj(args[i])); | ||
if (options && options.dependency) { | ||
var depProj = dom[i].proj(blame.swap().setNeg('the contract of ' + blame.name).addLocation(location)); | ||
depArgs.push(depProj(args[i])); | ||
} | ||
} | ||
@@ -147,3 +160,10 @@ } | ||
var rngProj = rng.proj(blame.addLocation('the return of')); | ||
return rngProj(rawResult); | ||
var rngResult = rngProj(rawResult); | ||
if (options && options.dependency && typeof options.dependency === 'function') { | ||
var depResult = options.dependency.apply(this, depArgs.concat(rngResult)); | ||
if (!depResult) { | ||
raiseBlame(blame.addExpected(options.dependencyStr).addGiven(false).addLocation('the return dependency of')); | ||
} | ||
} | ||
return rngResult; | ||
} | ||
@@ -348,2 +368,21 @@ // only use expensive proxies when needed (to distinguish between apply and construct) | ||
macro stringify { | ||
case {_ ($toks ...) } => { | ||
var toks = #{$toks ...}[0].token.inner; | ||
function traverse(stx) { | ||
return stx.map(function(s) { | ||
if (s.token.inner) { | ||
return s.token.value[0] + traverse(s.token.inner) + s.token.value[1]; | ||
} | ||
return s.token.value; | ||
}).join(" "); | ||
} | ||
var toksStr = traverse(toks); | ||
letstx $str = [makeValue(toksStr, #{here})]; | ||
return #{$str} | ||
} | ||
} | ||
macro base_contract { | ||
@@ -354,2 +393,20 @@ rule { $name } => { _c.$name } | ||
macro function_contract { | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | { $guard ... } } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
$guard ... | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify (($guard ...)) | ||
}) | ||
} | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | $guard:expr } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
return $guard; | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify ($guard) | ||
}) | ||
} | ||
rule { ($dom:any_contract (,) ...) -> $range:any_contract } => { | ||
@@ -356,0 +413,0 @@ _c.fun([$dom (,) ...], $range) |
@@ -55,2 +55,5 @@ var _c; | ||
}; | ||
BlameObj.prototype.setNeg = function (neg) { | ||
return Blame.clone(this, { neg: neg }); | ||
}; | ||
function assert(cond, msg) { | ||
@@ -122,4 +125,8 @@ if (!cond) { | ||
function fun(dom, rng, options) { | ||
var domName = '(' + dom.join(', ') + ')'; | ||
var contractName = domName + ' -> ' + rng; | ||
var domStr = dom.map(function (d, idx) { | ||
return options && options.namesStr ? options.namesStr[idx] + ': ' + d : d; | ||
}).join(', '); | ||
var domName = '(' + domStr + ')'; | ||
var rngStr = options && options.namesStr ? options.namesStr[options.namesStr.length - 1] + ': ' + rng : rng; | ||
var contractName = domName + ' -> ' + rngStr + (options && options.dependencyStr ? ' | ' + options.dependencyStr : ''); | ||
var c = new Contract(contractName, 'fun', function (blame) { | ||
@@ -133,2 +140,3 @@ return function (f) { | ||
var checkedArgs = []; | ||
var depArgs = []; | ||
for (var i = 0; i < dom.length; i++) { | ||
@@ -138,4 +146,9 @@ if (dom[i].type === 'optional' && args[i] === undefined) { | ||
} else { | ||
var domProj = dom[i].proj(blame.swap().addLocation('the ' + addTh(i + 1) + ' argument of')); | ||
var location = 'the ' + addTh(i + 1) + ' argument of'; | ||
var domProj = dom[i].proj(blame.swap().addLocation(location)); | ||
checkedArgs.push(domProj(args[i])); | ||
if (options && options.dependency) { | ||
var depProj = dom[i].proj(blame.swap().setNeg('the contract of ' + blame.name).addLocation(location)); | ||
depArgs.push(depProj(args[i])); | ||
} | ||
} | ||
@@ -147,3 +160,10 @@ } | ||
var rngProj = rng.proj(blame.addLocation('the return of')); | ||
return rngProj(rawResult); | ||
var rngResult = rngProj(rawResult); | ||
if (options && options.dependency && typeof options.dependency === 'function') { | ||
var depResult = options.dependency.apply(this, depArgs.concat(rngResult)); | ||
if (!depResult) { | ||
raiseBlame(blame.addExpected(options.dependencyStr).addGiven(false).addLocation('the return dependency of')); | ||
} | ||
} | ||
return rngResult; | ||
} | ||
@@ -348,2 +368,21 @@ // only use expensive proxies when needed (to distinguish between apply and construct) | ||
macro stringify { | ||
case {_ ($toks ...) } => { | ||
var toks = #{$toks ...}[0].token.inner; | ||
function traverse(stx) { | ||
return stx.map(function(s) { | ||
if (s.token.inner) { | ||
return s.token.value[0] + traverse(s.token.inner) + s.token.value[1]; | ||
} | ||
return s.token.value; | ||
}).join(" "); | ||
} | ||
var toksStr = traverse(toks); | ||
letstx $str = [makeValue(toksStr, #{here})]; | ||
return #{$str} | ||
} | ||
} | ||
macro base_contract { | ||
@@ -353,3 +392,25 @@ rule { $name } => { _c.$name } | ||
macroclass named_contract { | ||
rule { $name $[:] $contract:any_contract } | ||
} | ||
macro function_contract { | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | { $guard ... } } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
$guard ... | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify (($guard ...)) | ||
}) | ||
} | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | $guard:expr } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
return $guard; | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify ($guard) | ||
}) | ||
} | ||
rule { ($dom:any_contract (,) ...) -> $range:any_contract } => { | ||
@@ -406,2 +467,3 @@ _c.fun([$dom (,) ...], $range) | ||
macro non_or_contract { | ||
@@ -408,0 +470,0 @@ rule { $contract:function_contract } => { $contract } |
@@ -10,3 +10,3 @@ { | ||
"author": "Tim Disney", | ||
"version": "0.4.2", | ||
"version": "0.4.3", | ||
"licenses": [ | ||
@@ -13,0 +13,0 @@ { |
@@ -63,3 +63,9 @@ (function() { | ||
BlameObj.prototype.setNeg = function(neg) { | ||
return Blame.clone(this, { | ||
neg: neg | ||
}); | ||
}; | ||
function assert(cond, msg) { | ||
@@ -134,6 +140,12 @@ if(!cond) { | ||
function fun(dom, rng, options) { | ||
var domStr = dom.map(function (d, idx) { | ||
return options && options.namesStr ? options.namesStr[idx] + ": " + d : d; | ||
}).join(", "); | ||
var domName = "(" + domStr + ")"; | ||
var domName = "(" + dom.join(", ") + ")"; | ||
var contractName = domName + " -> " + rng; | ||
var rngStr = options && options.namesStr ? options.namesStr[options.namesStr.length - 1] + ": " + rng : rng; | ||
var contractName = domName + " -> " + rngStr + | ||
(options && options.dependencyStr ? " | " + options.dependencyStr : ""); | ||
var c = new Contract(contractName, "fun", function(blame) { | ||
@@ -151,3 +163,3 @@ return function(f) { | ||
var checkedArgs = []; | ||
var depArgs = []; | ||
for (var i = 0; i < dom.length; i++) { | ||
@@ -157,8 +169,14 @@ if (dom[i].type === "optional" && args[i] === undefined) { | ||
} else { | ||
var location = "the " + addTh(i+1) + " argument of"; | ||
var domProj = dom[i].proj(blame.swap() | ||
.addLocation("the " + | ||
addTh(i+1) + | ||
" argument of")); | ||
.addLocation(location)); | ||
checkedArgs.push(domProj(args[i])); | ||
if (options && options.dependency) { | ||
var depProj = dom[i].proj(blame.swap() | ||
.setNeg("the contract of " + blame.name) | ||
.addLocation(location)); | ||
depArgs.push(depProj(args[i])); | ||
} | ||
} | ||
@@ -172,3 +190,12 @@ } | ||
var rngProj = rng.proj(blame.addLocation("the return of")); | ||
return rngProj(rawResult); | ||
var rngResult = rngProj(rawResult); | ||
if (options && options.dependency && typeof options.dependency === "function") { | ||
var depResult = options.dependency.apply(this, depArgs.concat(rngResult)); | ||
if (!depResult) { | ||
raiseBlame(blame.addExpected(options.dependencyStr) | ||
.addGiven(false) | ||
.addLocation("the return dependency of")); | ||
} | ||
} | ||
return rngResult; | ||
} | ||
@@ -175,0 +202,0 @@ |
@@ -12,2 +12,21 @@ var _c; | ||
macro stringify { | ||
case {_ ($toks ...) } => { | ||
var toks = #{$toks ...}[0].token.inner; | ||
function traverse(stx) { | ||
return stx.map(function(s) { | ||
if (s.token.inner) { | ||
return s.token.value[0] + traverse(s.token.inner) + s.token.value[1]; | ||
} | ||
return s.token.value; | ||
}).join(" "); | ||
} | ||
var toksStr = traverse(toks); | ||
letstx $str = [makeValue(toksStr, #{here})]; | ||
return #{$str} | ||
} | ||
} | ||
macro base_contract { | ||
@@ -18,2 +37,20 @@ rule { $name } => { _c.$name } | ||
macro function_contract { | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | { $guard ... } } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
$guard ... | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify (($guard ...)) | ||
}) | ||
} | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | $guard:expr } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
return $guard; | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify ($guard) | ||
}) | ||
} | ||
rule { ($dom:any_contract (,) ...) -> $range:any_contract } => { | ||
@@ -20,0 +57,0 @@ _c.fun([$dom (,) ...], $range) |
@@ -12,2 +12,21 @@ var _c; | ||
macro stringify { | ||
case {_ ($toks ...) } => { | ||
var toks = #{$toks ...}[0].token.inner; | ||
function traverse(stx) { | ||
return stx.map(function(s) { | ||
if (s.token.inner) { | ||
return s.token.value[0] + traverse(s.token.inner) + s.token.value[1]; | ||
} | ||
return s.token.value; | ||
}).join(" "); | ||
} | ||
var toksStr = traverse(toks); | ||
letstx $str = [makeValue(toksStr, #{here})]; | ||
return #{$str} | ||
} | ||
} | ||
macro base_contract { | ||
@@ -17,3 +36,25 @@ rule { $name } => { _c.$name } | ||
macroclass named_contract { | ||
rule { $name $[:] $contract:any_contract } | ||
} | ||
macro function_contract { | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | { $guard ... } } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
$guard ... | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify (($guard ...)) | ||
}) | ||
} | ||
rule { ($dom:named_contract (,) ...) -> $range:named_contract | $guard:expr } => { | ||
_c.fun([$dom$contract (,) ...], $range$contract, { | ||
dependency: function($dom$name (,) ..., $range$name) { | ||
return $guard; | ||
}, | ||
namesStr: [$(stringify (($dom$name))) (,) ..., stringify (($range$name))], | ||
dependencyStr: stringify ($guard) | ||
}) | ||
} | ||
rule { ($dom:any_contract (,) ...) -> $range:any_contract } => { | ||
@@ -70,2 +111,3 @@ _c.fun([$dom (,) ...], $range) | ||
macro non_or_contract { | ||
@@ -72,0 +114,0 @@ rule { $contract:function_contract } => { $contract } |
@@ -1,2 +0,2 @@ | ||
var should = require("should"); | ||
var should = require("should"), assert = require("assert"); | ||
import @ from "contracts.js"; | ||
@@ -10,2 +10,3 @@ | ||
$body ... | ||
assert.fail("no exception", "exception", "Should have blamed: " + $expectedMsg); | ||
} catch (b) { | ||
@@ -31,3 +32,3 @@ (b.message).should.equal($expectedMsg); | ||
(Num) -> Num | ||
function numId guarded at line: 21 | ||
function numId guarded at line: 22 | ||
blaming: (calling context for numId) | ||
@@ -48,3 +49,3 @@ ` | ||
(Num) -> Num | ||
function numId guarded at line: 37 | ||
function numId guarded at line: 38 | ||
blaming: function numId | ||
@@ -65,3 +66,3 @@ ` | ||
(Num, Str) -> Num | ||
function f guarded at line: 53 | ||
function f guarded at line: 54 | ||
blaming: (calling context for f) | ||
@@ -93,3 +94,3 @@ ` | ||
(Num, opt Str) -> Num | ||
function f guarded at line: 78 | ||
function f guarded at line: 79 | ||
blaming: (calling context for f) | ||
@@ -110,3 +111,3 @@ ` | ||
((Num) -> Num) -> Num | ||
function f guarded at line: 96 | ||
function f guarded at line: 97 | ||
blaming: (calling context for f) | ||
@@ -132,3 +133,3 @@ ` | ||
((Num) -> Num) -> Num | ||
function numApp guarded at line: 112 | ||
function numApp guarded at line: 113 | ||
blaming: (calling context for numApp) | ||
@@ -152,3 +153,3 @@ ` | ||
((Num) -> Num) -> Num | ||
function bad guarded at line: 133 | ||
function bad guarded at line: 134 | ||
blaming: function bad | ||
@@ -170,3 +171,3 @@ ` | ||
({age: Num}) -> Num | ||
function f guarded at line: 152 | ||
function f guarded at line: 153 | ||
blaming: (calling context for f) | ||
@@ -191,3 +192,3 @@ ` | ||
({g: (Num) -> Num}) -> Num | ||
function f guarded at line: 169 | ||
function f guarded at line: 170 | ||
blaming: (calling context for f) | ||
@@ -208,3 +209,3 @@ ` | ||
({s: Str}) -> Str | ||
function f guarded at line: 189 | ||
function f guarded at line: 190 | ||
blaming: (calling context for f) | ||
@@ -233,3 +234,3 @@ ` | ||
({foo: opt Str}) -> Str | ||
function f guarded at line: 212 | ||
function f guarded at line: 213 | ||
blaming: (calling context for f) | ||
@@ -254,3 +255,3 @@ ` | ||
(Num) -> !{age: Num} | ||
function makePerson guarded at line: 229 | ||
function makePerson guarded at line: 230 | ||
blaming: (calling context for makePerson) | ||
@@ -275,3 +276,3 @@ ` | ||
(!{age: Num}) -> Num | ||
function f guarded at line: 249 | ||
function f guarded at line: 250 | ||
blaming: function f | ||
@@ -293,3 +294,3 @@ ` | ||
([Str]) -> Num | ||
function f guarded at line: 269 | ||
function f guarded at line: 270 | ||
blaming: (calling context for f) | ||
@@ -311,3 +312,3 @@ ` | ||
([Str, Num]) -> Num | ||
function f guarded at line: 286 | ||
function f guarded at line: 287 | ||
blaming: (calling context for f) | ||
@@ -330,3 +331,3 @@ ` | ||
(Num) -> ![Num] | ||
function makeArr guarded at line: 303 | ||
function makeArr guarded at line: 304 | ||
blaming: (calling context for makeArr) | ||
@@ -348,3 +349,3 @@ ` | ||
([....Num]) -> Num | ||
function f guarded at line: 321 | ||
function f guarded at line: 322 | ||
blaming: (calling context for f) | ||
@@ -385,3 +386,3 @@ ` | ||
(![....Num]) -> Num | ||
function f guarded at line: 354 | ||
function f guarded at line: 355 | ||
blaming: function f | ||
@@ -404,3 +405,3 @@ ` | ||
({o: {name: Str}}) -> Str | ||
function f guarded at line: 374 | ||
function f guarded at line: 375 | ||
blaming: (calling context for f) | ||
@@ -430,3 +431,3 @@ ` | ||
({name: Str}, [....{loc: Num}]) -> Str | ||
function calcAverageLoc guarded at line: 392 | ||
function calcAverageLoc guarded at line: 393 | ||
blaming: (calling context for calcAverageLoc) | ||
@@ -451,3 +452,3 @@ ` | ||
((Num) -> Num) -> Num | ||
function f guarded at line: 419 | ||
function f guarded at line: 420 | ||
blaming: (calling context for f) | ||
@@ -471,7 +472,42 @@ ` | ||
(Str or Num) -> Str | ||
function foo guarded at line: 438 | ||
function foo guarded at line: 439 | ||
blaming: (calling context for foo) | ||
` | ||
}); | ||
it("should work for dependent contracts", function() { | ||
@ (x: Pos) -> res: Num | res <= x | ||
function bad_square_root(x) { return x * x; } | ||
blame of { | ||
bad_square_root(100) | ||
} should be `bad_square_root: contract violation | ||
expected: res <= x | ||
given: false | ||
in: the return dependency of | ||
(x: Pos) -> res: Num | res <= x | ||
function bad_square_root guarded at line: 457 | ||
blaming: function bad_square_root | ||
` | ||
}); | ||
it("should blame the contract if the dependency breaks a domain contract", function() { | ||
@ (f: (Num) -> Num) -> res: Num | { return f("foo") > 10 } | ||
function foo(f) { return f(24) } | ||
blame of { | ||
foo(function(x) { | ||
return x; | ||
}); | ||
} should be `foo: contract violation | ||
expected: Num | ||
given: 'foo' | ||
in: the 1st argument of | ||
the 1st argument of | ||
(f: (Num) -> Num) -> res: Num | return f (foo) > 10 | ||
function foo guarded at line: 473 | ||
blaming: the contract of foo | ||
` | ||
}) | ||
}); |
Sorry, the diff of this file is not supported yet
5862239
271
131735