Examples
Whole programs that use staticjs, each with what it prints. The repository's tests build every one exactly as Getting started 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 | the smallest whole program: a context, an expression compiled, evaluated on fuel, its value written | ./hello |
| 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 | 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 | 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/ | 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:
clang -std=c11 -O2 -I. hello.c staticjs.c -o helloWith a prebuilt library, with any C11 compiler:
cc -std=c11 -Iinclude hello.c lib/libstaticjs.a -o helloIn this repository, mise run examples builds them all into build/examples/ and checks their output. Browser and Node 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.
/* 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:
{"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.
/* 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:
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 pieceseval.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:
[1,2,3]
{"total":6}
0.853438561751189The source of eval.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:
{"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
/* 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(¶ms, &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) }, ¤cy), "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"), ¢s), "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;
}