lips/NOTES.md

11 KiB

lips internal architecture

…since i'm new to parsers and doomed to forget how my own programs work.

public interface

(note: this should be moved to the readme or something)

in the simplest case, you just call the table returned by require 'lips.init' as seen in example.lua.

lips also returns, in said table: the usual package metadata, and a writers table of pre-defined writers.

in the call interface, a writer function and a table of options may be passed as further arguments after fn_or_asm.

TODO: example

writer could be one of the lips.writers provided, after instantiating with a call (it makes use of closure locals).

TODO: example

options is a table of string keys and any type of value. currently there is:

.unsafe (default false)
    if set, don't wrap the main assembler call in a pcall().
    i want to deprecate this, because it's unnecessary code
    that the user could handle just as well from outside the interface.
.offset (deprecated)
    sets options.origin and options.base simultaneously.
    since the address-base feature was added,
    the preferred way of doing this is by setting them individually,
    so this option is deprecated.
.origin (default 0)
    where to initially start writing in the file.
    this simply tacks on an .org directive
    to the start of the internal assembly.
.base (default 0)
    how far to offset the assembler in where it thinks it's writing.
    this is incredibly important for writing code in ROM
    that is read into a static place in RAM.
    this simply tacks on a .base directive
    to the start of the internal assembly.
.path (default containing directory of assembly file, if applicable)
    primarily used internally for handling relative imports.
.labels (default {})
    note: lips modifies the argument in-place.
    allows for exporting/importing of label data.
    that means you can declare labels in one file,
    and allow a second to access them, in two separate passes.
    otherwise, you would have to hardcode label locations.
    the label format is quite simply a dictionary of string/number pairs:
    { mylabel=0xDEADBEEF, ... }
.debug_tokens (default false)
    dumps the statements table after tokenizing and collecting into statements.
    this is after UNARY and RELLABELSYM tokens have been disambiguated.
.debug_pre (default false)
    dumps statements after basic preprocessing:
    variable substitution, expression parsing,
    relative label substitution, etc.
.debug_post (default false)
    dumps statements after expanding preprocessor commands:
    pseudo-instructions, expression evaluation, etc.
.debug_asm (default false)
    is arguably the least useful of states to dump in.
    this will dump statements after being reduced to
    !ORG and !DATA and !BIN statements. anything else is a bug.
    the values of the !BYTE statements are not printed.

init.lua

the path used to import lips.init is mangled so lips can find its components in each file. this has to be copy-pasted to every internal file, which is a small inconvenience.

afaik there isn't really a better way of doing this in vanilla Lua 5.1, besides mandating to lips be an installed Lua package, which would be an inconvenience to users (and myself!).

iirc the gsub can silently "fail" and allow a couple other methods of importing, import "lips" maybe? i don't remember. it might work without being in a dedicated directory too

other than that there's not a lot to say. i've intentionally written this file as stripped down as possible.

i've gone for a one-class-per-file structure, so file and class can be synonyms in the following text.

room for improvement

it would be nice to document options in init.lua, since ATM i'm abusing Lua's default-to-nil behavior of tables. that means options could be hidden within any file and don't demand any forward-declaration or inline documentation.

eventually i'd like to make writer a key of options just to simplify the interface even further. maybe i could pull off writer_or_options for backwards compatibility?

someday i'd like to add a reader option for handling of existing data, e.g. for implementing an automated .hook directive.

Parser

"Parser" is a bit of a misnomer, since the class doesn't do any parsing itself. it defers parsing to the Lexer, Collector, and Preproc classes. it also handles writing of the parsed data through the Dumper class.

the main method here is Parser:method which simply interfaces all the important bits of the assembling process.

self.statements refers to the "commands" so-to-speak of the assembler at any point. the general format of this table is:

statements={
    {'!BEEP', Tokens...},
    ...
    {'!BOOP', Tokens...},
}

the Parser:dump_debug method allows for dumping the state of the self.statements table after any of the primary stages of assembling. refer to the .debug_token, .debug_pre, .debug_post, and .debug_asm options above.

room for improvement

statements could be made type-restricted, instead of deferring "this crap ain't even assembled" to each individual stage/class.

i'd like to come up with a better name, but i'm not in any rush.

the debug dumper could be slightly prettier in certain cases.

Lexer

transforms strings into the tokens they represent. this does not handle nor consider how they will be collected into statements.

