plcc-ng 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. plcc/__init__.py +0 -0
  2. plcc/cmd/__init__.py +0 -0
  3. plcc/cmd/make.py +140 -0
  4. plcc/cmd/make_test.py +74 -0
  5. plcc/cmd/parse.py +146 -0
  6. plcc/cmd/rep.py +190 -0
  7. plcc/cmd/scan.py +112 -0
  8. plcc/cmd/skeleton_test.py +0 -0
  9. plcc/diagram/__init__.py +0 -0
  10. plcc/diagram/dispatch.py +48 -0
  11. plcc/diagram/dispatch_test.py +86 -0
  12. plcc/diagram/list.py +68 -0
  13. plcc/diagram/list_test.py +32 -0
  14. plcc/diagram/plantuml/__init__.py +0 -0
  15. plcc/diagram/plantuml/emit.py +61 -0
  16. plcc/diagram/plantuml/emit_test.py +79 -0
  17. plcc/lang/__init__.py +0 -0
  18. plcc/lang/build.py +43 -0
  19. plcc/lang/build_test.py +22 -0
  20. plcc/lang/emit.py +51 -0
  21. plcc/lang/emit_test.py +29 -0
  22. plcc/lang/ext/__init__.py +0 -0
  23. plcc/lang/ext/java/__init__.py +0 -0
  24. plcc/lang/ext/java/build.py +53 -0
  25. plcc/lang/ext/java/emit.py +106 -0
  26. plcc/lang/ext/java/emit_test.py +180 -0
  27. plcc/lang/ext/java/run.py +50 -0
  28. plcc/lang/ext/java/runtime/Deserializer.java +46 -0
  29. plcc/lang/ext/java/runtime/Node.java +4 -0
  30. plcc/lang/ext/java/runtime/Registry.java +28 -0
  31. plcc/lang/ext/java/runtime/Token.java +16 -0
  32. plcc/lang/ext/java/runtime/org.json-20250107.jar +0 -0
  33. plcc/lang/ext/java/templates/Main.java.jinja +37 -0
  34. plcc/lang/ext/java/templates/class_file.java.jinja +39 -0
  35. plcc/lang/ext/python/__init__.py +0 -0
  36. plcc/lang/ext/python/emit.py +100 -0
  37. plcc/lang/ext/python/emit_test.py +132 -0
  38. plcc/lang/ext/python/run.py +44 -0
  39. plcc/lang/ext/python/runtime/__init__.py +0 -0
  40. plcc/lang/ext/python/runtime/base.py +8 -0
  41. plcc/lang/ext/python/runtime/base_test.py +17 -0
  42. plcc/lang/ext/python/runtime/deserialize.py +22 -0
  43. plcc/lang/ext/python/runtime/deserialize_test.py +115 -0
  44. plcc/lang/ext/python/runtime/registry.py +27 -0
  45. plcc/lang/ext/python/runtime/registry_test.py +67 -0
  46. plcc/lang/ext/python/templates/class_file.py.jinja +21 -0
  47. plcc/lang/ext/python/templates/main.py.jinja +22 -0
  48. plcc/lang/list.py +69 -0
  49. plcc/lang/list_test.py +27 -0
  50. plcc/lang/run.py +45 -0
  51. plcc/lines/Line.py +8 -0
  52. plcc/lines/__init__.py +2 -0
  53. plcc/lines/parseLines.py +16 -0
  54. plcc/lines/parse_from_file.py +10 -0
  55. plcc/lines/parse_from_string.py +5 -0
  56. plcc/lines/parse_from_string_test.py +54 -0
  57. plcc/lines/parse_from_strings.py +6 -0
  58. plcc/ll1/__init__.py +0 -0
  59. plcc/ll1/ll1_cli.py +64 -0
  60. plcc/ll1/ll1_cli_test.py +93 -0
  61. plcc/ll1/ll1_result_builder.py +122 -0
  62. plcc/ll1/ll1_result_builder_test.py +225 -0
  63. plcc/ll1/spec_json_decoder.py +70 -0
  64. plcc/ll1/spec_json_decoder_test.py +184 -0
  65. plcc/model/__init__.py +0 -0
  66. plcc/model/build_model.py +155 -0
  67. plcc/model/build_model_test.py +468 -0
  68. plcc/model/model_cli.py +44 -0
  69. plcc/model/model_cli_test.py +62 -0
  70. plcc/parser/__init__.py +0 -0
  71. plcc/parser/list_cli.py +67 -0
  72. plcc/parser/predictive_parser.py +152 -0
  73. plcc/parser/predictive_parser_test.py +263 -0
  74. plcc/parser/table_cli.py +89 -0
  75. plcc/parser/table_cli_test.py +161 -0
  76. plcc/scan/LexError.py +12 -0
  77. plcc/scan/Skip.py +11 -0
  78. plcc/scan/Token.py +9 -0
  79. plcc/scan/__init__.py +0 -0
  80. plcc/scan/matcher.py +61 -0
  81. plcc/scan/matcher_test.py +126 -0
  82. plcc/scan/scanner.py +23 -0
  83. plcc/scan/scanner_test.py +101 -0
  84. plcc/scan/sink.py +12 -0
  85. plcc/scan/sink_test.py +118 -0
  86. plcc/scan/source.py +25 -0
  87. plcc/scan/source_test.py +119 -0
  88. plcc/schemas/ll1.schema.json +65 -0
  89. plcc/schemas/model.schema.json +61 -0
  90. plcc/schemas/spec.schema.json +46 -0
  91. plcc/schemas/token.schema.json +21 -0
  92. plcc/schemas/tree.schema.json +34 -0
  93. plcc/spec/Spec.py +10 -0
  94. plcc/spec/SpecError.py +11 -0
  95. plcc/spec/SpecError_test.py +34 -0
  96. plcc/spec/ValidationError.py +4 -0
  97. plcc/spec/__init__.py +48 -0
  98. plcc/spec/lexical/DuplicateName.py +7 -0
  99. plcc/spec/lexical/LexicalRule.py +11 -0
  100. plcc/spec/lexical/LexicalSpec.py +12 -0
  101. plcc/spec/lexical/LexicalSpecError.py +5 -0
  102. plcc/spec/lexical/NameExpected.py +8 -0
  103. plcc/spec/lexical/Parser.py +83 -0
  104. plcc/spec/lexical/PatternCompilationError.py +6 -0
  105. plcc/spec/lexical/PatternDelimiterExpected.py +6 -0
  106. plcc/spec/lexical/PatternExpected.py +5 -0
  107. plcc/spec/lexical/UnexpectedContent.py +5 -0
  108. plcc/spec/lexical/__init__.py +10 -0
  109. plcc/spec/lexical/check_for_duplicate_names.py +12 -0
  110. plcc/spec/lexical/parseLexicalSpec.py +10 -0
  111. plcc/spec/lexical/parse_from_lines.py +4 -0
  112. plcc/spec/lexical/parse_from_string.py +7 -0
  113. plcc/spec/lexical/parse_lexical_test.py +247 -0
  114. plcc/spec/parseSpec.py +13 -0
  115. plcc/spec/parseSpec_test.py +50 -0
  116. plcc/spec/plcc_spec_cli.py +50 -0
  117. plcc/spec/plcc_spec_cli_test.py +49 -0
  118. plcc/spec/rough/Block.py +8 -0
  119. plcc/spec/rough/CircularIncludeError.py +5 -0
  120. plcc/spec/rough/Divider.py +11 -0
  121. plcc/spec/rough/Include.py +9 -0
  122. plcc/spec/rough/UnclosedBlockError.py +5 -0
  123. plcc/spec/rough/__init__.py +6 -0
  124. plcc/spec/rough/iterate_rough.py +16 -0
  125. plcc/spec/rough/parseRough.py +10 -0
  126. plcc/spec/rough/parseRough_test.py +75 -0
  127. plcc/spec/rough/parse_blocks.py +66 -0
  128. plcc/spec/rough/parse_blocks_test.py +95 -0
  129. plcc/spec/rough/parse_dividers.py +77 -0
  130. plcc/spec/rough/parse_dividers_test.py +88 -0
  131. plcc/spec/rough/parse_from_lines.py +6 -0
  132. plcc/spec/rough/parse_from_lines_test.py +9 -0
  133. plcc/spec/rough/parse_from_string.py +6 -0
  134. plcc/spec/rough/parse_includes.py +18 -0
  135. plcc/spec/rough/parse_includes_test.py +53 -0
  136. plcc/spec/rough/raise_handler.py +2 -0
  137. plcc/spec/rough/resolve_includes.py +69 -0
  138. plcc/spec/rough/resolve_includes_test.py +52 -0
  139. plcc/spec/semantics/CodeFragment.py +10 -0
  140. plcc/spec/semantics/InvalidClassNameError.py +10 -0
  141. plcc/spec/semantics/SemanticSpec.py +11 -0
  142. plcc/spec/semantics/TargetLocator.py +10 -0
  143. plcc/spec/semantics/UndefinedBlockError.py +10 -0
  144. plcc/spec/semantics/UndefinedTargetLocatorError.py +10 -0
  145. plcc/spec/semantics/__init__.py +2 -0
  146. plcc/spec/semantics/parse_code_fragments.py +57 -0
  147. plcc/spec/semantics/parse_code_fragments_test.py +83 -0
  148. plcc/spec/semantics/parse_semantic_spec.py +15 -0
  149. plcc/spec/semantics/parse_semantic_spec_test.py +44 -0
  150. plcc/spec/semantics/parse_target_locator.py +16 -0
  151. plcc/spec/semantics/parse_target_locator_test.py +31 -0
  152. plcc/spec/semantics/validation.py +48 -0
  153. plcc/spec/semantics/validation_test.py +105 -0
  154. plcc/spec/split_rough.py +27 -0
  155. plcc/spec/syntax/CapturingSymbol.py +14 -0
  156. plcc/spec/syntax/CapturingTerminal.py +9 -0
  157. plcc/spec/syntax/DuplicateAttribute.py +12 -0
  158. plcc/spec/syntax/DuplicateLhsError.py +12 -0
  159. plcc/spec/syntax/InvalidAttribute.py +12 -0
  160. plcc/spec/syntax/InvalidLhsAltNameError.py +12 -0
  161. plcc/spec/syntax/InvalidLhsNameError.py +12 -0
  162. plcc/spec/syntax/InvalidNonterminal.py +12 -0
  163. plcc/spec/syntax/InvalidSeparator.py +12 -0
  164. plcc/spec/syntax/InvalidSymbolException.py +8 -0
  165. plcc/spec/syntax/InvalidSyntacticSpecException.py +8 -0
  166. plcc/spec/syntax/InvalidTerminal.py +12 -0
  167. plcc/spec/syntax/LL1Error.py +9 -0
  168. plcc/spec/syntax/LhsNonTerminal.py +10 -0
  169. plcc/spec/syntax/MalformedBNFError.py +3 -0
  170. plcc/spec/syntax/NonTerminal.py +9 -0
  171. plcc/spec/syntax/RepeatingSyntacticRule.py +10 -0
  172. plcc/spec/syntax/RhsNonTerminal.py +9 -0
  173. plcc/spec/syntax/StandardSyntacticRule.py +8 -0
  174. plcc/spec/syntax/Symbol.py +6 -0
  175. plcc/spec/syntax/SyntacticRule.py +13 -0
  176. plcc/spec/syntax/SyntacticSpec.py +26 -0
  177. plcc/spec/syntax/Terminal.py +10 -0
  178. plcc/spec/syntax/UndefinedNonterminal.py +12 -0
  179. plcc/spec/syntax/UndefinedTerminalError.py +12 -0
  180. plcc/spec/syntax/__init__.py +1 -0
  181. plcc/spec/syntax/parse_syntactic_spec.py +142 -0
  182. plcc/spec/syntax/parse_syntactic_spec_test.py +411 -0
  183. plcc/spec/syntax/validations/__init__.py +0 -0
  184. plcc/spec/syntax/validations/ll1/Grammar.py +68 -0
  185. plcc/spec/syntax/validations/ll1/Grammar_test.py +115 -0
  186. plcc/spec/syntax/validations/ll1/LL1Wrapper.py +17 -0
  187. plcc/spec/syntax/validations/ll1/LL1Wrapper_test.py +119 -0
  188. plcc/spec/syntax/validations/ll1/__init__.py +1 -0
  189. plcc/spec/syntax/validations/ll1/build_first_sets.py +60 -0
  190. plcc/spec/syntax/validations/ll1/build_first_sets_test.py +94 -0
  191. plcc/spec/syntax/validations/ll1/build_follow_sets.py +79 -0
  192. plcc/spec/syntax/validations/ll1/build_follow_sets_test.py +106 -0
  193. plcc/spec/syntax/validations/ll1/build_parsing_table.py +69 -0
  194. plcc/spec/syntax/validations/ll1/build_parsing_table_test.py +37 -0
  195. plcc/spec/syntax/validations/ll1/build_spec_grammar.py +104 -0
  196. plcc/spec/syntax/validations/ll1/build_spec_grammar_test.py +112 -0
  197. plcc/spec/syntax/validations/ll1/check_left_recursion.py +96 -0
  198. plcc/spec/syntax/validations/ll1/check_left_recursion_test.py +134 -0
  199. plcc/spec/syntax/validations/ll1/check_ll1.py +31 -0
  200. plcc/spec/syntax/validations/ll1/check_ll1_test.py +30 -0
  201. plcc/spec/syntax/validations/ll1/check_parsing_table_for_ll1.py +10 -0
  202. plcc/spec/syntax/validations/ll1/check_parsing_table_for_ll1_test.py +38 -0
  203. plcc/spec/syntax/validations/replace_repeating_with_standard_rules.py +107 -0
  204. plcc/spec/syntax/validations/replace_repeating_with_standard_rules_test.py +138 -0
  205. plcc/spec/syntax/validations/validate_lhs.py +63 -0
  206. plcc/spec/syntax/validations/validate_lhs_test.py +136 -0
  207. plcc/spec/syntax/validations/validate_rhs.py +99 -0
  208. plcc/spec/syntax/validations/validate_rhs_test.py +122 -0
  209. plcc/spec/syntax/validations/validate_syntactic_spec.py +38 -0
  210. plcc/spec/syntax/validations/validate_syntactic_spec_test.py +62 -0
  211. plcc/spec/syntax/validations/validate_terminals_defined.py +46 -0
  212. plcc/spec/syntax/validations/validate_terminals_defined_test.py +250 -0
  213. plcc/tokens/__init__.py +0 -0
  214. plcc/tokens/jsonl_formatter.py +20 -0
  215. plcc/tokens/jsonl_formatter_test.py +33 -0
  216. plcc/tokens/spec_loader.py +22 -0
  217. plcc/tokens/spec_loader_test.py +31 -0
  218. plcc/tokens/tokens_cli.py +61 -0
  219. plcc/tokens/tokens_cli_test.py +73 -0
  220. plcc/tree/__init__.py +0 -0
  221. plcc/tree/tree_cli.py +47 -0
  222. plcc/tree/tree_cli_test.py +16 -0
  223. plcc/verbose.py +181 -0
  224. plcc/verbose_test.py +267 -0
  225. plcc_ng-0.1.2.dist-info/METADATA +63 -0
  226. plcc_ng-0.1.2.dist-info/RECORD +228 -0
  227. plcc_ng-0.1.2.dist-info/WHEEL +4 -0
  228. plcc_ng-0.1.2.dist-info/entry_points.txt +27 -0
