Skip to content

Commit d76fd26

Browse files
committed
add preprocessor that can replace simple #define values in code
The preprocessor strips all comments and lines containing a define statement but keeps the empty lines in the output (to preserve line numbering). The output is then passed directly into the assembler. The preprocessor does not support "function style" #define macros (i.e. ADD(a,b) a+b) but this is not needed for expanding the constants used by WRITE_RTC_REG(), et al.
1 parent 99352a3 commit d76fd26

File tree

6 files changed

+245
-2
lines changed

6 files changed

+245
-2
lines changed

esp32_ulp/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .util import garbage_collect
44

5+
from .preprocess import preprocess
56
from .assemble import Assembler
67
from .link import make_binary
78
garbage_collect('after import')
@@ -23,6 +24,7 @@ def main(fn):
2324
with open(fn) as f:
2425
src = f.read()
2526

27+
src = preprocess(src)
2628
binary = src_to_binary(src)
2729

2830
if fn.endswith('.s') or fn.endswith('.S'):

esp32_ulp/preprocess.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from . import nocomment
2+
from .util import split_tokens
3+
4+
5+
class Preprocessor:
6+
def __init__(self):
7+
self._defines = {}
8+
9+
def parse_defines(self, content):
10+
result = {}
11+
for line in content.splitlines():
12+
line = line.strip()
13+
if not line.startswith("#define"):
14+
# skip lines not containing #define
15+
continue
16+
line = line[8:].strip() # remove #define
17+
parts = line.split(None, 1)
18+
if len(parts) != 2:
19+
# skip defines without value
20+
continue
21+
identifier, value = parts
22+
tmp = identifier.split('(', 1)
23+
if len(tmp) == 2:
24+
# skip parameterised defines (macros)
25+
continue
26+
value = "".join(nocomment.remove_comments(value)).strip()
27+
result[identifier] = value
28+
self._defines = result
29+
return result
30+
31+
def expand_defines(self, line):
32+
found = True
33+
while found: # do as many passed as needed, until nothing was replaced anymore
34+
found = False
35+
tokens = split_tokens(line)
36+
line = ""
37+
for t in tokens:
38+
lu = self._defines.get(t, t)
39+
if lu != t:
40+
found = True
41+
line += lu
42+
43+
return line
44+
45+
def preprocess(self, content):
46+
self.parse_defines(content)
47+
lines = nocomment.remove_comments(content)
48+
result = []
49+
for line in lines:
50+
line = self.expand_defines(line)
51+
result.append(line)
52+
result = "\n".join(result)
53+
return result
54+
55+
56+
def preprocess(content):
57+
return Preprocessor().preprocess(content)

tests/00_unit_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
set -e
66

7-
for file in opcodes assemble link util; do
7+
for file in opcodes assemble link util preprocess; do
88
echo testing $file...
99
micropython $file.py
1010
done

