JsContracts (empty) → 0.4
raw patch · 11 files changed
+1212/−0 lines, 11 filesdep +WebBitsdep +basedep +containersbuild-type:Customsetup-changed
Dependencies added: WebBits, base, containers, directory, filepath, mtl, parsec, pretty, syb
Files
- JsContracts.cabal +46/−0
- LICENSE +26/−0
- Setup.lhs +28/−0
- data/contracts.js +329/−0
- src/BrownPLT/JavaScript/Contracts.hs +21/−0
- src/BrownPLT/JavaScript/Contracts/Compiler.hs +314/−0
- src/BrownPLT/JavaScript/Contracts/Interface.hs +5/−0
- src/BrownPLT/JavaScript/Contracts/Parser.hs +164/−0
- src/BrownPLT/JavaScript/Contracts/Template.hs +134/−0
- src/BrownPLT/JavaScript/Contracts/Types.hs +45/−0
- src/Jscc.hs +100/−0
+ JsContracts.cabal view
@@ -0,0 +1,46 @@+Name: JsContracts+Version: 0.4+Cabal-Version: >= 1.2.3+Copyright: Copyright (c) 2008-2009 Arjun Guha and Spiridon Eliopoulos+License: BSD3+License-file: LICENSE+Author: Arjun Guha and Spiridon Eliopoulos+Maintainer: Arjun Guha <arjun@cs.brown.edu>+Homepage: http://www.cs.brown.edu/research/plt/+Stability: provisional+Category: Language+Build-Type: Custom+Synopsis: Design-by-contract for JavaScript+Data-Dir: data+Data-Files:+ contracts.js+Description:+ +Library+ Hs-Source-Dirs:+ src+ Build-Depends:+ base>=4, mtl>=1.1.0.1, parsec<3.0.0, pretty>=0.1, containers>=0.1, syb>=0.1,+ WebBits>=0.15, filepath, directory+ ghc-options:+ -fwarn-incomplete-patterns+ Extensions: + Exposed-Modules:+ BrownPLT.JavaScript.Contracts+ BrownPLT.JavaScript.Contracts.Interface+ Other-Modules:+ Paths_JsContracts + BrownPLT.JavaScript.Contracts.Parser+ BrownPLT.JavaScript.Contracts.Types+ BrownPLT.JavaScript.Contracts.Compiler+ BrownPLT.JavaScript.Contracts.Template++Executable jscc+ Main-Is: Jscc.hs+ Hs-Source-Dirs: src+ Build-Depends:+ base>=4, mtl>=1.1.0.1, parsec<3.0.0, pretty>=0.1, containers>=0.1, syb>=0.1,+ WebBits>=0.15+ ghc-options:+ -fwarn-incomplete-patterns+ Extensions:
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright (c) 2008-2009, Arjun Guha and Spiridon Eliopoulos.+All Rights Reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer.+ * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution.+ * Neither the name of Brown University nor the names of its contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ Setup.lhs view
@@ -0,0 +1,28 @@+#!/usr/bin/env runhaskell+> import Distribution.Simple+> import qualified Data.List as L+> import System.Directory+> import System.Process (runCommand,waitForProcess)+> import System.IO (hPutStrLn, stderr)++> isHaskellFile file = ".lhs" `L.isSuffixOf` file || ".hs" `L.isSuffixOf` file++> moduleName file = L.takeWhile (\ch -> ch /= '.') file++> testMain _ _ _ _ = do+> files <- getDirectoryContents "tests"+> let tests = filter isHaskellFile files+> let testModules = map moduleName tests+> let testFuncs = map (++ ".main") testModules+> let testExpr = "sequence [ " ++ concat (L.intersperse "," testFuncs) ++ +> " ] >>= \\cases -> runTestTT (TestList cases)"+> let moduleLine = concat (L.intersperse " " testModules)+> let cmd = "cd tests && ghc -XNoMonomorphismRestriction -fglasgow-exts " +++> "-package HUnit -package WebBits -package parsec-2.1.0.1 -i../src:../dist/build/autogen -e \"" ++ +> testExpr ++ " >> return ()\" " ++ moduleLine+> handle <- runCommand cmd+> waitForProcess handle+> hPutStrLn stderr "Testing complete. Errors reported above (if any)."+ ++> main = defaultMainWithHooks (simpleUserHooks { runTests = testMain })
+ data/contracts.js view
@@ -0,0 +1,329 @@+var contracts = { };++contracts.map = function(f,arr) {+ var dest = [ ];+ for (var i = 0; i < arr.length; i++) {+ dest.push(f(arr[i]));+ }+ return dest;+};++contracts.zipWith = function(f,arr1,arr2) {+ var dest = [ ];+ for (var i = 0; i < arr1.length; i++) {+ dest.push(f(arr1[i],arr2[i]));+ }+ return dest;+};++contracts.blame = function(guilty,expected,received,message,loc) {+ var guiltyMsg = typeof(guilty) == "string" + ? guilty : guilty.value;+ var msg = guiltyMsg + " violated the contract at " + loc + "; expected " + + expected + " but received " + received + "; " + message;+ var err = new Error(msg);+ err.guilty = msg;+ err.blamed = guiltyMsg;+ err.expected = expected;+ err.received = received;+ err.guardLoc = loc;+ try { console.log(err); } catch(_) { };+ throw err;+}++// The type of a contract combinator is: name -> args ... -> contract+//+// name is a human-readable name for the contract. args .. are constructor-+// specified arguments and contract is the resulting contract.+ ++contracts.flat = function(name) {+ return function(pred) {+ return {+ flat: function(val) { return pred(val); },+ server: function(s,loc) {+ return function(val) {+ if (pred(val)) { + return val; + }+ else { + contracts.blame(s,name,val,"does not satisfy the predicate",loc);+ }+ };+ },+ client: function(s,loc) {+ return function(val) { return val; };+ }+ };+ };+};+++contracts.unsizedArray = function(name) {+ return function(elt) {+ return {+ flat: function(val) {+ return val instanceof Array;+ for (var i = 0; i < val.length; i++) {+ if (!(elt.flat(val[i]))) { return false; }+ }+ return true;+ },+ server: function(s,loc) {+ return function(val) {+ if (val instanceof Array) {+ return contracts.map(elt.server(s,loc),val);+ }+ else {+ contracts.blame(s, name, val, "not an array",loc);+ }+ };+ },+ client: function(s,loc) {+ return function(val) {+ if (val instanceof Array) {+ return contracts.map(elt.client(s,loc),val);+ }+ else {+ return val;+ }+ }+ }+ };+ };+};++contracts.fixedArray = function(name) {+ return function() {+ var elts = arguments;+ return {+ flat: function(val) {+ if (!(val instanceof Array && val.length == elts.length)) {+ return false;+ }+ for (var i = 0; i < val.length; i++) {+ if (!elts[i].flat(val[i])) { return false; }+ }+ return true;+ },+ server: function(s,loc) {+ return function(val) {+ if (val instanceof Array && val.length == elts.length) {+ var result = [ ];+ for (var i = 0; i < elts.length; i++) {+ result.push(elts[i].server(s,loc)(val[i]));+ }+ return result;+ }+ else {+ contracts.blame(s,name,val,"not an array of the right size",loc);+ }+ };+ },+ client: function(s,loc) {+ return function(val) {+ if (val instanceof Array && val.length == elts.length) {+ var result = [ ];+ for (var i = 0; i < elts.length; i++) {+ result.push(elts[i].client(s,loc)(val[i]));+ }+ return result;+ }+ else {+ return val;+ }+ }+ }+ };+ };+};++contracts.isUndefined = contracts.flat(function(val) { + return val === undefined;+});++contracts.varArityFunc = function(name) {+ return function(fixedArgs,restArgs,result) {+ return {+ isHigherOrder: true,+ flat: function(val) { return typeof(val) == "function"; },+ server: function(s,loc) {+ return function(proc) {+ if (typeof(proc) == "function") {+ return function() {+ var guardedArgs = contracts.zipWith(function(ctc,arg) {+ return ctc.client(s,loc)(arg);+ }, fixedArgs, arguments);+ for (var i = fixedArgs.length; i < arguments.length; i++) {+ guardedArgs.push(restArgs.client(s,loc)(arguments[i]));+ }+ return result.server(s,loc)(proc.apply(this, guardedArgs));+ };+ }+ else { contracts.blame(s,name, proc,"not a function",loc); }+ }+ },+ client: function(s,loc) {+ return function(proc) {+ if (typeof(proc) == "function") {+ return function() {+ var guardedArgs = contracts.zipWith(function(ctc,arg) {+ return ctc.server(s,loc)(arg);+ }, fixedArgs, arguments);+ for (var i = fixedArgs.length; i < arguments.length; i++) {+ guardedArgs.push(restArgs.server(s,loc)(arguments[i]));+ }+ return result.client(s,loc)(proc.apply(this, guardedArgs));+ };+ }+ else {+ return proc;+ }+ };+ }+ };+ };+};++// Ensures value is an instanceof constr before checking that its shape+// matches sig. constrId is a human-readable name for the constructor.+contracts.instance = function(name) {+ return function(constr, sig) {+ return {+ flat: function(val) {+ return (typeof(val) == "object" || typeof(val) == "function") &&+ (val instanceof constr) && sig.flat(val);+ },+ server: function(s,loc) { return function(val) {+ if ((typeof(val) == "object" || typeof(val) == "function") && + (val instanceof constr)) {+ return sig.server(s,loc)(val);+ }+ else {+ contracts.blame(s, name, val, "wrong instance",loc);+ }+ } },+ client: function(s,loc) { return function(val) {+ if ((typeof(val) == "object" || typeof(val) == "function") && + (val instanceof constr)) {+ return sig.client(s,loc)(val);+ }+ else {+ return val;+ }+ } }+ };+ };+};++contracts.obj = function(name) {+ return function(sig) {+ return {+ flat: function(val) {+ for (var field in sig) {+ if (!(sig[field].flat(val[field]))) { return false; }+ }+ return true;+ },+ server: function(s,loc) {+ return function (obj) {+ var constr = function() { };+ constr.prototype = obj;+ var guardedObj = new constr();+ + for (var field in sig) {+ guardedObj[field] = sig[field].server(s,loc)(obj[field]);+ }+ return guardedObj;+ };+ },+ client: function(s,loc) {+ return function (obj) {+ var constr = function() { };+ constr.prototype = obj;+ var guardedObj = new constr();+ + for (var field in sig) {+ guardedObj[field] = sig[field].client(s,loc)(obj[field]);+ }+ return guardedObj;+ };+ }+ };+ };+};++// pos is the name of val. neg should be the name of the calling context.+// Since we do not rewrite call-sites, neg is simply "client".+// However, if ctc.isHigherOrder, we can determine the name of the calling+// context by examining the stack when val is called.+// if !ctc.isHigherOrder, the name of the calling context is the name of the+// definition point (neg)+contracts.guard = function(ctc,val,pos,neg,loc) {+ if (ctc.isHigherOrder) {+ if (typeof(val) != "function") {+ contracts.blame(pos, "a function", proc, "not a function","guard"); + }+ else {+ var deferredNeg = { value: "not called" };+ var fn = ctc.client(deferredNeg,loc)(ctc.server(pos,loc)(val));+ return function() {+ deferredNeg.value = contracts.stackTrace();+ return fn.apply(this,arguments);+ };+ }+ }+ else {+ return ctc.client(neg,loc)(ctc.server(pos,loc)(val));+ }+};++// Derived from http://eriwen.com/javascript/js-stack-trace/+contracts.stackTrace = function() {+ var callstack = [];+ var isCallstackPopulated = false;+ try {+ i.dont.exist+=0; //does not exist - that's the point+ } catch(e) {+ if (e.stack) { //Firefox+ var lines = e.stack.split("\n");+ for (var i = 0, len = lines.length; i < len; i++) {+ if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {+ callstack.push(lines[i]);+ }+ }+ //Remove call to printStackTrace()+ callstack.shift();+ isCallstackPopulated = true;+ }+ else if (window.opera && e.message) { //Opera+ var lines = e.message.split("\n");+ for (var i = 0, len = lines.length; i < len; i++) {+ if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {+ var entry = lines[i];+ //Append next line also since it has the file info+ if (lines[i+1]) {+ entry += " at " + lines[i+1];+ i++;+ }+ callstack.push(entry);+ }+ }+ //Remove call to printStackTrace()+ callstack.shift();+ isCallstackPopulated = true;+ }+ }+ if (!isCallstackPopulated) { //IE and Safari+ var currentFunction = arguments.callee.caller;+ while (currentFunction) {+ var fn = currentFunction.toString();+ //If we can't get the function name set to "anonymous"+ var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";+ callstack.push(fname);+ currentFunction = currentFunction.caller;+ }+ }++ return callstack.length == 0 ? "client" : callstack.join(" ");+};
+ src/BrownPLT/JavaScript/Contracts.hs view
@@ -0,0 +1,21 @@+module BrownPLT.JavaScript.Contracts+ ( Contract (..)+ , InterfaceItem (..)+ , compile+ , compileFormatted+ , compileRelease+ , parseInterface+ , getContractLibraryPath+ ) where++import System.FilePath+import Paths_JsContracts -- created by Cabal++import BrownPLT.JavaScript.Contracts.Types+import BrownPLT.JavaScript.Contracts.Compiler+import BrownPLT.JavaScript.Contracts.Parser++getContractLibraryPath :: IO FilePath+getContractLibraryPath = do+ dataDir <- getDataDir+ return $ dataDir </> "contracts.js"
+ src/BrownPLT/JavaScript/Contracts/Compiler.hs view
@@ -0,0 +1,314 @@+module BrownPLT.JavaScript.Contracts.Compiler+ ( compile+ , compile'+ , compileFormatted+ , compileRelease+ ) where++import Control.Monad++import qualified Data.Map as M++import Text.PrettyPrint.HughesPJ ( render, vcat )+import Text.ParserCombinators.Parsec.Pos (SourcePos)+import Paths_JsContracts -- created by Cabal+import System.FilePath ((</>))+import BrownPLT.JavaScript.Parser (ParsedExpression, ParsedStatement,+ parseJavaScriptFromFile, parseScriptFromString )+import BrownPLT.JavaScript.Environment (env)+import BrownPLT.JavaScript.Syntax+import BrownPLT.JavaScript.PrettyPrint (renderStatements)+import BrownPLT.JavaScript.Contracts.Types+import BrownPLT.JavaScript.Contracts.Parser+import BrownPLT.JavaScript.Contracts.Template+++-- Given the name foo, we get+--+-- try {+-- // allow the implementation to export to this directly+-- if (this.foo !== undefined) {+-- this.foo = foo;+-- }+-- }+-- catch (_) {+-- // in case the local variable foo is undefined+-- }+exposeImplementation :: [String]+ -> [ParsedStatement]+exposeImplementation names = map export names where+ var n = VarRef noPos (Id noPos n)+ undef = VarRef noPos (Id noPos "undefined")+ export n = TryStmt noPos + (IfSingleStmt noPos (InfixExpr noPos OpStrictNEq (var n) undef)+ (ExprStmt noPos $ AssignExpr noPos OpAssign+ (DotRef noPos (ThisRef noPos) (Id noPos n)) + (var n)))+ [CatchClause noPos (Id noPos "_") (EmptyStmt noPos)]+ Nothing++-- Given a namespace, e.g. flapjax, we get+--+-- window.flapjax = { };+-- for (var ix in impl) {+-- window.flapjax[ix] = impl[ix]; }+exportNamespace :: String+ -> [ParsedStatement]+exportNamespace namespace = [decl,loop] where+ ix = VarRef noPos (Id noPos "ix")+ window_namespace = + (DotRef noPos (VarRef noPos (Id noPos "window")) + (Id noPos namespace))+ decl = ExprStmt noPos $ AssignExpr noPos OpAssign window_namespace+ (ObjectLit noPos [])+ loop = ForInStmt noPos (ForInVar (Id noPos "ix")) + (VarRef noPos (Id noPos "impl")) $ ExprStmt noPos $+ AssignExpr noPos OpAssign (BracketRef noPos window_namespace ix)+ (BracketRef noPos (VarRef noPos (Id noPos "impl")) ix)++wrapImplementation :: [ParsedStatement] -> [String] -> [ParsedStatement]+wrapImplementation impl names = + [VarDeclStmt noPos [implDecl], ExprStmt noPos callThunkedImpl] where+ implDecl = VarDecl noPos (Id noPos "impl") (Just $ ObjectLit noPos [])+ implExport = exposeImplementation names+ callThunkedImpl = CallExpr noPos + (DotRef noPos + (ParenExpr noPos $ FuncExpr noPos [Id noPos "impl"] + (BlockStmt noPos (impl ++ implExport)))+ (Id noPos "apply"))+ [VarRef noPos (Id noPos "impl")]+ +escapeGlobals :: [ParsedStatement] -> [String] -> [ParsedStatement]+escapeGlobals impl exportNames = + [VarDeclStmt noPos [VarDecl noPos (Id noPos s) Nothing] | s <- allGlobals]+ where globalMap = snd (env M.empty impl)+ allGlobals = M.keys globalMap+++makeExportStatements :: InterfaceItem -> [ParsedStatement]+makeExportStatements (InterfaceExport id pos contract) = + [ ExprStmt noPos $ AssignExpr noPos OpAssign + (DotRef noPos (ThisRef noPos) (Id noPos id))+ (compileContract id contract pos $ + DotRef noPos (VarRef noPos (Id noPos "impl")) (Id noPos id))+ ]+-- allows external code to use "instanceof id"+makeExportStatements (InterfaceInstance id _ _) =+ [ ExprStmt noPos $ AssignExpr noPos OpAssign + (DotRef noPos (ThisRef noPos) (Id noPos id))+ (DotRef noPos (VarRef noPos (Id noPos "impl")) (Id noPos id))+ ]+makeExportStatements _ = [ ]++exportRelease :: InterfaceItem -> ParsedStatement+exportRelease (InterfaceExport id _ contract) = + ExprStmt noPos $ AssignExpr noPos OpAssign + (DotRef noPos (ThisRef noPos) (Id noPos id))+ (DotRef noPos (VarRef noPos (Id noPos "impl")) (Id noPos id))+exportRelease _ = error "exportRelease: expected InterfaceItem"+++-- Given source that reads:+--+-- foo = contract;+-- ...+--+-- Transform it to:+--+-- var foo = { }; ...+-- (function() {+-- var tmp = contract;+-- foo.client = tmp.client;+-- foo.server = tmp.server;+-- })();+--+-- The names are first initialized to empty object to permit mutually-recursive+-- contract definitions.+compileAliases :: [InterfaceItem] -> [ParsedStatement]+compileAliases aliases = concatMap init aliases ++ concatMap def aliases where+ init (InterfaceAlias id _) = templateStatements+ $ renameVar "alias" id (stmtTemplate "var alias = { };")+ init (InterfaceInstance id _ _) = templateStatements -- same as above+ $ renameVar "alias" id (stmtTemplate "var alias = { };")+ init _ = []+ def (InterfaceAlias id contract) = templateStatements+ $ renameVar "alias" id+ $ substVar "contract" (cc contract)+ (stmtTemplate "(function() { var tmp = contract;\n \+ \ alias.client = tmp.client;\n \+ \ alias.flat = tmp.flat; \+ \ alias.server = tmp.server; })(); \n")+ def (InterfaceInstance id loc contract) = templateStatements+ $ renameVar "alias" id+ $ substVar "contract" (cc contract)+ $ substVar "name" (StringLit noPos id)+ $ substVar "constr" (DotRef noPos (VarRef noPos (Id noPos "impl"))+ (Id noPos id))+ (stmtTemplate "(function() { \+ \ var tmp = contracts.instance(name)(constr,contract); \+ \ alias.client = tmp.client; \+ \ alias.flat = tmp.flat; \+ \ alias.server = tmp.server; })(); ")+ def _ = []++compile :: [ParsedStatement] -- ^implementation+ -> [InterfaceItem] -- ^the interface+ -> [ParsedStatement] -- ^contract library+ -> ParsedStatement -- ^encapsulated implementation+compile impl interface boilerplateStmts =+ let exportStmts = concatMap makeExportStatements interface+ exportNames = [n | InterfaceExport n _ _ <- interfaceExports]+ instanceNames = [ n | InterfaceInstance n _ _+ <- filter isInterfaceInstance interface ]+ aliases = filter isInterfaceAlias interface+ aliasStmts = compileAliases interface+ wrappedImpl = wrapImplementation (escapeGlobals impl exportNames ++ impl)+ (exportNames ++ instanceNames)+ interfaceStmts = map interfaceStatement $ + filter isInterfaceStatement interface+ interfaceExports = filter isInterfaceExport interface++ outerWrapper = ParenExpr noPos $ FuncExpr noPos [] $ BlockStmt noPos $ + wrappedImpl ++ boilerplateStmts ++ interfaceStmts ++ aliasStmts +++ exportStmts+ in ExprStmt noPos $ CallExpr noPos outerWrapper []+++libraryHeader =+ "(function () {\n \+ \ var impl = { };\n \+ \ (function() {\n"++compileRelease :: String -- ^implementation+ -> String -- ^implementation source+ -> String -- ^contract library+ -> [InterfaceItem] -- ^the interface+ -> Maybe String -- ^the namespace name+ -> String -- ^encapsulated implementation+compileRelease rawImpl implSource boilerplate interface namespace =+ libraryHeader ++ (renderStatements $ escapeGlobals impl exportNames) + ++ rawImpl ++ exposeStatements ++ "\n}).apply(impl,[]);\n" + ++ exportStatements ++ namespaceStatements ++ "\n})();" where+ impl = case parseScriptFromString implSource rawImpl of+ Left err -> error (show err)+ Right (Script _ stmts) -> stmts+ exports = filter isInterfaceExport interface+ exportStatements = renderStatements (map exportRelease exports)+ exportNames = [n | InterfaceExport n _ _ <- exports ]+ instanceNames = + [n | InterfaceInstance n _ _ <- filter isInterfaceInstance interface]+ exposeStatements = renderStatements+ (exposeImplementation (exportNames ++ instanceNames))+ namespaceStatements = case namespace of+ Nothing -> ""+ Just s -> renderStatements (exportNamespace s)++compileFormatted :: String -- ^implementation+ -> String -- ^implementation source+ -> String -- ^contract library+ -> [InterfaceItem] -- ^the interface+ -> String -- ^encapsulated implementation+compileFormatted rawImpl implSource boilerplate interface =+ libraryHeader ++ (renderStatements $ escapeGlobals impl exportNames) + ++ rawImpl+ ++ exposeStatements ++ "\n}).apply(impl,[]);\n" ++ boilerplate + ++ interfaceStatements+ ++ aliasStatements ++ exportStatements+ ++ "\n})();" where+ impl = case parseScriptFromString implSource rawImpl of+ Left err -> error (show err)+ Right (Script _ stmts) -> stmts+ exports = filter isInterfaceExport interface+ exportStatements = + renderStatements (concatMap makeExportStatements interface)+ exportNames = [n | InterfaceExport n _ _ <- exports ]+ aliases = filter isInterfaceAlias interface+ aliasStatements = renderStatements (compileAliases interface)+ instanceNames = + [n | InterfaceInstance n _ _ <- filter isInterfaceInstance interface]+ exposeStatements = renderStatements $+ exposeImplementation (exportNames ++ instanceNames)+ interfaceStatements = renderStatements $ map interfaceStatement $ + filter isInterfaceStatement interface+ +compile' :: [ParsedStatement] -> [InterfaceItem] -> IO ParsedStatement+compile' impl iface = do+ dataDir <- getDataDir+ boilerplateStmts <- parseJavaScriptFromFile (dataDir</>"contracts.js")+ return $ compile impl iface boilerplateStmts++ +compileContract :: String -- ^export name+ -> Contract -- ^ contract+ -> SourcePos -- ^ location of export+ -> ParsedExpression + -> ParsedExpression+compileContract exportId contract pos guardExpr =+ CallExpr noPos (DotRef noPos (VarRef noPos (Id noPos "contracts")) + (Id noPos "guard"))+ [cc contract, guardExpr, StringLit noPos exportId, + StringLit noPos "client", StringLit noPos (show (contractPos contract))]+ where loc = "on " ++ exportId ++ ", defined at " ++ show pos++++-- |contract name compiler+nc :: Contract+ -> String+nc (FlatContract pos predExpr) = + "value that satisfies the predicate at " ++ show pos++-- TODO: hygiene. Use an extended annotation (Either SourcePos ...) to+-- determine whether or not to substitute into a template.+-- |contract compiler++cc :: Contract -- ^parsed contract+ -> ParsedExpression -- ^contract in JavaScript+cc ctc = case ctc of+ FlatContract _ predExpr -> templateExpression+ $ substVar "pred" predExpr + $ substVar "name" (StringLit noPos (nc ctc))+ $ exprTemplate "contracts.flat(name)(pred)"+ FunctionContract pos domainContracts (Just restContract) rangeContract ->+ templateExpression+ $ substVar "restArg" (cc restContract)+ $ substVar "result" (cc rangeContract)+ $ substVarList "fixedArgs" + (map cc domainContracts) + $ substVar "name" (StringLit noPos $ "function at " ++ show pos)+ $ exprTemplate + "contracts.varArityFunc(name)([fixedArgs],restArg,result)"+ FunctionContract pos domainContracts Nothing rangeContract ->+ let isUndefined = DotRef noPos (VarRef noPos (Id noPos "contracts"))+ (Id noPos "isUndefined")+ in templateExpression+ $ substVar "restArg" isUndefined+ $ substVar "result" (cc rangeContract)+ $ substVarList "fixedArgs" (map cc domainContracts) + $ substVar "name" (StringLit noPos $ "function at " ++ show pos)+ $ exprTemplate + "contracts.varArityFunc(name)([fixedArgs],restArg,result)"+ -- User-defined contract constructor+ ConstructorContract pos constrName args -> CallExpr noPos+ (CallExpr noPos+ (VarRef noPos (Id noPos constrName)) + [StringLit noPos constrName])+ (map cc args)+ FixedArrayContract pos elts -> templateExpression+ $ substVarList "contracts" (map cc elts) + $ substVar "name" (StringLit noPos "fixed-array")+ $ exprTemplate "contracts.fixedArray(name)(contracts)"+ ArrayContract _ elt -> templateExpression+ $ substVar "contract" (cc elt) + $ substVar "name" (StringLit noPos "array")+ $ exprTemplate "contracts.unsizedArray(name)(contract)"+ ObjectContract pos fields ->+ let getField id = DotRef noPos (VarRef noPos $ Id noPos "val") (Id noPos id)+ mkProp id = PropId noPos (Id noPos id) + fieldContract (id,contract) = (id, cc contract)+ in templateExpression + $ substFieldList "fieldNames" (map fieldContract fields) + $ substVar "name" (StringLit noPos "name")+ $ exprTemplate "contracts.obj(name)({ fieldNames: 42 })"+ NamedContract _ nameRef ->+ VarRef noPos (Id noPos nameRef)
+ src/BrownPLT/JavaScript/Contracts/Interface.hs view
@@ -0,0 +1,5 @@+module BrownPLT.JavaScript.Contracts.Interface+ ( module BrownPLT.JavaScript.Contracts.Types+ ) where++import BrownPLT.JavaScript.Contracts.Types
+ src/BrownPLT/JavaScript/Contracts/Parser.hs view
@@ -0,0 +1,164 @@+module BrownPLT.JavaScript.Contracts.Parser+ ( interface+ , parseInterface+ ) where++import Control.Monad++import qualified Data.List as L+import Text.ParserCombinators.Parsec+import Text.ParserCombinators.Parsec.Expr+import Text.ParserCombinators.Parsec.Pos+import BrownPLT.JavaScript.Lexer+import BrownPLT.JavaScript.Parser (parseSimpleExpr', ParsedExpression, + parseBlockStmt)+import BrownPLT.JavaScript.Contracts.Types++{-+ interface = interfaceItem *++ interfaceItem = identifier :: contract;+ | instance identifier object;+ | identifier = contract;+ | blockStmt+ + function = nonFunction * -> function+ | nonFunction + ... -> function+ | nonFunction++ nonFunction = :flat+ | contractLabel+ | customConstructor(contract ,*)+ | object+ | ( function )+ | [ contract ,* ] -- fixed length array+ | [ contract , ... ] -- arbitrary length array++ flat = jsExpr++ object = { identifier : contract ,* }++-}++jsExpr = parseSimpleExpr'++contract :: CharParser st Contract+contract = function++function :: CharParser st Contract+function = do+ pos <- getPosition+ args <- nonFunction `sepBy` whiteSpace+ case args of+ [] -> do+ reserved "->"+ result <- function+ return (FunctionContract pos [] Nothing result)+ [arg] -> (do reserved "->"+ result <- function+ return (FunctionContract pos [arg] Nothing result)) <|>+ (do reserved "..."+ reserved "->"+ result <- function+ return (FunctionContract pos [] (Just arg) result)) <|>+ return arg -- nonfunction+ args' -> (do reserved "->"+ result <- function+ return (FunctionContract pos args' Nothing result)) <|>+ (do reserved "..."+ reserved "->" <?> "-> after ..."+ result <- function+ let (fixedArgs,[restArg]) = L.splitAt (length args' - 1) args'+ return (FunctionContract pos fixedArgs (Just restArg) result))++namedContract :: CharParser st Contract+namedContract = do+ idFirst <- letter <|> oneOf "$_"+ pos <- getPosition+ -- same as JavaScript (from WebBits' lexer)+ idRest <- many1 (alphaNum <|> oneOf "$_")+ let name = idFirst:idRest+ let constr = do+ args <- parens $ contract `sepBy1` comma+ return (ConstructorContract pos name args)+ let named = do+ whiteSpace+ return (NamedContract pos name)+ constr <|> named+ ++nonFunction = parens function <|> object <|> namedContract <|> array <|> flat ++array :: CharParser st Contract+array = do+ pos <- getPosition+ reservedOp "["+ elt1 <- contract+ comma+ let arbitrary = do+ reservedOp "..."+ reservedOp "]"+ return (ArrayContract pos elt1)+ let fixed = do+ elts <- contract `sepBy` comma <?> "elements in an array contract"+ reservedOp "]"+ return (FixedArrayContract pos (elt1:elts))+ arbitrary <|> fixed++field :: CharParser st (String,Contract)+field = do+ id <- identifier+ reservedOp ":"+ ctc <- contract+ return (id,ctc)+ ++object :: CharParser st Contract+object = do+ pos <- getPosition+ fields <- braces $ field `sepBy1` (reservedOp ",")+ return (ObjectContract pos fields) + +flat :: CharParser st Contract+flat = do+ pos <- getPosition+ reservedOp ":"+ expr <- jsExpr <?> "JavaScript expression"+ return (FlatContract pos expr)+ +interfaceExport = do+ reservedOp "::"+ return $ \id -> + liftM2 (InterfaceExport id) getPosition contract++interfaceAlias = do+ reservedOp "=" >> (return $ \id -> liftM (InterfaceAlias id) contract)++interfaceInstance = do+ reserved "instance"+ id <- identifier+ pos <- getPosition+ contract <- object+ reservedOp ";"+ return (InterfaceInstance id pos contract)++interfaceElement = interfaceExport <|> interfaceAlias++interface :: CharParser st [InterfaceItem]+interface = many $ interfaceInstance <|> + (stmt $ interfaceElement `fap` identifier) <|> + (liftM InterfaceStatement parseBlockStmt)+ where stmt p = do { e <- p; reservedOp ";"; return e }+ fap k m = do { e <- m; f <- k; f e }++parseInterface :: String -> IO [InterfaceItem]+parseInterface filename = do+ chars <- readFile filename+ let parser = do+ whiteSpace+ r <- interface+ eof+ return r+ case parse parser filename chars of+ Left err -> fail (show err)+ Right exports -> return exports
+ src/BrownPLT/JavaScript/Contracts/Template.hs view
@@ -0,0 +1,134 @@+module BrownPLT.JavaScript.Contracts.Template + ( JavaScriptTemplate+ , exprTemplate+ , stmtTemplate+ , substVar+ , substVarList+ , substIdList+ , substFieldList+ , expandCall+ , templateExpression+ , noPos+ , thunkExpr+ , renderTemplate+ , renameVar+ , templateStatements+ ) where++import Data.Data+import Data.Generics+import Text.ParserCombinators.Parsec (parse, many1)+import Text.ParserCombinators.Parsec.Pos (initialPos, SourcePos)+import Text.PrettyPrint.HughesPJ (render)+import BrownPLT.JavaScript.PrettyPrint (renderExpression, renderStatements)+import BrownPLT.JavaScript.Parser+import BrownPLT.JavaScript.Syntax+import BrownPLT.JavaScript.Instances()+import BrownPLT.JavaScript.Crawl() -- hack for instance ++noPos :: SourcePos+noPos = initialPos "template"++thunkExpr :: ParsedExpression -> ParsedExpression+thunkExpr expr = FuncExpr noPos [] (ReturnStmt noPos (Just expr))++-- We may extend this later so that template definitions explicitly+-- state their free identifiers+data JavaScriptTemplate+ = ExpressionTemplate ParsedExpression+ | StatementTemplate [ParsedStatement]++exprTemplate :: String -> JavaScriptTemplate+exprTemplate str = case parse parseAssignExpr "expression template" str of+ Left err -> error ("Error parsing template: " ++ show err ++ + "; template:\n\n" ++ str)+ Right expr -> ExpressionTemplate expr++stmtTemplate :: String -> JavaScriptTemplate+stmtTemplate str = case parse (many1 parseStatement) "statement template" str of+ Left err -> error ("Error parsing template: " ++ show err ++ + "; template:\n\n" ++ str)+ Right stmts -> StatementTemplate stmts+ +renderTemplate :: JavaScriptTemplate -> String+renderTemplate (ExpressionTemplate expr) = renderExpression expr+renderTemplate (StatementTemplate stmts) = renderStatements stmts++templateExpression :: JavaScriptTemplate -> ParsedExpression+templateExpression (ExpressionTemplate expr) = expr++templateStatements :: JavaScriptTemplate -> [ParsedStatement]+templateStatements (StatementTemplate stmts) = stmts++expandCall :: String -- ^function name to expand+ -> ([ParsedExpression] -> [ParsedExpression]) -- ^argument expander+ -> JavaScriptTemplate+ -> JavaScriptTemplate+expandCall functionId expander (StatementTemplate body) =+ StatementTemplate (everywhere (mkT subst) body) where+ subst (CallExpr p1 fn@(VarRef p2 (Id p3 id')) args) + | id' == functionId = CallExpr p1 fn (expander args)+ subst expr = expr++renameVar :: String -- ^original id+ -> String -- ^new id+ -> JavaScriptTemplate+ -> JavaScriptTemplate+renameVar idOld idNew body =+ let -- explicit signatures needed for generics+ substExpr :: ParsedExpression -> ParsedExpression+ substExpr (VarRef p1 (Id p2 thisId))+ | thisId == idOld = VarRef p1 (Id p2 idNew)+ substExpr v = v+ substDecl :: VarDecl SourcePos -> VarDecl SourcePos+ substDecl (VarDecl p1 (Id p2 thisId) val)+ | thisId == idOld = VarDecl p1 (Id p2 idNew) val+ substDecl v = v+ in case body of+ ExpressionTemplate body -> ExpressionTemplate $+ everywhere ((mkT substDecl) . (mkT substExpr)) body+ StatementTemplate stmts -> StatementTemplate $+ everywhere (mkT substDecl . mkT substExpr) stmts++substVar :: String -- ^free identifier+ -> ParsedExpression -- ^expression to substitute+ -> JavaScriptTemplate+ -> JavaScriptTemplate+substVar id expr body = + let subst (VarRef _ (Id _ id')) | id' == id = expr+ subst expr = expr+ in case body of+ ExpressionTemplate body -> + ExpressionTemplate (everywhere (mkT subst) body)+ StatementTemplate stmts ->+ StatementTemplate (everywhere (mkT subst) stmts)++substVarList :: String -- ^identifier in a list+ -> [ParsedExpression] -- ^list of expressions to substitute+ -> JavaScriptTemplate+ -> JavaScriptTemplate+substVarList id exprs (ExpressionTemplate body) =+ ExpressionTemplate (everywhere (mkT subst) body) where+ subst [VarRef _ (Id _ id')] | id' == id = exprs+ subst lst = lst++substIdList :: String -- ^identifier in a list+ -> [String] -- ^list of identifiers to substitute+ -> JavaScriptTemplate+ -> JavaScriptTemplate+substIdList id ids (ExpressionTemplate body) =+ ExpressionTemplate (everywhere (mkT subst) body) where+ subst [Id _ id'] | id' == id = map (Id noPos) ids+ subst lst = lst++substFieldList :: String -- ^ placeholder field name+ -> [(String,ParsedExpression)] -- ^list of fields+ -> JavaScriptTemplate+ -> JavaScriptTemplate+substFieldList fieldId fields (ExpressionTemplate body) = + ExpressionTemplate (everywhere (mkT subst) body) where+ fields' = map (\(name,expr) -> (PropId noPos (Id noPos name),expr)) fields+ subst [(PropId _ (Id _ id'), _)] | id' == fieldId = fields'+ subst lst = lst+ +
+ src/BrownPLT/JavaScript/Contracts/Types.hs view
@@ -0,0 +1,45 @@+module BrownPLT.JavaScript.Contracts.Types where++import qualified Data.List as L+import Text.ParserCombinators.Parsec.Pos (SourcePos)+import BrownPLT.JavaScript.Parser (ParsedExpression, ParsedStatement)++data Contract+ = FlatContract SourcePos ParsedExpression+ | NamedContract SourcePos String+ | FunctionContract SourcePos [Contract] (Maybe Contract) Contract+ | ConstructorContract SourcePos String [Contract]+ | FixedArrayContract SourcePos [Contract]+ | ArrayContract SourcePos Contract+ | ObjectContract SourcePos [(String,Contract)]+ deriving (Show)+++contractPos :: Contract -> SourcePos+contractPos ctc = case ctc of+ FlatContract p _ -> p+ NamedContract p _ -> p+ FunctionContract p _ _ _ -> p+ ConstructorContract p _ _ -> p+ FixedArrayContract p _ -> p+ ArrayContract p _ -> p+ ObjectContract p _ -> p++data InterfaceItem + = InterfaceExport String SourcePos Contract+ | InterfaceAlias String Contract+ | InterfaceStatement { interfaceStatement :: ParsedStatement }+ | InterfaceInstance String SourcePos Contract -- ^always an object contract+ deriving (Show)++isInterfaceStatement (InterfaceStatement _) = True+isInterfaceStatement _ = False++isInterfaceExport (InterfaceExport{}) = True+isInterfaceExport _ = False++isInterfaceAlias (InterfaceAlias{}) = True+isInterfaceAlias _ = False++isInterfaceInstance (InterfaceInstance{}) = True+isInterfaceInstance _ = False
+ src/Jscc.hs view
@@ -0,0 +1,100 @@+-- JavaScript Contract Compiler+module Main where++import System.Console.GetOpt+import System.Environment+import System.Directory+import System.FilePath+import System.Exit+import Control.Monad+import BrownPLT.JavaScript.Contracts+import Paths_JsContracts -- created by Cabal+import BrownPLT.JavaScript.Parser (parseJavaScriptFromFile)++data Flag+ = Help+ | Release+ | Debug+ | Namespace String+ | Interface String+ deriving (Eq,Ord,Show)+ ++options :: [ OptDescr Flag ]+options =+ [ Option ['h'] ["help"] (NoArg Help)+ "display this help message"+ , Option ['r'] ["release"] (NoArg Release)+ "encapsulate, ignoring all contracts"+ , Option ['d'] ["debug"] (NoArg Debug)+ "enable contracts and encapsulate (default)"+ , Option ['n'] ["namespace"] (ReqArg Namespace "NAMESPACE")+ "exports names to the namespace"+ , Option ['i'] ["interface"] (ReqArg Interface "PATH")+ "path to the interface; uses module.jsi by default"+ ]++usage = usageInfo+ "Usage: jscc [options] module.js\nOptions:\n" options++main = do+ args <- getArgs+ dataDir <- getDataDir+ let (opts, nonOpts, errors) = getOpt RequireOrder options args+ unless (null errors) $ do+ mapM_ putStrLn errors+ fail "jscc terminated"+ checkHelp opts+ (isDebugMode, opts) <- getDebugMode opts+ (namespace, opts) <- getNamespace opts+ (ifacePath, opts) <- getInterfacePath opts nonOpts+ when (not $ null opts) $ do+ putStrLn $ "spurious arguments: " ++ (show opts)+ fail "jscc terminated"+ case nonOpts of+ [implPath] -> do+ checkFile implPath+ rawImpl <- readFile implPath+ let boilerplatePath = dataDir </> "contracts.js"+ rawBoilerplate <- readFile boilerplatePath+ interface <- parseInterface ifacePath+ let result = if isDebugMode+ then compileFormatted rawImpl implPath rawBoilerplate + interface+ else compileRelease rawImpl implPath rawBoilerplate+ interface namespace+ putStrLn result+ return () + otherwise -> do+ putStrLn "expected a single filename.js"+ fail "jscc terminated"++checkFile path = do+ exists <- doesFileExist path+ unless exists $ do+ putStrLn $ "could not find the file: " ++ path+ exitFailure++getDebugMode (Release:rest) = return (False,rest)+getDebugMode (Debug:rest) = return (True,rest)+getDebugMode rest = return (True,rest)++getNamespace ((Namespace s):rest) = return (Just s, rest)+getNamespace rest = return (Nothing,rest)++checkHelp (Help:_) = do+ putStrLn usage+ exitSuccess+checkHelp _ = return ()++getInterfacePath :: [Flag] -> [String] -> IO (FilePath,[Flag])+getInterfacePath (Interface path:rest) _ = do+ checkFile path+ return (path,rest)+getInterfacePath rest (implPath:_) = do+ let path = addExtension (dropExtension implPath) "jsi"+ checkFile path+ return (path,rest)+getInterfacePath _ [] = do+ putStrLn "Invalid arguments (use -h for help)"+ exitFailure