.inc directives (and their friends) are handled here: the appropriate files are placed and tokenized inline, not unlike in C.

the HEX directive is its own mini-language and thus has its own mini-lexer.

expressions are not parsed nor lexed here. they are simply extracted as whole strings for later processing.

the yield closure wraps around the _yield function argument to pass error-handling metadata: the current filename and line number.

the rest of the code should be self-explanitory, albiet ugly.

room for improvement

this character-based lexer isn't driven by any particular grammar, making it unclear what syntax is and isn't valid.

but it works. it's the code i need to change the least to add new features.

there's a couple TODOs and FIXMEs in here.

Collector

collects tokens into statements. statements are basically our form of an abstract syntax tree, except statements don't need the depth of a tree (outside of expressions) so they're completely flat.

most of this is just validation of the lexed tokens.

room for improvement

the Collector currently squeezes many DATA statements into few through the :push_data method. as noted in the TODOs, this process should be done in the lexer, to greatly reduce creation of tables, thus greatly improving performance.

Preproc

transforms complex statements into simpler statements that, after expanding with Expander, Dumper can then understand.

the :check method asserts that a token exists and is of a given type (tt).

preprocessing is split into two passes:

pass 1

resolves variables by substitution, parses and evaluates expressions, and collects relative labels.

this pass starts by creating a new, empty table of statements to fill. statements are passed through, possibly modified, or read and left-out.

the reason for the copying is that taking indexes into an array (statements) that you're removing elements from is A Bad Idea.

all expression tokens are evaluated, and all variable tokens are substituted.

variable-declaring statements (!VAR) are read to a dictionary table for future substitution of their keys with values.

labels (!LABEL) are checked for RELLABEL tokens to collect for later replacement in pass 2. the positive and negative relative labels are collected into their own tables, appended and prepended respectively. the collection tables are arrays of tables containing the keys index and name.

pass 2

resolves relative labels by substitution.

the appending/prepending done in pass 1 ensures that the appropriate relative labels are found in the proper order.

room for improvment

looking back, the new_statements ordeal only seems necessary for the (poor) error-handling it provides.

Expander

expands pseudo-instructions, including the inferrence of implied registers.

pseudo-instructions are defined in overrides.lua. overrides act as extensions to the Expander class; they are passed Expander's self. this keeps boilerplate out of overrides.lua, but makes our own file more of a mess, with more dependencies for arbitrary token/statement handling.

room for improvment

expansion is kinda messy.

Expression

handles parsing and evaluation of simple (usually mathematical) expressions.

this class is actually completely independent of the rest of lips, besides the requirement of the Base class, which isn't specific to lips.

room for improvement

right now, this is just a quick and dirty port of some C++ code i wrote a while back. so basically, everything could be improved.

bitwise operators need to be implemented. possibly with LuaJIT and a Lua 5.1 fallback. maybe that should be its own file?

in the long term, i'll need to move lexing expressions to the main Lexer class, and do proper parsing to an AST in Collector. this will unify the syntax, and allow for inline expressions, e.g: lw s0, 5*4(sp).

Dumper

completes the assembler with the incredibly boring task of translating strings to numbers.

most directives are handled here in :load, simplified to raw !DATA, !BIN and !ORG directives.

room for improvement

the :load method is huge and desperately needs to be refactored into smaller methods, perhaps even of a different class.

helper classes

Token

implements error-checking for tokens, and provides convenience methods.

also handles computation of numeric tokens, since Token objects contain all the data necessary to do so.

Statement

implements some error-checking for statements.

TokenIter

used by Collector to iterate over statements, validating them.

Reader

used by Expander and Dumper to validate tokens in statements.

currently, this is the only class requiring inheritance.

room for improvement

Reader should probably be split into another class, instead of inherited.

etc.

etc!

overrides.lua

refer to the section on Preproc.

data.lua

contains most of the information required to assemble MIPS III assembly code.

this file does not expose any functions or methods, only constant data. however, some of the data may be generated through local functions.

util.lua

contains various utility functions to be lightly sprinkled over files.

most of this shouldn't be specific to lips.

writers.lua

implements a few must-have writer-generators.

make_tester is just a variant of make_verbose that only prints addresses as necessary, reducing noise.

room for improvement in general

for proper documentation, i need to copy-paste and rewrite most of the crap here into the appropriate files themselves.

see also the TODO file.