From 670d3fbbd7ebfb69bd223ac30b73ec47b195b380 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 3 Mar 2015 03:41:55 -0300 Subject: Add aperture macro parsing and evaluation. Aperture macros can get complex with arithmetical operations, variables and variables substitution. Current pcb-tools code just read each macro block as an independent unit, this cannot deal with variables that get changed after used. This patch splits the task in two: first we parse all macro content and creates a bytecode representation of all operations. This bytecode representation will be executed when an AD command is issues passing the required parameters. Parsing is heavily based on gerbv using a Shunting Yard approach to math parsing. Integration with rs274x.py code is not finished as I need to figure out how to integrate the final macro primitives with the graphical primitives already in use. --- gerber/am_read.py | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 gerber/am_read.py (limited to 'gerber/am_read.py') diff --git a/gerber/am_read.py b/gerber/am_read.py new file mode 100644 index 0000000..d1201b2 --- /dev/null +++ b/gerber/am_read.py @@ -0,0 +1,229 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# copyright 2014 Paulo Henrique Silva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" This module provides RS-274-X AM macro modifiers parsing. +""" + +from .am_eval import OpCode, eval_macro + +import string + + +class Token: + ADD = "+" + SUB = "-" + MULT = ("x", "X") # compatibility as many gerber writes do use non compliant X + DIV = "/" + OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV) + LEFT_PARENS = "(" + RIGHT_PARENS = ")" + EQUALS = "=" + + +def token_to_opcode(token): + if token == Token.ADD: + return OpCode.ADD + elif token == Token.SUB: + return OpCode.SUB + elif token in Token.MULT: + return OpCode.MUL + elif token == Token.DIV: + return OpCode.DIV + else: + return None + + +def precedence(token): + if token == Token.ADD or token == Token.SUB: + return 1 + elif token in Token.MULT or token == Token.DIV: + return 2 + else: + return 0 + + +def is_op(token): + return token in Token.OPERATORS + + +class Scanner: + def __init__(self, s): + self.buff = s + self.n = 0 + + def eof(self): + return self.n == len(self.buff) + + def peek(self): + if not self.eof(): + return self.buff[self.n] + return "" + + def ungetc(self): + if self.n > 0: + self.n -= 1 + + def getc(self): + if self.eof(): + return "" + + c = self.buff[self.n] + self.n += 1 + return c + + def readint(self): + n = "" + while not self.eof() and (self.peek() in string.digits): + n += self.getc() + return int(n) + + def readfloat(self): + n = "" + while not self.eof() and (self.peek() in string.digits or self.peek() == "."): + n += self.getc() + return float(n) + + def readstr(self, end="*"): + s = "" + while not self.eof() and self.peek() != end: + s += self.getc() + return s.strip() + + +def read_macro(macro): + + instructions = [] + + for block in macro.split("*"): + + is_primitive = False + is_equation = False + + found_equation_left_side = False + found_primitive_code = False + + equation_left_side = 0 + primitive_code = 0 + + if Token.EQUALS in block: + is_equation = True + else: + is_primitive = True + + scanner = Scanner(block) + + # inlined here for compactness and convenience + op_stack = [] + + def pop(): + return op_stack.pop() + + def push(op): + op_stack.append(op) + + def top(): + return op_stack[-1] + + def empty(): + return len(op_stack) == 0 + + while not scanner.eof(): + + c = scanner.getc() + + if c == ",": + found_primitive_code = True + + # add all instructions on the stack to finish last modifier + while not empty(): + instructions.append((token_to_opcode(pop()), None)) + + elif c in Token.OPERATORS: + while not empty() and is_op(top()) and precedence(top()) >= precedence(c): + instructions.append((token_to_opcode(pop()), None)) + + push(c) + + elif c == Token.LEFT_PARENS: + push(c) + + elif c == Token.RIGHT_PARENS: + while not empty() and top() != Token.LEFT_PARENS: + instructions.append((token_to_opcode(pop()), None)) + + if empty(): + raise ValueError("unbalanced parentheses") + + # discard "(" + pop() + + elif c.startswith("$"): + n = scanner.readint() + + if is_equation and not found_equation_left_side: + equation_left_side = n + else: + instructions.append((OpCode.LOAD, n)) + + elif c == Token.EQUALS: + found_equation_left_side = True + + elif c == "0": + if is_primitive and not found_primitive_code: + instructions.append((OpCode.PUSH, scanner.readstr("*"))) + else: + # decimal or integer disambiguation + if scanner.peek() not in '.': + instructions.append((OpCode.PUSH, 0)) + + elif c in "123456789.": + scanner.ungetc() + + if is_primitive and not found_primitive_code: + primitive_code = scanner.readint() + else: + instructions.append((OpCode.PUSH, scanner.readfloat())) + + else: + # whitespace or unknown char + pass + + # add all instructions on the stack to finish last modifier (if any) + while not empty(): + instructions.append((token_to_opcode(pop()), None)) + + # at end, we either have a primitive or a equation + if is_primitive: + instructions.append((OpCode.PRIM, primitive_code)) + + if is_equation: + instructions.append((OpCode.STORE, equation_left_side)) + + return instructions + +if __name__ == '__main__': + import sys + + instructions = read_macro(sys.argv[1]) + + print "insructions:" + for opcode, argument in instructions: + print "%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "") + + print "eval:" + for primitive in eval_macro(instructions): + print primitive -- cgit From a13b981c1c2ea9ede39e9821d9ba818566f044de Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 5 Mar 2015 14:43:30 -0300 Subject: Fix tests for macros with no variables. All AM*Primitive classes now handles float for all but the code modifiers. This simplifies the reading/parsing. --- gerber/am_read.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'gerber/am_read.py') diff --git a/gerber/am_read.py b/gerber/am_read.py index d1201b2..ade4389 100644 --- a/gerber/am_read.py +++ b/gerber/am_read.py @@ -32,6 +32,7 @@ class Token: LEFT_PARENS = "(" RIGHT_PARENS = ")" EQUALS = "=" + EOF = "EOF" def token_to_opcode(token): @@ -71,7 +72,8 @@ class Scanner: def peek(self): if not self.eof(): return self.buff[self.n] - return "" + + return Token.EOF def ungetc(self): if self.n > 0: @@ -104,6 +106,11 @@ class Scanner: return s.strip() +def print_instructions(instructions): + for opcode, argument in instructions: + print "%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "") + + def read_macro(macro): instructions = [] @@ -185,9 +192,10 @@ def read_macro(macro): elif c == "0": if is_primitive and not found_primitive_code: instructions.append((OpCode.PUSH, scanner.readstr("*"))) + found_primitive_code = True else: # decimal or integer disambiguation - if scanner.peek() not in '.': + if scanner.peek() not in '.' or scanner.peek() == Token.EOF: instructions.append((OpCode.PUSH, 0)) elif c in "123456789.": @@ -207,7 +215,7 @@ def read_macro(macro): instructions.append((token_to_opcode(pop()), None)) # at end, we either have a primitive or a equation - if is_primitive: + if is_primitive and found_primitive_code: instructions.append((OpCode.PRIM, primitive_code)) if is_equation: @@ -221,8 +229,7 @@ if __name__ == '__main__': instructions = read_macro(sys.argv[1]) print "insructions:" - for opcode, argument in instructions: - print "%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "") + print_instructions(instructions) print "eval:" for primitive in eval_macro(instructions): -- cgit From adc1ff6d72ac1201517fffcd8e377768be022e91 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 5 Mar 2015 14:58:36 -0300 Subject: Fix for py3 --- gerber/am_read.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/am_read.py') diff --git a/gerber/am_read.py b/gerber/am_read.py index ade4389..9fdd548 100644 --- a/gerber/am_read.py +++ b/gerber/am_read.py @@ -108,7 +108,7 @@ class Scanner: def print_instructions(instructions): for opcode, argument in instructions: - print "%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "") + print("%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "")) def read_macro(macro): -- cgit From 21fdb9cb57f5da938084fbf2b8133d903d0b0d77 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 5 Mar 2015 15:13:59 -0300 Subject: More py3 fixes --- gerber/am_read.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerber/am_read.py') diff --git a/gerber/am_read.py b/gerber/am_read.py index 9fdd548..05f3343 100644 --- a/gerber/am_read.py +++ b/gerber/am_read.py @@ -228,9 +228,9 @@ if __name__ == '__main__': instructions = read_macro(sys.argv[1]) - print "insructions:" + print("insructions:") print_instructions(instructions) - print "eval:" + print("eval:") for primitive in eval_macro(instructions): - print primitive + print(primitive) -- cgit