Package: abysmal
Abysmal stands for “appallingly basic yet somehow mostly adequate language”.Abysmal is a programming language designed to allow non-programmers
to implement simple business logic for computing prices, rankings, or
other kinds of numeric values without incurring the security and
stability risks that would normally result when non-professional coders
contribute to production code. In other words, it’s a sandbox in which
businesspeople can tinker with their business logic to their hearts’
content without involving your developers or breaking anything.FeaturesSupports Python 3.3 and aboveDependenciespython3-devnative library including Python C header fileslibmpdec-devnative library for decimal arithmeticLanguage ReferenceAbysmal programs are designed to be written by businesspeople, so the
language foregoes almost all the features programmers want in a programming
language in favor of mimicking something business people understand:
flowcharts.Just about the only way your businesspeople can “crash” an Abysmal program
is by dividing by zero, because:it’s not Turing-completeit can’t allocate memoryit can’t access the host process or environmentit operates on one and only one type: arbitrary-precision decimal numbersits only control flow construct is GOTOit doesn’t even allow loops!Example program# input variables:
#
#    flavor:         VANILLA, CHOCOLATE, or STRAWBERRY
#    scoops:         1, 2, etc.
#    cone:           SUGAR or WAFFLE
#    sprinkles:      0 or 1
#    weekday:        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, or SUNDAY

# output variables:
#
#    price:          total price, including tax

let TAX_RATE = 5.3%
let WEEKDAY_DISCOUNT = 25%
let GIVEAWAY_RATE = 1%

@start:
    random! <= GIVEAWAY_RATE => @giveaway_winner
    price = scoops * (flavor == STRAWBERRY ? 1.25 : 1.00)
    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

@giveaway_winner:
    price = 0.00

@compute_total:
    price = price * (1 + TAX_RATE)Control flowAn Abysmal program models a flowchart containing one or more steps, orstates.
