Skip to content

Commit

Permalink
added ability to mute byte code emission
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelkamprath committed Apr 21, 2024
1 parent db2dc19 commit 742c7ae
Show file tree
Hide file tree
Showing 18 changed files with 158 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Changes that are planned but not implemented yet:
* Added `.align` directive to align the current address to a multiple of a given value.
* Changed syntax highlight color theme name to be specific to the language rather than the generic "BespokeASM Theme" name.
* Added optional support for embedded strings in the assembly code. When enabled, strings can be ebdedded in the code withou a data directive such as `.cstr`. This is enabled by setting the `allow_embedded_strings` option in the `general` section of the configuration file to `true`.
* Added ability to mute byte code emission with the preprocessor directive `#mute` and unmute with `#unmute`.

## [0.4.1]
* added `.asciiz` as an equivalent data directive to `.cstr`
Expand Down
1 change: 1 addition & 0 deletions examples/slu4-minimal-64x4/software/stars.min64x4
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ UpdateStars: MIV STARDATA,ptr

STARDATA: ; put star data here

#mute
.org 0x0080 ; zero-page variables and constants

xa: .2byte 0xffff ; MinOS graphics interface (_SetPixel, _ClearPixel)
Expand Down
1 change: 1 addition & 0 deletions examples/slu4-minimal-64x4/software/vga.min64x4
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ VGA_Fill: STB vf_loopx+1 ; save fill value
DEZ 1 FNE vf_loopy
RTS

#mute
.org 0x0080

sx: .2byte 0xffff ; sprite engine
Expand Down
1 change: 1 addition & 0 deletions src/bespokeasm/assembler/assembly_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def load_line_objects(
for lobj in lobj_list:
if not isinstance(lobj, ConditionLine):
lobj.compilable = condition_stack.currently_active(preprocessor)
lobj.is_muted = condition_stack.is_muted

if lobj.compilable:
if isinstance(lobj, LabelLine):
Expand Down
8 changes: 6 additions & 2 deletions src/bespokeasm/assembler/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ def assemble_bytecode(self):
# Sort lines according to their assigned address. This allows for .org directives
compilable_line_obs.sort(key=lambda x: x.address)
max_generated_address = compilable_line_obs[-1].address
line_dict = {lobj.address: lobj for lobj in compilable_line_obs if isinstance(lobj, LineWithBytes)}
line_dict = {
lobj.address: lobj
for lobj in compilable_line_obs
if isinstance(lobj, LineWithBytes) and not lobj.is_muted
}

# second pass: build the machine code and check for overlaps
if self._verbose > 2:
Expand All @@ -146,7 +150,7 @@ def assemble_bytecode(self):
)
last_line = lobj

# Finally generate the binaey image
# Finally generate the binary image
fill_bytes = bytearray([self._binary_fill_value])
addr = self._binary_start

Expand Down
1 change: 1 addition & 0 deletions src/bespokeasm/assembler/keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
PREPROCESSOR_DIRECTIVES_SET = {
'include', 'require', 'create_memzone',
'define', 'if', 'elif', 'else', 'endif', 'ifdef', 'ifndef',
'mute', 'unmute', 'emit',
}

