From cc28da61d8e13b1730b2487fdf6700fc7368942d Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 16:58:50 +0200 Subject: [PATCH 01/15] added py_scadparser --- solid/py_scadparser/LICENSE | 121 ++++++++++++ solid/py_scadparser/README.md | 6 + solid/py_scadparser/scad_parser.py | 297 +++++++++++++++++++++++++++++ solid/py_scadparser/scad_tokens.py | 106 ++++++++++ 4 files changed, 530 insertions(+) create mode 100644 solid/py_scadparser/LICENSE create mode 100644 solid/py_scadparser/README.md create mode 100644 solid/py_scadparser/scad_parser.py create mode 100644 solid/py_scadparser/scad_tokens.py diff --git a/solid/py_scadparser/LICENSE b/solid/py_scadparser/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/solid/py_scadparser/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/solid/py_scadparser/README.md b/solid/py_scadparser/README.md new file mode 100644 index 00000000..29ec2812 --- /dev/null +++ b/solid/py_scadparser/README.md @@ -0,0 +1,6 @@ +# py_scadparser +A basic openscad parser written in python using ply. + +This parser is intended to be used within solidpython to import openscad code. For this purpose we only need to extract the global definitions of a openscad file. That's exactly what this package does. It parses a openscad file and extracts top level definitions. This includes "use"d and "include"d filenames, global variables, function and module definitions. + +Even though this parser actually parses (almost?) the entire openscad language (at least the portions used in my test libraries) 90% is dismissed and only the needed definitions are processed and extracted. diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py new file mode 100644 index 00000000..f7fa4932 --- /dev/null +++ b/solid/py_scadparser/scad_parser.py @@ -0,0 +1,297 @@ +from enum import Enum + +from ply import lex, yacc + +#workaround relative imports.... make this module runable as script +if __name__ == "__main__": + from scad_tokens import * +else: + from .scad_tokens import * + +class ScadTypes(Enum): + GLOBAL_VAR = 0 + MODULE = 1 + FUNCTION = 2 + USE = 3 + INCLUDE = 4 + +class ScadObject: + def __init__(self, scadType): + self.scadType = scadType + + def getType(self): + return self.scadType + +class ScadUse(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.USE) + self.filename = filename + +class ScadInclude(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.INCLUDE) + self.filename = filename + +class ScadGlobalVar(ScadObject): + def __init__(self, name): + super().__init__(ScadTypes.GLOBAL_VAR) + self.name = name + +class ScadModule(ScadObject): + def __init__(self, name, parameters): + super().__init__(ScadTypes.MODULE) + self.name = name + self.parameters = parameters + +class ScadFunction(ScadObject): + def __init__(self, name, parameters): + super().__init__(ScadTypes.FUNCTION) + self.name = name + self.parameters = parameters + +precedence = ( + ('nonassoc', "THEN"), + ('nonassoc', "ELSE"), + ('nonassoc', "?"), + ('nonassoc', ":"), + ('nonassoc', "[", "]", "(", ")", "{", "}"), + + ('nonassoc', '='), + ('left', "AND", "OR"), + ('nonassoc', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), + ('left', "%"), + ('left', '+', '-'), + ('left', '*', '/'), + ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), + ('right', '^'), + ) + +def p_statements(p): + '''statements : statements statement''' + p[0] = p[1] + if p[2] != None: + p[0].append(p[2]) + +def p_statements_empty(p): + '''statements : empty''' + p[0] = [] + +def p_empty(p): + 'empty : ' + +def p_statement(p): + ''' statement : IF "(" expression ")" statement %prec THEN + | IF "(" expression ")" statement ELSE statement + | for_loop statement + | LET "(" assignment_list ")" statement %prec THEN + | "{" statements "}" + | "%" statement %prec BACKGROUND + | "*" statement %prec BACKGROUND + | "!" statement %prec BACKGROUND + | call statement + | ";" + ''' + +def p_for_loop(p): + '''for_loop : FOR "(" parameter_list ")"''' + +def p_statement_use(p): + 'statement : USE FILENAME' + p[0] = ScadUse(p[2][1:len(p[2])-1]) + +def p_statement_include(p): + 'statement : INCLUDE FILENAME' + p[0] = ScadInclude(p[2][1:len(p[2])-1]) + +def p_statement_function(p): + 'statement : function' + p[0] = p[1] + +def p_statement_module(p): + 'statement : module' + p[0] = p[1] + +def p_statement_assignment(p): + 'statement : ID "=" expression ";"' + p[0] = ScadGlobalVar(p[1]) + +def p_expression(p): + '''expression : ID + | expression "." ID + | "-" expression %prec NEG + | "+" expression %prec POS + | "!" expression %prec NOT + | expression "?" expression ":" expression + | expression "%" expression + | expression "+" expression + | expression "-" expression + | expression "/" expression + | expression "*" expression + | expression "^" expression + | expression "<" expression + | expression ">" expression + | expression EQUAL expression + | expression NOT_EQUAL expression + | expression GREATER_OR_EQUAL expression + | expression LESS_OR_EQUAL expression + | expression AND expression + | expression OR expression + | LET "(" assignment_list ")" expression %prec THEN + | EACH expression %prec THEN + | "[" expression ":" expression "]" + | "[" expression ":" expression ":" expression "]" + | "[" for_loop expression "]" + | for_loop expression %prec THEN + | IF "(" expression ")" expression %prec THEN + | IF "(" expression ")" expression ELSE expression + | "(" expression ")" + | call + | expression "[" expression "]" + | tuple + | STRING + | NUMBER''' + +def p_assignment_list(p): + '''assignment_list : ID "=" expression + | assignment_list "," ID "=" expression + ''' + +def p_call(p): + ''' call : ID "(" call_parameter_list ")" + | ID "(" ")"''' + +def p_tuple(p): + ''' tuple : "[" opt_expression_list "]" + ''' + +def p_opt_expression_list(p): + '''opt_expression_list : expression_list + | expression_list "," + | empty''' +def p_expression_list(p): + ''' expression_list : expression_list "," expression + | expression + ''' + +def p_call_parameter_list(p): + '''call_parameter_list : call_parameter_list "," call_parameter + | call_parameter''' + +def p_call_parameter(p): + '''call_parameter : expression + | ID "=" expression''' + +def p_opt_parameter_list(p): + '''opt_parameter_list : parameter_list + | parameter_list "," + | empty + ''' + if p[1] != None: + p[0] = p[1] + else: + p[0] = [] + +def p_parameter_list(p): + '''parameter_list : parameter_list "," parameter + | parameter''' + if len(p) > 2: + p[0] = p[1] + [p[3]] + else: + p[0] = [p[1]] + +def p_parameter(p): + '''parameter : ID + | ID "=" expression''' + p[0] = p[1] + +def p_function(p): + '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression + ''' + + params = None + if p[4] != ")": + params = p[4] + + p[0] = ScadFunction(p[2], params) + +def p_module(p): + '''module : MODULE ID "(" opt_parameter_list ")" statement + ''' + + params = None + if p[4] != ")": + params = p[4] + + p[0] = ScadModule(p[2], params) + +def p_error(p): + print(f'{p.lineno}:{p.lexpos} {p.type} - {p.value}') + print("syntex error") + +def parseFile(scadFile): + from pathlib import Path + p = Path(scadFile) + f = p.open() + + lexer = lex.lex() + parser = yacc.yacc() + + uses = [] + includes = [] + modules = [] + functions = [] + globalVars = [] + + appendObject = { ScadTypes.MODULE : lambda x: modules.append(x), + ScadTypes.FUNCTION: lambda x: functions.append(x), + ScadTypes.GLOBAL_VAR: lambda x: globalVars.append(x), + ScadTypes.USE: lambda x: uses.append(x), + ScadTypes.INCLUDE: lambda x: includes.append(x), + } + + for i in parser.parse(f.read(), lexer=lexer): + appendObject[i.getType()](i) + + return uses, includes, modules, functions, globalVars + +def parseFileAndPrintGlobals(scadFile): + + print(f'======{scadFile}======') + uses, includes, modules, functions, globalVars = parseFile(scadFile) + + print("Uses:") + for u in uses: + print(f' {u.filename}') + + print("Includes:") + for i in includes: + print(f' {i.filename}') + + print("Modules:") + for m in modules: + print(f' {m.name}({m.parameters})') + + print("Functions:") + for m in functions: + print(f' {m.name}({m.parameters})') + + print("Global Vars:") + for m in globalVars: + print(f' {m.name}') + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} [-q] [ ...]\n -q : quiete") + + quiete = sys.argv[1] == "-q" + files = sys.argv[2:] if quiete else sys.argv[1:] + + for i in files: + if quiete: + print(i) + parseFile(i) + else: + parseFileAndPrintGlobals(i) + diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py new file mode 100644 index 00000000..9d69e289 --- /dev/null +++ b/solid/py_scadparser/scad_tokens.py @@ -0,0 +1,106 @@ +literals = [ + ".", ",", ";", + "=", + "!", + ">", "<", + "+", "-", "*", "/", "^", + "?", ":", + "[", "]", "{", "}", "(", ")", + "%", +] + +reserved = { + 'use' : 'USE', + 'include': 'INCLUDE', + 'module' : 'MODULE', + 'function' : 'FUNCTION', + 'if' : 'IF', + 'else' : 'ELSE', + 'for' : 'FOR', + 'let' : 'LET', + 'each' : 'EACH', +} + +tokens = [ + "ID", + "NUMBER", + "STRING", + "EQUAL", + "GREATER_OR_EQUAL", + "LESS_OR_EQUAL", + "NOT_EQUAL", + "AND", "OR", + "FILENAME", + ] + list(reserved.values()) + +#copy & paste from https://github.com/eliben/pycparser/blob/master/pycparser/c_lexer.py +#LICENSE: BSD +simple_escape = r"""([a-wyzA-Z._~!=&\^\-\\?'"]|x(?![0-9a-fA-F]))""" +decimal_escape = r"""(\d+)(?!\d)""" +hex_escape = r"""(x[0-9a-fA-F]+)(?![0-9a-fA-F])""" +bad_escape = r"""([\\][^a-zA-Z._~^!=&\^\-\\?'"x0-9])""" +escape_sequence = r"""(\\("""+simple_escape+'|'+decimal_escape+'|'+hex_escape+'))' +escape_sequence_start_in_string = r"""(\\[0-9a-zA-Z._~!=&\^\-\\?'"])""" +string_char = r"""([^"\\\n]|"""+escape_sequence_start_in_string+')' +t_STRING = '"'+string_char+'*"' + +t_EQUAL = "==" +t_GREATER_OR_EQUAL = ">=" +t_LESS_OR_EQUAL = "<=" +t_NOT_EQUAL = "!=" +t_AND = "\&\&" +t_OR = "\|\|" + +t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' + +t_ignore = "#$" + +def t_eat_escaped_quotes(t): + r"\\\"" + pass + +def t_comments1(t): + r'(/\*(.|\n)*?\*/)' + t.lexer.lineno += t.value.count("\n") + pass + +def t_comments2(t): + r'//.*[\n\']?' + t.lexer.lineno += 1 + pass + +def t_whitespace(t): + r'\s' + t.lexer.lineno += t.value.count("\n") + +def t_ID(t): + r'[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' + t.type = reserved.get(t.value,'ID') + return t + +def t_NUMBER(t): + r'\d*\.?\d+' + t.value = float(t.value) + return t + +def t_error(t): + print(f'Illegal character ({t.lexer.lineno}) "{t.value[0]}"') + t.lexer.skip(1) + +if __name__ == "__main__": + import sys + from ply import lex + from pathlib import Path + + if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} ") + + p = Path(sys.argv[1]) + f = p.open() + lex.lex() + lex.input(''.join(f.readlines())) + for tok in iter(lex.token, None): + if tok.type == "MODULE": + print("") + print(repr(tok.type), repr(tok.value), end='') + From e64dff08b726ce7cab7c1c5414d66a5f1888321b Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:01:13 +0200 Subject: [PATCH 02/15] removed unused function extract_callable_signatures --- solid/solidpython.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index c8b85a33..f1a1b8cd 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -611,9 +611,6 @@ def sp_code_in_scad_comment(calling_file: PathStr) -> str: # =========== # = Parsing = # =========== -def extract_callable_signatures(scad_file_path: PathStr) -> List[dict]: - scad_code_str = Path(scad_file_path).read_text() - return parse_scad_callables(scad_code_str) def parse_scad_callables(scad_code_str: str) -> List[dict]: callables = [] From 49988f22b9e0a7dc2977fc1e8784bb6aa63e368f Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:01:44 +0200 Subject: [PATCH 03/15] use py_scadparser --- solid/objects.py | 11 +--------- solid/solidpython.py | 49 ++++++-------------------------------------- 2 files changed, 7 insertions(+), 53 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index b1409e6a..2307e804 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -853,16 +853,7 @@ def use(scad_file_path: PathStr, use_not_include: bool = True, dest_namespace_di scad_file_path = _find_library(scad_file_path) - contents = None - try: - contents = scad_file_path.read_text() - except Exception as e: - raise Exception(f"Failed to import SCAD module '{scad_file_path}' with error: {e} ") - - # Once we have a list of all callables and arguments, dynamically - # add OpenSCADObject subclasses for all callables to the calling module's - # namespace. - symbols_dicts = parse_scad_callables(contents) + symbols_dicts = parse_scad_callables(scad_file_path) for sd in symbols_dicts: class_str = new_openscad_class_str(sd['name'], sd['args'], sd['kwargs'], diff --git a/solid/solidpython.py b/solid/solidpython.py index f1a1b8cd..9562e8c4 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -611,51 +611,14 @@ def sp_code_in_scad_comment(calling_file: PathStr) -> str: # =========== # = Parsing = # =========== +def parse_scad_callables(filename: str) -> List[dict]: + from .py_scadparser import scad_parser -def parse_scad_callables(scad_code_str: str) -> List[dict]: - callables = [] + _, _, modules, functions, _ = scad_parser.parseFile(filename) - # Note that this isn't comprehensive; tuples or nested data structures in - # a module definition will defeat it. - - # Current implementation would throw an error if you tried to call a(x, y) - # since Python would expect a(x); OpenSCAD itself ignores extra arguments, - # but that's not really preferable behavior - - # TODO: write a pyparsing grammar for OpenSCAD, or, even better, use the yacc parse grammar - # used by the language itself. -ETJ 06 Feb 2011 - - # FIXME: OpenSCAD use/import includes top level variables. We should parse - # those out (e.g. x = someValue;) as well -ETJ 21 May 2019 - no_comments_re = r'(?mxs)(//.*?\n|/\*.*?\*/)' - - # Also note: this accepts: 'module x(arg) =' and 'function y(arg) {', both - # of which are incorrect syntax - mod_re = r'(?mxs)^\s*(?:module|function)\s+(?P\w+)\s*\((?P.*?)\)\s*(?:{|=)' - - # See https://github.com/SolidCode/SolidPython/issues/95; Thanks to https://github.com/Torlos - args_re = r'(?mxs)(?P\w+)(?:\s*=\s*(?P([\w.\"\s\?:\-+\\\/*]+|\((?>[^()]|(?2))*\)|\[(?>[^\[\]]|(?2))*\])+))?(?:,|$)' - - # remove all comments from SCAD code - scad_code_str = re.sub(no_comments_re, '', scad_code_str) - # get all SCAD callables - mod_matches = re.finditer(mod_re, scad_code_str) - - for m in mod_matches: - callable_name = m.group('callable_name') - args = [] - kwargs = [] - all_args = m.group('all_args') - if all_args: - arg_matches = re.finditer(args_re, all_args) - for am in arg_matches: - arg_name = am.group('arg_name') - # NOTE: OpenSCAD's arguments to all functions are effectively - # optional, in contrast to Python in which all args without - # default values are required. - kwargs.append(arg_name) - - callables.append({'name': callable_name, 'args': args, 'kwargs': kwargs}) + callables = [] + for c in modules + functions: + callables.append({'name': c.name, 'args': [], 'kwargs': c.parameters}) return callables From 243bfd78230ca41552e4ee81ca3ad4263e1df41d Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:03:25 +0200 Subject: [PATCH 04/15] fixed wrong openscad function syntax in examples/scad_to_include.scad --- solid/examples/scad_to_include.scad | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index b2c04ba7..34c3414e 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -17,9 +17,7 @@ function scad_points() = [[0,0], [1,0], [0,1]]; // In Python, calling this function without an argument would be an error. // Leave this here to confirm that this works in OpenSCAD. -function optional_nondefault_arg(arg1){ - s = arg1 ? arg1 : 1; - cube([s,s,s]); -} +function optional_nondefault_arg(arg1) = + let(s = arg1 ? arg1 : 1) cube([s,s,s]); echo("This text should appear only when called with include(), not use()"); \ No newline at end of file From c749460076c06154924a90e06a7ed949c69717f0 Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:05:19 +0200 Subject: [PATCH 05/15] added another test module to examples/scad_to_include.scad (and added missing newline before EOF) --- solid/examples/scad_to_include.scad | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index 34c3414e..bc1a033b 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -13,6 +13,8 @@ module steps(howmany=3){ } } +module blub(a) cube([a, 2, 2]); + function scad_points() = [[0,0], [1,0], [0,1]]; // In Python, calling this function without an argument would be an error. @@ -20,4 +22,4 @@ function scad_points() = [[0,0], [1,0], [0,1]]; function optional_nondefault_arg(arg1) = let(s = arg1 ? arg1 : 1) cube([s,s,s]); -echo("This text should appear only when called with include(), not use()"); \ No newline at end of file +echo("This text should appear only when called with include(), not use()"); From fe6b85d23164319d6b83ba69e881bec28f392327 Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:15:17 +0200 Subject: [PATCH 06/15] fix OpenSCAD identifiers starting with a digit --- solid/solidpython.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index c8b85a33..de2e60fe 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -739,9 +739,17 @@ def new_openscad_class_str(class_name: str, def _subbed_keyword(keyword: str) -> str: """ Append an underscore to any python reserved word. + Prepend an underscore to any OpenSCAD identifier starting with a digit. No-op for all other strings, e.g. 'or' => 'or_', 'other' => 'other' """ - new_key = keyword + '_' if keyword in PYTHON_ONLY_RESERVED_WORDS else keyword + new_key = keyword + + if keyword in PYTHON_ONLY_RESERVED_WORDS: + new_key = keyword + "_" + + if keyword[0].isdigit(): + new_key = "_" + keyword + if new_key != keyword: print(f"\nFound OpenSCAD code that's not compatible with Python. \n" f"Imported OpenSCAD code using `{keyword}` \n" @@ -751,10 +759,16 @@ def _subbed_keyword(keyword: str) -> str: def _unsubbed_keyword(subbed_keyword: str) -> str: """ Remove trailing underscore for already-subbed python reserved words. + Remove prepending underscore if remaining identifier starts with a digit. No-op for all other strings: e.g. 'or_' => 'or', 'other_' => 'other_' """ - shortened = subbed_keyword[:-1] - return shortened if shortened in PYTHON_ONLY_RESERVED_WORDS else subbed_keyword + if subbed_keyword.endswith("_") and subbed_keyword[:-1] in PYTHON_ONLY_RESERVED_WORDS: + return subbed_keyword[:-1] + + if subbed_keyword.startswith("_") and subbed_keyword[1].isdigit(): + return subbed_keyword[1:] + + return subbed_keyword # now that we have the base class defined, we can do a circular import from . import objects From b883cfa54398ad02cda3fd175cd6ad5a6e2ee766 Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 18:15:51 +0200 Subject: [PATCH 07/15] added support for (non)optional arguments --- solid/examples/basic_scad_include.py | 1 + solid/examples/scad_to_include.scad | 2 +- solid/py_scadparser/scad_parser.py | 33 ++++++++++++++++++++-------- solid/solidpython.py | 11 +++++++++- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/solid/examples/basic_scad_include.py b/solid/examples/basic_scad_include.py index 5e432dad..a91020fc 100755 --- a/solid/examples/basic_scad_include.py +++ b/solid/examples/basic_scad_include.py @@ -13,6 +13,7 @@ def demo_import_scad(): scad_path = Path(__file__).parent / 'scad_to_include.scad' scad_mod = import_scad(scad_path) + scad_mod.optional_nondefault_arg(1) return scad_mod.steps(5) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index bc1a033b..e4e50e33 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -13,7 +13,7 @@ module steps(howmany=3){ } } -module blub(a) cube([a, 2, 2]); +module blub(a, b=1) cube([a, 2, 2]); function scad_points() = [[0,0], [1,0], [0,1]]; diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index f7fa4932..08f6d368 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -14,6 +14,7 @@ class ScadTypes(Enum): FUNCTION = 2 USE = 3 INCLUDE = 4 + PARAMETER = 5 class ScadObject: def __init__(self, scadType): @@ -37,17 +38,31 @@ def __init__(self, name): super().__init__(ScadTypes.GLOBAL_VAR) self.name = name -class ScadModule(ScadObject): - def __init__(self, name, parameters): - super().__init__(ScadTypes.MODULE) +class ScadCallable(ScadObject): + def __init__(self, name, parameters, scadType): + super().__init__(scadType) self.name = name self.parameters = parameters -class ScadFunction(ScadObject): + def __repr__(self): + return f'{self.name} ({self.parameters})' + +class ScadModule(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.MODULE) + +class ScadFunction(ScadCallable): def __init__(self, name, parameters): - super().__init__(ScadTypes.FUNCTION) + super().__init__(name, parameters, ScadTypes.FUNCTION) + +class ScadParameter(ScadObject): + def __init__(self, name, optional=False): + super().__init__(ScadTypes.PARAMETER) self.name = name - self.parameters = parameters + self.optional = optional + + def __repr__(self): + return self.name + "=..." if self.optional else self.name precedence = ( ('nonassoc', "THEN"), @@ -202,7 +217,7 @@ def p_parameter_list(p): def p_parameter(p): '''parameter : ID | ID "=" expression''' - p[0] = p[1] + p[0] = ScadParameter(p[1], len(p) == 4) def p_function(p): '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression @@ -269,11 +284,11 @@ def parseFileAndPrintGlobals(scadFile): print("Modules:") for m in modules: - print(f' {m.name}({m.parameters})') + print(f' {m}') print("Functions:") for m in functions: - print(f' {m.name}({m.parameters})') + print(f' {m}') print("Global Vars:") for m in globalVars: diff --git a/solid/solidpython.py b/solid/solidpython.py index 9562e8c4..38ee76a5 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -618,7 +618,16 @@ def parse_scad_callables(filename: str) -> List[dict]: callables = [] for c in modules + functions: - callables.append({'name': c.name, 'args': [], 'kwargs': c.parameters}) + args = [] + kwargs = [] + + for p in c.parameters: + if p.optional: + kwargs.append(p.name) + else: + args.append(p.name) + + callables.append({'name': c.name, 'args': args, 'kwargs': kwargs}) return callables From 9860be43e94000b79096ff8e7143711fba329dc5 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Wed, 19 May 2021 18:56:28 +0200 Subject: [PATCH 08/15] fixed test_parse_scad_callables - parse_scad_callables now receives a filename -> write test_code to tempfile - since the code get written to a file string escapes need to be double escaped ;) - corrected syntax in var_with_functions parameters --- solid/solidpython.py | 12 ++++++++---- solid/test/test_solidpython.py | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 38ee76a5..312c7f0e 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -621,11 +621,15 @@ def parse_scad_callables(filename: str) -> List[dict]: args = [] kwargs = [] + #for some reason solidpython needs to treat all openscad arguments as if + #they where optional. I don't know why, but at least to pass the tests + #it's neccessary to handle it like this !?!?! for p in c.parameters: - if p.optional: - kwargs.append(p.name) - else: - args.append(p.name) + kwargs.append(p.name) + #if p.optional: + # kwargs.append(p.name) + #else: + # args.append(p.name) callables.append({'name': c.name, 'args': args, 'kwargs': kwargs}) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 47135a10..14fe7349 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -135,17 +135,24 @@ def test_parse_scad_callables(self): module var_number(var_number = -5e89){} module var_empty_vector(var_empty_vector = []){} module var_simple_string(var_simple_string = "simple string"){} - module var_complex_string(var_complex_string = "a \"complex\"\tstring with a\\"){} + module var_complex_string(var_complex_string = "a \\"complex\\"\\tstring with a\\\\"){} module var_vector(var_vector = [5454445, 565, [44545]]){} module var_complex_vector(var_complex_vector = [545 + 4445, 565, [cos(75) + len("yes", 45)]]){} - module var_vector(var_vector = [5, 6, "string\twith\ttab"]){} + module var_vector(var_vector = [5, 6, "string\\twith\\ttab"]){} module var_range(var_range = [0:10e10]){} module var_range_step(var_range_step = [-10:0.5:10]){} module var_with_arithmetic(var_with_arithmetic = 8 * 9 - 1 + 89 / 15){} module var_with_parentheses(var_with_parentheses = 8 * ((9 - 1) + 89) / 15){} - module var_with_functions(var_with_functions = abs(min(chamferHeight2, 0)) */-+ 1){} + module var_with_functions(var_with_functions = abs(min(chamferHeight2, 0)) / 1){} module var_with_conditional_assignment(var_with_conditional_assignment = mytest ? 45 : yop){} + """ + + scad_file = "" + with tempfile.NamedTemporaryFile(suffix=".scad", delete=False) as f: + f.write(test_str.encode("utf-8")) + scad_file = f.name + expected = [ {'name': 'hex', 'args': [], 'kwargs': ['width', 'height', 'flats', 'center']}, {'name': 'righty', 'args': [], 'kwargs': ['angle']}, @@ -177,8 +184,12 @@ def test_parse_scad_callables(self): ] from solid.solidpython import parse_scad_callables - actual = parse_scad_callables(test_str) - self.assertEqual(expected, actual) + actual = parse_scad_callables(scad_file) + + for e in expected: + self.assertEqual(e in actual, True) + + os.unlink(scad_file) def test_use(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") From 1ece137e5802b4486405e6d3975067e53758fff9 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Wed, 19 May 2021 18:57:15 +0200 Subject: [PATCH 09/15] (hopefully) made run_all_tests.sh more portable - I hope this works for everybody else, but I think it should --- solid/test/run_all_tests.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solid/test/run_all_tests.sh b/solid/test/run_all_tests.sh index e99fc6ab..a40ad1a0 100755 --- a/solid/test/run_all_tests.sh +++ b/solid/test/run_all_tests.sh @@ -4,15 +4,16 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" cd $DIR +export PYTHONPATH="../../":$PYTHONPATH # Run all tests. Note that unittest's built-in discovery doesn't run the dynamic # testcase generation they contain for i in test_*.py; do echo $i; - python $i; + python3 $i; echo done # revert to original dir -cd - \ No newline at end of file +cd - From 010c815fcc95364199dfdf33b7cb9182d49e3a82 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Wed, 19 May 2021 19:03:58 +0200 Subject: [PATCH 10/15] fixed unclosed file handle in py_scadparser --- solid/py_scadparser/scad_parser.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 08f6d368..73e07013 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -244,9 +244,6 @@ def p_error(p): print("syntex error") def parseFile(scadFile): - from pathlib import Path - p = Path(scadFile) - f = p.open() lexer = lex.lex() parser = yacc.yacc() @@ -264,8 +261,10 @@ def parseFile(scadFile): ScadTypes.INCLUDE: lambda x: includes.append(x), } - for i in parser.parse(f.read(), lexer=lexer): - appendObject[i.getType()](i) + from pathlib import Path + with Path(scadFile).open() as f: + for i in parser.parse(f.read(), lexer=lexer): + appendObject[i.getType()](i) return uses, includes, modules, functions, globalVars From 18fe4874917ab228fdc40ae956f74427390a47ee Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Sat, 22 May 2021 22:54:10 +0200 Subject: [PATCH 11/15] allow strings to be quoted by single ticks -> ' - could this maybe fix the "MacOS mcad issue" from #170 --- solid/py_scadparser/scad_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index 9d69e289..f074c2ff 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -42,7 +42,7 @@ escape_sequence = r"""(\\("""+simple_escape+'|'+decimal_escape+'|'+hex_escape+'))' escape_sequence_start_in_string = r"""(\\[0-9a-zA-Z._~!=&\^\-\\?'"])""" string_char = r"""([^"\\\n]|"""+escape_sequence_start_in_string+')' -t_STRING = '"'+string_char+'*"' +t_STRING = '"'+string_char+'*"' + " | " + "'" +string_char+ "*'" t_EQUAL = "==" t_GREATER_OR_EQUAL = ">=" From 7a0f612e105de3a9a07f17c81fe10036c6728e1d Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Sat, 22 May 2021 23:12:21 +0200 Subject: [PATCH 12/15] improved py_scadparser error messages --- solid/py_scadparser/scad_parser.py | 4 ++-- solid/py_scadparser/scad_tokens.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 73e07013..9200107a 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -240,12 +240,12 @@ def p_module(p): p[0] = ScadModule(p[2], params) def p_error(p): - print(f'{p.lineno}:{p.lexpos} {p.type} - {p.value}') - print("syntex error") + print(f'py_scadparser: Syntax error: {p.lexer.filename}({p.lineno}) {p.type} - {p.value}') def parseFile(scadFile): lexer = lex.lex() + lexer.filename = scadFile parser = yacc.yacc() uses = [] diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index f074c2ff..f79bcc03 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -84,7 +84,7 @@ def t_NUMBER(t): return t def t_error(t): - print(f'Illegal character ({t.lexer.lineno}) "{t.value[0]}"') + print(f'py_scadparser: Illegal character: {t.lexer.filename}({t.lexer.lineno}) "{t.value[0]}"') t.lexer.skip(1) if __name__ == "__main__": @@ -97,9 +97,10 @@ def t_error(t): p = Path(sys.argv[1]) f = p.open() - lex.lex() - lex.input(''.join(f.readlines())) - for tok in iter(lex.token, None): + lexer = lex.lex() + lexer.filename = p.as_posix() + lexer.input(''.join(f.readlines())) + for tok in iter(lexer.token, None): if tok.type == "MODULE": print("") print(repr(tok.type), repr(tok.value), end='') From 2de5803e66c7ba3dd7f7d26b1f5d34967da15d62 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Mon, 24 May 2021 15:00:53 +0200 Subject: [PATCH 13/15] updated py_scadparser it's now based on the openscad/parser.y ;) --- solid/py_scadparser/scad_ast.py | 58 ++++ solid/py_scadparser/scad_parser.py | 445 ++++++++++++++--------------- solid/py_scadparser/scad_tokens.py | 13 +- 3 files changed, 287 insertions(+), 229 deletions(-) create mode 100644 solid/py_scadparser/scad_ast.py diff --git a/solid/py_scadparser/scad_ast.py b/solid/py_scadparser/scad_ast.py new file mode 100644 index 00000000..5e8b49a8 --- /dev/null +++ b/solid/py_scadparser/scad_ast.py @@ -0,0 +1,58 @@ +from enum import Enum + +class ScadTypes(Enum): + GLOBAL_VAR = 0 + MODULE = 1 + FUNCTION = 2 + USE = 3 + INCLUDE = 4 + PARAMETER = 5 + +class ScadObject: + def __init__(self, scadType): + self.scadType = scadType + + def getType(self): + return self.scadType + +class ScadUse(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.USE) + self.filename = filename + +class ScadInclude(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.INCLUDE) + self.filename = filename + +class ScadGlobalVar(ScadObject): + def __init__(self, name): + super().__init__(ScadTypes.GLOBAL_VAR) + self.name = name + +class ScadCallable(ScadObject): + def __init__(self, name, parameters, scadType): + super().__init__(scadType) + self.name = name + self.parameters = parameters + + def __repr__(self): + return f'{self.name} ({self.parameters})' + +class ScadModule(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.MODULE) + +class ScadFunction(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.FUNCTION) + +class ScadParameter(ScadObject): + def __init__(self, name, optional=False): + super().__init__(ScadTypes.PARAMETER) + self.name = name + self.optional = optional + + def __repr__(self): + return self.name + "=None" if self.optional else self.name + diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 9200107a..cd7e9317 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -1,243 +1,249 @@ -from enum import Enum - from ply import lex, yacc #workaround relative imports.... make this module runable as script if __name__ == "__main__": + from scad_ast import * from scad_tokens import * else: + from .scad_ast import * from .scad_tokens import * -class ScadTypes(Enum): - GLOBAL_VAR = 0 - MODULE = 1 - FUNCTION = 2 - USE = 3 - INCLUDE = 4 - PARAMETER = 5 - -class ScadObject: - def __init__(self, scadType): - self.scadType = scadType - - def getType(self): - return self.scadType - -class ScadUse(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.USE) - self.filename = filename - -class ScadInclude(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.INCLUDE) - self.filename = filename - -class ScadGlobalVar(ScadObject): - def __init__(self, name): - super().__init__(ScadTypes.GLOBAL_VAR) - self.name = name - -class ScadCallable(ScadObject): - def __init__(self, name, parameters, scadType): - super().__init__(scadType) - self.name = name - self.parameters = parameters - - def __repr__(self): - return f'{self.name} ({self.parameters})' - -class ScadModule(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.MODULE) - -class ScadFunction(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.FUNCTION) - -class ScadParameter(ScadObject): - def __init__(self, name, optional=False): - super().__init__(ScadTypes.PARAMETER) - self.name = name - self.optional = optional - - def __repr__(self): - return self.name + "=..." if self.optional else self.name - precedence = ( - ('nonassoc', "THEN"), - ('nonassoc', "ELSE"), - ('nonassoc', "?"), - ('nonassoc', ":"), - ('nonassoc', "[", "]", "(", ")", "{", "}"), - - ('nonassoc', '='), - ('left', "AND", "OR"), - ('nonassoc', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), - ('left', "%"), - ('left', '+', '-'), - ('left', '*', '/'), - ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), - ('right', '^'), - ) - -def p_statements(p): - '''statements : statements statement''' - p[0] = p[1] - if p[2] != None: - p[0].append(p[2]) + ('nonassoc', 'NO_ELSE'), + ('nonassoc', 'ELSE'), + ) -def p_statements_empty(p): - '''statements : empty''' +def p_input(p): + """input :""" p[0] = [] -def p_empty(p): - 'empty : ' +def p_input_use(p): + """input : input USE FILENAME + | input INCLUDE FILENAME""" + p[0] = p[1] + +def p_input_statement(p): + """input : input statement""" + p[0] = p[1] + if p[2] != None: + p[0].append(p[2]) def p_statement(p): - ''' statement : IF "(" expression ")" statement %prec THEN - | IF "(" expression ")" statement ELSE statement - | for_loop statement - | LET "(" assignment_list ")" statement %prec THEN - | "{" statements "}" - | "%" statement %prec BACKGROUND - | "*" statement %prec BACKGROUND - | "!" statement %prec BACKGROUND - | call statement - | ";" - ''' - -def p_for_loop(p): - '''for_loop : FOR "(" parameter_list ")"''' - -def p_statement_use(p): - 'statement : USE FILENAME' - p[0] = ScadUse(p[2][1:len(p[2])-1]) - -def p_statement_include(p): - 'statement : INCLUDE FILENAME' - p[0] = ScadInclude(p[2][1:len(p[2])-1]) + """statement : ';' + | '{' inner_input '}' + | module_instantiation + """ + p[0] = None + +def p_statement_assigment(p): + """statement : assignment""" + p[0] = p[1] def p_statement_function(p): - 'statement : function' - p[0] = p[1] + """statement : MODULE ID '(' parameters optional_commas ')' statement + | FUNCTION ID '(' parameters optional_commas ')' '=' expr ';' + """ + if p[1] == 'module': + p[0] = ScadModule(p[2], p[4]) + elif p[1] == 'function': + p[0] = ScadFunction(p[2], p[4]) + else: + assert(False) -def p_statement_module(p): - 'statement : module' - p[0] = p[1] +def p_inner_input(p): + """inner_input : + | inner_input statement + """ -def p_statement_assignment(p): - 'statement : ID "=" expression ";"' +def p_assignment(p): + """assignment : ID '=' expr ';'""" p[0] = ScadGlobalVar(p[1]) -def p_expression(p): - '''expression : ID - | expression "." ID - | "-" expression %prec NEG - | "+" expression %prec POS - | "!" expression %prec NOT - | expression "?" expression ":" expression - | expression "%" expression - | expression "+" expression - | expression "-" expression - | expression "/" expression - | expression "*" expression - | expression "^" expression - | expression "<" expression - | expression ">" expression - | expression EQUAL expression - | expression NOT_EQUAL expression - | expression GREATER_OR_EQUAL expression - | expression LESS_OR_EQUAL expression - | expression AND expression - | expression OR expression - | LET "(" assignment_list ")" expression %prec THEN - | EACH expression %prec THEN - | "[" expression ":" expression "]" - | "[" expression ":" expression ":" expression "]" - | "[" for_loop expression "]" - | for_loop expression %prec THEN - | IF "(" expression ")" expression %prec THEN - | IF "(" expression ")" expression ELSE expression - | "(" expression ")" - | call - | expression "[" expression "]" - | tuple - | STRING - | NUMBER''' - -def p_assignment_list(p): - '''assignment_list : ID "=" expression - | assignment_list "," ID "=" expression - ''' +def p_module_instantiation(p): + """module_instantiation : '!' module_instantiation + | '#' module_instantiation + | '%' module_instantiation + | '*' module_instantiation + | single_module_instantiation child_statement + | ifelse_statement + """ + +def p_ifelse_statement(p): + """ifelse_statement : if_statement %prec NO_ELSE + | if_statement ELSE child_statement + """ + +def p_if_statement(p): + """if_statement : IF '(' expr ')' child_statement + """ + +def p_child_statements(p): + """child_statements : + | child_statements child_statement + | child_statements assignment + """ + +def p_child_statement(p): + """child_statement : ';' + | '{' child_statements '}' + | module_instantiation + """ + +def p_module_id(p): + """module_id : ID + | FOR + | LET + | ASSERT + | ECHO + | EACH + """ + +def p_single_module_instantiation(p): + """single_module_instantiation : module_id '(' arguments ')' + """ + +def p_expr(p): + """expr : logic_or + | FUNCTION '(' parameters optional_commas ')' expr %prec NO_ELSE + | logic_or '?' expr ':' expr + | LET '(' arguments ')' expr + | ASSERT '(' arguments ')' expr_or_empty + | ECHO '(' arguments ')' expr_or_empty + """ + +def p_logic_or(p): + """logic_or : logic_and + | logic_or OR logic_and + """ + +def p_logic_and(p): + """logic_and : equality + | logic_and AND equality + """ + +def p_equality(p): + """equality : comparison + | equality EQUAL comparison + | equality NOT_EQUAL comparison + """ + +def p_comparison(p): + """comparison : addition + | comparison '>' addition + | comparison GREATER_OR_EQUAL addition + | comparison '<' addition + | comparison LESS_OR_EQUAL addition + """ + +def p_addition(p): + """addition : multiplication + | addition '+' multiplication + | addition '-' multiplication + """ + +def p_multiplication(p): + """multiplication : unary + | multiplication '*' unary + | multiplication '/' unary + | multiplication '%' unary + """ + +def p_unary(p): + """unary : exponent + | '+' unary + | '-' unary + | '!' unary + """ + +def p_exponent(p): + """exponent : call + | call '^' unary + """ def p_call(p): - ''' call : ID "(" call_parameter_list ")" - | ID "(" ")"''' - -def p_tuple(p): - ''' tuple : "[" opt_expression_list "]" - ''' - -def p_opt_expression_list(p): - '''opt_expression_list : expression_list - | expression_list "," - | empty''' -def p_expression_list(p): - ''' expression_list : expression_list "," expression - | expression - ''' - -def p_call_parameter_list(p): - '''call_parameter_list : call_parameter_list "," call_parameter - | call_parameter''' - -def p_call_parameter(p): - '''call_parameter : expression - | ID "=" expression''' - -def p_opt_parameter_list(p): - '''opt_parameter_list : parameter_list - | parameter_list "," - | empty - ''' - if p[1] != None: - p[0] = p[1] - else: - p[0] = [] - -def p_parameter_list(p): - '''parameter_list : parameter_list "," parameter - | parameter''' - if len(p) > 2: - p[0] = p[1] + [p[3]] - else: + """call : primary + | call '(' arguments ')' + | call '[' expr ']' + | call '.' ID + """ + +def p_primary(p): + """primary : TRUE + | FALSE + | UNDEF + | NUMBER + | STRING + | ID + | '(' expr ')' + | '[' expr ':' expr ']' + | '[' expr ':' expr ':' expr ']' + | '[' optional_commas ']' + | '[' vector_expr optional_commas ']' + """ + +def p_expr_or_empty(p): + """expr_or_empty : + | expr + """ + +def p_list_comprehension_elements(p): + """list_comprehension_elements : LET '(' arguments ')' list_comprehension_elements_p + | EACH list_comprehension_elements_or_expr + | FOR '(' arguments ')' list_comprehension_elements_or_expr + | FOR '(' arguments ';' expr ';' arguments ')' list_comprehension_elements_or_expr + | IF '(' expr ')' list_comprehension_elements_or_expr %prec NO_ELSE + | IF '(' expr ')' list_comprehension_elements_or_expr ELSE list_comprehension_elements_or_expr + """ + +def p_list_comprehension_elements_p(p): + """list_comprehension_elements_p : list_comprehension_elements + | '(' list_comprehension_elements ')' + """ + +def p_list_comprehension_elements_or_expr(p): + """list_comprehension_elements_or_expr : list_comprehension_elements_p + | expr + """ + +def p_optional_commas(p): + """optional_commas : + | ',' optional_commas + """ + +def p_vector_expr(p): + """vector_expr : expr + | list_comprehension_elements + | vector_expr ',' optional_commas list_comprehension_elements_or_expr + """ + +def p_parameters(p): + """parameters : + | parameter + | parameters ',' optional_commas parameter + """ + if len(p) == 1: + p[0] = [] + elif len(p) == 2: p[0] = [p[1]] + else: + p[0] = p[1] + [p[4]] def p_parameter(p): - '''parameter : ID - | ID "=" expression''' + """parameter : ID + | ID '=' expr + """ p[0] = ScadParameter(p[1], len(p) == 4) -def p_function(p): - '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression - ''' +def p_arguments(p): + """arguments : + | argument + | arguments ',' optional_commas argument + """ - params = None - if p[4] != ")": - params = p[4] - - p[0] = ScadFunction(p[2], params) - -def p_module(p): - '''module : MODULE ID "(" opt_parameter_list ")" statement - ''' - - params = None - if p[4] != ")": - params = p[4] - - p[0] = ScadModule(p[2], params) +def p_argument(p): + """argument : expr + | ID '=' expr + """ def p_error(p): print(f'py_scadparser: Syntax error: {p.lexer.filename}({p.lineno}) {p.type} - {p.value}') @@ -266,20 +272,12 @@ def parseFile(scadFile): for i in parser.parse(f.read(), lexer=lexer): appendObject[i.getType()](i) - return uses, includes, modules, functions, globalVars + return modules, functions, globalVars def parseFileAndPrintGlobals(scadFile): print(f'======{scadFile}======') - uses, includes, modules, functions, globalVars = parseFile(scadFile) - - print("Uses:") - for u in uses: - print(f' {u.filename}') - - print("Includes:") - for i in includes: - print(f' {i.filename}') + modules, functions, globalVars = parseFile(scadFile) print("Modules:") for m in modules: @@ -289,7 +287,7 @@ def parseFileAndPrintGlobals(scadFile): for m in functions: print(f' {m}') - print("Global Vars:") + print("Global Variables:") for m in globalVars: print(f' {m.name}') @@ -304,7 +302,6 @@ def parseFileAndPrintGlobals(scadFile): for i in files: if quiete: - print(i) parseFile(i) else: parseFileAndPrintGlobals(i) diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index f79bcc03..1bd9d85d 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -6,7 +6,7 @@ "+", "-", "*", "/", "^", "?", ":", "[", "]", "{", "}", "(", ")", - "%", + "%", "#" ] reserved = { @@ -16,9 +16,14 @@ 'function' : 'FUNCTION', 'if' : 'IF', 'else' : 'ELSE', - 'for' : 'FOR', 'let' : 'LET', + 'assert' : 'ASSERT', + 'echo' : 'ECHO', + 'for' : 'FOR', 'each' : 'EACH', + 'true' : 'TRUE', + 'false' : 'FALSE', + 'undef' : 'UNDEF', } tokens = [ @@ -53,8 +58,6 @@ t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' -t_ignore = "#$" - def t_eat_escaped_quotes(t): r"\\\"" pass @@ -74,7 +77,7 @@ def t_whitespace(t): t.lexer.lineno += t.value.count("\n") def t_ID(t): - r'[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' + r'[\$]?[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' t.type = reserved.get(t.value,'ID') return t From ca012e5fd9b4a8e3f235002375c6b7395363e4af Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Mon, 24 May 2021 15:03:33 +0200 Subject: [PATCH 14/15] adjusted solidpython to meet py_scadparser changes - [un]subbed_keyword should now handle $parameters - it should be possible to remove all $fn <-> segments code from everywhere and let [un]subbed_keyword do the work --- solid/solidpython.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 42026bee..2b56aa34 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -614,7 +614,7 @@ def sp_code_in_scad_comment(calling_file: PathStr) -> str: def parse_scad_callables(filename: str) -> List[dict]: from .py_scadparser import scad_parser - _, _, modules, functions, _ = scad_parser.parseFile(filename) + modules, functions, _ = scad_parser.parseFile(filename) callables = [] for c in modules + functions: @@ -720,9 +720,15 @@ def _subbed_keyword(keyword: str) -> str: if keyword in PYTHON_ONLY_RESERVED_WORDS: new_key = keyword + "_" - if keyword[0].isdigit(): + elif keyword[0].isdigit(): new_key = "_" + keyword + elif keyword == "$fn": + new_key = "segments" + + elif keyword[0] == "$": + new_key = "__" + keyword[1:] + if new_key != keyword: print(f"\nFound OpenSCAD code that's not compatible with Python. \n" f"Imported OpenSCAD code using `{keyword}` \n" @@ -738,9 +744,15 @@ def _unsubbed_keyword(subbed_keyword: str) -> str: if subbed_keyword.endswith("_") and subbed_keyword[:-1] in PYTHON_ONLY_RESERVED_WORDS: return subbed_keyword[:-1] - if subbed_keyword.startswith("_") and subbed_keyword[1].isdigit(): + elif subbed_keyword.startswith("__"): + return "$" + subbed_keyword[2:] + + elif subbed_keyword.startswith("_") and subbed_keyword[1].isdigit(): return subbed_keyword[1:] + elif subbed_keyword == "segments": + return "$fn" + return subbed_keyword # now that we have the base class defined, we can do a circular import From 389bdcd32ad178c3905756d2d72f9ae8f74fcf91 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Mon, 24 May 2021 15:37:13 +0200 Subject: [PATCH 15/15] removed dead code --- solid/py_scadparser/scad_ast.py | 10 ---------- solid/py_scadparser/scad_parser.py | 4 ---- 2 files changed, 14 deletions(-) diff --git a/solid/py_scadparser/scad_ast.py b/solid/py_scadparser/scad_ast.py index 5e8b49a8..9bfc1aa4 100644 --- a/solid/py_scadparser/scad_ast.py +++ b/solid/py_scadparser/scad_ast.py @@ -15,16 +15,6 @@ def __init__(self, scadType): def getType(self): return self.scadType -class ScadUse(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.USE) - self.filename = filename - -class ScadInclude(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.INCLUDE) - self.filename = filename - class ScadGlobalVar(ScadObject): def __init__(self, name): super().__init__(ScadTypes.GLOBAL_VAR) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index cd7e9317..fe56aa4a 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -254,8 +254,6 @@ def parseFile(scadFile): lexer.filename = scadFile parser = yacc.yacc() - uses = [] - includes = [] modules = [] functions = [] globalVars = [] @@ -263,8 +261,6 @@ def parseFile(scadFile): appendObject = { ScadTypes.MODULE : lambda x: modules.append(x), ScadTypes.FUNCTION: lambda x: functions.append(x), ScadTypes.GLOBAL_VAR: lambda x: globalVars.append(x), - ScadTypes.USE: lambda x: uses.append(x), - ScadTypes.INCLUDE: lambda x: includes.append(x), } from pathlib import Path