├── test ├── mocha.opts ├── .eslintrc.yml ├── parser.js ├── expressions │ ├── lookup.js │ ├── recordvalue.js │ └── apply.js ├── types │ ├── record.js │ ├── either.js │ ├── array.js │ └── varlen.js ├── source.js ├── statements │ ├── return.js │ ├── assign.js │ ├── while.js │ ├── rule.js │ ├── foreach.js │ ├── rulefor.js │ ├── match.js │ └── ifelse.js ├── environment.js ├── execution.js ├── pubsub.js ├── input.js ├── changesets.js └── compiler.js ├── bin ├── .eslintrc.yml ├── parser-main.js └── main.js ├── .travis.yml ├── .gitignore ├── CODEOWNERS ├── lib ├── prelude.model ├── statements │ ├── paramdecl.js │ ├── break.js │ ├── continue.js │ ├── print.js │ ├── statement.js │ ├── reset.js │ ├── typedecl.js │ ├── do.js │ ├── sequence.js │ ├── invariant.js │ ├── return.js │ ├── block.js │ ├── assert.js │ ├── rule.js │ ├── assign.js │ ├── while.js │ ├── factory.js │ ├── ifelse.js │ ├── vardecl.js │ ├── function.js │ ├── foreach.js │ ├── match.js │ └── rulefor.js ├── util.js ├── types │ ├── type.js │ ├── value.js │ ├── blackholenumber.js │ ├── output.js │ ├── number.js │ ├── factory.js │ ├── types.js │ ├── range.js │ ├── array.js │ ├── record.js │ ├── either.js │ └── varlen.js ├── expressions │ ├── number.js │ ├── expression.js │ ├── factory.js │ ├── index.js │ ├── recordvalue.js │ ├── id.js │ └── lookup.js ├── testing.js ├── source.js ├── compiler.js ├── pubsub.js ├── input.js ├── simulator.js ├── errors.js ├── modelchecker.js ├── changesets.js ├── environment.js ├── execution.js └── workspace.js ├── test-scripts ├── parser │ ├── runtest.sh │ ├── runtests.sh │ ├── input-2.model │ └── input-1.model ├── keywordcheck.sh └── runtests.sh ├── keywords ├── .eslintrc.yml ├── package.json ├── LICENSE.md ├── vim └── runway.vim ├── README.md └── doc ├── JAVASCRIPT-API.md └── LANGUAGE-GUIDE.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | -------------------------------------------------------------------------------- /bin/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bck 2 | /doc/JAVASCRIPT-API.html 3 | /doc/LANGUAGE-GUIDE.html 4 | /README.html 5 | /node_modules/ 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /lib/prelude.model: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | type Boolean: either { 10 | False, 11 | True, 12 | } 13 | -------------------------------------------------------------------------------- /test-scripts/parser/runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2015-2016, Salesforce.com, Inc. 4 | # All rights reserved. 5 | # Licensed under the MIT license. 6 | # For full license text, see LICENSE.md file in the repo root or 7 | # https://opensource.org/licenses/MIT 8 | 9 | 10 | set -e 11 | 12 | (node bin/parser-main.js $1 2>&1 || echo Exit status $?) >$2 13 | git diff -w --exit-code $2 14 | -------------------------------------------------------------------------------- /lib/statements/paramdecl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let VarDecl = require('./vardecl.js'); 12 | 13 | class ParamDecl extends VarDecl { 14 | } 15 | 16 | module.exports = ParamDecl; 17 | -------------------------------------------------------------------------------- /test-scripts/parser/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2015-2016, Salesforce.com, Inc. 4 | # All rights reserved. 5 | # Licensed under the MIT license. 6 | # For full license text, see LICENSE.md file in the repo root or 7 | # https://opensource.org/licenses/MIT 8 | 9 | 10 | set -e 11 | 12 | dir=test-scripts/parser 13 | 14 | for t in 1 2; do 15 | echo "parser test: input-${t}.model" 16 | $dir/runtest.sh $dir/input-${t}.model $dir/output-${t}.json 17 | done 18 | -------------------------------------------------------------------------------- /keywords: -------------------------------------------------------------------------------- 1 | Array 2 | Boolean 3 | False 4 | MultiSet 5 | OrderedSet 6 | Output 7 | Set 8 | Time 9 | True 10 | Vector 11 | as 12 | assert 13 | break 14 | capacity 15 | contains 16 | continue 17 | either 18 | else 19 | empty 20 | external 21 | for 22 | full 23 | function 24 | if 25 | in 26 | invariant 27 | later 28 | match 29 | param 30 | past 31 | pop 32 | pow 33 | push 34 | print 35 | record 36 | remove 37 | reset 38 | return 39 | rule 40 | size 41 | type 42 | urandom 43 | urandomRange 44 | var 45 | while 46 | -------------------------------------------------------------------------------- /test-scripts/keywordcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2016, Salesforce.com, Inc. 4 | # All rights reserved. 5 | # Licensed under the MIT license. 6 | # For full license text, see LICENSE.md file in the repo root or 7 | # https://opensource.org/licenses/MIT 8 | 9 | set -e 10 | 11 | status=0 12 | 13 | for keyword in $(cat keywords); do 14 | if ! (cat $* | grep "\b$keyword\b" >/dev/null); then 15 | echo "Error: keyword '$keyword' not found in $*" 16 | status=1 17 | fi 18 | done 19 | 20 | exit $status 21 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | commonjs: true 3 | shared-node-browser: true 4 | es6: true 5 | extends: 'eslint:recommended' 6 | rules: 7 | comma-dangle: 8 | - error 9 | - only-multiline 10 | no-console: 11 | - off 12 | indent: 13 | - error 14 | - 2 15 | linebreak-style: 16 | - error 17 | - unix 18 | no-unused-vars: 19 | - error 20 | - args: none 21 | quotes: 22 | - error 23 | - single 24 | - {avoidEscape: true, allowTemplateLiterals: true} 25 | semi: 26 | - error 27 | - always 28 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let stringCount = (haystack, needle) => { 12 | let count = 0; 13 | let i = -1; 14 | for (;;) { 15 | i = haystack.indexOf(needle, i + 1); 16 | if (i >= 0) { 17 | count += 1; 18 | } else { 19 | return count; 20 | } 21 | } 22 | }; 23 | 24 | module.exports = { 25 | stringCount: stringCount, 26 | }; 27 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../lib/testing.js'); 13 | 14 | describe('parser.js', function() { 15 | describe('Parser', function() { 16 | it('whitespace', function() { 17 | assert.throws(() => testing.run(` 18 | ifTrue { 19 | print 1; 20 | } 21 | `)); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/statements/break.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Statement = require('./statement.js'); 13 | 14 | class Break extends Statement { 15 | constructor(parsed, env) { 16 | super(parsed, env); 17 | } 18 | 19 | typecheck() {} 20 | 21 | execute() { 22 | throw new errors.Break(`Uncaught break at ${this.parsed.source}`); 23 | } 24 | } 25 | 26 | module.exports = Break; 27 | -------------------------------------------------------------------------------- /lib/statements/continue.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Statement = require('./statement.js'); 13 | 14 | class Continue extends Statement { 15 | constructor(parsed, env) { 16 | super(parsed, env); 17 | } 18 | 19 | typecheck() {} 20 | 21 | execute() { 22 | throw new errors.Continue(`Uncaught continue at ${this.parsed.source}`); 23 | } 24 | } 25 | 26 | module.exports = Continue; 27 | -------------------------------------------------------------------------------- /bin/parser-main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let fs = require('fs'); 12 | let Input = require('../lib/input.js'); 13 | let parser = require('../lib/parser.js'); 14 | 15 | 16 | if (require.main === module) { 17 | let filename = 'input.model'; 18 | if (process.argv.length > 2) { 19 | filename = process.argv[2]; 20 | } 21 | let text = fs.readFileSync(filename).toString(); 22 | let parsed = parser.parse(new Input(filename, text)); 23 | console.log(JSON.stringify(parsed, null, 2)); 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runway-compiler", 3 | "version": "0.1.0", 4 | "description": "Compiler for Runway models of distributed systems", 5 | "main": "main.js", 6 | "dependencies": { 7 | "docopt": "^0.6.2", 8 | "lodash": "^4.2.1", 9 | "parsimmon": "^0.7.2", 10 | "performance-now": "^0.2.0" 11 | }, 12 | "devDependencies": { 13 | "mocha": "^2.3.3" 14 | }, 15 | "scripts": { 16 | "test": "./test-scripts/runtests.sh", 17 | "test-mocha": "./node_modules/.bin/mocha", 18 | "test-parser": "./test-scripts/parser/runtests.sh" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/salesforce/runway-compiler.git" 23 | }, 24 | "author": "Diego Ongaro ", 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /lib/statements/print.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let makeExpression = require('../expressions/factory.js'); 12 | let Statement = require('./statement.js'); 13 | 14 | class Print extends Statement { 15 | constructor(parsed, env) { 16 | super(parsed, env); 17 | this.expr = makeExpression.make(this.parsed.expr, this.env); 18 | } 19 | 20 | typecheck() { 21 | this.expr.typecheck(); 22 | } 23 | 24 | execute(context) { 25 | console.log(this.expr.evaluate(context).toString()); 26 | } 27 | } 28 | 29 | module.exports = Print; 30 | -------------------------------------------------------------------------------- /test/expressions/lookup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('expressions/lookup.js', function() { 15 | describe('lookup', function() { 16 | 17 | it('basic', function() { 18 | let module = testing.run(` 19 | type T : record { 20 | inner: Boolean, 21 | } 22 | var t : T; 23 | var x : Boolean = True; 24 | x = t.inner; 25 | `); 26 | assert.equal(module.env.getVar('x').toString(), 'False'); 27 | }); 28 | 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/statements/statement.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | 13 | class Statement { 14 | constructor(parsed, env) { 15 | this.parsed = parsed; 16 | this.env = env; 17 | } 18 | 19 | typecheck() { 20 | throw new errors.Internal(`typecheck() not implemented for ${this.parsed.kind} statement`); 21 | } 22 | 23 | execute() { 24 | throw new errors.Internal(`execute() not implemented for ${this.parsed.kind} statement`); 25 | } 26 | 27 | toString(indent) { 28 | return `${indent}${this.parsed.kind} is missing toString();`; 29 | } 30 | } 31 | 32 | module.exports = Statement; 33 | -------------------------------------------------------------------------------- /lib/types/type.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | 13 | class Type { 14 | constructor(decl, env, name) { 15 | this.decl = decl; 16 | this.env = env; 17 | this.name = name; // may be undefined 18 | } 19 | equals(other) { 20 | throw new errors.Type(`equals() not implemented for ${this} type`); 21 | } 22 | getName() { 23 | if (this.name === undefined) { 24 | return undefined; 25 | } else if (typeof this.name == 'string') { 26 | return this.name; 27 | } else { 28 | return this.name.value; 29 | } 30 | } 31 | } 32 | 33 | module.exports = Type; 34 | -------------------------------------------------------------------------------- /lib/expressions/number.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Expression = require('./expression.js'); 12 | let NumberType = require('../types/number.js').Type; 13 | 14 | class NumberExpr extends Expression { 15 | constructor(parsed, env) { 16 | super(parsed, env); 17 | this.type = NumberType.singleton; 18 | } 19 | 20 | typecheck() { 21 | // no-op 22 | } 23 | 24 | evaluate() { 25 | let v = NumberType.singleton.makeDefaultValue(); 26 | v.assign(this.parsed.value); 27 | return v; 28 | } 29 | 30 | toString(indent) { 31 | return `${this.parsed.value}`; 32 | } 33 | } 34 | 35 | module.exports = NumberExpr; 36 | -------------------------------------------------------------------------------- /lib/expressions/expression.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | 13 | class Expression { 14 | constructor(parsed, env) { 15 | this.parsed = parsed; 16 | this.env = env; 17 | } 18 | 19 | typecheck() { 20 | throw new errors.Unimplemented(`typecheck() not implemented for ${this.parsed.kind} expression`); 21 | } 22 | 23 | evaluate() { 24 | throw new errors.Unimplemented(`evaluate() not implemented for ${this.parsed.kind} expression`); 25 | } 26 | 27 | toString(indent) { 28 | throw new errors.Unimplemented(`${this.parsed.kind} is missing toString()`); 29 | } 30 | } 31 | 32 | module.exports = Expression; 33 | -------------------------------------------------------------------------------- /lib/statements/reset.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Statement = require('./statement.js'); 12 | 13 | class Reset extends Statement { 14 | constructor(parsed, env) { 15 | super(parsed, env); 16 | this.globalEnv = this.env; 17 | while (this.globalEnv.rules === undefined) { 18 | this.globalEnv = this.globalEnv.enclosing; 19 | } 20 | } 21 | 22 | typecheck() {} 23 | 24 | execute() { 25 | this.globalEnv.vars.forEach((mvar, name) => { 26 | mvar.assign(mvar.type.makeDefaultValue()); 27 | }); 28 | // TODO: how to run global initialization? 29 | // module.ast.execute(context); 30 | } 31 | } 32 | 33 | module.exports = Reset; 34 | -------------------------------------------------------------------------------- /test/types/record.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('types/record.js', function() { 15 | describe('record', function() { 16 | it('basic', function() { 17 | let module = testing.run(` 18 | type DigitBox : record { 19 | digit: 0..9, 20 | } 21 | var a : DigitBox; 22 | var b : DigitBox = DigitBox { digit: 3 }; 23 | `); 24 | assert.equal(module.env.getVar('a').toString(), 'DigitBox { digit: 0 }'); 25 | assert.equal(module.env.getVar('b').toString(), 'DigitBox { digit: 3 }'); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/testing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let GlobalEnvironment = require('./environment.js').GlobalEnvironment; 12 | let Input = require('./input.js'); 13 | let compiler = require('./compiler.js'); 14 | let fs = require('fs'); 15 | 16 | let readFile = (filename) => fs.readFileSync(filename).toString(); 17 | 18 | let run = function(code) { 19 | let prelude = compiler.loadPrelude(readFile('lib/prelude.model')); 20 | let env = new GlobalEnvironment(prelude.env); 21 | let module = compiler.load(new Input('unit test', code), env); 22 | let context = {}; 23 | module.ast.execute(context); 24 | return module; 25 | }; 26 | 27 | module.exports = { 28 | run: run, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/statements/typedecl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let makeType = require('../types/factory.js'); 12 | let Statement = require('./statement.js'); 13 | 14 | // type T : Boolean; 15 | 16 | class TypeDecl extends Statement { 17 | constructor(parsed, env) { 18 | super(parsed, env); 19 | let type = makeType.make(this.parsed.type, this.env, this.parsed.id); 20 | this.env.assignType(this.parsed.id.value, type); 21 | } 22 | 23 | typecheck() { 24 | // no-op 25 | } 26 | 27 | execute() { 28 | // no-op 29 | } 30 | 31 | toString(indent) { 32 | return `${indent}type ${this.parsed.id.value} : ...;`; 33 | } 34 | } 35 | 36 | module.exports = TypeDecl; 37 | -------------------------------------------------------------------------------- /lib/types/value.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | 13 | class Value { 14 | constructor(type) { 15 | this.type = type; 16 | } 17 | 18 | assign(other) { 19 | throw new errors.Type(`assign() not implemented for ${this.type} values`); 20 | } 21 | 22 | assignJSON() { 23 | throw new errors.Type(`assignJSON() not implemented for ${this.type} values`); 24 | } 25 | 26 | equals(other) { 27 | throw new errors.Type(`equals() not implemented for ${this.type} values`); 28 | } 29 | 30 | toJSON() { 31 | throw new errors.Type(`toJSON() not implemented for ${this.type} values`); 32 | } 33 | } 34 | 35 | module.exports = Value; 36 | -------------------------------------------------------------------------------- /test/source.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let Input = require('../lib/input.js'); 13 | let Source = require('../lib/source.js'); 14 | 15 | let alphabet = `abc 16 | def 17 | ghi 18 | jkl 19 | mno 20 | pqr 21 | stu 22 | vwx 23 | yz 24 | `; 25 | 26 | describe('source.js', function() { 27 | describe('Source', function() { 28 | it('basic', function() { 29 | let source = new Source( 30 | {offset: 10, line: 3, column: 3}, 31 | {offset: 20, line: 6, column: 1}); 32 | assert.equal(source.toString(), 'chars 10-20'); 33 | source.setInput(new Input('foo.txt', alphabet)); 34 | assert.equal(source.toString(), 'foo.txt: line 3, col 3 to line 6, col 1'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/statements/do.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let makeExpression = require('../expressions/factory.js'); 13 | let Statement = require('./statement.js'); 14 | 15 | class Do extends Statement { 16 | constructor(parsed, env) { 17 | super(parsed, env); 18 | this.expr = makeExpression.make(this.parsed.expr, this.env); 19 | } 20 | 21 | typecheck() { 22 | this.expr.typecheck(); 23 | if (this.parsed.expr.kind != 'apply' || 24 | this.expr.fn.pure !== false) { 25 | throw new errors.Type(`Statement has no effect ` + 26 | `at ${this.parsed.source}`); 27 | } 28 | } 29 | 30 | execute(context) { 31 | this.expr.evaluate(context); 32 | } 33 | } 34 | 35 | module.exports = Do; 36 | -------------------------------------------------------------------------------- /lib/statements/sequence.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Statement = require('./statement.js'); 12 | let makeStatement = require('./factory.js'); 13 | 14 | class Sequence extends Statement { 15 | constructor(parsed, env) { 16 | super(parsed, env); 17 | this.statements = this.parsed.statements.map((s) => makeStatement.make(s, this.env)); 18 | } 19 | 20 | execute(context) { 21 | this.statements.forEach((s) => s.execute(context)); 22 | } 23 | 24 | typecheck() { 25 | this.statements.forEach((s) => s.typecheck()); 26 | } 27 | 28 | toString(indent) { 29 | if (indent === undefined) { 30 | indent = ''; 31 | } 32 | return this.statements.map((s) => s.toString(indent)).join('\n'); 33 | } 34 | } 35 | 36 | module.exports = Sequence; 37 | -------------------------------------------------------------------------------- /lib/statements/invariant.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Statement = require('./statement.js'); 12 | let makeStatement = require('./factory.js'); 13 | 14 | class Invariant extends Statement { 15 | constructor(parsed, env) { 16 | super(parsed, env); 17 | this.inner = makeStatement.make(this.parsed.code, this.env); 18 | this.env.invariants.set(this.parsed.id.value, this, this.parsed.source); 19 | } 20 | 21 | typecheck() { 22 | this.inner.typecheck(); 23 | } 24 | 25 | execute() { 26 | // do nothing 27 | } 28 | 29 | check(context) { 30 | this.inner.execute(context); 31 | } 32 | 33 | toString(indent) { 34 | return `${indent}invariant ${this.parsed.id.value} { 35 | ${this.inner.toString(indent + ' ')} 36 | }`; 37 | } 38 | } 39 | 40 | module.exports = Invariant; 41 | -------------------------------------------------------------------------------- /lib/statements/return.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let makeExpression = require('../expressions/factory.js'); 13 | let Statement = require('./statement.js'); 14 | 15 | class Return extends Statement { 16 | constructor(parsed, env) { 17 | super(parsed, env); 18 | this.expr = makeExpression.make(this.parsed.expr, this.env); 19 | } 20 | 21 | typecheck() { 22 | this.expr.typecheck(); 23 | // TODO: return value should be subtype of containing function's return type 24 | } 25 | 26 | execute(context) { 27 | let e = new errors.Return(`Uncaught return statement at ${this.parsed.source}`); 28 | e.value = this.expr.type.makeDefaultValue(); 29 | e.value.assign(this.expr.evaluate(context)); 30 | throw e; 31 | } 32 | } 33 | 34 | module.exports = Return; 35 | -------------------------------------------------------------------------------- /lib/statements/block.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Environment = require('../environment.js').Environment; 12 | let Statement = require('./statement.js'); 13 | let makeStatement = require('./factory.js'); 14 | 15 | class Block extends Statement { 16 | constructor(parsed, env) { 17 | super(parsed, env); 18 | this.innerEnv = new Environment(this.env); 19 | this.inner = makeStatement.make(this.parsed.code, this.innerEnv); 20 | } 21 | 22 | typecheck() { 23 | this.inner.typecheck(); 24 | } 25 | 26 | execute(context) { 27 | this.innerEnv.vars.forEachLocal(v => v.assign(v.type.makeDefaultValue())); 28 | this.inner.execute(context); 29 | } 30 | 31 | toString(indent) { 32 | return `${indent} { 33 | ${this.inner.toString(indent + ' ')} 34 | }`; 35 | } 36 | } 37 | 38 | module.exports = Block; 39 | -------------------------------------------------------------------------------- /lib/expressions/factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | 13 | let factory = {}; 14 | module.exports = factory; 15 | // export empty object before requiring circular dependencies 16 | 17 | let expressions = new Map([ 18 | ['apply', require('./apply.js')], 19 | ['id', require('./id.js')], 20 | ['index', require('./index.js')], 21 | ['lookup', require('./lookup.js')], 22 | ['number', require('./number.js')], 23 | ['recordvalue', require('./recordvalue.js')], 24 | ]); 25 | 26 | let make = function(parsed, env) { 27 | let expression = expressions.get(parsed.kind); 28 | if (expression !== undefined) { 29 | return new expression(parsed, env); 30 | } 31 | let o = JSON.stringify(parsed, null, 2); 32 | throw new errors.Unimplemented(`Unknown expression: ${o}`); 33 | }; 34 | factory.make = make; 35 | -------------------------------------------------------------------------------- /test/statements/return.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/return.js', function() { 15 | describe('return', function() { 16 | 17 | it('returns copy, not ref', function() { 18 | let module = testing.run(` 19 | type Box : record { 20 | bool: Boolean, 21 | }; 22 | var bools : Array[1..2]; 23 | function getFirst() -> Box { 24 | for box in bools { 25 | return box; 26 | } 27 | } 28 | var mbox : Box = getFirst(); 29 | mbox.bool = True; 30 | `); 31 | assert.equal(module.env.getVar('bools').toString(), 32 | '[1: Box { bool: False }, 2: Box { bool: False }]'); 33 | assert.equal(module.env.getVar('mbox').toString(), 34 | 'Box { bool: True }'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/source.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // Describes a character range of an input. 12 | // Most nodes in the AST have one of these hanging off of them named 'source'. 13 | // This can be used to provide meaningful error messages, tying back an 14 | // expression to its location in the original source file. 15 | class Source { 16 | constructor(startchar, endchar) { 17 | this.input = null; 18 | this.startchar = startchar; 19 | this.endchar = endchar; 20 | } 21 | 22 | setInput(input) { 23 | this.input = input; 24 | } 25 | 26 | toString() { 27 | if (this.input === null) { 28 | return `chars ${this.startchar.offset}-${this.endchar.offset}`; 29 | } else { 30 | return `${this.input.filename}: ` + 31 | `line ${this.startchar.line}, col ${this.startchar.column} to ` + 32 | `line ${this.endchar.line}, col ${this.endchar.column}`; 33 | } 34 | } 35 | } 36 | 37 | module.exports = Source; 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016, Salesforce.com, Inc. 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /lib/statements/assert.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let makeExpression = require('../expressions/factory.js'); 12 | let Statement = require('./statement.js'); 13 | let errors = require('../errors.js'); 14 | let Types = require('../types/types.js'); 15 | 16 | class Assert extends Statement { 17 | constructor(parsed, env) { 18 | super(parsed, env); 19 | this.expr = makeExpression.make(this.parsed.expr, this.env); 20 | } 21 | 22 | typecheck() { 23 | this.expr.typecheck(); 24 | let boolType = this.env.getType('Boolean'); 25 | if (!Types.subtypeOf(this.expr.type, boolType)) { 26 | throw new errors.Type(`Cannot assert ${this.expr.type}`); 27 | } 28 | } 29 | 30 | execute(context) { 31 | if (!this.expr.evaluate(context).equals(this.env.getVar('True'))) { 32 | let msg = `Assertion failed: ${this.expr} at ${this.parsed.source}`; 33 | throw new errors.Assertion(msg); 34 | } 35 | } 36 | } 37 | 38 | module.exports = Assert; 39 | -------------------------------------------------------------------------------- /lib/statements/rule.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Statement = require('./statement.js'); 12 | let makeStatement = require('./factory.js'); 13 | let errors = require('../errors.js'); 14 | 15 | class Rule extends Statement { 16 | constructor(parsed, env) { 17 | super(parsed, env); 18 | this.external = this.parsed.subkind === 'external'; 19 | this.inner = makeStatement.make(this.parsed.code, this.env); 20 | this.env.assignRule(this.parsed.id.value, this); 21 | } 22 | 23 | typecheck() { 24 | this.inner.typecheck(); 25 | } 26 | 27 | execute() { 28 | // do nothing 29 | } 30 | 31 | fire(context) { 32 | try { 33 | this.inner.execute(context); 34 | } catch ( e ) { 35 | if (e instanceof errors.Return) { 36 | return; 37 | } else { 38 | throw e; 39 | } 40 | } 41 | } 42 | 43 | toString(indent) { 44 | return `${indent}rule ${this.parsed.id.value} { 45 | ${this.inner.toString(indent + ' ')} 46 | }`; 47 | } 48 | } 49 | 50 | module.exports = Rule; 51 | -------------------------------------------------------------------------------- /test-scripts/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2016, Salesforce.com, Inc. 4 | # All rights reserved. 5 | # Licensed under the MIT license. 6 | # For full license text, see LICENSE.md file in the repo root or 7 | # https://opensource.org/licenses/MIT 8 | 9 | status=0 10 | 11 | echo "Running unit tests" 12 | ./node_modules/.bin/mocha 13 | (( status += $? )) 14 | 15 | echo "Running parser tests" 16 | test-scripts/parser/runtests.sh 17 | (( status += $? )) 18 | 19 | echo "Checking that compiler files mention every keyword" 20 | test-scripts/keywordcheck.sh \ 21 | lib/expressions/apply.js \ 22 | lib/types/factory.js \ 23 | lib/parser.js \ 24 | lib/prelude.model 25 | (( status += $? )) 26 | 27 | echo "Checking that LANGUAGE-GUIDE.md mentions every keyword" 28 | test-scripts/keywordcheck.sh doc/LANGUAGE-GUIDE.md 29 | (( status += $? )) 30 | 31 | echo "Checking that runway.vim mentions every keyword" 32 | test-scripts/keywordcheck.sh vim/runway.vim 33 | (( status += $? )) 34 | 35 | for dir in node_modules/language-runway ../language-runway; do 36 | if [ -f $dir/grammars/runway.json ]; then 37 | echo "Checking that language-runway mentions every keyword" 38 | test-scripts/keywordcheck.sh $dir/grammars/runway.json 39 | (( status += $? )) 40 | break 41 | fi 42 | done 43 | 44 | exit $status 45 | -------------------------------------------------------------------------------- /lib/statements/assign.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let makeExpression = require('../expressions/factory.js'); 13 | let Statement = require('./statement.js'); 14 | let Types = require('../types/types.js'); 15 | 16 | class Assign extends Statement { 17 | constructor(parsed, env) { 18 | super(parsed, env); 19 | this.lhs = makeExpression.make(this.parsed.lhs, this.env); 20 | this.rhs = makeExpression.make(this.parsed.rhs, this.env); 21 | } 22 | 23 | typecheck() { 24 | this.lhs.typecheck(); 25 | this.rhs.typecheck(); 26 | if (!Types.subtypeOf(this.rhs.type, this.lhs.type)) { 27 | throw new errors.Type(`Cannot assign ${this.rhs.type} to variable of ` + 28 | `type ${this.lhs.type} at ${this.parsed.source}`); 29 | } 30 | } 31 | 32 | execute(context) { 33 | this.lhs.evaluate(context).assign(this.rhs.evaluate(context)); 34 | } 35 | 36 | toString(indent) { 37 | return `${indent}${this.lhs.toString(indent)} = ${this.rhs.toString(indent)};`; 38 | } 39 | } 40 | 41 | module.exports = Assign; 42 | -------------------------------------------------------------------------------- /lib/expressions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Expression = require('./expression.js'); 13 | let Types = require('../types/types.js'); 14 | let makeExpression = require('./factory.js'); 15 | 16 | class Index extends Expression { 17 | constructor(parsed, env) { 18 | super(parsed, env); 19 | this.container = makeExpression.make(this.parsed.parent, this.env); 20 | this.by = makeExpression.make(this.parsed.by, this.env); 21 | } 22 | 23 | typecheck() { 24 | this.container.typecheck(); 25 | this.by.typecheck(); 26 | if (!Types.implementsIndexable(this.container.type)) { 27 | throw new errors.Type(`Can't index into ${this.container.type}`); 28 | } 29 | this.type = this.container.type.valuetype; 30 | // TODO: more checks 31 | } 32 | 33 | evaluate(context) { 34 | return this.container.evaluate(context).index(this.by.evaluate(context)); 35 | } 36 | 37 | toString(indent) { 38 | return `${this.container.toString(indent)}[${this.by.toString(indent)}]`; 39 | } 40 | } 41 | 42 | module.exports = Index; 43 | -------------------------------------------------------------------------------- /test/statements/assign.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/assign.js', function() { 15 | describe('assign', function() { 16 | 17 | it('LHS is simple', function() { 18 | let module = testing.run(` 19 | var x : 0..2; 20 | x = 1; 21 | `); 22 | assert.equal(module.env.getVar('x').toString(), '1'); 23 | }); 24 | 25 | it('RHS is expression', function() { 26 | let module = testing.run(` 27 | var x : Boolean = False; 28 | x = False == False; 29 | `); 30 | assert.equal(module.env.getVar('x').toString(), 'True'); 31 | }); 32 | 33 | it('LHS has lookups', function() { 34 | let module = testing.run(` 35 | type T : record { 36 | first: 0..2, 37 | second: 0..2, 38 | } 39 | var x : T; 40 | x.first = 1; 41 | x.second = 2; 42 | `); 43 | assert.equal(module.env.getVar('x').toString(), 'T { first: 1, second: 2 }'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let parser = require('./parser.js'); 12 | let Environment = require('./environment.js').Environment; 13 | let Input = require('./input.js'); 14 | let makeStatement = require('./statements/factory.js').make; 15 | let NumberType = require('./types/number.js').Type; 16 | let BlackHoleNumberType = require('./types/blackholenumber.js').Type; 17 | 18 | let load = function(input, env) { 19 | let parsed = parser.parse(input); 20 | let ast = makeStatement(parsed, env); 21 | ast.typecheck(); 22 | return { 23 | ast: ast, 24 | env: env, 25 | }; 26 | }; 27 | 28 | let loadPrelude = function(text, options) { 29 | if (options === undefined) { 30 | options = {}; 31 | } 32 | let env = new Environment(); 33 | let prelude = load(new Input('prelude.model', text), env); 34 | if (options.clock) { 35 | prelude.env.types.set('Time', NumberType.singleton); 36 | } else { 37 | prelude.env.types.set('Time', BlackHoleNumberType.singleton); 38 | } 39 | return prelude; 40 | }; 41 | 42 | 43 | module.exports = { 44 | load: load, 45 | loadPrelude: loadPrelude, 46 | }; 47 | -------------------------------------------------------------------------------- /test-scripts/parser/input-2.model: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | type ServerId: 1..5; 10 | 11 | type Token: either { 12 | AtServer { 13 | at: ServerId, 14 | }, 15 | InTransit { 16 | from: ServerId, 17 | to: ServerId, 18 | }, 19 | } 20 | var token: Token; 21 | 22 | type Server: record { 23 | hasToken: Boolean, 24 | } 25 | 26 | var servers: Array[ServerId]; 27 | servers[1].hasToken = True; 28 | 29 | rule deliverToken { 30 | match token { 31 | InTransit(transit) { 32 | servers[transit.to].hasToken = True; 33 | token = AtServer { at: transit.to }; 34 | } 35 | AtServer { /* do nothing */ } 36 | } 37 | } 38 | 39 | rule passToken for id, server in servers { 40 | if server.hasToken { 41 | var next : ServerId = id % 5 + 1; 42 | token = InTransit { 43 | from: id, 44 | to: next, 45 | }; 46 | server.hasToken = False; 47 | } 48 | } 49 | 50 | invariant tokenAgreement { 51 | for id, server in servers { 52 | match token { 53 | InTransit { 54 | assert !server.hasToken; 55 | } 56 | AtServer(t) { 57 | assert server.hasToken == (t.at == id); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/types/either.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('types/either.js', function() { 15 | describe('either', function() { 16 | it('basic', function() { 17 | let module = testing.run(` 18 | type Maybe : either { 19 | Nothing, 20 | Something { digit: 0..9 }, 21 | } 22 | var a : Maybe; 23 | var b : Boolean = (Something { digit: 3 } == Something { digit: 3 }); 24 | var c : Boolean = (Something { digit: 3 } == Something { digit: 4 }); 25 | var d : Boolean = (Something { digit: 3 } == Nothing); 26 | var e : Boolean = (Nothing == Something { digit: 0 }); 27 | `); 28 | assert.equal(module.env.getVar('a').toString(), 'Nothing'); 29 | assert.equal(module.env.getVar('b').toString(), 'True'); 30 | assert.equal(module.env.getVar('c').toString(), 'False'); 31 | assert.equal(module.env.getVar('d').toString(), 'False'); 32 | }); 33 | 34 | it('empty disallowed', function() { 35 | assert.throws(() => testing.run(`type E : either {}`)); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/statements/while.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 10 | 'use strict'; 11 | 12 | let assert = require('assert'); 13 | let testing = require('../../lib/testing.js'); 14 | 15 | describe('statements/while.js', function() { 16 | describe('while', function() { 17 | 18 | it('basic', function() { 19 | let module = testing.run(` 20 | var i : 0..99 = 30; 21 | var j : 0..99 = 0; 22 | while j < i { 23 | j = j + 1; 24 | } 25 | `); 26 | assert.equal(module.env.getVar('j').toString(), '30'); 27 | }); 28 | 29 | it('break', function() { 30 | let module = testing.run(` 31 | var i : 0..99 = 30; 32 | var j : 0..99 = 0; 33 | while True { 34 | if j == i { 35 | break; 36 | } 37 | j = j + 1; 38 | } 39 | `); 40 | assert.equal(module.env.getVar('j').toString(), '30'); 41 | }); 42 | 43 | it('continue', function() { 44 | let module = testing.run(` 45 | var i : 0..99 = 30; 46 | var j : 0..99 = 0; 47 | while True { 48 | if j >= i { 49 | break; 50 | } 51 | j = j + 1; 52 | } 53 | `); 54 | assert.equal(module.env.getVar('j').toString(), '30'); 55 | }); 56 | 57 | 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/expressions/recordvalue.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let errors = require('../../lib/errors.js'); 13 | let testing = require('../../lib/testing.js'); 14 | 15 | describe('expressions/recordvalue.js', function() { 16 | describe('record value', function() { 17 | 18 | it('either', function() { 19 | let module = testing.run(` 20 | type Elevator: record { 21 | location: either { 22 | AtFloor { 23 | at: 1..5, 24 | }, 25 | Between { 26 | next: 1..5, 27 | }, 28 | }, 29 | } 30 | var elevator : Elevator; 31 | elevator.location = AtFloor { at: 3 }; 32 | var floor : 1..5; 33 | match elevator.location { 34 | AtFloor as a => { floor = a.at; }, 35 | Between as b => { floor = b.next; }, 36 | } 37 | `); 38 | assert.equal(module.env.getVar('floor').toString(), '3'); 39 | }); 40 | 41 | it('outside range', function() { 42 | assert.throws(() => testing.run(` 43 | type T: record { 44 | f: 1..5, 45 | } 46 | var t : T; 47 | t = T { f: 6 }; 48 | `), errors.RangeError); 49 | }); 50 | 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/statements/while.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let makeExpression = require('../expressions/factory.js'); 13 | let makeStatement = require('./factory.js'); 14 | let Statement = require('./statement.js'); 15 | let Types = require('../types/types.js'); 16 | 17 | class While extends Statement { 18 | constructor(parsed, env) { 19 | super(parsed, env); 20 | this.expr = makeExpression.make(this.parsed.expr, this.env); 21 | this.code = makeStatement.make(this.parsed.code, this.env); 22 | } 23 | 24 | typecheck() { 25 | this.expr.typecheck(); 26 | if (!Types.subtypeOf(this.expr.type, this.env.getType('Boolean'))) { 27 | throw new errors.Type(`Expected boolean expression but got ` + 28 | `${this.expr.type.getName()} at ${this.expr.source}`); 29 | } 30 | this.code.typecheck(); 31 | } 32 | 33 | execute(context) { 34 | try { 35 | while (this.expr.evaluate(context).equals(this.env.getVar('True'))) { 36 | try { 37 | this.code.execute(context); 38 | } catch ( e ) { 39 | if (!(e instanceof errors.Continue)) { 40 | throw e; 41 | } 42 | } 43 | } 44 | } catch ( e ) { 45 | if (!(e instanceof errors.Break)) { 46 | throw e; 47 | } 48 | } 49 | } 50 | } 51 | 52 | module.exports = While; 53 | -------------------------------------------------------------------------------- /lib/expressions/recordvalue.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Expression = require('./expression.js'); 13 | let makeExpression = require('./factory.js'); 14 | 15 | class RecordValue extends Expression { 16 | constructor(parsed, env) { 17 | super(parsed, env); 18 | this.fields = new Map(this.parsed.fields.map((field) => [ 19 | field.id.value, 20 | makeExpression.make(field.expr, this.env), 21 | ])); 22 | } 23 | 24 | evaluate(context) { 25 | let value = this.type.makeDefaultValue(); 26 | this.fields.forEach((field, name) => { 27 | value.lookup(name).assign(field.evaluate(context)); 28 | }); 29 | return value; 30 | } 31 | 32 | typecheck() { 33 | this.type = this.env.getType(this.parsed.type.value); 34 | if (this.type === undefined) { 35 | throw new errors.Lookup(`No type ${this.parsed.type.value} found in environment: ${this.parsed.type.source}`); 36 | } 37 | 38 | this.fields.forEach((field) => field.typecheck()); 39 | 40 | this.fields.forEach((field, name) => { 41 | if (this.type.fieldType(name) === undefined) { 42 | throw new errors.Type(`No field ${name} found in ${this.type} record`); 43 | } 44 | }); 45 | 46 | // XXX- throw TypeError if field is missing? or just default it? 47 | } 48 | 49 | } 50 | 51 | module.exports = RecordValue; 52 | -------------------------------------------------------------------------------- /lib/pubsub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let _ = require('lodash'); 12 | 13 | // Represents a source of events that multiple handlers can subscribe to. 14 | class PubSub { 15 | constructor() { 16 | this._handlers = []; 17 | } 18 | 19 | // Call the handlers in order with the arguments provided. 20 | // 21 | // If a handler removes one that has not yet been called, it won't be called 22 | // here or again. If a handler adds another, it won't be called now but will 23 | // be called in subsequent calls to pub(). 24 | pub(/* arguments */) { 25 | // The awkwardness here is to give decent semantics to sub()/unsub() calls 26 | // done in handlers (a simple call to map() doesn't do the trick). 27 | let results = []; 28 | _.clone(this._handlers).forEach(handler => { 29 | if (_.indexOf(this._handlers, handler) >= 0) { 30 | results.push(handler(...arguments)); 31 | } 32 | }); 33 | return results; 34 | } 35 | 36 | // Appends 'cb' to the list of handlers to be called on future, 37 | // not-yet-started invocations of pub(). 38 | sub(cb) { 39 | this._handlers.push(cb); 40 | } 41 | 42 | // Removes the first occurrence of 'cb' from the list of handlers. 43 | // It will no longer be called by pub(). 44 | unsub(cb) { 45 | _.pullAt(this._handlers, _.indexOf(this._handlers, cb)); 46 | } 47 | } 48 | 49 | module.exports = PubSub; 50 | -------------------------------------------------------------------------------- /lib/types/blackholenumber.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Type = require('./type.js'); 13 | let Value = require('./value.js'); 14 | 15 | class BlackHoleNumberValue extends Value { 16 | constructor(type) { 17 | super(type); 18 | this.value = 0; 19 | } 20 | 21 | assign(newValue) { 22 | if (typeof newValue !== 'number' && 23 | (newValue.value === undefined || 24 | typeof newValue.value !== 'number')) { 25 | throw new errors.Internal(`Trying to assign ${newValue.type} to Number;`); 26 | } 27 | } 28 | 29 | equals(other) { 30 | return this.type == other.type; 31 | } 32 | 33 | innerToString() { 34 | return 0; 35 | } 36 | 37 | assignJSON(spec) { 38 | } 39 | 40 | toJSON() { 41 | return 0; 42 | } 43 | 44 | toString() { 45 | return 0; 46 | } 47 | } 48 | 49 | class BlackHoleNumberType extends Type { 50 | constructor() { 51 | super(null, null, 'BlackHoleNumber'); 52 | } 53 | equals(other) { 54 | return other === BlackHoleNumberType.singleton; 55 | } 56 | makeDefaultValue() { 57 | return new BlackHoleNumberValue(this); 58 | } 59 | toString() { 60 | return 'BlackHoleNumber'; 61 | } 62 | } 63 | 64 | BlackHoleNumberType.singleton = new BlackHoleNumberType(); 65 | 66 | module.exports = { 67 | Type: BlackHoleNumberType, 68 | Value: BlackHoleNumberValue, 69 | }; 70 | -------------------------------------------------------------------------------- /lib/types/output.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Output = {}; 12 | module.exports = Output; 13 | 14 | let makeType = require('./factory.js'); 15 | let Type = require('./type.js'); 16 | let Value = require('./value.js'); 17 | 18 | class OutputValue extends Value { 19 | constructor(type) { 20 | super(type); 21 | this.isConstant = true; 22 | } 23 | toString() { 24 | return `(Output)`; 25 | } 26 | 27 | toJSON() { 28 | return {}; 29 | } 30 | push(v, context) { 31 | let name = '?'; 32 | this.type.env.vars.forEach((evar, ename) => { 33 | if (evar == this) { 34 | name = ename; 35 | } 36 | }); 37 | if (context.output === undefined) { 38 | console.log('created output'); 39 | context.output = {}; 40 | } 41 | if (context.output[name] === undefined) { 42 | context.output[name] = []; 43 | } 44 | context.output[name].push(v.toJSON()); 45 | } 46 | assign(v) { 47 | // needed for reset statement 48 | } 49 | } 50 | 51 | class OutputType extends Type { 52 | constructor(decl, env, name) { 53 | super(decl, env, name); 54 | this.valuetype = makeType.make(this.decl.args[0], this.env); 55 | } 56 | makeDefaultValue() { 57 | return new OutputValue(this); 58 | } 59 | toString() { 60 | return `Output<${this.valuetype}>`; 61 | } 62 | } 63 | 64 | Output.Type = OutputType; 65 | Output.Value = OutputValue; 66 | -------------------------------------------------------------------------------- /test/types/array.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('types/array.js', function() { 15 | describe('Array', function() { 16 | it('basic', function() { 17 | let module = testing.run(` 18 | var bools : Array[1..3]; 19 | bools[3] = True; 20 | var a : Boolean = bools[1]; 21 | var b : Boolean = bools[3]; 22 | `); 23 | assert.equal(module.env.getVar('bools').toString(), 24 | '[1: False, 2: False, 3: True]'); 25 | assert.equal(module.env.getVar('a').toString(), 26 | 'False'); 27 | assert.equal(module.env.getVar('b').toString(), 28 | 'True'); 29 | }); 30 | 31 | it('bounds', function() { 32 | assert.throws(() => testing.run(` 33 | var bools : Array[1..3]; 34 | bools[4] = True; 35 | `)); 36 | }); 37 | 38 | it('compound', function() { 39 | let module = testing.run(` 40 | type DigitBox : record { digit: 0..9 }; 41 | var digits : Array[1..3]; 42 | digits[2] = DigitBox { digit: 8 }; 43 | digits[3].digit = 4; 44 | `); 45 | assert.equal(module.env.getVar('digits').toString(), 46 | '[1: DigitBox { digit: 0 }, ' + 47 | '2: DigitBox { digit: 8 }, ' + 48 | '3: DigitBox { digit: 4 }]'); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/types/number.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Type = require('./type.js'); 13 | let Value = require('./value.js'); 14 | 15 | class NumberValue extends Value { 16 | constructor(type) { 17 | super(type); 18 | this.value = 0; 19 | } 20 | 21 | assign(newValue) { 22 | if (typeof newValue == 'number') { 23 | this.value = newValue; 24 | } else if (newValue.value !== undefined && typeof newValue.value == 'number') { 25 | this.value = newValue.value; 26 | } else { 27 | throw new errors.Internal(`Trying to assign ${newValue.type} to Number;`); 28 | } 29 | } 30 | 31 | equals(other) { 32 | return this.type == other.type && this.value == other.value; 33 | } 34 | 35 | innerToString() { 36 | return `${this.value}`; 37 | } 38 | 39 | assignJSON(spec) { 40 | this.value = spec; 41 | } 42 | 43 | toJSON() { 44 | return this.value; 45 | } 46 | 47 | toString() { 48 | return `${this.value}`; 49 | } 50 | } 51 | 52 | class NumberType extends Type { 53 | constructor() { 54 | super(null, null, 'Number'); 55 | } 56 | equals(other) { 57 | return other === NumberType.singleton; 58 | } 59 | makeDefaultValue() { 60 | return new NumberValue(this); 61 | } 62 | toString() { 63 | return 'Number'; 64 | } 65 | } 66 | 67 | NumberType.singleton = new NumberType(); 68 | 69 | module.exports = { 70 | Type: NumberType, 71 | Value: NumberValue, 72 | }; 73 | -------------------------------------------------------------------------------- /test/environment.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let Environment = require('../lib/environment.js').Environment; 13 | 14 | describe('environment.js', function() { 15 | describe('Environment', function() { 16 | it('assignType, getType', function() { 17 | let outer = new Environment(); 18 | let inner = new Environment(outer); 19 | outer.assignType('foo', 'footype'); 20 | inner.assignType('bar', 'bartype'); 21 | inner.assignVar('bar', 'barvar'); 22 | assert.throws(() => { 23 | inner.assignType('foo', 'fail'); 24 | }, Error); 25 | assert.deepEqual(inner.getType('foo'), 'footype'); 26 | assert.deepEqual(inner.getType('bar'), 'bartype'); 27 | assert.deepEqual(outer.getTypeNames(), ['foo']); 28 | assert.deepEqual(inner.getTypeNames(), ['foo', 'bar']); 29 | }); 30 | 31 | it('assignVar, getVar', function() { 32 | let outer = new Environment(); 33 | let inner = new Environment(outer); 34 | outer.assignVar('foo', 'foovar'); 35 | inner.assignVar('bar', 'barvar'); 36 | inner.assignType('bar', 'bartype'); 37 | assert.throws(() => { 38 | inner.assignVar('foo', 'fail'); 39 | }, Error); 40 | assert.deepEqual(inner.getVar('foo'), 'foovar'); 41 | assert.deepEqual(inner.getVar('bar'), 'barvar'); 42 | assert.deepEqual(outer.getVarNames(), ['foo']); 43 | assert.deepEqual(inner.getVarNames(), ['foo', 'bar']); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/statements/rule.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/rule.js', function() { 15 | let context = {}; 16 | describe('rule', function() { 17 | it('basic', function() { 18 | let module = testing.run(` 19 | var bool : Boolean; 20 | rule setToTrue { 21 | bool = True; 22 | } 23 | `); 24 | assert.equal(module.env.getVar('bool').toString(), 'False'); 25 | module.env.getRule('setToTrue').fire(context); 26 | assert.equal(module.env.getVar('bool').toString(), 'True'); 27 | }); 28 | 29 | it('variables reset to start', function() { 30 | let module = testing.run(` 31 | rule setToTrue { 32 | var bool : Boolean; 33 | assert !bool; 34 | bool = True; 35 | } 36 | `); 37 | module.env.getRule('setToTrue').fire(context); 38 | module.env.getRule('setToTrue').fire(context); 39 | }); 40 | 41 | it('variables reset to start in nested environments', function() { 42 | let module = testing.run(` 43 | rule setToTrue { 44 | var done : Boolean = False; 45 | while !done { 46 | var bool : Boolean; 47 | assert !bool; 48 | bool = True; 49 | done = True; 50 | } 51 | } 52 | `); 53 | module.env.getRule('setToTrue').fire(context); 54 | module.env.getRule('setToTrue').fire(context); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/statements/factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | 13 | let factory = {}; 14 | module.exports = factory; 15 | // export empty object before requiring circular dependencies 16 | 17 | let statements = new Map([ 18 | ['assign', require('./assign.js')], 19 | ['assert', require('./assert.js')], 20 | ['block', require('./block.js')], 21 | ['break', require('./break.js')], 22 | ['continue', require('./continue.js')], 23 | ['function', require('./function.js')], 24 | ['do', require('./do.js')], 25 | ['foreach', require('./foreach.js')], 26 | ['ifelse', require('./ifelse.js')], 27 | ['invariant', require('./invariant.js')], 28 | ['match', require('./match.js')], 29 | ['paramdecl', require('./paramdecl.js')], 30 | ['print', require('./print.js')], 31 | ['reset', require('./reset.js')], 32 | ['returnstmt', require('./return.js')], 33 | ['rule', require('./rule.js')], 34 | ['rulefor', require('./rulefor.js')], 35 | ['sequence', require('./sequence.js')], 36 | ['typedecl', require('./typedecl.js')], 37 | ['vardecl', require('./vardecl.js')], 38 | ['while', require('./while.js')], 39 | ]); 40 | 41 | let make = function(parsed, env) { 42 | if (parsed !== undefined && 'kind' in parsed) { 43 | let statement = statements.get(parsed.kind); 44 | if (statement !== undefined) { 45 | return new statement(parsed, env); 46 | } 47 | } 48 | let o = JSON.stringify(parsed, null, 2); 49 | throw new errors.Unimplemented(`Unknown statement: ${o}`); 50 | }; 51 | factory.make = make; 52 | -------------------------------------------------------------------------------- /lib/statements/ifelse.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let makeExpression = require('../expressions/factory.js'); 13 | let makeStatement = require('./factory.js'); 14 | let Statement = require('./statement.js'); 15 | let Types = require('../types/types.js'); 16 | 17 | class IfElse extends Statement { 18 | constructor(parsed, env) { 19 | super(parsed, env); 20 | this.condition = makeExpression.make(this.parsed.condition, this.env); 21 | this.trueStatements = makeStatement.make(this.parsed.thenblock, this.env); 22 | this.falseStatements = makeStatement.make(this.parsed.elseblock, this.env); 23 | } 24 | 25 | typecheck() { 26 | this.condition.typecheck(); 27 | if (!Types.subtypeOf(this.condition.type, this.env.getType('Boolean'))) { 28 | throw new errors.Type(`Condition of if statement must be a Boolean, ` + 29 | `found ${this.condition.type} at ${this.condition.source}`); 30 | } 31 | this.trueStatements.typecheck(); 32 | this.falseStatements.typecheck(); 33 | } 34 | 35 | execute(context) { 36 | if (this.condition.evaluate(context).equals(this.env.getVar('True'))) { 37 | this.trueStatements.execute(context); 38 | } else { 39 | this.falseStatements.execute(context); 40 | } 41 | } 42 | 43 | toString(indent) { 44 | let next = indent + ' '; 45 | return `${indent}if ${this.condition.toString(indent)} { 46 | ${this.trueStatements.toString(next)} 47 | ${indent}} else { 48 | ${this.falseStatements.toString(next)} 49 | ${indent}}`; 50 | } 51 | } 52 | 53 | module.exports = IfElse; 54 | -------------------------------------------------------------------------------- /lib/expressions/id.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Expression = require('./expression.js'); 13 | let Environment = require('../environment.js'); 14 | 15 | class Identifier extends Expression { 16 | typecheck() { 17 | let v = this.env.getVar(this.parsed.value); 18 | if (v === undefined) { 19 | throw new errors.Lookup(`'${this.parsed.value}' is not a ` + 20 | `variable/constant in scope, ` + 21 | `attempted access at ${this.parsed.source}`); 22 | } 23 | this.type = v.type; 24 | } 25 | 26 | evaluate(context) { 27 | if (context === undefined) { 28 | // This is the best way to know we're threading the context through every 29 | // statement and expression. 30 | throw new errors.Internal('Evaluation context not provided'); 31 | } 32 | let r = this.env.vars.get(this.parsed.value); 33 | if (r === undefined) { 34 | throw new errors.Internal(`'${this.parsed.value}' is not a ` + 35 | `variable/constant in scope`); 36 | } 37 | 38 | if (context.readset !== undefined && !r.isConstant) { 39 | let genv = this.env; 40 | do { 41 | if (genv instanceof Environment.GlobalEnvironment && 42 | genv.vars.get(this.parsed.value) === r) { 43 | context.readset.add(this.parsed.value); 44 | break; 45 | } 46 | genv = genv.enclosing; 47 | } while (genv !== null); 48 | } 49 | 50 | return r; 51 | } 52 | 53 | toString(indent) { 54 | return `${this.parsed.value}`; 55 | } 56 | } 57 | 58 | module.exports = Identifier; 59 | -------------------------------------------------------------------------------- /lib/expressions/lookup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Expression = require('./expression.js'); 13 | let RecordType = require('../types/record.js'); 14 | let Either = require('../types/either.js'); 15 | let makeExpression = require('./factory.js'); 16 | 17 | class Lookup extends Expression { 18 | constructor(parsed, env) { 19 | super(parsed, env); 20 | this.parent = makeExpression.make(this.parsed.parent, this.env); 21 | } 22 | 23 | typecheck() { 24 | this.parent.typecheck(); 25 | if (!(this.parent.type instanceof RecordType) && 26 | !(this.parent.type instanceof Either.Variant)) { 27 | let hint = ''; 28 | if (this.parent.type instanceof Either.Type) { 29 | hint = ': use a match statement?'; 30 | } 31 | throw new errors.Type(`Cannot lookup field in ${this.parent} which is a ` + 32 | `${this.parent.type}${hint} ` + 33 | `(defined at ${this.parent.parsed.source})`); 34 | } 35 | this.type = this.parent.type.fieldType(this.parsed.child.value); 36 | if (this.type === undefined) { 37 | throw new errors.Type(`${this.parent.type} has no field ${this.parsed.child.value}`); 38 | } 39 | } 40 | 41 | evaluate(context) { 42 | let parval = this.parent.evaluate(context); 43 | if (parval === undefined) { 44 | throw new errors.Internal(`Expr ${this.parent} evaluated to undefined`); 45 | } 46 | return parval.lookup(this.parsed.child.value); 47 | } 48 | 49 | toString(indent) { 50 | return `${this.parent.toString(indent)}.${this.parsed.child.value}`; 51 | } 52 | } 53 | 54 | module.exports = Lookup; 55 | -------------------------------------------------------------------------------- /lib/statements/vardecl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let makeExpression = require('../expressions/factory.js'); 13 | let makeType = require('../types/factory.js'); 14 | let Statement = require('./statement.js'); 15 | let Types = require('../types/types.js'); 16 | 17 | // var x : Boolean; 18 | // var x : Boolean = False; 19 | // var x : Boolean = (False == False); 20 | 21 | class VarDecl extends Statement { 22 | constructor(parsed, env) { 23 | super(parsed, env); 24 | let type = makeType.make(this.parsed.type, this.env); 25 | this.value = type.makeDefaultValue(); 26 | this.env.vars.set(this.parsed.id.value, this.value, this.parsed.source); 27 | if (this.parsed.default === undefined) { 28 | this.defaultExpr = null; 29 | } else { 30 | this.defaultExpr = makeExpression.make(this.parsed.default, this.env); 31 | } 32 | } 33 | 34 | typecheck() { 35 | if (this.defaultExpr !== null) { 36 | this.defaultExpr.typecheck(); 37 | if (!Types.subtypeOf(this.defaultExpr.type, this.value.type)) { 38 | throw new errors.Type(`Cannot assign ${this.defaultExpr.type} to variable of type ${this.value.type}`); 39 | } 40 | } 41 | } 42 | 43 | execute(context) { 44 | if (this.defaultExpr !== null) { 45 | this.value.assign(this.defaultExpr.evaluate(context)); 46 | } 47 | } 48 | 49 | toString(indent) { 50 | if (indent === undefined) { 51 | indent = ''; 52 | } 53 | let defVal = ''; 54 | if (this.defaultExpr !== null) { 55 | defVal = ` = ${this.defaultExpr}`; 56 | } 57 | return `${indent}var ${this.parsed.id.value} : ${this.value.type}${defVal};`; 58 | } 59 | } 60 | 61 | module.exports = VarDecl; 62 | -------------------------------------------------------------------------------- /lib/input.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // Holds the textual input to the compiler that has been read from a particular file. 12 | class Input { 13 | // filename: where the text comes from. For interactive input, make up something. 14 | // text is optional: if not given, will read from the filename on disk. 15 | constructor(filename, text) { 16 | this.filename = filename; 17 | console.assert(text !== undefined, 'Input needs text supplied'); 18 | this.getText = () => text; // don't store directly so as to avoid printing on every json dump 19 | } 20 | 21 | // Given a character offset into the entire file, determine the line number, 22 | // column, etc. 23 | lookup(charOffset) { 24 | let lineno = 1; // line number 25 | let lineStartOffset = 0; 26 | let lineEndOffset = -1; 27 | for (;;) { 28 | lineEndOffset = this.getText().indexOf('\n', lineStartOffset); 29 | if (lineEndOffset >= charOffset) { 30 | break; 31 | } 32 | if (lineEndOffset == -1) { 33 | lineEndOffset = this.getText().length; 34 | break; 35 | } 36 | lineStartOffset = lineEndOffset + 1; 37 | lineno += 1; 38 | } 39 | return { 40 | line: lineno, 41 | col: charOffset - lineStartOffset + 1, 42 | lineStartOffset: lineStartOffset, 43 | lineEndOffset: lineEndOffset, 44 | charOffset: charOffset, 45 | }; 46 | } 47 | 48 | // Given a lookup result, print out the containing line and an arrow pointing 49 | // to the character in question. 50 | highlight(lookup) { 51 | let line = this.getText().slice(lookup.lineStartOffset, lookup.lineEndOffset); 52 | let indent = ' '.repeat(lookup.col - 1); 53 | return `${line} 54 | ${indent}^`; 55 | } 56 | 57 | } 58 | 59 | module.exports = Input; 60 | -------------------------------------------------------------------------------- /test/execution.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let Execution = require('../lib/execution.js'); 13 | 14 | describe('execution.js', function() { 15 | describe('Execution', function() { 16 | let execution = new Execution({msg: 'A1', i: 1}); 17 | let A1 = execution.last(); 18 | let A2 = A1.addEvent({msg: 'A2', i: 2}); 19 | let A3 = A2.addEvent({msg: 'A3', i: 3}); 20 | let B3 = A2.addEvent({msg: 'B3', i: 4}); 21 | 22 | it('addEvent, map', function() { 23 | assert.equal(A1.map(e => e.msg).join(' '), 24 | 'A1'); 25 | assert.equal(A2.map(e => e.msg).join(' '), 26 | 'A1 A2'); 27 | assert.equal(A3.map(e => e.msg).join(' '), 28 | 'A1 A2 A3'); 29 | assert.equal(B3.map(e => e.msg).join(' '), 30 | 'A1 A2 B3'); 31 | assert.equal(execution.map(e => e.msg).join(' '), 32 | 'A1 A2 A3'); 33 | assert.equal(B3.execution.map(e => e.msg).join(' '), 34 | 'A1 A2 B3'); 35 | 36 | }); 37 | 38 | it('preceding', function() { 39 | let find = (e, i) => e.preceding(e => (e.i <= i)); 40 | assert.equal(find(execution, 1).getEvent().msg, 'A1'); 41 | assert.equal(find(execution, 2).getEvent().msg, 'A2'); 42 | assert.equal(find(execution, 3).getEvent().msg, 'A3'); 43 | assert.equal(find(execution, 4).getEvent().msg, 'A3'); 44 | assert.equal(find(execution, 0), undefined); 45 | assert.equal(find(B3.execution, 1).getEvent().msg, 'A1'); 46 | assert.equal(find(B3.execution, 2).getEvent().msg, 'A2'); 47 | assert.equal(find(B3.execution, 3).getEvent().msg, 'A2'); 48 | assert.equal(find(B3.execution, 4).getEvent().msg, 'B3'); 49 | assert.equal(find(B3.execution, 5).getEvent().msg, 'B3'); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test-scripts/parser/input-1.model: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | param ELEVATORS: 1..1024 = 6; 10 | param PEOPLE: 1..(ELEVATORS*100) = 100; 11 | param FLOORS: 2..1000 = 10; 12 | 13 | type ElevatorId: 1..ELEVATORS; 14 | type Floor: 1..FLOORS; 15 | 16 | // How long the doors stay open 17 | function openTime() -> Time { 18 | return 3s; 19 | } 20 | 21 | // How long the doors take to open or close 22 | function doorTime() -> Time { 23 | return 1s; 24 | } 25 | 26 | function makeDestination(start: Floor) -> Floor { 27 | return start; 28 | } 29 | 30 | type Person: node { 31 | state: either { 32 | Sleeping { 33 | floor: Floor, 34 | }, 35 | Waiting { 36 | floor: Floor, 37 | destination: Floor, 38 | }, 39 | Riding { 40 | elevator: ElevatorId, 41 | destination: Floor, 42 | }, 43 | }, 44 | } 45 | var people: Array[PersonId]; 46 | 47 | type FloorControl: node { 48 | downActive: Boolean, 49 | upActive: Boolean 50 | } 51 | var floorControls: Array[Floor]; 52 | 53 | type Elevator: node { 54 | destinations: Set, 55 | riders: Set, 56 | direction: either { 57 | Neutral, 58 | Up, 59 | Down, 60 | }, 61 | location: either { 62 | AtFloor { 63 | at: Floor, 64 | doors: either { 65 | Closed, 66 | Opening { 67 | openAt: Time, 68 | }, 69 | Open { 70 | closeAt: Time, 71 | }, 72 | Closing { 73 | closedAt: Time, 74 | }, 75 | }, 76 | }, 77 | Between { 78 | next: Floor, 79 | // doors are closed for safety purposes 80 | }, 81 | }, 82 | } 83 | var elevators: Array[ElevatorId]; 84 | -------------------------------------------------------------------------------- /lib/types/factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let factory = {}; 12 | module.exports = factory; 13 | // export empty object before requiring circular dependencies 14 | 15 | let errors = require('../errors.js'); 16 | let ArrayType = require('./array.js'); 17 | let Either = require('./either.js'); 18 | let Output = require('./output.js'); 19 | let Range = require('./range.js'); 20 | let RecordType = require('./record.js'); 21 | let VarLen = require('./varlen.js'); 22 | 23 | let make = function(decl, env, name) { 24 | if (decl.kind == 'range') { 25 | return new Range.Type(decl, env, name); 26 | } else if (decl.kind == 'record') { 27 | return new RecordType(decl, env, name); 28 | } else if (decl.kind == 'either') { 29 | return new Either.Type(decl, env, name); 30 | } else if (decl.kind == 'alias') { 31 | let t = env.getType(decl.value); 32 | if (t === undefined) { 33 | throw new errors.Lookup(`Unknown type ${decl.value}`); 34 | } 35 | return t; 36 | } else if (decl.kind == 'generic') { 37 | if (decl.base.value == 'Array') { 38 | return new ArrayType.Type(decl, env, name); 39 | } else if (decl.base.value == 'MultiSet') { 40 | return new VarLen.MultiSetType(decl, env, name); 41 | } else if (decl.base.value == 'Output') { 42 | return new Output.Type(decl, env, name); 43 | } else if (decl.base.value == 'OrderedSet') { 44 | return new VarLen.OrderedSetType(decl, env, name); 45 | } else if (decl.base.value == 'Vector') { 46 | return new VarLen.VectorType(decl, env, name); 47 | } else if (decl.base.value == 'Set') { 48 | return new VarLen.SetType(decl, env, name); 49 | } else { 50 | throw new errors.Unimplemented(`Unknown type '${decl.base.value}'`); 51 | } 52 | } 53 | let o = JSON.stringify(decl, null, 2); 54 | throw new errors.Unimplemented(`Unknown type '${name}': ${o}`); 55 | }; 56 | 57 | factory.make = make; 58 | -------------------------------------------------------------------------------- /test/statements/foreach.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/foreach.js', function() { 15 | describe('foreach', function() { 16 | 17 | it('basic', function() { 18 | let module = testing.run(` 19 | var bools : Array[1..3]; 20 | bools[2] = True; 21 | for bool in bools { 22 | bool = (bool == False); 23 | } 24 | `); 25 | assert.equal(module.env.getVar('bools').toString(), 26 | '[1: True, 2: False, 3: True]'); 27 | }); 28 | 29 | it('with index', function() { 30 | let module = testing.run(` 31 | type Digit : 0..9; 32 | var ints : Array[4..6]; 33 | for i, v in ints { 34 | v = i; 35 | } 36 | `); 37 | assert.equal(module.env.getVar('ints').toString(), 38 | '[4: 4, 5: 5, 6: 6]'); 39 | }); 40 | 41 | it('break', function() { 42 | let module = testing.run(` 43 | type Digit : 0..9; 44 | var ints : Array[0..2]; 45 | ints[0] = 4; 46 | ints[1] = 5; 47 | ints[2] = 5; 48 | var fives : 0..100 = 0; 49 | for v in ints { 50 | if v == 5 { 51 | fives = fives + 1; 52 | break; 53 | } 54 | } 55 | `); 56 | assert.equal(module.env.getVar('fives').toString(), '1'); 57 | }); 58 | 59 | it('continue', function() { 60 | let module = testing.run(` 61 | type Digit : 0..9; 62 | var ints : Array[0..2]; 63 | ints[0] = 4; 64 | ints[1] = 5; 65 | ints[2] = 5; 66 | var fives : 0..100 = 0; 67 | for v in ints { 68 | if v != 5 { 69 | continue; 70 | } 71 | fives = fives + 1; 72 | } 73 | `); 74 | assert.equal(module.env.getVar('fives').toString(), '2'); 75 | }); 76 | 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/statements/rulefor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/rulefor.js', function() { 15 | let context = {}; 16 | describe('rulefor', function() { 17 | it('basic', function() { 18 | let module = testing.run(` 19 | var bools : Array[1..3]; 20 | bools[2] = True; 21 | rule invert for bool in bools { 22 | bool = (bool == False); 23 | } 24 | `); 25 | assert.equal(module.env.getVar('bools').toString(), 26 | '[1: False, 2: True, 3: False]'); 27 | module.env.getRule('invert').fire(3, context); 28 | assert.equal(module.env.getVar('bools').toString(), 29 | '[1: False, 2: True, 3: True]'); 30 | }); 31 | 32 | it('with index', function() { 33 | let module = testing.run(` 34 | type Digit : 0..9; 35 | var ints : Array[4..6]; 36 | rule setToIndex for i, v in ints { 37 | v = i; 38 | } 39 | `); 40 | assert.equal(module.env.getVar('ints').toString(), 41 | '[4: 0, 5: 0, 6: 0]'); 42 | module.env.getRule('setToIndex').fire(5, context); 43 | assert.equal(module.env.getVar('ints').toString(), 44 | '[4: 0, 5: 5, 6: 0]'); 45 | }); 46 | 47 | it('nested', function() { 48 | let module = testing.run(` 49 | var bools : Array[1..3]; 50 | bools[2] = True; 51 | rule xor for bool1 in bools 52 | for bool2 in bools { 53 | bool1 = (bool1 != bool2); 54 | } 55 | `); 56 | assert.equal(module.env.getVar('bools').toString(), 57 | '[1: False, 2: True, 3: False]'); 58 | module.env.getRule('xor').fire([3, 2], context); 59 | module.env.getRule('xor').fire([2, 3], context); 60 | assert.equal(module.env.getVar('bools').toString(), 61 | '[1: False, 2: False, 3: True]'); 62 | }); 63 | 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/pubsub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let PubSub = require('../lib/pubsub.js'); 13 | 14 | describe('pubsub.js', function() { 15 | describe('PubSub', function() { 16 | it('basic', function() { 17 | let ps = new PubSub(); 18 | let count = 0; 19 | ps.sub(x => {count += x;}); 20 | ps.pub(3); 21 | assert.equal(count, 3); 22 | let plus2 = x => {count += 2;}; 23 | ps.sub(plus2); 24 | ps.pub(5); 25 | assert.equal(count, 10); 26 | ps.unsub(plus2); 27 | ps.pub(9); 28 | assert.equal(count, 19); 29 | }); 30 | 31 | it('pub/sub from handlers', function() { 32 | let ps = new PubSub(); 33 | let count = 0; 34 | let plusX = x => {count += x;}; // do then don't 35 | let plus2 = x => {count += 2;}; // don't then don't 36 | let times3 = x => {count *= 3;}; // do then do 37 | let minus1 = x => {count -= 1;}; // don't then do 38 | let manip = () => { 39 | ps.unsub(plusX); 40 | ps.unsub(plus2); 41 | ps.sub(minus1); 42 | }; 43 | ps.sub(plusX); 44 | ps.sub(manip); 45 | ps.sub(plus2); 46 | ps.sub(times3); 47 | ps.pub(5); 48 | ps.unsub(manip); 49 | assert.equal(count, 15); 50 | ps.pub(9); 51 | assert.equal(count, 44); 52 | }); 53 | 54 | it('duplicates', function() { 55 | let ps = new PubSub(); 56 | let count = 0; 57 | let plusX = x => {count += x;}; // do then don't 58 | ps.sub(plusX); 59 | ps.sub(plusX); 60 | ps.pub(5); 61 | assert.equal(count, 10); 62 | ps.unsub(plusX); 63 | ps.pub(5); 64 | assert.equal(count, 15); 65 | }); 66 | 67 | it('unsub invalid', function() { 68 | let ps = new PubSub(); 69 | let count = 0; 70 | let plusX = x => {count += x;}; // do then don't 71 | ps.sub(plusX); 72 | ps.unsub(() => 'q'); 73 | ps.pub(5); 74 | assert.equal(count, 5); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/statements/match.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/match.js', function() { 15 | describe('match', function() { 16 | it('enum', function() { 17 | let module = testing.run(` 18 | var a : 0..2; 19 | var t : Boolean = True; 20 | match t { 21 | False => { a = 1; }, 22 | True => { a = 2; }, 23 | } 24 | `); 25 | assert.equal(module.env.getVar('a').toString(), '2'); 26 | }); 27 | 28 | it('with fields', function() { 29 | let module = testing.run(` 30 | type T : either { 31 | Nothing, 32 | Something { digit: 0..9 }, 33 | }; 34 | var t : T = Something { digit: 3 }; 35 | var a : 0..10; 36 | match t { 37 | Nothing => { a = 10; }, 38 | Something as s => { a = s.digit; }, 39 | } 40 | `); 41 | assert.equal(module.env.getVar('a').toString(), '3'); 42 | }); 43 | 44 | it('assign same variant is ok', function() { 45 | let module = testing.run(` 46 | type T : either { 47 | Nothing, 48 | Something { digit: 0..9 }, 49 | }; 50 | var t : T = Something { digit: 3 }; 51 | var a : 0..10; 52 | match t { 53 | Nothing => { a = 10; }, 54 | Something as s => { 55 | s = Something { digit: 9 }; 56 | a = s.digit; 57 | }, 58 | } 59 | `); 60 | assert.equal(module.env.getVar('a').toString(), '9'); 61 | }); 62 | 63 | it('assign different variant is not ok', function() { 64 | assert.throws(() => testing.run(` 65 | type T : either { 66 | Nothing, 67 | Something { digit: 0..9 }, 68 | }; 69 | var t : T = Something { digit: 3 }; 70 | match t { 71 | Nothing => {}, 72 | Something as s => { 73 | s = Nothing; 74 | }, 75 | } 76 | `)); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/input.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let Input = require('../lib/input.js'); 13 | 14 | let alphabet = `abc 15 | def 16 | ghi 17 | jkl 18 | `; 19 | 20 | describe('input.js', function() { 21 | describe('Input', function() { 22 | it('lookup', function() { 23 | let input = new Input('foo.txt', alphabet); 24 | assert.deepEqual(input.lookup(0), { 25 | line: 1, 26 | col: 1, 27 | lineStartOffset: 0, 28 | lineEndOffset: 3, 29 | charOffset: 0, 30 | }); 31 | 32 | 33 | assert.deepEqual(input.lookup(2), { 34 | line: 1, 35 | col: 3, 36 | lineStartOffset: 0, 37 | lineEndOffset: 3, 38 | charOffset: 2, 39 | }); 40 | 41 | assert.deepEqual(input.lookup(3), { 42 | line: 1, 43 | col: 4, 44 | lineStartOffset: 0, 45 | lineEndOffset: 3, 46 | charOffset: 3, 47 | }); 48 | 49 | assert.deepEqual(input.lookup(4), { 50 | line: 2, 51 | col: 1, 52 | lineStartOffset: 4, 53 | lineEndOffset: 7, 54 | charOffset: 4, 55 | }); 56 | 57 | assert.deepEqual(input.lookup(7), { 58 | line: 2, 59 | col: 4, 60 | lineStartOffset: 4, 61 | lineEndOffset: 7, 62 | charOffset: 7, 63 | }); 64 | }); 65 | 66 | it('highlight', function() { 67 | let input = new Input('foo.txt', alphabet); 68 | let h = (o) => input.highlight(input.lookup(o)); 69 | assert.equal(h(0), 'abc\n^'); 70 | assert.equal(h(2), 'abc\n ^'); 71 | assert.equal(h(3), 'abc\n ^'); 72 | assert.equal(h(4), 'def\n^'); 73 | assert.equal(h(7), 'def\n ^'); 74 | }); 75 | 76 | it('nonewline', function() { 77 | let input = new Input('test', 'abc'); 78 | assert.deepEqual(input.lookup(3), { 79 | line: 1, 80 | col: 4, 81 | lineStartOffset: 0, 82 | lineEndOffset: 3, 83 | charOffset: 3, 84 | }); 85 | assert.equal(input.highlight(input.lookup(3)), 'abc\n ^'); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /lib/simulator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let RuleFor = require('./statements/rulefor.js'); 12 | let Changesets = require('./changesets.js'); 13 | let _ = require('lodash'); 14 | 15 | let slow = (module, controller) => { 16 | 17 | let simpleRules = []; 18 | let context = []; 19 | module.env.rules.forEach((rule, name) => { 20 | if (rule.external) { 21 | return; 22 | } 23 | if (rule instanceof RuleFor) { 24 | rule.expr.evaluate(context).forEach((v, i) => { 25 | simpleRules.push({ 26 | name: `${name}(${i})`, 27 | fire: () => rule.fire(i, context), 28 | }); 29 | }); 30 | } else { 31 | simpleRules.push({ 32 | name: name, 33 | fire: () => rule.fire(context), 34 | }); 35 | } 36 | }); 37 | 38 | simpleRules = _.shuffle(simpleRules); 39 | for (let rule of simpleRules) { 40 | let changes = controller.tryChangeState(() => { 41 | rule.fire(); 42 | return rule.name; 43 | }); 44 | if (!Changesets.empty(changes)) { 45 | return true; 46 | } 47 | } 48 | console.log('deadlock'); 49 | return false; 50 | }; 51 | 52 | class Simulator { 53 | constructor(module, genContext) { 54 | this.module = module; 55 | this.genContext = genContext; 56 | } 57 | 58 | step() { 59 | //let start = performance.now(); 60 | let rulesets = _.reject(this.genContext.getRulesets(), 61 | rs => rs.source.external); 62 | for (;;) { 63 | let nextWake = Number.MAX_VALUE; 64 | let rules = _.flatMap(rulesets, ruleset => ruleset.rules); 65 | rules = _.shuffle(rules); 66 | for (let rule of rules) { 67 | if (!Changesets.empty(rule.fire())) { 68 | //let stop = performance.now(); 69 | //console.log(`simulate took ${_.round(stop - start, 3)} ms`); 70 | return true; 71 | } 72 | nextWake = Math.min(nextWake, rule.getNextWake()); 73 | } 74 | if (nextWake < Number.MAX_VALUE) { 75 | this.genContext.setClock(nextWake); 76 | } else { 77 | console.log('deadlock'); 78 | return false; 79 | } 80 | } 81 | } 82 | 83 | } 84 | 85 | module.exports = { 86 | slow: slow, 87 | Simulator: Simulator, 88 | }; 89 | -------------------------------------------------------------------------------- /lib/types/types.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let ArrayType = require('./array.js'); 12 | let Either = require('./either.js'); 13 | let BlackHoleNumberType = require('./blackholenumber.js').Type; 14 | let NumberType = require('./number.js').Type; 15 | let RangeType = require('./range.js').Type; 16 | let VarLen = require('./varlen.js'); 17 | 18 | 19 | let subtypeOf = function(sub, par) { 20 | if (sub.equals(par)) { 21 | return true; 22 | } 23 | if (isNumeric(sub) && isNumeric(par)) { 24 | // let runtime check handle ranges for now 25 | return true; 26 | } 27 | if (sub instanceof Either.Variant && 28 | par instanceof Either.Type && 29 | sub.parenttype == par) { 30 | return true; 31 | } 32 | return false; 33 | }; 34 | 35 | let haveEquality = function(left, right) { 36 | if (subtypeOf(left, right)) { 37 | return true; 38 | } 39 | if (subtypeOf(right, left)) { 40 | return true; 41 | } 42 | if (left instanceof Either.Variant && 43 | right instanceof Either.Variant && 44 | left.parenttype == right.parenttype) { 45 | return true; 46 | } 47 | return false; 48 | }; 49 | 50 | let isNumeric = function(t) { 51 | return (t instanceof NumberType || 52 | t instanceof RangeType || 53 | t instanceof BlackHoleNumberType); 54 | }; 55 | 56 | let haveOrdering = function(left, right) { 57 | return isNumeric(left) && isNumeric(right); 58 | }; 59 | 60 | let implementsSet = function(t) { 61 | // push pop remove contains empty full 62 | return (t instanceof VarLen.SetType || 63 | t instanceof VarLen.OrderedSetType || 64 | t instanceof VarLen.MultiSetType || 65 | t instanceof VarLen.VectorType); 66 | }; 67 | 68 | let implementsIterable = function(t) { 69 | // t needs .valueType, .indexType 70 | // output of evaluating value needs .forEach((v, i) => ...) 71 | return (t instanceof ArrayType.Type || 72 | implementsSet(t)); 73 | }; 74 | 75 | let implementsIndexable = function(t) { 76 | return (t instanceof ArrayType.Type || 77 | implementsSet(t)); 78 | }; 79 | 80 | module.exports = { 81 | subtypeOf: subtypeOf, 82 | haveEquality: haveEquality, 83 | haveOrdering: haveOrdering, 84 | isNumeric: isNumeric, 85 | implementsSet: implementsSet, 86 | implementsIterable: implementsIterable, 87 | implementsIndexable: implementsIndexable, 88 | }; 89 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let prefix = 'Modeling'; 12 | 13 | class Base extends Error { 14 | constructor(message) { 15 | super(message); 16 | this.name = prefix + 'Error'; 17 | } 18 | } 19 | 20 | class Internal extends Error { 21 | constructor(message) { 22 | super(message); 23 | this.name = prefix + 'InternalError'; 24 | } 25 | } 26 | 27 | class Unimplemented extends Internal { 28 | constructor(message) { 29 | super(message); 30 | this.name = prefix + 'UnimplementedError'; 31 | } 32 | } 33 | 34 | class Parse extends Base { 35 | constructor(message) { 36 | super(message); 37 | this.name = prefix + 'ParseError'; 38 | } 39 | } 40 | 41 | class Type extends Base { 42 | constructor(message) { 43 | super(message); 44 | this.name = prefix + 'TypeError'; 45 | } 46 | } 47 | 48 | class Lookup extends Type { 49 | constructor(message) { 50 | super(message); 51 | this.name = prefix + 'LookupError'; 52 | } 53 | } 54 | 55 | class Runtime extends Base { 56 | constructor(message) { 57 | super(message); 58 | this.name = prefix + 'RuntimeError'; 59 | } 60 | } 61 | 62 | class Bounds extends Runtime { 63 | constructor(message) { 64 | super(message); 65 | this.name = prefix + 'BoundsError'; 66 | } 67 | } 68 | 69 | class Break extends Runtime { 70 | constructor(message) { 71 | super(message); 72 | this.name = prefix + 'BreakError'; 73 | } 74 | } 75 | 76 | class Continue extends Runtime { 77 | constructor(message) { 78 | super(message); 79 | this.name = prefix + 'ContinueError'; 80 | } 81 | } 82 | 83 | class Return extends Runtime { 84 | constructor(message) { 85 | super(message); 86 | this.name = prefix + 'ReturnError'; 87 | } 88 | } 89 | 90 | class Assertion extends Runtime { 91 | constructor(message) { 92 | super(message); 93 | this.name = prefix + 'AssertionError'; 94 | } 95 | } 96 | 97 | module.exports = { 98 | Assertion: Assertion, 99 | Base: Base, 100 | Break: Break, 101 | Bounds: Bounds, 102 | Continue: Continue, 103 | Internal: Internal, 104 | Lookup: Lookup, 105 | Parse: Parse, 106 | Return: Return, 107 | Runtime: Runtime, 108 | Type: Type, 109 | Unimplemented: Unimplemented, 110 | }; 111 | -------------------------------------------------------------------------------- /test/changesets.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let Changesets = require('../lib/changesets.js'); 13 | 14 | 15 | describe('changesets.js', function() { 16 | describe('Changeset', function() { 17 | describe('compareJSON', function() { 18 | let cmp = (x, y) => Changesets.compareJSON(x, y).join(' '); 19 | 20 | it('globals', function() { 21 | assert.equal(cmp( 22 | {a: 1, b: 2}, 23 | {a: 1, b: 3, c: 4}), 24 | 'b c'); 25 | }); 26 | 27 | it('record', function() { 28 | assert.equal(cmp( 29 | {a: {x: 3, y: 4}}, 30 | {a: {x: 3}}), 31 | 'a'); 32 | assert.equal(cmp( 33 | {a: {x: 3}}, 34 | {a: {x: 3, y: 4}}), 35 | 'a'); 36 | assert.equal(cmp( 37 | {a: {x: 3, y: 5}}, 38 | {a: {x: 4, y: 5}}), 39 | 'a.x'); 40 | }); 41 | 42 | it('either', function() { 43 | assert.equal(cmp( 44 | {a: 'Red', b: 'Blue'}, 45 | {a: 'Green', b: 'Blue'}), 46 | 'a'); 47 | assert.equal(cmp( 48 | {a: 'Red', b: 'Blue'}, 49 | {a: {tag: 'Yellow', fields: {y: 3}}, b: 'Blue'}), 50 | 'a'); 51 | assert.equal(cmp( 52 | {a: {tag: 'Yellow', fields: {y: 2}}, b: 'Blue'}, 53 | {a: {tag: 'Yellow', fields: {y: 3}}, b: 'Blue'}), 54 | 'a!Yellow.y'); 55 | }); 56 | 57 | it('array', function() { 58 | assert.equal(cmp( 59 | {a: [[1, 'one'], [2, 'two']]}, 60 | {a: [[1, 'one'], [2, 'two']]}), 61 | ''); 62 | assert.equal(cmp( 63 | {a: [[1, 'one']]}, 64 | {a: [[1, 'one'], [2, 'two']]}), 65 | 'a'); 66 | assert.equal(cmp( 67 | {a: [[1, 'one'], [2, 'two']]}, 68 | {a: [[1, 'one'], [2, 'bar']]}), 69 | 'a[2]'); 70 | }); 71 | }); 72 | 73 | it('affected', function() { 74 | let cs = [ 75 | 'a', 'b[2]', 'c.x', 76 | ]; 77 | assert.equal(Changesets.affected(cs, ['a']), true); 78 | assert.equal(Changesets.affected(cs, ['b']), true); 79 | assert.equal(Changesets.affected(cs, ['b[2].x']), true); 80 | assert.equal(Changesets.affected(cs, ['c.y']), false); 81 | assert.equal(Changesets.affected(cs, ['c']), true); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /lib/types/range.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Type = require('./type.js'); 13 | let Value = require('./value.js'); 14 | let NumberValue = require('./number.js').Value; 15 | 16 | class RangeValue extends Value { 17 | 18 | constructor(type) { 19 | super(type); 20 | this.value = this.type.low; 21 | } 22 | 23 | assign(newValue) { 24 | if (typeof newValue == 'number') { 25 | if (newValue < this.type.low || newValue > this.type.high) { 26 | throw new errors.Bounds(`Cannot assign value of ${newValue} to range ${this.type.getName()}: ${this.type.low}..${this.type.high};`); 27 | } 28 | this.value = newValue; 29 | } else if (newValue instanceof NumberValue) { 30 | return this.assign(newValue.value); 31 | } else if (newValue instanceof RangeValue) { 32 | return this.assign(newValue.value); 33 | } else { 34 | let t = 'undefined'; 35 | if (newValue !== undefined) { 36 | t = `${newValue.type}`; 37 | } 38 | throw new errors.Internal(`Trying to assign ${t} to range ${this.type.getName()}: ${this.type.low}..${this.type.high};`); 39 | } 40 | } 41 | 42 | assignJSON(spec) { 43 | this.assign(spec); 44 | } 45 | 46 | equals(other) { 47 | if (typeof other == 'number') { 48 | return this.value == other; 49 | } else if (other instanceof RangeValue || other instanceof NumberValue) { 50 | return this.value == other.value; 51 | } else { 52 | throw new errors.Internal(`Trying to compare ${other.type} to range ${this.type.getName()}: ${this.type.low}..${this.type.high};`); 53 | } 54 | } 55 | 56 | innerToString() { 57 | return `${this.value}`; 58 | } 59 | 60 | toString() { 61 | return `${this.value}`; 62 | } 63 | 64 | toJSON() { 65 | return this.value; 66 | } 67 | } 68 | 69 | class RangeType extends Type { 70 | constructor(decl, env, name) { 71 | super(decl, env, name); 72 | this.low = this.decl.low.value; 73 | this.high = this.decl.high.value; 74 | } 75 | equals(other) { 76 | return (other instanceof RangeType && 77 | this.low == other.low && 78 | this.high == other.high); 79 | } 80 | makeDefaultValue() { 81 | return new RangeValue(this); 82 | } 83 | toString() { 84 | let name = this.getName(); 85 | if (name !== undefined) { 86 | return name; 87 | } 88 | return `${this.low}..${this.high}`; 89 | } 90 | } 91 | 92 | module.exports = { 93 | Type: RangeType, 94 | Value: RangeValue, 95 | }; 96 | -------------------------------------------------------------------------------- /lib/statements/function.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Environment = require('../environment.js').Environment; 13 | let makeStatement = require('./factory.js'); 14 | let makeType = require('../types/factory.js'); 15 | let Statement = require('./statement.js'); 16 | let Types = require('../types/types.js'); 17 | 18 | class FunctionDecl extends Statement { 19 | constructor(parsed, env) { 20 | super(parsed, env); 21 | if (parsed.returntype) { 22 | this.returntype = makeType.make(parsed.returntype, this.env); 23 | } else { 24 | this.returntype = null; 25 | } 26 | this.codeEnv = new Environment(this.env); 27 | this.code = makeStatement.make(this.parsed.code, this.codeEnv); 28 | this.params = this.parsed.params.map((param) => ({ 29 | id: param.id.value, 30 | type: makeType.make(param.type, this.env), 31 | decl: param, 32 | })); 33 | this.params.forEach((param) => { 34 | this.codeEnv.vars.set(param.id, 35 | param.type.makeDefaultValue(), 36 | param.decl.source); 37 | }); 38 | this.pure = false; 39 | this.env.functions.set(parsed.id.value, this, this.parsed.source); 40 | } 41 | 42 | typecheck() { 43 | this.code.typecheck(); 44 | return this.returntype; 45 | } 46 | 47 | typecheckApply(args) { 48 | if (args.length != this.params.length) { 49 | throw new errors.Type(`${this} takes exactly ` + 50 | `${this.params.length} parameters, ` + 51 | `${args.length} given`); 52 | } 53 | args.forEach(arg => arg.typecheck()); 54 | this.params.forEach((param, i) => { 55 | let arg = args[i]; 56 | if (!Types.subtypeOf(arg.type, param.type)) { 57 | throw new errors.Type(`${this} requires ` + 58 | `${param.type} but given ${arg.type} ` + 59 | `for argument ${i + 1}`); 60 | } 61 | }); 62 | } 63 | 64 | 65 | execute() {} 66 | 67 | evaluate(args, env, gargs, context) { 68 | this.params.forEach((param, i) => { 69 | let arg = args[i]; 70 | this.codeEnv.vars.get(param.id).assign(arg); 71 | }); 72 | try { 73 | this.code.execute(context); 74 | } catch ( e ) { 75 | if (!(e instanceof errors.Return)) { 76 | throw e; 77 | } 78 | return e.value; 79 | } 80 | if (this.returntype !== null) { 81 | throw new errors.Internal(`No value returned from ${this.parsed.id.value}`); 82 | } 83 | } 84 | 85 | toString() { 86 | return `${this.parsed.id.value}()`; 87 | } 88 | } 89 | 90 | module.exports = FunctionDecl; 91 | -------------------------------------------------------------------------------- /lib/statements/foreach.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Environment = require('../environment.js').Environment; 13 | let makeExpression = require('../expressions/factory.js'); 14 | let makeStatement = require('./factory.js'); 15 | let Statement = require('./statement.js'); 16 | let Types = require('../types/types.js'); 17 | 18 | class ForEach extends Statement { 19 | constructor(parsed, env) { 20 | super(parsed, env); 21 | this.expr = makeExpression.make(this.parsed.expr, this.env); 22 | this.codeEnv = new Environment(this.env); 23 | this.code = makeStatement.make(this.parsed.code, this.codeEnv); 24 | } 25 | 26 | typecheck() { 27 | this.expr.typecheck(); 28 | if (!Types.implementsIterable(this.expr.type)) { 29 | throw new errors.Type(`Cannot iterate on a ${this.expr.type} ` + 30 | `at ${this.expr.source}`); 31 | } 32 | let dummyValue = this.expr.type.valuetype.makeDefaultValue(); 33 | this.codeEnv.vars.set(this.parsed.value.value, dummyValue); 34 | if (this.parsed.index !== undefined) { 35 | let index = this.expr.type.indextype.makeDefaultValue(); 36 | this.codeEnv.vars.set(this.parsed.index.value, index); 37 | } 38 | this.code.typecheck(); 39 | } 40 | 41 | execute(context) { 42 | let dummyValue = this.codeEnv.getVar(this.parsed.value.value); 43 | let restoreValue = () => { 44 | this.codeEnv.vars.shadow(this.parsed.value.value, dummyValue); 45 | }; 46 | let restoreIndex = () => { 47 | }; 48 | if (this.parsed.index !== undefined) { 49 | let index = this.codeEnv.getVar(this.parsed.index.value); 50 | restoreIndex = () => { 51 | index.assign(this.expr.type.indextype.makeDefaultValue()); 52 | }; 53 | } 54 | try { 55 | this.expr.evaluate(context).forEach((v, i) => { 56 | // This is a little dangerous in that it assumes that no one ever does a 57 | // getVar and holds onto it. 58 | this.codeEnv.vars.shadow(this.parsed.value.value, v); 59 | if (this.parsed.index !== undefined) { 60 | this.codeEnv.getVar(this.parsed.index.value).assign(i); 61 | } 62 | try { 63 | this.code.execute(context); 64 | } catch ( e ) { 65 | if (!(e instanceof errors.Continue)) { 66 | throw e; 67 | } 68 | } 69 | }); 70 | } catch ( e ) { 71 | if (!(e instanceof errors.Break)) { 72 | throw e; 73 | } 74 | } 75 | restoreIndex(); 76 | restoreValue(); 77 | } 78 | } 79 | 80 | module.exports = ForEach; 81 | -------------------------------------------------------------------------------- /test/statements/ifelse.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('statements/ifelse.js', function() { 15 | describe('ifelse', function() { 16 | 17 | it('if-else False', function() { 18 | let module = testing.run(` 19 | var x : 0..2; 20 | if False { 21 | x = 1; 22 | } else { 23 | x = 2; 24 | } 25 | `); 26 | assert.equal(module.env.getVar('x').toString(), '2'); 27 | }); 28 | 29 | it('if-else True', function() { 30 | let module = testing.run(` 31 | var x : 0..2; 32 | if True { 33 | x = 1; 34 | } else { 35 | x = 2; 36 | } 37 | `); 38 | assert.equal(module.env.getVar('x').toString(), '1'); 39 | }); 40 | 41 | it('if-else expression', function() { 42 | let module = testing.run(` 43 | var x : 0..2; 44 | if True == True { 45 | x = 1; 46 | } else { 47 | x = 2; 48 | } 49 | `); 50 | assert.equal(module.env.getVar('x').toString(), '1'); 51 | }); 52 | 53 | it('if-only False', function() { 54 | let module = testing.run(` 55 | var x : 0..2; 56 | if False { 57 | x = 1; 58 | } 59 | `); 60 | assert.equal(module.env.getVar('x').toString(), '0'); 61 | }); 62 | 63 | it('if-only True', function() { 64 | let module = testing.run(` 65 | var x : 0..2; 66 | if True { 67 | x = 1; 68 | } 69 | `); 70 | assert.equal(module.env.getVar('x').toString(), '1'); 71 | }); 72 | 73 | it('else-if True', function() { 74 | let module = testing.run(` 75 | var x : 0..3; 76 | if False { 77 | x = 1; 78 | } else if True { 79 | x = 2; 80 | } else { 81 | x = 3; 82 | } 83 | `); 84 | assert.equal(module.env.getVar('x').toString(), '2'); 85 | }); 86 | 87 | it('else-if False', function() { 88 | let module = testing.run(` 89 | var x : 0..3; 90 | if False { 91 | x = 1; 92 | } else if False { 93 | x = 2; 94 | } else { 95 | x = 3; 96 | } 97 | `); 98 | assert.equal(module.env.getVar('x').toString(), '3'); 99 | }); 100 | 101 | it('else-if expression', function() { 102 | let module = testing.run(` 103 | var x : 0..3; 104 | if False { 105 | x = 1; 106 | } else if True == True { 107 | x = 2; 108 | } else { 109 | x = 3; 110 | } 111 | `); 112 | assert.equal(module.env.getVar('x').toString(), '2'); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /lib/types/array.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let array = {}; 12 | module.exports = array; 13 | 14 | let makeType = require('./factory.js'); 15 | let Type = require('./type.js'); 16 | let Value = require('./value.js'); 17 | let errors = require('../errors.js'); 18 | let NumberValue = require('./number.js'); 19 | let RangeValue = require('./range.js'); 20 | 21 | class ArrayValue extends Value { 22 | constructor(type) { 23 | super(type); 24 | let length = this.type.indextype.high - this.type.indextype.low + 1; 25 | this.items = Array.from({ 26 | length: length 27 | }, 28 | () => this.type.valuetype.makeDefaultValue()); 29 | } 30 | index(i) { 31 | if (i instanceof NumberValue.Value || i instanceof RangeValue.Value) { 32 | i = i.value; 33 | } 34 | if (typeof i !== 'number') { 35 | throw new errors.Internal(`Trying to index array with ${i}`); 36 | } 37 | if (i < this.type.indextype.low || i > this.type.indextype.high) { 38 | throw new errors.Bounds(`Cannot access index ${i} of ${this}`); 39 | } 40 | let v = this.items[i - this.type.indextype.low]; 41 | if (v == undefined) { 42 | throw new errors.Internal(`Bounds check failed to catch issue: ${i}`); 43 | } 44 | return v; 45 | } 46 | forEach(cb) { 47 | this.items.forEach((v, i) => cb(v, this.type.indextype.low + i)); 48 | } 49 | map(cb) { 50 | return this.items.map((v, i) => cb(v, this.type.indextype.low + i)); 51 | } 52 | toString() { 53 | let inner = this.items.map((v, i) => { 54 | return `${this.type.indextype.low + i}: ${v.toString()}`; 55 | }).join(', '); 56 | return `[${inner}]`; 57 | } 58 | 59 | toJSON() { 60 | return this.items.map((v, i) => [this.type.indextype.low + i, v.toJSON()]); 61 | } 62 | assignJSON(spec) { 63 | spec.forEach(x => { 64 | this.index(x[0]).assignJSON(x[1]); 65 | }); 66 | } 67 | assign(other) { 68 | this.forEach((v, i) => this.index(i).assign(other.index(i))); 69 | } 70 | equals(other) { 71 | let allEqual = true; 72 | this.forEach((v, i) => { 73 | if (!v.equals(other.index(i))) { 74 | allEqual = false; 75 | } 76 | }); 77 | return allEqual; 78 | } 79 | size() { 80 | return this.items.length; 81 | } 82 | capacity() { 83 | return this.items.length; 84 | } 85 | } 86 | 87 | class ArrayType extends Type { 88 | constructor(decl, env, name) { 89 | super(decl, env, name); 90 | this.valuetype = makeType.make(this.decl.args[0], this.env); 91 | this.indextype = makeType.make(this.decl.indexBy, this.env); 92 | } 93 | makeDefaultValue() { 94 | return new ArrayValue(this); 95 | } 96 | toString() { 97 | return `Array<${this.valuetype}>[${this.indextype}]`; 98 | } 99 | } 100 | 101 | array.Type = ArrayType; 102 | array.Value = ArrayValue; 103 | -------------------------------------------------------------------------------- /lib/statements/match.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Environment = require('../environment.js').Environment; 13 | let makeExpression = require('../expressions/factory.js'); 14 | let makeStatement = require('./factory.js'); 15 | let Statement = require('./statement.js'); 16 | let EitherType = require('../types/either.js').Type; 17 | 18 | class Match extends Statement { 19 | constructor(parsed, env) { 20 | super(parsed, env); 21 | this.expr = makeExpression.make(this.parsed.expr, this.env); 22 | this.variants = new Map(this.parsed.variants.map((variant, i) => { 23 | let variantEnv = new Environment(this.env); 24 | if (variant.type.value === 'default') { 25 | if (i < this.parsed.variants.length - 1) { 26 | throw new errors.Parse(`default must be last in match ` + 27 | `at ${this.parsed.source}`); 28 | } 29 | if (variant.id !== undefined) { 30 | throw new errors.Parse(`default cannot have variable in match ` + 31 | `at ${this.parsed.source}`); 32 | } 33 | } 34 | return [variant.type.value, 35 | { 36 | id: variant.id, 37 | code: makeStatement.make(variant.code, variantEnv), 38 | env: variantEnv, 39 | }]; 40 | })); 41 | } 42 | 43 | typecheck() { 44 | this.expr.typecheck(); 45 | if (!(this.expr.type instanceof EitherType)) { 46 | throw new errors.Type(`Cannot match on a ${this.expr.type.getName()} ` + 47 | `at ${this.expr.source}`); 48 | } 49 | 50 | let tagsPresent = new Set(); 51 | this.variants.forEach((variant, tag) => { 52 | tagsPresent.add(tag); 53 | if (variant.id !== undefined) { 54 | let value = this.expr.type.getVariant(tag).makeDefaultValue(); 55 | variant.env.vars.set(variant.id.value, value, variant.id.source); 56 | } 57 | variant.code.typecheck(); 58 | }); 59 | 60 | if (!tagsPresent.has('default')) { 61 | this.expr.type.variants.forEach(v => { 62 | if (!tagsPresent.has(v.name)) { 63 | throw new errors.Type(`Missing variant ${v.name} in match ` + 64 | `at ${this.parsed.source}`); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | execute(context) { 71 | let value = this.expr.evaluate(context); 72 | let variant = this.variants.get(value.varianttype.name); 73 | if (variant === undefined) { 74 | variant = this.variants.get('default'); 75 | if (variant === undefined) { 76 | throw new errors.Internal(`Bad variant: ${value.varianttype.name}`); 77 | } 78 | } else { 79 | if (variant.id !== undefined) { 80 | variant.env.getVar(variant.id.value).assign(value); 81 | } 82 | } 83 | variant.code.execute(context); 84 | } 85 | 86 | toString(indent) { 87 | return `${indent}match ${this.expr.toString(indent)} { 88 | ${indent} ... 89 | ${indent}}`; 90 | } 91 | } 92 | 93 | module.exports = Match; 94 | -------------------------------------------------------------------------------- /lib/types/record.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors'); 12 | let Type = require('./type.js'); 13 | let Value = require('./value.js'); 14 | let makeType = require('./factory.js'); 15 | 16 | class RecordValue extends Value { 17 | 18 | constructor(type) { 19 | super(type); 20 | this.type.fieldtypes.forEach((fieldtype) => { 21 | this[fieldtype.name] = fieldtype.type.makeDefaultValue(); 22 | }); 23 | } 24 | 25 | assign(other) { 26 | if (other === undefined) { 27 | throw new errors.Internal(`Can't assign undefined to ${this}`); 28 | } 29 | if (this.type !== other.type) { 30 | throw new errors.Internal(`Can't assign ${other} to ${this}`); 31 | } 32 | this.type.fieldtypes.forEach((fieldtype) => { 33 | this[fieldtype.name].assign(other[fieldtype.name]); 34 | }); 35 | } 36 | 37 | equals(other) { 38 | if (this.type !== other.type) { 39 | return false; 40 | } 41 | let equal = true; 42 | this.type.fieldtypes.forEach((fieldtype) => { 43 | if (typeof this[fieldtype.name].equals != 'function') { 44 | throw new errors.Internal(`This value doesn't have equals(): ${this[fieldtype.name]}`); 45 | } 46 | if (!this[fieldtype.name].equals(other[fieldtype.name])) { 47 | equal = false; 48 | } 49 | }); 50 | return equal; 51 | } 52 | 53 | lookup(fieldname) { 54 | return this[fieldname]; 55 | } 56 | 57 | set(fieldname, value) { 58 | this[fieldname] = value; 59 | } 60 | 61 | innerToString() { 62 | let fields = this.type.decl.fields.map((v) => { 63 | let rhs = this[v.id.value].toString(); 64 | return `${v.id.value}: ${rhs}`; 65 | }).join(', '); 66 | return fields; 67 | } 68 | 69 | toString() { 70 | let name = this.type.getName(); 71 | let fields = this.type.decl.fields.map((v) => { 72 | let rhs = this[v.id.value].toString(); 73 | return `${v.id.value}: ${rhs}`; 74 | }).join(', '); 75 | return `${name} { ${fields} }`; 76 | } 77 | 78 | toJSON() { 79 | let o = {}; 80 | this.type.decl.fields.forEach((v) => { 81 | o[v.id.value] = this[v.id.value].toJSON(); 82 | }); 83 | return o; 84 | } 85 | 86 | assignJSON(spec) { 87 | this.type.fieldtypes.forEach(fieldtype => { 88 | this[fieldtype.name].assignJSON(spec[fieldtype.name]); 89 | }); 90 | } 91 | } 92 | 93 | 94 | class RecordType extends Type { 95 | constructor(decl, env, name) { 96 | super(decl, env, name); 97 | this.fieldtypes = this.decl.fields.map((field) => ({ 98 | name: field.id.value, 99 | type: makeType.make(field.type, this.env), 100 | })); 101 | } 102 | equals(other) { 103 | return this === other; 104 | } 105 | fieldType(name) { 106 | let retval = undefined; 107 | this.fieldtypes.forEach((ft) => { 108 | if (ft.name == name) { 109 | retval = ft.type; 110 | } 111 | }); 112 | return retval; 113 | } 114 | makeDefaultValue() { 115 | return new RecordValue(this); 116 | } 117 | toString() { 118 | let name = this.getName(); 119 | if (name !== undefined) { 120 | return name; 121 | } 122 | return 'anonymous record'; 123 | } 124 | } 125 | 126 | module.exports = RecordType; 127 | -------------------------------------------------------------------------------- /test/types/varlen.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('types/varlen.js', function() { 15 | describe('Set', function() { 16 | it('basic', function() { 17 | let module = testing.run(` 18 | type Stuff : 0..99; 19 | var set : Set[1..5]; 20 | push(set, 33); 21 | push(set, 37); 22 | push(set, 33); 23 | var b1 : Boolean = contains(set, 33); 24 | pop(set); 25 | var b2 : Boolean = empty(set); 26 | `); 27 | assert.equal(module.env.getVar('set').toString(), 28 | '{1: 33}'); 29 | assert.equal(module.env.getVar('b1').toString(), 30 | 'True'); 31 | assert.equal(module.env.getVar('b2').toString(), 32 | 'False'); 33 | }); 34 | }); 35 | 36 | describe('OrderedSet', function() { 37 | it('basic', function() { 38 | let module = testing.run(` 39 | type Stuff : 0..99; 40 | var set : OrderedSet[1..5]; 41 | push(set, 33); 42 | push(set, 37); 43 | var b1 : Boolean = contains(set, 33); 44 | pop(set); 45 | var b2 : Boolean = empty(set); 46 | `); 47 | assert.equal(module.env.getVar('set').toString(), 48 | '{1: 33}'); 49 | assert.equal(module.env.getVar('b1').toString(), 50 | 'True'); 51 | assert.equal(module.env.getVar('b2').toString(), 52 | 'False'); 53 | }); 54 | 55 | it('pop (issue-5 regression)', function() { 56 | let module = testing.run(` 57 | type Number : 0..9; 58 | var os : OrderedSet[1..3]; 59 | push(os, 2); 60 | push(os, 4); 61 | var e1 : Number = pop(os); 62 | var e2 : Number = pop(os); 63 | `); 64 | assert.equal(module.env.getVar('os').toString(), 65 | '{}'); 66 | assert.equal(module.env.getVar('e1').toString(), 67 | '4'); 68 | assert.equal(module.env.getVar('e2').toString(), 69 | '2'); 70 | }); 71 | }); 72 | 73 | describe('MultiSet', function() { 74 | it('basic', function() { 75 | let module = testing.run(` 76 | type Stuff : 0..99; 77 | var set : MultiSet[1..5]; 78 | push(set, 33); 79 | push(set, 37); 80 | push(set, 33); 81 | var b1 : Boolean = contains(set, 33); 82 | pop(set); 83 | var b2 : Boolean = empty(set); 84 | `); 85 | assert.equal(module.env.getVar('set').toString(), 86 | '{1: 33, 2: 33}'); 87 | assert.equal(module.env.getVar('b1').toString(), 88 | 'True'); 89 | assert.equal(module.env.getVar('b2').toString(), 90 | 'False'); 91 | }); 92 | }); 93 | 94 | describe('Vector', function() { 95 | it('basic', function() { 96 | let module = testing.run(` 97 | type Stuff : 0..99; 98 | var set : Vector[1..5]; 99 | push(set, 33); 100 | push(set, 37); 101 | push(set, 33); 102 | var b1 : Boolean = contains(set, 33); 103 | pop(set); 104 | var b2 : Boolean = empty(set); 105 | `); 106 | assert.equal(module.env.getVar('set').toString(), 107 | '{1: 33, 2: 37}'); 108 | assert.equal(module.env.getVar('b1').toString(), 109 | 'True'); 110 | assert.equal(module.env.getVar('b2').toString(), 111 | 'False'); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /lib/statements/rulefor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let makeExpression = require('../expressions/factory.js'); 12 | let Environment = require('../environment.js').Environment; 13 | let Statement = require('./statement.js'); 14 | let Types = require('../types/types.js'); 15 | let makeStatement = require('./factory.js'); 16 | let errors = require('../errors.js'); 17 | let _ = require('lodash'); 18 | 19 | class RuleFor extends Statement { 20 | constructor(parsed, env) { 21 | super(parsed, env); 22 | this.external = this.parsed.subkind === 'external'; 23 | this.loops = this.parsed.loops.map(loop => ({ 24 | expr: makeExpression.make(loop.expr, this.env), 25 | parsed: loop, 26 | })); 27 | this.innerEnv = new Environment(this.env); 28 | this.inner = makeStatement.make(this.parsed.code, this.innerEnv); 29 | this.env.assignRule(this.parsed.id.value, this); 30 | } 31 | 32 | typecheck() { 33 | this.loops.forEach(loop => { 34 | loop.expr.typecheck(); 35 | if (!Types.implementsIterable(loop.expr.type)) { 36 | throw new errors.Type(`Cannot iterate on a ${loop.expr.type} ` + 37 | `at ${loop.expr.source}`); 38 | } 39 | let dummyValue = loop.expr.type.valuetype.makeDefaultValue(); 40 | this.innerEnv.vars.set(loop.parsed.value.value, dummyValue, loop.parsed.value.source); 41 | if (loop.parsed.index !== undefined) { 42 | let dummyIndex = loop.expr.type.indextype.makeDefaultValue(); 43 | this.innerEnv.vars.set(loop.parsed.index.value, dummyIndex, loop.parsed.value.source); 44 | } 45 | }); 46 | this.inner.typecheck(); 47 | } 48 | 49 | execute() { 50 | // do nothing 51 | } 52 | 53 | fire(indexArg, context) { 54 | this.loops.forEach((loop, i) => { 55 | // index might come in as a plain JS number, but we want it in a proper 56 | // value. 57 | let index = loop.expr.type.indextype.makeDefaultValue(); 58 | if (this.loops.length == 1 && indexArg === undefined) { 59 | // no index given, fire the first one 60 | } else if (this.loops.length == 1 && !(indexArg instanceof Array)) { 61 | index.assign(indexArg); 62 | } else { 63 | index.assign(indexArg[i]); 64 | } 65 | 66 | let array = loop.expr.evaluate(context); 67 | let value = array.index(index); 68 | // This is a little dangerous in that it assumes that no one ever does a 69 | // getVar and holds onto it. 70 | this.innerEnv.vars.shadow(loop.parsed.value.value, value); 71 | if (loop.parsed.index !== undefined) { 72 | this.innerEnv.vars.get(loop.parsed.index.value).assign(index); 73 | } 74 | }); 75 | this.inner.execute(context); 76 | } 77 | 78 | enumerate(context) { 79 | let results = []; 80 | let helper = (prefix, ranges) => { 81 | if (ranges.length == 0) { 82 | results.push(prefix); 83 | } else { 84 | _.first(ranges).forEach(i => { 85 | helper(prefix.concat(i), _.tail(ranges)); 86 | }); 87 | } 88 | }; 89 | helper([], this.loops.map(loop => { 90 | let array = loop.expr.evaluate(context); // fill in readset for caller 91 | return array.map((v, i) => i); 92 | })); 93 | return results; 94 | } 95 | 96 | toString(indent) { 97 | return `${indent}rule ${this.parsed.id.value} for ... in ... { 98 | ${this.inner.toString(indent + ' ')} 99 | }`; 100 | } 101 | } 102 | 103 | module.exports = RuleFor; 104 | -------------------------------------------------------------------------------- /lib/modelchecker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let crypto = require('crypto'); 12 | let RuleFor = require('./statements/rulefor.js'); 13 | 14 | let hash = input => 15 | crypto.createHash('sha1') 16 | .update(input) 17 | .digest('binary'); 18 | 19 | let serializeState = (module) => { 20 | let state = {}; 21 | module.env.vars.forEach((mvar, name) => { 22 | if (!mvar.isConstant) { 23 | state[name] = mvar.toJSON(); 24 | } 25 | }); 26 | return JSON.stringify(state, null, 2); 27 | }; 28 | 29 | let restoreState = (module, state) => { 30 | state = JSON.parse(state); 31 | module.env.vars.forEach((mvar, name) => { 32 | if (!mvar.isConstant) { 33 | mvar.assignJSON(state[name]); 34 | } 35 | }); 36 | }; 37 | 38 | let context = {}; 39 | 40 | let extractSimpleRules = (module) => { 41 | let simpleRules = []; 42 | module.env.rules.forEach((rule, name) => { 43 | if (rule instanceof RuleFor) { 44 | rule.enumerate(context).forEach(indexes => { 45 | simpleRules.push({ 46 | name: `${name}(${indexes.join(', ')})`, 47 | fire: () => rule.fire(indexes, context), 48 | }); 49 | }); 50 | } else { 51 | simpleRules.push({ 52 | name: name, 53 | fire: () => rule.fire(context), 54 | }); 55 | } 56 | }); 57 | return simpleRules; 58 | }; 59 | 60 | let checker = function(module) { 61 | 62 | let states = new Set(); // stores hashes of all known states satisfying invariants 63 | let unexplored = new Set(); // stores JSON of unexplored states (already known to satisfy invariants) 64 | 65 | let start = serializeState(module); 66 | module.env.invariants.forEach((invariant, name) => { 67 | try { 68 | invariant.check(context); 69 | } catch (e) { 70 | console.log('Initial state', start); 71 | console.log('Failed', name); 72 | throw e; 73 | } 74 | }); 75 | states.add(hash(start)); 76 | unexplored.add(start); 77 | 78 | let printStatus = (cond) => { 79 | let expanded = states.size - unexplored.size; 80 | if (cond === undefined || cond(expanded)) { 81 | console.log(`Checked ${states.size}, ` + 82 | `expanded ${expanded} ` + 83 | `(${Math.round(expanded / states.size * 100)}% of checked)`); 84 | } 85 | }; 86 | 87 | while (unexplored.size > 0) { 88 | let start = unexplored.values().next().value; 89 | unexplored.delete(start); 90 | restoreState(module, start); 91 | 92 | extractSimpleRules(module).forEach(rule => { 93 | try { 94 | rule.fire(); 95 | } catch (e) { 96 | console.log('Started at', start); 97 | console.log('Fired', rule.name); 98 | throw e; 99 | } 100 | let state = serializeState(module); 101 | if (state !== start) { 102 | let stateHash = hash(state); 103 | if (!states.has(stateHash)) { 104 | module.env.invariants.forEach((invariant, name) => { 105 | try { 106 | invariant.check(context); 107 | } catch (e) { 108 | console.log('Was at', start); 109 | console.log('Applied', rule.name); 110 | console.log('Reached', state); 111 | console.log('Failed', name); 112 | throw e; 113 | } 114 | }); 115 | states.add(stateHash); 116 | unexplored.add(state); 117 | } 118 | restoreState(module, start); 119 | } 120 | }); 121 | 122 | printStatus(expanded => (expanded % 100 == 0)); 123 | } 124 | printStatus(); 125 | }; 126 | 127 | module.exports = { 128 | checker: checker, 129 | }; 130 | -------------------------------------------------------------------------------- /vim/runway.vim: -------------------------------------------------------------------------------- 1 | " Vim syntax file 2 | " Language: Runway modelling description language 3 | " Maintainer: Diego Ongaro 4 | " Last Change: Fri May 6 11:13:02 PDT 2016 5 | " Version: 0 6 | " 7 | " Copyright (c) 2015-2016, Salesforce.com, Inc. 8 | " All rights reserved. 9 | " Licensed under the MIT license. 10 | " For full license text, see LICENSE.md file in the repo root or 11 | " https://opensource.org/licenses/MIT 12 | 13 | if exists("b:current_syntax") 14 | finish 15 | endif 16 | 17 | syntax case ignore 18 | syn keyword runwayConditional as 19 | syn keyword runwayStatement assert 20 | syn keyword runwayRepeat break 21 | syn keyword runwayRepeat continue 22 | syn keyword runwayStructure either 23 | syn keyword runwayConditional else 24 | syn keyword runwayStatement external 25 | syn keyword runwayRepeat for 26 | syn keyword runwayStatement function 27 | syn keyword runwayConditional if 28 | syn keyword runwayRepeat in 29 | syn keyword runwayStatement invariant 30 | syn keyword runwayConditional match 31 | syn keyword runwayStatement param 32 | syn keyword runwayStatement print 33 | syn keyword runwayStructure record 34 | syn keyword runwayRepeat reset 35 | syn keyword runwayStatement return 36 | syn keyword runwayStatement rule 37 | syn keyword runwayStatement type 38 | syn keyword runwayStatement var 39 | syn keyword runwayRepeat while 40 | 41 | syn keyword runwayTodo contained fixme 42 | syn keyword runwayTodo contained todo 43 | syn keyword runwayTodo contained xxx 44 | 45 | syntax case match 46 | 47 | " These are case-sensitive: 48 | syn keyword runwayStructure Array 49 | syn keyword runwayStructure Boolean 50 | syn keyword runwayBoolean False 51 | syn keyword runwayStructure MultiSet 52 | syn keyword runwayStructure OrderedSet 53 | syn keyword runwayStructure Output 54 | syn keyword runwayStructure Set 55 | syn keyword runwayStructure Time 56 | syn keyword runwayBoolean True 57 | syn keyword runwayStructure Vector 58 | syn keyword runwayFunction capacity 59 | syn keyword runwayFunction contains 60 | syn keyword runwayFunction empty 61 | syn keyword runwayFunction full 62 | syn keyword runwayFunction later 63 | syn keyword runwayFunction past 64 | syn keyword runwayFunction pop 65 | syn keyword runwayFunction pow 66 | syn keyword runwayFunction push 67 | syn keyword runwayFunction remove 68 | syn keyword runwayFunction size 69 | syn keyword runwayFunction urandom 70 | syn keyword runwayFunction urandomRange 71 | 72 | 73 | " Integers. 74 | syn match runwayNumber "\<\d\+\>" 75 | 76 | " Operators and special characters 77 | syn match runwayOperator "[:\+\-\*=<>&|]" 78 | syn match runwayDelimiter "[\.,:]" 79 | syn match runwaySpecial "[{}\(\)\[\]]" 80 | 81 | " Comments. This is defined so late so that it overrides previous matches. 82 | syn region runwayComment start="//" end="$" contains=runwayTodo 83 | syn region runwayComment start="/\*" end="\*/" contains=runwayTodo 84 | 85 | highlight link runwayComment Comment 86 | highlight link runwayString String 87 | highlight link runwayNumber Number 88 | highlight link runwayBoolean Boolean 89 | highlight link runwayIdentifier Identifier 90 | highlight link runwayFunction Function 91 | highlight link runwayStatement Statement 92 | highlight link runwayConditional Conditional 93 | highlight link runwayRepeat Repeat 94 | highlight link runwayLabel Label 95 | highlight link runwayOperator Operator 96 | highlight link runwayKeyword Keyword 97 | highlight link runwayType Type 98 | highlight link runwayStructure Structure 99 | highlight link runwaySpecial Special 100 | highlight link runwayDelimiter Delimiter 101 | highlight link runwayError Error 102 | highlight link runwayTodo Todo 103 | 104 | let b:current_syntax = "runway" 105 | -------------------------------------------------------------------------------- /lib/changesets.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let _ = require('lodash'); 12 | 13 | // Returns sorted array of paths that differ 14 | let compareJSON = (state1, state2) => { 15 | let same = []; 16 | let different = ['']; 17 | let compare = (state1, state2) => { 18 | //console.log(JSON.stringify(state1, null, 2)); 19 | //console.log(JSON.stringify(state2, null, 2)); 20 | if (state1 instanceof Array && state2 instanceof Array) { 21 | return compareCollection(state1, state2); 22 | } 23 | if (state1 instanceof Object && state2 instanceof Object) { 24 | if ('tag' in state1 && 'tag' in state2) { 25 | return compareEither(state1, state2); 26 | } else { 27 | return compareRecord(state1, state2); 28 | } 29 | } else { 30 | // deep equality is sufficient for numbers and eithers 31 | return _.isEqual(state1, state2) ? same : different; 32 | } 33 | }; 34 | let compareCollection = (state1, state2) => { 35 | if (state1.length == state2.length && 36 | _.isEqual(state1.map(_.head), 37 | state2.map(_.head))) { 38 | return _.flatMap(_.zip(state1, state2), (kv12) => { 39 | let kv1 = kv12[0]; 40 | let kv2 = kv12[1]; 41 | return _.map(compare(kv1[1], kv2[1]), 42 | change => `[${kv1[0]}]${change}`); 43 | }); 44 | } else { 45 | return different; 46 | } 47 | }; 48 | let compareEither = (state1, state2) => { 49 | if (state1.tag === state2.tag) { 50 | return _.map(compareRecord(state1.fields, state2.fields), 51 | change => `!${state1.tag}${change}`); 52 | } else { 53 | return different; 54 | } 55 | }; 56 | let compareRecord = (state1, state2) => { 57 | let keys1 = Object.keys(state1).sort(); 58 | let keys2 = Object.keys(state2).sort(); 59 | if (_.isEqual(keys1, keys2)) { 60 | return _.flatMap(keys1, key => 61 | _.map(compare(state1[key], state2[key]), 62 | change => `.${key}${change}`)); 63 | } else { 64 | return different; 65 | } 66 | }; 67 | let compareGlobals = (state1, state2) => { 68 | let keys = _.union(_.keys(state1), _.keys(state2)).sort(); 69 | return _.flatMap(keys, key => { 70 | if (key in state1 && key in state2) { 71 | return _.map(compare(state1[key], state2[key]), 72 | change => `${key}${change}`); 73 | } else { 74 | return key; 75 | } 76 | }); 77 | }; 78 | return compareGlobals(state1, state2); 79 | }; 80 | 81 | let affected = (changeset, readset) => { 82 | if (_.isString(changeset)) { 83 | changeset = [changeset]; 84 | } 85 | if (_.isString(readset)) { 86 | readset = [readset]; 87 | } 88 | // Could try _.sortedIndex to speed this up, but it's not clear that 89 | // would be faster for the small arrays expected here. 90 | for (let read of readset) { 91 | for (let change of changeset) { 92 | if (change.startsWith(read) || 93 | read.startsWith(change)) { 94 | return true; 95 | } 96 | } 97 | } 98 | return false; 99 | }; 100 | 101 | let empty = changeset => { 102 | if ('length' in changeset) { // Array 103 | return changeset.length === 0; 104 | } else if ('size' in changeset) { // Set 105 | return changeset.size === 0; 106 | } else { // hmm 107 | console.error(`Don't know what ${changeset} is`); 108 | return false; 109 | } 110 | }; 111 | 112 | let union = (cs1, cs2) => { 113 | let changes = Array.from(cs1).concat(Array.from(cs2)); 114 | changes.sort(); 115 | return _.uniq(changes); 116 | }; 117 | 118 | module.exports = { 119 | compareJSON: compareJSON, 120 | affected: affected, 121 | empty: empty, 122 | union: union, 123 | }; 124 | -------------------------------------------------------------------------------- /test/compiler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let environment = require('../lib/environment.js'); 13 | let Environment = environment.Environment; 14 | let GlobalEnvironment = environment.GlobalEnvironment; 15 | let Input = require('../lib/input.js'); 16 | let compiler = require('../lib/compiler.js'); 17 | let fs = require('fs'); 18 | 19 | let inline = (text) => new Input('unit test', text); 20 | let readFile = (filename) => fs.readFileSync(filename).toString(); 21 | let loadPrelude = () => compiler.loadPrelude(readFile('lib/prelude.model')); 22 | 23 | describe('compiler.js', function() { 24 | 25 | describe('alias', function() { 26 | it('missing', function() { 27 | let code = inline(` 28 | type FailBoat: WhatIsThis; 29 | `); 30 | let env = new Environment(); 31 | assert.throws(() => { 32 | compiler.load(code, env); 33 | }); 34 | }); 35 | 36 | it('basic', function() { 37 | let code = inline(` 38 | type Boolean: either { False, True }; 39 | type Truthful: Boolean; 40 | `); 41 | let env = new Environment(); 42 | compiler.load(code, env); 43 | let value = env.getType('Truthful').makeDefaultValue(); 44 | assert.equal(value.toString(), 'False'); 45 | }); 46 | }); 47 | 48 | describe('loadPrelude', function() { 49 | it('prelude loads', function() { 50 | let prelude = loadPrelude().env; 51 | let booleanType = prelude.getType('Boolean'); 52 | let booleanValue = booleanType.makeDefaultValue(); 53 | assert.equal(booleanValue, 'False'); 54 | }); 55 | }); // loadPrelude 56 | 57 | describe('params', function() { 58 | it('basic', function() { 59 | let code = inline('param ELEVATORS: 1..1024 = 6;'); 60 | let env = new Environment(); 61 | let module = compiler.load(code, env); 62 | let context = {}; 63 | module.ast.execute(context); 64 | assert.equal(env.getVar('ELEVATORS').toString(), '6'); 65 | }); 66 | }); 67 | 68 | describe('variable declarations', function() { 69 | it('basic', function() { 70 | let code = inline(` 71 | var foo: 0..10 = 8; 72 | var bar: 11..20; 73 | `); 74 | let env = new Environment(); 75 | let module = compiler.load(code, env); 76 | let context = {}; 77 | module.ast.execute(context); 78 | assert.equal(env.getVar('foo').toString(), '8'); 79 | assert.equal(env.getVar('bar').toString(), '11'); 80 | }); 81 | 82 | it('array', function() { 83 | let code = inline(` 84 | var bitvector: Array[11..13]; 85 | `); 86 | let env = new Environment(loadPrelude().env); 87 | compiler.load(code, env); 88 | assert.equal(env.getVar('bitvector').toString(), 89 | '[11: False, 12: False, 13: False]'); 90 | }); 91 | 92 | }); 93 | 94 | 95 | describe('code evaluation', function() { 96 | it('basic', function() { 97 | let prelude = loadPrelude(); 98 | let env = new GlobalEnvironment(prelude.env); 99 | let code = inline(` 100 | var x : Boolean; 101 | var y : 1..3; 102 | rule foo { 103 | x = True; 104 | y = 2; 105 | } 106 | `); 107 | let module = compiler.load(code, env); 108 | let context = {}; 109 | module.ast.execute(context); 110 | env.getRule('foo').fire(context); 111 | assert.equal(env.getVar('x').toString(), 'True'); 112 | assert.equal(env.getVar('y'), '2'); 113 | }); 114 | 115 | it('+=', function() { 116 | let prelude = loadPrelude(); 117 | let env = new GlobalEnvironment(prelude.env); 118 | let code = inline(` 119 | var y : 1..3; 120 | y += 1; 121 | `); 122 | let module = compiler.load(code, env); 123 | let context = {}; 124 | module.ast.execute(context); 125 | assert.equal(env.getVar('y'), '2'); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /lib/environment.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('./errors.js'); 12 | let util = require('util'); 13 | 14 | class EnvironmentMap { 15 | constructor(enclosing, kind) { 16 | this.enclosing = enclosing; 17 | this.kind = kind; 18 | this.entries = new Map(); 19 | } 20 | 21 | forEach(cb) { 22 | if (this.enclosing !== null) { 23 | this.enclosing.forEach(cb); 24 | } 25 | this.forEachLocal(cb); 26 | } 27 | 28 | forEachLocal(cb) { 29 | this.entries.forEach((vs, name) => cb(vs.value, name)); 30 | } 31 | 32 | map(cb) { 33 | if (this.enclosing === null) { 34 | return this.mapLocal(cb); 35 | } else { 36 | return this.enclosing.map(cb).concat(this.mapLocal(cb)); 37 | } 38 | } 39 | 40 | mapLocal(cb) { 41 | let result = []; 42 | this.entries.forEach((vs, name) => { 43 | result.push(cb(vs.value, name)); 44 | }); 45 | return result; 46 | } 47 | 48 | getValueSource(id) { 49 | let vs = this.entries.get(id); 50 | if (vs != undefined) { 51 | return vs; 52 | } 53 | if (this.enclosing != null) { 54 | return this.enclosing.getValueSource(id); 55 | } 56 | return undefined; 57 | } 58 | 59 | get(id) { 60 | let vs = this.getValueSource(id); 61 | if (vs === undefined) { 62 | return undefined; 63 | } else { 64 | return vs.value; 65 | } 66 | } 67 | 68 | list() { 69 | let here = Array.from(this.entries.keys()); 70 | if (this.enclosing != null) { 71 | return this.enclosing.list().concat(here); 72 | } else { 73 | return here; 74 | } 75 | } 76 | 77 | shadow(id, value, source) { 78 | this.entries.set(id, { 79 | value: value, 80 | source: source 81 | }); 82 | } 83 | 84 | set(id, value, source) { 85 | let vs = this.getValueSource(id); 86 | if (vs != undefined) { 87 | throw new errors.Type(`Cannot shadow ${this.kind} ${id} ` + 88 | `(${vs.value} from ${vs.source}) with ${value} at ${source}`); 89 | } 90 | this.entries.set(id, { 91 | value: value, 92 | source: source 93 | }); 94 | } 95 | } 96 | 97 | class Environment { 98 | constructor(enclosing) { 99 | if (enclosing == undefined) { 100 | this.enclosing = null; 101 | this.types = new EnvironmentMap(null, 'type'); 102 | this.vars = new EnvironmentMap(null, 'variable'); 103 | this.functions = new EnvironmentMap(null, 'function'); 104 | } else { 105 | this.enclosing = enclosing; 106 | this.types = new EnvironmentMap(this.enclosing.types, 'type'); 107 | this.vars = new EnvironmentMap(this.enclosing.vars, 'variable'); 108 | this.functions = new EnvironmentMap(this.enclosing.functions, 'function'); 109 | } 110 | } 111 | 112 | toString() { 113 | return util.inspect(this); 114 | } 115 | 116 | // The following are deprectated. 117 | // Use env.types, env.vars, env.functions directly. 118 | assignType(id, decl) { 119 | return this.types.set(id, decl, 'none'); 120 | } 121 | getType(id) { 122 | return this.types.get(id); 123 | } 124 | getTypeNames() { 125 | return this.types.list(); 126 | } 127 | assignVar(id, decl, source) { 128 | return this.vars.set(id, decl, 'none'); 129 | } 130 | getVar(id) { 131 | return this.vars.get(id); 132 | } 133 | getVarNames() { 134 | return this.vars.list(); 135 | } 136 | } 137 | 138 | class GlobalEnvironment extends Environment { 139 | constructor(enclosing) { 140 | super(enclosing); 141 | this.rules = new EnvironmentMap(null, 'rule'); 142 | this.invariants = new EnvironmentMap(null, 'invariant'); 143 | } 144 | 145 | // The following are deprectated. 146 | // Use env.rules, env.invariants directly. 147 | getRule(id) { 148 | return this.rules.get(id); 149 | } 150 | assignRule(id, decl) { 151 | return this.rules.set(id, decl, 'none'); 152 | } 153 | listRules() { 154 | return this.rules.list(); 155 | } 156 | } 157 | 158 | module.exports = { 159 | Environment: Environment, 160 | GlobalEnvironment: GlobalEnvironment, 161 | }; 162 | -------------------------------------------------------------------------------- /lib/execution.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | class Execution { 12 | constructor(parent, startIndex, firstEvents) { 13 | // invariant: null or Execution 14 | this._parent = parent; 15 | // invariant: If _parent is null, startIndex is 0. Otherwise, ancestors 16 | // contain at least _startIndex > 0 number of events. 17 | this._startIndex = startIndex; 18 | // invariant: non-empty 19 | if (firstEvents instanceof Array) { 20 | this._events = firstEvents; 21 | } else { 22 | this._events = [firstEvents]; 23 | } 24 | } 25 | 26 | size() { 27 | return this._startIndex + this._events.length; 28 | } 29 | 30 | forkStart() { 31 | return new Cursor(this, this._startIndex); 32 | } 33 | 34 | last() { 35 | return new Cursor(this, this._startIndex + this._events.length - 1); 36 | } 37 | 38 | map(cb, _endIndex) { 39 | if (_endIndex === undefined) { 40 | _endIndex = this._startIndex + this._events.length - 1; 41 | } 42 | let result = []; 43 | if (this._startIndex > 0) { 44 | result = this._parent.map(cb, 45 | Math.min(this._startIndex - 1, _endIndex)); 46 | } 47 | for (let i = this._startIndex; i <= _endIndex; ++i) { 48 | result.push(cb(this._events[i - this._startIndex], i)); 49 | } 50 | return result; 51 | } 52 | 53 | 54 | // Like preceding() but returns index. 55 | _precedingIndex(leq, endIndex) { 56 | if (endIndex >= this._startIndex && leq(this._events[0])) { // here 57 | let low = 0; 58 | let high = endIndex - this._startIndex; 59 | let mid = high; // check high end first, pretty common case 60 | while (low < high) { 61 | if (leq(this._events[mid])) { 62 | low = mid; 63 | } else { 64 | high = mid - 1; 65 | } 66 | mid = low + Math.ceil((high - low) / 2); 67 | } 68 | return this._startIndex + low; 69 | } else { 70 | if (this._startIndex > 0) { // parent 71 | return this._parent._precedingIndex(leq, 72 | Math.min(this._startIndex - 1, endIndex)); 73 | } else { // invalid 74 | return undefined; 75 | } 76 | } 77 | } 78 | 79 | // Binary-ish search to find an event in the execution. 80 | // 'leq' should be a function returning true for all events earlier or equal 81 | // to the one being searched for, false for all later events. 82 | // Returns the latest event for which 'leq' returns true. 83 | preceding(leq) { 84 | let index = this._precedingIndex(leq, this._startIndex + this._events.length - 1); 85 | if (index === undefined) { 86 | return undefined; 87 | } else { 88 | return new Cursor(this, index); 89 | } 90 | } 91 | 92 | _at(index) { 93 | if (index < this._startIndex) { 94 | return this._parent._at(index); 95 | } else { 96 | return this._events[index - this._startIndex]; 97 | } 98 | } 99 | 100 | } 101 | 102 | class RootExecution extends Execution { 103 | constructor(start) { 104 | super(null, 0, start); 105 | } 106 | } 107 | 108 | class Cursor { 109 | constructor(execution, index) { 110 | this.execution = execution; 111 | this._index = index; 112 | } 113 | 114 | index() { 115 | return this._index; 116 | } 117 | 118 | equals(other) { 119 | return (this.execution === other.execution && 120 | this._index === other._index); 121 | } 122 | 123 | previous() { 124 | if (this.execution._at(this._index - 1) !== undefined) { 125 | return new Cursor(this.execution, this._index - 1); 126 | } else { 127 | return undefined; 128 | } 129 | } 130 | 131 | next() { 132 | if (this.execution._at(this._index + 1) !== undefined) { 133 | return new Cursor(this.execution, this._index + 1); 134 | } else { 135 | return undefined; 136 | } 137 | } 138 | 139 | parent() { 140 | if (this.execution._parent === null) { 141 | return null; 142 | } else { 143 | return new Cursor(this.execution._parent, 144 | this.execution._startIndex - 1); 145 | } 146 | } 147 | 148 | getEvent() { 149 | return this.execution._at(this._index); 150 | } 151 | 152 | addEvent(event) { 153 | let e = this.execution; 154 | if (this._index < this.execution.last()._index) { // fork 155 | e = new Execution(this.execution, this._index + 1, event); 156 | } else { // add to current execution 157 | e._events.push(event); 158 | } 159 | return new Cursor(e, this._index + 1); 160 | } 161 | 162 | map(cb) { 163 | return this.execution.map(cb, this._index); 164 | } 165 | } 166 | 167 | module.exports = RootExecution; 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Runway Compiler 2 | 3 | [![Build Status](https://travis-ci.org/salesforce/runway-compiler.svg?branch=master)](https://travis-ci.org/salesforce/runway-compiler) 4 | 5 | This is an interpreter for the Runway specification language, which was created 6 | to model distributed and concurrent systems. The interpreter is typically used 7 | as part of [runway-browser](https://github.com/salesforce/runway-browser), 8 | which provides a web-based UI in which you can run interactive visualizations 9 | based on the models. 10 | 11 | (We expect this repo to become a compiler over time, which is why it's called 12 | runway-compiler and not runway-interpreter.) 13 | 14 | 15 | ## Setup 16 | 17 | First make sure you have `node` and `npm` (node package manager) installed. 18 | Clone this repository and run `npm install` within it to get started. 19 | 20 | ### Syntax Highlighting: Vim 21 | 22 | Syntax highlighting for the [Vim](http://www.vim.org) text editor is available 23 | using the syntax file [vim/runway.vim](vim/runway.vim). Copy it into 24 | `~/.vim/syntax/` and set your filetype to `runway` in `~/.vimrc`: 25 | 26 | autocmd BufRead,BufNewFile *.model set filetype=runway 27 | 28 | ### Syntax Highlighting: Atom 29 | 30 | Syntax highlighting for the [Atom](https://atom.io) text editor can be found in 31 | a separate [language-runway](https://github.com/salesforce/language-runway) 32 | repo. 33 | 34 | 35 | ## REPL 36 | 37 | Use the REPL to try out statements and expressions in your console: 38 | 39 | $ node bin/main.js 40 | > 3 * 4 41 | 12 42 | > type Pair : record { first: 0..9, second: 10..99 }; 43 | > var p : Pair; 44 | > p 45 | Pair { first: 0, second: 10 } 46 | > p.first = 20 47 | ModelingBoundsError: Cannot assign value of 20 to range undefined: 0..9; 48 | > p.first = 3 49 | > p 50 | Pair { first: 3, second: 10 } 51 | > type Maybe : either { Nothing, Something { it: 3..5 } } 52 | > var m : Maybe 53 | > m 54 | Nothing 55 | > m = Something { it: 4 } 56 | > m 57 | Something { it: 4 } 58 | > match m { Something as s => { print s.it; }, Nothing => { print False; } } 59 | 4 60 | > m = Nothing 61 | > match m { Something as s => { print s.it; }, Nothing => { print False; } } 62 | False 63 | > 64 | 65 | 66 | ### Running Example in REPL 67 | 68 | You can also load Runway models into the REPL, such as the [Too Many 69 | Bananas](https://github.com/salesforce/runway-model-toomanybananas/) model. 70 | 71 | $ node main.js ~/runway-model-toomanybananas/toomanybananas.model 72 | bananas = 0 73 | notePresent = False 74 | roommates = [1: Happy, 2: Happy, 3: Happy, 4: Happy, 5: Happy] 75 | 76 | Executing step 77 | bananas = 0 78 | notePresent = False 79 | roommates = [1: Hungry, 2: Happy, 3: Happy, 4: Happy, 5: Happy] 80 | 81 | > .fire step 3 82 | bananas = 0 83 | notePresent = False 84 | roommates = [1: Hungry, 2: Happy, 3: Hungry, 4: Happy, 5: Happy] 85 | 86 | > .fire step 3 87 | bananas = 0 88 | notePresent = True 89 | roommates = [1: Hungry, 2: Happy, 3: GoingToStore, 4: Happy, 5: Happy] 90 | 91 | > bananas = 7 92 | > .fire step 3 93 | bananas = 7 94 | notePresent = True 95 | roommates = [1: Hungry, 2: Happy, 3: ReturningFromStore { carrying: 3 }, 4: Happy, 5: Happy] 96 | 97 | > .fire step 3 98 | bananas = 10 99 | notePresent = False 100 | roommates = [1: Hungry, 2: Happy, 3: Hungry, 4: Happy, 5: Happy] 101 | 102 | Note that invariants are not automatically checked in the REPL 103 | (issue [#1](/salesforce/runway-compiler/issues/1)). 104 | 105 | 106 | ## Writing a Model 107 | 108 | You're encouraged to look at existing examples to begin with, such as 109 | [runway-model-toomanybananas](https://github.com/salesforce/runway-model-toomanybananas) 110 | and 111 | [runway-model-elevators](https://github.com/salesforce/runway-model-elevators). 112 | The specification language is documented in 113 | [doc/LANGUAGE-GUIDE.md](doc/LANGUAGE-GUIDE.md), and the most important thing to 114 | note is that most things are pass-by-value (copy semantics), but for loops are 115 | by reference. 116 | 117 | ## Internals 118 | 119 | ### Interpreter 120 | 121 | The lexer+parser ([lib/parser.js](lib/parser.js)) is written using the 122 | [Parsimmon](https://github.com/jneen/parsimmon) library. It outputs a really 123 | big basically JSON parse tree like what you find in 124 | [test-scripts/parser/output-2.json](test-scripts/parser/output-2.json). 125 | Every object in the 126 | parse tree has a "kind" field specifying its type and a "source" field 127 | specifying where it comes from in the input file (for error messages). 128 | 129 | After parsing completes, the entire structure is converting into an AST 130 | (abstract syntax tree). There's mostly a one-to-one mapping between a node in 131 | the parse tree and a node in the AST, but the AST is actual JavaScript objects. 132 | There are two kinds of nodes in the AST: [statements](lib/statements/) and 133 | [expressions](lib/expressions/). These refer to [types](lib/types/) and values 134 | (value classes are defined next to the corresponding type). 135 | 136 | After the AST is set up, `typecheck()` is called on it, which is invoked 137 | through the entire tree (children before parents). Then `execute()` calls the 138 | top-level initialization statements, if any. 139 | 140 | ### Tests 141 | 142 | Run `npm test`. 143 | 144 | Unit tests use the [Mocha](https://mochajs.org/) library. To add a new test 145 | file, place it in the `test/` directory. 146 | 147 | The parser is tested by feeding it a couple of files 148 | (`test-scripts/parser/input*.model`) and automatically checking their parser output 149 | (against `test-scripts/parser/output*.json`). Eventually we'll want more targeted tests 150 | for the parser, but this has worked pretty well so far at making sure there 151 | aren't any regressions. 152 | -------------------------------------------------------------------------------- /bin/main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 4 | * All rights reserved. 5 | * Licensed under the MIT license. 6 | * For full license text, see LICENSE.md file in the repo root or 7 | * https://opensource.org/licenses/MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | let GlobalEnvironment = require('../lib/environment.js').GlobalEnvironment; 13 | let Input = require('../lib/input.js'); 14 | let compiler = require('../lib/compiler.js'); 15 | let errors = require('../lib/errors.js'); 16 | let docopt = require('docopt').docopt; 17 | let fs = require('fs'); 18 | let process = require('process'); 19 | let checker = require('../lib/modelchecker.js').checker; 20 | let Workspace = require('../lib/workspace.js').Workspace; 21 | let Simulator = require('../lib/simulator.js').Simulator; 22 | 23 | let printEnv = (env) => { 24 | env.vars.forEach((value, name) => { 25 | if (value.isConstant !== true) { 26 | console.log(`${name} = ${value}`); 27 | } 28 | }); 29 | console.log(); 30 | }; 31 | 32 | let repl = function(env) { 33 | var readline = require('readline').createInterface({ 34 | input: process.stdin, 35 | output: process.stdout 36 | }); 37 | 38 | let printError = function(error) { 39 | if (error instanceof errors.Base) { // modeling error 40 | console.log(`${error}`); 41 | } else { // JS error 42 | if (error.stack !== undefined) { 43 | console.log(`${error.stack}`); 44 | } else { 45 | console.log(`${error}`); 46 | } 47 | } 48 | }; 49 | 50 | var forgivingLoad = function(input, env) { 51 | let load = (input) => compiler.load(new Input('REPL', input), env); 52 | let rescueAttempts = [ 53 | (input) => load(input + ';'), 54 | (input) => load('print ' + input), 55 | (input) => load('print ' + input + ';'), 56 | ]; 57 | try { 58 | return load(input); 59 | } catch ( originalError ) { 60 | let module = null; 61 | rescueAttempts.forEach((attempt) => { 62 | if (module === null) { 63 | try { 64 | module = attempt(input); 65 | } catch ( uselessError ) { 66 | // do nothing 67 | } 68 | } 69 | }); 70 | if (module === null) { 71 | throw originalError; 72 | } else { 73 | return module; 74 | } 75 | } 76 | }; 77 | 78 | let context = {}; 79 | var loop = function() { 80 | let processInput = function(input) { 81 | if (input.endsWith('\\')) { 82 | readline.question('... ', (more) => processInput(input.slice(0, -1) + more)); 83 | return; 84 | } else if (input == '.types') { 85 | console.log(`${env.getTypeNames().join(' ')}`); 86 | } else if (input == '.vars') { 87 | console.log(`${env.getVarNames().join(' ')}`); 88 | } else if (input.startsWith('.js')) { 89 | try { 90 | eval(input.slice(3)); 91 | } catch ( e ) { 92 | printError(e); 93 | } 94 | } else if (input == 'exit') { 95 | readline.close(); 96 | return; 97 | } else if (input.startsWith('.fire')) { 98 | let args = input.split(' '); 99 | try { 100 | if (args.length == 2) { 101 | env.getRule(args[1]).fire(context); 102 | printEnv(env); 103 | } else if (args.length == 3) { 104 | env.getRule(args[1]).fire(Number(args[2]), context); 105 | printEnv(env); 106 | } else { 107 | console.log('huh?'); 108 | } 109 | } catch ( e ) { 110 | printError(e); 111 | } 112 | } else if (input[0] == '.') { 113 | console.log('huh?'); 114 | } else { 115 | try { 116 | let module = forgivingLoad(input, env); 117 | module.ast.execute(context); 118 | } catch ( e ) { 119 | printError(e); 120 | } 121 | } 122 | loop(); 123 | }; 124 | readline.question('> ', processInput); 125 | }; 126 | loop(); 127 | }; 128 | 129 | let readFile = (filename) => fs.readFileSync(filename).toString(); 130 | 131 | module.exports = { 132 | repl: repl, 133 | }; 134 | 135 | if (require.main === module) { 136 | let doc = ` 137 | Usage: main.js [options] [] 138 | main.js check [options] 139 | main.js simulate [options] 140 | 141 | Options: 142 | -a, --async Do not use the clock: past(n) always returns true. 143 | Note that the model checker never uses the clock. 144 | -h, --help Show this usage message.`; 145 | 146 | let usageError = () => { 147 | console.log(doc); 148 | process.exit(1); 149 | }; 150 | 151 | let options = docopt(doc); 152 | //console.log('Options:', options); 153 | if (options[''] == 'check' || 154 | options[''] == 'simulate') { 155 | usageError(); 156 | } 157 | 158 | let prelude = compiler.loadPrelude(readFile('lib/prelude.model'), { 159 | clock: !options['--async'], 160 | }); 161 | let env = new GlobalEnvironment(prelude.env); 162 | 163 | let module; 164 | if (options['']) { 165 | let filename = options['']; 166 | module = compiler.load(new Input(filename, readFile(filename)), env); 167 | let context = { 168 | clock: 0, 169 | }; 170 | module.ast.execute(context); 171 | } 172 | 173 | if (options.simulate) { 174 | let workspace = new Workspace(module); 175 | let simulator = new Simulator(module, workspace); 176 | let i = 0; 177 | for (;;) { 178 | process.stdout.write(`${i}: `); 179 | let ok = simulator.step(i); 180 | process.stdout.write(`${workspace.cursor.getEvent().msg} at clock ${workspace.clock}\n`); 181 | if (!ok) { 182 | break; 183 | } 184 | i += 1; 185 | } 186 | printEnv(env); 187 | } else if (options.check) { 188 | checker(module); 189 | } else { // repl 190 | printEnv(env); 191 | repl(env); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/types/either.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let errors = require('../errors.js'); 12 | let Type = require('./type.js'); 13 | let Value = require('./value.js'); 14 | let makeType = require('./factory.js'); 15 | 16 | // An instance of an EitherVariant. 17 | // 'eithertype' is the EitherType and doesn't change, 18 | // 'varianttype' is the current EitherVariant. 19 | // 'type' is eithertype unless the Variant is known statically, then it's varianttype. 20 | // If the variant has record fields, those will be in an attribute named 21 | // 'fields'; otherwise, it'll be set to undefined. 22 | class EitherValue extends Value { 23 | constructor(type, eithertype, varianttype) { 24 | super(type); 25 | this.eithertype = eithertype; 26 | this.varianttype = varianttype; 27 | if (this.varianttype.recordtype === null) { 28 | this.fields = undefined; 29 | } else { 30 | this.fields = this.varianttype.recordtype.makeDefaultValue(); 31 | } 32 | } 33 | 34 | // call this from JS as follows: 35 | // let x = thing.match({ 36 | // Variant1: (t) => 1, 37 | // Variant2: (t) => { return t.two + 2; }, 38 | // }); 39 | match(variants) { 40 | let fn = variants[this.varianttype.name]; 41 | if (fn === undefined) { 42 | return undefined; 43 | } 44 | if (typeof fn === 'function') { 45 | return fn(this.fields); 46 | } else { // maybe it's an expression 47 | return fn; 48 | } 49 | } 50 | 51 | assign(newValue) { 52 | let ok = false; 53 | if (this.type == this.eithertype) { 54 | ok = (newValue instanceof EitherValue && 55 | this.eithertype == newValue.eithertype); 56 | } else { // this.type == this.varianttype 57 | ok = (newValue instanceof EitherValue && 58 | this.varianttype == newValue.varianttype); 59 | } 60 | if (ok) { 61 | this.varianttype = newValue.varianttype; 62 | if (this.varianttype.recordtype === null) { 63 | this.fields = undefined; 64 | } else { 65 | this.fields = this.varianttype.recordtype.makeDefaultValue(); 66 | this.fields.assign(newValue.fields); 67 | } 68 | } else { 69 | throw new errors.Internal(`Cannot assign value of ${newValue} to ` + 70 | `either-type ${this.type.getName()}`); 71 | } 72 | } 73 | 74 | lookup(name) { 75 | return this.fields.lookup(name); 76 | } 77 | 78 | set(name, value) { 79 | return this.fields.set(name, value); 80 | } 81 | 82 | equals(other) { 83 | if (this.varianttype != other.varianttype) { 84 | return false; 85 | } 86 | if (this.fields !== undefined) { 87 | return this.fields.equals(other.fields); 88 | } 89 | return true; 90 | } 91 | 92 | innerToString() { 93 | if (this.fields !== undefined) { 94 | return this.fields.toString(); 95 | } else { 96 | return this.varianttype.name; 97 | } 98 | } 99 | 100 | toString() { 101 | if (this.fields !== undefined) { 102 | return `${this.varianttype.name} { ${this.fields.innerToString()} }`; 103 | } else { 104 | return `${this.varianttype.name}`; 105 | } 106 | } 107 | 108 | assignJSON(spec) { 109 | if (typeof spec === 'string') { 110 | this.assign( 111 | this.eithertype.getVariant(spec) 112 | .makeDefaultValue()); 113 | } else { 114 | this.assign( 115 | this.eithertype.getVariant(spec.tag) 116 | .makeDefaultValue()); 117 | this.fields.assignJSON(spec.fields); 118 | } 119 | } 120 | 121 | toJSON() { 122 | if (this.fields !== undefined) { 123 | let o = {}; 124 | o.tag = this.varianttype.name; 125 | o.fields = this.fields.toJSON(); 126 | return o; 127 | } else { 128 | return this.varianttype.name; 129 | } 130 | } 131 | } 132 | 133 | // In: 134 | // type T: either { A, B } 135 | // this represents an A or a B, and its parenttype is T. 136 | // Sometimes we know statically that we have an A or a B. 137 | class EitherVariant extends Type { 138 | constructor(decl, env, name, parenttype) { 139 | super(decl, env, name); 140 | this.parenttype = parenttype; 141 | if (this.decl.kind == 'enumvariant') { 142 | this.recordtype = null; 143 | let constant = new EitherValue(this, this.parenttype, this); 144 | constant.isConstant = true; 145 | this.env.vars.set(this.name, constant, this.decl.id.source); 146 | } else { 147 | this.recordtype = makeType.make(decl.type, this.env, this.name); 148 | this.env.assignType(this.name, this); 149 | } 150 | } 151 | 152 | equals(other) { 153 | return this === other; 154 | } 155 | 156 | makeDefaultValue() { 157 | return new EitherValue(this, this.parenttype, this); 158 | } 159 | 160 | fieldType(name) { 161 | return this.recordtype.fieldType(name); 162 | } 163 | 164 | toString() { 165 | return `${this.name} (EitherVariant)`; 166 | } 167 | } 168 | 169 | // The type T in: 170 | // type T: either { A, B } 171 | // An EitherType is made up of a set of EitherVariant types (A and B in this 172 | // example). 173 | class EitherType extends Type { 174 | constructor(decl, env, name) { 175 | super(decl, env, name); 176 | this.variants = this.decl.fields.map( 177 | (field) => new EitherVariant(field, this.env, field.id.value, this) 178 | ); 179 | } 180 | 181 | equals(other) { 182 | return this === other; 183 | } 184 | 185 | getVariant(tag) { 186 | let variant = undefined; 187 | this.variants.forEach((v) => { 188 | if (v.name == tag) { 189 | variant = v; 190 | } 191 | }); 192 | return variant; 193 | } 194 | 195 | makeDefaultValue() { 196 | return new EitherValue(this, this, this.variants[0]); 197 | } 198 | 199 | toString() { 200 | let name = this.getName(); 201 | if (name !== undefined) { 202 | return name; 203 | } 204 | return `anonymous either (${this.decl.source})`; 205 | } 206 | } 207 | 208 | module.exports = { 209 | Variant: EitherVariant, 210 | Type: EitherType, 211 | }; 212 | -------------------------------------------------------------------------------- /doc/JAVASCRIPT-API.md: -------------------------------------------------------------------------------- 1 | JavaScript is used to create visual and interactive web pages of models. Thus, 2 | JavaScript will need to access the model state and occasionally manipulate it. 3 | 4 | Note that this API is subject to change. 5 | 6 | ## Environment 7 | 8 | Get a variable named `bananas` with `model.env.vars.get('bananas')`. 9 | Note that you shouldn't keep this object around. Look it up again later. 10 | 11 | Each variable has: 12 | - `.toString()` with a human-readable string representation 13 | - `.toJSON()` with a JSON dump 14 | - `.assign(other)` to assign a different value 15 | - `.type.createDefaultValue()` to return a new empty value 16 | 17 | ## Types 18 | 19 | ### Ranges 20 | 21 | > b = model.env.vars.get('bananas'); 22 | > b.value 23 | 0 24 | > b.assign(3); // accepts JavaScript Number for convenience 25 | > b.value 26 | 3 27 | > b.toString() 28 | "3" 29 | > b.toJSON() 30 | 3 31 | 32 | ### Records 33 | 34 | Using the following model: 35 | 36 | type Scores : record { 37 | home: 0..99, 38 | away: 0..99, 39 | } 40 | var scores : Scores; 41 | 42 | We can access it from JavaScript with: 43 | 44 | > scores = model.env.vars.get('scores') 45 | > scores.toString() 46 | "Scores { home: 0, away: 0 }" 47 | > scores.toJSON() 48 | { 49 | "home": 0, 50 | "away": 0 51 | } 52 | 53 | The method `lookup` takes a field name and returns another variable. 54 | 55 | > scores.lookup('home').toString() 56 | "0" 57 | 58 | ### Either 59 | 60 | 61 | #### Enum-Like Either Types 62 | 63 | Using the following model: 64 | 65 | type TrafficLight : either { Green, Yellow, Red }; 66 | var light : TrafficLight; 67 | 68 | We can access it from JavaScript with: 69 | 70 | > light = module.ast.env.vars.get('light') 71 | > light.toString() 72 | "Green" 73 | > light.toJSON() 74 | "Green" 75 | > light.varianttype.name 76 | "Green" 77 | > light.match({ Green: 1, Yellow: 2, Red: 3}) 78 | 1 79 | > light.match({ Green: () => 1, Yellow: () => 2, Red: () => 3}) 80 | 1 81 | 82 | Assignment is tedious (TODO: make the first one work): 83 | 84 | > light.assign('Yellow') 85 | Uncaught Error: Cannot assign value of Yellow to either-type TrafficLight 86 | > light.assign(light.type.getVariant('Red').makeDefaultValue()) 87 | > light.toString() 88 | "Red" 89 | 90 | #### Compound Either Types 91 | 92 | Using the following model: 93 | 94 | type Person : either { 95 | Dead, 96 | Alive { 97 | heartRate: 1..200, 98 | asleep: Boolean, 99 | }, 100 | }; 101 | var dead : Person = Dead; 102 | var alive : Person = Alive { heartRate: 80, asleep: True }; 103 | 104 | We can access it from JavaScript with: 105 | 106 | > dead = module.ast.env.vars.get('dead') 107 | > alive = module.ast.env.vars.get('alive') 108 | > dead.toString() 109 | "Dead" 110 | > dead.toJSON() 111 | "Dead" 112 | > alive.toString() 113 | "Alive { heartRate: 80, asleep: True }" 114 | > alive.toJSON() 115 | { 116 | "tag": "Alive", 117 | "fields": { 118 | "heartRate": 80, 119 | "asleep": "True" 120 | } 121 | } 122 | > alive.match({ 123 | . Dead: 0, 124 | . Alive: details => details.heartRate, 125 | . }).toString() 126 | "80" 127 | 128 | Assignment is also tedious: 129 | 130 | > dead.assign(dead.type.getVariant('Alive').makeDefaultValue()) 131 | > dead.lookup('heartRate').assign(10) 132 | 133 | Don't use `.lookup()` unless you know what variant the value is (TODO: make 134 | this always return `undefined`). 135 | 136 | 137 | ### Collections 138 | 139 | .index 140 | .forEach 141 | 142 | #### Arrays 143 | 144 | > rm = module.ast.env.vars.get('roommates') 145 | > rm.toString() 146 | "[1: Happy, 2: Happy, 3: Happy, 4: Happy, 5: Happy]" 147 | > rm.toJSON() 148 | [ 149 | [ 150 | 1, 151 | "Happy" 152 | ], 153 | [ 154 | 2, 155 | "Happy" 156 | ], 157 | [ 158 | 3, 159 | "Happy" 160 | ], 161 | [ 162 | 4, 163 | "Happy" 164 | ], 165 | [ 166 | 5, 167 | "Happy" 168 | ] 169 | ] 170 | > rm.index(2).toString() 171 | "Happy" 172 | > rm.size() 173 | 5 174 | > rm.capacity() 175 | 5 176 | > rm.forEach((v, i) => console.log(v.toString(), i)) 177 | Happy 1 178 | Happy 2 179 | Happy 3 180 | Happy 4 181 | Happy 5 182 | 183 | #### Sets 184 | 185 | Using the following model: 186 | 187 | var bools : Set[3..5]; 188 | 189 | We can access it from JavaScript with: 190 | 191 | > bools = module.ast.env.vars.get('bools') 192 | > bools.toString() 193 | "{}" 194 | > bools.toJSON() 195 | [] 196 | > bools.push(module.ast.env.vars.get('True')) 197 | > bools.toJSON() 198 | ["True"] 199 | > bools.toString() 200 | "{True}" 201 | > bools.push(module.ast.env.vars.get('False')) 202 | > bools.toString() 203 | "{False, True}" 204 | > bools.toJSON() 205 | ["False", "True"] 206 | > bools.index(3).toString() 207 | "True" 208 | > bools.index(4).toString() 209 | "False" 210 | > bools.index(5).toString() 211 | Uncaught Error: Cannot access index 5 of {3: True, 4: False} 212 | > bools.size() 213 | 2 214 | > bools.capacity() 215 | 3 216 | > bools.contains(module.ast.env.vars.get('False')) 217 | true 218 | > bools.empty() 219 | false 220 | > bools.full() 221 | false 222 | 223 | #### Ordered Sets 224 | 225 | Using the following model: 226 | 227 | var bools : OrderedSet[3..5]; 228 | 229 | We can access it from JavaScript with: 230 | 231 | > bools = module.ast.env.vars.get('bools') 232 | > bools.toString() 233 | "{}" 234 | > bools.toJSON() 235 | [] 236 | > bools.push(module.ast.env.vars.get('True')) 237 | > bools.toJSON() 238 | [[3, "True"]] 239 | > bools.toString() 240 | "{3: True}" 241 | > bools.push(module.ast.env.vars.get('False')) 242 | > bools.toString() 243 | "{3: True, 4: False}" 244 | > bools.toJSON() 245 | [[3, "True"], [4, "False"]] 246 | > bools.index(3).toString() 247 | "True" 248 | > bools.index(4).toString() 249 | "False" 250 | > bools.index(5).toString() 251 | Uncaught Error: Cannot access index 5 of {3: True, 4: False} 252 | > bools.size() 253 | 2 254 | > bools.capacity() 255 | 3 256 | > bools.contains(module.ast.env.vars.get('False')) 257 | true 258 | > bools.empty() 259 | false 260 | > bools.full() 261 | false 262 | 263 | When removing items, the remaining ones get renumbered: 264 | 265 | > bools.remove(bools.index(3)) 266 | true 267 | > bools.toString() 268 | "{3: False}" 269 | -------------------------------------------------------------------------------- /test/expressions/apply.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let assert = require('assert'); 12 | let testing = require('../../lib/testing.js'); 13 | 14 | describe('expressions/apply.js', function() { 15 | describe('apply', function() { 16 | 17 | it('basic eq', function() { 18 | let module = testing.run(` 19 | var t1 : Boolean = False == False; 20 | var f1 : Boolean = False == True; 21 | var t2 : Boolean = True != False; 22 | var f2 : Boolean = False != False; 23 | `); 24 | assert.equal(module.env.getVar('t1').toString(), 'True'); 25 | assert.equal(module.env.getVar('f1').toString(), 'False'); 26 | assert.equal(module.env.getVar('t2').toString(), 'True'); 27 | assert.equal(module.env.getVar('f2').toString(), 'False'); 28 | }); 29 | 30 | it('basic cmp', function() { 31 | let module = testing.run(` 32 | var t1 : Boolean = 3 < 4; 33 | var f1 : Boolean = 3 < 3; 34 | var t2 : Boolean = 3 <= 3; 35 | var f2 : Boolean = 3 <= 2; 36 | var t3 : Boolean = 3 >= 3; 37 | var f3 : Boolean = 3 >= 4; 38 | var t4 : Boolean = 3 > 2; 39 | var f4 : Boolean = 3 > 3; 40 | `); 41 | assert.equal(module.env.getVar('t1').toString(), 'True'); 42 | assert.equal(module.env.getVar('f1').toString(), 'False'); 43 | assert.equal(module.env.getVar('t2').toString(), 'True'); 44 | assert.equal(module.env.getVar('f2').toString(), 'False'); 45 | assert.equal(module.env.getVar('t3').toString(), 'True'); 46 | assert.equal(module.env.getVar('f3').toString(), 'False'); 47 | assert.equal(module.env.getVar('t4').toString(), 'True'); 48 | assert.equal(module.env.getVar('f4').toString(), 'False'); 49 | }); 50 | 51 | it('basic arithmetic', function() { 52 | let module = testing.run(` 53 | var a : 0..99 = 3 + 4; 54 | var b : 0..99 = 3 - 1; 55 | var c : 0..99 = 3 * 3; 56 | `); 57 | assert.equal(module.env.getVar('a').toString(), '7'); 58 | assert.equal(module.env.getVar('b').toString(), '2'); 59 | assert.equal(module.env.getVar('c').toString(), '9'); 60 | }); 61 | 62 | it('negation', function() { 63 | let module = testing.run(` 64 | var a : Boolean = !False; 65 | var b : Boolean = !True; 66 | var c : Boolean = !(3 < 4); 67 | var d : Boolean = !(3 > 4); 68 | `); 69 | assert.equal(module.env.getVar('a').toString(), 'True'); 70 | assert.equal(module.env.getVar('b').toString(), 'False'); 71 | assert.equal(module.env.getVar('c').toString(), 'False'); 72 | assert.equal(module.env.getVar('d').toString(), 'True'); 73 | }); 74 | 75 | it('equals', function() { 76 | let module = testing.run(` 77 | var ff : Boolean = False == False; 78 | var ft : Boolean = False == True; 79 | var tf : Boolean = True == False; 80 | var tt : Boolean = True == True; 81 | `); 82 | assert.equal(module.env.getVar('ff').toString(), 'True'); 83 | assert.equal(module.env.getVar('ft').toString(), 'False'); 84 | assert.equal(module.env.getVar('tf').toString(), 'False'); 85 | assert.equal(module.env.getVar('tt').toString(), 'True'); 86 | }); 87 | 88 | it('equals complex', function() { 89 | let module = testing.run(` 90 | var ft : Boolean = False == (False == False); 91 | `); 92 | assert.equal(module.env.getVar('ft').toString(), 'False'); 93 | }); 94 | 95 | it('precedence', function() { 96 | let module = testing.run(` 97 | var a : 0..99 = 3 + 4 * 5; 98 | var b : 0..99 = 3 * 4 + 5; 99 | var c : Boolean = 3 * 4 > 5; 100 | var d : Boolean = 5 > 3 * 4; 101 | var e : Boolean = 3 < 4 == True; 102 | var f : Boolean = True != 3 < 4; 103 | `); 104 | assert.equal(module.env.getVar('a').toString(), '23'); 105 | assert.equal(module.env.getVar('b').toString(), '17'); 106 | assert.equal(module.env.getVar('c').toString(), 'True'); 107 | assert.equal(module.env.getVar('d').toString(), 'False'); 108 | assert.equal(module.env.getVar('e').toString(), 'True'); 109 | assert.equal(module.env.getVar('f').toString(), 'False'); 110 | }); 111 | 112 | it('function', function() { 113 | let module = testing.run(` 114 | var x : 1..1024 = pow(2, 3); 115 | `); 116 | assert.equal(module.env.getVar('x').toString(), '8'); 117 | }); 118 | 119 | it('&& basic', function() { 120 | let module = testing.run(` 121 | var x1 : Boolean = False && False; 122 | var x2 : Boolean = False && True; 123 | var x3 : Boolean = True && False; 124 | var x4 : Boolean = True && True; 125 | `); 126 | assert.equal(module.env.getVar('x1').toString(), 'False'); 127 | assert.equal(module.env.getVar('x2').toString(), 'False'); 128 | assert.equal(module.env.getVar('x3').toString(), 'False'); 129 | assert.equal(module.env.getVar('x4').toString(), 'True'); 130 | }); 131 | 132 | it('|| basic', function() { 133 | let module = testing.run(` 134 | var x1 : Boolean = False || False; 135 | var x2 : Boolean = False || True; 136 | var x3 : Boolean = True || False; 137 | var x4 : Boolean = True || True; 138 | `); 139 | assert.equal(module.env.getVar('x1').toString(), 'False'); 140 | assert.equal(module.env.getVar('x2').toString(), 'True'); 141 | assert.equal(module.env.getVar('x3').toString(), 'True'); 142 | assert.equal(module.env.getVar('x4').toString(), 'True'); 143 | }); 144 | 145 | it('&& || precedence', function() { 146 | let module = testing.run(` 147 | var x1 : Boolean = False && False || True; 148 | var x2 : Boolean = True || True && False; 149 | `); 150 | assert.equal(module.env.getVar('x1').toString(), 'True'); 151 | assert.equal(module.env.getVar('x2').toString(), 'True'); 152 | }); 153 | 154 | it('&& || short-cirtuit', function() { 155 | let module = testing.run(` 156 | type Digit : 0..9; 157 | var set : OrderedSet[Digit]; 158 | push(set, 0); 159 | push(set, 1); 160 | push(set, 2); 161 | push(set, 3); 162 | var x1 : Boolean = False && pop(set) == 0; 163 | var x2 : Boolean = True || pop(set) == 0; 164 | `); 165 | assert.equal(module.env.getVar('x1').toString(), 'False'); 166 | assert.equal(module.env.getVar('x2').toString(), 'True'); 167 | assert.equal(module.env.getVar('set').toString(), 168 | '{0: 0, 1: 1, 2: 2, 3: 3}'); 169 | }); 170 | 171 | 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /lib/types/varlen.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let varlen = {}; 12 | module.exports = varlen; 13 | 14 | let makeType = require('./factory.js'); 15 | let Type = require('./type.js'); 16 | let Value = require('./value.js'); 17 | let errors = require('../errors.js'); 18 | let NumberValue = require('./number.js'); 19 | let RangeValue = require('./range.js'); 20 | 21 | class VarLenBaseValue extends Value { 22 | constructor(type) { 23 | super(type); 24 | let length = this.type.indextype.high - this.type.indextype.low + 1; 25 | this.items = Array.from({ 26 | length: length 27 | }, 28 | () => this.type.valuetype.makeDefaultValue()); 29 | this.used = 0; 30 | } 31 | usedItems() { 32 | return this.items.filter((v, i) => i < this.used); 33 | } 34 | 35 | 36 | index(i) { 37 | if (i instanceof NumberValue.Value || i instanceof RangeValue.Value) { 38 | i = i.value; 39 | } 40 | if (typeof i !== 'number') { 41 | throw new errors.Internal(`Trying to index array with ${i}`); 42 | } 43 | if (i < this.type.indextype.low || i > this.type.indextype.low + this.used - 1) { 44 | throw new errors.Bounds(`Cannot access index ${i} of ${this}`); 45 | } 46 | let v = this.items[i - this.type.indextype.low]; 47 | if (v == undefined) { 48 | throw new errors.Internal(`Bounds check failed to catch issue: ${i}`); 49 | } 50 | return v; 51 | } 52 | forEach(cb) { 53 | this.usedItems().forEach((v, i) => cb(v, this.type.indextype.low + i)); 54 | } 55 | map(cb) { 56 | return this.usedItems().map((v, i) => cb(v, this.type.indextype.low + i)); 57 | } 58 | contains(v) { 59 | return this.indexOf(v) !== null; 60 | } 61 | indexOf(v) { 62 | let ret = null; 63 | this.forEach((x, i) => { 64 | if (x.equals(v)) { 65 | ret = this.type.indextype.low + i; 66 | } 67 | }); 68 | return ret; 69 | } 70 | empty() { 71 | return this.used == 0; 72 | } 73 | full() { 74 | return this.used == this.type.indextype.high - this.type.indextype.low + 1; 75 | } 76 | size() { 77 | return this.used; 78 | } 79 | capacity() { 80 | return this.items.length; 81 | } 82 | 83 | equals(other) { 84 | if (this.used != other.used) { 85 | return false; 86 | } 87 | let allEqual = true; 88 | this.forEach((v, i) => { 89 | if (!v.equals(other.index(i))) { 90 | allEqual = false; 91 | } 92 | }); 93 | return allEqual; 94 | } 95 | 96 | toString() { 97 | let inner = this.usedItems().map((v, i) => { 98 | return `${this.type.indextype.low + i}: ${v.toString()}`; 99 | }).join(', '); 100 | return `{${inner}}`; 101 | } 102 | toJSON() { 103 | return this.usedItems().map((v, i) => 104 | [this.type.indextype.low + i, v.toJSON()]); 105 | } 106 | 107 | 108 | push(v) { 109 | if (this.full()) { 110 | throw new errors.Bounds(`Cannot push onto ${this}`); 111 | } 112 | this.items[this.used].assign(v); 113 | this.used += 1; 114 | } 115 | pop() { 116 | if (this.used == 0) { 117 | throw new errors.Bounds(`Cannot pop from empty ${this}`); 118 | } 119 | this.used -= 1; 120 | let removed = this.items[this.used]; 121 | this.items[this.used] = this.type.valuetype.makeDefaultValue(); 122 | return removed; 123 | } 124 | remove(v) { 125 | let index = this.usedItems().findIndex(item => item.equals(v)); 126 | if (index >= 0) { 127 | this.items = this.items.slice(0, index) 128 | .concat(this.items.slice(index + 1)) 129 | .concat([this.type.valuetype.makeDefaultValue()]); 130 | this.used -= 1; 131 | return true; 132 | } 133 | return false; 134 | } 135 | 136 | assign(other) { 137 | this.used = 0; 138 | other.forEach(v => this.push(v)); 139 | } 140 | assignJSON(spec) { 141 | this.used = 0; 142 | spec.forEach(x => { 143 | this.items[this.used].assignJSON(x[1]); 144 | this.used += 1; 145 | }); 146 | } 147 | } 148 | 149 | let insertOrdered = function(v) { 150 | if (this.full()) { 151 | throw new errors.Bounds(`Cannot push onto ${this}`); 152 | } 153 | let cmpkey = v => JSON.stringify(v.toJSON()); 154 | let value = this.type.valuetype.makeDefaultValue(); 155 | value.assign(v); 156 | let inskey = cmpkey(value); 157 | let index = 0; 158 | 159 | this.forEach((v, i) => { 160 | if (inskey >= cmpkey(v)) { 161 | index += 1; 162 | } 163 | }); 164 | this.items = this.items.slice(0, index) 165 | .concat(value) 166 | .concat(this.items.slice(index, this.items.length - 1)); 167 | this.used += 1; 168 | }; 169 | 170 | class SetValue extends VarLenBaseValue { 171 | push(v) { 172 | if (!this.contains(v)) { 173 | insertOrdered.call(this, v); 174 | } 175 | } 176 | } 177 | class MultiSetValue extends VarLenBaseValue { 178 | push(v) { 179 | insertOrdered.call(this, v); 180 | } 181 | } 182 | 183 | class OrderedSetValue extends VarLenBaseValue { 184 | push(v) { 185 | if (!this.contains(v)) { 186 | super.push(v); 187 | } 188 | } 189 | } 190 | 191 | class VectorValue extends VarLenBaseValue { 192 | } 193 | 194 | 195 | class VarLenBaseType extends Type { 196 | constructor(decl, env, name) { 197 | super(decl, env, name); 198 | this.valuetype = makeType.make(this.decl.args[0], this.env); 199 | this.indextype = makeType.make(this.decl.indexBy, this.env); 200 | } 201 | equals(other) { 202 | return (this.constructor === other.constructor && 203 | this.valuetype.equals(other.valuetype) && 204 | this.indextype.equals(other.indextype)); 205 | 206 | } 207 | } 208 | 209 | class SetType extends VarLenBaseType { 210 | makeDefaultValue() { 211 | return new SetValue(this); 212 | } 213 | toString() { 214 | return `Set<${this.valuetype}>[${this.indextype}]`; 215 | } 216 | } 217 | 218 | class MultiSetType extends VarLenBaseType { 219 | makeDefaultValue() { 220 | return new MultiSetValue(this); 221 | } 222 | toString() { 223 | return `MultiSet<${this.valuetype}>[${this.indextype}]`; 224 | } 225 | } 226 | 227 | class OrderedSetType extends VarLenBaseType { 228 | makeDefaultValue() { 229 | return new OrderedSetValue(this); 230 | } 231 | toString() { 232 | return `OrderedSet<${this.valuetype}>[${this.indextype}]`; 233 | } 234 | } 235 | 236 | class VectorType extends VarLenBaseType { 237 | makeDefaultValue() { 238 | return new VectorValue(this); 239 | } 240 | toString() { 241 | return `Vector<${this.valuetype}>[${this.indextype}]`; 242 | } 243 | } 244 | 245 | varlen.SetType = SetType; 246 | varlen.SetValue = SetValue; 247 | varlen.MultiSetType = MultiSetType; 248 | varlen.MultiSetValue = MultiSetValue; 249 | varlen.OrderedSetType = OrderedSetType; 250 | varlen.OrderedSetValue = OrderedSetValue; 251 | varlen.VectorType = VectorType; 252 | varlen.VectorValue = VectorValue; 253 | -------------------------------------------------------------------------------- /lib/workspace.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2016, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * Licensed under the MIT license. 5 | * For full license text, see LICENSE.md file in the repo root or 6 | * https://opensource.org/licenses/MIT 7 | */ 8 | 9 | 'use strict'; 10 | 11 | let Changesets = require('./changesets.js'); 12 | let Execution = require('./execution.js'); 13 | let errors = require('./errors.js'); 14 | let RuleFor = require('./statements/rulefor.js'); 15 | let PubSub = require('./pubsub.js'); 16 | 17 | class Invariant { 18 | constructor(workspace, name, source) { 19 | this.workspace = workspace; 20 | this.name = name; 21 | this.source = source; 22 | // if false, checking the invariant is a waste of time 23 | this.active = true; 24 | // if !active, a change in one of these variables will 25 | // make the invariant active 26 | this.readset = null; 27 | } 28 | 29 | check() { 30 | if (!this.active) { 31 | return false; 32 | } 33 | let econtext = { 34 | readset: new Set(), 35 | clock: this.clock, 36 | }; 37 | this.source.check(econtext); // throws 38 | this.readset = econtext.readset; 39 | } 40 | 41 | reportChanges(changes) { 42 | if (this.active) { 43 | return; 44 | } 45 | if (Changesets.affected(changes, this.readset)) { 46 | this.active = true; 47 | this.readset = null; 48 | } 49 | } 50 | } 51 | 52 | class Rule { 53 | constructor(workspace, name, _fire) { 54 | this.ACTIVE = 1; // firing the rule would change the state 55 | this.INACTIVE = 2; // firing the rule might change the state 56 | this.UNKNOWN = 3; // firing the rule would not change the state 57 | 58 | this.workspace = workspace; 59 | this.name = name; 60 | this._fire = _fire; 61 | this._unknown(); 62 | } 63 | 64 | _unknown() { 65 | this.active = this.UNKNOWN; 66 | this.readset = null; // valid when INACTIVE or ACTIVE 67 | this.nextWake = null; // valid when INACTIVE 68 | this.changeset = null; // valid when INACTIVE ([]) or ACTIVE 69 | } 70 | 71 | getNextWake() { 72 | if (this.active == this.INACTIVE) { 73 | return this.nextWake; 74 | } else { 75 | throw new errors.Internal('May only call getNextWake() on INACTIVE rule'); 76 | } 77 | } 78 | 79 | fire() { 80 | if (this.active == this.INACTIVE) { 81 | return this.changeset; 82 | } 83 | let econtext = { 84 | readset: new Set(), 85 | clock: this.workspace.clock, 86 | nextWake: Number.MAX_VALUE, 87 | output: this.workspace._output, 88 | }; 89 | let changes = this.workspace.tryChangeState(() => { 90 | this._fire(econtext); 91 | return this.name; 92 | }); 93 | if (Changesets.empty(changes)) { 94 | this.active = this.INACTIVE; 95 | this.readset = econtext.readset; 96 | this.nextWake = econtext.nextWake; 97 | this.changeset = changes; 98 | } else { 99 | this._unknown(); 100 | } 101 | return changes; 102 | } 103 | 104 | wouldChangeState() { 105 | if (this.active === this.ACTIVE || 106 | this.active === this.INACTIVE) { 107 | return this.changeset; 108 | } else { 109 | //console.log(`checking if ${this.name} would change state`); 110 | let econtext = { 111 | readset: new Set(), 112 | clock: this.workspace.clock, 113 | nextWake: Number.MAX_VALUE, 114 | }; 115 | let changes = this.workspace.wouldChangeState(() => { 116 | this._fire(econtext); 117 | }); 118 | if (Changesets.empty(changes)) { 119 | this.active = this.INACTIVE; 120 | this.readset = econtext.readset; 121 | this.nextWake = econtext.nextWake; 122 | this.changeset = changes; 123 | } else { 124 | this.active = this.ACTIVE; 125 | this.readset = econtext.readset; 126 | this.nextWake = null; 127 | this.changeset = changes; 128 | } 129 | return changes; 130 | } 131 | } 132 | 133 | reportChanges(changes) { 134 | if (this.active === this.ACTIVE) { 135 | if (Changesets.affected(changes, this.readset)) { 136 | this._unknown(); 137 | } 138 | } else if (this.active === this.INACTIVE) { 139 | if (Changesets.affected(changes, this.readset) || 140 | this.nextWake <= this.workspace.clock) { 141 | this._unknown(); 142 | } 143 | } 144 | } 145 | } 146 | 147 | class MultiRuleSet { 148 | constructor(workspace, source, name) { 149 | this.workspace = workspace; 150 | this.source = source; 151 | this.name = name; 152 | this.rulefor = true; 153 | this._update(); 154 | } 155 | _update() { 156 | let econtext = { 157 | readset: new Set(), 158 | clock: this.workspace.clock, 159 | }; 160 | this.rules = this.source.enumerate(econtext).map(indexes => 161 | new Rule(this.workspace, `${this.name}(${indexes.join(', ')})`, 162 | econtext => this.source.fire(indexes, econtext))); 163 | this.readset = econtext.readset; 164 | } 165 | reportChanges(changes) { 166 | // note that rule-for cannot access the clock for now (syntactically 167 | // prohibited in the modeling language) 168 | if (Changesets.affected(changes, this.readset)) { 169 | this._update(); 170 | } else { 171 | this.rules.forEach(rule => rule.reportChanges(changes)); 172 | } 173 | } 174 | } 175 | 176 | class SingleRuleSet { 177 | constructor(workspace, source, name) { 178 | this.workspace = workspace; 179 | this.source = source; 180 | this.name = name; 181 | this.rulefor = false; 182 | this.readset = []; 183 | this.rule = new Rule(this.workspace, name, 184 | econtext => this.source.fire(econtext)); 185 | this.rules = [this.rule]; 186 | } 187 | reportChanges(changes) { 188 | return this.rule.reportChanges(changes); 189 | } 190 | } 191 | 192 | class Workspace { 193 | constructor(module) { 194 | this.module = module; 195 | this.clock = 0; 196 | this.update = new PubSub(); 197 | this.forked = new PubSub(); 198 | this.postReset = new PubSub(); 199 | this.invariantError = new PubSub(); 200 | this.invariantError.sub((msg, e) => console.error(msg, e)); 201 | this.cursor = new Execution({ 202 | msg: 'Initial state', 203 | state: this._serializeState(), 204 | clock: this.clock, 205 | changes: [''], 206 | }).last(); 207 | this.invariants = this.module.env.invariants.map((invariant, name) => 208 | new Invariant(this, name, invariant)); 209 | this.checkInvariants(); 210 | 211 | this.rulesets = this.module.env.rules.map((rule, name) => { 212 | if (rule instanceof RuleFor) { 213 | return new MultiRuleSet(this, rule, name); 214 | } else { 215 | return new SingleRuleSet(this, rule, name); 216 | } 217 | }); 218 | this.update.sub(changes => { 219 | if (changes === undefined) { 220 | changes = ['']; 221 | } 222 | this.invariants.forEach(invariant => invariant.reportChanges(changes)); 223 | this.rulesets.forEach(ruleset => ruleset.reportChanges(changes)); 224 | }); 225 | this._output = {}; 226 | } 227 | 228 | takeOutput() { 229 | let o = this._output; 230 | this._output = {}; 231 | return o; 232 | } 233 | 234 | getRulesets() { 235 | return this.rulesets; 236 | } 237 | 238 | checkInvariants() { 239 | for (let invariant of this.invariants) { 240 | try { 241 | invariant.check(); 242 | } catch ( e ) { 243 | if (e instanceof errors.Runtime) { 244 | let msg = `Failed invariant ${invariant.name}: ${e}`; 245 | this.invariantError.pub(msg, e); 246 | return false; 247 | } else { 248 | throw e; 249 | } 250 | } 251 | } 252 | return true; 253 | } 254 | 255 | _serializeState() { 256 | let state = {}; 257 | this.module.env.vars.forEach((mvar, name) => { 258 | if (!mvar.isConstant) { 259 | state[name] = mvar.toJSON(); 260 | } 261 | }); 262 | return state; 263 | } 264 | 265 | _loadState(state) { 266 | this.module.env.vars.forEach((mvar, name) => { 267 | if (!mvar.isConstant) { 268 | mvar.assignJSON(state[name]); 269 | } 270 | }); 271 | } 272 | 273 | tryChangeState(mutator) { 274 | let startCursor = this.cursor; 275 | let oldState = startCursor.getEvent().state; 276 | let msg = mutator(); 277 | if (msg === undefined) { 278 | msg = 'state changed'; 279 | } 280 | let newState = this._serializeState(); 281 | let changes = Changesets.compareJSON(oldState, newState); 282 | if (Changesets.empty(changes)) { 283 | return changes; 284 | } else { 285 | msg += ' (changed ' + changes.join(', ') + ')'; 286 | //console.log(msg); 287 | let event = { 288 | msg: msg, 289 | state: newState, 290 | clock: this.clock, 291 | changes: changes, 292 | }; 293 | this.clock += 1; 294 | this.cursor = startCursor.addEvent(event); 295 | if (this.cursor.execution !== startCursor.execution) { 296 | this.forked.pub(this.cursor.execution); 297 | } 298 | this.update.pub(Changesets.union(changes, ['clock', 'execution'])); 299 | event.passedInvariants = this.checkInvariants(); 300 | return changes; 301 | } 302 | } 303 | 304 | wouldChangeState(mutator) { 305 | let oldState = this.cursor.getEvent().state; 306 | mutator(); 307 | let newState = this._serializeState(); 308 | let changes = Changesets.compareJSON(oldState, newState); 309 | if (!Changesets.empty(changes)) { 310 | this._loadState(oldState); 311 | } 312 | return changes; 313 | } 314 | 315 | setClock(newClock) { 316 | newClock = Math.round(newClock); 317 | let oldCursor = this.cursor; 318 | this.clock = newClock; 319 | this.cursor = oldCursor.execution.preceding(e => (e.clock <= newClock)); 320 | if (oldCursor.equals(this.cursor)) { 321 | this.update.pub(['clock:advanced']); 322 | return; 323 | } 324 | let prev = this.cursor.getEvent(); 325 | this._loadState(prev.state); 326 | if (oldCursor.next() !== undefined && 327 | oldCursor.next().equals(this.cursor)) { 328 | let changes = Changesets.union(prev.changes, ['clock']); 329 | this.update.pub(changes); 330 | return; 331 | } 332 | let changes = Changesets.compareJSON( 333 | oldCursor.getEvent().state, 334 | prev.state); 335 | changes = Changesets.union(changes, ['clock']); 336 | this.update.pub(changes); 337 | // TODO: is this updating views twice when clocks advance AND rules fire? 338 | } 339 | 340 | reset(newCursor, newClock) { 341 | let newState = newCursor.getEvent().state; 342 | let changes = Changesets.compareJSON( 343 | this.cursor.getEvent().state, 344 | newState); 345 | changes = Changesets.union(changes, ['clock']); 346 | this._loadState(newState); 347 | this.cursor = newCursor; 348 | this.clock = newClock; 349 | this.postReset.pub(changes); 350 | this.update.pub(changes); 351 | } 352 | 353 | advanceClock(amount) { 354 | this.setClock(this.clock + amount); 355 | } 356 | } 357 | 358 | module.exports = { 359 | Workspace: Workspace, 360 | }; 361 | -------------------------------------------------------------------------------- /doc/LANGUAGE-GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Types 2 | 3 | ### Ranges 4 | 5 | A `range` is an integer within a fixed interval. The interval is written 6 | `..` where low and high are both inclusive. Ranges default to the 7 | low end of their interval. 8 | 9 | > type T : 1..3; 10 | > var t : T; 11 | > t 12 | 1 13 | > t = 3 14 | > t 15 | 3 16 | 17 | Assigning to a range outside its interval will result in a BoundsError and 18 | leave the value unchanged. 19 | 20 | > t = 4 21 | ModelingBoundsError: Cannot assign value of 4 to range T: 1..3; 22 | > t 23 | 3 24 | 25 | ### Records 26 | 27 | A `record` is a compound object made up of a fixed number of fields (called a 28 | `struct` in some languages). Each field is identified by name and has an 29 | associated type. A default record simply has all its fields defaulted. 30 | 31 | > type Scores : record { 32 | . home: 0..99, 33 | . away: 0..99, 34 | . } 35 | > var scores : Scores; 36 | > scores 37 | Scores { home: 0, away: 0 } 38 | > scores.home = 50 39 | > scores.away = 30 40 | > scores 41 | Scores { home: 50, away: 30 } 42 | 43 | Records can also be created and assigned wholesale: 44 | 45 | > scores = Scores { 46 | . home: 20, 47 | . away: 30, 48 | . } 49 | > scores 50 | Scores { home: 20, away: 30 } 51 | 52 | ### Either 53 | 54 | An `either` type is a compound object that is in one of many possible variants. 55 | A default either type is set to its first variant. 56 | 57 | #### Enum-Like Either Types 58 | 59 | The simplest either types distinguish between names, like `enum` in C: 60 | 61 | > type TrafficLight : either { Green, Yellow, Red }; 62 | > var tl : TrafficLight; 63 | > tl 64 | Green 65 | > tl = Yellow; 66 | > tl 67 | Yellow 68 | > tl = False; 69 | ModelingTypeError: Cannot assign False (EitherVariant) to variable of type TrafficLight 70 | 71 | These simple variant names (Green, Yellow, and Red) are placed in the outer 72 | namespace, so they must be unique across all accessible either definitions. 73 | 74 | You can use `==` to compare simple variant names, or you can use `match` statements: 75 | 76 | > var go : Boolean; 77 | > match tl { 78 | . Green { go = True; } 79 | . Yellow { go = True; /* reckless driver */ } 80 | . Red {go = False; } 81 | . } 82 | > go 83 | True 84 | 85 | In a match statement, all variants must be accounted for, 86 | or use the `default` keyword to catch all the rest. 87 | 88 | The built-in type ``Boolean`` is an either type defined as follows: 89 | 90 | type Boolean: either { 91 | False, 92 | True, 93 | } 94 | 95 | Use Boolean when there are two possibilities and the context makes it obvious 96 | what `True` and `False` mean. Otherwise, define your own either type. 97 | 98 | #### Compound Either Types 99 | 100 | Variants of an either type can also carry data, like a nested record. The 101 | compiler ensures that this data is only accessed when it's valid. 102 | 103 | > type Person : either { 104 | . Dead, 105 | . Alive { 106 | . heartRate: 1..200, 107 | . asleep: Boolean, 108 | . }, 109 | . }; 110 | > var p : Person; 111 | > p 112 | Dead 113 | > p = Alive { heartRate: 80, asleep: False }; 114 | > p 115 | Alive { heartRate: 80, asleep: False } 116 | 117 | Note that we can't write `p == Alive`, nor can we write `p.heartRate`. 118 | A match statement is the only way to get at a Person's heart rate: 119 | 120 | > match p { 121 | . Dead { 122 | . isResting = True; 123 | . } 124 | . Alive(details) { 125 | . isResting = (details.heartRate < 100); 126 | . } 127 | . } 128 | > isResting 129 | True 130 | 131 | The variable in parenthesis in a match statement variant makes a copy of the variable. 132 | As a result, this would be an incorrect way to go to sleep: 133 | 134 | > match p { 135 | . Dead { /* resting quite well already */ } 136 | . Alive(details) { 137 | . details.asleep = True; 138 | . } 139 | . } 140 | > p 141 | Alive { heartRate: 80, asleep: False } 142 | 143 | The correct implementation assigns back to the variable: 144 | 145 | > match p { 146 | . Dead { /* resting quite well already */ } 147 | . Alive(details) { 148 | . p = Alive { heartRate: 80, asleep: True }; 149 | . } 150 | . } 151 | > p 152 | Alive { heartRate: 80, asleep: True } 153 | 154 | When you have variables that are only valid sometimes, try to express that 155 | using either types rather than convention. This will help avoid bugs and reduce 156 | redundancy in your state variables. 157 | 158 | ### Collections 159 | 160 | Various collection types are baked into the compiler. 161 | 162 | Arrays are fixed length. 163 | 164 | The following collections are of varying length (up to a fixed limit) and 165 | present similar interfaces: 166 | - Set: no duplicates, no ordering 167 | - MultiSet: allows duplicates, no ordering 168 | - OrderedSet: no duplicates, preserves insertion order 169 | - Vector: allows duplicates, preserves insertion order 170 | 171 | #### Arrays 172 | 173 | Arrays are of fixed size and capacity and contain values of uniform type. They 174 | are indexed by a specified range type. 175 | 176 | > var bools : Array[11..14]; 177 | > bools 178 | [11: False, 12: False, 13: False, 14: False] 179 | > bools[12] = True 180 | > bools 181 | [11: False, 12: True, 13: False, 14: False] 182 | > 183 | 184 | Use a `for` loop to iterate through an array by reference: 185 | 186 | > for b in bools { 187 | . b = !b; 188 | . } 189 | > bools 190 | [11: True, 12: False, 13: True, 14: True] 191 | 192 | #### Sets 193 | 194 | Sets contain distinct values of uniform type, and have a varying size up to a 195 | fixed capacity. Like arrays, they are declared with a range type that 196 | determines their capacity. Use `push` to add items to sets. 197 | 198 | > var boolSet : Set[22..25]; 199 | > boolSet 200 | {} 201 | > push(boolSet, True); 202 | > push(boolSet, False); 203 | > boolSet 204 | {False, True} 205 | > push(boolSet, False); 206 | > boolSet 207 | {False, True} 208 | 209 | You can use a for loop to iterate through the items of a set by reference, just 210 | like arrays. 211 | 212 | The function `pop` removes an unspecified item from a non-empty set: 213 | 214 | > var b : Boolean = pop(boolSet); 215 | > b 216 | True 217 | > b = pop(boolSet); 218 | > b 219 | False 220 | > b = pop(boolSet); 221 | ModelingBoundsError: Cannot pop from empty set {} 222 | 223 | The function `remove` removes a particular item from a set. It returns `True` 224 | if something was removed, `False` otherwise. 225 | 226 | > push(boolSet, True) 227 | > boolSet 228 | {True} 229 | > print remove(boolSet, True); 230 | True 231 | > print remove(boolSet, True); 232 | False 233 | > boolSet 234 | {} 235 | 236 | 237 | #### Ordered Sets 238 | 239 | An `OrderedSet` is just like a `Set` but the indexing is meaningful and 240 | retained. The pushes and pops obey stack-like FIFO ordering. 241 | 242 | TODO: specify better 243 | 244 | ## Variable Declarations 245 | 246 | Done using the keyword `var`, as seen above. All variables must be declared 247 | with an explicit type. 248 | 249 | The intent is to provide proper block scoping. As of this writing, this has not 250 | been tested very well. 251 | 252 | Declaring a variable with a name that shadows another will (should, at least) 253 | result in a compiler error. 254 | 255 | > var x : Boolean; 256 | > while True { var y: Boolean; break; } 257 | > print y; 258 | ModelingLookupError: 'y' is not a variable/constant in scope, attempted access 259 | > while True { var x: Boolean; break; } 260 | ModelingTypeError: Cannot shadow variable x (False) with False 261 | 262 | ## Control Structures 263 | 264 | ### If Statements 265 | 266 | The standard stuff; `if`, `else`, and `else if` statements should work like you 267 | expect. No parentheses are normally required around the expression (if you have 268 | an empty code block and a single identifier for your condition, you may need 269 | parentheses to disambiguate parsing; it's rare but came up in the 270 | toomanybananas example). 271 | Braces are required around the code blocks. 272 | 273 | > if bananas > 0 { 274 | . bananas -= 1; 275 | . state = Happy; 276 | . } else if (notePresent) { 277 | . // Roommate at store: try again later 278 | . } else { 279 | . notePresent = True; 280 | . state = GoingToStore; 281 | . } 282 | > if bananas == 3 { 283 | . bananas = 4; 284 | . } 285 | 286 | ### Match Statements 287 | 288 | These are described in the Either Types section above. 289 | 290 | ### For Loops 291 | 292 | Only for-each loops are provided, and these are described in the Containers 293 | section above. `break` and `continue` work as in most languages. 294 | 295 | TODO: define modification during iteration. 296 | 297 | ### While Loops 298 | 299 | The standard stuff. `break` and `continue` work as in most languages. 300 | 301 | > while True { 302 | . // useless loop 303 | . break; 304 | . } 305 | 306 | Prefer for-each loops when possible, since termination is more obvious. 307 | 308 | ## Functions and Operators 309 | 310 | ### Built-In Operators 311 | 312 | - Negation is defined over `Boolean`: `!x` 313 | - Equality is defined over all types: `x == y`, `x != y`. 314 | - Ordering is defined over ranges and integer literals: 315 | `x < y`, `x <= y`, `x >= y`, `x > y`. 316 | - The standard arithmetic operators are defined over ranges and integer 317 | literals: 318 | `x + y`, `x - y`, `x * y`, `x / y` (truncating), `x % y`. 319 | - The same have shorthand for assigment: 320 | `x += y` (syntactic sugar for `x = x + y`), `x -= y`, `x *= y`, `x /= y`, `x %= y`. 321 | - Logical and and or are defined over `Boolean`: 322 | `x && y`, `x || y` (short-circuiting). 323 | 324 | 325 | ### Built-In Functions 326 | 327 | - `push(c: Collection, i: Item)`: add item to non-full container 328 | - `pop(c: Collection) -> Item`: remove some item from non-empty container 329 | - `remove(c: Collection, i: Item) -> Boolean`: remove given item from container, return whether it was removed 330 | - `contains(c: Collection, i: Item) -> Boolean` 331 | - `size(c: Collection) -> Integer`: current number of items in container 332 | - `capacity(c: Collection) -> Integer`: maximum possible size for container 333 | - `empty(c: Collection) -> Boolean`: `size(c) == 0` 334 | - `full(c: Collection) -> Boolean`: `size(c) == capacity(c)` 335 | - `urandom() -> A` where `A` is a range: uniform random number in range 336 | - `urandomRange(low: Integer, high: Integer) -> Integer`: uniform random number between low and high, inclusive 337 | - `pow(Integer, Integer) -> Integer`: exponentiation 338 | 339 | ### User-Defined Functions 340 | 341 | User-defined functions are defined as follows: 342 | 343 | > function mod4(digit: 0..9) -> 0..4 { 344 | . return digit % 4; 345 | . } 346 | > print mod4(9); 347 | 1 348 | > print mod4(8); 349 | 0 350 | 351 | 352 | Functions are permitted to read and write global state, and no return type is required: 353 | 354 | > var b : Boolean; 355 | > function setToTrue() { b = True; } 356 | > setToTrue() 357 | > b 358 | True 359 | 360 | All arguments are copied into the function, and the return value (if any) is 361 | copied out (pass-by-value). 362 | 363 | ## Execution Model 364 | 365 | After initialization, rules are fired one at a time, nondeterministically. 366 | All invariants are re-checked in between firing rules. 367 | 368 | ### Initialization 369 | 370 | Statements placed at the top-level of a module are executed when the module is 371 | loaded. This can be used to initialize state. 372 | 373 | ### Rules 374 | 375 | A `rule` is a named block of code that modifies the global state. One rule can 376 | be applied at a time. A rule is said to be *active* if applying it would change 377 | the state and *inactive* otherwise (TODO: this definition fails for 378 | non-deterministic rules). 379 | 380 | > var counter : 0..9; 381 | > rule doubleIncrement { 382 | . counter = (counter + 2) % 10; 383 | . } 384 | 385 | Note that after a rule is applied, its stack is gone. Any changes need to be 386 | saved back to the global variables. 387 | 388 | #### Rule-For 389 | 390 | Sometimes you'll want to define a rule that applies to each of many variables 391 | within a container. The `rule-for` syntax combines a rule definition with a for 392 | loop. 393 | 394 | By placing a normal for loop within a rule, the single rule will apply to all 395 | objects atomically: 396 | 397 | > rule move { 398 | . for elevator in elevators { 399 | . // move elevator 400 | . } 401 | . } 402 | 403 | Using rule-for declares several rules, one for each object. In this example, 404 | elevators move at different times (each elevator moves atomically, but they do 405 | not move as a group atomically). 406 | 407 | > rule move for elevator in elevators { 408 | . // move elevator 409 | . } 410 | 411 | ### State Invariants 412 | 413 | An `invariant` is a named block of code that runs assertions on the global state. 414 | Invariants are checked in between applying rules. 415 | 416 | > invariant counterIsEven { 417 | . assert counter % 2 == 0; 418 | . } 419 | 420 | Note that you can also use `assert` within a rule or a function. 421 | 422 | Currently, the compiler won't stop you from changing state in an invariant. 423 | Seriously, don't do that. 424 | 425 | ### Inductive Invariants 426 | 427 | An `inductive invariant` is a type of invariant that needs to access both the 428 | previous and the current state. For example, a state invariant can't express 429 | that an integer monotonically increases, but an inductive invariant can 430 | (`prev.x <= curr.x`). 431 | 432 | TODO: Implement and document. 433 | 434 | ### TODO 435 | 436 | keyword `external` is like `rule` but user-controlled during simulation (not fired automatically) 437 | 438 | keywords `later`, `past`, and `Time` are for synchronous models 439 | 440 | keyword `param` is for arguments to models but is pretty broken right now 441 | 442 | keyword `reset` sets all the variables to their default values. It ought to re-run model initialization code, but it doesn't yet. 443 | 444 | keyword `Output` is a container you can push values into for the runtime to drain 445 | --------------------------------------------------------------------------------