plcc/__init__.py ADDED
File without changes
plcc/cmd/__init__.py ADDED
File without changes
plcc/cmd/make.py ADDED
@@ -0,0 +1,140 @@
1
+ import contextlib
2
+ import enum
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+
10
+ from docopt import docopt
11
+
12
+ from plcc.verbose import VerboseContext, VERBOSE_OPTIONS
13
+
14
+ __doc__ = """plcc-make
15
+ Build a PLCC project from a grammar file.
16
+
17
+ Usage:
18
+ plcc-make [options] GRAMMAR
19
+
20
+ Arguments:
21
+ GRAMMAR Path to the PLCC grammar file.
22
+
23
+ Options:
24
+ -h --help Show this message.
25
+ """ + VERBOSE_OPTIONS
26
+
27
+ _TOOL_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
28
+
29
+
30
+ class Events(enum.Enum):
31
+ STARTED = "started"
32
+ PHASE = "phase"
33
+ FINISHED = "finished"
34
+
35
+
36
+ def main(argv=None):
37
+ if argv is None:
38
+ argv = sys.argv[1:]
39
+ args = docopt(__doc__, argv)
40
+ verbose = VerboseContext.from_args("plcc-make", Events, args)
41
+ grammar = args['GRAMMAR']
42
+ build_dir = 'build'
43
+
44
+ verbose.emit(Events.STARTED, message=f"building {grammar}")
45
+
46
+ # 1. Clean
47
+ if os.path.exists(build_dir):
48
+ shutil.rmtree(build_dir)
49
+ os.makedirs(build_dir)
50
+
51
+ child_flags = verbose.child_flags_for_orchestrator(min_level=0)
52
+
53
+ # 2. Spec
54
+ verbose.emit(Events.PHASE, message="spec")
55
+ spec_json = os.path.join(build_dir, 'spec.json')
56
+ _run_or_die(['plcc-spec', grammar] + child_flags, stdout_file=spec_json, verbose=verbose)
57
+
58
+ # 3. LL(1)
59
+ verbose.emit(Events.PHASE, message="ll1")
60
+ ll1_json = os.path.join(build_dir, 'll1.json')
61
+ _run_or_die(['plcc-ll1'] + child_flags, stdin_file=spec_json, stdout_file=ll1_json, verbose=verbose)
62
+ with open(ll1_json) as f:
63
+ ll1 = json.load(f)
64
+ if not ll1.get("is_ll1", True):
65
+ _report_ll1_failure(ll1, ll1_json, verbose)
66
+ sys.exit(1)
67
+
68
+ # 4. Model
69
+ verbose.emit(Events.PHASE, message="model")
70
+ model_json = os.path.join(build_dir, 'model.json')
71
+ _run_or_die(['plcc-model', spec_json] + child_flags, stdout_file=model_json, verbose=verbose)
72
+
73
+ # 5 & 6. Emit and build per semantic section
74
+ with open(spec_json) as f:
75
+ spec = json.load(f)
76
+ for section in spec.get('semantics', []):
77
+ tool = section['tool']
78
+ lang = section['language']
79
+ try:
80
+ validate_tool_name(tool)
81
+ except ValueError as e:
82
+ print(f"plcc-make: {e}", file=sys.stderr)
83
+ sys.exit(1)
84
+ output_dir = os.path.join(build_dir, tool)
85
+ os.makedirs(output_dir, exist_ok=True)
86
+ verbose.emit(Events.PHASE, message=f"emit {lang} -> {tool}")
87
+ _run_or_die(
88
+ ['plcc-lang-emit', f'--target={lang}', f'--output={output_dir}'] + child_flags,
89
+ stdin_file=model_json,
90
+ verbose=verbose,
91
+ )
92
+ verbose.emit(Events.PHASE, message=f"build {lang} -> {tool}")
93
+ _run_or_die(
94
+ ['plcc-lang-build', f'--target={lang}', f'--output={output_dir}'] + child_flags,
95
+ verbose=verbose,
96
+ )
97
+
98
+ verbose.emit(Events.FINISHED, message="done")
99
+
100
+
101
+ def validate_tool_name(name):
102
+ if not name or not _TOOL_NAME_RE.match(name):
103
+ raise ValueError(
104
+ f"Invalid tool name '{name}'. "
105
+ "Tool names must match [a-zA-Z0-9_-]+ to prevent path traversal."
106
+ )
107
+
108
+
109
+ def _report_ll1_failure(ll1, path, verbose):
110
+ print(
111
+ f"plcc-make: error: grammar is not LL(1); see {path}",
112
+ file=sys.stderr,
113
+ )
114
+ for conflict in ll1.get("conflicts", []):
115
+ print(
116
+ f"plcc-make: error: conflict at "
117
+ f"{conflict.get('nonterminal', '?')} on "
118
+ f"{conflict.get('lookahead', '?')}: "
119
+ f"{conflict.get('productions', [])}",
120
+ file=sys.stderr,
121
+ )
122
+ for entry in ll1.get("left_recursion", []):
123
+ cycle = entry.get("cycle", [])
124
+ print(
125
+ f"plcc-make: error: left-recursion cycle: {' -> '.join(cycle)}",
126
+ file=sys.stderr,
127
+ )
128
+
129
+
130
+ def _run_or_die(cmd, stdout_file=None, stdin_file=None, verbose=None):
131
+ with contextlib.ExitStack() as stack:
132
+ stdin = stack.enter_context(open(stdin_file)) if stdin_file else None
133
+ stdout = stack.enter_context(open(stdout_file, 'w')) if stdout_file else None
134
+ result = subprocess.run(cmd, stdin=stdin, stdout=stdout, stderr=subprocess.PIPE)
135
+ if verbose and result.stderr:
136
+ events = verbose.parse_child_events(result.stderr.decode("utf-8", errors="replace"))
137
+ verbose.reformat_child_events(events)
138
+ if result.returncode != 0:
139
+ print(f"plcc-make: {cmd[0]} failed (exit {result.returncode})", file=sys.stderr)
140
+ sys.exit(result.returncode)
plcc/cmd/make_test.py ADDED
@@ -0,0 +1,74 @@
1
+ import pytest
2
+ import docopt
3
+
4
+ from .make import main as run_main, validate_tool_name, _report_ll1_failure
5
+
6
+
7
+ def test_no_args_prints_usage():
8
+ with pytest.raises((docopt.DocoptExit, SystemExit)):
9
+ run_main([])
10
+
11
+
12
+ def test_help(capsys):
13
+ with pytest.raises(SystemExit):
14
+ run_main(['--help'])
15
+ out, err = capsys.readouterr()
16
+ assert 'Usage' in out
17
+
18
+
19
+ def test_validate_tool_name_accepts_valid():
20
+ validate_tool_name('diagram')
21
+ validate_tool_name('Java')
22
+ validate_tool_name('my-tool')
23
+ validate_tool_name('tool_123')
24
+
25
+
26
+ def test_validate_tool_name_rejects_path_traversal():
27
+ with pytest.raises(ValueError):
28
+ validate_tool_name('../etc')
29
+ with pytest.raises(ValueError):
30
+ validate_tool_name('foo/bar')
31
+ with pytest.raises(ValueError):
32
+ validate_tool_name('/absolute')
33
+
34
+
35
+ def test_validate_tool_name_rejects_empty():
36
+ with pytest.raises(ValueError):
37
+ validate_tool_name('')
38
+
39
+
40
+ def test_report_ll1_failure_prints_error_and_conflicts(capsys):
41
+ ll1 = {
42
+ "is_ll1": False,
43
+ "conflicts": [
44
+ {"nonterminal": "E", "lookahead": "+", "competing": ["E + T", "E"]}
45
+ ],
46
+ "left_recursion": [],
47
+ }
48
+ _report_ll1_failure(ll1, "build/ll1.json", verbose=None)
49
+ _, err = capsys.readouterr()
50
+ assert "plcc-make: error:" in err
51
+ assert "build/ll1.json" in err
52
+ assert "E" in err
53
+ assert "+" in err
54
+
55
+
56
+ def test_report_left_recursion_cycle(capsys):
57
+ ll1 = {
58
+ "conflicts": [],
59
+ "left_recursion": [{"cycle": ["A", "B", "A"]}],
60
+ }
61
+ _report_ll1_failure(ll1, "build/ll1.json", None)
62
+ _, err = capsys.readouterr()
63
+ assert "A -> B -> A" in err
64
+
65
+
66
+ def test_report_conflict(capsys):
67
+ ll1 = {
68
+ "conflicts": [{"nonterminal": "E", "lookahead": "PLUS", "productions": []}],
69
+ "left_recursion": [],
70
+ }
71
+ _report_ll1_failure(ll1, "build/ll1.json", None)
72
+ _, err = capsys.readouterr()
73
+ assert "E" in err
74
+ assert "PLUS" in err
plcc/cmd/parse.py ADDED
@@ -0,0 +1,146 @@
1
+ import enum
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+
8
+ from docopt import docopt
9
+
10
+ from plcc.verbose import VerboseContext, VERBOSE_OPTIONS, reap_pipeline
11
+
12
+ __doc__ = """plcc-parse
13
+ Parse source input and print parse tree in human-readable format.
14
+
15
+ Usage:
16
+ plcc-parse [options] GRAMMAR [SOURCE ...]
17
+
18
+ Arguments:
19
+ GRAMMAR Path to the PLCC grammar file.
20
+ SOURCE Source files to parse. Reads stdin if none given.
21
+
22
+ Options:
23
+ -h --help Show this message.
24
+ """ + VERBOSE_OPTIONS
25
+
26
+
27
+ class Events(enum.Enum):
28
+ STARTED = "started"
29
+ FINISHED = "finished"
30
+
31
+
32
+ def _location_str(source):
33
+ file = source.get("file")
34
+ line = source.get("line", "?")
35
+ col = source.get("column", "?")
36
+ if file and file != "<stdin>":
37
+ return f"{file}:{line}:{col}"
38
+ return f"{line}:{col}"
39
+
40
+
41
+ def main(argv=None):
42
+ if argv is None:
43
+ argv = sys.argv[1:]
44
+ args = docopt(__doc__, argv)
45
+ verbose = VerboseContext.from_args("plcc-parse", Events, args)
46
+ grammar = args["GRAMMAR"]
47
+ sources = args["SOURCE"]
48
+
49
+ verbose.emit(Events.STARTED, message=f"parsing with {grammar}")
50
+ child_flags = verbose.child_flags_for_orchestrator(min_level=0)
51
+
52
+ spec_path = tempfile.mktemp(suffix=".json")
53
+ ll1_path = tempfile.mktemp(suffix=".json")
54
+ try:
55
+ # plcc-spec
56
+ _run_child(["plcc-spec", grammar] + child_flags, stdout_file=spec_path, verbose=verbose, label="plcc-spec")
57
+ # plcc-ll1
58
+ _run_child(["plcc-ll1"] + child_flags, stdin_file=spec_path, stdout_file=ll1_path, verbose=verbose, label="plcc-ll1")
59
+
60
+ # Build input
61
+ input_data = b""
62
+ for src in sources:
63
+ with open(src, "rb") as sf:
64
+ input_data += sf.read()
65
+ if not sources:
66
+ input_data = sys.stdin.buffer.read()
67
+
68
+ # plcc-tokens | plcc-tree
69
+ tokens_proc = subprocess.Popen(
70
+ ["plcc-tokens", spec_path] + child_flags,
71
+ stdin=subprocess.PIPE,
72
+ stdout=subprocess.PIPE,
73
+ stderr=subprocess.PIPE,
74
+ )
75
+ tree_proc = subprocess.Popen(
76
+ ["plcc-tree", f"--ll1={ll1_path}"] + child_flags,
77
+ stdin=tokens_proc.stdout,
78
+ stdout=subprocess.PIPE,
79
+ stderr=subprocess.PIPE,
80
+ )
81
+ tokens_proc.stdout.close()
82
+ tokens_proc.stdin.write(input_data)
83
+ tokens_proc.stdin.close()
84
+
85
+ tree_out, tree_err = tree_proc.communicate()
86
+ tokens_err = tokens_proc.stderr.read()
87
+ tokens_proc.wait()
88
+ tokens_proc.stderr_captured = tokens_err
89
+ tree_proc.stderr_captured = tree_err
90
+
91
+ result = reap_pipeline([
92
+ (tokens_proc, "plcc-tokens"),
93
+ (tree_proc, "plcc-tree"),
94
+ ])
95
+ verbose.reformat_child_events(result.events_to_render)
96
+ if result.failed_stage:
97
+ sys.exit(result.exit_code)
98
+
99
+ # Print tree in human-readable format
100
+ for line in tree_out.decode("utf-8").splitlines():
101
+ if not line.strip():
102
+ continue
103
+ tree = json.loads(line)
104
+ _print_tree(tree, indent=0)
105
+ finally:
106
+ for p in (spec_path, ll1_path):
107
+ if os.path.exists(p):
108
+ os.unlink(p)
109
+
110
+ verbose.emit(Events.FINISHED, message="done")
111
+
112
+
113
+ def _run_child(cmd, stdout_file, verbose, label, stdin_file=None):
114
+ with open(stdout_file, "w") as out:
115
+ stdin = open(stdin_file) if stdin_file else None
116
+ result = subprocess.run(cmd, stdin=stdin, stdout=out, stderr=subprocess.PIPE)
117
+ if stdin:
118
+ stdin.close()
119
+ if result.stderr:
120
+ events = verbose.parse_child_events(result.stderr.decode("utf-8", errors="replace"))
121
+ verbose.reformat_child_events(events)
122
+ if result.returncode != 0:
123
+ print(f"plcc-parse: {label} failed (exit {result.returncode})", file=sys.stderr)
124
+ sys.exit(result.returncode)
125
+
126
+
127
+ def _print_tree(node, indent):
128
+ prefix = " " * indent
129
+ kind = node.get("kind", "?")
130
+ if kind == "tree":
131
+ rule = node.get("rule", "?")
132
+ print(f"{prefix}{rule}")
133
+ for _field, child in node.get("children", []):
134
+ _print_tree(child, indent + 1)
135
+ elif kind == "token":
136
+ name = node.get("name", "?")
137
+ lexeme = node.get("lexeme", "?")
138
+ source = node.get("source", {})
139
+ loc = _location_str(source)
140
+ print(f"{prefix}{name} '{lexeme}' [{loc}]")
141
+ # forward-looking: plcc-tree may emit error records inline in a future protocol
142
+ elif kind == "error":
143
+ source = node.get("source", {})
144
+ loc = _location_str(source)
145
+ message = node.get("message", "unknown error")
146
+ print(f"{prefix}{loc}: error: {message}")
plcc/cmd/rep.py ADDED
@@ -0,0 +1,190 @@
1
+ import enum
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+
7
+ from docopt import docopt
8
+
9
+ from plcc.verbose import VerboseContext, VERBOSE_OPTIONS
10
+
11
+ __doc__ = """plcc-rep
12
+ REPL — read, eval, print loop for a PLCC grammar.
13
+
14
+ Usage:
15
+ plcc-rep [options] GRAMMAR [SOURCE ...]
16
+
17
+ Arguments:
18
+ GRAMMAR Path to the PLCC grammar file (build/ is resolved from the current directory).
19
+ SOURCE Source files to evaluate before entering interactive mode.
20
+
21
+ Options:
22
+ --tool=NAME Semantic section to run (inferred if only one exists).
23
+ -h --help Show this message.
24
+ """ + VERBOSE_OPTIONS
25
+
26
+
27
+ class Events(enum.Enum):
28
+ STARTED = "started"
29
+ FINISHED = "finished"
30
+
31
+
32
+ def main(argv=None):
33
+ if argv is None:
34
+ argv = sys.argv[1:]
35
+ args = docopt(__doc__, argv)
36
+ verbose = VerboseContext.from_args("plcc-rep", Events, args)
37
+ sources = args['SOURCE']
38
+ tool_name = args['--tool']
39
+ verbose_format = args['--verbose-format'] or 'text'
40
+
41
+ verbose.emit(Events.STARTED, message='starting rep')
42
+
43
+ spec_path = os.path.join('build', 'spec.json')
44
+ ll1_path = os.path.join('build', 'll1.json')
45
+
46
+ if not os.path.exists(spec_path) or not os.path.exists(ll1_path):
47
+ print('plcc-rep: build/ not found. Run plcc-make first.', file=sys.stderr)
48
+ sys.exit(1)
49
+
50
+ with open(spec_path) as f:
51
+ spec = json.load(f)
52
+
53
+ tool_name, language = _resolve_tool(spec, tool_name)
54
+ tool_dir = os.path.join('build', tool_name)
55
+
56
+ if not os.path.exists(tool_dir):
57
+ print(f'plcc-rep: build/{tool_name}/ not found. Run plcc-make first.', file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+ interpreter = subprocess.Popen(
61
+ ['plcc-lang-run', f'--target={language}', f'--output={tool_dir}'],
62
+ stdin=subprocess.PIPE,
63
+ stdout=subprocess.PIPE,
64
+ stderr=None,
65
+ )
66
+
67
+ try:
68
+ for src in sources:
69
+ with open(src, 'rb') as f:
70
+ chunk = f.read()
71
+ _eval_chunk(chunk, interpreter, spec_path, ll1_path, verbose_format)
72
+
73
+ if not sources:
74
+ interactive = sys.stdin.isatty()
75
+ if interactive:
76
+ while True:
77
+ try:
78
+ print('>>> ', end='', flush=True, file=sys.stderr)
79
+ line = sys.stdin.readline()
80
+ if not line:
81
+ break
82
+ chunk = line.encode()
83
+ _eval_chunk(chunk, interpreter, spec_path, ll1_path, verbose_format)
84
+ except KeyboardInterrupt:
85
+ print(file=sys.stderr)
86
+ break
87
+ else:
88
+ chunk = sys.stdin.buffer.read()
89
+ _eval_chunk(chunk, interpreter, spec_path, ll1_path, verbose_format)
90
+ finally:
91
+ try:
92
+ interpreter.stdin.close()
93
+ except BrokenPipeError:
94
+ pass
95
+ interpreter.wait()
96
+
97
+ verbose.emit(Events.FINISHED, message='done')
98
+
99
+
100
+ def _resolve_tool(spec, tool_name):
101
+ sections = spec.get('semantics', [])
102
+ if tool_name:
103
+ for s in sections:
104
+ if s['tool'] == tool_name:
105
+ return s['tool'], s['language']
106
+ print(f"plcc-rep: no semantic section with tool '{tool_name}'", file=sys.stderr)
107
+ sys.exit(1)
108
+
109
+ if len(sections) == 0:
110
+ print("plcc-rep: no semantic sections found. Run plcc-make first.", file=sys.stderr)
111
+ sys.exit(1)
112
+
113
+ if len(sections) == 1:
114
+ return sections[0]['tool'], sections[0]['language']
115
+
116
+ names = [s['tool'] for s in sections]
117
+ print(f"plcc-rep: multiple semantic sections: {names}. Use --tool=NAME.", file=sys.stderr)
118
+ sys.exit(1)
119
+
120
+
121
+ def _eval_chunk(chunk, interpreter, spec_path, ll1_path, verbose_format):
122
+ tokens_proc = subprocess.Popen(
123
+ ['plcc-tokens', spec_path],
124
+ stdin=subprocess.PIPE,
125
+ stdout=subprocess.PIPE,
126
+ stderr=subprocess.PIPE,
127
+ )
128
+ tree_proc = subprocess.Popen(
129
+ ['plcc-tree', f'--ll1={ll1_path}'],
130
+ stdin=tokens_proc.stdout,
131
+ stdout=subprocess.PIPE,
132
+ stderr=subprocess.PIPE,
133
+ )
134
+ tokens_proc.stdout.close()
135
+ tokens_proc.stdin.write(chunk)
136
+ tokens_proc.stdin.close()
137
+
138
+ tree_out, tree_err = tree_proc.communicate()
139
+ tokens_err = tokens_proc.stderr.read()
140
+ tokens_proc.wait()
141
+
142
+ if tokens_proc.returncode != 0 or tree_proc.returncode != 0:
143
+ for msg in [tokens_err, tree_err]:
144
+ if msg:
145
+ sys.stderr.buffer.write(msg)
146
+ return
147
+
148
+ tree_line = tree_out.strip()
149
+ if not tree_line:
150
+ return
151
+
152
+ try:
153
+ interpreter.stdin.write(tree_line + b'\n')
154
+ interpreter.stdin.flush()
155
+ except BrokenPipeError:
156
+ print('plcc-rep: interpreter exited unexpectedly', file=sys.stderr)
157
+ sys.exit(1)
158
+
159
+ _read_response(interpreter.stdout, verbose_format)
160
+
161
+
162
+ def _read_response(stdout, verbose_format):
163
+ while True:
164
+ raw = stdout.readline()
165
+ if not raw:
166
+ print('plcc-rep: interpreter exited unexpectedly', file=sys.stderr)
167
+ sys.exit(1)
168
+ line = raw.decode('utf-8', errors='replace').rstrip('\n')
169
+ try:
170
+ record = json.loads(line)
171
+ except json.JSONDecodeError:
172
+ print(line)
173
+ continue
174
+ if 'kind' not in record:
175
+ print(line)
176
+ continue
177
+ _render_record(record, verbose_format)
178
+ return
179
+
180
+
181
+ def _render_record(record, verbose_format):
182
+ if verbose_format == 'json':
183
+ print(json.dumps(record))
184
+ return
185
+ if record['kind'] == 'result':
186
+ value = record.get('value')
187
+ if value is not None:
188
+ print(value)
189
+ elif record['kind'] == 'error':
190
+ print(f"error: {record.get('type')}: {record.get('message')}", file=sys.stderr)
plcc/cmd/scan.py ADDED
@@ -0,0 +1,112 @@
1
+ import enum
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+
8
+ from docopt import docopt
9
+
10
+ from plcc.verbose import VerboseContext, VERBOSE_OPTIONS
11
+
12
+
13
+ def _location_str(source):
14
+ file = source.get("file")
15
+ line = source.get("line", "?")
16
+ col = source.get("column", "?")
17
+ if file and file != "<stdin>":
18
+ return f"{file}:{line}:{col}"
19
+ return f"{line}:{col}"
20
+
21
+
22
+ __doc__ = """plcc-scan
23
+ Tokenize source input and print tokens in human-readable format.
24
+
25
+ Usage:
26
+ plcc-scan [options] GRAMMAR [SOURCE ...]
27
+
28
+ Arguments:
29
+ GRAMMAR Path to the PLCC grammar file.
30
+ SOURCE Source files to tokenize. Reads stdin if none given.
31
+
32
+ Options:
33
+ -h --help Show this message.
34
+ """ + VERBOSE_OPTIONS
35
+
36
+
37
+ class Events(enum.Enum):
38
+ STARTED = "started"
39
+ FINISHED = "finished"
40
+
41
+
42
+ def main(argv=None):
43
+ if argv is None:
44
+ argv = sys.argv[1:]
45
+ args = docopt(__doc__, argv)
46
+ verbose = VerboseContext.from_args("plcc-scan", Events, args)
47
+ grammar = args["GRAMMAR"]
48
+ sources = args["SOURCE"]
49
+
50
+ verbose.emit(Events.STARTED, message=f"scanning with {grammar}")
51
+ child_flags = verbose.child_flags_for_orchestrator(min_level=0)
52
+
53
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
54
+ spec_path = f.name
55
+ try:
56
+ # plcc-spec grammar > spec.json
57
+ with open(spec_path, "w") as spec_out:
58
+ result = subprocess.run(
59
+ ["plcc-spec", grammar] + child_flags,
60
+ stdout=spec_out,
61
+ stderr=subprocess.PIPE,
62
+ )
63
+ if result.stderr:
64
+ events = verbose.parse_child_events(result.stderr.decode("utf-8", errors="replace"))
65
+ verbose.reformat_child_events(events)
66
+ if result.returncode != 0:
67
+ print(f"plcc-scan: plcc-spec failed (exit {result.returncode})", file=sys.stderr)
68
+ sys.exit(result.returncode)
69
+
70
+ # Build input: concatenate source files, then stdin
71
+ input_data = b""
72
+ for src in sources:
73
+ with open(src, "rb") as sf:
74
+ input_data += sf.read()
75
+ if not sources:
76
+ input_data = sys.stdin.buffer.read()
77
+
78
+ # plcc-tokens spec.json < input
79
+ result = subprocess.run(
80
+ ["plcc-tokens", spec_path] + child_flags,
81
+ input=input_data,
82
+ stdout=subprocess.PIPE,
83
+ stderr=subprocess.PIPE,
84
+ )
85
+ if result.stderr:
86
+ events = verbose.parse_child_events(result.stderr.decode("utf-8", errors="replace"))
87
+ verbose.reformat_child_events(events)
88
+ if result.returncode != 0:
89
+ # lex error: plcc-tokens already emitted the error to stderr via verbose;
90
+ # treat as non-fatal — pipeline completed with an error in-band
91
+ pass
92
+ else:
93
+ for line in result.stdout.decode("utf-8").splitlines():
94
+ if not line.strip():
95
+ continue
96
+ record = json.loads(line)
97
+ if record.get("kind") == "token":
98
+ name = record.get("name", "?")
99
+ lexeme = record.get("lexeme", "?")
100
+ source = record.get("source", {})
101
+ loc = _location_str(source)
102
+ print(f"{loc} {name} '{lexeme}'")
103
+ # forward-looking: plcc-tokens may emit error records inline in a future protocol
104
+ elif record.get("kind") == "error":
105
+ source = record.get("source", {})
106
+ loc = _location_str(source)
107
+ message = record.get("message", "unknown error")
108
+ print(f"{loc}: error: {message}")
109
+ finally:
110
+ os.unlink(spec_path)
111
+
112
+ verbose.emit(Events.FINISHED, message="done")
File without changes
File without changes