Skip to content

Preprocessor for ULP/RTC macros #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
79db90f
add units test for the .set directive
wnienhaus Jul 22, 2021
84d734d
add support for left aligned assembler directives (e.g. .set)
wnienhaus Jul 22, 2021
ec81ecc
fix a crash bug where BSS size calculation was attempted on the value…
wnienhaus Jul 22, 2021
c184924
raise error when attempting to store values in .bss section
wnienhaus Jul 29, 2021
25d34b0
fix reference to non-existing variable
wnienhaus Jul 22, 2021
76a81ac
fix typo in comment of instruction definition
wnienhaus Jul 22, 2021
56f4530
add support for the .global directive. only symbols flagged as global…
wnienhaus Jul 22, 2021
9907b10
let SymbolTable.export() optionally export non-global symbols too
wnienhaus Jul 22, 2021
27ab850
support ULP opcodes in upper case
wnienhaus Jul 22, 2021
54b117e
add a compatibility test for the recent fixes and improvements
wnienhaus Jul 22, 2021
feb42dc
add support for evaluating expressions
wnienhaus Jul 22, 2021
87507c9
add a compatibility test for evaluating expressions
wnienhaus Jul 23, 2021
99352a3
docs: add that expressions are now supported
wnienhaus Jul 29, 2021
d76fd26
add preprocessor that can replace simple #define values in code
wnienhaus Jul 23, 2021
4dded94
allow assembler to skip comment removal to avoid removing comments twice
wnienhaus Aug 7, 2021
219f939
fix evaluation of expressions during first assembler pass
wnienhaus Jul 25, 2021
5c3eeb8
remove no-longer-needed pass dependent code from SymbolTable
wnienhaus Jul 26, 2021
3e8c0d5
add support for macros such as WRITE_RTC_REG
wnienhaus Jul 26, 2021
ac1de99
add simple include file processing
wnienhaus Jul 26, 2021
8d88fd1
add support for using a btree database (DefinesDB) to store defines f…
wnienhaus Jul 27, 2021
46f1442
add special handling for the BIT macro used in the esp-idf framework
wnienhaus Jul 27, 2021
2f6ee78
add include processor tool for populating a defines.db from include f…
wnienhaus Jul 28, 2021
69ae946
add compatibility tests using good example code off the net
wnienhaus Jul 28, 2021
4f90f76
add documentation for the preprocessor
wnienhaus Jul 29, 2021
d44384f
fix use of treg field in i_move instruction to match binutils-esp32 o…
wnienhaus Jul 28, 2021
254adf9
allow specifying the address for reg_rd and reg_wr in 32-bit words
wnienhaus Jul 28, 2021
c3bd101
support .int data type
wnienhaus Jul 29, 2021
2a0a39a
refactor: small improvements based on PR comments.
wnienhaus Aug 9, 2021
47d5e8a
Updated LICENSE file and added AUTHORS file
wnienhaus Aug 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions esp32_ulp/definesdb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os
import btree
from .util import file_exists

DBNAME = 'defines.db'


class DefinesDB:
def __init__(self):
self._file = None
self._db = None
self._db_exists = None

def clear(self):
self.close()
try:
os.remove(DBNAME)
self._db_exists = False
except OSError:
pass

def open(self):
if self._db:
return
try:
self._file = open(DBNAME, 'r+b')
except OSError:
self._file = open(DBNAME, 'w+b')
self._db = btree.open(self._file)
self._db_exists = True

def close(self):
if not self._db:
return
self._db.close()
self._db = None
self._file.close()
self._file = None

def db_exists(self):
if self._db_exists is None:
self._db_exists = file_exists(DBNAME)
return self._db_exists

def update(self, dictionary):
for k, v in dictionary.items():
self.__setitem__(k, v)

def get(self, key, default):
try:
result = self.__getitem__(key)
except KeyError:
result = default
return result

def keys(self):
if not self.db_exists():
return []

self.open()
return [k.decode() for k in self._db.keys()]

def __getitem__(self, key):
if not self.db_exists():
raise KeyError

self.open()
return self._db[key.encode()].decode()

def __setitem__(self, key, value):
self.open()
self._db[key.encode()] = str(value).encode()

def __iter__(self):
return iter(self.keys())
61 changes: 45 additions & 16 deletions esp32_ulp/preprocess.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import nocomment
from .util import split_tokens
from .definesdb import DefinesDB


class RTC_Macros:
Expand Down Expand Up @@ -56,6 +57,7 @@ def parse_define_line(self, line):
def parse_defines(self, content):
for line in content.splitlines():
self._defines.update(self.parse_define_line(line))

return self._defines

def expand_defines(self, line):
Expand All @@ -66,21 +68,22 @@ def expand_defines(self, line):
line = ""
for t in tokens:
lu = self._defines.get(t, t)
if lu == t and self._defines_db:
lu = self._defines_db.get(t, t)
if lu != t:
found = True
line += lu

return line

def process_include_file(self, filename):
defines = self._defines

with open(filename, 'r') as f:
for line in f:
result = self.parse_defines(line)
defines.update(result)
with self.open_db() as db:
with open(filename, 'r') as f:
for line in f:
result = self.parse_define_line(line)
db.update(result)

return defines
return db

def expand_rtc_macros(self, line):
clean_line = line.strip()
Expand All @@ -103,17 +106,43 @@ def expand_rtc_macros(self, line):

return macro_fn(*macro_args)

def use_db(self, defines_db):
self._defines_db = defines_db

def open_db(self):
class ctx:
def __init__(self, db):
self._db = db

def __enter__(self):
# not opening DefinesDB - it opens itself when needed
return self._db

def __exit__(self, type, value, traceback):
if isinstance(self._db, DefinesDB):
self._db.close()

