-
Notifications
You must be signed in to change notification settings - Fork 14
Concise implementation of a lisp-like language for low-end and embedded devices
License
BSD-3-Clause, LGPL-2.1 licenses found
Licenses found
BSD-3-Clause
LICENSE.BSD
LGPL-2.1
LICENSE.LGPL
sbp/hedgehog
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
-*- indented-text -*- Introduction ============ Hedgehog is a very concise implementation of a LISP-like language for low-end and embedded devices, primarily for machine-to-machine systems. It consists of a compiler to byte code and a corresponding interpreter. The byte code interpreter is written in standard conforming C, is efficient and easily portable, and can be compiled to a very small executable or library of only some 20-30 kilobytes. The Hedgehog LISP dialect has proper support for local and lambda functions, lexical scoping, variable argument functions, garbage collection, exceptions, macros, and over a hundred predefined functions or special forms. The built-in types are lists, symbols, strings, 32-bit integers, AVL-trees, and tuples up to 16 elements wide. Proper 32-bit wide integers are necessary for various bit-level operations in embedded systems. The Hedgehog compiler and byte code interpreter, essentially all code written in C, is distributed under the "Lesser" GNU General Public License (GPL). In layman's terms, you have to ship the source code of the Hedgehog compiler and interpreter whenever you ship the corresponding binary executable. However, all files written in Hedgehog Lisp itself, including the standard library in the prelude.d directory, are distributed the revised BSD license, which allows them to be included also in programs that are distributed without full source code. See the files LICENSE.LGPL and LICENSE.BSD. This public version of the lisp compiler is routinely exercised on Linux and Cygwin, it has been briefly tested on FreeBSD and SunOS, and should be easily portable to any Posix system. The interpreter part has additionally been ported to several embedded operating systems. Support, alternative licenses, and ports can be discussed with Oliotalo Ltd. The ports have support for various protocols and devices specific for that platform. For further information or to download, see http://hedgehog.oliotalo.fi/ or mail [email protected]. Authors: Kenneth Oksanen <[email protected]> Lars Wirzenius <[email protected]> Version-specific Note ===================== This series of versions, hedgehog-2.*, has been extracted and cleaned up from a significantly more featureful hedgehog-1.6, which is used by Oliotalo in several embedded machine-to-machine solutions. While many parts of the code are mature and well-tested, this version undoubtedly contains also immature and incorrect pieces of code. Also some parts are missing, such as a few good demo applications and various features planned for the state machine library (which hedgehog-1.6 has but we wish to improve for 2.0). Therefore we consider this as an beta-quality at best. But we will gradually improve. Running HedgeHog ================ To compile and install Hedgehog on a typical Linux installation, unpack the sources and change to the source directory and then do the following: mkdir o cd o ../configure linux /usr/local make where `make' refers to GNU make. This builds to executables the lisp compiler `hhc' and the byte code interpreter `hhi', and some lisp library files in the directory `prelude.d'. Hedgehog has been compiled and tested on a number of other platforms, including FreeBSD and SunOS, but you will need GNU make and in many cases, especially if you wish to edit the develop the code further, also the GNU awk and lex replacements gawk and flex. Assuming the above completed successfully, you should now be able to compile tests in the `tests'-directory, for example by issuing ./hhc -g -p prelude.d ../tests/fib.hl in order to compile the ubiquitous Fibonacci-test. This produces two output files, `fib.hlo' and `fib.hls'. The latter is a symbolic assembler file and used mostly for debugging after a possible error reported by the interpreter. It is particularly useful if you didn't pass `hhc' the flag `-g' telling it to add a little debugging information to the `fib.hlo'. The former is the actual byte code program, augmented with a small header and the program's constant data. You should now be able to run the compiled Fibonacci-test in `fib.hlo' by by issuing ./hhi fib.hlo Depending on your machine's speed this will last a few seconds, but eventually you should see the result: fib(30) = 1346269 or fib(22) = 28657 depending on which distribution you have. The flag `-p prelude.d' you passed to `hhc' tells it to use the lisp library in the subdirectory `prelude.d'. Conventionally you should make install with sufficient priviledges. This places `hhc' and `hhi' in `/usr/local/bin' and the standard library `/usr/local/lib/hh', as you had instructed `configure' to do. Now that the prelude is in its predefined place, you can refrain from passing `-p prelude.d' to `hhc'. Pass `hhc' and `hhc' the flag `--help' or `-h' to see their full command line interface. Note that the heap and stack sizes are fixed during each execution, but are adjustable from the command line. The prelude =========== The Hedgehog standard library, or prelude, consists of all the files in the `prelude.d' directory that have a `.hl' suffix. The filenames are expected to be sorted according to ASCII character codes (in the "C" locale, that is: some other locales have weird rules for sorting, e.g., the fi_FI one). The naming convention is, therefore, to begin each filename with three digits and a dash. This will allow ordering between the files. XXX Here comes a little tour of the prelude. Porting and Adaptation ====================== XXX Design and write a guide. If memcmp performs signed comparison of bytes on your platform, you must define HH_MEMCMP to some function which performs unsigned comparison. This is necessary for Lisp-level string comparisons and compression to work correctly. The macros HH_UNIX, HH_SUNOS, HH_BSD, HH_LINUX etc. refer to the target platform where we run hhi, but not necessarily to hhc. But while nothing is written yet, a few words of advice: - In general, the code is supposed to be touched. - Note that the configure script is not GNU autoconf, so you are supposed to add definitions relevant to your target in it. - The interpreter is able to change byte order of the `.hlo'-files automatically as needed, so that you needn't worry about this. If your target looks like unix: - No or at least small editions in hh_common.h should be sufficient to compile hhi. - By default `make' (cross-)compiles and (cross-)executes a small C program which digs out various flag values and struct layouts of the system call interface into the prelude file `300-unix.hl'. Ensure the cross-execution succeeded. If your target does not look like unix: - You have to write a new main program file. You can take `hh_interp_unix.c' as your starting point. - Search for occurrences of HH_UNIX in the code (including the compiler), and adapt the code for your target in similar respects. Byte Code Interpreter ===================== Instruction Format ------------------ The byte code instruction set is designed to be concise, allow a small implementation of the interpreter, but still allow a reasonably efficient implementation. The instructions are of variable instruction length. The uppermost two bits in the first byte of the instruction tell the byte code type. Bits 00 indicate an instruction without an immediate field, bits 01 indicate that the second byte of the instruction is a one-byte signed immediate field. Bits 10 indicate a two-byte immediate value and bits 11 indicate a four-byte immediate value. A handful of instructions use more than a single immediate field. The lowermost six bits of the first byte of each instruction indicate the mnemonic, such as `push' or `load_imm'. The byte code instructions with an immediate field and the ones without are separate. This means there are at most 64 instructions with an immediate field and 64 instructions without. The latter is often too few, and therefore rarely used non-immediate instructions are encoded as "ext insns" a special immediate instruction plus an immediate field that specifies the instructions. All instructions are defined in the file `hh_insn.def', which with some C macro trickery is included in several places for various purposes. Some instructions, such `load_imm' or `pick', have variants which first push the value of accu to stack before actual computation. This both reduces byte code program lengths and improves performance because of reduced instruction dispatching. For some other instructions, for example for jumps and many non-atomic expression, such variants would be rarely used, and are therefore omitted in order to save instruction space. Virtual Machine --------------- The virtual machine is a stack machine but so that the least recently computed value cached in a special "register" `accu'. The benefit of `accu' is twofold: Firstly, if the result is not used, there is no need to pop it off the stack. Secondly, `accu' reduces the traffic to and from the stack, thereby somewhat improving the performance. The stack pointer `sp' points to the position in stack where the next pushed value goes to. The stack grows upward. A function is a tuple whose first word refers to the beginning of the function's byte code and the second word refers to the function's environment, or HH_NIL (i.e., the Lisp nil) if there is only the global environment. The first byte code instruction of each function is actually not an instruction at all, but a byte of information of the arity of the function. The seven lowermost bits tell the number of arguments the function expects, and if the uppermost bit is set, then the function is a variable argument function. The same stack is used to store arguments to both byte code instructions, builtin functions and user-defined functions. Let's follow through a proper function call. The caller evaluates all subexpressions in the s-expression from left (the called function) to right, pushing all but the last one which is left in `accu'. Now it issues the byte code instruction `call' with the immediate value giving the number of arguments in the call. This instruction does all of the following: - Find the definition of the function. - Check that the number of arguments of the callee matches the number of passed arguments. - If the callee is a variable argument function, make a list of the excess arguments. The list is left in `accu'. - Store the caller's next instruction's program counter into the place where the callee function pointer was in stack. - Load the callee's pc and environment pointer from the function tuple. And then we're already executing the callee. Returning occurs with `return n' which pops `n' values off the stack to find and resume the caller's stored program counter. Dynamic extent is achieved by creating small tuples, called "environments" or "closures", which contain exactly those variables which do not by their nature have infinite lifetime (such as global variables) and are referred to from local "lambda" functions created by `fn's or `def's. For example (def (mkf2 x) (fn (y) (+ y x))) is automatically turned into something like (def (mkf1 x) (.make_new_env 2) (.put_new_env 0 x) (.bind_env $.0)) (def ($.0 y) (+ y (.get_env 0))) The `$.0' is an automatically generated symbol for the local `fn' lifted to the top-level. `.bind_env' leaves to `accu' the function `$.0' bound to the created environment, which contains the value of variable `x', which the local function refers to. A callable function is a cons-cell, whose CAR-slot contains a program counter and CDR-slot contains the environment tuple, or nil if the function does not refer to local variables defined outside of it. Calling a function always reads the environment bound to that function, but otherwise environment pointers are pushed and restored from the stack on a need -basis. Since global functions refer only to values in their argument list or the global environment, there is no need for environment pointer storage/restoration in global functions. Catch is implemented by pushing onto stack a special header (see later) word which indicates the catch tag, the catching program counter and current environment pointer. The throw scans the stack downwards looking for a matching catch tag, and if it sees one, then it resumes from the program counter and environment after the catch header. Byte Code Program File Format ----------------------------- The program is a sequence of bytes so that - Bytes 0..3 are a magic cookie and contain the values 0x4E, 0xD6, 0xE4 and 0x06, respectively. - Bytes 4..7 contain a 32-bit checksum of the rest of the program. This can be used to check that the program has been received correctly, but it is not resilient to malicious attacks. - Byte 8 is reserved for future use, most probably a version number for the format of constant values, debugging information, and this header. Currently it should be 1. - Bytes 9..11 tell `proglen', the length of the program block, in bytes. - Bytes 12..12+proglen-1 contain the program, the execution starts at the 12th byte. - Bytes 12+proglen..end contain constant data, the "constant pool", most significantly strings and names of quoted symbols for the `symboltostring' primitive. All values are encoded in the most significant byte first order. There is no general assumption of alignment inside the byte code, but the first byte of the program must be aligned to a 32-bit word boundary. Garbage Collection ------------------ The current garbage collector is a trivial two-semispace stop©, which draws no benefit from the fact that objects are never mutated. Garbage collection may initiate at the beginning of any instruction, but not anywhere else. This means that if the instruction allocates memory, it must prior to changing the state of the program, such as manipulating `accu', the stack pointer, or the stack, compute how much memory it is going to allocate, and issue garbage collection if necessary. The macro `HH_RESERVE' is a convenience macro for the last task. If garbage collection does not free enough memory for the computation to proceed, the virtual machine tries to throw an "out-of-memory-exception". If it is caught, a second garbage collection is issued and the execution tries to resume after the catch. If the throw is not caught, the virtual machine exits with a special error code. Tagging ------- Various bits of each word is reserved to represent type information. The deduction rules for determining the type of any given 32-bit word goes as follows: If the lowest bit is 1, the word is a signed integer in the range from -2^30 to 2^30-1 and it is obtained by arithmetically shifting the word one right. If the lowest bits are 00, the word is valid pointer to a heap-allocated object. The next 6 bits give partial type information of the referred object: 0000 0000 Forward pointer used by the garbage collector. 0000 0100 Pointer to an object whose type is in a heap-allocated header word. 00nn nn00 Pointer to an n-tuple, if nnnn=0010, a cons-cell. 0100 0x00 Pointer to an integer so that the 32-bit signed integer value can be reconstructed by exclusive or of the referred word and the x-bit shifted right by 2. 1xxx xx00 Pointer to a cell in the constant pool, `xxxxx' have the same type meaning as described above. Other cases are reserved for future use. And finally, if the two lowest bits are 10, the word is a header in a cell: 0000 0010 Some special symbols. The symbol is identified by the upper 24 bits: 0000 0000 0000 0000 0000 0000 Empty list, NIL. 0000 0000 0000 0000 0000 0001 Symbol t, true. 0000 0110 Reference to somewhere in the byte code program. 0000 1010 A heap-allocated string. The upper 24 bits indicate the length of the string in number of bytes. The following bytes after the initial word are used as bytes in the string. Contrary to C-string, these strings may contain null-characters which do not terminate the string. But for convenience, there is also a redundant null-character after the end of the string so that the string can be passed to normal C routines and system calls without copying it out of the heap. 0000 1110 A symbol. The second word contains a pointer to the string. 0001 0010 A catch cell. This can exist only in the stack. The upper 24 bits tell the catch/throw tag, the next word is catch handler program counter, and the third word is the catcher's environment. 0001 0110 AVL-tree node. The bits 8..15 and 16..23 tell the heights of the left and right subtrees, respectively, and bits 24..31 the height of the the subtree of the current node. 0001 1010 Debugging info header. If this exists, then it must exist as a second word in the constant pool. The third word contains the filename list and the fourth word contains a pointer to the pc to filename-linenumber -mapping. Other cases are reserved for future use. XXX Debugging information Byte Code Compiler ================== Passes and Data Structures -------------------------- The main compiler driver routine is in file `hh_compiler.c'. It calls the function `hh_ast_read_file' in file `hh_ast.c' to read in Lisp-files and construct syntac trees of them. The lexer in `hh_lex.l' is implemented with the help of the GNU scanner generator flex. The abstract syntax tree of the program is represented as a directed acyclic graph of `hh_ast_t' structs. For normal s-expressions it behaves like a tuple - the i'th element of an s-expression is accessed with an array lookup - but for potentially extremely large s-expressions, such as function bodies, do-forms, and top-level program definitions, the `hh_ast_t's form a NULL-terminated list. Each `hh_ast_t' node maintains information of its source location. All strings and symbols are interned in the compiler, i.e. identical symbols and strings are the same object in the compiler's memory. The corresponding types are `hh_string_t' and `hh_symbol_t'. After reading in the syntax tree of the whole program, it is passed to `hh_macroexpand.c' for macro expansion. The macro expansion returns the expanded program without any macro definitions and macro uses. The lambda-lifting is one of the most complicated parts of the compiler. It "lift" locally defined functions (`fn's and `def's) into top-level functions which assume to be bound to suitable environment tuples. Naturally lambda-lifting also augments the site of the definition of the local function with code that creates a suitable environment for the lifted function, and binds the environment to the lifted function (as a program counter). Furthermore the lifted function's references to variables in the environment are replaced with expressions that read the variables' values from the environment. After lambda-lifting we perform algebraic optimization passes implemented in `hh_opt.c'. Currently the only algebraic optimization is a simple folding of boolean constants for the primitives `if', `and', `or', and `not'. Note that the pass removes code which becomes unreachable by these boolean constant foldings. Next the compiler passes the whole program through uses-analysis, which computes the transitive closure of all functions referred from the top-level main program. Unreferred functions, ofter a huge majority if numerous libraries are included, can be discarded from the final output based on this analysis. Next it is time for code generation. The code is generated by a recursive walk-through in the syntax tree into small `hh_code_t' structs linked together in a doubly linked list. Applications of built-in primitives are recognized and generated according to definitions in `hh_builtins.def'. The code generation pass keeps track of the tail-call position of the currently generated expression and automatically uses tailcalls wherever possible. This pass also keeps track of stack positions of variables in the scope, and generates corresponding code for the variable references. Note that code generation uses a "fake" heap (in the fake execution context `hh_constant_ctx') where it generates all constant data. This trick makes it possible to use the definitions in files `hh_data.[hc]' for both run-time and compile-time data management. Here I suggest one should add at least a modest peephole optimization, but currently nothing to that effect is done. Finally it is time to output the generated code. This begins with assigning an absolute position for all byte code instructions. Note that immediate fields' width depend on the size of the absolute value of the immediate field, and the value of the absolute fields in e.g. relative branch instructions may depend on the width and relative position of other instructions in the vicinity. This chicken-and-egg problem is solved by an approximate "cooling" process which uses increasingly much unnecessarily large immediate field widths if the absolute placement of instructions would otherwise seem impossible. The actual byte code output is then a rather trivial loop over all generated and placed `hh_code_t's. The same loop generates both the actual code file (suffixed `.hlo') and the symbolic assembler file (suffix `.hls'), which is necessary for any kind of debugging. The byte code file is prepended by a small header and prepended by the contents of the "fake" heap of constant data. The compiler is admittably somewhat sloppy regarding its memory management: symbols, string constants, abstract syntax tree nodes and byte code structs are never actively freed. But since the memory consumption and running time of the compiler is negligible in any case, this shouldn't matter as long as the host's operating system provides proper process cleanup after the compiler is finished. Files and Conventions ===================== The byte code interpreter has been designed to be usable as a stand-alone program as well as a library linked into some larger system. It is, for example, possible to run the byte code program for a while (say, 1000 byte code instructions, or until nothing else can be done) and then return to do something else in the rest of the system. It is also possible to merge select-based event loops of the interpreted lisp programs and the containing system into one. The most essential interface the larger system needs to know is in file `hh_interp.h', but routines in `hh_data.h' and the ability implement new byte code instructions in `hh_insn.def' and new Lisp primitives in `hh_builtins.def' may also be of use when implementing some kind of communication between the interpreter and the containing system. Only the files hh_interp.c hh_data.c hh_error.c hh_printf.c hh_avl.c and hh_insn.def hh_error.def and the headers hh_common.h hh_interp.h hh_data.h hh_error.h hh_printf.h hh_avl.h are used by the byte code interpreter. The byte code compiler uses the same `hh_data.[hc]' as the interpreter in order to construct cells in the identical format to the constant pool of the generated byte code program. But there are some minor allocation direction differences, and hence it defines the macro `HH_COMPILER'. All file-, macro and identifier names are prefixed with `hh_' or `HH_'. This hopefully helps to port HedgeHog to systems with polluted namespaces. We have tried to put calls to platform-specific or frequently broken functions behind macros in the interpreter. For example, because many systems have broken `memmove's, all calls go through the macro `HH_MEMMOVE', which may be defined to something platform-specific. Other such macros are `HH_ASSERT' and `HH_MEMCMP'. `malloc' and `free' (defined to `HH_MALLOC' and `HH_FREE') are used sparingly. We also use a private version of the `printf' function family, which not only is much smaller than typical `printf's, but also has a few useful additional features (and lacks some that are not useful to us). The interpreter's main program file (`hh_interp_unix.c' in UNIXes) implements the code that writes the `printf'ed characters to stdout, system log, serial line, or wherever. Because `hh_data.c' is used by both the compiler and interpreter, all calls to the `printf' are hidden behind the macro `HH_PRINT'.
About
Concise implementation of a lisp-like language for low-end and embedded devices
Resources
License
BSD-3-Clause, LGPL-2.1 licenses found
Licenses found
BSD-3-Clause
LICENSE.BSD
LGPL-2.1
LICENSE.LGPL
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published