diff --git a/.gitignore b/.gitignore index 3f8a666..bd5fac8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ *.pyc +__javascript__ +node_modules +.python-version +yarn.lock /boolean.py.egg-info/ /build/ /.tox/ diff --git a/.travis.yml b/.travis.yml index a4c1183..8985abc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false language: python python: - - "2.7" +# - "2.7" - "3.4" - "3.5" - "3.6" diff --git a/boolean.js/index.html b/boolean.js/index.html new file mode 100644 index 0000000..63eed0e --- /dev/null +++ b/boolean.js/index.html @@ -0,0 +1,25 @@ + + + + + License Expression Sandbox + + +

+ If you would like to run boolean.py in the browser, + you should tell Transcrypt to transpile it for the browser: +

+ python transpile/transpile.py --browser +

+ This will define function boolean on the + window object. +

+

+ Without the --browser flag, the boolean + function will be defined on the module.exports object, + which works for node.js +

+

To debug boolean.py in the browser, open developer tools.

+ + + diff --git a/boolean/boolean.py b/boolean/boolean.py index 369f074..4f78c63 100644 --- a/boolean/boolean.py +++ b/boolean/boolean.py @@ -27,11 +27,6 @@ import inspect import itertools -try: - basestring # Python 2 -except NameError: - basestring = str # Python 3 - # Set to True to enable tracing for parsing TRACE_PARSE = False @@ -121,10 +116,10 @@ def __init__(self, TRUE_class=None, FALSE_class=None, Symbol_class=None, standard types. """ # TRUE and FALSE base elements are algebra-level "singleton" instances - self.TRUE = self._wrap_type(TRUE_class or _TRUE) + self.TRUE = TRUE_class or _TRUE self.TRUE = self.TRUE() - self.FALSE = self._wrap_type(TRUE_class or _FALSE) + self.FALSE = TRUE_class or _FALSE self.FALSE = self.FALSE() # they cross-reference each other @@ -132,12 +127,12 @@ def __init__(self, TRUE_class=None, FALSE_class=None, Symbol_class=None, self.FALSE.dual = self.TRUE # boolean operation types, defaulting to the standard types - self.NOT = self._wrap_type(NOT_class or NOT) - self.AND = self._wrap_type(AND_class or AND) - self.OR = self._wrap_type(OR_class or OR) + self.NOT = NOT_class or NOT + self.AND = AND_class or AND + self.OR = OR_class or OR # class used for Symbols - self.Symbol = self._wrap_type(Symbol_class or Symbol) + self.Symbol = Symbol_class or Symbol tf_nao = {'TRUE': self.TRUE, 'FALSE': self.FALSE, 'NOT': self.NOT, 'AND': self.AND, 'OR': self.OR, @@ -147,12 +142,6 @@ def __init__(self, TRUE_class=None, FALSE_class=None, Symbol_class=None, # attribute for every other types and objects, including themselves. self._cross_refs(tf_nao) - def _wrap_type(self, base_class): - """ - Wrap the base class using its name as the name of the new type - """ - return type(base_class.__name__, (base_class,), {}) - def _cross_refs(self, objects): """ Set every object as attributes of every object in an `objects` mapping @@ -162,6 +151,9 @@ def _cross_refs(self, objects): for name, value in objects.items(): setattr(obj, name, value) + def _is_function(self, obj): + return obj is self.AND or obj is self.OR or obj is self.NOT + def definition(self): """ Return a tuple of this algebra defined elements and types as: @@ -196,7 +188,7 @@ def parse(self, expr, simplify=False): precedence = {self.NOT: 5, self.AND: 10, self.OR: 15, TOKEN_LPAR: 20} - if isinstance(expr, basestring): + if isinstance(expr, str): tokenized = self.tokenize(expr) else: tokenized = iter(expr) @@ -277,7 +269,8 @@ def is_sym(_t): # the parens are properly nested # the top ast node should be a function subclass - if not (inspect.isclass(ast[1]) and issubclass(ast[1], Function)): + if not (inspect.isclass(ast[1]) and self._is_function(ast[1])): + print(ast[1]) raise ParseError(token, tokstr, position, PARSE_INVALID_NESTING) subex = ast[1](*ast[2:]) @@ -340,7 +333,7 @@ def _start_operation(self, ast, operation, precedence): if TRACE_PARSE: print(' start_op: prec == op_prec:', repr(ast)) return ast - if not (inspect.isclass(ast[1]) and issubclass(ast[1], Function)): + if not (inspect.isclass(ast[1]) and self._is_function(ast[1])): # the top ast node should be a function subclass at this stage raise ParseError(error_code=PARSE_INVALID_NESTING) @@ -404,7 +397,7 @@ def tokenize(self, expr): - True symbols: 1 and True - False symbols: 0, False and None """ - if not isinstance(expr, basestring): + if not isinstance(expr, str): raise TypeError('expr must be string but it is %s.' % type(expr)) # mapping of lowercase token strings to a token type id for the standard @@ -437,9 +430,10 @@ def tokenize(self, expr): break position -= 1 - try: - yield TOKENS[tok.lower()], tok, position - except KeyError: + value = TOKENS.get(tok.lower()) + if value: + yield value, tok, position + else: if sym: yield TOKEN_SYMBOL, tok, position elif tok not in (' ', '\t', '\r', '\n'): @@ -730,7 +724,8 @@ def __gt__(self, other): def __and__(self, other): return self.AND(self, other) - __mul__ = __and__ + def __mul__(self, other): + return self.__and__(other) def __invert__(self): return self.NOT(self) @@ -738,12 +733,14 @@ def __invert__(self): def __or__(self, other): return self.OR(self, other) - __add__ = __or__ + def __add__(self, other): + return self.__or__(other) def __bool__(self): raise TypeError('Cannot evaluate expression as a Python Boolean.') - __nonzero__ = __bool__ + def __nonzero__(self): + return self.__bool__() class BaseElement(Expression): @@ -754,7 +751,7 @@ class BaseElement(Expression): sort_order = 0 def __init__(self): - super(BaseElement, self).__init__() + super().__init__() self.iscanonical = True # The dual Base Element class for this element: TRUE.dual returns @@ -767,7 +764,11 @@ def __lt__(self, other): return self == self.FALSE return NotImplemented - __nonzero__ = __bool__ = lambda s: None + def __bool__(self): + return None + + def __nonzero__(self): + return None def pretty(self, indent=0, debug=False): """ @@ -783,7 +784,7 @@ class _TRUE(BaseElement): """ def __init__(self): - super(_TRUE, self).__init__() + super().__init__() # assigned at singleton creation: self.dual = FALSE def __hash__(self): @@ -798,7 +799,14 @@ def __str__(self): def __repr__(self): return 'TRUE' - __nonzero__ = __bool__ = lambda s: True + def toString(self): + return self.__str__() + + def __bool__(self): + return True + + def __nonzero__(self): + return True class _FALSE(BaseElement): @@ -808,7 +816,7 @@ class _FALSE(BaseElement): """ def __init__(self): - super(_FALSE, self).__init__() + super().__init__() # assigned at singleton creation: self.dual = TRUE def __hash__(self): @@ -823,7 +831,14 @@ def __str__(self): def __repr__(self): return 'FALSE' - __nonzero__ = __bool__ = lambda s: False + def toString(self): + return self.__str__() + + def __bool__(self): + return False + + def __nonzero__(self): + return False class Symbol(Expression): @@ -843,7 +858,7 @@ class Symbol(Expression): sort_order = 5 def __init__(self, obj): - super(Symbol, self).__init__() + super().__init__() # Store an associated object. This object determines equality self.obj = obj self.iscanonical = True @@ -873,9 +888,12 @@ def __str__(self): return str(self.obj) def __repr__(self): - obj = "'%s'" % self.obj if isinstance(self.obj, basestring) else repr(self.obj) + obj = "'%s'" % self.obj if isinstance(self.obj, str) else repr(self.obj) return '%s(%s)' % (self.__class__.__name__, obj) + def toString(self): + return self.__str__() + def pretty(self, indent=0, debug=False): """ Return a pretty formatted representation of self. @@ -884,7 +902,7 @@ def pretty(self, indent=0, debug=False): if debug: debug_details += '' % (self.isliteral, self.iscanonical) - obj = "'%s'" % self.obj if isinstance(self.obj, basestring) else repr(self.obj) + obj = "'%s'" % self.obj if isinstance(self.obj, str) else repr(self.obj) return (' ' * indent) + ('%s(%s%s)' % (self.__class__.__name__, debug_details, obj)) @@ -898,7 +916,7 @@ class Function(Expression): """ def __init__(self, *args): - super(Function, self).__init__() + super().__init__() # Specifies an infix notation of an operator for printing such as | or &. self.operator = None @@ -988,12 +1006,12 @@ class NOT(Function): For example:: >>> class NOT2(NOT): def __init__(self, *args): - super(NOT2, self).__init__(*args) + super().__init__(*args) self.operator = '!' """ def __init__(self, arg1): - super(NOT, self).__init__(arg1) + super().__init__(arg1) self.isliteral = isinstance(self.args[0], Symbol) self.operator = '~' @@ -1067,7 +1085,7 @@ def pretty(self, indent=1, debug=False): pretty_literal = self.args[0].pretty(indent=0, debug=debug) return (' ' * indent) + '%s(%s%s)' % (self.__class__.__name__, debug_details, pretty_literal) else: - return super(NOT, self).pretty(indent=indent, debug=debug) + return super().pretty(indent=indent, debug=debug) class DualBase(Function): @@ -1080,7 +1098,7 @@ class DualBase(Function): """ def __init__(self, arg1, arg2, *args): - super(DualBase, self).__init__(arg1, arg2, *args) + super().__init__(arg1, arg2, *args) # identity element for the specific operation. # This will be TRUE for the AND operation and FALSE for the OR operation. @@ -1394,14 +1412,14 @@ class AND(DualBase): For example:: >>> class AND2(AND): def __init__(self, *args): - super(AND2, self).__init__(*args) + super().__init__(*args) self.operator = 'AND' """ sort_order = 10 def __init__(self, arg1, arg2, *args): - super(AND, self).__init__(arg1, arg2, *args) + super().__init__(arg1, arg2, *args) self.identity = self.TRUE self.annihilator = self.FALSE self.dual = self.OR @@ -1418,14 +1436,14 @@ class OR(DualBase): For example:: >>> class OR2(OR): def __init__(self, *args): - super(OR2, self).__init__(*args) + super().__init__(*args) self.operator = 'OR' """ sort_order = 25 def __init__(self, arg1, arg2, *args): - super(OR, self).__init__(arg1, arg2, *args) + super().__init__(arg1, arg2, *args) self.identity = self.FALSE self.annihilator = self.TRUE self.dual = self.AND diff --git a/package.json b/package.json new file mode 100644 index 0000000..ae3d954 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "boolean.js", + "version": "3.4", + "description": "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL.", + "main": "boolean.js/__javascript__/boolean.js", + "repository": "git@github.com:all3fox/boolean.py", + "author": "Alexander Lisianoi ", + "license": "BSD-3-Clause", + "scripts": { + "test": "mocha tests.js" + }, + "devDependencies": { + "mocha": "^3.5.0" + } +} diff --git a/tests.js/test_boolean.js b/tests.js/test_boolean.js new file mode 100644 index 0000000..0deaf43 --- /dev/null +++ b/tests.js/test_boolean.js @@ -0,0 +1,519 @@ +let assert = require('assert') + +let boolean = require('../boolean.js/__javascript__/boolean.js') + +let Symbol = boolean.boolean.Symbol +let BooleanAlgebra = boolean.boolean.BooleanAlgebra + +describe('BaseElement', function() { + let algebra + + beforeEach(function() { + algebra = BooleanAlgebra() + }) + + it('TRUE and FALSE make sense', function() { + assert.equal(algebra.TRUE, algebra.TRUE) + assert.equal(algebra.FALSE, algebra.FALSE) + + assert.ok(algebra.TRUE === algebra.TRUE) + assert.ok(algebra.FALSE === algebra.FALSE) + + assert.notEqual(algebra.TRUE, algebra.FALSE) + assert.notEqual(algebra.FALSE, algebra.TRUE) + + assert.ok(algebra.TRUE != algebra.FALSE) + assert.ok(algebra.FALSE != algebra.TRUE) + }) + + it('literals are empty sets', function() { + // algebra.TRUE.literals and algebra.FALSE.literals are weird objects + assert.ok(algebra.TRUE .literals().__class__.__name__ === 'set') + assert.ok(algebra.FALSE.literals().__class__.__name__ === 'set') + }) + + it('.literalize() works on basics', function() { + assert.equal(algebra.TRUE.literalize(), algebra.TRUE) + assert.equal(algebra.FALSE.literalize(), algebra.FALSE) + + assert.notEqual(algebra.TRUE.literalize(), algebra.FALSE) + assert.notEqual(algebra.FALSE.literalize(), algebra.TRUE) + }) + + it('.simplify() works on basics', function() { + assert.equal(algebra.TRUE.simplify(), algebra.TRUE) + assert.equal(algebra.FALSE.simplify(), algebra.FALSE) + + assert.notEqual(algebra.TRUE.simplify(), algebra.FALSE) + assert.notEqual(algebra.FALSE.simplify(), algebra.TRUE) + }) + + it('dual works on basics', function() { + assert.equal(algebra.TRUE.dual, algebra.FALSE) + assert.equal(algebra.FALSE.dual, algebra.TRUE) + }) + + it('order works on basics', function() { + assert.ok(algebra.FALSE < algebra.TRUE) + assert.ok(algebra.TRUE > algebra.FALSE) + }) + + it('converting to string makes sense', function() { + assert.ok(algebra.FALSE.toString() === '0') + assert.ok(algebra.TRUE .toString() === '1') + + assert.ok(algebra.FALSE.__repr__() === 'FALSE') + assert.ok(algebra.TRUE .__repr__() === 'TRUE' ) + }) +}) + +describe('Symbol', function() { + let symbol, symbol1, symbol2, same0, same1 + + beforeEach(function() { + symbol0 = Symbol('string as a symbol') + + symbol1 = Symbol(1) + symbol2 = Symbol(2) + + same0 = Symbol('sibling symbol') + same1 = Symbol('sibling symbol') + }) + + it('isliteral is true by default', function() { + assert.equal(symbol1.isliteral, true) + }) + + it('symbol contains itself in .literals', function() { + assert.ok(symbol0.literals().indexOf(symbol0) !== -1) + }) + + it('symbols with same obj compare equal', function() { + assert.ok(same0.__eq__(same1)) + assert.equal(same0.obj, same1.obj) + + // Javascript will not let you overload !=, let alone !== + assert.ok(same0 != same1) + assert.notEqual(same0, same1) + }) + + it('.literalize() a symbol gives that symbol', function() { + assert.ok(symbol0.literalize() == symbol0) + assert.ok(symbol0.literalize() === symbol0) + + assert.ok(symbol1.literalize() == symbol1) + assert.ok(symbol1.literalize() === symbol1) + + assert.ok(symbol0.literalize() != symbol1) + assert.ok(symbol1.literalize() != symbol0) + }) + + it('.simplify() a symbol gives that symbol', function() { + assert.ok(symbol0.simplify() == symbol0) + assert.ok(symbol0.simplify() === symbol0) + + assert.ok(symbol0.simplify() != symbol1) + assert.ok(symbol1.simplify() != symbol0) + }) +}) + +describe('BooleanAlgebra', function() { + let algebra, expressions, variables + + beforeEach(function() { + algebra = BooleanAlgebra() + variables = ['a', 'b', 'c', 'd', 'e', 'f'] + }) + + it('parse a single variable', function() { + expression = algebra.parse('a') + + assert.ok(expression.__name__ === 'Symbol') + assert.ok(expression.obj === 'a') + }) + + expressions = [ + 'a or b', 'a OR b', 'a | b', 'a || b', 'a oR b', 'a oR b' + ] + for (let expression of expressions) { + it('parse ' + expression, function() { + expression = algebra.parse(expression) + + assert.ok(expression.__name__ === 'OR') + assert.equal(expression.args.length, 2) + + let fst = expression.args[0], snd = expression.args[1] + + assert.equal(fst.__name__, 'Symbol') + assert.equal(snd.__name__, 'Symbol') + + assert.equal(fst.obj, 'a') + assert.equal(snd.obj, 'b') + }) + } + + expressions = [ + 'a and b', 'a AND b', 'a & b', 'a && b', 'a aND b', 'a aNd b' + ] + for (let expression of expressions) { + it('parse ' + expression, function() { + expression = algebra.parse(expression) + + assert.ok(expression.__name__ === 'AND') + assert.equal(expression.args.length, 2) + + let fst = expression.args[0], snd = expression.args[1] + + assert.equal(fst.__name__, 'Symbol') + assert.equal(snd.__name__, 'Symbol') + + assert.equal(fst.obj, 'a') + assert.equal(snd.obj, 'b') + }) + } + + expressions = ['not a', '~a', '!a', 'nOt a', 'nOT a'] + for (let expression of expressions) { + it('parse ' + expression, function() { + expression = algebra.parse(expression) + + assert.ok(expression.__name__ === 'NOT') + assert.equal(expression.args.length, 1) + + assert.equal(expression.args[0].obj, 'a') + }) + } + + it.skip('parse empty parenthesis', function() { + expression = algebra.parse('()') + }) + + it('parse (a)', function() { + expression = algebra.parse('(a)') + + assert.equal(expression.obj, 'a') + }) + + it('parse (a or b)', function() { + expression = algebra.parse('(a or b)') + + assert.equal(expression.__name__, 'OR') + assert.equal(expression.args.length, 2) + + assert.equal(expression.args[0], 'a') + assert.equal(expression.args[1], 'b') + }) + + it('parse (a and b)', function() { + expression = algebra.parse('(a and b)') + + assert.equal(expression.__name__, 'AND') + assert.equal(expression.args.length, 2) + + assert.equal(expression.args[0].obj, 'a') + assert.equal(expression.args[1].obj, 'b') + }) + + it('parse (not a)', function() { + expression = algebra.parse('(not a)') + + assert.equal(expression.__name__, 'NOT') + assert.equal(expression.args.length, 1) + + assert.equal(expression.args[0].obj, 'a') + }) + + expressions = ['not (a)', '!(a)', '! (a)', '~(a)', '~ (a)'] + for (let expression of expressions) { + it('parse ' + expression, function() { + expression = algebra.parse(expression) + + assert.equal(expression.__name__, 'NOT') + assert.equal(expression.args.length, 1) + + assert.equal(expression.args[0].obj, 'a') + }) + } + + expressions = [ + 'not a & not b', '~a & ~b', '!a & !b', + 'not a && not b', '~a && ~b', '!a && !b', + 'not a and not b', '~a and ~b', '!a and !b', + 'not a & not b & not c', '~a & ~b & ~c', '!a & !b & !c', + 'not a && not b && not c', '~a && ~b && ~c', '!a && !b && !c', + 'not a and not b and not c', '~a and ~b and ~c', '!a and !b and !c' + ] + expressions.forEach((expression, i) => { + it.skip('parse ' + expression, function() { + expression = algebra.parse(expression) + + assert.equal(expression.__name__, 'AND') + if (i < 9) { + assert.equal(expression.args.length, 2) + } else { + assert.equal(expression.args.length, 3) + } + }) + }) + + expressions = [ + 'not a | not b', '~a | ~b', '!a | !b', + 'not a || not b', '~a || ~b', '!a || !b', + 'not a or not b', '~a or ~b', '!a or !b', + 'not a | not b | not c', '~a | ~b | ~c', '!a | !b | !c', + 'not a || not b && not c', '~a || ~b || ~c', '!a || !b || !c', + 'not a or not b or not c', '~a or ~b or ~c', '!a or !b or !c' + ] + expressions.forEach((expression, i) => { + it.skip('parse ' + expression, function() { + expression = algebra.parse(expression) + + assert.equal(expression.__name__, 'OR') + if (i < 9) { + assert.equal(expression.args.length, 2) + } else { + assert.equal(expression.args.length, 3) + } + }) + }) + + expressions = ['not a', '!a', '~a', 'not(a)', '(not a)', '!(a)'] + for (let expression of expressions) { + it('literalize ' + expression, function() { + expression = algebra.parse(expression).literalize() + + assert.equal(expression.__name__, 'NOT') + assert.equal(expression.args.length, 1) + + assert.equal(expression.args[0].obj, 'a') + }) + } + + expressions = [ + 'not (a | b)', '~(a | b)', '!(a | b)', + 'not (a || b)', '~(a || b)', '!(a || b)', + 'not (a or b)', '~(a or b)', '!(a or b)', + 'not (a | b | c)', '~(a | b | c)', '!(a | b | c)', + 'not (a || b || c)', '~(a || b || c)', '!(a || b || c)', + 'not (a or b or c)', '~(a or b or c)', '!(a or b or c)', + 'not (a | b || c)', '~(a | b or c)', '!(a or b || c)' + ] + expressions.forEach((expression, i) => { + it ('literalize ' + expression, function() { + expression = algebra.parse(expression).literalize() + + assert.equal(expression.__name__, 'AND') + if (i < 9) { + assert.equal(expression.args.length, 2) + } else { + assert.equal(expression.args.length, 3) + } + + for (let j = 0; j != expression.args.length; ++j) { + assert.equal(expression.args[j].__name__, 'NOT') + + assert.equal(expression.args[j].args[0].obj, variables[j]) + } + }) + }) + + expressions = [ + 'not (a & b)', '~(a & b)', '!(a & b)', + 'not (a && b)', '~(a && b)', '!(a && b)', + 'not (a and b)', '~(a and b)', '!(a and b)', + 'not (a & b & c)', '~(a & b & c)', '!(a & b & c)', + 'not (a && b && c)', '~(a && b && c)', '!(a && b && c)', + 'not (a and b and c)', '~(a and b and c)', '!(a and b and c)', + 'not (a & b && c)', '~(a & b and c)', '!(a && b and c)' + ] + expressions.forEach((expression, i) => { + it('literalize ' + expression, function() { + expression = algebra.parse(expression).literalize() + + assert.equal(expression.__name__, 'OR') + if (i < 9) { + assert.equal(expression.args.length, 2) + } else { + assert.equal(expression.args.length, 3) + } + + for (let j = 0; j != expression.args.length; ++j) { + assert.equal(expression.args[j].__name__, 'NOT') + + assert.equal(expression.args[j].args[0].obj, variables[j]) + } + }) + }) + + expressions = [ + '!(a and b)', '!(a & b)', '!(a && b)', + '~(a and b)', '~(a & b)', '~(a && b)', + 'not (a and b)', 'not (a & b)', 'not (a && b)', + ] + expressions.forEach((expression, i) => { + it('.demorgan() on ' + expression, function() { + expr = algebra.parse(expression).demorgan() + + assert.equal(expr.__name__, 'OR') + assert.equal(expr.args.length, 2) + + for (let j = 0; j != expr.args.length; ++j) { + assert.equal(expr.args[j].__name__, 'NOT') + assert.equal(expr.args[j].args.length, 1) + + assert.equal(expr.args[j].args[0].obj, variables[j]) + } + }) + }) + + expressions = [ + '!(a and b and c)', '!(a & b & c)', '!(a && b && c)', + '~(a and b and c)', '~(a & b & c)', '~(a && b && c)', + 'not (a and b and c)', 'not (a & b & c)', 'not (a && b && c)' + ] + expressions.forEach((expression, i) => { + it('.demorgan() on ' + expression, function() { + expr = algebra.parse(expression).demorgan() + + assert.equal(expr.__name__, 'OR') + assert.equal(expr.args.length, 3) + + for (let j = 0; j != expr.args.length; ++j) { + assert.equal(expr.args[j].__name__, 'NOT') + assert.equal(expr.args[j].args[0], variables[j]) + } + }) + }) + + expressions = [ + '!(a or b)', '!(a | b)', '!(a || b)', + '~(a or b)', '~(a | b)', '~(a || b)', + 'not (a or b)', 'not (a | b)', 'not (a || b)' + ] + expressions.forEach((expression, i) => { + it('.demorgan() on ' + expression, function() { + expr = algebra.parse(expression).demorgan() + + assert.equal(expr.__name__, 'AND') + assert.equal(expr.args.length, 2) + + for (let j = 0; j != expr.args.length; ++j) { + assert.equal(expr.args[j].__name__, 'NOT') + assert.equal(expr.args[j].args[0], variables[j]) + } + }) + }) + + expressions = [ + '!(a or b or c)', '!(a | b | c)', '!(a || b || c)', + '~(a or b or c)', '~(a | b | c)', '!(a || b || c)', + 'not (a or b or c)', 'not (a | b | c)', 'not (a || b || c)' + ] + expressions.forEach((expression, i) => { + it('.demorgan() on ' + expression, function() { + expr = algebra.parse(expression).demorgan() + + assert.equal(expr.__name__, 'AND') + assert.equal(expr.args.length, 3) + + for (let j = 0; j != expr.args.length; ++j) { + assert.equal(expr.args[j].__name__, 'NOT') + assert.equal(expr.args[j].args[0].obj, variables[j]) + } + }) + }) + + expressions = ['a and a', 'a & a', 'a && a'] + expressions.forEach((expression, i) => { + it('annihilator is *not* set for ' + expression, function() { + expr = algebra.parse(expression) + + assert.equal(expr.annihilator, false) + }) + }) + + expressions = ['a or a', 'a | a', 'a || a'] + expressions.forEach((expression, i) => { + it('annihilator is set for ' + expression, function() { + expr = algebra.parse(expression) + + assert.equal(expr.annihilator, true) + }) + }) + + it.skip('identity is set for a & b', function() { + expr = algebra.parse('a & b') + + assert.equal(expr.identity, true) + }) + + it.skip('identity is *not* set for a | b', function() { + expr = algebra.parse('a | b') + + assert.equal(expr.identity, false) + }) +}) + +describe('NOT', function() { + let algebra, symbol, expressions + + beforeEach(function() { + algebra = BooleanAlgebra() + }) + + expressions = [ + 'not not a', '!!a', '~~a', + 'not !a', '! not a', '~ not a', 'not ~ a', + 'not not not not a', '!!!!a', '~~~~a' + ] + expressions.forEach((expression, i) => { + it('.cancel() on ' + expression, function() { + expr = algebra.parse(expression).cancel() + + assert.equal(expr.obj, 'a') + }) + + it.skip('.literalize() on ' + expression, function() { + expr = algebra.parse(expression).literalize() + + assert.equal(expr.obj, 'a') + }) + + it('.simplify() on ' + expression, function() { + expr = algebra.parse(expression).simplify() + + assert.equal(expr.obj, 'a') + }) + }) + + expressions = [ + 'not a', '!a', '~a', + 'not not not a', '!!!a', '~~~a', + 'not !!a', 'not ! not a', '! not not a' + ] + expressions.forEach((expression, i) => { + it('.cancel() on ' + expression, function() { + expr = algebra.parse(expression).cancel() + + assert.equal(expr.__name__, 'NOT') + assert.equal(expr.args.length, 1) + assert.equal(expr.args[0].obj, 'a') + }) + + it.skip('.literalize() on ' + expression, function() { + expr = algebra.parse(expression).literalize() + + assert.equal(expression.__name__, 'NOT') + assert.equal(expr.args.length, 1) + assert.equal(expr.args[0].obj, 'a') + }) + + it('.simplify() on ' + expression, function() { + expr = algebra.parse(expression).simplify() + + assert.equal(expr.__name__, 'NOT') + assert.equal(expr.args.length, 1) + assert.equal(expr.args[0].obj, 'a') + }) + }) +}) diff --git a/transpile/requirements.txt b/transpile/requirements.txt new file mode 100644 index 0000000..91a6c6a --- /dev/null +++ b/transpile/requirements.txt @@ -0,0 +1,3 @@ +astor +pyaml +transcrypt diff --git a/transpile/transpile.py b/transpile/transpile.py new file mode 100755 index 0000000..f7308fe --- /dev/null +++ b/transpile/transpile.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import logging.config +import shutil +import subprocess +import yaml +import sys + +from argparse import ArgumentParser +from pathlib import Path + + +logger = logging.getLogger(__name__) + +def configure_logging(parent, args): + """ + Configure logging (level, format, etc.) for this module + + Sensible defaults come from 'transpile.yml' and can be changed + by values that come from console. + + :param parent: -- directory where 'transpile.yml' resides + :param args: -- general arguments for transpile + """ + with open(Path(parent, 'transpile.yml'), 'r') as config: + params = yaml.load(config) + + logging.config.dictConfig(params['logging']) + + if args.verbose == 0: + pass # use level specified by the config file + elif args.verbose == 1: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.DEBUG) + + if args.quiet: + logging.disable(logging.CRITICAL) + # Message below should not reach the user + logger.critical('logging is active despite --quiet') + + logger.debug('configure_logging() done') + +def create_transcrypt_cmd(args, transcrypt_args): + """ + Create a subprocess command that calls transcrypt + + :param args: -- general arguments for transpile + :param transcrypt_args: -- arguments specific to transcrypt + + :returns: a command line suitable for subprocess.run() + """ + logger.debug('create_transcrypt_cmd() call') + + # Assume transcrypt executable is available + cmd = ['transcrypt'] + + # You can specify '--' on the command line to pass parameters + # directly to transcrypt, example: transpile -- --help + # In this case '--' is also passed first, so need to remove it: + if transcrypt_args and transcrypt_args[0] == '--': + transcrypt_args = transcrypt_args[1:] + + cmd += transcrypt_args + + # If you are manually passing transcrypt arguments, please specify + # them all yourself. Otherwise, let me provide sensible defaults: + if not transcrypt_args: + # Force transpiling from scratch + cmd.append('-b') + # Force compatibility with Python truth-value testing. + # There is a warning that this switch will slow everything down a lot. + # This forces empty dictionaries, lists, and tuples to compare as false. + cmd.append('-t') + # Force EcmaScript 6 to enable generators + cmd.append('-e') + cmd.append('6') + + if args.browser: + logger.debug('transpile license_expression for the browser') + + pass + else: + logger.debug('transpile license_expression for node.js') + # Drop global 'window' object and prepare for node.js runtime instead + cmd.append('-p') + cmd.append('module.exports') + + # Supply path to the python file to be transpiled + cmd.append(str(args.src[0])) + + logger.info('constructed the following command') + logger.info(str(cmd)) + + logger.debug('create_transcrypt_cmd() done') + return cmd + +def transpile(): + """ + Call transcrypt to transpile boolean.py into JavaScript + """ + + fpath = Path(__file__).resolve() + parent = fpath.parent + + parser = ArgumentParser( + prog=fpath.name, + description="Transpile boolean.py into JavaScript" + ) + + # file path to boolean.py, usually ../boolean/boolean.py + spath = Path(parent.parent, 'boolean') + + # boolean.py path + bpath = Path(spath, 'boolean.py') + parser.add_argument( + '--src', nargs=1, default=[bpath], + help='start transpilation from here' + ) + + # javascript path, for output + jpath = Path(parent.parent, 'boolean.js', '__javascript__') + parser.add_argument( + '--dst', nargs=1, default=[jpath], + help='store produced javascript here' + ) + + parser.add_argument( + '-v', '--verbose', action='count', + help='print more output information' + ) + + parser.add_argument( + '--browser', action='store_true', + help='transpile boolean.py for the browser' + ) + + parser.add_argument( + '-q', '--quiet', action='store_true', + help='print nothing (transcrypt might print though)' + ) + + args, transcrypt_args = parser.parse_known_args() + + configure_logging(parent, args) + + # User can specify '--quiet' to suppress output. So, delay any + # logging calls until we know the desired verbosity level. + logger.debug('transpile() call') + + logger.debug('.parse_known_args() call') + logger.debug('src : ' + str(args.src)) + logger.debug('dst : ' + str(args.dst)) + logger.debug('transcrypt_args: ' + str(transcrypt_args)) + logger.debug('.parse_known_args() done') + + cmd = create_transcrypt_cmd(args, transcrypt_args) + + logger.debug('subprocess.run() call') + process = subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + logger.debug('subprocess.run() done') + + if process.returncode != 0: + logger.warning('Transcrypt failed:') + + for line in str(process.stdout).split('\\n'): + logger.warning(line) + for line in str(process.stderr).split('\\n'): + logger.warning(line) + + sys.exit(1) + + # Transcrypt always puts the transpiled result into __javascript__, + # move it to ./src/license_expression.js, create directories if necessary + stdout = [line for line in str(process.stdout).split('\\n') if line] + lines = list( + filter(lambda line: line.startswith('Saving result in:'), stdout) + ) + + if len(lines) != 1: + logger.warning('Transcrypt output format changed!') + logger.warning('Expected a path to __javascript__ result, instead got:') + + for line in lines: + logger.warning(line) + + src = Path(lines[0].split(': ')[1]).parent + dst = args.dst[0] + + if src != dst: + logger.debug('Copy original __javascript__') + logger.debug('copy src: ' + str(src)) + logger.debug('copy dst: ' + str(dst)) + + if dst.exists(): + logger.debug('Remove previous __javascript__') + shutil.rmtree(str(dst)) + + shutil.copytree(str(src), str(dst)) + + if src.exists(): + logger.debug('Remove original __javascript__') + shutil.rmtree(str(src)) + + logger.debug('transpile() done') + +if __name__ == "__main__": + transpile() diff --git a/transpile/transpile.yml b/transpile/transpile.yml new file mode 100644 index 0000000..1cd1da0 --- /dev/null +++ b/transpile/transpile.yml @@ -0,0 +1,15 @@ +logging: + version: 1 + formatters: + simple: + format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + handlers: + console: + class: logging.StreamHandler + level: DEBUG + formatter: simple + loggers: + __main__: + level: DEBUG + handlers: [console] + propagate: no