Program execution begins at the beginning of the first state and continues
until it reaches a dead end. Along the way, variables can be assigned new
values, and execution can jump to other states.That’s it.Every state has a name that starts with@. A state is declared like this:@start:A state declaration is followed by a sequence ofactions. Each action appears
on its own line, and is one of the following:anassignmentof a value to a variable, like this:price = scoops * flavor == STRAWBERRY ? 1.25 : 1.00aconditional jumpto another state, like this:weekday not in {SATURDAY, SUNDAY} => @apply_weekday_discountanunconditional jumpto another state, like this:=> @compute_totalWhen execution reaches a state, that state’s actions are executed in order.
If execution reaches the end of a state without jumping to a new state, the
program exits.Programs are not allowed to contain loops or any other exeuction cycles.
Any program containing a cycle will fail to compile.Actions are typically indented to make the state labels easier to see, but
this is just a stylistic convention and is not enforced by the language.CommentsAnything following a#on a line is treated as a comment and is ignored.Line continuationsA\at the end of a line indicates that the next line is a continuation of
the line. This makes it easy to format long lines readably by splitting them
into multiple, shorter lines. Note that comments can appear after a\.NumbersAbysmal supports integer and fixed-point decimal literals like123,3.14159, etc. In addition, numbers can have the following suffixes:suffixmeaning%percent (12.5%is equivalent to0.125)korKthousand (50kis equivalent to50000)morMmillion (1.2mis equivalent to1200000)borBbillion (0.5bis equivalent to500000000)Scientific notation is not supported.BooleansAbysmal uses1and0to represent the result of any operation that
yields a logical true/false value. When evaluating conditions in a
conditional jump or a?expression, zero is considered false and
any non-zero value is considered true.ExpressionsPrograms can evaluate expressions containing the following operators:operatorprecedencemeaningexample( exp )0 (highest)grouping(x + 1) * y!1logical NOT!x+1unary plus (has no effect)+x-1unary minus-x^2exponentiation (right associative)x ^ 3*3multiplicationx * 100/3divisionx / 2+4additionx + 5-4subtractionx - 3in { exp, … }5is a member of the setx in {0, y, -z}not in { exp, … }5is not a member of the setx not in {0, y, -z}in [ low , high ]5falls within the interval (see Intervals)x in [-3, 7]not in [ low , high ]5does not fall within the intervalx not in [-3, 7]<6is less thanx < y<=6is less than or equal tox <= y>6is greater thanx > y>=6is greater than or equal tox >= y==7is equal tox == y!=7is not equal tox != y&&8logical AND (short-circuiting)x && (y / x > 0.8)||9logical OR (short-circuiting)x > 3 || y > 7exp ? exp : exp10 (lowest)if-then-elsex < 0 ? -x : xIntervalsIntervals support inclusive endpoints (specified with square brackets)
and exclusive endpoints (specified with parentheses), and the two can be
freely mixed. For example, the follwing are all valid checks:x in (0, 1)x in (0, 1]x in [0, 1)x in [0, 1]Note that “backwards” intervals (where the first endpoint is greater
than the second) are considered pathological and treated as empty.
Therefore2 in (1, 3)evaluates to1(aka true), but2 in (3, 1)evaluates to0(aka false).FunctionsExpressions can take advantage of the following built-in functions:functionreturnsABS(exp)the absolute value of the specified valueCEILING(exp)the nearest integer value greater than or equal to the specified valueFLOOR(exp)the nearest integer value less than or equal to the specified valueMAX(exp1, exp2, …)the maximum of the specified valuesMIN(exp1, exp2, …)the minimum of the specified valuesROUND(exp)the specified value, rounded to the nearest integerVariablesAbysmal programs can read from and write to variables that you define
when you compile the program. Some of these variables will be inputs,
whose values you will set before you run the program. Others will be outputs,
whose values the program will compute so that those values can be examined
after the program has terminated.Abysmal does not distinguish between input and output variables.Allvariables and constant values are decimal numbers. Abysmal does not
have any concept of strings, booleans, null, or any other types.If not explicitly set, variables default to 0.random!is a special, read-only variable that yields a new, random value
every time it is referenced.You can also provide named constants to your programs when you compile them.
Constants cannot be modified.A program can also declare custom variables that it can use to store
intermediate results while the model is being run, or simply to define
friendlier names for values that are used within the model. Custom variables
must be declared before the first state is declared.Each custom variable is declared on its own line, like this:let PI = 3.14159
let area = PI * r * rUsageAn Abysmal program must be compiled before it can be run. The compiler needs
to know the names of the variables that the program should have access to
and names and values of any constants you want to define:ICE_CREAM_VARIABLES={# inputs'flavor','scoops','cone','sprinkles','weekday',# outputs'price',}ICE_CREAM_CONSTANTS={# flavors'VANILLA':1,'CHOCOLATE':2,'STRAWBERRY':3,# cones'SUGAR':1,'WAFFLE':2,# weekdays'MONDAY':1,'TUESDAY':2,'WEDNESDAY':3,'THURSDAY':4,'FRIDAY':5,'SATURDAY':6,'SUNDAY':7,}compiled_program,source_map=abysmal.compile(source_code,ICE_CREAM_VARIABLES,ICE_CREAM_CONSTANTS)Ignore the second value returned byabysmal.compile()for now (refer to the
Measuring Coverage section to see what it’s useful for).Next, we need to make a virtual machine for the compiled program to run on:machine=compiled_program.machine()Next, we can set any variables as we see fit:# Variables can be set in bulk during reset()...machine.reset(flavor=ICE_CREAM_CONSTANTS['CHOCOLATE'],scoops=2,cone=ICE_CREAM_CONSTANTS['WAFFLE'])# ... or one at a time (though this is less efficient)machine['sprinkles']=True# automatically converted to '1'Finally, we can run the machine and examine final variable values:price=Decimal('0.00')try:machine.run()price=round(Decimal(machine['price']),2)exceptabysmal.ExecutionErrorasex:print('The ice cream pricing algorithm is broken: '+str(ex))else:print('Two scoops of chocolate ice cream in a waffle cone with sprinkles costs: ${0}'.format(price))Note that the virtual machine exposes variable values as strings, which
may be formatted in scientific or fixed-point notation.Variables can be set from int, float, bool, Decimal, and string values
but are converted to strings when assigned. When examining variables
after running a machine, you need to convert to the values back to
Decimal, float, or whatever numeric type you are interested in.Random NumbersBy default,random!generates numbers between 0 and 1 with 9 decimal
places of precision, and uses the default Python PRNG (random.randrange).If you require a more secure PRNG, or different precision, or if you want
to force certain values to be produced for testing purposes, you can supply
your own random number iterator before running a machine:# force random! to yield 0, 1, 0, 1, ...machine.random_number_iterator=itertools.cycle([0,1])The values you return are not required to fall within any particular
range, but [0, 1] is recommended, for consistency with the default behavior.LimitsDecimal values are constrained in accordance with the IEEE 754decimal128format. This provides 34 digits of precision and an exponent range of
-6143 to +6144.Infinity, negative infinity, and NaN (not-a-number) are not allowed.
Calculations that would give rise to one of these will instead trigger
an error.In addition, a calculation can result in overflow or underflow if its
result is too large or too small to fit into thedecimal128range.Errorsabysmal.CompilationErrorraised byabysmal.compile()if the source code cannot be compiledabysmal.ExecutionErrorraised bymachine.run()andmachine.run_with_coverage()if a program encounters an error while running; this includes conditions
such as: division by zero, invalid exponentiation, stack overflow,
floating-point overflow, floating-point underflow, out-of-space, and
failure to generate a random numberabysmal.InstructionLimitExceededErrorraised bymachine.run()andmachine.run_with_coverage()if a program exceeds its allowed instruction count and is aborted;
this error is a subclass ofabysmal.ExecutionErrorPerformance TipsAbysmal programs run very quickly once compiled, and the virtual machine is
optimized to make repeated runs with different inputs as cheap as possible.As always, decide on your performance goals and measure before optimizing.To get the best performance, follow these tips:Avoid recompilationCompiling a program is orders of magnitude slower than actually running it.Save the compiled program and reuse it rather than recompiling every time.
Compiled programs are pickleable, so they are easy to cache.Use baseline imagesWhen you create a machine, you can pass keyword arguments to set the machine’s
variables to initial values. The state of the variables at this moment is
called abaseline image. When you reset a machine, it restores all variables
to the baseline image very efficiently. Therefore, if you are going to run a
particular program repeatedly with some inputs having the same values for all
the runs, you should specify those input values in the baseline.For example:defcompute_shipping_costs(product,weight,zip_codes,compiled_program):shipping_costs={}machine=compiled_program.machine(product=product,weight=weight)forzip_codeinzip_codes:machine.reset(zip=zip_code).run()shipping_costs[zip_code]=round(Decimal(machine['shippingCost']),2)returnshipping_costsSet multiple variables at onceOverride baseline variable values by passing keywords tomachine.reset()rather than assigning variables one-by-one. The overhead of making multiple
Python function calls is non-trivial if your scenario needs performance!Only read and write variables you needInitializing variables before a program runs and reading variables afterwards
can easily add up to more time it takes to actually run a typical program.
If performance is critical for your scenario, you can save time by only
examining variables whose values you really need.Limit instruction executionSince Abysmal does not support loops, it is very difficult to create a program
that runs for very long. However, you can impose an additional limit on the
number of instructions that a program can execute by setting theinstruction_limitattribute of a machine:machine.instruction_limit=5000If a program exceeds its instruction limit, it will raise anabysmal.InstructionLimitExceededError.The default instruction limit is 10000.Therun()method returns the number of instructions that were run before
the program exited.Measuring CoverageIn addition torun(), virtual machines expose arun_with_coverage()method
which can be used in conjunction with the source map returned byabysmal.compile()to generate coverage reports for Abysmal programs.coverage_tuples=[machine.reset(**test_case_inputs).run_with_coverage()fortest_case_inputsintest_cases]coverage_report=abysmal.get_uncovered_lines(source_map,coverage_tuples)print('Partially covered lines: '+', '.join(map(str,coverage_report.partially_covered_line_numbers)))print('Totally uncovered lines: '+', '.join(map(str,coverage_report.uncovered_line_numbers))How coverage works:run_with_coverage()returns acoverage tuplewhose length is equal
to the number of instructions in the compiled program. The value at indexiin the coverage tuple will be True or False depending on whether instructioniwas executed during the program’s run.Thesource mapis another tuple, with the same length as the coverage tuple.
The value at indexiin the source map indicates which line or lines in the
source code generated instructioniof the compiled program. There are three
possibilities:None - the instruction was not directly generated by any source lineint - the instruction was generated by a single source line(int, int, …) - the instruction was generated by multiple source lines
(due to line continuations being used)InstallationNote that native library dependencies must be installed BEFORE
you install theabysmallibrary.pip install abysmalDevelopment#Installsystem-leveldependenciesonDebian/Ubuntumake setup#Rununittestsmake test#Checkcodecleanlinessmake pylint#Checkcodecoveragemake cover#Createsdistpackagemake package