Teeny Scheme Manual (Hairspray Queen Enemy+)
Teeny Scheme is a fork of TinyScheme with a focus on maintaining the original’s simplicity. (Even if only in spirit.) And allowing easier extensibility, maintenance, and use. This manual focuses of these. The goal is to help you understand how Teeny Scheme works and make you confident in tailoring it for your needs. That’s the best I can hope for.
Note that significant portions of this manual are minimally edited excerpts from the original Manual.txt. Copyright the same as for the code, see Teeny Scheme LICENSE.
Table of Contents
Usage #
Teeny Scheme can be used both as an embedded interpreter for cheap Scheme evaluation. And as a standalone Scheme implementation with REPL, libraries, and script execution.
When used as an embedded interpreter, Teeny Scheme can be linked to an executable as a dynamic library or bundled as a static library. See the Provided APIs section for how to use it But there’s not much to say on that, as I didn’t really explore embedded use.
REPL use, however, works well enough. Teeny Scheme can be called on CLI in a variety of ways:
teenyscheme [-v] [-V] [-h] # 1 teenyscheme # 2 teenyscheme [<file1> file2> ...] # 3 teenyscheme [<file1> <file2> ...] -1 <file> [<arg1> <arg2> ...] # 4 teenyscheme [<file1> <file2> ...] -c <expr> [<arg1> <arg2> ...] # 5
- Show version (
-vand-V) or help (-h) - Start a REPL with a subset of
(scheme r5rs)loaded. See environment variables for how to load more libraries. - Run the provided files as scripts and exit.
- Load the initial files, then evaluate the provided file with
(args)parameter bound to the args listed after it.- The
-1flag is intended for shell shebangs. If you specify#!/somewhere/teenyscheme -1then teenyscheme will be called to process the file. For example, the following script echoes the Scheme list of its arguments:#!/somewhere/teenyscheme -1 (display (args))
- The
- Same as (3), but evaluate an expression
exprwith(args)bound to args after it.
Return codes are:
- 0
- Normal exit
- 1
- Help printed
- 2
- Cannot initialize
- 7
- Eval limit was reached.
- Anything else
- User-provided quit code.
Environment Variables #
Run-time Teeny Scheme is mainly configured through environment variables. (Compile-time macro defines control the initial configuration.)
- TEENYINITFILE (defaults to
init.scmin the current directory) - Path to an init file.
Given that the default Teeny Scheme comes with only a subset of R5RS, init file is needed to get it at least up to R7RS’
(scheme r5rs)or (aspirationally)(scheme base). Use with caution, defaults should be fine. To add more code, checkTEENYLIBPATHinstead. Also note that default build of Teeny loads R5RS-compatible init it is compiled with whenever the init file cannot be found. - TEENYLIBPATH (defaults to
/usr/local/, can be pre-set in config.h)lib/teenyscheme - Path to a directory with loadable R7RS libaries.
Directory structure should reflect the library name.
So
(srfi 160 f64)must be defined in$TEENYLIBPATH/. teenylib is a good example of the structure for this.srfi/160/f64.sld - TEENYCELLSEGSIZE
- Size of a single cell segment, in bytes.
- TEENYCELLNSEGMENT
- Maximum number of cell segments.
- TEENYEVALLIMIT
- Number of steps to stop execution after, for debugging. Only works if you compile with
-DEVAL_LIMIT=1
Description of the last three, courtesy of Rodion Gorkovenko:
CELL_SEGSIZE and CELL_NSEGMENT - expand available memory; memory is allocated by segments, starting with 3, up to CELL_NSEGMENT; every segment can hold CELL_SEGSIZE cells; before allocating more, GC will try to free up existing. Defaults are 20000 and 12.Error code on memory limit reaching is 9.EVAL_LIMIT (if compiled with define of the same name) will stop execution on reaching given amount of steps (with error code 7).
Compile-Time Defines #
A lot of Teeny behaviors are also mandated by C preprocessor macros defined as 1 or 0.
These can be found in config.h, together with their default values and explanations.
To override the defaults, provide the name of the macro with a value in FEATURES when compiling:
# Disable all the features enabled by default make FEATURES="-DUSE_DL=0 -DUSE_MATH=0 -DUSE_UNICODE=0" teenyscheme # Enable the most memory-hungry setup make FEATURES="-DUSE_DL=1 -DUSE_MATH=1 -DUSE_UNICODE=1 -DCELL_SEGSIZE=100000 -DCELL_NSEGMENT=30" teenyscheme # Disable _all_ features make FEATURES="-DUSE_NO_FEATURES=1" teenyscheme
Note that the USE_* macros have to be defined as true or false (1 or 0.)
The code checks them for their value and not them being (un)defined.
If they are not defined, they are set to (sane) default values in config.h.
Standard Compliance #
Like with original TinyScheme, Teeny Scheme currently supports a subset of R5RS.
Roughly corresponding with (scheme r5rs) library of R7RS.
Things woefully missing:
- Full non-decimal flonum notation.
- Circular literals.
- Unwind-aware continuations, at least without current init—it defines dynamic-wind kinda properly.
Things that are not supported, and likely will never be, because they are not Teeny:
- Ratios.
- Unbounded bignums.
- Complex / imaginary numbers.
- Tail call optimization. To avoid blowing up the stack, use
whileordoand iterate. - Re-binding of symbols like
quasiquoteand=>, as these are hard-coded too aggressively in the parser.
Feature Identifiers #
There are some features indicating what functionality is supported:
- math
- When built with
USE_MATH. If it’s not defined, then many math operations will be unavailable and libraries relying on them will be severely broken. - full-unicode
- When built with
USE_UNICODE. Makes procedures likestring->utf8andutf8->stringavailable. Also indicates Unicode support in strings and source code. - foreign-extensions
- When built with
USE_DL. load-extension is available. - big-endian, little-endian
- Endianness
- windows, android, linux, freebsd, solaris, hp-ux, aix, ios, osx, unix
- OSes
- x86-64, x86-32, i386, arm, arm64, riscv, mips, ppc, ppc64, sparc, m68k
- Architectures
Macro Systems #
There are three macro systems coexisting in Teeny at the moment.
Only one of them is native: macro form.
There’s also a tiny wrapper around it, define-macro.
The syntax is:
(macro (unless form) ;; 1
`(if (not ,(cadr form)) (begin ,@(cddr form))))
(macro quasiquote ;; 2
(lambda (l)
something))
(define-macro (begin1 form . other-forms) ;; 3
(let ([res (gensym "result")])
`(let ([,res ,form])
,@other-forms
,res)))
(define-macro quasiquote ;; 4
(lambda (l)
something))
(gensym [string]) ;; 5
(with-gensyms (variables ...) body ...) ;; 6
Explanation:
- This form is misleadingly procedure-like.
However, it accepts only one argument—the form to expand.
Including its head—macro name.
Prefer
define-macroto that, to avoid confusion. - This is a more honest version of the
macroform, emphasizing that it binds a variable to a transformer. -
define-macrois more featureful and procedure-like, in that its argument list is actually an argument list, allowing for positional and variadic arguments and ignoring the macro keyword itself, unlikemacro. Not any more advanced though. Still unhygienic macros and symbol juggling. - Alternative form of
define-macrosynonymous to the matchingmacroform. Not used actively at the moment. -
gensymis a vital helper for macro writing. Returns a new interned symbol each time it’s called. Taking string as the base when provided. Make sure to use it to bind forms that might be otherwise multiple-evaluated. - (Init-provided, trivial)
with-gensymsmakes gensym management slightly less annoying. It binds the provided variable-s to freshgensym-s. If the variable is a two-element list(symbol string), where string is a gensym string instead of symbol name. Otherwise variable should be a symbol whose name will also be used forgensym.
Bundled init also provides a severely incomplete and buggy version of
R7RS syntax-rules
which is sufficient to write some simple macros, but can fail in obscure ways when left unattended.
Prefer define-macro whenever you have the choice.
Syntactic Extensions #
There is a number of syntactic extensions made in Teeny:
- Named lambdas
- Providing any symbol after lambda arglist and before lambda body names the lambda with this symbol. This is heavily heuristic, but should be fine for most cases. At least until there are symbol macros.
- Modifiable parameters
- Calling a parameter on the value sets this parameter to the value, given converter passing.
- Square brackets
- Used as aliases for parentheses. Not configurable (yet?)
Typing #
One important extension that Teeny adds to Scheme syntax is type annotations / abbreviations.
These can be used anywhere lambda formals are implied.
Including in procedure-shaped form of define and in define-macro.
Every procedure argument that needs to be type-checked is represented as a list (name type)
instead of a name symbol.
The syntax of the type part is that of opcode type abbreviations, but represented as a string of 1+ chars.
Only the first char is significant, the rest can be used for documentation purposes.
So all of "s", "string", and "super-confusing" mean a type check for a string.
Same type annotations can be used in define-record-type, whenever it is loaded from either bundled init or teenylib. These types will be checked on record creation and on field modification. Extended syntax of fields then is:
(plain-field plain-accessor) (plain-field plain-accessor plain-modifier) (typed-field "type" plain-accessor) (typed-field "type" plain-accessor typed-modifier)
Some examples:
;; https://dthompson.dev/posts/optimizing-guile-scheme.html
(define (magnitude [x "number"] [y "number"])
(sqrt (+ (* x x) (* y y))))
;; From Teeny init
(define (cddr [x ". pair"])
(cdr (cdr x)))
(define (get-environment-variable [var "string name"])
(let ([val (assoc var (get-environment-variables))])
(and val (cdr val))))
(define (struct-equal? [x "$truct"] [y "$truct"])
(and (eq? (struct-name x)
(struct-name y))
(= (struct-length x)
(struct-length y))
(do ([i 0 (+ i 1)]
[acc #t (and acc
(equal? (struct-ref x i)
(struct-ref y i)))])
((>= i (struct-length x)) acc))))
(define (memq obj [lst "list (any)"])
(generic-member eq? obj lst))
(define (filter [pred "/applicable"] [lst "list"])
(do ([lst lst (cdr lst)]
[acc '() (if (pred (car lst))
(cons (car lst) acc)
acc)])
((null? lst)
(reverse! acc))))
(define-record-type time
(make-time type [nanosecond "integer"] second)
time?
[type "ymbol" time-type set-time-type!]
[nanosecond "number" time-nanosecond set-time-nanosecond!]
[second "integer" time-second set-time-second!])
Return type checking and let-binding checks are possible,
but not yet implemented.
If you want to make them work, check OP_APPLY and type_mismatch function.
Non-standard Goodies #
Most of these descriptions are taken from TinyScheme Manual.txt.
- (current-environment)
- The environment in effect at the time of the call.
Beware: this might not be the environment you want.
It is ephemeral and changes all the time, so the bindings made with
evalorloadmight be lost to oblivion. - (defined? symbol [environment])
- Checks whether the given symbol is defined in the current (or given) environment.
- (gc)
- Performs garbage collection immediatelly.
- (gcverbose [bool])
- The argument (defaulting to
#t) controls whether GC produces visible outcome. - (quit [num])
- Stops the interpreter and sets the retcode internal field (defaults to 0). When standalone, retcode is returned as exit code to the OS. Whenever possible and provided by init, use exit.
- (load-extension [filename-without-extension])
- Loads a DLL declaring foreign procedures.
On Unix/Linux, one can make use of the
ld.so.conffile or theLD_RUN_PATHsystem variable in order to place the library in a directory other than the current one. Please refer to the appropriatemanpage. (This description is likely quite dated, update pending.) - (oblist)
- Returns the oblist, an immutable list of all the symbols. Unlikely to ever be necessary nor useful, as it simply exposes an internal structure of symbol table.
- (macro-expand form) (macro-expand-all form)
- Returns the expanded form of the macro call denoted by the argument.
- (define-with-return (procname args...) body ...)
- Like plain
define, but makes the continuation available asreturninside the procedure. Handy for imperative programs. - (new-segment num)
- Allocates more memory segments.
- (procedure-code procedure)
- Gets the code as Scheme data.
The structure is usually
(lambda args . body). - (procedure-name procedure)
- Gets the name attached to the closure.
- (procedure-arity procedure)
- Gets the
(minimum maximum)values describing the arity of procedure- minimum is always an integer
- maximum is either an integer or
#tfor variadic procedures
#f #f - (make-closure code [environment])
- Makes a new closure in the given environment.
- (eval-count)
- Count of main “evaluation loop” of interpreter, may be used as a time-independent metric of code (in)efficiency.
- (struct? object)
- Whether object is a structure / record.
- (make-struct name size)
- Create a new struct instance with type name and size slots
- (struct-length struct), (struct-name struct)
- Return the number of slots in the struct and its type name, respectively.
- (struct-ref struct index), (struct-set! struct index new-value)
- Getter and setter for a struct.
- (set-parameter-unsafe! parameter value)
- Set parameter value directly, ignoring parameter converter Use with caution.
- (libdir [new-libdir])
- Parameter returning the current libdir, can be set to an optional argument.
- (load-path-name)
- Current file name, whenever being loaded. Not a parameter!
- (type-of object)
- Returns the type of a given object, as a symbol.
In case it returns
unknownfor you, you should probably report it as a bug. - (%cast! object type)
- Cast the object to a given type (symbol, one of the symbols returned by
type-of.) Beware: this opcode converts the object in place, so any use of the same object will now use it as the new type. Prone to breakages, use carefully and cast things back when done. - (make-immutable! object)
- Make the object immutable and return it. Note that this is not reversible, so make sure to use it reasonably.
- (reexport libspec ...)
- Whenever used in
define-library, imports the provided libraries and re-exports their exports from the current library. - (error [type] message irritants ...)
- Extended form of standard error procedure,
allowing for a symbol tag before message argument.
This tag is in particular used for error subtypes and can be accessed with
error-object-type. - (system command)
- Passes a given command to the OS and returns the returned code.
- (open-input-pipe command), (open-output-pipe command)
- Makes a sub-process running the command and returns a port that allows reading or writing to it using regular Scheme APIs like display.
procedure-code is my personal favorite.
Do you know of any Scheme that allows you to see the full code for a lambda, just like that?
Provided APIs #
All the provided APIs are available from teenyscheme/scheme.h header.
Provided APIs differ based on whether USE_INTERFACE was true at build time.
These C functions are provided regardless of its value:
- teeny_scheme *scheme_init_new(void)
- Allocate a new
teeny_schemeobject and return it. - teeny_scheme *scheme_init_new_custom_alloc
(malloc, free) - Allocate a new
teeny_schemeobject and return it. But set its malloc and free attributes to provided functions. Useful in e.g. making allocation stricter with something likexmalloc.- int scheme_init(teeny_scheme *sc)
- int scheme_init_custom_alloc(teeny_scheme *sc, malloc, free)
- Initialize an already allocated
schemeobject, like above.- void scheme_deinit(teeny_scheme *sc)
- De-initialize and cleanup the sc.
- void scheme_load_file(teeny_scheme *sc, FILE *fin)
- void scheme_load_named_file(teeny_scheme *sc, FILE *fin, const char *filename)
- Load / evaluate the given file in sc. Use the
*_named_fileversion whenever possible, so that error messages are more useful.- void scheme_load_string(teeny_scheme *sc, const char *cmd)
- Evaluate expressions from the cmd. Note that this does not return any values. If you want return values, use
scheme_eval.- teeny_object_t scheme_eval(teeny_scheme * sc, teeny_object_t obj)
- Evaluate the s-expression contained in obj. Return the primary returned value.
- teeny_object_t scheme_apply0(teeny_scheme *sc, const char *procname)
- teeny_object_t scheme_call(teeny_scheme *sc, teeny_object_t func, teeny_object_t args)
- Call the respective procedure, without or with args.
scheme_apply0is somewhat useless, because it accepts procedure name and not the procedure itself.- void scheme_set_external_data(teeny_scheme *sc, void *p)
- Set the external data accessible to foreign functions.
- void scheme_define(teeny_scheme *sc, teeny_object_t env, teeny_object_t symbol, teeny_object_t value)
- Bind the given symbol to a value in env.
- Allocate a new
This is a pretty small set of operations.
Not enough to deal with complex data for sure.
That’s why Teeny also exposes data-processing functions when USE_INTERFACE is defined as truthy.
All of these functions are documented in interface.h and are accessible through the sc->vptr field.
So something like integer creation is sc->vptr->mk_integer(sc, 3).
Multiple-interpreter cross-compiler use
In case you want to use scheme.h with multiple compilers, re-build it for each one.
The code in it is mangled before installation to adapt it to the implementation in place.
So, in case you compiled Teeny Scheme with Clang, the scheme.h will contain some Clang-isms.
Like _Nullable or whatever.
Rebuilding for GCC might require doing a clean build and copying the modified file to the right place directly.
In case you want to have a truly compiler-agnostic header, build with a __NOCC__ macro defined:
make FEATURES="-D__NOCC__ -DUSE_MATH=1 -DUSE_UNICODE=1..." clean all
If you also want to use Teeny with C++, build with __cplusplus defined (defined automatically by C++ compilers:)
make FEATURES="-D__cplusplus -DUSE_MATH=1 -DUSE_UNICODE=1..." clean all
This multi-compiler build, however, is a virtue—multiple versions of Teeny Scheme as a library can be generated, suitable for different uses. Play with the compilation flags and features included in config.h to find your perfect Scheme!
Many of these APIs deal with Teeny-specific C types:
- teeny_object_t
- Arbitrary scheme value
- teeny_char_t
- Representation of a character, changing based on Unicode configuration
- teeny_fixnum_t and teeny_flonum_t
- Types representing fixnums / exact integers and flonums / inexact reals, respectively.
tgscheme.h—type-generic Scheme API #
In case you use a C11+ compiler, you can alse use a tgscheme.h header.
(Available as teenyscheme/tgscheme.h include.)
It provides many of the APIs above, wrapped into short and convenient generic macros.
Provided APIs are:
- method(sc, name, args…)
- Call the name-d (interface) method on the provided args. Note that sc has to be passed with all the other args, because some “methods” don’t use it. Providing it automatically would be rude to these.
- mk(sc, literal)
- mknum(sc, number)
- Creates data from C literals. Beware: while
mkis more generic, it does not have floating-point numbers support. Usemknumto create numbers. Also be careful when using these with non-literals—proper functioning not guaranteed. - mksym(sc, symbol)
- Create a new symbol named by symbol.
symbol can be a C string literal or a pre-existing symbol (
teeny_object_t.) - define(sc, env, name, value)
- name is converted to a symbol (as per
mksym) and value is converted to a suitable Scheme value (as permk) and then get assigned as per scheme_define. - eval(sc, expr)
- Shortcut for scheme_eval. Whenever provided a C string as expr, evaluates the expr-named symbol and returns its value. Otherwise equivalent to scheme_eval.
- length(sc, data)
- Return the length (number of elements) in the given piece of data. For now, it’s implemented as a function instead of macro. Might change later.
- apply(sc, fn, args…)
- Applies the provided fn to args, which must be a C arglist of
teeny_object_t-s, with the last one required to be a listteeny_object_t. So the behavior is much alike the Scheme procedure—variable arity, last arg is a list.
Here’s an example showcasing the type-generic API use:
#include "teenyscheme/scheme.h"
#include "teenyscheme/tgscheme.h"
int main(int argc, char *argv[], char *envp[])
{
teeny_scheme sc_, *sc = &sc_;
scheme_init(sc);
teeny_object_t a = mk(sc, 1);
teeny_object_t b = mknum(sc, 1.012321);
teeny_object_t fn = eval(sc, "+");
teeny_object_t res = apply(sc, fn, a, b, sc->NIL);
printf("Result is %Lf\n", method(sc, rvalue, res));
return EXIT_SUCCESS;
}
Extending Teeny Scheme #
This section is loosely based on the old TinyScheme hack.txt example of a memory block. Basically describing how to add a new data structure to Teeny Scheme.
Adding a New Data Type #
New type essentially consists out of some layout of things in memory and opcodes to operate on that.
First, this data type must have some memory layout and a place to belong.
This place is struct cell in scheme.h
(likely to be renamed or removed, but it’s here for now.)
So add it there:
struct cell {
unsigned int flag;
union {
struct {
char *svalue;
int length;
} string;
// New type added:
struct {
// Technically, the same as string above
// But I prefer cleaner and stupider code
char *data;
size_t length;
} memblock;
struct {
teeny_object_t *data;
teeny_object_t name;
int length;
} struct;
/* ... */
} object;
};
Nicer macros can be added for easier access in scheme.c:
#define veclength(p) ((p)->object.vector.length) #define vecelems(p) ((p)->object.vector.elems) // Added: #define memblocklength(p) ((p)->object.memblock.length) #define memblockdata(p) ((p)->object.memblock.data)
After layouting, this type should be registered in types.h. This is the main source of all type info. The fields are:
- Lowercase name
- Used in predicate generation, the predicate gets the
is_$lowername - Uppercase name
- Used in type tag generation, type tag is
T_$UPPER - simple?
- Boolean for whether the value of this type can be easily printed and treated as non-compound.
Make sure to add printing of simple values to
simple2str. - Description
- Human-readable description of a type, as seen in type mismatch error messages
- Char-encoded type abbreviation
- This are a bit cryptic. It assigns type check to a certain character to later be used in type checks in e.g. ops.h. Basically pick a char you like and see if there are compilation warnings.
Currently existing type abbreviations are:
| Abbreviation | Description |
|---|---|
| * | anything |
| p | port (input / output / input+output) |
| < | input port |
| > | output port |
| . | pair (dotted cons) |
| l | list (pair or null) |
| n | number (real or integer) |
| i | integer |
| + | natural / non-negative integer |
| f | procedure in a broad sense—opcode, closure, continuation |
| / | same as f, deprecated |
| s | string |
| c | character |
| y | symbol |
| o | opcode |
| ^ | closure (procedure) |
| ? | continuation procedure |
| @ | foreign procedure |
| v | vector |
| ` | macro transformer |
| ! | promise |
| 8 | bytevector |
| $ | struct (record) |
| : | parameter |
| b | boolean |
| 0 | null |
| x | error object |
| & | foreign pointer |
We can add a record like this to generate our type tags and predicates and all.
_TYPE(parameter, PARAMETER, true, "parameter", ':') _TYPE(boolean, BOOLEAN, true, "boolean", 'b') // Added: _TYPE(memblock, MEMBLOCK, true, "memory block", 'm')
Optionally, add printing of this type to simple2str:
static void
simple2str(teeny_scheme *sc, teeny_object_t l, int f, char **pp, int *plen)
{
string p;
*plen = -1;
if (l is sc->NIL) {
p = (string)"()";
} else if (l is sc->T) {
p = (string)"#t";
} else if (l is sc->F) {
p = (string)"#f";
/* ... */
} else if (is_struct(l)) {
p = sc->strbuff;
snprintf(p, sc->strbuff_size, "#<struct %s>",
symname(structname(l)));
// Added:
} else if (is_memblock(l)) {
p = sc->strbuff;
snprintf(p, sc->strbuff_size, "#<memblock %p [%lu]>",
(void *)l, memblocklength(l));
} else {
p = (string)"#<error>";
}
/* ... */
}
GC-ing the New Type #
GC mismanagement is a source of many segfaults and double-free-s.
Make sure to account for all the resources you use!
mark-ing all the Scheme data.
And finalizing all C data.
If something segfaults, it most likely is GC.
Then we need to modify the garbage collection procedures to be aware of this memory block thing.
The only mandatory place is the finalize_cell that frees up the memory:
static void
finalize_cell(teeny_scheme *sc, teeny_object_t a)
{
if (is_string(a)) {
sc->free(strvalue(a));
} else if (is_vector(a)) {
for (size_t i = 0; i < veclength(a); ++i)
finalize_cell(sc, vecelems(a)[i]);
sc->free(vecelems(a));
// Added clause:
} else if (is_memblock(a)) {
sc->free(memblockdata(a));
} else if (is_port(a)) {
if (a->_object._port->kind & PORT_FILE
and a->_object._port->rep.stdio.closeit) {
port_close(sc, a, PORT_INPUT | PORT_OUTPUT);
}
sc->free(a->_object._port);
}
}
In case your new type contains other Scheme data, you need to make it known to garbage collector too.
Otherwise this data might be silently swiped away in the next round of GC.
Marking the data involves only one change, in mark() function in scheme.c.
In case your data structure is a sequence of Scheme datums, this struct incantation might be a good template:
if (is_struct(p)) {
size_t len = structlength(p);
for (size_t i = 0; i < len; i++) {
mark(structdata(p)[i]);
}
}
In case your structure is box-like, containing a restricted number of values, something like this might work:
if (is_parameter(p)) mark(paramvalue(p));
The heuristic basically is: find all the Scheme data in your data structure and mark every one of them. GC will handle the rest.
Memblock example does not need marking, as its data is all C-side.
Adding Embedded Interface Entries #
You likely want to make the data structure you created accessible to the embedded users. Which means: interfaces and function pointers. At the very least, define a type predicate, type constructor, and type getters / setters for embedded user convenience:
interface teeny_object_t
mk_memblock(teeny_scheme *sc, size_t len, char fill)
{
char *p = (char *) sc->malloc(len);
if (p is nil)
return sc->NIL;
teeny_object_t x = get_cell(sc, sc->NIL, sc->NIL);
typeflag(x) = T_MEMBLOCK | T_ATOM;
memblockdata(x) = p;
memblocklength(x) = len;
memset(p, fill, len);
return x;
}
/* is_memblock is defined by types.h */
interface char
memblock_elem(teeny_object_t p, size_t index)
{
return memblockdata(x)[index];
}
interface void
set_memblock_elem(teeny_object_t p, size_t index, char value)
{
memblockdata(x)[index] = value;
}
interface size_t
memblock_length(teeny_object_t p)
{
return memblocklength(p);
}
And then add the respective entries to interface.h:
// Constructors /* ... */ _INTERFACE(teeny_object_t, mk_memblock, mk_memblock, teeny_scheme *sc, size_t len, char fill) // Predicates /* ... */ _INTERFACE(bool, is_memblock, is_memblock, teeny_object_t p) // Value handling /* ... */ _INTERFACE(size_t, memblock_length, memblock_length, teeny_object_t p) _INTERFACE(char, memblock_elem, memblock_elem, teeny_object_t p, size_t index) _INTERFACE(void, set_memblock_elem, set_memblock_elem, teeny_object_t p, size_t index, char value)
The fields are:
- 1: return type
- Return type of the “exported” function, stripped from all the
staticet al. - 2: exported name
- The name it will be exposed by the interpreter under e.g.
sc->vtbl.is_memblock, a callable struct member - 3: implementation function name
- It’s fine if it’s the same name as in (2)—separating these is more of a hack than a rule. Most of the time you just paste the same name here.
- rest: arguments
- Signature of a given function, basically. Useful in auto-generated type declarations.
That’s basically it.
interface.h is used in a bunch of places to generate nice and proper types and fields.
So just add things there and use them through sc->vptr!
Adding a New Opcode #
There are two main places to edit for new opcode:
- ops.h where metadata about opcodes resides, and
- scheme.c where the implementation / handler of the opcode goes.
First, ops.h.
Add an _OP_DEF line there.
The fields are:
- 1: opcode name
- An optional string naming the Scheme-side symbol this opcode will be bound to.
Can be set to
nilin case the opcode is internal or otherwise unnamed. - 2: minimum arity
- Mandatory integer determining how many required arguments there are.
- 3: maximum arity
- How many arguments can the procedure accept.
In case it’s
INF_ARG, the procedure is variadic and accepts any number of arguments after the required ones. - 4: type checks
- A string of type check abbreviations, format explained in type addition section. Strictly one check / char per argument. In case the procedure is variadic, the variadic argument should be typed too, with the last type check applicable to every of its elements. In case the procedure is not variadic but has optional arguments, these too can be typed.
- 5: opcode tag
- This will become an opcode-identifying enum tag and will be used in handler definition.
So the example memblock opcodes are:
// Notice that the optional argument is typed too
_OP_DEF("make-block", 1, 2, "++", OP_MKBLOCK)
// Note the use of the "m" type check
_OP_DEF("block-length", 1, 1, "m", OP_BLOCKLEN)
_OP_DEF("block-ref", 2, 2, "m+", OP_BLOCKREF)
_OP_DEF("block-set!", 3, 3, "m++", OP_BLOCKSET)
_OP_DEF("block?", 1, 1, "*", OP_BLOCKP)
After the line describing the opcode is added, it’s possible to write a handler in scheme.c.
(It’s also possible to write a handler and only then add an opcode metadata—it won’t compile without either of these anyway.)
Two forms that help with that are DEFHANDLER and DEFSHORTHANDLER.
DEFSHORTHANDLER is for simple one-line opcodes returning a single value.
While DEFHANDLER is for more involved handlers.
Respective syntax:
DEFHANDLER(OP_CODE_TAG, "scheme-procedure-name or freeform description", teeny_scheme *sc)
{
some;
statements;
s_return(sc, ...); // or s_return_values(sc, values_cons);
// ENDHANDLER is mandatory for some macro magic
ENDHANDLER}
DEFSHORTHANDLER(OP_CODE_TAG, "scheme-procedure-name or freeform description", return_value);
// essentially equivalent to:
// DEFHANDLER(OP_CODE_TAG, "scheme-procedure-name or freeform description", teeny_scheme *sc)
// {
// s_return(sc, return_value);
// ENDHANDLER}
DEFHANDLER always has a single argument, Scheme interpreter.
And it implies a proper function body.
While DEFSHORTHANDLER has this argument implied as sc.
And always returns the value provided after description.
See scheme.c for actual opcode definitions.
I tried to make them as readable as possible.
Shame on me if they aren’t.
First, the opcode to create memory blocks and the underlying function:
DEFHANDLER(OP_MKBLOCK, "make-block", teeny_scheme *sc)
{
int fill = 0;
size_t len = ivalue(car(sc->args));
if (cdr(sc->args) != sc->NIL)
fill = charvalue(cadr(sc->args)) % 255;
s_return(sc, mk_memblock(sc, len, (char)fill));
ENDHANDLER}
Then, two short handlers—for block length and block checking. Preferably, put them into the same group with the other short handlers:
DEFSHORTHANDLER(OP_CURR_ENV, "current-environment", sc->envir) // Added: DEFSHORTHANDLER(OP_BLOCKLEN, "block-length", mk_integer(sc, memblock_length(car(sc->args)))) DEFSHORTHANDLER(OP_BLOCKP, "block?", mk_boolean(is_memblock(car(sc->args)))) // *INDENT-ON*
Now the gory part: element references
DEFHANDLER(OP_BLOCKREF, "block-ref", teeny_scheme *sc)
{
char *data = memblockdata(car(sc->args));
int index = ivalue(cadr(sc->args));
if (index < 0 or (size_t) index >= memblocklength(car(sc->args)))
error_named_1(sc, "out of bounds:", cadr(sc->args));
s_return(sc, mk_integer(sc, data[index]));
ENDHANDLER}
DEFHANDLER(OP_BLOCKSET, "block-set!", teeny_scheme *sc)
{
check_immutable(car(sc->args),
"unable to alter immutable memory block:");
char *data = memblockdata(car(sc->args));
int index = ivalue(cadr(sc->args));
if (index < 0 or (size_t) index >= memblocklength(car(sc->args)))
error_named_1(sc, "out of bounds:", cadr(sc->args));
char c = ivalue(caddr(sc->args)) % 255;
data[index] = (char)c;
s_return(sc, car(sc->args));
ENDHANDLER}
Note that, compared to the old tutorial, all the type checks are gone. They are performed by the interpreter based on opcode definitions.
With that, we have a new data structure with a proper memory layout. And with opcodes to work with it.
Helpers for Opcode Writing #
Some of these featured in the preceding section, some are slightly more esoteric. You’ll know it when you’ll need them.
- s_return(sc, value)
- s_return_values(sc, first, other)
- These essentially return value(s) that the respective opcode should return to the caller.
s_return_valuesgets ansc; first / primary value; and then acons-ed list of other values. Use these instead of Creturnin handlers, as they manage stack and registers properly. - s_goto(sc, op)
- This “jumps” to another opcode, in case you want to delegate execution to it. Make sure to set registers properly before using that!
- s_goto_args(sc, op, args)
- Same as
s_gotobut setssc->argsto its argument. While not strictly necessary, it’s too frequent of a pattern to just ignore. - s_save_args_code(sc, op, args, code)
- This one is particularly useful when
s_gotois used. It’s a way to chain opcodes so that the “saved” one is executed after the “goto-ed” one (in last-in-first-out order.) Might be useful if your opcode delegates some work to another opcode but then needs to process the results too. - sc->args, sc->code, sc->value, sc->other_values
- These are registers of
struct schemethat are used a lot in opcodes.-
sc->argsis the cons list of arguments passed to the opcode. -
sc->codeis somewhat arbitrary and is mostly used in syntactic opcodes, representing the code they process. But some opcodes use it in a loose “s-expression to process” sense. So meta. -
sc->valueandsc->other_valuesare basically the primary value and the values after it. This is a weird design, but it’s there for backwards-compatibility and because the primary value is used too often to destructure conses all the time.
-
- error_0(sc, msg)
- error_1(sc, msg, arg)
- error_many(sc, msg, arg_cons)
- error_typed_many(sc, type_string, msg, arg_cons)
- error_1(sc, msg, arg)
- Use these as much as possible—opcodes should be as strict as possible, like all of Teeny Scheme.
They error out in the REPL or call
(error-hook)on error. With nice metadata like function or opcode name and file / line.
As an example of s_save_args_code and other helpers,
call-with-values
uses one additional opcode:
_OP_DEF("call-with-values", 2, 2, TST_PROCEDURE TST_PROCEDURE, OP_CALLWV0)
_OP_DEF(nil, 0, 0, nil, OP_CALLWV1)
And then delegates the work to OP_APPLY when applying value-producing thunk:
DEFHANDLER(OP_CALLWV0, "call-with-values (entry)", teeny_scheme *sc)
{
s_save_args_code(sc, OP_CALLWV1, sc->NIL, cadr(sc->args));
sc->code = car(sc->args);
sc->args = sc->NIL;
s_goto(sc, OP_APPLY);
ENDHANDLER}
DEFHANDLER(OP_CALLWV1, "call-with-values (apply the second procedure)",
teeny_scheme *sc)
{
sc->args = cons(sc, sc->value, sc->other_values);
s_goto(sc, OP_APPLY);
ENDHANDLER}
Adding a New Base Syntax Form #
On the example of a recently added while form.
First, add name detection to syntaxnum (yeah, this code sucks:)
static int
syntaxnum(teeny_object_t p)
{
char *s = symname(p);
switch (strlen(s)) {
/* ... */
case 4:
/* ... */
case 5:
switch (s[2]) {
case 'g':
return OP_BEGIN; // begin
case 'l':
return OP_DELAY; // delay
case 'c':
return OP_MACRO0; // macro
// Added:
case 'i':
return OP_WHILE0; // while
default:
return OP_QUOTE; // quote
}
/* ... */
}
}
Then, assign the syntax alongside all the rest in scheme_init_custom_alloc:
assign_syntax(sc, "macro");
assign_syntax(sc, "case");
// Added:
assign_syntax(sc, "while");
What’s left is writing opcodes processing this new syntax. First, declarations in opcodes file:
_OP_DEF(nil, 0, 0, nil, OP_WHILE0) _OP_DEF(nil, 0, 0, nil, OP_WHILE1)
And then actual code in scheme.c:
DEFHANDLER(OP_WHILE0, "while", teeny_scheme *sc)
{
s_save(sc, OP_WHILE1, car(sc->code), cdr(sc->code));
sc->code = car(sc->code);
s_goto(sc, OP_EVAL);
ENDHANDLER}
DEFHANDLER(OP_WHILE1, "while...", teeny_scheme *sc)
{
/* ... */
ENDHANDLER}
Note that syntactic opcodes don’t get sc->args.
They get sc->code, containing the forms after their keyword.
So a form like (while (positive? a) (display a) (set! a (- a 1))) gets
((positive? a) (display a) (set! a (- a 1))) as its sc->code.
What’s left is figuring out how to write proper opcodes for my syntactic monstrosities.
Adding an Extension Library in C #
This is a fragment copied from the original Manual.txt and edited slightly. To be extended.
The user can add foreign functions and extension libraries in C when Teeny was compiled with USE_DL.
For example, a function that squares its argument may look like:
teeny_object_t square(teeny_scheme *sc, teeny_object_t args) {
if (args != sc->NIL) {
if (sc->vptr->is_number(sc->vptr->pair_car(args))) {
flonum_t v = sc->vptr->rvalue(sc->vptr->pair_car(args));
return sc->vptr->mk_real(sc, v * v);
}
}
return sc->NIL;
}
This function can then be bound to a symbol:
sc->vptr->define( sc, sc->global_env, sc->vptr->mk_symbol(sc, "square"), sc->vptr->mk_foreign_func(sc, square, "n"));
The syntax of the third argument is that of type tests, as used in opcodes and closure typing. In particular, optional and rest arguments can be typed too.
Foreign functions can use the external data (ext_data) in the scheme struct
to implement any kind of external state.
External data are set with scheme_set_external_data.
vptr is inconvenient
It is. Type-generic interface and custom macros can help.
But for that foreign function to be accessible in the Scheme world, it has to belong to a library. This library is required to:
- Be accessible to OS-specific DLL loading facilities (
dlopenon Unix-es.) - Have a name (part of a file (base)name before the dynamic library extension.)
- Have an initialization function of the form
init_$name. This init function must acceptschemepointer and returnvoid. Put all thedefine-s there.
Foreign Pointers #
One can add foreign functions to wrap C functions on the Scheme side. However, there is a need for representing C pointers on Scheme side too. That’s what foreign pointers API does: representing and manipulating pointers.
- (datum->pointer object)
- Convert object to a pointer object with address matching the internal address of the object itself.
- (pointer->integer ptr), (integer->pointer n)
- Convert between Scheme integer and pointer having address equal to this integer.
- (bytevector->pointer bytevector)
- Converts a given bytevector to a pointer object pointing directly to its contents.
Try
(pointer-ref (bytevector->pointer #u8(1 2 3)) 8) - (pointer-ref ptr size), (pointer-set! ptr size value)
- Getter and setter for pointer contents. Sets the first size bits of the ptr to value (positive integer.) Endianness depends on the system and is not penetrable.
- (pointer+ ptr offset)
- Add offset bits (can be negative) to ptr.
In case you want to reference some data at a distance from a pointer,
the combo is
(pointer-ref (pointer+ ptr offset) size) - (pointer? object), bool is_pointer(teeny_object_t p)
- Scheme- and C-side predicates to check whether something is a pointer object.
- void *pointer_raw(teeny_object_t ptr)
- C function to get the actual pointer from the pointer object.
Right now, only 8, 16, 32, and 64 are supported as sizes in accessors.
And only multiples of 8 are supported as offsets in pointer+.
Extending Parts of Implementation with Hooks #
A part of Teeny implementation logic can be overridden through the use of hook variables / parameters. These are subject to change, but useful nonetheless. The variables / parameters are:
- parameter: (compile-hook [value])
- Called on a lambda code—
(args . body)—before it is evaluated. Might be useful in optimization. - parameter: (error-hook [value])
- Called with a single argument—R7RS error object. (File and read errors, along with other error classes, are in progress.) Allows overriding the exception handling and preventing errors “bubbling up” to toplevel.
- parameter: (sharp-hook [value])
- Called whenever the implementation encounters an unknown piece of syntax prefixed with hash
#symbol. Should be a zero-argument procedure handling read-ing on its own.
Contribute to Teeny!
I will be happy to accept contributions as long as they are small, focused, and have no AI-generated content. Visit Teeny Scheme repository to learn more and get accustomed with the code.