Read time: 3.1 minutes (315 words)

Processing Include Files

One of the more complicated aspects of parsing the OpenSCAD language us dealing with include and use files. Basically, when OpenSCAD encounters one of these lines, it stops processing the current file, and starts processing the specified include or use file. For include files, the effect is as though the included file was literally copied in-plqce into the original file. For use files, only the module specification lines are processed. We will deal with use files later.

The basic format of an include line is handled using the fileinclude rule

scadparser/ebnf/scad.ebnf
fileinclude
	=
	'include'
	~
	'<'
	file: filename
	'>'
	;

filename
	=
	/[^>]+/
	;

Parsing this line is simple, making the parser actually process the included file is not so simple.

Note

Notice that the rule for a filename is actually a bad one. I cheated and accept any characters up to the closing right angle bracket. This needs to be fixed with a better regular expression, but it will work for now.

Here is a basic test to check that an include line is properly formed:

tests/test_includes.py
 1import pytest
 2import tatsu
 3
 4
 5@pytest.mark.parametrize('t, e', [
 6    ('include <constraints.scad>', "{'file': 'constraints.scad'}"),
 7])
 8def test_includes(scadparser, t, e):
 9    ast = scadparser.parse(t, start='fileinclude')
10    assert str(ast) == e
11

Include File Handling

Now that we have a rule that will properly parse the include line, we need to figure out ow to cause the parser to actually handler the named file. This requires teaching our parser to fire off Python code when it deals with the rule.

TatSu provides a nice way to do this. Through something called Semantic Actions we can call a Python function with the same name as the rule in question when the parser processes that rule.

We need to define a new Python class for these actions:

scadparser/ScadSemantics.py
 1import os
 2import tatsu
 3
 4class ScadSemantics(object):
 5
 6    def fileinclude(self, ast):
 7        g = open('scadparser/ebnf/scad.ebnf').read()
 8        incfile = os.path.join('scad', ast.file)
 9        prog = open(incfile).read()
10        parser = tatsu.compile(g)
11        ast = parser.parse(prog,
12            start='start', semantics=ScadSemantics())
13        return ast

Basically, what this code does is to launch another parser for the same language, and collects the AST generated by that new parser. It will return that new AST as the result of processing the include file to the original parser which will add it to its current parse results. Here is a test program that shows this result;

sandbox/step03.py
 1import tatsu
 2from pprint import pprint
 3from scadparser.ScadSemantics import ScadSemantics
 4
 5def test():
 6    g = open('scadparser/ebnf/scad.ebnf').read()
 7    parser = tatsu.compile(g)
 8    code = open('scad/model.scad').read()
 9    ast = parser.parse(code, start='start', semantics=ScadSemantics())
10    pprint(ast)
11
12if __name__ == '__main__':
13    test()

And here is he (messy) result:

$ python sandbox/step03.py
[({'id': 'max_wing_span', 'value': {'int': '18'}},
  {'id': 'wing_x_offset', 'value': {'float': '1.675'}}),
 {'id': 'wing_span',
  'value': {'left': 'max_wing_span', 'op': '*', 'right': {'float': '1.2'}}}]

This AST is a bit complex, but it will be simple enough to process later.