Teeny Scheme Manual

Teeny Scheme is a fork of TinyScheme with a focus on maintaining the original’s simplicity. (Even if only in spirit.) And allow 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 copy-paste of original Manual.txt. Copyright the same as the code, see Teeny Scheme LICENSE.

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. The available functions are all listed in interface.h. There’s not much more to say on that, as I didn’t really explore this use. Refer to the original Manual.txt in the meanwhile. I’ll be back with fresher explanations at some point.

REPL use, however, works well enough. Teeny Scheme can be called on CLI in a variety of ways:

teenyscheme # 1
teenyscheme [<file1> file2> ...] # 2
teenyscheme [<file1> <file2> ...] -1 <file> [<arg1> <arg2> ...] # 3
teenyscheme [<file1> <file2> ...] -c <expr> [<arg1> <arg2> ...] # 4
  1. Start a REPL with a subset of (scheme r5rs) loaded. See environment variables for how to load more libraries.
  2. Run the provided files as scripts and exit.
  3. Load the initial files, then evaluate the provided file with *args* bound to the args listed after it.
    • The -1 flag is intended for shell shebangs. If you specify #!/somewhere/teenyscheme -1 then 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*)
      
  4. Same as (3), but evaluate an expression expr with *args* bound to args after it.

Environment Variables

Run-time Teeny Scheme is mainly configured through environment variables, so this list is likely to grow and to be the most useful piece of knowledge about Teeny Scheme. Heed.

TEENYINITFILE
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, check TEENYLIBPATH instead.
TEENYLIBPATH (TODO)
Path to a directory with loadable R7RS libaries. Directory structure should reflect the library name. So (srfi 160 f64) must be defined in $TEENYLIBPATH/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 5000 and 10. 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 scheme.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

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:

Once these are covered, the real work on R7RS compatibility will begin. With define-library being the prime target for implementation, with all the surrounding import, include yada yada.

Things that are not supported, and likely will never be, because they are not Teeny:

Non-standard Goodies

Most of these descriptions are taken from TinyScheme Manual.txt.

;; The environment in effect at the time of the call.
(current-environment)

;; Checks whether the given symbol is defined in the current (or given) environment.
(defined? <symbol>) (defined? <symbol> <environment>)

;; Returns a new interned symbol each time.
;; Taking string as the base when provided.
(gensym) (gensym <string>)

;; Performs garbage collection immediatelly.
(gc)

;; The argument (defaulting to #t) controls whether GC produces visible outcome.
(gcverbose) (gcverbose <bool>)


;; Stops the interpreter and sets the 'retcode' internal field (defaults
;; to 0). When standalone, 'retcode' is returned as exit code to the OS.
(quit) (quit <num>)

;; Loads a DLL declaring foreign procedures. On Unix/Linux, one can make
;; use of the ld.so.conf file or the LD_RUN_PATH system variable in order
;; to place the library in a directory other than the current one. Please
;; refer to the appropriate 'man' page.
(load-extension <filename without extension>)

;; Returns the oblist, an immutable list of all the symbols.
(oblist)

;; Returns the expanded form of the macro call denoted by the argument
(macro-expand <form>)

;; Like plain 'define', but makes the continuation available as 'return'
;; inside the procedure. Handy for imperative programs.
(define-with-return (<procname> <args>...) <body>)

;; Allocates more memory segments.
(new-segment <num>)

;; Gets the code as scheme data.
(get-closure-code <closure>)

;; Gets the (minimum . maximum) pair describing the arity of procedure
;; - minimum is always an integer
;; - maximum is either an integer or #t for variadic procedures
;; Works on both opcodes and actual closures / procedures
;; In all other cases, returns #f
(procedure-arity <procedure>)

;; Makes a new closure in the given environment.
(make-closure <code> <environment>)

;; Time since UNIX Epoch in seconds.
(current-second)

;; Count of main "evaluation loop" of interpreter, may be used as a
;; time-independent metric of code (in)efficiency.
(eval-count)
get-closure-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?

Extending Teeny Scheme

This section is loosely based on the old TinyScheme Manual.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 exist out of some layout of things in memory and opcodes to operate on that. Opcodes are covered in the respective section. Data structure part is covered here.

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 {
			pointer *_data;
			pointer _name;
			int _length;
		} _struct;
		/* ... */
	} _object;
};

Now, this type needs a type tag. It can be added into enum scheme_types in scheme.c:

enum scheme_types {
	T_STRING = 1,
	/* ... */
	T_MEMBLOCK,
};

Predicate for this structure is mirroring that of other types:

INTERFACE bool
is_string(pointer p)
{
	return (type(p) is T_STRING);
}

// Added:
INTERFACE bool
is_memblock(pointer p)
{
	return (type(p) is T_MEMBLOCK);
}

Nicer macros can be added too:

#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)

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(scheme *sc, pointer 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(a->_object._memblock._data);
	} 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);
	}
}

Optionally, add printing of this type to atom2str:

static void
atom2str(scheme *sc, pointer l, int f, char **pp, int *plen)
{
	char *p;

	*plen = -1;
	if (l is sc->NIL) {
		p = (char *)"()";
	} else if (l is sc->T) {
		p = (char *)"#t";
	} else if (l is sc->F) {
		p = (char *)"#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 [%lu]>",
			 memblocklength(l));
	} else {
		p = (char *)"#<error>";
	}
	/* ... */
}

Also optionally, add a typecheck (useful for opcodes) too:

static struct {
	test_predicate fct;
	const char *kind;
} tests[] = {
	{nullptr, nullptr},
	{is_any, nullptr},
	/* .. */
	{is_procedure, "procedure"},
	{is_memblock, "memory block"},
};

