def test_parse_unknown_function(self): source_code = '''\ @start: price = BOGUS(flavor) ''' with self.assert_raises_compilation_error('reference to unknown function "BOGUS"', line_number=2, char_number=12): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_undefined_label(self): source_code = '''\ @start: => @bogus ''' with self.assert_raises_compilation_error('branch to undefined label "@bogus"', line_number=2): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_trivial_cycle(self): source_code = '''\ @a: => @a ''' with self.assert_raises_compilation_error('branch to itself in state "@a"', line_number=2): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_redeclared_variable(self): source_code = '''\ let flavor = 0 @start: ''' with self.assert_raises_compilation_error('redeclaration of variable "flavor"', line_number=1, char_number=8): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_assigned_custom_variable_to_self(self): source_code = '''\ let temp = temp @start: ''' with self.assert_raises_compilation_error('reference to undeclared variable "temp"', line_number=1, char_number=15): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_assignment_to_undeclared_variable(self): source_code = '''\ @start: bogus = 1 ''' with self.assert_raises_compilation_error('reference to undeclared variable "bogus"', line_number=2, char_number=4): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_late_declaration(self): source_code = '''\ @start: let temp = 0 ''' with self.assert_raises_compilation_error('variables must be declared before the first state definition'): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_redeclared_constant(self): source_code = '''\ let WAFFLE = 0 @start: ''' with self.assert_raises_compilation_error('redeclaration of constant "WAFFLE"', line_number=1, char_number=8): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_unexpected_text_after_line_continuation(self): source_code = '''\ @start: price = scoops + \\ ! sprinkles * 0.50 ''' with self.assert_raises_compilation_error('unexpected text after line-continuation character', line_number=2, char_number=23): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_redeclared_custom_variable(self): source_code = '''\ let temp = 0 let temp = 1 @start: ''' with self.assert_raises_compilation_error('redeclaration of variable "temp"', line_number=2, char_number=8): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_invalid_constant_value(self): source_code = '''\ @start: ''' with self.assertRaises(ValueError) as raised: abysmal.compile(source_code, ICE_CREAM_VARIABLES, dict(ICE_CREAM_CONSTANTS, WAFFLE=None)) self.assertEqual( str(raised.exception), 'the value of constant "WAFFLE" (None) is not an int, float, or Decimal' )
def test_overlapping_variable_and_constant_names(self): source_code = '''\ @start: ''' with self.assertRaises(ValueError) as raised: abysmal.compile(source_code, ICE_CREAM_VARIABLES, dict(ICE_CREAM_CONSTANTS, scoops=1)) self.assertEqual( str(raised.exception), '"scoops" cannot be both a variable and a constant' )
def test_parse_duplicate_label(self): source_code = '''\ @a: => @b @b: => @c @a: price = 1 ''' with self.assert_raises_compilation_error('duplicate label "@a"', line_number=5): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_wrong_number_of_function_parameters(self): source_code = '''\ @start: price = MIN(1) ''' with self.assert_raises_compilation_error('function MIN() accepts between 2 and 100 parameters (1 provided)', line_number=2, char_number=12): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = ABS(-1, 2) ''' with self.assert_raises_compilation_error('function ABS() accepts 1 parameter (2 provided)', line_number=2, char_number=12): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_illegal_assignment(self): source_code = '''\ @start: 2 = 1 ''' with self.assert_raises_compilation_error('illegal assignment', line_number=2, char_number=6): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: WAFFLE = 1 ''' with self.assert_raises_compilation_error('illegal assignment', line_number=2, char_number=11): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: random! = 1 ''' with self.assert_raises_compilation_error('illegal assignment', line_number=2, char_number=12): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = tax = 1 ''' with self.assert_raises_compilation_error('chained assignment is not allowed - did you mean == instead?', line_number=2, char_number=16): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_illegal_branch_condition(self): source_code = '''\ @start: bogus => @b ''' with self.assert_raises_compilation_error('reference to undeclared variable "bogus"', line_number=2, char_number=4): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: flavor || bogus => @b ''' with self.assert_raises_compilation_error('reference to undeclared variable "bogus"', line_number=2, char_number=14): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_run(self): source_code = '''\ @start: scoops == 0 => @explode price = (scoops * 1.25) + \\ ''' + ''' ((cone == WAFFLE) * 1.00) + \\ (sprinkles * 0.25) tax = price * 0.10 total = price + tax stampsEarned = scoops > 5 ? 2 * scoops : scoops @explode: price = scoops / 0 # division by zero! @unused: price = 1M # optimized out ''' for newline in ['\n', '\r', '\r\n']: program, _ = abysmal.compile(source_code.replace('\n', newline), ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) machine = program.machine(flavor=1, scoops=1, cone=1, sprinkles=0) machine.run() self.assertEqual(machine['price'], '1.25') machine.reset() machine.run() self.assertEqual(machine['price'], '1.25') machine = program.machine(scoops=2, cone=2, sprinkles=True) machine.run() self.assertEqual(machine['price'], '3.75')
def assert_compiles_to(self, source_code, expected_variable_names, expected_constants, expected_instructions): program, source_map = abysmal.compile(source_code, ['x', 'y', 'result'], {'TRUE': True, 'FALSE': False, 'PI': Decimal('3.14159'), 'HALF': 0.5}) actual_variable_names, actual_constants, actual_instructions = program.dsmal.split(';') self.assertEqual(actual_variable_names, expected_variable_names, 'variables section does not match') self.assertEqual(actual_constants, expected_constants, 'constants section does not match') actual_instructions = list(zip(re.findall(r'[A-Z][a-z]\d*', actual_instructions), source_map)) self.assertEqual(actual_instructions, expected_instructions, 'instructions section does not match')
def test_get_uncovered_lines(self): compiled_program, source_map = abysmal.compile(ICE_CREAM_SOURCE_CODE, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) machine = compiled_program.machine( sprinkles=False, weekday=ICE_CREAM_CONSTANTS['SATURDAY']) machine.random_number_iterator = itertools.cycle( [1]) # random! is always 1 coverage_tuples = [ machine.reset(flavor=flavor, scoops=scoops, cone=cone).run_with_coverage() for flavor in [ICE_CREAM_CONSTANTS['VANILLA'], ICE_CREAM_CONSTANTS['CHOCOLATE']] for scoops in [1, 2, 3] for cone in [ICE_CREAM_CONSTANTS['SUGAR'], ICE_CREAM_CONSTANTS['WAFFLE']] ] self.assertEqual( abysmal.get_uncovered_lines(source_map, coverage_tuples), abysmal.CoverageReport(partially_covered_line_numbers=[19, 20, 23], uncovered_line_numbers=[27, 28, 31])) # No runs means all lines are uncovered. self.assertEqual( abysmal.get_uncovered_lines(source_map, []), abysmal.CoverageReport(partially_covered_line_numbers=[], uncovered_line_numbers=[ 18, 19, 20, 21, 22, 23, 24, 27, 28, 31, 34 ]))
def test_parse_cycle_detected(self): source_code = '''\ @a: => @b => @c @b: => @d => @e @c: => @d @d: => @e @e: => @c ''' with self.assert_raises_compilation_error('cycle exists between states "@c", "@e", "@d"'): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_compile_fold_constants(self): program, _ = abysmal.compile( '''\ @start: a = 0 || 0 || 1 == 2 || 3 != 3 || 4 < 4 || 5 <= 4 || 6 > 7 || 8 >= 9 b = 0 || 5 || 0 c = 5 || 3 d = 5 && 0 e = 0 && 5 && 3 f = 5 && (0 || 3) && 2 g = 3 + 2 h = 3 - 2 i = 3 * 2 j = 3 / 2 k = 3 ^ 2 l = !5 m = !0 n = +++5 o = ---5 p = ABS(-5) q = CEILING(-5.2) r = FLOOR(5.2) s = MAX(-1, 5, 0) t = MIN(-2, 13, 0) u = ROUND(5.2) v = ROUND(5.8) w = 0 ? 3 : 4 x = 5 ? 3 : 4 y = 5 in {1, a, 3, b} z = 5 not in {1, a, 5, b} za = 3 in [0, a] zb = -5 in (a, 9) zc = 1 in (1, a) zd = 1 in (a, 1) 0 => @deadcode 1 => @alwaysexecuted @deadcode: @alwaysexecuted: ''', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'za', 'zb', 'zc', 'zd'], # pylint: disable=line-too-long {} ) self.assertEqual( program.dsmal, 'a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|za|zb|zc|zd;' '5|-5|3|6|-2|1.5|4|9;' 'LzSt0LoSt1LoSt2LzSt3LzSt4LoSt5Lc0St6LoSt7Lc3St8Lc5St9Lc7St10LzSt11LoSt12Lc0St13Lc1St14Lc0St15Lc1St16Lc0St17Lc0St18Lc4St19Lc0St20Lc3St21Lc6St22Lc2St23Lc0CpLv0EqJn60CpLv1EqJn60PpLzJu62PpLoSt24LzSt25Lv0Lc2GeSt26Lc1Lv0GtSt27LzSt28LzSt29Xx' # pylint: disable=line-too-long )
def test_compile_eliminate_constant_declared_variables(self): program, _ = abysmal.compile( '''\ let t1 = 5 let t2 = t1 * 3 let t3 = t2 + a @start: b = t3 c = t2 - 9 ''', ['a', 'b', 'c'], {} ) self.assertEqual(program.dsmal, 't3|a|b|c;15|6;Lc0Lv1AdSt0Lv0St2Lc1St3Xx')
def check(expr, expected_result): source_code = '@start:\n result = ' + expr program, _ = abysmal.compile( source_code, {'zero', 'one', 'tenth', 'result'}, { 'TRUE': True, 'FALSE': False, 'TWO': 2, 'PI': Decimal('3.14159'), } ) machine = program.machine(zero=0, one=1.0, tenth=Decimal('0.1')) try: machine.run() result = Decimal(machine['result']) except abysmal.ExecutionError as ex: self.assertEqual(str(ex), expected_result) else: self.assertEqual(result, expected_result)
def test_compile_eliminated_unreachable_code(self): program, _ = abysmal.compile( '''\ @start: c = a => @a c = b @a: 0 > 1 => @nope a > b => @yup @nope: => @nope2 @yup: c = b @nope2: c = 100 a = 200 @nope3: c = 300 ''', ['a', 'b', 'c'], {} ) self.assertEqual(program.dsmal, 'a|b|c;;Lv0St2Lv0Lv1GtJn7XxLv1St2Xx')
def test_parse_unknown_token(self): for source_code in ['$flavor', '$4', '"flavor"']: with self.assert_raises_compilation_error('unknown token', line_number=1, char_number=0): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_ice_cream_price(self): # abysmal implementation FLAVOR_CONSTANTS = { 'VANILLA': 1, 'CHOCOLATE': 2, 'STRAWBERRY': 3, } CONE_CONSTANTS = { 'SUGAR': 1, 'WAFFLE': 2, } WEEKDAY_CONSTANTS = { 'MONDAY': 1, 'TUESDAY': 2, 'WEDNESDAY': 3, 'THURSDAY': 4, 'FRIDAY': 5, 'SATURDAY': 6, 'SUNDAY': 7, } SOURCE_CODE = '''\ let TAX_RATE = 5.3% let WEEKDAY_DISCOUNT = 25% @start: price = scoops * (flavor == STRAWBERRY ? 1.25 : 1) price = price + (cone == WAFFLE ? 1.00 : 0.00) price = price + (sprinkles * 0.25) weekday not in {SATURDAY, SUNDAY} => @apply_weekday_discount => @compute_total @apply_weekday_discount: price = price * (1 - WEEKDAY_DISCOUNT) => @compute_total @compute_total: price = price * (1 + TAX_RATE) ''' program, _ = abysmal.compile( SOURCE_CODE, { 'flavor', 'scoops', 'cone', 'sprinkles', 'weekday', 'price', }, dict(**FLAVOR_CONSTANTS, **CONE_CONSTANTS, **WEEKDAY_CONSTANTS)) machine = program.machine(flavor=FLAVOR_CONSTANTS['VANILLA'], scoops=1, cone=CONE_CONSTANTS['SUGAR'], sprinkles=False, weekday=WEEKDAY_CONSTANTS['MONDAY']) # native implementation STRAWBERRY_MULTIPLIER = Decimal('1.25') WAFFLE_CONE_COST = Decimal('1.00') SPRINKLES_COST = Decimal('0.25') WEEKDAY_MULTIPLIER = Decimal('0.75') TAX_MULTIPLIER = Decimal('1.053') def native(flavor, scoops, cone, sprinkles, weekday): price = Decimal(scoops) if flavor == FLAVOR_CONSTANTS['STRAWBERRY']: price *= STRAWBERRY_MULTIPLIER if cone == CONE_CONSTANTS['WAFFLE']: price += WAFFLE_CONE_COST if sprinkles: price += SPRINKLES_COST if weekday not in (WEEKDAY_CONSTANTS['SATURDAY'], WEEKDAY_CONSTANTS['SUNDAY']): price *= WEEKDAY_MULTIPLIER price = price * TAX_MULTIPLIER return price # test cases cases = [{ 'flavor': flavor, 'scoops': scoops, 'cone': cone, 'sprinkles': sprinkles, 'weekday': weekday, } for flavor in FLAVOR_CONSTANTS.values() for scoops in (1, 2, 3) for cone in CONE_CONSTANTS.values() for sprinkles in (False, True) for weekday in WEEKDAY_CONSTANTS.values()] def run_abysmal(): for case in cases: machine.reset(**case).run() _ = Decimal(machine['price']) def run_native(): for case in cases: _ = native(**case) number = 1000 runs = number * len(cases) abysmal_us = 1000000 * min( timeit.repeat(stmt='run_abysmal()', number=number, repeat=5, globals=locals())) / runs native_us = 1000000 * min( timeit.repeat( stmt='run_native()', number=number, repeat=5, globals=locals())) / runs print(''' ICE CREAM PRICE BENCHMARK RESULTS: abysmal : {0:.3f} us/run native : {1:.3f} us/run'''.format(abysmal_us, native_us))
def test_parse_no_states(self): for source_code in ['', ' ', '# nothing', 'let z = 0']: with self.assert_raises_compilation_error('no states are defined'): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_missing_start_state_label(self): for source_code in ['price = 1', '=> @_', 'flavor == 1 => @_']: with self.assert_raises_compilation_error('missing start state label'): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_line_numbers(self): source_code = '#1\r#2\n#3\r\n\r\n\n\r$' with self.assert_raises_compilation_error('unknown token', line_number=7, char_number=0): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
def test_parse_unexpected_token(self): source_code = '''\ @start ''' with self.assert_raises_compilation_error('expected : but found end-of-line instead', line_number=1, char_number=6): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let ''' with self.assert_raises_compilation_error('expected identifier but found end-of-line instead', line_number=1, char_number=3): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp ''' with self.assert_raises_compilation_error('expected = but found end-of-line instead', line_number=1, char_number=8): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp 3 ''' with self.assert_raises_compilation_error('expected = but found literal instead', line_number=1, char_number=9): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp + ''' with self.assert_raises_compilation_error('expected = but found + instead', line_number=1, char_number=9): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp, temp2 ''' with self.assert_raises_compilation_error('expected = but found , instead', line_number=1, char_number=8): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp = ''' with self.assert_raises_compilation_error('unexpected end-of-line', line_number=1, char_number=10): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp = price = 3 ''' with self.assert_raises_compilation_error('chained assignment is not allowed - did you mean == instead?', line_number=1, char_number=17): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ let temp = 0 @start: temp = price = 3 ''' with self.assert_raises_compilation_error('chained assignment is not allowed - did you mean == instead?', line_number=3, char_number=17): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: => y ''' with self.assert_raises_compilation_error('expected label but found identifier instead', line_number=2, char_number=7): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = flavor in 3 ''' with self.assert_raises_compilation_error('expected { or [ or ( but found literal instead', line_number=2, char_number=22): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = flavor not ''' with self.assert_raises_compilation_error('expected in but found end-of-line instead', line_number=2, char_number=22): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = flavor not in 5 ''' with self.assert_raises_compilation_error('expected { or [ or ( but found literal instead', line_number=2, char_number=26): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = @b ''' with self.assert_raises_compilation_error('unexpected label', line_number=2, char_number=12): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: flavor || || => @b ''' with self.assert_raises_compilation_error('unexpected ||', line_number=2, char_number=14): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: < 3 => @b ''' with self.assert_raises_compilation_error('unexpected <', line_number=2, char_number=4): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = ! ''' with self.assert_raises_compilation_error('unexpected end-of-line', line_number=2, char_number=13): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = flavor in {} ''' with self.assert_raises_compilation_error('unexpected }', line_number=2, char_number=23): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = flavor in {3,} ''' with self.assert_raises_compilation_error('unexpected }', line_number=2, char_number=25): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = scoops in [] ''' with self.assert_raises_compilation_error('unexpected ]', line_number=2, char_number=23): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = scoops in [1] ''' with self.assert_raises_compilation_error('expected , but found ] instead', line_number=2, char_number=24): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = scoops in [1, ] ''' with self.assert_raises_compilation_error('unexpected ]', line_number=2, char_number=26): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS) source_code = '''\ @start: price = scoops in [1, 2, 3] ''' with self.assert_raises_compilation_error('expected ] or ) but found , instead', line_number=2, char_number=27): abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)