tests/01_compat_tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ for src_file in $(ls -1 compat/*.S); do
1313
log_file="${src_name}.log"
1414
micropython -m esp32_ulp $src_file 1>$log_file # generates $ulp_file
1515

16+
pre_file="${src_name}.pre"
1617
obj_file="${src_name}.o"
1718
elf_file="${src_name}.elf"
1819
bin_file="${src_name}.bin"
1920

2021
echo -e "\tBuilding using binutils"
21-
esp32ulp-elf-as -o $obj_file $src_file
22+
gcc -E -o ${pre_file} $src_file
23+
esp32ulp-elf-as -o $obj_file ${pre_file}
2224
esp32ulp-elf-ld -T esp32.ulp.ld -o $elf_file $obj_file
2325
esp32ulp-elf-objcopy -O binary $elf_file $bin_file
2426

tests/compat/preprocess_simple.S

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#define GPIO 2
2+
#define BASE 0x100
3+
#define ADDR (BASE + GPIO)
4+
5+
entry:
6+
move r0, GPIO
7+
move r1, ADDR

tests/preprocess.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from esp32_ulp.preprocess import Preprocessor
2+
3+
tests = []
4+
5+
6+
def test(param):
7+
tests.append(param)
8+
9+
10+
@test
11+
def test_replace_defines_should_return_empty_line_given_empty_string():
12+
p = Preprocessor()
13+
14+
assert p.preprocess("") == ""
15+
16+
17+
@test
18+
def replace_defines_should_return_remove_comments():
19+
p = Preprocessor()
20+
21+
line = "// some comment"
22+
expected = ""
23+
assert p.preprocess(line) == expected
24+
25+
26+
@test
27+
def test_parse_defines():
28+
p = Preprocessor()
29+
30+
assert p.parse_defines("") == {}
31+
assert p.parse_defines("// comment") == {}
32+
assert p.parse_defines(" // comment") == {}
33+
assert p.parse_defines(" /* comment */") == {}
34+
assert p.parse_defines(" /* comment */ #define A 42") == {} # #define must be the first thing on a line
35+
assert p.parse_defines("#define a 1") == {"a": "1"}
36+
assert p.parse_defines(" #define a 1") == {"a": "1"}
37+
assert p.parse_defines("#define a 1 2") == {"a": "1 2"}
38+
assert p.parse_defines("#define f(a,b) 1") == {} # macros not supported
39+
assert p.parse_defines("#define f(a, b) 1") == {} # macros not supported
40+
assert p.parse_defines("#define f (a,b) 1") == {"f": "(a,b) 1"} # f is not a macro
41+
assert p.parse_defines("#define f (a, b) 1") == {"f": "(a, b) 1"} # f is not a macro
42+
assert p.parse_defines("#define RTC_ADDR 0x12345 // start of range") == {"RTC_ADDR": "0x12345"}
43+
44+
45+
@test
46+
def test_parse_defines_handles_multiple_input_lines():
47+
p = Preprocessor()
48+
49+
multi_line_1 = """\
50+
#define ID_WITH_UNDERSCORE something
51+
#define ID2 somethingelse
52+
"""
53+
assert p.parse_defines(multi_line_1) == {"ID_WITH_UNDERSCORE": "something", "ID2": "somethingelse"}
54+
55+
56+
@test
57+
def test_parse_defines_does_not_understand_comments_by_current_design():
58+
# comments are not understood. lines are expected to already have comments removed!
59+
p = Preprocessor()
60+
61+
multi_line_2 = """\
62+
#define ID_WITH_UNDERSCORE something
63+
/*
64+
#define ID2 somethingelse
65+
*/
66+
"""
67+
assert "ID2" in p.parse_defines(multi_line_2)
68+
69+
70+
@test
71+
def test_parse_defines_does_not_understand_line_continuations_with_backslash_by_current_design():
72+
p = Preprocessor()
73+
74+
multi_line_3 = r"""
75+
#define ID_WITH_UNDERSCORE something \
76+
line2
77+
"""
78+
79+
assert p.parse_defines(multi_line_3) == {"ID_WITH_UNDERSCORE": "something \\"}
80+
81+
82+
@test
83+
def preprocess_should_remove_comments_and_defines_but_keep_the_lines_as_empty_lines():
84+
p = Preprocessor()
85+
86+
lines = """\
87+
// copyright
88+
#define A 1
89+
90+
move r1, r2"""
91+
92+
assert p.preprocess(lines) == "\n\n\n\tmove r1, r2"
93+
94+
95+
@test
96+
def preprocess_should_replace_words_defined():
97+
p = Preprocessor()
98+
99+
lines = """\
100+
#define DR_REG_RTCIO_BASE 0x3ff48400
101+
102+
move r1, DR_REG_RTCIO_BASE"""
103+
104+
assert "move r1, 0x3ff48400" in p.preprocess(lines)
105+
106+
107+
@test
108+
def preprocess_should_replace_words_defined_multiple_times():
109+
p = Preprocessor()
110+
111+
lines = """\
112+
#define DR_REG_RTCIO_BASE 0x3ff48400
113+
114+
move r1, DR_REG_RTCIO_BASE #once
115+
move r2, DR_REG_RTCIO_BASE #second time"""
116+
117+
assert "move r1, 0x3ff48400" in p.preprocess(lines)
118+
assert "move r2, 0x3ff48400" in p.preprocess(lines)
119+
120+
121+
@test
122+
def preprocess_should_replace_all_defined_words():
123+
p = Preprocessor()
124+
125+
lines = """\
126+
#define DR_REG_RTCIO_BASE 0x3ff48400
127+
#define SOME_OFFSET 4
128+
129+
move r1, DR_REG_RTCIO_BASE
130+
add r2, r1, SOME_OFFSET"""
131+
132+
assert "move r1, 0x3ff48400" in p.preprocess(lines)
133+
assert "add r2, r1, 4" in p.preprocess(lines)
134+
135+
136+
@test
137+
def preprocess_should_not_replace_substrings_within_identifiers():
138+
p = Preprocessor()
139+
140+
# ie. if AAA is defined don't touch PREFIX_AAA_SUFFIX
141+
lines = """\
142+
#define RTCIO 4
143+
move r1, DR_REG_RTCIO_BASE"""
144+
145+
assert "DR_REG_4_BASE" not in p.preprocess(lines)
146+
147+
# ie. if A and AA are defined, don't replace AA as two A's but with AA
148+
lines = """\
149+
#define A 4
150+
#define AA 8
151+
move r1, A
152+
move r2, AA"""
153+
154+
assert "move r1, 4" in p.preprocess(lines)
155+
assert "move r2, 8" in p.preprocess(lines)
156+
157+
158+
@test
159+
def preprocess_should_replace_defines_used_in_defines():
160+
p = Preprocessor()
161+
162+
lines = """\
163+
#define BITS (BASE << 4)
164+
#define BASE 0x1234
165+
166+
move r1, BITS
167+
move r2, BASE"""
168+
169+
assert "move r1, (0x1234 << 4)" in p.preprocess(lines)
170+
171+
172+
if __name__ == '__main__':
173+
# run all methods marked with @test
174+
for t in tests:
175+
t()

0 commit comments

Comments
 (0)