#define TST_NONE nullptr
#define TST_ANY "\001"
/* .. */
#define TST_PROCEDURE "\021"
#define TST_MEMBLOCK "\022"

Make sure that the string value is consistent with the tests structure length. But yes, you can now use TST_MEMBLOCK in opcode type checks.

Adding Embedded Interface Entries

You likely want to make this data structure 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 pointer
mk_memblock(scheme *sc, size_t len, char fill)
{
	char *p = (char *) sc->malloc(len);
	if (p is nullptr)
		return sc->NIL;
	pointer 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;
}
INTERFACE bool is_memblock(pointer p) { /* defined above */ }
INTERFACE char
memblock_elem(pointer p, size_t index)
{
	return memblockdata(x)[index];
}
INTERFACE void
set_memblock_elem(pointer p, size_t index, char value)
{
	memblockdata(x)[index] = value;
}
INTERFACE size_t
memblock_length(pointer p)
{
	return memblocklength(p);
}

And then add the respective entries to interface.h:

_INTERFACE(pointer, mk_memblock,       mk_memblock,        scheme *sc, size_t len, char fill)
_INTERFACE(bool,    is_memblock,       is_memblock,        pointer p)
_INTERFACE(size_t,  memblock_length,   memblock_length,    pointer p)
_INTERFACE(char,    memblock_elem,     memblock_elem,      pointer p, size_t index)
_INTERFACE(void,    set_memblock_elem, set_memblock_elem,  pointer p, size_t index, char value)

The fields are:

1: return type
Return type of the “exported” function, stripped from all the static et 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
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->vtbl!

Adding a New Opcode

There are two main places to edit for new opcode:

First, scheme-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 nullptr in case the opcode is internal or otherwise unnamed.
2: minimal arity
Mandatory integer determining how many required arguments there are.
3: maximal 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 list of TST_ type checks for arguments. Strictly one check per argument. In case the procedure is variadic, the variadic argument should be typed too, with 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,       TST_NATURAL TST_NATURAL,              OP_MKBLOCK)
// Note the use of the TST_MEMBLOCK type check
_OP_DEF("block-length",                   1,  1,       TST_MEMBLOCK,                         OP_BLOCKLEN)
_OP_DEF("block-ref",                      2,  2,       TST_MEMBLOCK TST_NATURAL,             OP_BLOCKREF)
_OP_DEF("block-set!",                     3,  3,       TST_MEMBLOCK TST_NATURAL TST_NATURAL, OP_BLOCKSET)
_OP_DEF("block?",                         1,  1,       TST_ANY,                              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", scheme *sc)
{
	some;
	statements;
	s_return(sc, ...); // or s_return_values(sc, values_cons);
}
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", scheme *sc)
// {
//	s_return(sc, return_value);
// }

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", scheme *sc)
{
	int fill = 0;

	if (not is_number(car(sc->args)))
		error_named_1(sc, "not a number:", car(sc->args));
	size_t len = ivalue(car(sc->args));
	if (len <= 0)
		error_named_1(sc, "not positive:", car(sc->args));
	if (cdr(sc->args) != sc->NIL) {
		if (not is_number(cadr(sc->args)) or ivalue(cadr(sc->args)) < 0)
			error_named_1(sc, "not a positive number:",
				      cadr(sc->args));
		fill = charvalue(cadr(sc->args)) % 255;
	}
	s_return(sc, mk_memblock(sc, len, (char)fill));
}

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?", ((is_memblock(car(sc->args))) ? sc->T : sc->F))
// *INDENT-ON*

Now the gory part: element references

DEFHANDLER(OP_BLOCKREF, "block-ref", 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]));
}
DEFHANDLER(OP_BLOCKSET, "block-set!", 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));
}

Note that, compared to the old tutorial, all the type checks are gone. They are performed by the interpreter based on opcode definitions.

But, with that, we have a new data structure with a proper memory layout. And with opcodes to work with it.

Useful Helpers for Opcode Writing

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_values gets an sc, first / primary value, and then a consed list of other values.
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_save(sc, op, args, code)
This one is particularly useful when s_goto is used. It’s a way to chain opcodes so that the “saved” one is executed after the “goto-ed” one. 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 scheme that are used a lot in opcodes.
  • sc->args is the cons list of arguments passed to the opcode.
  • sc->code is 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->value and sc->other_values are 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_named_0, error_named_1
Use these as much as possible—opcodes should be as strict as possible, like all of Teeny Scheme. Use them over the error_0/1 in particular: they append the opcode name to the error message, which is nice.
error_0, error_1
Sometimes useful, but I’m not sure. Just use named versions when you’re writing opcodes, and only use these when outside opcodes?

As an example of s_save 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(nullptr,            0, 0, nullptr,                     OP_CALLWV1)

And then delegates the work to OP_APPLY when applying value-producing thunk:

DEFHANDLER(OP_CALLWV0, "call-with-values (entry)", scheme *sc)
{
	s_save(sc, OP_CALLWV1, sc->NIL, cadr(sc->args));
	sc->code = car(sc->args);
	sc->args = sc->NIL;
	s_goto(sc, OP_APPLY);
}
DEFHANDLER(OP_CALLWV1, "call-with-values (apply the second procedure)",
	   scheme *sc)
{
	sc->args = cons(sc, sc->value, sc->other_values);
	s_goto(sc, OP_APPLY);
}

Adding a New Base Syntax Form

TODO

Adding an Interpreter Extension

TODO

Adding an Extension Library in C

TODO