# Examples

Whole programs that use staticjs, each with what it prints. The
repository's tests build every one exactly as
[Getting started](./getting-started.md) says, from `staticjs.c` and
`staticjs.h` alone, and check what it prints, so they stay correct as the
library changes.

| Example | Shows | Run |
|---|---|---|
| [hello.c](https://github.com/dhamidi/tinyactors/blob/main/staticjs/examples/hello.c) | the smallest whole program: a context, an expression compiled, evaluated on fuel, its value written | `./hello` |
| [eval.c](https://github.com/dhamidi/tinyactors/blob/main/staticjs/examples/eval.c) | a command-line evaluator: expressions and modules, error reports with positions, a fuel budget, the clock from the host, `Math.random`'s seed | `./eval '[3, 1, 2].toSorted((a, b) => a - b)'` |
| [host.c](https://github.com/dhamidi/tinyactors/blob/main/staticjs/examples/host.c) | a host that runs a plugin its users wrote: a library module, the plugin compiled once and evaluated per record, globals built through the API, a native `rate()`, a module imported by name, the clock pushed by the host | `./host` |
| [stream.c](https://github.com/dhamidi/tinyactors/blob/main/staticjs/examples/stream.c) | every stage pushed: source in 7-byte chunks, a sink of its own before the compiler, evaluation in slices of fuel, output through a 16-byte buffer | `./stream` |
| [wasm/](https://github.com/dhamidi/tinyactors/tree/main/staticjs/examples/wasm) | `staticjs.wasm` from JavaScript: a wrapper (`staticjs.mjs`) with contexts, globals, results as values, JSON or source, and slices of fuel; a page (`index.html`) that evaluates in slices; and a Node script (`node.mjs`) | `node wasm/node.mjs staticjs.wasm staticjs.api.json` |

## Building them

From a source release, with clang:

```sh
clang -std=c11 -O2 -I. hello.c staticjs.c -o hello
```

With a prebuilt library, with any C11 compiler:

```sh
cc -std=c11 -Iinclude hello.c lib/libstaticjs.a -o hello
```

In this repository, `mise run examples` builds them all into
`build/examples/` and checks their output. [Browser and Node](./wasm.md)
says how to run the wasm example.

## hello.c

The smallest whole program: a context, an expression compiled, evaluated on
fuel, and its value written as text.

```c
/* hello: the smallest whole staticjs program. It compiles one expression,
 * evaluates it on a budget of fuel, and prints the value as JavaScript
 * source.
 *
 *   clang -std=c11 -O2 -I. hello.c staticjs.c -o hello   (from a source tarball)
 *   ./hello
 *   {"total":55,"names":["ada","grace"]}
 */
#include "staticjs.h"

#include <stdio.h>
#include <stdlib.h>

/* staticjs allocates only through the embedder. It asks for 16-byte
 * alignment, which malloc gives on 64-bit hosts; aligned_alloc covers any
 * larger alignment. */
static void *allocate(void *self, size_t size, size_t alignment) {
    (void)self;
    if (alignment <= _Alignof(max_align_t)) return malloc(size);
    return aligned_alloc(alignment, (size + alignment - 1) / alignment * alignment);
}

static void release(void *self, const sjs_allocator_free_params *params) {
    (void)self;
    free(params->ptr);
}

static const sjs_allocator_vtable heap = { allocate, release };

static const char SOURCE[] =
    "({ total: [1, 2, 3, 4, 5].map((n) => n * n).reduce((sum, n) => sum + n, 0),"
    "   names: [\"grace\", \"ada\"].toSorted() })";

int main(void) {
    sjs_context *context;
    sjs_context_config config = { .allocator = { NULL, &heap }, .limits = SJS_LIMITS_DEFAULT };
    if (sjs_context_init(&context, &config) != SJS_OK) return 1;

    /* Compile: a source held in memory, as one expression. */
    sjs_program *program;
    sjs_error error = { 0 };
    sjs_compile_params compile = {
        .compiler = { .context = context, .goal = SJS_GOAL_EXPRESSION },
        .source = SJS_STR(SOURCE),
        .error = &error,
    };
    if (sjs_compile(&compile, &program) != SJS_OK) {
        fprintf(stderr, "%u:%u: %.*s\n", error.at.line, error.at.column, (int)error.message.length,
                error.message.ptr);
        return 1;
    }

    /* Evaluate: fuel is pushed, and the evaluation stops where it runs out. */
    sjs_vm *vm;
    if (sjs_vm_init(&vm, &(sjs_vm_config){ .context = context }) != SJS_OK) return 1;
    if (sjs_vm_start(vm, &(sjs_run_params){ .program = program }) != SJS_OK) return 1;
    sjs_run status = sjs_vm_run(vm, 100000);
    if (status != SJS_RUN_DONE) {
        if (status == SJS_RUN_FAULT) sjs_vm_error(vm, &error);
        fprintf(stderr, "%s: %.*s\n", status == SJS_RUN_PAUSED ? "out of fuel" : "fault",
                (int)error.message.length, error.message.ptr);
        return 1;
    }
    sjs_value result;
    sjs_vm_take_result(vm, &result);

    /* Write the value as text, into a buffer of any size. */
    sjs_writer *writer;
    if (sjs_writer_init(&writer, &(sjs_writer_config){ .context = context, .value = result }) != SJS_OK) return 1;
    uint8_t bytes[64];
    sjs_result written;
    do {
        sjs_buffer buffer = { bytes, sizeof bytes, 0 };
        written = sjs_writer_write(writer, &buffer);
        fwrite(bytes, 1, buffer.length, stdout);
    } while (written == SJS_NEED_SPACE);
    putchar('\n');

    /* Everything made with the context goes before the context does. */
    sjs_writer_destroy(writer);
    sjs_release(context, result);
    sjs_vm_destroy(vm);
    sjs_program_release(program);
    sjs_context_destroy(context);
    return written == SJS_OK ? 0 : 1;
}
```

It prints:

```text
{"total":55,"names":["ada","grace"]}
```

## stream.c

Every stage pushed by the host: the source in 7-byte chunks, a sink of its
own in front of the compiler, evaluation in slices of fuel, and output
through a 16-byte buffer.

```c
/* stream: every stage pushed by its caller, as a host with an event loop
 * drives them.
 *
 * - the source arrives in chunks (from a socket, a file) and is pushed to
 *   the parser as it comes; a token may span two chunks;
 * - a sink of the host's own sees the parser's events before the compiler
 *   does (a linter, a formatter, a metric could be another);
 * - the evaluation runs on small slices of fuel, so the host can do other
 *   work, or give up, between them;
 * - the result is written through a buffer smaller than it.
 *
 *   ./stream
 */
#include "staticjs.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void *allocate(void *self, size_t size, size_t alignment) {
    (void)self;
    if (alignment <= _Alignof(max_align_t)) return malloc(size);
    return aligned_alloc(alignment, (size + alignment - 1) / alignment * alignment);
}

static void release(void *self, const sjs_allocator_free_params *params) {
    (void)self;
    free(params->ptr);
}

static const sjs_allocator_vtable heap = { allocate, release };

/* A sink in front of the compiler: it counts the events and the functions
 * the program declares, then passes each event on. */
typedef struct {
    sjs_sink next;
    uint32_t events;
    uint32_t functions;
} counting_sink;

static sjs_result count_event(void *self, const sjs_event *event) {
    counting_sink *sink = self;
    sink->events++;
    if (event->kind == SJS_EV_ARROW_BEGIN) sink->functions++;
    return sink->next.v->event(sink->next.self, event);
}

static const sjs_sink_vtable counting_vtable = { count_event };

static const char SOURCE[] =
    "const range = (n) => Array.from({ length: n }, (_, i) => i);\n"
    "const primes = (n) => range(n).filter((k) => k > 1 &&\n"
    "  range(k).every((d) => d < 2 || k % d !== 0));\n"
    "export const first = primes(60);\n"
    "export const sum = first.reduce((a, b) => a + b, 0);\n";

static void report(const char *stage, const sjs_error *error) {
    fprintf(stderr, "%s: %u:%u: %.*s\n", stage, error->at.line, error->at.column, (int)error->message.length,
            error->message.ptr);
}

int main(void) {
    sjs_allocator allocator = { NULL, &heap };
    sjs_context *context;
    if (sjs_context_init(&context, &(sjs_context_config){ allocator, SJS_LIMITS_DEFAULT }) != SJS_OK) return 1;

    /* Bytes in, events out: the parser feeds our sink, which feeds the
     * compiler. */
    sjs_compiler *compiler;
    if (sjs_compiler_init(&compiler, &(sjs_compiler_config){ .context = context, .goal = SJS_GOAL_MODULE }) != SJS_OK)
        return 1;
    counting_sink sink = { sjs_compiler_sink(compiler), 0, 0 };
    sjs_parser *parser;
    sjs_parser_config parser_config = { allocator, SJS_LIMITS_DEFAULT, SJS_GOAL_MODULE, { &sink, &counting_vtable } };
    if (sjs_parser_init(&parser, &parser_config) != SJS_OK) return 1;

    const size_t chunk = 7; /* as small as a slow network delivers */
    sjs_result parsed = SJS_OK;
    for (size_t at = 0; parsed == SJS_OK && at < sizeof SOURCE - 1; at += chunk) {
        size_t length = sizeof SOURCE - 1 - at < chunk ? sizeof SOURCE - 1 - at : chunk;
        parsed = sjs_parser_push(parser, (sjs_bytes){ (const uint8_t *)SOURCE + at, (uint32_t)length });
    }
    if (parsed == SJS_OK) parsed = sjs_parser_finish(parser);
    sjs_error error;
    if (parsed != SJS_OK) {
        /* The parser's error, or the compiler's when our sink passed on a
         * refusal from it. */
        if (sjs_compiler_error(compiler, &error) == SJS_OK) report("compile", &error);
        else if (sjs_parser_error(parser, &error) == SJS_OK) report("parse", &error);
        return 1;
    }
    sjs_parser_destroy(parser);
    sjs_program *program;
    if (sjs_compiler_finish(compiler, &program) != SJS_OK) return 1;
    sjs_compiler_destroy(compiler);
    printf("compiled from %zu-byte chunks: %u events, %u functions\n", chunk, sink.events, sink.functions);

    /* Fuel in, a value out: 500 units at a time. */
    sjs_vm *vm;
    if (sjs_vm_init(&vm, &(sjs_vm_config){ .context = context }) != SJS_OK) return 1;
    if (sjs_vm_start(vm, &(sjs_run_params){ .program = program }) != SJS_OK) return 1;
    unsigned slices = 1;
    sjs_run status;
    while ((status = sjs_vm_run(vm, 500)) == SJS_RUN_PAUSED) {
        slices++; /* the host's other work goes here; sjs_vm_reset() gives up */
    }
    if (status == SJS_RUN_FAULT) {
        sjs_vm_error(vm, &error);
        report("fault", &error);
        return 1;
    }
    printf("evaluated in %u slices, %llu units of fuel\n", slices, (unsigned long long)sjs_vm_fuel_used(vm));
    sjs_value result;
    sjs_vm_take_result(vm, &result);

    /* A value out as text, through a buffer of 16 bytes. */
    sjs_writer *writer;
    if (sjs_writer_init(&writer, &(sjs_writer_config){ .context = context, .value = result }) != SJS_OK) return 1;
    uint8_t bytes[16];
    unsigned writes = 0;
    sjs_result written;
    do {
        sjs_buffer buffer = { bytes, sizeof bytes, 0 };
        written = sjs_writer_write(writer, &buffer);
        fwrite(bytes, 1, buffer.length, stdout);
        writes++;
    } while (written == SJS_NEED_SPACE);
    printf("\nwritten in %u pieces\n", writes);

    sjs_writer_destroy(writer);
    sjs_release(context, result);
    sjs_vm_destroy(vm);
    sjs_program_release(program);
    sjs_context_destroy(context);
    return written == SJS_OK ? 0 : 1;
}
```

It prints:

```text
compiled from 7-byte chunks: 109 events, 6 functions
evaluated in 29 slices, 14530 units of fuel
{"first":[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59],"sum":440}
written in 5 pieces
```

## eval.c

A command-line evaluator, with expressions and modules, error reports with
positions, a fuel budget, the host's clock and `Math.random`'s seed. It
prints:

```text
[1,2,3]
{"total":6}
0.853438561751189
```

The source of eval.c:
```c
/* eval: evaluate a staticjs expression or module and print its value.
 *
 *   eval [--module] [--fuel N] [--seed N] [--stats] [SOURCE]
 *
 * SOURCE is the program's text; without it, eval reads standard input. A
 * module's value is its namespace, the object of its exports. The clock that
 * Temporal.Now, Date.now() and new Date() read is set from the host's clock,
 * and Math.random draws from --seed (as `node --random-seed=N` does). Exit
 * status: 0 for a value, 1 when the program is refused or faults, 2 for a
 * usage or I/O error. --stats prints the fuel used and the peak of memory the
 * context held to standard error.
 *
 *   ./eval '[3, 1, 2].toSorted((a, b) => a - b)'
 *   [1,2,3]
 *   ./eval '({ a: 1 }).b.c'
 *   fault at 1:14: cannot read a property of undefined (c)
 */
#include "staticjs.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* The allocator: the C library's heap, with the bytes the context holds and
 * their peak counted for --stats. */
typedef struct {
    size_t live;
    size_t peak;
} heap_usage;

static void *allocate(void *self, size_t size, size_t alignment) {
    heap_usage *usage = self;
    void *ptr = alignment <= _Alignof(max_align_t)
                    ? malloc(size)
                    : aligned_alloc(alignment, (size + alignment - 1) / alignment * alignment);
    if (ptr == NULL) return NULL;
    usage->live += size;
    if (usage->live > usage->peak) usage->peak = usage->live;
    return ptr;
}

static void release(void *self, const sjs_allocator_free_params *params) {
    heap_usage *usage = self;
    usage->live -= params->size;
    free(params->ptr);
}

static const sjs_allocator_vtable heap = { allocate, release };

typedef struct {
    sjs_goal goal;
    uint64_t fuel;
    int64_t seed;
    bool stats;
    const char *source;
} options;

static const char *const KINDS[] = { "", "syntax error", "unsupported", "name error", "limit", "out of memory", "fault" };

/* "fault at 1:14: cannot read a property of undefined (c)". The subject is
 * read only while the stage that reported the error lives: the evaluator
 * here. sjs_compile has destroyed its parser and compiler when it returns,
 * so a compile error is reported without one. */
static void report(const sjs_error *error, bool with_subject) {
    fprintf(stderr, "%s at %u:%u: %.*s", KINDS[error->code], error->at.line, error->at.column,
            (int)error->message.length, error->message.ptr);
    if (with_subject && error->subject.length > 0) {
        fprintf(stderr, " (%.*s)", (int)error->subject.length, error->subject.ptr);
    }
    fputc('\n', stderr);
}

static char *read_all(FILE *in, size_t *length) {
    size_t capacity = 4096;
    char *text = malloc(capacity);
    *length = 0;
    while (text != NULL) {
        *length += fread(text + *length, 1, capacity - *length, in);
        if (*length < capacity) return ferror(in) ? (free(text), NULL) : text;
        char *grown = realloc(text, capacity *= 2);
        if (grown == NULL) free(text);
        text = grown;
    }
    return NULL;
}

static bool parse_options(int argc, char **argv, options *out) {
    *out = (options){ SJS_GOAL_EXPRESSION, 10u * 1000u * 1000u, 1, false, NULL };
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--module") == 0) out->goal = SJS_GOAL_MODULE;
        else if (strcmp(argv[i], "--stats") == 0) out->stats = true;
        else if (strcmp(argv[i], "--fuel") == 0 && i + 1 < argc) out->fuel = strtoull(argv[++i], NULL, 10);
        else if (strcmp(argv[i], "--seed") == 0 && i + 1 < argc) out->seed = strtoll(argv[++i], NULL, 10);
        else if (argv[i][0] != '-' && out->source == NULL) out->source = argv[i];
        else return false;
    }
    return true;
}

/* Run a compiled program to its end: its value written to standard output,
 * or the fault reported. */
static int run(sjs_context *context, sjs_program *program, const options *o) {
    sjs_vm *vm;
    if (sjs_vm_init(&vm, &(sjs_vm_config){ .context = context }) != SJS_OK) return 2;
    struct timespec now;
    timespec_get(&now, TIME_UTC);
    sjs_vm_set_clock(vm, &(sjs_epoch){ now.tv_sec, (uint32_t)now.tv_nsec });
    sjs_vm_set_seed(vm, o->seed);
    sjs_vm_start(vm, &(sjs_run_params){ .program = program });
    sjs_run status = sjs_vm_run(vm, o->fuel);
    if (o->stats) fprintf(stderr, "fuel: %llu\n", (unsigned long long)sjs_vm_fuel_used(vm));
    int exit_status = 1;
    if (status == SJS_RUN_PAUSED) {
        fprintf(stderr, "out of fuel after %llu units\n", (unsigned long long)o->fuel);
    } else if (status == SJS_RUN_FAULT) {
        sjs_error error;
        sjs_vm_error(vm, &error);
        report(&error, true);
    } else {
        sjs_value value;
        sjs_vm_take_result(vm, &value);
        sjs_writer *writer;
        sjs_writer_config config = { context, value, SJS_STR("<function>") };
        if (sjs_writer_init(&writer, &config) == SJS_OK) {
            uint8_t bytes[4096];
            sjs_result written;
            do {
                sjs_buffer buffer = { bytes, sizeof bytes, 0 };
                written = sjs_writer_write(writer, &buffer);
                fwrite(bytes, 1, buffer.length, stdout);
            } while (written == SJS_NEED_SPACE);
            putchar('\n');
            sjs_writer_destroy(writer);
            exit_status = written == SJS_OK ? 0 : 2;
        }
        sjs_release(context, value);
    }
    sjs_vm_destroy(vm);
    return exit_status;
}

int main(int argc, char **argv) {
    options o;
    if (!parse_options(argc, argv, &o)) {
        fprintf(stderr, "usage: eval [--module] [--fuel N] [--seed N] [--stats] [SOURCE]\n");
        return 2;
    }
    size_t length = o.source == NULL ? 0 : strlen(o.source);
    char *input = o.source == NULL ? read_all(stdin, &length) : NULL;
    if (o.source == NULL && input == NULL) return 2;

    heap_usage usage = { 0, 0 };
    sjs_context *context;
    sjs_context_config config = { { &usage, &heap }, SJS_LIMITS_DEFAULT };
    if (sjs_context_init(&context, &config) != SJS_OK) return 2;
    sjs_program *program;
    sjs_error error = { 0 };
    sjs_compile_params compile = {
        .compiler = { .context = context, .goal = o.goal },
        .source = { input != NULL ? input : o.source, (uint32_t)length },
        .error = &error,
    };
    sjs_result compiled = sjs_compile(&compile, &program);
    int exit_status = 2;
    if (compiled == SJS_OK) {
        exit_status = run(context, program, &o);
        sjs_program_release(program);
    } else if (compiled == SJS_ERR_PROGRAM) {
        report(&error, false);
        exit_status = 1;
    }
    sjs_context_destroy(context);
    if (o.stats) fprintf(stderr, "peak memory: %zu bytes\n", usage.peak);
    free(input);
    return exit_status;
}
```

## host.c

A host that runs a plugin its users wrote to convert order records to
euros: the plugin compiled once and evaluated per record, globals built
through the API, a native function `rate()` that answers from the host's
table, and a module imported by name. It prints:

```text
{"id":"a1","eur":11.04}
{"id":"a2","eur":93.6}
{"id":"a3","eur":5.5}
fault: a native function failed (rate() takes a currency the host has a rate for)
{"sizes":["small","medium","large"]}
"2024-01-31T10:00:00"
"2024-01-31T11:30:00"
```

The source of host.c:
```c
/* host: a host that runs a plugin its users wrote to transform records.
 *
 * - a library module, compiled and evaluated once; its exports are a value;
 * - the plugin, an expression compiled once with the names the host binds
 *   (globals), evaluated for each record with that record's values;
 * - a native function, rate(), answering from the host's own tables;
 * - a second module importing the first by name, through a modules adapter;
 * - the clock, pushed by the host: the library never reads one.
 *
 *   ./host
 */
#include "staticjs.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void *allocate(void *self, size_t size, size_t alignment) {
    (void)self;
    if (alignment <= _Alignof(max_align_t)) return malloc(size);
    return aligned_alloc(alignment, (size + alignment - 1) / alignment * alignment);
}

static void release(void *self, const sjs_allocator_free_params *params) {
    (void)self;
    free(params->ptr);
}

static const sjs_allocator_vtable heap = { allocate, release };

/* What every step needs: the context, one evaluator kept for every run, and
 * the library module's exports. */
typedef struct {
    sjs_context *context;
    sjs_vm *vm;
    sjs_value money;
} host;

static void fail(const char *what, const sjs_error *error) {
    fprintf(stderr, "%s: %u:%u: %.*s\n", what, error->at.line, error->at.column, (int)error->message.length,
            error->message.ptr);
    exit(1);
}

static void check(sjs_result result, const char *what) {
    if (result != SJS_OK) {
        fprintf(stderr, "%s: %d\n", what, (int)result);
        exit(1);
    }
}

static void print(host *h, sjs_value value) {
    sjs_writer *writer;
    check(sjs_writer_init(&writer, &(sjs_writer_config){ .context = h->context, .value = value }), "writer");
    char text[256];
    sjs_buffer buffer = { (uint8_t *)text, sizeof text, 0 };
    check(sjs_writer_write(writer, &buffer), "write");
    printf("%.*s\n", (int)buffer.length, text);
    sjs_writer_destroy(writer);
}

static sjs_program *compile(const sjs_compiler_config *config, const char *source) {
    sjs_program *program;
    sjs_error error = { 0 };
    sjs_compile_params params = { *config, { source, (uint32_t)strlen(source) }, &error };
    if (sjs_compile(&params, &program) != SJS_OK) fail("compile", &error);
    return program;
}

/* Evaluate a program with values for its globals; the caller owns the result. */
static sjs_result evaluate(host *h, const sjs_run_params *run, sjs_value *out) {
    check(sjs_vm_start(h->vm, run), "start");
    sjs_run status = sjs_vm_run(h->vm, 100000);
    if (status == SJS_RUN_DONE) return sjs_vm_take_result(h->vm, out);
    sjs_error error = { 0 };
    if (status == SJS_RUN_FAULT) sjs_vm_error(h->vm, &error);
    else sjs_vm_reset(h->vm); /* out of fuel: abandon the evaluation */
    printf("fault: %.*s", (int)error.message.length, error.message.ptr);
    if (error.subject.length > 0) printf(" (%.*s)", (int)error.subject.length, error.subject.ptr);
    putchar('\n');
    return SJS_ERR_PROGRAM;
}

/* The host's exchange rates to euros: the table rate() answers from. */
typedef struct {
    const char *currency;
    double euros;
} rate_row;

static const rate_row rates[] = { { "EUR", 1 }, { "GBP", 1.17 }, { "USD", 0.92 } };

/* rate(currency): one unit of a currency, in euros. The table is the
 * native's `self`; a native must not change anything a program can see. */
static sjs_result rate(void *self, sjs_native_call *call) {
    const rate_row *row = self;
    if (call->count == 1 && call->arguments[0].kind == SJS_STRING) {
        sjs_str code = sjs_string_text(call->arguments[0]);
        for (size_t i = 0; i < sizeof rates / sizeof rates[0]; i++) {
            if (strlen(row[i].currency) == code.length && memcmp(row[i].currency, code.ptr, code.length) == 0) {
                call->result = sjs_number(row[i].euros);
                return SJS_OK;
            }
        }
    }
    call->fault = SJS_STR("rate() takes a currency the host has a rate for");
    return SJS_ERR_PROGRAM;
}

static const sjs_native_vtable rate_vtable = { rate };

/* The modules adapter: one module, "money", whose exports the host holds. */
static sjs_result find_module(void *self, sjs_str name, sjs_value *exports) {
    host *h = self;
    if (name.length != 5 || memcmp(name.ptr, "money", 5) != 0) return SJS_ERR_NOT_FOUND;
    *exports = h->money; /* borrowed: the program retains what it imports */
    return SJS_OK;
}

static const sjs_modules_vtable modules_vtable = { find_module };

/* A record as the host builds it: { id, amount, currency }. */
typedef struct {
    const char *id;
    double amount;
    const char *currency;
} order;

static sjs_value record(host *h, const order *o) {
    sjs_value r, id, currency;
    check(sjs_object_new(h->context, 3, &r), "record");
    check(sjs_string_new(h->context, (sjs_str){ o->id, (uint32_t)strlen(o->id) }, &id), "id");
    check(sjs_string_new(h->context, (sjs_str){ o->currency, (uint32_t)strlen(o->currency) }, &currency), "currency");
    check(sjs_object_put(h->context, &r, &(sjs_property){ SJS_STR("id"), id }), "put id");
    check(sjs_object_put(h->context, &r, &(sjs_property){ SJS_STR("amount"), sjs_number(o->amount) }), "put amount");
    check(sjs_object_put(h->context, &r, &(sjs_property){ SJS_STR("currency"), currency }), "put currency");
    return r;
}

int main(void) {
    host h = { 0 };
    check(sjs_context_init(&h.context, &(sjs_context_config){ { NULL, &heap }, SJS_LIMITS_DEFAULT }), "context");
    check(sjs_vm_init(&h.vm, &(sjs_vm_config){ .context = h.context }), "evaluator");

    /* 1. A library module: compiled and evaluated once. Its value is its
     * namespace, an object of its exports. */
    sjs_program *money = compile(&(sjs_compiler_config){ .context = h.context, .goal = SJS_GOAL_MODULE },
                                 "export const cents = (x) => Math.round(x * 100) / 100;\n"
                                 "export const size = (eur) => eur >= 100 ? \"large\" : eur >= 20 ? \"medium\" : \"small\";\n");
    if (evaluate(&h, &(sjs_run_params){ .program = money }, &h.money) != SJS_OK) return 1;
    sjs_program_release(money);

    /* 2. The plugin: an expression a user wrote, compiled once against the
     * names the host binds, then evaluated for each record. */
    static const sjs_str names[] = { SJS_STR("rate"), SJS_STR("cents"), SJS_STR("record") };
    sjs_program *plugin = compile(
                                  &(sjs_compiler_config){ .context = h.context, .goal = SJS_GOAL_EXPRESSION,
                                                          .globals = names, .global_count = 3 },
                                  "({ id: record.id, eur: cents(record.amount * rate(record.currency)) })");
    sjs_value rate_fn, cents;
    check(sjs_function_native(h.context, (sjs_native){ (void *)rates, &rate_vtable }, &rate_fn), "native");
    check(sjs_object_get(h.money, SJS_STR("cents"), &cents), "cents"); /* borrowed from the exports */
    static const order orders[] = { { "a1", 12, "USD" }, { "a2", 80, "GBP" }, { "a3", 5.5, "EUR" } };
    for (int i = 0; i < 3; i++) {
        sjs_value values[] = { rate_fn, cents, record(&h, &orders[i]) };
        sjs_value result;
        if (evaluate(&h, &(sjs_run_params){ plugin, values, 3 }, &result) == SJS_OK) {
            print(&h, result);
            sjs_release(h.context, result);
        }
        sjs_release(h.context, values[2]);
    }
    sjs_program_release(plugin);

    /* A native that refuses its arguments faults the evaluation. */
    sjs_program *misuse = compile(
                                  &(sjs_compiler_config){ .context = h.context, .goal = SJS_GOAL_EXPRESSION,
                                                          .globals = names, .global_count = 1 },
                                  "rate(\"JPY\")");
    sjs_value result;
    if (evaluate(&h, &(sjs_run_params){ misuse, &rate_fn, 1 }, &result) == SJS_OK) sjs_release(h.context, result);
    sjs_program_release(misuse);
    sjs_release(h.context, rate_fn);

    /* 3. A module importing "money" by name, through the adapter. */
    sjs_program *report = compile(
                                  &(sjs_compiler_config){ .context = h.context, .goal = SJS_GOAL_MODULE,
                                                          .modules = { &h, &modules_vtable } },
                                  "import { cents, size } from \"money\";\n"
                                  "export const sizes = [5.004, 42, 250].map((eur) => size(cents(eur)));\n");
    if (evaluate(&h, &(sjs_run_params){ .program = report }, &result) == SJS_OK) {
        print(&h, result);
        sjs_release(h.context, result);
    }
    sjs_program_release(report);

    /* 4. The clock: set by the host, advanced between runs, never read by
     * the library itself. 2024-01-31T10:00:00Z, then 90 minutes later. */
    sjs_program *now = compile(&(sjs_compiler_config){ .context = h.context, .goal = SJS_GOAL_EXPRESSION },
                               "Temporal.Now.plainDateTimeISO().toString()");
    check(sjs_vm_set_clock(h.vm, &(sjs_epoch){ 1706695200, 0 }), "clock");
    for (int i = 0; i < 2; i++) {
        if (evaluate(&h, &(sjs_run_params){ .program = now }, &result) == SJS_OK) {
            print(&h, result);
            sjs_release(h.context, result);
        }
        check(sjs_vm_advance_clock(h.vm, 90ull * 60u * 1000000000u), "advance");
    }
    sjs_program_release(now);

    sjs_release(h.context, h.money);
    sjs_vm_destroy(h.vm);
    if (sjs_context_live_bytes(h.context) != 0) return fprintf(stderr, "leaked\n"), 1;
    sjs_context_destroy(h.context);
    return 0;
}
```
