33 """PDDM - Poor Developers' Debug-able Macros
35 A simple markup that can be added in comments of source so they can then be
36 expanded out into code. Most of this could be done with CPP macros, but then
37 developers can't really step through them in the debugger, this way they are
38 expanded to the same code, but you can debug them.
40 Any file can be processed, but the syntax is designed around a C based compiler.
41 Processed lines start with "//%". There are three types of sections you can
42 create: Text (left alone), Macro Definitions, and Macro Expansions. There is
43 no order required between definitions and expansions, all definitions are read
44 before any expansions are processed (thus, if desired, definitions can be put
45 at the end of the file to keep them out of the way of the code).
47 Macro Definitions are started with "//%PDDM-DEFINE Name(args)" and all lines
48 afterwards that start with "//%" are included in the definition. Multiple
49 macros can be defined in one block by just using a another "//%PDDM-DEFINE"
50 line to start the next macro. Optionally, a macro can be ended with
51 "//%PDDM-DEFINE-END", this can be useful when you want to make it clear that
52 trailing blank lines are included in the macro. You can also end a definition
55 Macro Expansions are started by single lines containing
56 "//%PDDM-EXPAND Name(args)" and then with "//%PDDM-EXPAND-END" or another
57 expansions. All lines in-between are replaced by the result of the expansion.
58 The first line of the expansion is always a blank like just for readability.
60 Expansion itself is pretty simple, one macro can invoke another macro, but
61 you cannot nest the invoke of a macro in another macro (i.e. - can't do
62 "foo(bar(a))", but you can define foo(a) and bar(b) where bar invokes foo()
65 When macros are expanded, the arg references can also add "$O" suffix to the
66 name (i.e. - "NAME$O") to specify an option to be applied. The options are:
68 $S - Replace each character in the value with a space.
69 $l - Lowercase the first letter of the value.
70 $L - Lowercase the whole value.
71 $u - Uppercase the first letter of the value.
72 $U - Uppercase the whole value.
74 Within a macro you can use ## to cause things to get joined together after
75 expansion (i.e. - "a##b" within a macro will become "ab").
81 //%PDDM-EXPAND case(Enum_Left, 1)
82 //%PDDM-EXPAND case(Enum_Center, 2)
83 //%PDDM-EXPAND case(Enum_Right, 3)
87 //%PDDM-DEFINE case(_A, _B)
91 A macro ends at the start of the next one, or an optional %PDDM-DEFINE-END
92 can be used to avoid adding extra blank lines/returns (or make it clear when
95 One macro can invoke another by simply using its name NAME(ARGS). You cannot
96 nest an invoke inside another (i.e. - NAME1(NAME2(ARGS)) isn't supported).
98 Within a macro you can use ## to cause things to get joined together after
99 processing (i.e. - "a##b" within a macro will become "ab").
111 _MACRO_RE = re.compile(
r'(?P<name>\w+)\((?P<args>.*?)\)')
113 _MACRO_ARG_NAME_RE = re.compile(
r'^\w+$')
116 _GENERATED_CODE_LINE = (
117 '// This block of code is generated, do not edit it directly.'
124 return re.compile(
r'\b(?P<macro_ref>(?P<name>(%s))\((?P<args>.*?)\))' %
125 '|'.join(macro_names))
131 return re.compile(
r'\b(?P<name>(%s))(\$(?P<option>.))?\b' %
132 '|'.join(macro_arg_names))
135 class PDDMError(Exception):
136 """Error thrown by pddm."""
140 class MacroCollection(object):
141 """Hold a set of macros and can resolve/expand them."""
144 """Initializes the collection.
147 a_file: The file like stream to parse.
150 PDDMError if there are any issues.
157 """Holds a macro definition."""
161 self.
_args = tuple(arg_names)
184 """Consumes input extracting definitions.
187 a_file: The file like stream to parse.
190 PDDMError if there are any issues.
192 input_lines = a_file.read().splitlines()
196 """Parses list of lines.
199 input_lines: A list of strings of input to parse (no newlines on the
203 PDDMError if there are any issues.
206 for line
in input_lines:
207 if line.startswith(
'PDDM-'):
208 directive = line.split(
' ', 1)[0]
209 if directive ==
'PDDM-DEFINE':
212 raise PDDMError(
'Attempt to redefine macro: "%s"' % line)
214 self.
_macros[name] = current_macro
216 if directive ==
'PDDM-DEFINE-END':
217 if not current_macro:
218 raise PDDMError(
'Got DEFINE-END directive without an active macro:'
222 raise PDDMError(
'Hit a line with an unknown directive: "%s"' % line)
225 current_macro.AppendLine(line)
229 if line.strip() ==
'':
232 raise PDDMError(
'Hit a line that wasn\'t a directive and no open macro'
233 ' definition: "%s"' % line)
236 assert input_line.startswith(
'PDDM-DEFINE')
237 line = input_line[12:].strip()
238 match = _MACRO_RE.match(line)
240 if match
is None or match.group(0) != line:
241 raise PDDMError(
'Failed to parse macro definition: "%s"' % input_line)
242 name = match.group(
'name')
243 args_str = match.group(
'args').strip()
246 for part
in args_str.split(
','):
249 raise PDDMError(
'Empty arg name in macro definition: "%s"'
251 if not _MACRO_ARG_NAME_RE.match(arg):
252 raise PDDMError(
'Invalid arg name "%s" in macro definition: "%s"'
255 raise PDDMError(
'Arg name "%s" used more than once in macro'
256 ' definition: "%s"' % (arg, input_line))
258 return (name, tuple(args))
261 """Expands the macro reference.
264 macro_ref_str: String of a macro reference (i.e. foo(a, b)).
267 The text from the expansion.
270 PDDMError if there are any issues.
272 match = _MACRO_RE.match(macro_ref_str)
273 if match
is None or match.group(0) != macro_ref_str:
274 raise PDDMError(
'Failed to parse macro reference: "%s"' % macro_ref_str)
275 if match.group(
'name')
not in self.
_macros:
276 raise PDDMError(
'No macro named "%s".' % match.group(
'name'))
277 return self.
_Expand(match, [], macro_ref_str)
281 for _, macro_ref
in reversed(macro_ref_stack):
282 result +=
'\n...while expanding "%s".' % macro_ref
285 def _Expand(self, macro_ref_match, macro_stack, macro_ref_str=None):
286 if macro_ref_str
is None:
287 macro_ref_str = macro_ref_match.group(
'macro_ref')
288 name = macro_ref_match.group(
'name')
289 for prev_name, prev_macro_ref
in macro_stack:
290 if name == prev_name:
291 raise PDDMError(
'Found macro recursion, invoking "%s":%s' %
294 args_str = macro_ref_match.group(
'args').strip()
296 if args_str
or len(macro.args):
297 args = [x.strip()
for x
in args_str.split(
',')]
298 if len(args) !=
len(macro.args):
299 raise PDDMError(
'Expected %d args, got: "%s".%s' %
300 (
len(macro.args), macro_ref_str,
305 new_macro_stack = macro_stack + [(name, macro_ref_str)]
309 eval_result = eval_result.replace(
'##',
'')
310 if eval_result == result:
316 macro, arg_values, macro_ref_to_report, macro_stack):
317 if len(arg_values) == 0:
320 assert len(arg_values) ==
len(macro.args)
321 args = dict(zip(macro.args, arg_values))
323 def _lookupArg(match):
324 val = args[match.group(
'name')]
325 opt = match.group(
'option')
328 return ' ' *
len(val)
331 return val[0].lower() + val[1:]
338 return val[0].upper() + val[1:]
344 raise PDDMError(
'Unknown arg option "%s$%s" while expanding "%s".%s'
345 % (match.group(
'name'), match.group(
'option'),
351 return macro_arg_ref_re.sub(_lookupArg, macro.body)
356 def _resolveMacro(match):
357 return self.
_Expand(match, macro_stack)
358 return macro_ref_re.sub(_resolveMacro, text)
362 """Represents a source file with PDDM directives in it."""
365 """Initializes the file reading in the file.
368 a_file: The file to read in.
369 import_resolver: a function that given a path will return a stream for
373 PDDMError if there are any issues.
387 """Try appending a line.
390 line: The line to append.
391 line_num: The number of the line.
394 A tuple of (SUCCESS, CAN_ADD_MORE). If SUCCESS if False, the line
395 wasn't append. If SUCCESS is True, then CAN_ADD_MORE is True/False to
396 indicate if more lines can be added after this one.
398 assert False,
"subclass should have overridden"
399 return (
False,
False)
402 """Called when the EOF was reached for for a given section."""
406 """Binds the chunk to a macro collection.
409 macro_collection: The collection to bind too.
414 self._lines.append(line)
436 return '\n'.join(self.
lines) +
'\n'
439 """Text section that is echoed out as is."""
442 if line.startswith(
'//%PDDM'):
443 return (
False,
False)
448 """Section that is the result of an macro expansion."""
451 SourceFile.SectionBase.__init__(self, first_line_num)
455 if line.startswith(
'//%PDDM'):
456 directive = line.split(
' ', 1)[0]
457 if directive ==
'//%PDDM-EXPAND':
460 if directive ==
'//%PDDM-EXPAND-END':
463 raise PDDMError(
'Ran into directive ("%s", line %d) while in "%s".' %
469 raise PDDMError(
'Hit the end of the file while in "%s".' %
477 captured_lines = SourceFile.SectionBase.lines.fget(self)
478 directive_len =
len(
'//%PDDM-EXPAND')
480 for line
in captured_lines:
485 result.extend([_GENERATED_CODE_LINE,
'// clang-format off',
''])
486 macro = line[directive_len:].strip()
491 lines = [x.rstrip()
for x
in expand_result.split(
'\n')]
492 lines.append(
'// clang-format on')
493 result.append(
'\n'.join(lines))
494 except PDDMError
as e:
495 raise PDDMError(
'%s\n...while expanding "%s" from the section'
496 ' that started:\n Line %d: %s' %
501 if len(captured_lines) == 1:
502 result.append(
'//%%PDDM-EXPAND-END %s' %
503 captured_lines[0][directive_len:].strip())
505 result.append(
'//%%PDDM-EXPAND-END (%s expansions)' %
510 """Section containing macro definitions"""
513 if not line.startswith(
'//%'):
514 return (
False,
False)
515 if line.startswith(
'//%PDDM'):
516 directive = line.split(
' ', 1)[0]
517 if directive ==
"//%PDDM-EXPAND":
519 if directive
not in (
'//%PDDM-DEFINE',
'//%PDDM-DEFINE-END'):
520 raise PDDMError(
'Ran into directive ("%s", line %d) while in "%s".' %
529 macro_collection.ParseLines([x[3:]
for x
in self.
lines])
530 except PDDMError
as e:
531 raise PDDMError(
'%s\n...while parsing section that started:\n'
536 """Section containing an import of PDDM-DEFINES from an external file."""
538 def __init__(self, first_line_num, import_resolver):
539 SourceFile.SectionBase.__init__(self, first_line_num)
543 if not line.startswith(
'//%PDDM-IMPORT-DEFINES '):
544 return (
False,
False)
550 if not macro_colletion:
553 raise PDDMError(
'Got an IMPORT-DEFINES without a resolver (line %d):'
557 if imported_file
is None:
558 raise PDDMError(
'Resolver failed to find "%s" (line %d):'
563 imported_src_file._ParseFile()
564 for section
in imported_src_file._sections:
565 section.BindMacroCollection(macro_colletion)
566 except PDDMError
as e:
567 raise PDDMError(
'%s\n...while importing defines:\n'
575 for line_num, line
in enumerate(lines, 1):
578 was_added, accept_more = cur_section.TryAppend(line, line_num)
581 was_added, accept_more = cur_section.TryAppend(line, line_num)
590 if not line.startswith(
'//%PDDM'):
593 directive = line.split(
' ', 1)[0]
594 if directive ==
'//%PDDM-EXPAND':
596 elif directive ==
'//%PDDM-DEFINE':
598 elif directive ==
'//%PDDM-IMPORT-DEFINES':
601 raise PDDMError(
'Unexpected line %d: "%s".' % (line_num, line))
606 """Processes the file contents."""
614 section.BindMacroCollection(collection)
617 result += section.text
630 usage =
'%prog [OPTIONS] PATH ...'
632 'Processes PDDM directives in the given paths and write them back out.'
634 parser = optparse.OptionParser(usage=usage, description=description)
635 parser.add_option(
'--dry-run',
636 default=
False, action=
'store_true',
637 help=
'Don\'t write back to the file(s), just report if the'
638 ' contents needs an update and exit with a value of 1.')
639 parser.add_option(
'--verbose',
640 default=
False, action=
'store_true',
641 help=
'Reports is a file is already current.')
642 parser.add_option(
'--collapse',
643 default=
False, action=
'store_true',
644 help=
'Removes all the generated code.')
645 opts, extra_args = parser.parse_args(args)
648 parser.error(
'Need at least one file to process')
651 for a_path
in extra_args:
652 if not os.path.exists(a_path):
653 sys.stderr.write(
'ERROR: File not found: %s\n' % a_path)
656 def _ImportResolver(name):
658 a_dir = os.path.dirname(a_path)
659 import_path = os.path.join(a_dir, name)
660 if not os.path.exists(import_path):
662 return open(import_path,
'r')
664 with open(a_path,
'r')
as f:
668 src_file.ProcessContent(strip_expansion=opts.collapse)
669 except PDDMError
as e:
670 sys.stderr.write(
'ERROR: %s\n...While processing "%s"\n' %
674 if src_file.processed_content != src_file.original_content:
676 print(
'Updating for "%s".' % a_path)
677 with open(a_path,
'w')
as f:
678 f.write(src_file.processed_content)
681 print(
'Update needed for "%s".' % a_path)
684 print(
'No update for "%s".' % a_path)
689 if __name__ ==
'__main__':
690 sys.exit(
main(sys.argv[1:]))