Creating a scripting language: establishing order

Previously, the grammatical structure for the language was completed, but it still doesn’t actually do anything useful. This article isn’t going to change that.

Rather, this article will serve as the hand-off point from the grammar to the interpreter, which will be the next part I’ll write. If you want to get access to the source and see how to use it, this is all you really need to read, but the stuff that came before was pretty cool, so you might want to at least skim it when you have time.

All that’s left for now is the parser interface, which essentially takes a script’s source and provides intelligible pieces. So, like, here goes:
[python]
from structure.syntax import parser as _parser
from structure.syntax import (
NODE, FUNCTION,

STMT_GOTO, STMT_RETURN, STMT_EXIT,
STMT_BREAK, STMT_CONTINUE,

COND_IF, COND_ELIF, COND_ELSE,
COND_WHILE,

TERM_IDENTIFIER_LOCAL, TERM_IDENTIFIER_SCOPED, TERM_NONE, TERM_BOOL, TERM_STRING, TERM_INTEGER,
TERM_FLOAT,

SEQUENCE,

TEST_EQUALITY, TEST_INEQUALITY, TEST_GREATER_EQUAL, TEST_GREATER, TEST_LESSER_EQUAL, TEST_LESSER,

MATH_MULTIPLY, MATH_DIVIDE, MATH_DIVIDE_INTEGER, MATH_ADD, MATH_SUBTRACT, MATH_MOD,
MATH_AND, MATH_OR, MATH_NAND, MATH_NOR,

FUNCTIONCALL_LOCAL, FUNCTIONCALL_SCOPED, FUNCTIONCALL_UNDEFINED,

ASSIGN, ASSIGN_ADD, ASSIGN_SUBTRACT, ASSIGN_MULTIPLY, ASSIGN_DIVIDE, ASSIGN_DIVIDE_INTEGER,
ASSIGN_MOD, ASSIGN_SEQUENCE,
)
[/python]
In this setup step (yes, two import statements constitute a step), the parsing system is imported (which implicitly makes PLY do its table-construction work), and every expression-identifier is imported into the local namespace, which is good, because you don’t want to couple the interpreter to the grammar — it doesn’t matter how the expressions are derived, as long as they have identifiers that match this set of constants and consistent structures. Ideally (and you’d better make sure it’s more than just an ideal), this is the only module the interpreter will need to see.

Next is the all-important parsing code:
[python]
def parse(source):
"""
Digests a script as “source“ and provides a (“nodes“, “functions“, “structure“) tuple as
output:

– “nodes“: A dictionary of “name“:“expressionlist“ items
– “functions“: A dictionary of (“name“, “parameters“):“expressionlist“ items
– “structure“: The raw digest, of interest only for advanced applications or debugging

“name“ is always a string, “parameters“ is always a frozenset of strings, and
“expressionlist“ items are any type of interpretable expression, as described in the
programming guide.

An invalid script will never be partially salvaged by this routine. If something is illegal,
`ValueError` will be raised.
"""
structure = _parser.parse(source)

nodes = {}
functions = {}
for node in structure:
if node[0] == NODE:
nodes[node[1]] = node[2]
elif node[0] == FUNCTION:
functions[(node[1], frozenset(node[2]))] = node[3]
return (nodes, functions, structure)
[/python]
Yes, despite its significance, it is more than 50% docstring.

It basically just steps through the nodeset (which was returned as a list, not a set, because there was no point to optimizing it at the time) and decides how to categorize each element, then returns nodes and functions in their own dictionaries, as well as the raw digest.

And that’s really all there is to top-level parsing. Each individual instruction in the digest will be similarly easy to interpret, once the interpreter actually exists.

If you want to grab the source, it’s available on Launchpad, along with a reasonably full unit-test assembly, which you can consult to help figure out how to step through the digests.

An example script follows, just for closure’s sake:
[code]
setup{ #Things that need to happen before an event can begin.
INITIAL_CONTROL = storage.retrieve_control(key="abcdefg");
FALLBACK_CONTROL = storage.retrieve_control(key="hijklmn");
TRANSFER_CONTROL = storage.retrieve_control(key="opqrstu");
TRANSFER_FAILURE = storage.retrieve_control(key="vwxyz");

TRANSFER_LIMIT = 5;
TRANSFER_HANDLER = "10.0.0.1";
EVENT_TIMEOUT = 12000; #Two minutes
}

start{
event.set_kill_timeout(timeout=EVENT_TIMEOUT);

event.prime_anomaly_detection(timeout=5000, fork=start_fallback);
goto start_event;
}

start_event{
panel.listen(timeout=0, button_timeout=2000);
event.start_event(ruleset=INITIAL_CONTROL, break_on_panel_input=True);
if(panel.input == panel.INTERRUPT_PROCESS){
if(current_events.count(handler=TRANSFER_HANDLER) < TRANSFER_LIMIT){
event.start_event(ruleset=TRANSFER_CONTROL, asynchronous=True);
goto start_transfer;
}else{
goto transfer_failed;
}
}
exit 0;
}

start_fallback{
event.set_kill_timeout(timeout=EVENT_TIMEOUT);

event.prime_anomaly_detection(timeout=1000, fork=start_fallback);
event.start_event(ruleset=FALLBACK_CONTROL);

exit 0;
}

start_transfer{
if(event.transfer(handler=TRANSFER_HANDLER)){
exit 1;
}else{
goto transfer_failed;
}
}

transfer_failed{
event.start_event(ruleset=TRANSFER_FAILURE);
exit 2;
}
[/code]

Leave a Reply

Your email address will not be published. Required fields are marked *