if self._defines_db:
return ctx(self._defines_db)

return ctx(self._defines)
Comment on lines +121 to +136
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you try having the contextmanager inside DefinesDB class?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it works, also call the context manager from the tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried to put the context manager into the DefinesDB class, but because it handles both the "with DefinesDB" and the "without a DefinesDB" case, this doesn't fit into the DefinesDB class.

I could find an elegant way to add a condition to the "with" statement - ie. only use the context manager from DefinesDB when we're actually using the DefinesDB, otherwise work with the class-internal dict.

I'll mull over this a bit more and see if I can find a way.

Regarding testing: in effect this was tested - because the existing tests exercise the code which uses the context manager, but I added another test now to check specifically that the db is actually closed too.


def preprocess(self, content):
self.parse_defines(content)
lines = nocomment.remove_comments(content)
result = []
for line in lines:
line = self.expand_defines(line)
line = self.expand_rtc_macros(line)
result.append(line)
result = "\n".join(result)

with self.open_db():
lines = nocomment.remove_comments(content)
result = []
for line in lines:
line = self.expand_defines(line)
line = self.expand_rtc_macros(line)
result.append(line)
result = "\n".join(result)

return result


def preprocess(content):
return Preprocessor().preprocess(content)
def preprocess(content, use_defines_db=True):
preprocessor = Preprocessor()
preprocessor.use_db(DefinesDB())
return preprocessor.preprocess(content)
10 changes: 10 additions & 0 deletions esp32_ulp/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
DEBUG = False

import gc
import os

NORMAL, WHITESPACE = 0, 1

Expand Down Expand Up @@ -67,3 +68,12 @@ def validate_expression(param):
if c not in '0123456789abcdef':
state = 0
return True


def file_exists(filename):
try:
os.stat(filename)
return True
except OSError:
pass
return False
2 changes: 1 addition & 1 deletion tests/00_unit_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

set -e

for file in opcodes assemble link util preprocess; do
for file in opcodes assemble link util preprocess definesdb; do
echo testing $file...
micropython $file.py
done
60 changes: 60 additions & 0 deletions tests/definesdb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import os

from esp32_ulp.definesdb import DefinesDB, DBNAME
from esp32_ulp.util import file_exists

tests = []


def test(param):
tests.append(param)


@test
def test_definesdb_clear_removes_all_keys():
db = DefinesDB()
db.open()
db.update({'KEY1': 'VALUE1'})

db.clear()

assert 'KEY1' not in db

db.close()


@test
def test_definesdb_persists_data_across_instantiations():
db = DefinesDB()
db.open()
db.clear()

db.update({'KEY1': 'VALUE1'})

assert 'KEY1' in db

db.close()
del db
db = DefinesDB()
db.open()

assert db.get('KEY1', None) == 'VALUE1'

db.close()


@test
def test_definesdb_should_not_create_a_db_file_when_only_reading():
db = DefinesDB()

db.clear()
assert not file_exists(DBNAME)

assert db.get('some-key', None) is None
assert not file_exists(DBNAME)


if __name__ == '__main__':
# run all methods marked with @test
for t in tests:
t()
93 changes: 93 additions & 0 deletions tests/preprocess.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import os

from esp32_ulp.preprocess import Preprocessor
from esp32_ulp.definesdb import DefinesDB, DBNAME
from esp32_ulp.util import file_exists

tests = []

Expand Down Expand Up @@ -186,6 +190,7 @@ def test_process_include_file():
p = Preprocessor()

defines = p.process_include_file('fixtures/incl.h')

assert defines['CONST1'] == '42'
assert defines['CONST2'] == '99'
assert defines.get('MULTI_LINE', None) == 'abc \\' # correct. line continuations not supported
Expand All @@ -204,6 +209,94 @@ def test_process_include_file_with_multiple_files():
assert defines['CONST3'] == '777', "constant from incl2.h"


@test
def test_process_include_file_using_database():
db = DefinesDB()
db.clear()

p = Preprocessor()
p.use_db(db)

p.process_include_file('fixtures/incl.h')
p.process_include_file('fixtures/incl2.h')

assert db['CONST1'] == '42', "constant from incl.h"
assert db['CONST2'] == '123', "constant overridden by incl2.h"
assert db['CONST3'] == '777', "constant from incl2.h"

db.close()


@test
def test_process_include_file_should_not_load_database_keys_into_instance_defines_dictionary():
db = DefinesDB()
db.clear()

p = Preprocessor()
p.use_db(db)

p.process_include_file('fixtures/incl.h')

# a bit hackish to reference instance-internal state
# but it's important to verify this, as we otherwise run out of memory on device
assert 'CONST2' not in p._defines



@test
def test_preprocess_should_use_definesdb_when_provided():
p = Preprocessor()

content = """\
#define LOCALCONST 42

entry:
move r1, LOCALCONST
move r2, DBKEY
"""

# first try without db
result = p.preprocess(content)

assert "move r1, 42" in result
assert "move r2, DBKEY" in result
assert "move r2, 99" not in result

# now try with db
db = DefinesDB()
db.clear()
db.update({'DBKEY': '99'})
p.use_db(db)

result = p.preprocess(content)

assert "move r1, 42" in result
assert "move r2, 99" in result
assert "move r2, DBKEY" not in result


@test
def test_preprocess_should_ensure_no_definesdb_is_created_when_only_reading_from_it():
content = """\
#define CONST 42
move r1, CONST"""

# remove any existing db
db = DefinesDB()
db.clear()
assert not file_exists(DBNAME)

# now preprocess using db
p = Preprocessor()
p.use_db(db)

result = p.preprocess(content)

assert "move r1, 42" in result

assert not file_exists(DBNAME)


if __name__ == '__main__':
# run all methods marked with @test
for t in tests:
Expand Down
Loading