EXPRESSION_FUNCTIONS_SET = set([
Expand Down
14 changes: 14 additions & 0 deletions src/bespokeasm/assembler/line_object/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, line_id: LineIdentifier, instruction: str, comment: str, memz
self._label_scope = None
self._memzone = memzone
self._compilable = True
self._is_muted = False

def __repr__(self):
return str(self)
Expand Down Expand Up @@ -84,6 +85,19 @@ def compilable(self) -> bool:
def compilable(self, value: bool):
self._compilable = value

@property
def is_muted(self) -> bool:
"""
Returns True if this line object should be ignored during
emission of bytecode or certain types of pretty printing
"""
return self._is_muted

@is_muted.setter
def is_muted(self, value: bool):
"""Sets the muted state of this line object"""
self._is_muted = value


class LineWithBytes(LineObject):
def __init__(self, line_id: LineIdentifier, instruction: str, comment: str, memzone: MemoryZone):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
from bespokeasm.assembler.line_identifier import LineIdentifier
from bespokeasm.assembler.memory_zone import MemoryZone
import bespokeasm.assembler.preprocessor.condition as condition
from bespokeasm.assembler.preprocessor import Preprocessor
from bespokeasm.assembler.preprocessor.condition_stack import ConditionStack


CONDITIONAL_LINE_PREFIX_LIST = ['#if ', '#ifdef ', '#ifndef ', '#elif ', '#else', '#endif']
CONDITIONAL_LINE_PREFIX_LIST = ['#if ', '#ifdef ', '#ifndef ', '#elif ', '#else', '#endif', '#mute', '#emit', '#unmute']


class ConditionLine(PreprocessorLine):
Expand All @@ -17,7 +18,8 @@ def __init__(
instruction: str,
comment: str,
memzone: MemoryZone,
condition_stack: ConditionStack
preprocessor: Preprocessor,
condition_stack: ConditionStack,
):
super().__init__(line_id, instruction, comment, memzone)

Expand All @@ -31,11 +33,15 @@ def __init__(
self._condition = condition.EndifPreprocessorCondition(instruction, line_id)
elif instruction.startswith('#ifdef ') or instruction.startswith('#ifndef '):
self._condition = condition.IfdefPreprocessorCondition(instruction, line_id)
elif instruction == '#mute':
self._condition = condition.MutePreprocessorCondition(instruction, line_id)
elif instruction == '#unmute' or instruction == '#emit':
self._condition = condition.UnmutePreprocessorCondition(instruction, line_id)
else:
raise ValueError(f'Invalid condition line: {instruction}')

try:
condition_stack.process_condition(self._condition)
condition_stack.process_condition(self._condition, preprocessor)
except IndexError:
sys.exit(
f'ERROR - {line_id}: Preprocessor condition has no matching counterpart'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,13 @@ def parse_line(
preprocessor,
log_verbosity,
)]
if instruction.startswith('#page'):
raise NotImplementedError('PageAlignLine is not implemented')
if instruction.startswith(tuple(CONDITIONAL_LINE_PREFIX_LIST)):
return [ConditionLine(
line_id,
instruction,
comment,
current_memzone,
preprocessor,
condition_stack,
)]
return []
23 changes: 23 additions & 0 deletions src/bespokeasm/assembler/preprocessor/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __str__(self) -> str:
return self.__repr__()

def evaluate(self, preprocessor: Preprocessor) -> bool:
"""Evaluates whether this condition is true or false."""
raise NotImplementedError()

def is_lineage_true(self, preprocessor: Preprocessor) -> bool:
Expand Down Expand Up @@ -239,3 +240,25 @@ def is_dependent(self) -> bool:

def evaluate(self, preprocessor: Preprocessor) -> bool:
return True


class MutePreprocessorCondition(PreprocessorCondition):
def __init__(self, line_str: str, line: LineIdentifier):
super().__init__(line_str, line)

def __repr__(self) -> str:
return f'MutePreprocessorCondition<{self.self._line_str}>'

def evaluate(self, preprocessor: Preprocessor) -> bool:
return True


class UnmutePreprocessorCondition(PreprocessorCondition):
def __init__(self, line_str: str, line: LineIdentifier):
super().__init__(line_str, line)

def __repr__(self) -> str:
return f'UnmutePreprocessorCondition<{self.self._line_str}>'

def evaluate(self, preprocessor: Preprocessor) -> bool:
return True
24 changes: 22 additions & 2 deletions src/bespokeasm/assembler/preprocessor/condition_stack.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
from bespokeasm.assembler.preprocessor.condition import \
PreprocessorCondition, EndifPreprocessorCondition
PreprocessorCondition, EndifPreprocessorCondition, \
MutePreprocessorCondition, UnmutePreprocessorCondition
from bespokeasm.assembler.preprocessor import Preprocessor


class ConditionStack:
def __init__(self):
self._stack: list[PreprocessorCondition] = []
self._mute_counter = 0

def process_condition(self, condition: PreprocessorCondition):
def process_condition(self, condition: PreprocessorCondition, preprocessor: Preprocessor):
if isinstance(condition, EndifPreprocessorCondition):
popped_consition = self._stack.pop()
if popped_consition.is_dependent:
pass
elif isinstance(condition, MutePreprocessorCondition):
if self.currently_active(preprocessor):
self._increment_mute_counter()
elif isinstance(condition, UnmutePreprocessorCondition):
if self.currently_active(preprocessor):
self._decrement_mute_counter()
elif condition.is_dependent:
# a dependent condition pops the current condition and makes it the parent to the new condition.
# this way the dependent chain is only ever 1-deep in the stack, making nested #if/#else/#endif
Expand All @@ -26,3 +34,15 @@ def currently_active(self, preprocessor: Preprocessor) -> bool:
if len(self._stack) == 0:
return True
return self._stack[-1].evaluate(preprocessor)

def _increment_mute_counter(self):
self._mute_counter += 1

def _decrement_mute_counter(self):
if self._mute_counter > 0:
self._mute_counter -= 1

@property
def is_muted(self) -> bool:
"""Returns True if the current condition stack is muted."""
return self._mute_counter > 0
2 changes: 1 addition & 1 deletion src/bespokeasm/assembler/pretty_printer/intelhex.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, line_objs: list[LineObject], model: AssemblerModel, as_intel
def pretty_print(self) -> str:
output = io.StringIO()
for lobj in self.line_objects:
if isinstance(lobj, LineWithBytes):
if isinstance(lobj, LineWithBytes) and not lobj.is_muted:
line_bytes = lobj.get_bytes().decode(encoding='latin-')
self._intel_hex.puts(lobj.address, line_bytes)

Expand Down
2 changes: 1 addition & 1 deletion src/bespokeasm/assembler/pretty_printer/minhex.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def pretty_print(self) -> str:
line_byte_count = 0
address_width = int(self.model.address_size/4)
for lobj in self.line_objects:
if isinstance(lobj, LineWithBytes):
if isinstance(lobj, LineWithBytes) and not lobj.is_muted:
line_bytes = lobj.get_bytes()
for b in line_bytes:
if line_byte_count == 0:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
{
"name": "String Punctuation",
"scope": "punctuation.definition.string",
"foreground": "#FF5733"
"foreground": "#ed80a2"

},
{
Expand Down
2 changes: 1 addition & 1 deletion src/bespokeasm/configgen/vscode/resources/tmTheme.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#FF5733</string>
<string>#ed80a2</string>
</dict>
</dict>
<dict>
Expand Down
12 changes: 10 additions & 2 deletions test/test_configgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,11 @@ def test_sublime_configgen_no_registers(self):
item_match_str = item['match']
self._assert_grouped_item_list(
item_match_str,
['include', 'require', 'create_memzone', 'define', 'if', 'elif', 'else', 'endif', 'ifdef', 'ifndef'],
[
'include', 'require', 'create_memzone', 'define', 'if',
'elif', 'else', 'endif', 'ifdef', 'ifndef',
'mute', 'unmute', 'emit',
],
'data type directives'
)
self.assertIsFile(os.path.join(test_tmp_dir, 'bespokeasm-test.sublime-color-scheme'))
Expand Down Expand Up @@ -326,7 +330,11 @@ def test_sublime_configgen_with_registers(self):
item_match_str = item['match']
self._assert_grouped_item_list(
item_match_str,
['include', 'require', 'create_memzone', 'define', 'if', 'elif', 'else', 'endif', 'ifdef', 'ifndef'],
[
'include', 'require', 'create_memzone', 'define',
'if', 'elif', 'else', 'endif', 'ifdef', 'ifndef',
'mute', 'unmute', 'emit',
],
'data type directives'
)
self.assertIsFile(os.path.join(test_tmp_dir, 'tester-assembly.sublime-color-scheme'))
Expand Down
2 changes: 1 addition & 1 deletion test/test_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def test_negative_values(self):
)

def test_unknown_expression_parts(self):
line_id = LineIdentifier(1927, 'test_character_ordinals_in_expressions')
line_id = LineIdentifier(1928, 'test_unknown_expression_parts')

with self.assertRaises(SystemExit, msg='extraneous comparison operator'):
parse_expression(line_id, '<$2024').get_value(TestExpression.label_values, 1)
Expand Down
69 changes: 63 additions & 6 deletions test/test_preprocessor_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from bespokeasm.assembler.preprocessor import Preprocessor
from bespokeasm.assembler.preprocessor.condition import \
IfPreprocessorCondition, IfdefPreprocessorCondition, ElifPreprocessorCondition, \
ElsePreprocessorCondition, EndifPreprocessorCondition
ElsePreprocessorCondition, EndifPreprocessorCondition, MutePreprocessorCondition, \
UnmutePreprocessorCondition
from bespokeasm.assembler.line_identifier import LineIdentifier
from bespokeasm.assembler.model import AssemblerModel
from bespokeasm.assembler.memory_zone.manager import MemoryZoneManager
Expand Down Expand Up @@ -323,25 +324,81 @@ def test_condition_stack(self):

# add an if condition that should be false
c1 = IfPreprocessorCondition('#if s1 < 50', LineIdentifier('test_condition_stack', 1))
stack.process_condition(c1)
stack.process_condition(c1, preprocessor)
self.assertFalse(stack.currently_active(preprocessor), 'condition should be False')

# add an elif condition that should be true
c2 = ElifPreprocessorCondition('#elif s1 >= 55', LineIdentifier('test_condition_stack', 2))
stack.process_condition(c2)
stack.process_condition(c2, preprocessor)
self.assertTrue(stack.currently_active(preprocessor), 'condition should be True')

# add an elif that could be true but is false because it follows a true elif
c3 = ElifPreprocessorCondition('#elif s1 < 60', LineIdentifier('test_condition_stack', 3))
stack.process_condition(c3)
stack.process_condition(c3, preprocessor)
self.assertFalse(stack.currently_active(preprocessor), 'condition should be False')

# add the else condition
c4 = ElsePreprocessorCondition('#else', LineIdentifier('test_condition_stack', 4))
stack.process_condition(c4)
stack.process_condition(c4, preprocessor)
self.assertFalse(stack.currently_active(preprocessor), 'condition should be False')

# add endif
c5 = EndifPreprocessorCondition('#endif', LineIdentifier('test_condition_stack', 5))
stack.process_condition(c5)
stack.process_condition(c5, preprocessor)
self.assertTrue(stack.currently_active(preprocessor), 'condition should be True')

def test_muting(self):
stack = ConditionStack()
preprocessor = Preprocessor()

self.assertFalse(stack.is_muted, 'initial condition should be False')
stack.process_condition(MutePreprocessorCondition('#mute', LineIdentifier('test_muting', 1)), preprocessor)
self.assertTrue(stack.is_muted, 'condition should be True')
stack.process_condition(UnmutePreprocessorCondition('#unmute', LineIdentifier('test_muting', 2)), preprocessor)
self.assertFalse(stack.is_muted, 'condition should be False')
stack.process_condition(MutePreprocessorCondition('#mute', LineIdentifier('test_muting', 3)), preprocessor)
stack.process_condition(MutePreprocessorCondition('#mute', LineIdentifier('test_muting', 4)), preprocessor)
stack.process_condition(MutePreprocessorCondition('#mute', LineIdentifier('test_muting', 5)), preprocessor)
self.assertTrue(stack.is_muted, 'condition should be True')
stack.process_condition(UnmutePreprocessorCondition('#unmute', LineIdentifier('test_muting', 6)), preprocessor)
self.assertTrue(stack.is_muted, 'condition should be True')
stack.process_condition(UnmutePreprocessorCondition('#unmute', LineIdentifier('test_muting', 7)), preprocessor)
self.assertTrue(stack.is_muted, 'condition should be True')
stack.process_condition(UnmutePreprocessorCondition('#unmute', LineIdentifier('test_muting', 8)), preprocessor)
self.assertFalse(stack.is_muted, 'condition should be False')

def test_conditional_muting(self):
"""Demonstrate that conditional compilation controls the mute/unmute preprocessor directives."""
# if a #mute is inside a false condition, it should not mute
stack = ConditionStack()
preprocessor = Preprocessor()
preprocessor.create_symbol('s1', '57')
preprocessor.create_symbol('s2', 's1*2')

self.assertTrue(stack.currently_active(preprocessor), 'condition should be True')
self.assertFalse(stack.is_muted, 'mute should be False')

c1 = IfPreprocessorCondition('#if s1 < 50', LineIdentifier('test_condition_stack', 1))
stack.process_condition(c1, preprocessor)
self.assertFalse(stack.currently_active(preprocessor), 'condition should be False')
self.assertFalse(stack.is_muted, 'mute should be False')
stack.process_condition(MutePreprocessorCondition('#mute', LineIdentifier('test_muting', 2)), preprocessor)
self.assertFalse(stack.currently_active(preprocessor), 'condition should be False')
self.assertFalse(stack.is_muted, 'mute should be False')

# if a #mute is inside a true condition, it should mute
c2 = ElsePreprocessorCondition('#else', LineIdentifier('test_condition_stack', 3))
stack.process_condition(c2, preprocessor)
self.assertTrue(stack.currently_active(preprocessor), 'condition should be True')
self.assertFalse(stack.is_muted, 'mute should be False')
stack.process_condition(MutePreprocessorCondition('#mute', LineIdentifier('test_muting', 4)), preprocessor)
self.assertTrue(stack.currently_active(preprocessor), 'condition should be False')
self.assertTrue(stack.is_muted, 'mute should be True')

c3 = EndifPreprocessorCondition('#endif', LineIdentifier('test_condition_stack', 5))
stack.process_condition(c3, preprocessor)
self.assertTrue(stack.currently_active(preprocessor), 'condition should be True')
self.assertTrue(stack.is_muted, 'mute should be True')
stack.process_condition(UnmutePreprocessorCondition('#unmute', LineIdentifier('test_muting', 6)), preprocessor)
self.assertTrue(stack.currently_active(preprocessor), 'condition should be True')
self.assertFalse(stack.is_muted, 'mute should be False')

0 comments on commit 742c7ae

Please sign in to comment.