diff --git a/cocas/assembler/assembler.py b/cocas/assembler/assembler.py index 22da7e9c..1db8d8a7 100644 --- a/cocas/assembler/assembler.py +++ b/cocas/assembler/assembler.py @@ -17,7 +17,8 @@ def assemble_module(input_stream: antlr4.InputStream, target_instructions: TargetInstructions, macros_library: dict[str, dict[int, MacroDefinition]], - filepath: Path) -> ObjectModule: + filepath: Path, + include_paths: list[Path]) -> ObjectModule: """ Convert lines of an assembler file to object code @@ -26,7 +27,7 @@ def assemble_module(input_stream: antlr4.InputStream, :param macros_library: standard macros of assembler :param filepath: path of the file to use in error handling """ - macro_expanded_input_stream = process_macros(input_stream, macros_library, filepath) + macro_expanded_input_stream = process_macros(input_stream, macros_library, filepath, include_paths, False) r = build_ast(macro_expanded_input_stream, filepath) return generate_object_module(r, target_instructions) @@ -52,6 +53,7 @@ def assemble_files(target: str, relative_path: Optional[Path], absolute_path: Optional[Path], realpath: bool, + include_paths: list[Path], macro_libraries: list[dict[str, dict[int, MacroDefinition]]] = None ) -> list[tuple[Path, ObjectModule]]: """ @@ -63,6 +65,7 @@ def assemble_files(target: str, :param relative_path: if debug paths should be relative to some path :param absolute_path: if relative paths should be converted to absolute :param realpath: if paths should be converted to canonical + :param include_paths: list of include search paths :param macro_libraries: user's .mlb files, different from standard .mlb :return: list of pairs [source file path, object module] """ @@ -88,7 +91,7 @@ def assemble_files(target: str, if not data.endswith('\n'): data += '\n' input_stream = antlr4.InputStream(data) - obj = assemble_module(input_stream, target_instructions, macros, filepath) + obj = assemble_module(input_stream, target_instructions, macros, filepath, include_paths) debug_info_path = get_debug_info_path(filepath, debug, relative_path, realpath) if debug_info_path: diff --git a/cocas/assembler/macro_processor.py b/cocas/assembler/macro_processor.py index 1902d005..a8f19024 100644 --- a/cocas/assembler/macro_processor.py +++ b/cocas/assembler/macro_processor.py @@ -1,4 +1,5 @@ import codecs +import antlr4 import itertools import re from base64 import b64encode @@ -15,7 +16,7 @@ from .generated import MacroLexer, MacroParser, MacroVisitor -def unique(params: list[str]): +def unique(params: list[str], macro_stack: list[str]): register_available = [True] * 4 var_params = [] for param in params: @@ -39,9 +40,27 @@ def unique(params: list[str]): defined_vars[param] = f'r{i}' return defined_vars +def mpush(params: list[str], macro_stack: list[str]): + for param in params: + macro_stack.append(param) + + return dict() + +def mpop(params: list[str], macro_stack: list[str]): + defined_vars = dict() + for param in params: + if not macro_stack: + raise CdmTempException('mpop: macro stack is empty') + + defined_vars[param] = macro_stack.pop() + + return defined_vars + macro_instructions = { 'unique': unique, + 'mpush': mpush, + 'mpop': mpop, } @@ -127,13 +146,16 @@ def sub_all(ps): # noinspection PyPep8Naming class ExpandMacrosVisitor(MacroVisitor): - def __init__(self, rewriter: Optional[TokenStreamRewriter], mlb_macros, filepath: str): + def __init__(self, rewriter: Optional[TokenStreamRewriter], mlb_macros, filepath: str, include_paths: list[Path], nested): # rewriter should be None if then will be called .visit(MlbContext) # rewriter should be valid if then will be called .visit(ProgramContext) + self.nested = nested self.nonce = 0 + self.macro_stack = [] self.macros = {name: mlb_macros[name].copy() for name in mlb_macros} self.rewriter = rewriter self.filepath = filepath + self.include_paths = include_paths @staticmethod def _generate_location_line(filepath: str, line: int, info: str = None) -> str: @@ -151,6 +173,34 @@ def add_macro(self, macro: MacroDefinition): raise CdmTempException(f"Redefinition of macro {macro.name}/{macro.arity} with different body") self.macros[macro.name][macro.arity] = macro + def include_file(self, include_filename: str): + # Firstly check in directoru where file is + with Path(self.filepath).parent as path: + include_filepath = path / include_filename + if include_filepath.is_file(): + with include_filepath.open('rb') as file: + data = file.read() + data = codecs.decode(data, 'utf8', 'strict') + if not data.endswith('\n'): + data += '\n' + input_stream = antlr4.InputStream(data) + + return ''.join([chr(i) for i in process_macros(input_stream, dict(), include_filepath, self.include_paths, True, self).data]) + + for path in self.include_paths: + include_filepath = path / include_filename + if include_filepath.is_file(): + with include_filepath.open('rb') as file: + data = file.read() + data = codecs.decode(data, 'utf8', 'strict') + if not data.endswith('\n'): + data += '\n' + input_stream = antlr4.InputStream(data) + return ''.join([chr(i) for i in process_macros(input_stream, dict(), include_filepath, self.include_paths, True, self).data]) + + # Raise exception if there is no such file in include paths + raise CdmTempException(f'Include: file {include_filename} not found in any search path') + # Returns a None for things as asect or empty line. # Returns string of macro def expand_macro(self, macro_name: str, macro_params: list[str]): @@ -180,7 +230,7 @@ def expand_macro(self, macro_name: str, macro_params: list[str]): # each line that does not contain another macro # MUST add exactly ONE line to ret_parts if instruction in macro_instructions: - variables.update(macro_instructions[instruction](parameters)) + variables.update(macro_instructions[instruction](parameters, self.macro_stack)) if label != '': ret_parts.append(f'{label}\n') else: @@ -219,14 +269,23 @@ def visitProgram(self, ctx: MacroParser.ProgramContext): self.add_macro(self.visitMacro(child)) elif isinstance(child, MacroParser.LineContext): label, instruction, parameters = self.visitLine(child) - expanded_text = self.expand_macro(instruction, parameters) + if instruction == 'include': + if len(parameters) != 1: + raise CdmTempException('Include: wrong amount of parameters, expected 1 parameter') + expanded_text = self.include_file(parameters[0]) + else: + expanded_text = self.expand_macro(instruction, parameters) if expanded_text is not None: if label != '': expanded_text = f'{label}\n{expanded_text}' - mstart = self._generate_location_line(self.filepath, child.start.line, "mstart") - mstop = self._generate_location_line(self.filepath, child.stop.line + 1, "mstop") - expanded_text = f'{mstart}{expanded_text}{mstop}' + if self.nested: + stop_mark = self._generate_location_line(self.filepath, child.stop.line + 1) + expanded_text = f'{expanded_text}{stop_mark}' + else: + mstart = self._generate_location_line(self.filepath, child.start.line, "mstart") + mstop = self._generate_location_line(self.filepath, child.stop.line + 1, "mstop") + expanded_text = f'{mstart}{expanded_text}{mstop}' self.rewriter.insertBeforeToken(child.start, expanded_text) self.rewriter.delete(self.rewriter.DEFAULT_PROGRAM_NAME, child.start, child.stop) except CdmTempException as e: @@ -327,11 +386,14 @@ def read_mlb(filepath: Path) -> dict[str, dict[int, MacroDefinition]]: token_stream = CommonTokenStream(lexer) parser = MacroParser(token_stream) cst = parser.mlb() - emv = ExpandMacrosVisitor(None, dict(), str_path) + emv = ExpandMacrosVisitor(None, dict(), str_path, [], False) return emv.visit(cst) - -def process_macros(input_stream: InputStream, library_macros, filepath: Path): +# Added arguments: +# include_paths - list of include paths +# nested - if file macroprocessed for insertion into another file +# init_ctx - ExpandMacrosVisitor - initial context which used for sharing same macros, nonce value and macro stack with some file, for insertion into which we macroprocessing this file +def process_macros(input_stream: InputStream, library_macros, filepath: Path, include_paths: list[Path], nested, init_ctx: ExpandMacrosVisitor = None): str_path = filepath.absolute().as_posix() lexer = MacroLexer(input_stream) lexer.removeErrorListeners() @@ -343,7 +405,16 @@ def process_macros(input_stream: InputStream, library_macros, filepath: Path): parser.addErrorListener(AntlrErrorListener(AssemblerExceptionTag.MACRO, str_path)) cst = parser.program() rewriter = TokenStreamRewriter(token_stream) - emv = ExpandMacrosVisitor(rewriter, library_macros, str_path) + + emv = ExpandMacrosVisitor(rewriter, library_macros, str_path, include_paths, nested) + # If init_ctx passed, take its nonce value, macro_stack and macros to emv (all except nonce by reference) + if init_ctx is not None: + emv.nonce = init_ctx.nonce + emv.macro_stack = init_ctx.macro_stack + emv.macros = init_ctx.macros emv.visit(cst) new_text = rewriter.getDefaultText() + # Modify init_ctx nonce + if init_ctx is not None: + init_ctx.nonce = emv.nonce return InputStream(new_text) diff --git a/cocas/main.py b/cocas/main.py index 44fb35f6..d748bd47 100755 --- a/cocas/main.py +++ b/cocas/main.py @@ -26,6 +26,7 @@ def main(): parser.add_argument('-c', '--compile', action='store_true', help='compile into object files without linking') parser.add_argument('-m', '--merge', action='store_true', help='merge object files into one') parser.add_argument('-o', '--output', type=Path, help='specify output file name') + parser.add_argument('-I', metavar='', action='append', dest='include_paths', type=Path, help='specify include search path') debug_group = parser.add_argument_group('debug') debug_group.add_argument('--debug', type=Path, nargs='?', const=True, help='export debug information') debug_path_group = debug_group.add_mutually_exclusive_group() @@ -36,6 +37,9 @@ def main(): debug_group.add_argument('--realpath', action='store_true', help='canonicalize paths by following symlinks and resolving . and ..') args = parser.parse_args() + if args.include_paths is None: + args.include_paths = [] + if args.list_targets: print('Available targets: ' + ', '.join(available_targets)) return @@ -90,7 +94,7 @@ def main(): try: macro_libraries = [read_mlb(mlb) for mlb in mlb_files] objects: list[tuple[Path, ObjectModule]] = list(itertools.chain( - assemble_files(target, asm_files, bool(args.debug), relative_path, absolute_path, realpath, + assemble_files(target, asm_files, bool(args.debug), relative_path, absolute_path, realpath, args.include_paths, macro_libraries=macro_libraries), read_object_files(target, obj_files, bool(args.debug), relative_path, absolute_path, realpath) )) diff --git a/docs/README.md b/docs/README.md index 136de080..2d9be6f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ This folder contains some useful information about various `cdm-devkit` parts. 7. [Using CLI](./guides/7-using-cli.md) 8. [Using external build system](./guides/8-external-build-systems.md) 9. [Using Python environment](./guides/9-using-python-environments.md) +10. [Macros and macro libraries](./guides/10-macros.md) ### Processors documentation diff --git a/docs/guides/10-macros.md b/docs/guides/10-macros.md new file mode 100644 index 00000000..2f5fd3fd --- /dev/null +++ b/docs/guides/10-macros.md @@ -0,0 +1,179 @@ +# Defining macro in assembly source file + +You can define macro right in your assembly source file. Every such macro definition has following structure: + +``` +macro / + +mend +``` + +1. First line - **macro header**, consist of keyword `macro`, name of the macro and its arity. +2. Macro body - several lines of assembly code. Here you can use **macro parameters** (if arity of macro isn't 0), **nonce**, **macro variables** and **macro instructions** which will be described later. +3. Last line - **macro footer** - keyword `mend` marking the end of macro definition. + +## Macro parameters + +You can use macro parameters passed to macro with `$` + number of parameter (numeration of parameters starts from 1), all parameters will be just substituted into macro body: + +``` +# Defining macro ldv +macro ldv/2 + ldi $1, $2 + ldw $1, $1 +mend + +rsect main +main> + # Using macro ldv + ldv r0, label + + halt +end +``` + +This will expand into following: + +``` +rsect main +main> + ldi r0, label + ldw r0, r0 + + halt +end +``` + +## Nonce + +If you need to use some unique label name in macro, you can use **nonce** (an apostrophe): + +``` +macro uselessMacro/1 +loop': + dec $1 + bnz loop' +mend +``` + +**Nonce** will expand into some number, unique for each macro expanding, e.g.: + +``` +ldi r0, 5 +uselessMacro r0 # In this macro expansion ' will be replaced by 1. So, label in substituted code will have name `loop1` +ldi r0, 10 +uselessMacro r0 # In this macro expansion ' will be replaced by 2 +``` + +## Macro instructions + +There is some instructions you can use in macros: + +- `unique` +- `mpush` +- `mpop` + +### unique + +If you need to select register which is different from some registers passed as **macro arguments**, you can use **unique** macro instruction, e.g.: + +``` +macro multBy10/1 + unique $1, temp # Stores register, different from $1 in macro variable temp + + move $1, ?temp # ?temp will be replaced by value of macro variable + + shl $1 + + shl ?temp + shl ?temp + shl ?temp + + add ?temp, $1 +mend +``` + +### mpush and mpop + +If you need to pass some pieces of text through several macros, you can use **macro stack** - LIFO memory existing at compilation time. + +- `mpush` pushes specified piece of text (or several pieces) on macro stack +- `mpop` pops pieces from stack to specified **macro variables** (or several variables, one by one) + +E.g.: + +``` +# $1 <= $2 <= $3 +macro isInBound/3 + cmp $2, $1 + blo alt' # If $2 < $1, branches to alt' + + cmp $2, $3 + bhi alt' # If $2 > $3, branches to alt' + + mpush alt' # Push alt' on macro stack to let another macro place this label +mend + +macro notInBound/0 + mpop where # Pop label pushed by isInBound + mpush new?where # Push new label, which will lead to end of this if block + br new?where + ?where: # Place label pushed by isInBound +mend + +macro fiInBound/0 + mpop term # Pop label pushed by notInBound + ?term: # Place this label +mend + +rsect main +main> + ldi r0, 2 + isInBound 1, r0, 3 + ldi r0, 1 + notInBound + ldi r0, 0 + fiInBound + + halt +end +``` + +# Creating macro library (.mlb) + +Instead of defining macros right in assembly files, you can store them separately, in **macro libraries** - .mlb files, and then just pass them to cocas with another sources, which use macros from library. + +Defining of mlb macro is a little bit different from defining macro in assembly file: + +``` +*/ + + +*/ + +``` + +For example, here is `ldv` macro from beginning of the guide, defined as mlb macro: + +``` +*ldv/2 + ldi $1, $2 + ldw $1, $1 +``` + +# Include macro directive + +If you need to insert in your file content of another file, you can use `include ` macro directive, it will macroprocess specified file (all macros definied in including file will be available in included file and vice versa) and insert processed text into including file. + +Firstly, cocas will try to find specified file in same directory with including file, if there is no such file, it will try to find it in specified **include paths**. You can specify this paths with `-I` option of cocas: + +```bash +cocas -I ./headers -I ./another_headers -o test.img test.asm +``` + +It is useful, when you want to create some static library and link it with your programms. Instead of declaring every external label from this lib in every source file, you can declare all this labels in some header file and include it in every source file, which use this lib. + +Also, you can create files, which contain some tplates, incldude and use them in multiple files. + +> [!TIP] +> Technically, you can include some code blocks or macro definitions from another source files, but better practice will be using **separete compilation** and **macro libraries** for these purposes instead. diff --git a/vscode-cdm-extension/syntaxes/cdm16-assembly.tmLanguage.json b/vscode-cdm-extension/syntaxes/cdm16-assembly.tmLanguage.json index a3530341..bd6022ce 100644 --- a/vscode-cdm-extension/syntaxes/cdm16-assembly.tmLanguage.json +++ b/vscode-cdm-extension/syntaxes/cdm16-assembly.tmLanguage.json @@ -90,7 +90,7 @@ "directives": { "patterns": [{ "name": "entity.name.type.cdm16-assembly", - "match": "\\b(align|asect|rsect|dc|ds|tplate|end|end.|macro|mpop|mpush|mend|define|db|dw)\\b" + "match": "\\b(align|asect|rsect|dc|ds|tplate|end|end.|macro|mpop|mpush|mend|define|db|dw|include)\\b" }] }, "numbers": { @@ -101,4 +101,4 @@ } }, "scopeName": "source.asm.cdm16" -} \ No newline at end of file +}