Skip to content

Commit d5c274f

Browse files
committed
add preprocessor that can replace simple #define values in code
The preprocessor strips all comments, blank lines and lines containing a define statement from the output. 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 521e924 commit d5c274f

File tree

6 files changed

+256
-2
lines changed

6 files changed

+256
-2
lines changed

esp32_ulp/__main__.py

+7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
from .util import garbage_collect
44

5+
from .preprocess import Preprocessor
56
from .assemble import Assembler
67
from .link import make_binary
78
garbage_collect('after import')
89

910

11+
def preprocess(src):
12+
preprocessor = Preprocessor()
13+
return preprocessor.preprocess(src)
14+
15+
1016
def src_to_binary(src):
1117
assembler = Assembler()
1218
assembler.assemble(src)
@@ -23,6 +29,7 @@ def main(fn):
2329
with open(fn) as f:
2430
src = f.read()
2531

32+
src = preprocess(src)
2633
binary = src_to_binary(src)
2734

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

esp32_ulp/preprocess.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 line[:7] != "#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 = [l for l in nocomment.remove_comments(content) if l != ""]
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

tests/00_unit_tests.sh

+1-1
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

+3-1
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

+7
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

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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_blank_lines_and_lines_with_defines():
84+
p = Preprocessor()
85+
86+
lines = """\
87+
// copyright
88+
#define A 1
89+
90+
move r1, r2"""
91+
92+
assert p.preprocess(lines) == "\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+
expected = "\tmove r1, 0x3ff48400"
105+
assert p.preprocess(lines) == expected
106+
107+
108+
@test
109+
def preprocess_should_replace_words_defined_multiple_times():
110+
p = Preprocessor()
111+
112+
lines = """\
113+
#define DR_REG_RTCIO_BASE 0x3ff48400
114+
115+
move r1, DR_REG_RTCIO_BASE #once
116+
move r2, DR_REG_RTCIO_BASE #second time"""
117+
118+
expected = """\
119+
\tmove r1, 0x3ff48400
120+
\tmove r2, 0x3ff48400"""
121+
122+
assert p.preprocess(lines) == expected
123+
124+
125+
@test
126+
def preprocess_should_replace_all_defined_words():
127+
p = Preprocessor()
128+
129+
lines = """\
130+
#define DR_REG_RTCIO_BASE 0x3ff48400
131+
#define SOME_OFFSET 4
132+
133+
move r1, DR_REG_RTCIO_BASE
134+
add r2, r1, SOME_OFFSET"""
135+
136+
expected = """\
137+
\tmove r1, 0x3ff48400
138+
\tadd r2, r1, 4"""
139+
140+
assert p.preprocess(lines) == expected
141+
142+
143+
@test
144+
def preprocess_should_not_replace_substrings_within_identifiers():
145+
p = Preprocessor()
146+
147+
# ie. if AAA is defined don't touch PREFIX_AAA_SUFFIX
148+
lines = """\
149+
#define RTCIO 4
150+
move r1, DR_REG_RTCIO_BASE"""
151+
152+
assert "DR_REG_4_BASE" not in p.preprocess(lines)
153+
154+
# ie. if A and AA are defined, don't replace AA as two A's but with AA
155+
lines = """\
156+
#define A 4
157+
#define AA 8
158+
move r1, A
159+
move r2, AA"""
160+
161+
expected = """\
162+
\tmove r1, 4
163+
\tmove r2, 8"""
164+
165+
assert p.preprocess(lines) == expected
166+
167+
168+
@test
169+
def preprocess_should_replace_defines_used_in_defines():
170+
p = Preprocessor()
171+
172+
lines = """\
173+
#define BITS (BASE << 4)
174+
#define BASE 0x1234
175+
176+
move r1, BITS
177+
move r2, BASE"""
178+
179+
assert "move r1, (0x1234 << 4)" in p.preprocess(lines)
180+
181+
182+
if __name__ == '__main__':
183+
# run all methods marked with @test
184+
for t in tests:
185+
t()

0 commit comments

Comments
 (0)