Initial feature set complete.

This commit is contained in:
Scott Duensing 2026-07-03 20:00:29 -05:00
parent 3c39f48bde
commit b5cf00b014
49 changed files with 2155 additions and 83 deletions

219
API.md Normal file
View file

@ -0,0 +1,219 @@
# calog script API reference
Every function below is a **native** that calog registers into each engine, so a script in
any language can call it. This reference covers the natives the `calog` runner and its
libraries expose. (For the C embedding API -- `calogCreate`, `calogRegister`, `calogFnInvoke`,
etc. -- see the README and `src/calog.h`.)
## Conventions
- **Calling syntax per engine.** Most engines call a native by its bare name --
`cryptoUuid()`, `kvSet("k", 1)`. Two differ:
- **Wren**: `Calog.call("name", [args])`, e.g. `Calog.call("kvSet", ["k", 1])`.
- **Scheme (s7)**: s-expression form, `(kvSet "k" 1)`.
- **Output & exit** (provided by the `calog` runner): `calogPrint(...)` writes to stdout;
`calogExit([code])` tears everything down and exits. calog is event-driven, so a script
must call `calogExit` (or be interrupted) to end -- a finished top level does not exit.
- **Values.** Arguments and results marshal through one canonical type: nil, bool, integer,
real, string, list, and map (keyed record). Strings are **binary-safe** (may contain
embedded NULs) everywhere the underlying library allows it.
- **Handles** (db connections, sockets, ssh sessions) are opaque values owned by the context
that created them; do not share a live handle across contexts.
- **Callbacks.** Natives that take a *function* argument (`psSubscribe`, `timerAfter`,
`timerEvery`, `calogExport`) accept a first-class function value. All engines support this,
including my-basic (a top-level `def` or `lambda`; see `vendor/ourbasic`).
- **Availability.** Every library in this reference is compiled into `bin/calog`: crypto,
json, kv, fs, time, timer, export, pubsub, task, net (TCP / UDP / ENet), db (SQLite /
PostgreSQL / MySQL), http, and ssh. ssh needs a reachable server; http needs a reachable
endpoint.
---
## Runner (`calog` binary)
| Function | Description |
|---|---|
| `calogPrint(...)` | Write each argument to stdout, space-separated, with a trailing newline. |
| `calogExit([code])` | Tear down the runtime and exit the process with `code` (default `0`). |
## crypto
Binary-safe cryptographic primitives over OpenSSL.
| Function | Description |
|---|---|
| `cryptoHashSha256(data) -> hex` | SHA-256 as a 64-char lowercase hex digest. |
| `cryptoHashSha1(data) -> hex` | SHA-1 as a 40-char lowercase hex digest. |
| `cryptoHmacSha256(key, data) -> hex` | HMAC-SHA-256 as 64-char lowercase hex. |
| `cryptoRandomBytes(count) -> bytes` | `count` cryptographically-random bytes (binary string). |
| `cryptoBase64Encode(data) -> text` | Base64-encode. |
| `cryptoBase64Decode(text) -> data` | Base64-decode (trailing whitespace tolerated). |
| `cryptoHexEncode(data) -> text` | Lowercase hex encode. |
| `cryptoHexDecode(hexText) -> data` | Hex decode (case-insensitive). |
| `cryptoUuid() -> string` | A random RFC 4122 version-4 UUID string. |
## db
SQL over SQLite, PostgreSQL, and MySQL/MariaDB. Parameters are always **bound** (never
string-spliced), so queries are injection-safe. Values marshal as: NULL <-> nil, integers/
reals as-is, text and BLOBs as binary-safe strings.
| Function | Description |
|---|---|
| `dbOpen(driver, conn) -> handle` | Open a connection. `driver` is `"sqlite"`, `"postgres"`, or `"mysql"`. `conn` is a SQLite path or `":memory:"`, a libpq conninfo, or a MySQL `key=value ...` string. |
| `dbExec(handle, sql, ...params) -> rowsAffected` | Run a non-query statement with bound params; returns rows affected. |
| `dbQuery(handle, sql, ...params) -> rows` | Run a query; returns a list of `{column: value}` row maps. |
| `dbClose(handle)` | Close the connection. |
## export
Share a function by name across contexts and engines.
| Function | Description |
|---|---|
| `calogExport(name, fn)` | Publish function `fn` under a global `name`. |
| `calogUnexport(name)` | Remove an exported name. |
| `calogCall(name, ...args) -> result` | Call an exported function by name -- works in **every** engine. (On hook engines -- Lua/JS/Squirrel/s7 -- an export is also reachable by its bare name.) |
## fs
POSIX filesystem access. A failed operation raises a catchable script error carrying
`strerror(errno)`.
| Function | Description |
|---|---|
| `fsRead(path) -> string` | Read a whole file (binary-safe). |
| `fsWrite(path, data)` | Create/truncate and write `data`. |
| `fsAppend(path, data)` | Create if absent, append at the end. |
| `fsExists(path) -> bool` | Whether the path exists. |
| `fsRemove(path)` | Unlink a file. |
| `fsMkdir(path)` | Create one directory level (existing dir is OK). |
| `fsList(path) -> list` | Entry name strings, excluding `.` and `..`. |
| `fsStat(path) -> map` | `{size, isDir, isFile, mtime}`, or nil if the path is absent. |
## http
Minimal HTTP/1.1 client over `http://` and `https://`. Each call is its own connection
(`Connection: close`); redirects are not followed. `https://` verifies the server certificate
against the system CA store by default.
| Function | Description |
|---|---|
| `httpGet(url) -> map` | GET a URL. Returns `{status, body, headers}` (headers keyed by lowercased name). |
| `httpRequest(opts) -> map` | `opts` is `{method (default "GET"), url, headers (map), body, insecure (bool)}`. `insecure=true` skips TLS verification. Returns `{status, body, headers}`. |
## json
| Function | Description |
|---|---|
| `jsonParse(text) -> value` | Parse JSON: object -> map, array -> list, number -> int or real, string, true/false, null -> nil. |
| `jsonStringify(value) -> text` | Serialize a value to compact JSON text. |
## kv
A process-wide, thread-safe store shared by every context and engine. Holds **data only** (a
function value is rejected). Keys are binary-safe.
| Function | Description |
|---|---|
| `kvSet(key, value)` | Store a deep copy of `value` under `key` (replaces any existing). |
| `kvGet(key) -> value` | A deep copy of the stored value, or nil if absent. |
| `kvHas(key) -> bool` | Whether the key is present. |
| `kvDelete(key)` | Remove the key (no error if absent). |
| `kvKeys() -> list` | A list of the stored keys (strings). |
## net
Three first-class transports -- **TCP**, **UDP**, and **ENet** (reliable/ordered delivery
over UDP) -- all always available. Payloads are binary-safe strings. Blocking calls
(`tcpAccept`/`tcpRecv`/`udpRecvFrom`/`enetService`) stall only the calling context's thread.
TCP and UDP:
| Function | Description |
|---|---|
| `tcpConnect(host, port) -> handle` | Connect to a TCP server. |
| `tcpListen(port) -> handle` | Listen on a TCP port. |
| `tcpAccept(handle) -> handle` | Block for a client; returns a connection handle. |
| `tcpSend(handle, data) -> bytesSent` | Send all of `data`. |
| `tcpRecv(handle, maxBytes) -> data` | Read up to `maxBytes`; nil at end of stream. |
| `tcpClose(handle)` | Close a socket. |
| `udpOpen(port) -> handle` | Open a UDP socket (`port` 0 = ephemeral). |
| `udpSendTo(handle, host, port, data) -> bytesSent` | Send a datagram. |
| `udpRecvFrom(handle, maxBytes) -> map` | Receive one datagram: `{data, host, port}`. |
| `udpClose(handle)` | Close a UDP socket. |
ENet (reliable UDP -- ordered, reliable channels over UDP):
| Function | Description |
|---|---|
| `enetHost(port, maxPeers) -> hostHandle` | Create an ENet host. |
| `enetConnect(hostHandle, host, port, channels) -> peerHandle` | Initiate a connection to a peer. |
| `enetService(hostHandle, timeoutMs) -> event` | Poll for one event within `timeoutMs`. Returns `{type, ...}` where `type` is `"none"`, `"connect"`, `"receive"` (with `peer`, `channel`, `data`), or `"disconnect"`. |
| `enetSend(peerHandle, channel, data, reliable)` | Queue a packet on a channel; `reliable` is a bool. |
| `enetDisconnect(peerHandle)` | Begin disconnecting a peer. |
| `enetClose(hostHandle)` | Destroy an ENet host. |
## pubsub
Deliver a message to every subscriber of a topic, across contexts and engines. Delivery is
synchronous; each subscriber runs on its own context's thread and gets a deep copy of the
message. Keep publish graphs acyclic.
| Function | Description |
|---|---|
| `psSubscribe(topic, fn) -> id` | Register `fn` to receive messages published on `topic`. |
| `psUnsubscribe(id)` | Drop the subscription with that id. |
| `psPublish(topic, msg) -> count` | Deliver a copy of `msg` to every subscriber; returns how many were invoked. |
## ssh
SSH/SFTP over libssh2. Requires a reachable SSH server. Payloads are binary-safe.
| Function | Description |
|---|---|
| `sshConnect(host[, port]) -> handle` | Connect (port defaults to 22). |
| `sshAuthPassword(handle, user, password) -> bool` | Password authentication. |
| `sshAuthKey(handle, user, privateKeyPath[, publicKeyPath, passphrase]) -> bool` | Public-key authentication. |
| `sshExec(handle, command) -> map` | Run a remote command: `{stdout, stderr, exitCode}`. |
| `sshClose(handle)` | Close the session. |
| `sftpGet(handle, remotePath) -> data` | Read a remote file whole (binary-safe). |
| `sftpPut(handle, remotePath, data)` | Create/truncate a remote file (mode 0644). |
| `sftpList(handle, path) -> list` | `[{name, size, isDir}, ...]`. |
| `sftpStat(handle, path) -> map` | `{size, isDir}`, or nil if the path is missing. |
| `sftpRemove(handle, path)` | Remove a remote file. |
| `sftpMkdir(handle, path)` | Create a remote directory (mode 0755). |
## task
Launch and manage other calog script contexts. Tasks are fire-and-forget: a spawned context
runs on its own thread; results come back through host natives or the shared kv/pubsub. A
task is owned by the context that spawned it, and only that owner may `taskEval`/`taskClose`.
| Function | Description |
|---|---|
| `taskSpawn(engine, code) -> handle` | Run a code string on a named engine (`"lua"`, `"javascript"`, `"squirrel"`, `"mybasic"`, `"berry"`, `"scheme"`, `"wren"`). |
| `taskLoad(baseName) -> handle` | Launch a script *file* (engine chosen by extension). |
| `taskEval(handle, code)` | Feed more code into a running task (runs on its thread). |
| `taskClose(handle)` | Stop a task (cooperative: waits for its thread to exit). |
| `taskSelf() -> id` | The calling script's own context id. |
| `taskCount() -> n` | Number of tasks this library currently holds open. |
## time
| Function | Description |
|---|---|
| `timeNow() -> real` | Wall-clock epoch seconds, fractional (CLOCK_REALTIME). |
| `timeMonotonic() -> real` | Seconds from an unspecified origin (CLOCK_MONOTONIC); use for intervals. |
| `timeSleep(ms)` | Block the calling context for `ms` milliseconds. |
## timer
One background thread drives every timer; each callback runs on the context that created the
timer.
| Function | Description |
|---|---|
| `timerAfter(ms, fn) -> id` | Fire `fn` once, `ms` milliseconds from now. |
| `timerEvery(ms, fn) -> id` | Fire `fn` every `ms` milliseconds. |
| `timerCancel(id)` | Stop a pending or repeating timer. |

View file

@ -28,38 +28,79 @@ SOFTWARE.
## Third-party components ## Third-party components
calog vendors the following scripting engines under `vendor/`, built from calog vendors its dependencies under `vendor/`, each built from source. Every
source. Each is distributed under its own license (MIT unless noted), which dependency is distributed under its own license, and the full license text
applies to its files; the full text ships alongside each engine's source. ships alongside that dependency's source at the path shown below. All are
permissive (MIT / BSD / Apache-2.0 / PostgreSQL / public domain) **except**
MariaDB Connector/C, which is **LGPL-2.1** -- see the note under the database
libraries below.
- **Lua 5.4** — Copyright (C) 1994-2023 Lua.org, PUC-Rio. MIT License. ### Scripting engines
`vendor/lua/` (see `vendor/lua/README` and the notice in `src/lua.h`).
- **QuickJS-ng** (JavaScript) — Copyright (c) 2017-2024 Fabrice Bellard, Charlie - **Lua 5.4** -- MIT License. Copyright (C) 1994-2023 Lua.org, PUC-Rio.
Gordon, and QuickJS-ng contributors. MIT License. `vendor/quickjs/quickjs.h`. `vendor/lua/` (notice in `src/lua.h`).
- **Squirrel 3.2** — Copyright (c) 2003-2022 Alberto Demichelis. MIT License. - **QuickJS-ng** (JavaScript) -- MIT License. Copyright (c) 2017-2024 Fabrice
Bellard, Charlie Gordon, and QuickJS-ng contributors. `vendor/quickjs/quickjs.h`.
- **Squirrel 3.2** -- MIT License. Copyright (c) 2003-2022 Alberto Demichelis.
`vendor/squirrel-src/COPYRIGHT`. `vendor/squirrel-src/COPYRIGHT`.
- **MY-BASIC** — Copyright (C) 2011-2026 Tony Wang. MIT License. Notice in - **our-basic** -- calog's fork of MY-BASIC. MIT License. Copyright (C)
`vendor/mybasic/myBasic.h`. 2011-2026 Tony Wang. `vendor/ourbasic/` (license notice in `ourBasic.h`). The
fork's changes are catalogued in `vendor/ourbasic/NOTICE` and
`vendor/ourbasic/CHANGELOG`; the pristine upstream source is preserved as
`vendor/ourbasic/ourBasic.c.upstream`.
> Note: `vendor/mybasic/myBasic.c` carries one small calog patch (making the - **s7 Scheme** -- BSD Zero-Clause License (0BSD; `SPDX-License-Identifier: 0BSD`).
> global allocation counter `_Atomic` so interpreters are thread-safe); the Copyright (c) Bill Schottstaedt; derived from TinyScheme 1.39. `vendor/s7/s7.c`.
> unmodified original is preserved as `vendor/mybasic/myBasic.c.orig`.
- **s7 Scheme** — Copyright (c) Bill Schottstaedt and s7 contributors. BSD-style - **Wren 0.4.0** -- MIT License. Copyright (c) 2013-2021 Robert Nystrom and Wren
license (TinyScheme lineage: use for any purpose, no royalty). Notice in Contributors. Notice in `vendor/wren/wren.c` and `vendor/wren/wren.h`.
`vendor/s7/s7.c`.
- **Wren** — Copyright (c) 2013-2021 Robert Nystrom and Wren Contributors. MIT > Note: `vendor/wren/wren.c` and `wren.h` carry one small calog patch adding a
License. `vendor/wren/LICENSE` (amalgamated into `vendor/wren/wren.c`). > public C map-key iterator (`wrenGetMapCapacity` / `wrenGetMapEntry`), which
> upstream Wren lacks; it mirrors Wren's own internal `map_iterate`. The
> additions are bracketed by `--- calog patch ---` comments. Re-apply them if
> the amalgamation is regenerated from upstream (`util/generate_amalgamation.py`).
> Note: `vendor/wren/wren.c` and `wren.h` carry one small calog patch adding a public - **Berry** -- MIT License. Copyright (c) 2018-2022 Guan Wenliang and Berry
> C map-key iterator (`wrenGetMapCapacity` / `wrenGetMapEntry`), which upstream Wren contributors. Notice in `vendor/berry/src/berry.h`.
> lacks; it mirrors Wren's own internal `map_iterate`. The additions are bracketed by
> `--- calog patch ---` comments. Re-apply them if the amalgamation is regenerated from
> upstream (`util/generate_amalgamation.py`).
- **Berry** — Copyright (c) 2018-2026 Guan Wenliang and Berry contributors. MIT ### Database, network, and cryptography libraries
License. `vendor/berry/` (see the upstream `LICENSE`).
All of the following are linked into the default build and into `bin/calog`.
`bin/calog` ships with all three SQL drivers (`dbOpen("sqlite" | "postgres" |
"mysql", ...)`).
- **SQLite 3.53.3** -- Public Domain. The SQLite source is dedicated to the
public domain; in lieu of a license it carries a blessing ("May you do good
and not evil..."). `vendor/sqlite/sqlite3.c`.
- **PostgreSQL / libpq** -- The PostgreSQL License (a permissive, MIT-style
license). Portions Copyright (c) 1996-2026 PostgreSQL Global Development Group;
Portions Copyright (c) 1994 The Regents of the University of California.
`vendor/postgres/COPYRIGHT`.
- **MariaDB Connector/C** -- GNU Lesser General Public License, version 2.1
(LGPL-2.1). Copyright (c) MariaDB Corporation Ab and others.
`vendor/mariadb/COPYING.LIB`.
- **ENet** -- MIT License. Copyright (c) 2002-2024 Lee Salzman.
`vendor/enet/LICENSE`.
- **OpenSSL 3.5.7** -- Apache License 2.0. Copyright (c) The OpenSSL Project
Authors. `vendor/openssl/LICENSE.txt`.
- **libssh2 1.11.1** -- BSD-3-Clause License. Copyright (c) 2004-2023 Daniel
Stenberg, Sara Golemon, The Written Word, Inc., and other contributors (full
list in the file). `vendor/libssh2/COPYING`.
> **Note on the database backends and LGPL.** MariaDB Connector/C is the only
> copyleft dependency in the tree. It is **statically linked** into `bin/calog`,
> which carries the usual LGPL-2.1 obligations for a distributed binary (chiefly,
> providing the means to relink against a modified Connector/C). SQLite and
> PostgreSQL/libpq are permissive. A redistributor who wants an entirely
> permissive binary can build without the MySQL backend -- drop
> `-DCALOG_WITH_MYSQL` and the `$(MYSQLARCH)` archive from the `bin/calog` link in
> the Makefile -- and keep SQLite + PostgreSQL.

View file

@ -32,8 +32,8 @@ VPATH = src:src/lua:src/mybasic:src/squirrel:src/js:src/berry:src/s7:src/wre
CORE = obj/value.o obj/broker.o CORE = obj/value.o obj/broker.o
# --- vendored my-basic (C) --- # --- vendored our-basic: calog's fork of MY-BASIC (see vendor/ourbasic/NOTICE) ---
MBDIR = vendor/mybasic MBDIR = vendor/ourbasic
MBINC = -I$(MBDIR) MBINC = -I$(MBDIR)
MBFLAGS = -std=gnu11 -w -g -O1 -DMB_DOUBLE_FLOAT MBFLAGS = -std=gnu11 -w -g -O1 -DMB_DOUBLE_FLOAT
@ -136,7 +136,7 @@ ENETOBJ = $(foreach n,$(ENETNAMES),obj/enet_$(n).o)
BINS = bin/testBroker bin/testLua bin/testMyBasic bin/testPolyglot bin/testActor \ BINS = bin/testBroker bin/testLua bin/testMyBasic bin/testPolyglot bin/testActor \
bin/testEngineLua bin/testEngineMyBasic bin/testSquirrel bin/testEngineSquirrel bin/testJs bin/testEngineJs \ bin/testEngineLua bin/testEngineMyBasic bin/testSquirrel bin/testEngineSquirrel bin/testJs bin/testEngineJs \
bin/testEngineBerry bin/testEngineS7 bin/testEngineWren bin/testLoad bin/testDb bin/testNet bin/testTask bin/testExport bin/testJson bin/testTime bin/testFs bin/testCrypto bin/testKv bin/testTimer bin/testPubsub bin/testHttp bin/testHttps bin/embed bin/testEngineBerry bin/testEngineS7 bin/testEngineWren bin/testLoad bin/testDb bin/testNet bin/testTask bin/testExport bin/testJson bin/testTime bin/testFs bin/testCrypto bin/testKv bin/testTimer bin/testPubsub bin/testHttp bin/testHttps bin/embed bin/calog
all: $(BINS) all: $(BINS)
@ -215,7 +215,7 @@ $(DBADP): obj/%.o: %.c | obj
# always enabled -- unlike the DB clients, ENet needs no server) # always enabled -- unlike the DB clients, ENet needs no server)
NETADP = obj/calogNet.o NETADP = obj/calogNet.o
$(NETADP): obj/%.o: %.c | obj $(NETADP): obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) $(ENETINC) -DCALOG_WITH_ENET -pthread -c -o $@ $< $(CC) $(COREFLAGS) $(INC) $(ENETINC) -pthread -c -o $@ $<
# task library: strict C with every engine name compiled in (it references each engine # task library: strict C with every engine name compiled in (it references each engine
# vtable under its CALOG_WITH_* guard), so a task-using binary links all engine archives. # vtable under its CALOG_WITH_* guard), so a task-using binary links all engine archives.
@ -224,12 +224,17 @@ TASKADP = obj/calogTask.o
$(TASKADP): obj/%.o: %.c | obj $(TASKADP): obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) $(ENGINEDEFS) -pthread -c -o $@ $< $(CC) $(COREFLAGS) $(INC) $(ENGINEDEFS) -pthread -c -o $@ $<
# the calog CLI runner: strict C with every engine name compiled in, so
# calogRegisterBuiltinEngines registers all seven (it links every engine archive + library).
obj/calogMain.o: calogMain.c src/calog.h | obj
$(CC) $(COREFLAGS) $(INC) $(ENGINEDEFS) -pthread -c -o $@ $<
# polyglot test pulls in both Lua and my-basic # polyglot test pulls in both Lua and my-basic
obj/testPolyglot.o: testPolyglot.c | obj obj/testPolyglot.o: testPolyglot.c | obj
$(CC) $(ADPFLAGS) $(INC) $(LUAINC) $(MBINC) -DMB_DOUBLE_FLOAT -c -o $@ $< $(CC) $(ADPFLAGS) $(INC) $(LUAINC) $(MBINC) -DMB_DOUBLE_FLOAT -c -o $@ $<
# ---- vendored engine objects (also land in obj/) ---- # ---- vendored engine objects (also land in obj/) ----
obj/myBasic.o: $(MBDIR)/myBasic.c | obj obj/ourBasic.o: $(MBDIR)/ourBasic.c | obj
$(CC) $(MBFLAGS) -c -o $@ $< $(CC) $(MBFLAGS) -c -o $@ $<
obj/%.o: $(LUADIR)/src/%.c | obj obj/%.o: $(LUADIR)/src/%.c | obj
@ -277,7 +282,7 @@ lib/libquickjs.a: $(QJSOBJ) | lib
ar rcs $@ $^ ar rcs $@ $^
lib/libsquirrel.a: $(SQOBJ) | lib lib/libsquirrel.a: $(SQOBJ) | lib
ar rcs $@ $^ ar rcs $@ $^
lib/libmybasic.a: obj/myBasic.o | lib lib/libmybasic.a: obj/ourBasic.o | lib
ar rcs $@ $^ ar rcs $@ $^
lib/libberry.a: $(BERRYOBJ) | lib lib/libberry.a: $(BERRYOBJ) | lib
ar rcs $@ $^ ar rcs $@ $^
@ -375,6 +380,18 @@ obj/embed.o: examples/embed.c src/calog.h | obj
bin/embed: obj/embed.o lib/libcalog.a lib/libquickjs.a | bin bin/embed: obj/embed.o lib/libcalog.a lib/libquickjs.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ -lm $(CC) $(LDFLAGS) -pthread -o $@ $^ -lm
# the calog CLI runner: every engine archive + every library object + their vendored deps.
# The DB library is compiled with ALL backends (calogDbFull.o: SQLite + PostgreSQL + MySQL/
# MariaDB), so the shipped runner speaks every driver out of the box. The PG archives need a
# link group; MariaDB Connector/C is LGPL-2.1, so a redistributor who wants an all-permissive
# binary can drop the MySQL backend (see LICENSE.md). libssh2 precedes OpenSSL (it depends on
# it); SSLARCH follows the archives that reference it.
bin/calog: obj/calogMain.o \
obj/calogCrypto.o obj/calogDbFull.o obj/calogExport.o obj/calogFs.o obj/calogHttp.o obj/calogJson.o obj/calogKv.o obj/calogNet.o obj/calogPubsub.o obj/calogSsh.o obj/calogTask.o obj/calogTime.o obj/calogTimer.o obj/calogHandle.o \
lib/libcalog.a lib/liblua.a lib/libquickjs.a lib/libsquirrel.a lib/libmybasic.a lib/libberry.a lib/libs7.a lib/libwren.a \
lib/libsqlite3.a lib/libenet.a $(LIBSSH2LIB) | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ -Wl,--start-group $(PGARCHIVES) -Wl,--end-group $(MYSQLARCH) $(SSLARCH) -lstdc++ -ldl -lm -lpthread
# ---- fully-static build -------------------------------------------------------------- # ---- fully-static build --------------------------------------------------------------
# A self-contained calog executable with NO shared-library dependencies at runtime. The # A self-contained calog executable with NO shared-library dependencies at runtime. The
# calog objects are recompiled without sanitizers (ASan does not support -static) into # calog objects are recompiled without sanitizers (ASan does not support -static) into
@ -390,7 +407,7 @@ obj/rel:
mkdir -p obj/rel mkdir -p obj/rel
$(RELOBJ): obj/rel/%.o: %.c | obj/rel $(RELOBJ): obj/rel/%.o: %.c | obj/rel
$(CC) $(RELFLAGS) $(INC) $(LUAINC) $(SQLITEINC) $(ENETINC) -DCALOG_WITH_SQLITE -DCALOG_WITH_ENET -pthread -c -o $@ $< $(CC) $(RELFLAGS) $(INC) $(LUAINC) $(SQLITEINC) $(ENETINC) -DCALOG_WITH_SQLITE -pthread -c -o $@ $<
obj/rel/staticDemo.o: examples/staticDemo.c src/calog.h libs/calogDb.h libs/calogNet.h | obj/rel obj/rel/staticDemo.o: examples/staticDemo.c src/calog.h libs/calogDb.h libs/calogNet.h | obj/rel
$(CC) $(RELFLAGS) $(INC) -pthread -c -o $@ $< $(CC) $(RELFLAGS) $(INC) -pthread -c -o $@ $<
@ -507,12 +524,12 @@ tsanjs: | bin obj
# process-global state (mb_init singletons, the _mb_allocated counter) is verified # process-global state (mb_init singletons, the _mb_allocated counter) is verified
# across the whole stack -- it races without the engine lock. # across the whole stack -- it races without the engine lock.
tsanmb: | bin obj tsanmb: | bin obj
$(CC) $(MBFLAGS) -fsanitize=thread -c $(MBDIR)/myBasic.c -o obj/myBasic.tsan.o $(CC) $(MBFLAGS) -fsanitize=thread -c $(MBDIR)/ourBasic.c -o obj/ourBasic.tsan.o
$(CC) -std=c11 $(WARN) -g -O1 -fsanitize=thread -pthread $(INC) $(MBINC) -DMB_DOUBLE_FLOAT -o bin/testEngineMyBasicTsan \ $(CC) -std=c11 $(WARN) -g -O1 -fsanitize=thread -pthread $(INC) $(MBINC) -DMB_DOUBLE_FLOAT -o bin/testEngineMyBasicTsan \
tests/testEngineMyBasic.c src/mybasic/mybasicEngine.c src/mybasic/mybasicAdapter.c src/context.c src/value.c src/broker.c \ tests/testEngineMyBasic.c src/mybasic/mybasicEngine.c src/mybasic/mybasicAdapter.c src/context.c src/value.c src/broker.c \
obj/myBasic.tsan.o -lm obj/ourBasic.tsan.o -lm
setarch -R ./bin/testEngineMyBasicTsan setarch -R ./bin/testEngineMyBasicTsan
rm -f obj/myBasic.tsan.o rm -f obj/ourBasic.tsan.o
# ThreadSanitizer build of the Berry engine path: the vendored VM is recompiled under # ThreadSanitizer build of the Berry engine path: the vendored VM is recompiled under
# TSan (throwaway objs) so races are caught across the whole stack. # TSan (throwaway objs) so races are caught across the whole stack.

View file

@ -55,7 +55,7 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`,
| Lua 5.4 | `calogLuaEngine` | `.lua` | | | Lua 5.4 | `calogLuaEngine` | `.lua` | |
| JavaScript | `calogJsEngine` | `.js` | QuickJS-ng (ES2023+, BigInt → int64) | | JavaScript | `calogJsEngine` | `.js` | QuickJS-ng (ES2023+, BigInt → int64) |
| Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM | | Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM |
| MY-BASIC | `calogMyBasicEngine` | `.bas` | interpreters serialize at load | | MY-BASIC | `calogMyBasicEngine` | `.bas` | our-basic fork; def/lambda callbacks; loads serialize |
| Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints | | Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints |
| Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call(…)` | | Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call(…)` |
| Berry | `calogBerryEngine` | `.be` | scalars + callbacks | | Berry | `calogBerryEngine` | `.be` | scalars + callbacks |
@ -64,8 +64,10 @@ A native can return a keyed `CalogValueT` record and **every** engine reads its
native syntax (`user.name`, `user["name"]`, `(user "name")`), and a script can hand a native syntax (`user.name`, `user["name"]`, `(user "name")`), and a script can hand a
list/map *back* to C on **every** engine. (Wren's C API can't enumerate a map's keys, so list/map *back* to C on **every** engine. (Wren's C API can't enumerate a map's keys, so
calog carries a small documented patch to the vendored Wren that adds one.) A host function calog carries a small documented patch to the vendored Wren that adds one.) A host function
value also crosses *into* a script on every engine but MY-BASIC (which has no first-class value also crosses *into* a script on every engine but MY-BASIC (whose value model has no
callable values). slot for a *foreign* callable) -- though a MY-BASIC script can now hand its own `def`/lambda
routines *out* to a native as a callback (our vendored my-basic fork adds first-class
routine values; see below).
You can also bring your own: `CalogEngineT` is a public four-function vtable. You can also bring your own: `CalogEngineT` is a public four-function vtable.
@ -110,6 +112,24 @@ Everything links with `-pthread`.
--- ---
## Running scripts with `calog`
`make` also builds **`bin/calog`**, a ready-to-run interpreter that links every engine and
library. Point it at one or more script files:
```sh
bin/calog hello.lua # run on the engine its extension selects
bin/calog config # no extension -> search config.lua / .js / .nut / .bas / .be / .scm / .wren
bin/calog producer.js consumer.lua # several files share one runtime (kv, pubsub, exports)
```
Scripts print with `calogPrint(...)` and end by calling `calogExit([code])` (calog is
event-driven, so a script asks to exit; `Ctrl-C` also works). [`API.md`](API.md) documents
every native a script can call; [`examples/scripts/`](examples/scripts/) has runnable
examples across every engine and library, plus polyglot and multi-file demos.
---
## A complete example ## A complete example
```c ```c

View file

@ -343,6 +343,19 @@ appends one if absent). While parked there, the loop holds a valid `l`, which it
`mb_get_routine` returns `MB_FUNC_OK` with a *nil* value when a name is absent, so the `mb_get_routine` returns `MB_FUNC_OK` with a *nil* value when a name is absent, so the
not-found test is `routine.type != MB_DT_ROUTINE`, not the status code. not-found test is `routine.type != MB_DT_ROUTINE`, not the status code.
**Update (calog's my-basic fork -- see `vendor/ourbasic/CHANGELOG`).** The live-frame
requirement is now lifted for the callback direction. The fork adds
`mb_eval_routine_cold(s, routine, args, argc, ret)`, which invokes a routine value from an
*idle* interpreter: it supplies the last AST node as a clean return landing (the head
segfaults) and passes args directly, so no live `l` is needed. Combined with the
bare-`def`-as-value evaluator patch (a routine identifier not followed by `(` yields the
routine value instead of "Open bracket expected"), a my-basic script can pass a top-level
`def`/lambda to `psSubscribe` / `timerAfter` / `calogExport` and have it fired later --
including cross-engine -- like the other engines. The adapter invokes synchronously via the
live-frame `mb_eval_routine` when a callback runs inside a serving native call, and via
`mb_eval_routine_cold` when it is delivered to an idle context. (A callback must be a
top-level routine; one local to a function still dangles once that function returns.)
### 5.6 Numeric and identity caveats ### 5.6 Numeric and identity caveats
`int_t` is 32-bit unconditionally (64-bit broker ints truncate -- range-check + error or `int_t` is 32-bit unconditionally (64-bit broker ints truncate -- range-check + error or
@ -574,7 +587,7 @@ lives in that state's registry).
**Build/verify.** Core compiled strict (`-Wconversion -Wsign-conversion`); adapters drop **Build/verify.** Core compiled strict (`-Wconversion -Wsign-conversion`); adapters drop
those two (engine headers use wide macros) but keep `-Wall -Wextra -Werror`. **All three those two (engine headers use wide macros) but keep `-Wall -Wextra -Werror`. **All three
engines are vendored under `vendor/` and built from source** -- `vendor/lua` (Lua 5.4.6, engines are vendored under `vendor/` and built from source** -- `vendor/lua` (Lua 5.4.6,
library = `src/*.c` minus the `lua.c`/`luac.c` mains), `vendor/mybasic` (my-basic), library = `src/*.c` minus the `lua.c`/`luac.c` mains), `vendor/ourbasic` (our-basic, a fork of my-basic),
`vendor/squirrel-src` (Squirrel 3.2) -- each relaxed and un-sanitized but linked into the `vendor/squirrel-src` (Squirrel 3.2) -- each relaxed and un-sanitized but linked into the
sanitized binaries so cross-boundary heap misuse is still caught. Nothing depends on a sanitized binaries so cross-boundary heap misuse is still caught. Nothing depends on a
system-installed engine or `pkg-config`, so the build is reproducible. The Lua platform system-installed engine or `pkg-config`, so the build is reproducible. The Lua platform
@ -732,7 +745,7 @@ global `_mb_allocated` counter touched on every allocation (forced on in the ven
header) -- so two my-basic contexts on different threads race (TSan-confirmed). The header) -- so two my-basic contexts on different threads race (TSan-confirmed). The
singletons are built once by `mb_init` and read-only thereafter, so the only singletons are built once by `mb_init` and read-only thereafter, so the only
execution-time shared write is that counter; a one-line vendored patch makes it execution-time shared write is that counter; a one-line vendored patch makes it
`_Atomic` (the original is preserved as `vendor/mybasic/myBasic.c.orig`). With the `_Atomic` (the original is preserved as `vendor/ourbasic/ourBasic.c.upstream`). With the
counter safe, the my-basic *engine* (not the adapter, which stays usable single-threaded counter safe, the my-basic *engine* (not the adapter, which stays usable single-threaded
and lock-free) needs a lock only across *lifecycle* -- `mb_init`'s first-context build, and lock-free) needs a lock only across *lifecycle* -- `mb_init`'s first-context build,
`mb_dispose`'s last-context teardown, and the shared context refcount -- and NOT across `mb_dispose`'s last-context teardown, and the shared context refcount -- and NOT across
@ -972,7 +985,10 @@ Per engine:
`call` takes a *list* (Wren method arity is fixed, so a list absorbs any argument count); `call` takes a *list* (Wren method arity is fixed, so a list absorbs any argument count);
finalize releases. Script calls `f.call([...])`. Gotcha: Wren requires newlines between finalize releases. Script calls `f.call([...])`. Gotcha: Wren requires newlines between
class members, so the preamble is multi-line. class members, so the preamble is multi-line.
- **MY-BASIC**: inherent gap -- BASIC has no first-class callable values to invoke. - **MY-BASIC**: gap -- my-basic's value model has no slot for a *foreign* (host) callable,
so `calogFnFromNative` can't hand a native *into* a my-basic script. (The reverse now
works: calog's my-basic fork makes `def`/lambda routines first-class values a script can
pass *out* to a native and have invoked later via `mb_eval_routine_cold` -- see sec 5.5.)
**Aggregate ingress** (`*ToValue` map/list): Lua/JS/Squirrel/MY-BASIC already read both. **Aggregate ingress** (`*ToValue` map/list): Lua/JS/Squirrel/MY-BASIC already read both.
Added: Added:

View file

@ -0,0 +1,90 @@
# calog example scripts
Small, runnable programs for the `calog` command-line runner (`bin/calog`). Each one is
verified to run against the built binary. They double as a tour of the engines and the
built-in libraries.
## Running
```sh
make # builds bin/calog (among others)
bin/calog examples/scripts/languages/lua.lua # run by extension
bin/calog examples/scripts/apps/uuidGen # no extension -> searches .lua/.js/.nut/.bas/.be/.scm/.wren
bin/calog examples/scripts/multifile/producer.js \
examples/scripts/multifile/consumer.lua # several files share one runtime
```
Conventions every example follows:
- **`calogPrint(...)`** writes to stdout (calog-prefixed so it never clashes with an
engine's own `print`/keyword; on Wren it is `Calog.call("calogPrint", [...])`).
- **`calogExit([code])`** ends the run. calog is event-driven -- a script's top level
finishing does not exit the process (it may still have timers/subscriptions live), so a
script asks to exit explicitly. `Ctrl-C` also tears things down cleanly.
- Extensions map to engines: `.lua .js .nut .bas .be .scm .wren`.
## `languages/` -- one guided tour per engine
| file | shows |
|---|---|
| `lua.lua` | Lua 5.4: locals, a function, a numeric `for` building a table, then crypto + json natives |
| `javascript.js` | JavaScript (QuickJS, ES2023): `const`/`let`, arrow fns, `Array.map`/`filter`, template literals, JSON round-trip |
| `squirrel.nut` | Squirrel: locals, a function, `foreach` over an array, a table, a small class |
| `mybasic.bas` | my-basic: variables, a `FOR..NEXT` sum, `IF..THEN..ELSE`, plus crypto + kv |
| `berry.be` | Berry (Python-like): a `def`, a list built with a `for`, a map, then json + uuid |
| `scheme.scm` | s7 Scheme: a recursive factorial, list build + map, string ops (all in one `(begin ...)`) |
| `wren.wren` | Wren: a class, a `List`, and every native reached via `Calog.call(name, [args])` |
## `libraries/` -- one focused example per built-in library
| file | library / shows |
|---|---|
| `crypto.lua` | crypto: SHA-256/SHA-1, HMAC, base64 + hex round-trips, UUID, random bytes |
| `cryptoInJs.js` | crypto from JavaScript |
| `json.lua` | json: parse a nested doc, read fields, stringify a table |
| `jsonInBerry.be` | json from Berry |
| `kv.lua` | kv store: set / get / has / keys / delete |
| `kvInScheme.scm` | kv from s7 Scheme |
| `fs.lua` | fs: mkdir/write/append/read/stat/list/remove, all in a unique `/tmp` dir |
| `time.lua` | time: `timeNow`, and measuring a `timeSleep` with `timeMonotonic` |
| `timer.lua` | timer (Lua callbacks): a `timerAfter` one-shot + a self-cancelling `timerEvery` |
| `timerInMyBasic.bas` | timer callbacks **in my-basic** -- the our-basic fork makes `def`/lambda first-class |
| `database.lua` | db: an in-memory SQLite table, bound-parameter inserts, a query + an aggregate |
| `export.lua` | export: publish a function, call it by name (and, on Lua, by bare name) |
| `pubsub.lua` | pubsub: two handlers subscribe, publish delivers to both, then unsubscribe |
| `task.lua` | task: `taskSelf`, spawn a sibling child, `taskCount` |
| `net.lua` | net: a UDP echo over loopback between a spawned listener and a sender |
| `http.lua` | http: self-contained -- a spawned child runs a one-shot HTTP/1.1 server, then `httpGet`s it |
| `ssh.lua` | ssh/sftp: a documented, safe-to-run template (prints guidance if no server is configured) |
## `polyglot/` -- several languages sharing one runtime
| file | shows |
|---|---|
| `exportAcrossEngines.lua` | Lua exports a function; a spawned **JavaScript** task calls it |
| `pubsubAcrossEngines.lua` | Lua subscribes to a topic; a spawned **JavaScript** task publishes to it |
| `sharedKv.lua` | Lua seeds the shared kv store; a spawned **JavaScript** task reads it back and mutates it |
| `taskFanout.lua` | one Lua script spawns a task on **five** different engines, each printing in its own language |
## `multifile/` -- multiple script files in one run
Files listed on the `calog` command line run **concurrently in one process** and share the
same runtime (kv store, pubsub bus, exports). One `calogExit()` from any file ends the run.
```sh
bin/calog examples/scripts/multifile/producer.js examples/scripts/multifile/consumer.lua
```
| file | role |
|---|---|
| `producer.js` | (JavaScript) writes shared data to kv, then signals `"ready"` on the pubsub bus after a short delay |
| `consumer.lua` | (Lua) subscribes to `"ready"`, and on the signal reads the shared kv data, prints it, and exits |
## `apps/` -- tiny end-to-end tools
| file | shows |
|---|---|
| `uuidGen.lua` | print five random UUIDs |
| `hashText.lua` | write text to a temp file, read it back, print its SHA-256/SHA-1/length, clean up |
| `wordCount.js` | word-count a paragraph (totals, uniques, top frequencies), emit the summary as JSON |

View file

@ -0,0 +1,19 @@
-- hashText.lua -- tiny hashing tool: write text to a temp file, read it back,
-- and print its SHA-256, SHA-1, and byte length, then clean up.
-- run: bin/calog examples/scripts/apps/hashText.lua
local path = "/tmp/hashText-" .. tostring(timeNow()) .. ".txt"
local text = "The quick brown fox jumps over the lazy dog"
fsWrite(path, text)
local data = fsRead(path)
calogPrint("path: ", path)
calogPrint("bytes: ", #data)
calogPrint("sha256:", cryptoHashSha256(data))
calogPrint("sha1: ", cryptoHashSha1(data))
fsRemove(path)
calogExit(0)

View file

@ -0,0 +1,8 @@
-- uuidGen.lua -- generate five random UUIDs, one per line.
-- run: bin/calog examples/scripts/apps/uuidGen.lua
for i = 1, 5 do
calogPrint(cryptoUuid())
end
calogExit(0)

View file

@ -0,0 +1,43 @@
// wordCount.js -- tiny word-count app in JavaScript.
// Splits a paragraph into words, counts totals, uniques, and top frequencies,
// then emits a JSON summary via jsonStringify.
// Run: bin/calog examples/scripts/apps/wordCount.js
const paragraph =
"the quick brown fox jumps over the lazy dog " +
"the dog was not amused and the fox did not care " +
"the quick fox and the lazy dog became the best of friends";
// Normalize to lowercase and split on any run of non-letters.
const words = paragraph
.toLowerCase()
.split(/[^a-z]+/)
.filter((w) => w.length > 0);
// Tally frequency of each word.
const freq = {};
for (const w of words) {
freq[w] = (freq[w] || 0) + 1;
}
const uniqueWords = Object.keys(freq);
// Rank words by count (descending), then alphabetically for stable ties.
const ranked = uniqueWords.slice().sort((a, b) => {
if (freq[b] !== freq[a]) {
return freq[b] - freq[a];
}
return a < b ? -1 : a > b ? 1 : 0;
});
const topFive = ranked.slice(0, 5).map((w) => ({ word: w, count: freq[w] }));
const summary = {
totalWords: words.length,
uniqueWords: uniqueWords.length,
top: topFive,
};
calogPrint(jsonStringify(summary));
calogExit(0);

View file

@ -0,0 +1,42 @@
# A guided tour of Berry (Python-like) running on calog.
# Demonstrates: a def function, a list built and iterated with a for loop, a map
# iterated over its keys, then the jsonStringify and cryptoUuid natives.
# Run: bin/calog examples/scripts/languages/berry.be
# A function.
def square(n)
return n * n
end
# Build a list of squares with a numeric range for loop.
var squares = []
for i : 1..6
squares.push(square(i))
end
# Iterate the list, accumulating a sum.
var total = 0
for v : squares
total = total + v
end
# list.concat joins the elements into a readable string.
calogPrint("squares 1..6 =", squares.concat(", "))
calogPrint("sum of squares =", total)
# A map (Berry's associative type), iterated over its keys.
var record = {
"language": "Berry",
"count": squares.size(),
"total": total,
"id": cryptoUuid()
}
for k : record.keys()
calogPrint(" ", k, "=", record[k])
end
# Serialize the map to JSON with the native.
calogPrint("json:", jsonStringify(record))
# Tear down and exit cleanly (required -- calog does not auto-exit).
calogExit(0)

View file

@ -0,0 +1,38 @@
// Tour of JavaScript (QuickJS ES2023) on calog: const/let, arrow functions,
// Array.map/filter, template literals, and JSON round-tripping.
// Run: bin/calog examples/scripts/languages/javascript.js
// const and let bindings.
const languageName = "JavaScript";
let engine = "QuickJS ES2023";
// An arrow function.
const square = (n) => n * n;
// Array.filter then Array.map with an arrow function.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
const evens = numbers.filter((n) => n % 2 === 0);
const evenSquares = evens.map(square);
// Template literals.
calogPrint(`Engine: ${languageName} (${engine})`);
calogPrint(`Evens: ${evens.join(", ")}`);
calogPrint(`Even squares: ${evenSquares.join(", ")}`);
// Compute a sum with reduce to show more of the standard library.
const total = evenSquares.reduce((acc, n) => acc + n, 0);
calogPrint(`Sum of even squares: ${total}`);
// jsonParse a nested object, mutate it, then jsonStringify it back out.
const source = '{"user":{"name":"Ada","roles":["admin","dev"]},"active":true}';
const parsed = jsonParse(source);
parsed.user.roles.push("owner");
parsed.stats = { evens: evens.length, total: total };
const roundTripped = jsonStringify(parsed);
calogPrint(`User: ${parsed.user.name}`);
calogPrint(`Roles: ${parsed.user.roles.join(", ")}`);
calogPrint(`JSON: ${roundTripped}`);
// Tear down and exit cleanly (required -- calog does not auto-exit).
calogExit(0);

View file

@ -0,0 +1,43 @@
-- A guided tour of Lua running on calog.
-- Demonstrates: local variables, a function, a numeric for loop building a
-- table, iterating that table, then cryptoHashSha256 and jsonStringify natives.
-- Run: bin/calog examples/scripts/languages/lua.lua
-- A local variable.
local squareCount = 6
-- A function.
local function square(n)
return n * n
end
-- A numeric for loop that builds a table of squares.
local squares = {}
for i = 1, squareCount do
squares[i] = square(i)
end
-- Iterate the table and print each element.
local total = 0
for i, v in ipairs(squares) do
calogPrint("square of", i, "is", v)
total = total + v
end
calogPrint("sum of squares 1..6 =", total)
-- Hash a string with the crypto native.
local message = "calog loves Lua"
local digest = cryptoHashSha256(message)
calogPrint("sha256(" .. message .. ") =", digest)
-- Build a small record and serialize it as JSON.
local record = {
language = "lua",
version = "5.4",
squares = squares,
total = total,
digest = digest
}
calogPrint("json:", jsonStringify(record))
calogExit(0)

View file

@ -0,0 +1,38 @@
' A guided tour of my-basic running on calog.
' Demonstrates: variables, a FOR..NEXT loop that computes a running sum, an
' IF..THEN..ELSE..ENDIF decision, then the cryptoHashSha256 and kvSet/kvGet
' natives. my-basic has no first-class functions, so there are no callbacks.
' Run: bin/calog examples/scripts/languages/mybasic.bas
' Plain variables. my-basic folds identifiers to a common case at parse time.
language = "my-basic"
upto = 6
total = 0
' A FOR..NEXT loop building the sum 1 + 2 + ... + upto.
for i = 1 to upto
total = total + i
calogPrint("added", i, "running total is", total)
next i
calogPrint("sum of 1..", upto, "=", total)
' An IF..THEN..ELSE..ENDIF decision on the computed sum.
if total > 20 then
calogPrint("total", total, "is greater than 20")
else
calogPrint("total", total, "is 20 or less")
endif
' Hash a string with the crypto native; it returns a hex digest.
message = "calog loves BASIC"
digest = cryptoHashSha256(message)
calogPrint("sha256(" + message + ") =", digest)
' Store the digest in the process-wide key/value store, then read it back.
kvSet("mybasic.digest", digest)
roundTrip = kvGet("mybasic.digest")
calogPrint("kv round-trip matches:", roundTrip = digest)
' Tear down and exit cleanly (required -- calog does not auto-exit).
calogExit(0)

View file

@ -0,0 +1,36 @@
; A guided tour of s7 Scheme running on calog.
; Demonstrates: a recursive factorial, a list built and mapped over, string
; assembly, and the cryptoHashSha256 native -- all inside ONE (begin ...) form,
; because s7's runSource reads a single top-level form per script.
; Run: bin/calog examples/scripts/languages/scheme.scm
(begin
; A classic recursive function.
(define (factorial n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))
; Build a list, then derive a second list by mapping factorial over it.
(define nums (list 1 2 3 4 5 6))
(define facts (map factorial nums))
; object->string turns an aggregate into a readable literal for printing
; (calogPrint renders a raw list as "[aggregate]").
(calogPrint "nums =" (object->string nums))
(calogPrint "facts =" (object->string facts))
; Fold the mapped list into a running sum with a tail-recursive helper.
(define (sum-list lst acc)
(if (null? lst)
acc
(sum-list (cdr lst) (+ acc (car lst)))))
(calogPrint "sum of factorials =" (sum-list facts 0))
; Hash a message with the crypto native; it returns a lowercase hex digest.
(define message "calog + s7 scheme")
(calogPrint "sha256(" message ") =" (cryptoHashSha256 message))
; Tear down and exit cleanly (required -- calog does not auto-exit).
(calogExit 0))

View file

@ -0,0 +1,58 @@
// A guided tour of Squirrel running on calog.
// Demonstrates: local variables, a function, an array with foreach, a table
// (map) with foreach, a simple class with a method, then the cryptoUuid native.
// Run: bin/calog examples/scripts/languages/squirrel.nut
// A local variable.
local engine = "Squirrel"
// A function.
function square(n) {
return n * n
}
// An array, iterated with foreach to build a table of squares.
local numbers = [1, 2, 3, 4, 5, 6]
local squares = {}
local total = 0
foreach (idx, value in numbers) {
local s = square(value)
squares[value] <- s
total = total + s
calogPrint("square of", value, "is", s)
}
calogPrint("sum of squares 1..6 =", total)
// A table (map), iterated with foreach over key/value pairs.
local record = {
language = engine
version = "3.x"
total = total
}
foreach (key, value in record) {
calogPrint("record[" + key + "] =", value)
}
// A simple class with a constructor and a method.
class Counter {
count = 0
constructor(start) {
count = start
}
function bump(by) {
count = count + by
return count
}
}
local counter = Counter(10)
counter.bump(5)
counter.bump(7)
calogPrint("counter after two bumps =", counter.count)
// Use the cryptoUuid native to mint a unique id.
local id = cryptoUuid()
calogPrint("generated uuid:", id)
// Tear down and exit cleanly (required -- calog does not auto-exit).
calogExit(0)

View file

@ -0,0 +1,37 @@
// A guided tour of Wren running on calog. Wren reaches natives only through
// Calog.call(name, [args]), so print, crypto, and exit all go through it.
// Demonstrates: a class with a method, a List transformed with map, and the
// cryptoUuid / cryptoBase64Encode / calogPrint natives. Synchronous only.
// Run: bin/calog examples/scripts/languages/wren.wren
// A small class with a constructor and a method.
class Square {
construct new(n) {
_n = n
}
value { _n * _n }
}
// Build a List, then transform it with map (a Sequence) and materialize it.
var numbers = [1, 2, 3, 4, 5, 6]
var squares = numbers.map {|n| Square.new(n).value }.toList
// Reduce to a sum to show more of the List API.
var total = squares.reduce(0) {|acc, v| acc + v }
// Natives are reached via Calog.call(name, [args]).
Calog.call("calogPrint", ["squares 1..6 =", squares.join(", ")])
Calog.call("calogPrint", ["sum of squares =", total])
// A generated UUID, then base64-encoded through two more natives.
var id = Calog.call("cryptoUuid", [])
var encoded = Calog.call("cryptoBase64Encode", [id])
Calog.call("calogPrint", ["uuid =", id])
Calog.call("calogPrint", ["base64 =", encoded])
// String interpolation works too; pass the finished string to the native.
Calog.call("calogPrint", ["Wren squared %(numbers.count) numbers, total %(total)."])
// Tear down and exit cleanly (required -- calog does not auto-exit).
Calog.call("calogExit", [0])

View file

@ -0,0 +1,20 @@
-- Showcase the calog crypto library: hashing, HMAC, base64/hex round-trips,
-- UUID generation, and random bytes.
-- Run: bin/calog examples/scripts/libraries/crypto.lua
calogPrint("sha256(abc):", cryptoHashSha256("abc"))
calogPrint("sha1(abc):", cryptoHashSha1("abc"))
calogPrint("hmacSha256(key,abc):", cryptoHmacSha256("key", "abc"))
local b64 = cryptoBase64Encode("hello world")
calogPrint("base64 encode:", b64)
calogPrint("base64 decode:", cryptoBase64Decode(b64))
local hex = cryptoHexEncode("hello world")
calogPrint("hex encode:", hex)
calogPrint("hex decode:", cryptoHexDecode(hex))
calogPrint("uuid:", cryptoUuid())
calogPrint("randomBytes(16) length:", #cryptoRandomBytes(16))
calogExit(0)

View file

@ -0,0 +1,19 @@
// Crypto in JavaScript: SHA-256 hashing, a base64 encode/decode round-trip,
// and UUID generation via the calog crypto natives.
// Run: bin/calog examples/scripts/libraries/cryptoInJs.js
// NOTE: QuickJS has no console/print builtin -> use calogPrint.
const message = "hello world";
calogPrint("sha256(hello world):", cryptoHashSha256(message));
const encoded = cryptoBase64Encode(message);
const decoded = cryptoBase64Decode(encoded);
calogPrint("base64 encode:", encoded);
calogPrint("base64 decode:", decoded);
calogPrint("round-trip ok:", decoded === message);
calogPrint("uuid:", cryptoUuid());
calogExit(0);

View file

@ -0,0 +1,39 @@
-- Showcase the calog db library on SQLite: open an in-memory database, create a
-- table, insert several rows using BOUND parameters (safe from injection), run a
-- SELECT and print each returned row map field by field, run a COUNT aggregate,
-- then close the handle.
-- Run: bin/calog examples/scripts/libraries/database.lua
local db = dbOpen("sqlite", ":memory:")
dbExec(db, "CREATE TABLE fruit (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER, price REAL)")
-- Insert rows with bound parameters. dbExec returns rows affected.
local stock = {
{ "apple", 12, 0.35 },
{ "banana", 30, 0.20 },
{ "cherry", 8, 1.10 },
{ "date", 15, 0.75 },
}
local inserted = 0
for _, item in ipairs(stock) do
inserted = inserted + dbExec(db, "INSERT INTO fruit (name, qty, price) VALUES (?, ?, ?)", item[1], item[2], item[3])
end
calogPrint("rows inserted:", inserted)
-- SELECT returning a list of row maps. Print each field explicitly.
local rows = dbQuery(db, "SELECT id, name, qty, price FROM fruit WHERE qty >= ? ORDER BY name", 10)
calogPrint("rows with qty >= 10:", #rows)
for _, row in ipairs(rows) do
calogPrint(string.format(" #%d %-7s qty=%d price=%.2f", row.id, row.name, row.qty, row.price))
end
-- Aggregate query: COUNT and SUM in a single row.
local summary = dbQuery(db, "SELECT COUNT(*) AS n, SUM(qty) AS total FROM fruit")
calogPrint("distinct fruits:", summary[1].n)
calogPrint("total quantity:", summary[1].total)
dbClose(db)
calogExit(0)

View file

@ -0,0 +1,26 @@
-- Showcase the calog export library: publish a function with calogExport, then
-- invoke it two ways -- via calogCall by name, and (a Lua convenience) by its bare
-- name, since exports resolve as unknown globals. Finally calogUnexport it and show
-- a guarded call fails cleanly instead of returning a stale result.
-- Run: bin/calog examples/scripts/libraries/export.lua
calogExport("square", function(x)
return x * x
end)
-- Invoke through the broker by name.
calogPrint("calogCall square 7:", calogCall("square", 7))
-- Invoke by bare name (Lua resolves the unknown global through the export registry).
calogPrint("bare-name square 9:", square(9))
-- Retire the export; the name is no longer resolvable anywhere.
calogUnexport("square")
local ok, err = pcall(calogCall, "square", 7)
calogPrint("call after unexport ok:", ok)
if not ok then
calogPrint("call after unexport failed cleanly:", tostring(err))
end
calogExit(0)

View file

@ -0,0 +1,34 @@
-- Showcase the calog fs library end to end inside a unique temp dir: create a
-- directory, write and append a file, read it back, stat it, list the dir, then
-- remove the file and directory so nothing is left behind.
-- Run: bin/calog examples/scripts/libraries/fs.lua
local dir = "/tmp/calogFsExample." .. cryptoUuid()
local file = dir .. "/notes.txt"
fsMkdir(dir)
calogPrint("dir exists after mkdir:", fsExists(dir))
fsWrite(file, "line one\n")
fsAppend(file, "line two\n")
calogPrint("file exists after write:", fsExists(file))
local contents = fsRead(file)
calogPrint("contents:", contents)
local info = fsStat(file)
calogPrint("size:", info.size)
calogPrint("isFile:", info.isFile)
calogPrint("isDir:", info.isDir)
local entries = fsList(dir)
calogPrint("entry count:", #entries)
calogPrint("first entry:", entries[1])
fsRemove(file)
calogPrint("file exists after remove:", fsExists(file))
fsRemove(dir)
calogPrint("dir exists after remove:", fsExists(dir))
calogExit(0)

View file

@ -0,0 +1,50 @@
-- Showcase the calog http client end to end with NO external dependency: a
-- spawned "lua" child task acts as a tiny one-shot HTTP/1.1 server -- it
-- tcpListens on a fixed high port, accepts one connection, reads the request,
-- and sends back a valid 200 response with a Content-Length body. The main
-- context sleeps briefly so the child can bind, then httpGets that URL and
-- prints the returned status and body before exiting.
-- Run: bin/calog examples/scripts/libraries/http.lua
local PORT = 48080
-- The child runs as its own context. Its tcpAccept/tcpRecv block that child's
-- thread, leaving the main context free to make the request. Inside this long
-- string the \r\n sequences are literal text that the child parses as escapes.
local serverCode = [[
local PORT = 48080
local listener = tcpListen(PORT)
calogPrint("server: listening on port", PORT)
local client = tcpAccept(listener)
local request = tcpRecv(client, 4096)
local requestLine = request and string.match(request, "^[^\r\n]+") or "?"
calogPrint("server: request line:", requestLine)
local body = "Hello from the calog TCP server!\n"
local response =
"HTTP/1.1 200 OK\r\n" ..
"Content-Type: text/plain\r\n" ..
"Content-Length: " .. #body .. "\r\n" ..
"Connection: close\r\n" ..
"\r\n" ..
body
tcpSend(client, response)
tcpClose(client)
tcpClose(listener)
calogPrint("server: response sent")
]]
taskSpawn("lua", serverCode)
-- Give the child a moment to reach its tcpListen before we connect.
timeSleep(400)
local url = "http://127.0.0.1:" .. PORT .. "/"
calogPrint("client: requesting", url)
local response = httpGet(url)
calogPrint("client: status", response.status)
calogPrint("client: body", response.body)
calogExit(0)

View file

@ -0,0 +1,35 @@
-- Showcase the calog json library: jsonParse a nested JSON document, read its
-- fields, then build a Lua table and jsonStringify it back to text.
-- Run: bin/calog examples/scripts/libraries/json.lua
local text = [[
{
"name": "calog",
"version": 2,
"stable": true,
"notes": null,
"engines": ["lua", "javascript", "scheme"],
"limits": { "maxTasks": 64, "ratio": 1.5 }
}
]]
local doc = jsonParse(text)
calogPrint("name:", doc.name)
calogPrint("version:", doc.version)
calogPrint("stable:", doc.stable)
calogPrint("notes is nil:", doc.notes == nil)
calogPrint("first engine:", doc.engines[1])
calogPrint("engine count:", #doc.engines)
calogPrint("limits.maxTasks:", doc.limits.maxTasks)
calogPrint("limits.ratio:", doc.limits.ratio)
local report = {}
report.tool = "calog"
report.ok = true
report.count = doc.engines
report.summary = { engines = #doc.engines, maxTasks = doc.limits.maxTasks }
calogPrint("stringified:", jsonStringify(report))
calogExit(0)

View file

@ -0,0 +1,26 @@
# Showcase the calog json library from Berry: jsonParse a small JSON document,
# read fields out of the resulting map, then build a Berry map and jsonStringify
# it back to text.
# Run: bin/calog examples/scripts/libraries/jsonInBerry.be
# Parse a small JSON string into a Berry map.
var text = '{ "name": "calog", "version": 2, "stable": true, "engines": ["lua", "berry", "scheme"] }'
var doc = jsonParse(text)
# Read fields out of the parsed map.
calogPrint("name:", doc["name"])
calogPrint("version:", doc["version"])
calogPrint("stable:", doc["stable"])
calogPrint("first engine:", doc["engines"][0])
calogPrint("engine count:", doc["engines"].size())
# Build a fresh map and serialize it to JSON text.
var report = {
"tool": doc["name"],
"ok": doc["stable"],
"engines": doc["engines"].size()
}
calogPrint("stringified:", jsonStringify(report))
# Tear down and exit cleanly (required -- calog does not auto-exit).
calogExit(0)

View file

@ -0,0 +1,33 @@
-- Showcase the calog kv store: set string and number values, read them back,
-- test presence with kvHas, list keys with kvKeys, then delete one key and
-- confirm it is gone. The kv store is process-wide and shared across engines.
-- Run: bin/calog examples/scripts/libraries/kv.lua
kvSet("tool", "calog")
kvSet("version", 2)
kvSet("ratio", 1.5)
kvSet("stable", "yes")
calogPrint("tool:", kvGet("tool"))
calogPrint("version:", kvGet("version"))
calogPrint("ratio:", kvGet("ratio"))
calogPrint("stable:", kvGet("stable"))
calogPrint("has tool:", kvHas("tool"))
calogPrint("has missing:", kvHas("missing"))
local keys = kvKeys()
table.sort(keys)
calogPrint("key count:", #keys)
calogPrint("keys:", table.concat(keys, ", "))
kvDelete("stable")
calogPrint("has stable after delete:", kvHas("stable"))
calogPrint("get stable after delete is nil:", kvGet("stable") == nil)
local remaining = kvKeys()
table.sort(remaining)
calogPrint("remaining count:", #remaining)
calogPrint("remaining keys:", table.concat(remaining, ", "))
calogExit(0)

View file

@ -0,0 +1,22 @@
; The calog kv store from s7 Scheme, inside ONE (begin ...) form (s7's
; runSource reads a single top-level form per script). Sets a couple of keys,
; reads them back with kvGet, and checks presence with kvHas. The kv store is
; process-wide and shared across every engine.
; Run: bin/calog examples/scripts/libraries/kvInScheme.scm
(begin
; Store a string and a number under two keys.
(kvSet "tool" "calog")
(kvSet "answer" 42)
; Read the values back and print them.
(calogPrint "tool =" (kvGet "tool"))
(calogPrint "answer =" (kvGet "answer"))
; kvHas reports whether a key exists.
(calogPrint "has tool =" (kvHas "tool"))
(calogPrint "has missing =" (kvHas "missing"))
; Tear down and exit cleanly (required -- calog does not auto-exit).
(calogExit 0))

View file

@ -0,0 +1,35 @@
-- Showcase the calog net library over loopback with UDP datagrams: a spawned
-- "lua" listener task binds a fixed high port and blocks in udpRecvFrom, while
-- the main context opens an ephemeral socket and sends one datagram to it. The
-- listener's blocking recv runs on its own task thread, so the main thread stays
-- free to send. A short delay lets the datagram arrive and print before we exit.
-- Run: bin/calog examples/scripts/libraries/net.lua
local PORT = 47651
-- The listener runs in its own context/thread, so its blocking udpRecvFrom does
-- not stall the main script. It prints the payload and sender it received.
local listenerCode = string.format([[
local sock = udpOpen(%d)
calogPrint("listener bound on port", %d)
local packet = udpRecvFrom(sock, 1024)
calogPrint("listener received:", packet.data, "from", packet.host .. ":" .. packet.port)
udpClose(sock)
]], PORT, PORT)
local listener = taskSpawn("lua", listenerCode)
calogPrint("spawned listener handle:", listener)
-- Give the listener a beat to bind its socket before we transmit.
timeSleep(200)
local sender = udpOpen(0)
local sent = udpSendTo(sender, "127.0.0.1", PORT, "hello over udp")
calogPrint("sender transmitted", sent, "bytes to 127.0.0.1:" .. PORT)
udpClose(sender)
-- Let the datagram land and the listener print, then tear everything down.
timerAfter(300, function()
taskClose(listener)
calogExit(0)
end)

View file

@ -0,0 +1,25 @@
-- Showcase the calog pubsub library within a single Lua context: two handlers
-- subscribe to the "news" topic, a publish delivers to both (count 2), then one
-- handler unsubscribes and a second publish delivers to just one (count 1).
-- A short timer defers the exit so all deliveries land first.
-- Run: bin/calog examples/scripts/libraries/pubsub.lua
local firstId = psSubscribe("news", function(msg)
calogPrint("handler one got:", msg)
end)
local secondId = psSubscribe("news", function(msg)
calogPrint("handler two got:", msg)
end)
local firstCount = psPublish("news", "hello subscribers")
calogPrint("delivered to", firstCount, "handlers")
psUnsubscribe(firstId)
local secondCount = psPublish("news", "only one left")
calogPrint("delivered to", secondCount, "handlers")
timerAfter(150, function()
calogExit(0)
end)

View file

@ -0,0 +1,57 @@
-- Showcase the calog ssh/sftp library as a safe-to-run, documented template:
-- connect + password auth, run a remote command and print its stdout, then a
-- round-trip through SFTP (upload a small payload with sftpPut, read it back
-- with sftpGet, and verify it matches). The entire flow is wrapped in pcall,
-- so with no reachable server it prints one clear hint and exits cleanly.
-- Run: bin/calog examples/scripts/libraries/ssh.lua
-- EDIT THESE: point them at a real SSH server you control before running.
-- The placeholder host below deliberately does not resolve, so the example is
-- safe to run as-is: it prints a hint and exits 0 instead of touching a server.
local HOST = "ssh.example.invalid"
local USER = "youruser"
local PASS = "yourpass"
local PORT = 22
-- A remote path we may create and delete; keep it under /tmp so cleanup is safe.
local REMOTE_PATH = "/tmp/calog_ssh_example.txt"
local PAYLOAD = "hello from the calog ssh example\n"
-- Everything network-facing lives here so a single pcall can catch a missing
-- server, refused connection, or auth failure without crashing the script.
local function demo()
local session = sshConnect(HOST, PORT)
calogPrint("connected to", HOST .. ":" .. PORT)
if not sshAuthPassword(session, USER, PASS) then
sshClose(session)
error("password authentication was rejected for user " .. USER)
end
calogPrint("authenticated as", USER)
-- Run a command remotely and show its captured output.
local run = sshExec(session, "uname -a")
calogPrint("remote uname:", run.stdout)
calogPrint("exit code:", run.exitCode)
-- SFTP round-trip: upload the payload, read it back, and compare.
sftpPut(session, REMOTE_PATH, PAYLOAD)
calogPrint("uploaded", #PAYLOAD, "bytes to", REMOTE_PATH)
local fetched = sftpGet(session, REMOTE_PATH)
calogPrint("downloaded", #fetched, "bytes back")
calogPrint("round-trip matches:", fetched == PAYLOAD)
-- Tidy up the remote file, then close the session.
sftpRemove(session, REMOTE_PATH)
sshClose(session)
calogPrint("session closed")
end
local ok, err = pcall(demo)
if not ok then
calogPrint("ssh example: set HOST/USER/PASS and point at a real SSH server")
calogPrint(" (" .. tostring(err) .. ")")
end
calogExit(0)

View file

@ -0,0 +1,21 @@
-- Showcase the calog task library: taskSelf reports this context's id,
-- taskSpawn launches a sibling "lua" child that runs its own code string and
-- calogPrints a message, taskCount reports how many contexts are now alive.
-- A short timer gives the child time to run before we exit, so its output lands.
-- Run: bin/calog examples/scripts/libraries/task.lua
calogPrint("self id:", taskSelf())
local childCode = [[
calogPrint("child says hello from task", taskSelf())
]]
local child = taskSpawn("lua", childCode)
calogPrint("spawned child handle:", child)
calogPrint("task count:", taskCount())
timerAfter(200, function()
taskClose(child)
calogExit(0)
end)

View file

@ -0,0 +1,15 @@
-- Showcase the calog time library: read the real wall-clock epoch with
-- timeNow(), then measure a ~50ms timeSleep by sampling timeMonotonic()
-- before and after (monotonic is immune to wall-clock adjustments).
-- Run: bin/calog examples/scripts/libraries/time.lua
calogPrint("epoch now:", timeNow())
local start = timeMonotonic()
timeSleep(50)
local elapsed = timeMonotonic() - start
calogPrint("slept for (s):", elapsed)
calogPrint("about 0.05s:", elapsed >= 0.04 and elapsed < 0.5)
calogExit(0)

View file

@ -0,0 +1,20 @@
-- Showcase the calog timer library with Lua callbacks: timerAfter schedules a
-- one-shot, timerEvery fires a repeating tick, and the tick handler cancels its
-- own timer after 3 ticks and then exits.
-- Run: bin/calog examples/scripts/libraries/timer.lua
timerAfter(100, function()
calogPrint("one-shot")
end)
local ticks = 0
local tickId
tickId = timerEvery(60, function()
ticks = ticks + 1
calogPrint("tick", ticks)
if ticks >= 3 then
timerCancel(tickId)
calogExit(0)
end
end)

View file

@ -0,0 +1,20 @@
' Timer callbacks in my-basic. calog's my-basic fork (vendor/ourbasic) makes def/lambda
' routines first-class values, so a BASIC script can hand a callback to a native just like
' every other engine. Here a def is armed on a repeating timer; it counts ticks in a
' global, and after the third tick cancels its own timer and ends the run.
' run: bin/calog examples/scripts/libraries/timerInMyBasic.bas
n = 0
tid = 0
def tick()
n = n + 1
calogPrint("my-basic tick", n)
if n >= 3 then
timerCancel(tid)
calogExit(0)
endif
enddef
tid = timerEvery(80, tick)
calogPrint("my-basic armed a repeating timer with a def callback")

View file

@ -0,0 +1,16 @@
-- consumer.lua -- the other half of a MULTI-FILE calog run (see producer.js).
--
-- Run BOTH files together:
-- bin/calog examples/scripts/multifile/producer.js examples/scripts/multifile/consumer.lua
--
-- Subscribes to the shared pubsub bus; when the producer (a separate .js file) signals,
-- reads the values it left in the shared kv store, prints them, and ends the whole run.
-- One calogExit() from any file tears the whole (multi-file) process down.
psSubscribe("ready", function()
local greeting = kvGet("greeting")
local from = kvGet("from")
calogPrint("consumer.lua: got the signal ->", greeting, "from", from)
calogExit(0)
end)
calogPrint("consumer.lua: subscribed to 'ready', waiting for the producer")

View file

@ -0,0 +1,18 @@
// producer.js -- one half of a MULTI-FILE calog run (see consumer.lua).
//
// Run BOTH files together, in one process:
// bin/calog examples/scripts/multifile/producer.js examples/scripts/multifile/consumer.lua
//
// Files listed on the command line run CONCURRENTLY and share one runtime -- so this
// JavaScript file and the Lua file talk through the same kv store and pubsub bus.
// This side writes the shared data, then signals "ready" after a short delay (giving the
// consumer, loaded from the other file, time to subscribe first).
kvSet("greeting", "Hello");
kvSet("from", "JavaScript (producer.js)");
calogPrint("producer.js: wrote greeting + from into the shared kv store");
timerAfter(300, function () {
calogPrint("producer.js: signalling 'ready'");
psPublish("ready", "go");
});

View file

@ -0,0 +1,26 @@
-- Cross-engine function call: Lua exports a "greet" function through the broker,
-- then a spawned JavaScript task invokes that Lua function via calogCall and prints
-- the string it returns. The export happens BEFORE the spawn so the child always
-- sees the registered name; a short timer gives the JS task time to run, then exits.
-- Run: bin/calog examples/scripts/polyglot/exportAcrossEngines.lua
-- Publish a Lua function into the process-wide export registry.
calogExport("greet", function(name)
return "hello, " .. name
end)
-- The child runs on the JavaScript engine and reaches back into the Lua function.
local jsCode = [[
var msg = calogCall("greet", "world");
calogPrint("javascript received:", msg);
]]
local child = taskSpawn("js", jsCode)
calogPrint("spawned javascript task:", child)
-- Give the JS task time to run its call, then tear everything down.
timerAfter(300, function()
taskClose(child)
calogUnexport("greet")
calogExit(0)
end)

View file

@ -0,0 +1,29 @@
-- Cross-engine pubsub: Lua subscribes to the "events" topic through the broker,
-- then a spawned JavaScript task publishes a message on that same topic. Because
-- pubsub is process-wide, the Lua handler receives the JS-published message and
-- prints it. The subscribe happens BEFORE the spawn so the child's publish is
-- never missed; a short timer gives delivery time to land, then exits.
-- Run: bin/calog examples/scripts/polyglot/pubsubAcrossEngines.lua
-- Register the Lua handler first so the topic has a listener before anyone publishes.
local subId = psSubscribe("events", function(msg)
calogPrint("lua handler received:", msg)
end)
calogPrint("lua subscribed to 'events' with id:", subId)
-- The child runs on the JavaScript engine and publishes into the shared topic.
local jsCode = [[
var count = psPublish("events", "from JavaScript");
calogPrint("javascript published to", count, "subscriber(s)");
]]
local child = taskSpawn("js", jsCode)
calogPrint("spawned javascript task:", child)
-- Give the JS publish time to be delivered to the Lua handler, then tear down.
timerAfter(300, function()
taskClose(child)
psUnsubscribe(subId)
calogExit(0)
end)

View file

@ -0,0 +1,30 @@
-- Shared key/value store across engines: Lua seeds the process-wide kv with a JSON
-- config blob and a numeric counter, then a spawned JavaScript task reads both back
-- (parsing the JSON), prints them, and bumps the counter to 42. Because kv is shared
-- across every engine, Lua sees the JS write: after a short timer it reads the counter
-- again, confirms it is now 42, then tears everything down.
-- Run: bin/calog examples/scripts/polyglot/sharedKv.lua
-- Seed shared state from Lua before spawning the child.
kvSet("config", jsonStringify({ name = "calog", version = 1 }))
kvSet("counter", 41)
calogPrint("lua seeded config and counter =", kvGet("counter"))
-- The child runs on the JavaScript engine and reads/writes the same shared kv.
local jsCode = [[
var config = jsonParse(kvGet("config"));
calogPrint("javascript read config name:", config.name, "version:", config.version);
calogPrint("javascript read counter:", kvGet("counter"));
kvSet("counter", 42);
calogPrint("javascript set counter to 42");
]]
local child = taskSpawn("js", jsCode)
calogPrint("spawned javascript task:", child)
-- Give the JS task time to run its read/write, then observe the shared write from Lua.
timerAfter(300, function()
calogPrint("lua sees counter =", kvGet("counter"))
taskClose(child)
calogExit(0)
end)

View file

@ -0,0 +1,34 @@
-- Polyglot task fan-out: one Lua script spawns a tiny task on FIVE different
-- engines (JavaScript, Squirrel, Berry, Scheme/s7, Wren). Each child prints a
-- one-line hello in its own language through the shared calogPrint native, so a
-- single run shows all five runtimes executing side by side under one broker.
-- The Scheme child is wrapped in (begin ...) because s7 evaluates one form; the
-- Wren child reaches natives via Calog.call. A short timer lets every child run
-- before we tear down and exit.
-- Run: bin/calog examples/scripts/polyglot/taskFanout.lua
-- Each entry pairs a taskSpawn engine name with a minimal, idiomatic snippet.
local children = {
{ engine = "js", code = [[calogPrint("javascript: hello from JavaScript");]] },
{ engine = "squirrel", code = [[calogPrint("squirrel: hello from Squirrel");]] },
{ engine = "berry", code = [[calogPrint("berry: hello from Berry")]] },
{ engine = "s7", code = [[(begin (calogPrint "scheme: hello from Scheme"))]] },
{ engine = "wren", code = [[Calog.call("calogPrint", ["wren: hello from Wren"])]] },
}
local handles = {}
for index = 1, #children do
local child = children[index]
local handle = taskSpawn(child.engine, child.code)
handles[index] = handle
calogPrint("spawned", child.engine, "task:", handle)
end
-- Give all five children time to run their single print, then close and exit.
timerAfter(400, function()
for index = 1, #handles do
taskClose(handles[index])
end
calogExit(0)
end)

View file

@ -20,9 +20,7 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <unistd.h> #include <unistd.h>
#ifdef CALOG_WITH_ENET
#include <enet/enet.h> #include <enet/enet.h>
#endif
// Handle type tags, distinct across the whole registry so a stray handle of the wrong kind // Handle type tags, distinct across the whole registry so a stray handle of the wrong kind
// fails to resolve (e.g. a listener passed to tcpSend). // fails to resolve (e.g. a listener passed to tcpSend).
@ -51,14 +49,12 @@ typedef struct NetLibT {
static pthread_mutex_t gNetLibMutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t gNetLibMutex = PTHREAD_MUTEX_INITIALIZER;
static NetLibT *gNetLib = NULL; static NetLibT *gNetLib = NULL;
#ifdef CALOG_WITH_ENET
static int32_t enetClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t enetConnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetConnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t enetDisconnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetDisconnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t enetHost(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetHost(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t enetSend(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetSend(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t enetService(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetService(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
#endif
static void netCloser(uint32_t type, void *resource); static void netCloser(uint32_t type, void *resource);
static int32_t netMapSetInt(CalogAggT *map, const char *key, int64_t value); static int32_t netMapSetInt(CalogAggT *map, const char *key, int64_t value);
static int32_t netMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length); static int32_t netMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length);
@ -92,14 +88,12 @@ int32_t calogNetRegister(CalogT *calog) {
pthread_mutex_unlock(&gNetLibMutex); pthread_mutex_unlock(&gNetLibMutex);
return calogErrOomE; return calogErrOomE;
} }
#ifdef CALOG_WITH_ENET
if (enet_initialize() != 0) { if (enet_initialize() != 0) {
calogHandleTableDestroy(lib->handles, NULL); calogHandleTableDestroy(lib->handles, NULL);
free(lib); free(lib);
pthread_mutex_unlock(&gNetLibMutex); pthread_mutex_unlock(&gNetLibMutex);
return calogErrOomE; return calogErrOomE;
} }
#endif
gNetLib = lib; gNetLib = lib;
} }
gNetLib->refCount++; gNetLib->refCount++;
@ -114,14 +108,12 @@ int32_t calogNetRegister(CalogT *calog) {
calogRegisterInline(calog, "udpSendTo", udpSendTo, gNetLib); calogRegisterInline(calog, "udpSendTo", udpSendTo, gNetLib);
calogRegisterInline(calog, "udpRecvFrom", udpRecvFrom, gNetLib); calogRegisterInline(calog, "udpRecvFrom", udpRecvFrom, gNetLib);
calogRegisterInline(calog, "udpClose", udpClose, gNetLib); calogRegisterInline(calog, "udpClose", udpClose, gNetLib);
#ifdef CALOG_WITH_ENET
calogRegisterInline(calog, "enetHost", enetHost, gNetLib); calogRegisterInline(calog, "enetHost", enetHost, gNetLib);
calogRegisterInline(calog, "enetConnect", enetConnect, gNetLib); calogRegisterInline(calog, "enetConnect", enetConnect, gNetLib);
calogRegisterInline(calog, "enetService", enetService, gNetLib); calogRegisterInline(calog, "enetService", enetService, gNetLib);
calogRegisterInline(calog, "enetSend", enetSend, gNetLib); calogRegisterInline(calog, "enetSend", enetSend, gNetLib);
calogRegisterInline(calog, "enetDisconnect", enetDisconnect, gNetLib); calogRegisterInline(calog, "enetDisconnect", enetDisconnect, gNetLib);
calogRegisterInline(calog, "enetClose", enetClose, gNetLib); calogRegisterInline(calog, "enetClose", enetClose, gNetLib);
#endif
return calogOkE; return calogOkE;
} }
@ -135,9 +127,7 @@ void calogNetShutdown(void) {
gNetLib->refCount--; gNetLib->refCount--;
if (gNetLib->refCount <= 0) { if (gNetLib->refCount <= 0) {
calogHandleTableDestroy(gNetLib->handles, netCloser); calogHandleTableDestroy(gNetLib->handles, netCloser);
#ifdef CALOG_WITH_ENET
enet_deinitialize(); enet_deinitialize();
#endif
free(gNetLib); free(gNetLib);
gNetLib = NULL; gNetLib = NULL;
} }
@ -156,14 +146,12 @@ static void netCloser(uint32_t type, void *resource) {
free(sock); free(sock);
break; break;
} }
#ifdef CALOG_WITH_ENET
case NET_TYPE_ENET_HOST: case NET_TYPE_ENET_HOST:
enet_host_destroy((ENetHost *)resource); enet_host_destroy((ENetHost *)resource);
break; break;
case NET_TYPE_ENET_PEER: case NET_TYPE_ENET_PEER:
// Peers are owned by their host; enet_host_destroy frees them. // Peers are owned by their host; enet_host_destroy frees them.
break; break;
#endif
default: default:
break; break;
} }
@ -599,7 +587,6 @@ static int32_t udpSendTo(CalogValueT *args, int32_t argCount, CalogValueT *resul
} }
#ifdef CALOG_WITH_ENET
static int32_t enetClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t enetClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
NetLibT *lib; NetLibT *lib;
ENetHost *host; ENetHost *host;
@ -852,4 +839,3 @@ static int32_t enetService(CalogValueT *args, int32_t argCount, CalogValueT *res
calogValueAgg(result, map); calogValueAgg(result, map);
return calogOkE; return calogOkE;
} }
#endif

View file

@ -11,14 +11,20 @@
// udpSendTo(handle, host, port, data) -> bytesSent // udpSendTo(handle, host, port, data) -> bytesSent
// udpRecvFrom(handle, maxBytes) -> { data, host, port } // udpRecvFrom(handle, maxBytes) -> { data, host, port }
// udpClose(handle) // udpClose(handle)
// Payloads are binary-safe strings. The blocking natives (accept/recv/recvFrom) are INLINE, // enetHost(port, maxPeers) -> hostHandle
// so they stall only the calling script's context thread, not the host. Handles are NOT // enetConnect(hostHandle, host, port, channels) -> peerHandle
// reference-counted: a socket/host/peer handle belongs to the context that created it, and // enetService(hostHandle, timeoutMs) -> { type[, peer, channel, data] }
// using one handle concurrently from two contexts -- or closing it while another context is // enetSend(peerHandle, channel, data, reliable)
// mid-operation -- is undefined (use-after-free). IPv4 for v1; // enetDisconnect(peerHandle)
// hostname resolution uses getaddrinfo (works in dynamic builds; a fully-static glibc build // enetClose(hostHandle)
// cannot resolve names -- connect by IP, or link musl). Backends: TCP/UDP always; ENet is // Payloads are binary-safe strings. The blocking natives (accept/recv/recvFrom/enetService)
// added behind CALOG_WITH_ENET. // are INLINE, so they stall only the calling script's context thread, not the host. Handles
// are NOT reference-counted: a socket/host/peer handle belongs to the context that created
// it, and using one handle concurrently from two contexts -- or closing it while another
// context is mid-operation -- is undefined (use-after-free). IPv4 for v1; hostname resolution
// uses getaddrinfo (works in dynamic builds; a fully-static glibc build cannot resolve names
// -- connect by IP, or link musl). Three transports, all always compiled in and first-class:
// TCP, UDP, and ENet (reliable UDP over UDP).
#ifndef CALOG_NET_H #ifndef CALOG_NET_H
#define CALOG_NET_H #define CALOG_NET_H

534
src/calogMain.c Normal file
View file

@ -0,0 +1,534 @@
// calogMain.c -- the `calog` command-line runner.
//
// Links every engine and every library into one binary, then executes the script files named
// on the command line. A file named with a known extension runs on that engine (.lua .js .nut
// .bas .be .scm .wren); a name with no recognized extension is searched for as <name>.<ext> in
// engine-registration priority order (the first existing file wins), matching calogContextLoad.
//
// Each script is launched fire-and-forget on its own context, and the process pumps the host
// thread so scripts can call back into the registered natives (print, the libraries). Because
// calog is event-driven, a script finishing its top level does NOT end the program -- its
// context stays alive to service timer/pubsub/task callbacks -- so there is no way to infer
// "all processing is done". Instead a script calls calogExit([code]) to tear everything down
// and exit; SIGINT/SIGTERM do the same. A script that ERRORS has its context retired (closed);
// once no launched contexts remain live, calog exits on its own. Absent any of these, calog
// runs until killed.
//
// Teardown order matters (see the library headers): the cross-context reference holders
// (export/pubsub/timer/kv) are released while contexts are still alive, THEN calogDestroy joins
// every context thread, THEN the libraries whose registries outlive the runtime (db/net/ssh/
// task) are freed.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogCrypto.h"
#include "calogDb.h"
#include "calogExport.h"
#include "calogFs.h"
#include "calogHttp.h"
#include "calogJson.h"
#include "calogKv.h"
#include "calogNet.h"
#include "calogPubsub.h"
#include "calogSsh.h"
#include "calogTask.h"
#include "calogTime.h"
#include "calogTimer.h"
#include <errno.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
// One host-thread pump iteration parks this long between drains (0.5 ms), matching the test
// harness -- long enough not to spin a core, short enough to feel responsive.
#define PUMP_INTERVAL_NS 500000
// The base for a signal-derived exit code (128 + signal number), the shell convention.
#define SIGNAL_EXIT_BASE 128
static const CalogEngineT *engineForExtension(const char *ext);
static const char *extensionOf(const char *arg);
static bool fileReadable(const char *path);
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void onSignal(int sig);
static void printUsage(FILE *stream, const char *program);
static void printValue(const CalogValueT *value);
static char *readFile(const char *path);
static bool resolveArg(const char *arg, const CalogEngineT **outEngine, char **outSource);
// Requested by a script (calogExit), a signal, or the last live context erroring out; the host
// pump loop watches gShutdown and exits with gExitCode.
static _Atomic bool gShutdown = false;
static _Atomic int32_t gExitCode = 0;
// One entry per script context the runner launched. onError flags `failed` for a context whose
// script errors; the pump loop then closes that context and drops the live count. When the live
// count reaches zero -- every launched context has errored out -- the runner exits.
typedef struct LaunchedT {
uint64_t id;
CalogContextT *context; // NULL once the context has been closed
_Atomic bool failed;
} LaunchedT;
static LaunchedT *gLaunched = NULL;
static int32_t gLaunchedCount = 0;
// The engines the runner knows, in load-search priority order (the same order
// calogRegisterBuiltinEngines uses, so an extensionless name resolves identically here and via
// taskLoad). Referencing the vtables directly pulls each engine archive into the link.
static const CalogEngineT *const gEngines[] = {
&calogLuaEngine,
&calogJsEngine,
&calogSquirrelEngine,
&calogMyBasicEngine,
&calogBerryEngine,
&calogS7Engine,
&calogWrenEngine
};
// s7 interns a small, bounded set of "permanent" strings it never reclaims (an s7 trait, not a
// leak); suppress exactly that site so an ASan/LSan build of the runner stays quiet. LSan calls
// this weak hook automatically.
const char *__lsan_default_suppressions(void);
const char *__lsan_default_suppressions(void) {
return "leak:make_permanent_string\n";
}
// Keep the "Suppressions used" banner off stdout/stderr on a clean run.
const char *__lsan_default_options(void);
const char *__lsan_default_options(void) {
return "print_suppressions=0";
}
// Find the engine that claims file extension ext (case-insensitive), or NULL if none does.
static const CalogEngineT *engineForExtension(const char *ext) {
size_t index;
for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
const CalogEngineT *engine;
int32_t which;
engine = gEngines[index];
if (engine->extensions == NULL) {
continue;
}
for (which = 0; engine->extensions[which] != NULL; which++) {
if (strcasecmp(engine->extensions[which], ext) == 0) {
return engine;
}
}
}
return NULL;
}
// Return a pointer to the extension of arg (the bytes after the last '.' of the final path
// component), or NULL when there is none -- a name with no dot, or a leading-dot dotfile.
static const char *extensionOf(const char *arg) {
const char *base;
const char *slash;
const char *dot;
slash = strrchr(arg, '/');
base = (slash != NULL) ? slash + 1 : arg;
dot = strrchr(base, '.');
if (dot == NULL || dot == base) {
return NULL;
}
return dot + 1;
}
static bool fileReadable(const char *path) {
FILE *file;
file = fopen(path, "rb");
if (file == NULL) {
return false;
}
fclose(file);
return true;
}
// calogExit([code]) -- a script asks the runner to tear everything down and exit. Inline: it
// only sets two atomics, which is thread-safe, so it needs no host hop.
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount >= 1) {
if (args[0].type == calogIntE) {
atomic_store(&gExitCode, (int32_t)args[0].as.i);
} else if (args[0].type == calogRealE) {
atomic_store(&gExitCode, (int32_t)args[0].as.r);
} else {
return calogFail(result, calogErrArgE, "calogExit expects an optional integer exit code");
}
}
atomic_store(&gShutdown, true);
return calogOkE;
}
// calogPrint(...) -- write each argument to stdout, space-separated, with a trailing newline. A
// host native (runs on the host thread), so concurrent scripts' lines do not interleave. Named
// with the calog prefix (like calogExit) so it never collides with an engine's built-in print
// or the my-basic PRINT keyword.
static int32_t nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int32_t index;
(void)userData;
calogValueNil(result);
for (index = 0; index < argCount; index++) {
if (index > 0) {
fputc(' ', stdout);
}
printValue(&args[index]);
}
fputc('\n', stdout);
fflush(stdout);
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
int32_t index;
(void)userData;
fprintf(stderr, "calog: script error: %s\n", (message != NULL) ? message : "(unknown)");
// Flag the failing context so the pump loop retires it (a script error closes its context).
for (index = 0; index < gLaunchedCount; index++) {
if (gLaunched[index].id == contextId) {
atomic_store(&gLaunched[index].failed, true);
break;
}
}
}
// Signal handler for SIGINT/SIGTERM: request the same orderly shutdown as calogExit. Only
// async-signal-safe atomic stores happen here.
static void onSignal(int sig) {
atomic_store(&gExitCode, (int32_t)(SIGNAL_EXIT_BASE + sig));
atomic_store(&gShutdown, true);
}
static void printUsage(FILE *stream, const char *program) {
fprintf(stream, "usage: %s <script> [script ...]\n", program);
fprintf(stream, "\n");
fprintf(stream, "Run each script through the calog multi-language runtime.\n");
fprintf(stream, "A name with a known extension runs on that engine:\n");
fprintf(stream, " .lua .js .nut .bas .be .scm .wren\n");
fprintf(stream, "A name with no recognized extension is searched for as <name>.<ext>\n");
fprintf(stream, "in that priority order; the first existing file wins.\n");
fprintf(stream, "\n");
fprintf(stream, "Scripts share the crypto, db, export, fs, http, json, kv, net, pubsub,\n");
fprintf(stream, "ssh, task, time, and timer libraries plus calogPrint(). Call calogExit([code])\n");
fprintf(stream, "to tear everything down and exit; otherwise calog runs until interrupted.\n");
}
static void printValue(const CalogValueT *value) {
switch (value->type) {
case calogNilE:
fputs("nil", stdout);
break;
case calogBoolE:
fputs(value->as.b ? "true" : "false", stdout);
break;
case calogIntE:
printf("%lld", (long long)value->as.i);
break;
case calogRealE:
printf("%g", value->as.r);
break;
case calogStringE:
fwrite(value->as.s.bytes, 1, (size_t)value->as.s.length, stdout);
break;
case calogAggE:
fputs("[aggregate]", stdout);
break;
case calogFnE:
fputs("[function]", stdout);
break;
default:
break;
}
}
// Read a whole file into a fresh, NUL-terminated buffer (binary content is preserved up to the
// terminator; scripts are text). Returns NULL on failure, leaving errno set.
static char *readFile(const char *path) {
FILE *file;
char *buffer;
long size;
size_t readCount;
file = fopen(path, "rb");
if (file == NULL) {
return NULL;
}
if (fseek(file, 0, SEEK_END) != 0 || (size = ftell(file)) < 0) {
fclose(file);
return NULL;
}
rewind(file);
buffer = (char *)malloc((size_t)size + 1);
if (buffer == NULL) {
fclose(file);
return NULL;
}
readCount = fread(buffer, 1, (size_t)size, file);
fclose(file);
buffer[readCount] = '\0';
return buffer;
}
// Resolve one command-line argument to an engine + its script source (a fresh buffer the caller
// frees). A recognized extension binds the exact file to that engine; otherwise the argument is
// treated as a base name and searched as <arg>.<ext> across the engines in priority order. On
// failure prints a diagnostic and returns false.
static bool resolveArg(const char *arg, const CalogEngineT **outEngine, char **outSource) {
const char *ext;
const CalogEngineT *engine;
char *source;
size_t index;
*outEngine = NULL;
*outSource = NULL;
ext = extensionOf(arg);
if (ext != NULL) {
engine = engineForExtension(ext);
if (engine != NULL) {
source = readFile(arg);
if (source == NULL) {
fprintf(stderr, "calog: %s: %s\n", arg, strerror(errno));
return false;
}
*outEngine = engine;
*outSource = source;
return true;
}
}
for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
int32_t which;
engine = gEngines[index];
if (engine->extensions == NULL) {
continue;
}
for (which = 0; engine->extensions[which] != NULL; which++) {
char *path;
size_t pathSize;
pathSize = strlen(arg) + strlen(engine->extensions[which]) + 2; // '.' + '\0'
path = (char *)malloc(pathSize);
if (path == NULL) {
fprintf(stderr, "calog: out of memory\n");
return false;
}
snprintf(path, pathSize, "%s.%s", arg, engine->extensions[which]);
if (!fileReadable(path)) {
free(path);
continue;
}
source = readFile(path);
if (source == NULL) {
fprintf(stderr, "calog: %s: %s\n", path, strerror(errno));
free(path);
return false;
}
free(path);
*outEngine = engine;
*outSource = source;
return true;
}
}
fprintf(stderr, "calog: %s: no runnable script found\n", arg);
return false;
}
int main(int argc, char **argv) {
const CalogEngineT **engines;
char **sources;
const char *program;
CalogT *calog;
struct timespec tick = { 0, PUMP_INTERVAL_NS };
int32_t scriptCount;
int32_t liveCount;
int32_t index;
program = strrchr(argv[0], '/');
program = (program != NULL) ? program + 1 : argv[0];
if (argc < 2) {
printUsage(stderr, program);
return 2;
}
for (index = 1; index < argc; index++) {
if (strcmp(argv[index], "-h") == 0 || strcmp(argv[index], "--help") == 0) {
printUsage(stdout, program);
return 0;
}
}
scriptCount = argc - 1;
engines = (const CalogEngineT **)malloc((size_t)scriptCount * sizeof(*engines));
sources = (char **)malloc((size_t)scriptCount * sizeof(*sources));
if (engines == NULL || sources == NULL) {
fprintf(stderr, "calog: out of memory\n");
free(engines);
free(sources);
return 1;
}
// Resolve every argument before running any (fail fast): one bad name runs nothing.
for (index = 0; index < scriptCount; index++) {
if (!resolveArg(argv[index + 1], &engines[index], &sources[index])) {
int32_t undo;
for (undo = 0; undo < index; undo++) {
free(sources[undo]);
}
free(engines);
free(sources);
return 1;
}
}
calog = calogCreate();
if (calog == NULL) {
fprintf(stderr, "calog: failed to create runtime\n");
for (index = 0; index < scriptCount; index++) {
free(sources[index]);
}
free(engines);
free(sources);
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
if (calogRegister(calog, "calogPrint", nativeCalogPrint, NULL) != calogOkE ||
calogRegisterInline(calog, "calogExit", nativeCalogExit, NULL) != calogOkE ||
calogCryptoRegister(calog) != calogOkE ||
calogDbRegister(calog) != calogOkE ||
calogExportRegister(calog) != calogOkE ||
calogFsRegister(calog) != calogOkE ||
calogHttpRegister(calog) != calogOkE ||
calogJsonRegister(calog) != calogOkE ||
calogKvRegister(calog) != calogOkE ||
calogNetRegister(calog) != calogOkE ||
calogPubsubRegister(calog) != calogOkE ||
calogSshRegister(calog) != calogOkE ||
calogTaskRegister(calog) != calogOkE ||
calogTimeRegister(calog) != calogOkE ||
calogTimerRegister(calog) != calogOkE) {
fprintf(stderr, "calog: failed to register libraries\n");
calogDestroy(calog);
for (index = 0; index < scriptCount; index++) {
free(sources[index]);
}
free(engines);
free(sources);
return 1;
}
calogRegisterBuiltinEngines(calog);
signal(SIGINT, onSignal);
signal(SIGTERM, onSignal);
// Launch every script fire-and-forget on its own context, recording the live ones so a
// later error can retire exactly that context. calogContextEval keeps its own copy of the
// source, so the resolved buffer is freed right after.
gLaunched = (LaunchedT *)calloc((size_t)scriptCount, sizeof(*gLaunched));
if (gLaunched == NULL) {
fprintf(stderr, "calog: out of memory\n");
calogDestroy(calog);
for (index = 0; index < scriptCount; index++) {
free(sources[index]);
}
free(engines);
free(sources);
return 1;
}
for (index = 0; index < scriptCount; index++) {
CalogContextT *context;
context = calogContextOpen(calog, engines[index]);
if (context == NULL) {
fprintf(stderr, "calog: %s: failed to open a context\n", argv[index + 1]);
} else if (calogContextEval(context, sources[index]) != calogOkE) {
fprintf(stderr, "calog: %s: failed to start script\n", argv[index + 1]);
calogContextClose(context);
} else {
gLaunched[gLaunchedCount].id = calogContextId(context);
gLaunched[gLaunchedCount].context = context;
atomic_store(&gLaunched[gLaunchedCount].failed, false);
gLaunchedCount++;
}
free(sources[index]);
}
free(engines);
free(sources);
liveCount = gLaunchedCount;
if (liveCount == 0) {
atomic_store(&gShutdown, true); // nothing runnable launched
}
// Service script->native calls until a script calls calogExit, we are signalled, or every
// launched context has errored out. A script error retires (closes) its own context; when
// the last live context is gone, the runner exits.
while (!atomic_load(&gShutdown)) {
calogPump(calog);
for (index = 0; index < gLaunchedCount; index++) {
if (gLaunched[index].context != NULL && atomic_load(&gLaunched[index].failed)) {
int32_t expected;
calogContextClose(gLaunched[index].context);
gLaunched[index].context = NULL;
liveCount--;
expected = 0;
atomic_compare_exchange_strong(&gExitCode, &expected, 1);
}
}
if (liveCount == 0) {
atomic_store(&gShutdown, true);
break;
}
nanosleep(&tick, NULL);
}
calogPump(calog);
// Release cross-context references while the contexts are still alive, tear the runtime
// down (joining every context thread), then free the registries that outlive it.
calogExportShutdown();
calogPubsubShutdown();
calogTimerShutdown();
calogKvShutdown();
calogDestroy(calog);
calogDbShutdown();
calogNetShutdown();
calogSshShutdown();
calogTaskShutdown();
free(gLaunched);
return (int)atomic_load(&gExitCode);
}

View file

@ -8,7 +8,7 @@
// currentL so an exported BASIC routine can be evaluated through mb_eval_routine. // currentL so an exported BASIC routine can be evaluated through mb_eval_routine.
#include "mybasicAdapter.h" #include "mybasicAdapter.h"
#include "myBasic.h" #include "ourBasic.h"
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
@ -423,16 +423,13 @@ static int32_t mbCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT
routine = (MyBasicRoutineT *)userData; routine = (MyBasicRoutineT *)userData;
context = routine->context; context = routine->context;
calogValueNil(result); calogValueNil(result);
if (context->currentL == NULL) { // Always allocate a non-NULL args buffer (at least one slot): mb_eval_routine treats a NULL
return calogFail(result, calogErrUnsupportedE, "BASIC routine invoked outside a serving frame"); // args pointer as "parse the arguments from source", which fails for a cold / no-source call
} // -- so even a zero-argument callback needs a non-NULL (direct-args) buffer.
mbArgs = NULL; mbArgs = (mb_value_t *)calloc((size_t)(argCount > 0 ? argCount : 1), sizeof(mb_value_t));
if (argCount > 0) {
mbArgs = (mb_value_t *)calloc((size_t)argCount, sizeof(mb_value_t));
if (mbArgs == NULL) { if (mbArgs == NULL) {
return calogFail(result, calogErrOomE, "out of memory marshalling routine args"); return calogFail(result, calogErrOomE, "out of memory marshalling routine args");
} }
}
for (index = 0; index < argCount; index++) { for (index = 0; index < argCount; index++) {
mb_make_nil(mbArgs[index]); mb_make_nil(mbArgs[index]);
status = mbFromValueDepth(context, context->currentL, &args[index], &mbArgs[index], 0); status = mbFromValueDepth(context, context->currentL, &args[index], &mbArgs[index], 0);
@ -446,7 +443,15 @@ static int32_t mbCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT
} }
} }
mb_make_nil(ret); mb_make_nil(ret);
if (context->currentL != NULL) {
// Synchronous: invoked while this context is serving a native call (e.g. an exported
// routine reached via calogMyBasicExportRoutine) -- run it in that live frame.
code = mb_eval_routine(context->bas, context->currentL, routine->routine, mbArgs, (unsigned)argCount, &ret); code = mb_eval_routine(context->bas, context->currentL, routine->routine, mbArgs, (unsigned)argCount, &ret);
} else {
// [calog fork] Deferred callback (timer/pubsub/export delivered to an idle interpreter):
// invoke the routine cold -- mb_eval_routine_cold supplies its own return frame.
code = mb_eval_routine_cold(context->bas, routine->routine, mbArgs, (unsigned)argCount, &ret);
}
if (code == MB_FUNC_OK) { if (code == MB_FUNC_OK) {
// Marshal the return value BEFORE releasing the arguments: a routine may // Marshal the return value BEFORE releasing the arguments: a routine may
// return one of its (borrowed) arguments, so result must be deep-copied // return one of its (borrowed) arguments, so result must be deep-copied
@ -586,6 +591,12 @@ static int mbDispatch(int32_t slot, struct mb_interpreter_t *s, void **l) {
mb_dispose_value(context->bas, popped); mb_dispose_value(context->bas, popped);
} }
if (status != calogOkE) { if (status != calogOkE) {
// A refused routine argument (e.g. a lambda created just for this call) was popped
// and is ours to free; dispose it so its scope is not leaked on the error path.
// (Strings are borrowed interior pointers and collections are disposed just above.)
if (popped.type == MB_DT_ROUTINE) {
mb_dispose_value(context->bas, popped);
}
code = mb_raise_error(s, l, MB_NATIVE_ERROR, MB_FUNC_ERR); code = mb_raise_error(s, l, MB_NATIVE_ERROR, MB_FUNC_ERR);
goto cleanupArgs; goto cleanupArgs;
} }
@ -767,6 +778,12 @@ static int32_t mbToValueDepth(CalogMyBasicT *context, void **l, const mb_value_t
case MB_DT_DICT: case MB_DT_DICT:
return mbDictToAggregate(context, l, *value, out, depth); return mbDictToAggregate(context, l, *value, out, depth);
case MB_DT_ROUTINE: case MB_DT_ROUTINE:
// [calog fork] Marshal a def/lambda routine to a first-class calog callable, so a
// BASIC script can hand a callback to a native (pubsub/timer/export) like any other
// engine. The routine value is borrowed (owned by the interpreter's scope) and is
// invoked later via mb_eval_routine_cold, so it must have a PERSISTENT scope -- a
// top-level def or lambda. (A routine local to a function would dangle once that
// function returns; callbacks are registered from the top level, as in every engine.)
status = mbWrapRoutine(context, *value, &callable); status = mbWrapRoutine(context, *value, &callable);
if (status != calogOkE) { if (status != calogOkE) {
return status; return status;

View file

@ -2,10 +2,12 @@
// vtable, so a my-basic interpreter runs on its own context thread and is loadable by // vtable, so a my-basic interpreter runs on its own context thread and is loadable by
// file extension (.bas) via calogContextLoad. // file extension (.bas) via calogContextLoad.
// //
// my-basic keeps a little process-global state, but only two things are shared across // The engine is vendor/ourbasic -- calog's fork of MY-BASIC (see vendor/ourbasic/NOTICE +
// interpreters: an allocation counter (patched to _Atomic in vendor/mybasic/myBasic.c, // CHANGELOG; pristine upstream kept as ourBasic.c.upstream). my-basic keeps a little
// preserved as myBasic.c.orig) and the mb_init singletons, which are built once and are // process-global state, but only two things are shared across interpreters: an allocation
// read-only thereafter. So interpreters EXECUTE in parallel; only lifecycle needs // counter (patched to _Atomic in vendor/ourbasic/ourBasic.c) and the mb_init singletons,
// which are built once and are read-only thereafter. So interpreters EXECUTE in parallel;
// only lifecycle needs
// serializing -- mb_init's lazy build on the first context, mb_dispose on the last, and // serializing -- mb_init's lazy build on the first context, mb_dispose on the last, and
// the adapter's shared context refcount. This lock covers just create and destroy; // the adapter's shared context refcount. This lock covers just create and destroy;
// runSource runs unlocked, so several my-basic scripts run at once. // runSource runs unlocked, so several my-basic scripts run at once.

40
vendor/ourbasic/CHANGELOG vendored Normal file
View file

@ -0,0 +1,40 @@
our-basic CHANGELOG -- calog's modifications to upstream MY-BASIC
================================================================
Every change below is marked in-source with the comment tag "[calog fork]".
Baseline: ourBasic.c.upstream (pristine upstream MY-BASIC). Run
diff ourBasic.c.upstream ourBasic.c
to see the full patch set.
Concurrency
- The _mb_allocated allocation counter was changed from `volatile` to `_Atomic`,
so independent interpreters (one per calog context thread) can be created and
destroyed concurrently without racing the counter. Execution already runs
unlocked; only lifecycle is serialized, in the adapter.
Memory
- A my-basic routine (lambda) popped as a refused/errored native argument is now
disposed on the error path so its scope is not leaked. (In the adapter's
arg-marshal loop, src/mybasic/mybasicAdapter.c.)
First-class routines -- the reason for the fork
- Bare-def-as-value: in _calc_expression, a routine identifier NOT followed by
'(' now pushes the routine as an operand (a first-class value) instead of
raising "Open bracket expected". A script can now pass a `def` (or a lambda) to
a native and store it in a variable -- required for cross-engine callbacks
(pubsub / timer / export). It mirrors the array-index branch beside it; calls,
recursion, and closures are unchanged.
- mb_eval_routine_cold(): a new public entry (declared in ourBasic.h) that invokes
a routine value from an IDLE interpreter -- one with no live call frame. It
supplies the last AST node as a clean return landing and passes arguments
directly. calog uses it to fire script callbacks that were registered earlier
and delivered when the interpreter is otherwise idle.
Constraint: a callback must be a TOP-LEVEL def/lambda (persistent scope); a routine
local to a function would dangle once that function returns -- the same rule every
engine's closures follow.
Verified: full calog `make test` (28 binaries, 0 failed) plus the my-basic engine
suite (testMyBasic / testEngineMyBasic / testPolyglot / testLoad / testTask),
ASan/UBSan-clean, with def and lambda callbacks firing via timer/pubsub/export and
cross-engine (a Lua task calling a my-basic def).

45
vendor/ourbasic/NOTICE vendored Normal file
View file

@ -0,0 +1,45 @@
our-basic -- calog's fork of MY-BASIC
=====================================
This directory (vendor/ourbasic) is calog's vendored fork of MY-BASIC, a small
embeddable BASIC interpreter by Tony Wang.
Upstream: MY-BASIC -- https://github.com/paladin-t/my_basic/
License: MIT (the permission notice is reproduced in the header of every
source file: ourBasic.c and ourBasic.h)
Copyright (C) 2011 - 2026 Tony Wang
MY-BASIC is MIT-licensed, which permits this fork. calog's modifications are marked
in-source with the comment tag "[calog fork]" and are catalogued in CHANGELOG (this
directory).
ourBasic.c the forked interpreter (patched)
ourBasic.h the forked public header (patched)
ourBasic.c.upstream the pristine upstream MY-BASIC source, kept as the diff
baseline: `diff ourBasic.c.upstream ourBasic.c` shows the fork
The internal C API keeps its upstream "mb_" prefix (renaming it would be pointless
churn). The BASIC dialect calog exposes to scripts is still called "my-basic" -- the
".bas" extension, the "mybasic" engine name, and CALOG_WITH_MYBASIC are unchanged.
"our-basic" names only this modified implementation.
--- MIT License (MY-BASIC) ---
Copyright (C) 2011 - 2026 Tony Wang
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -29,7 +29,7 @@
# endif /* _CRT_SECURE_NO_WARNINGS */ # endif /* _CRT_SECURE_NO_WARNINGS */
#endif /* _MSC_VER */ #endif /* _MSC_VER */
#include "myBasic.h" #include "ourBasic.h"
#if defined ARDUINO && !defined MB_CP_ARDUINO #if defined ARDUINO && !defined MB_CP_ARDUINO
# define MB_CP_ARDUINO # define MB_CP_ARDUINO
#endif /* ARDUINO && !MB_CP_ARDUINO */ #endif /* ARDUINO && !MB_CP_ARDUINO */
@ -3980,6 +3980,16 @@ _array:
_ls_pushback(opnd, c); _ls_pushback(opnd, c);
f++; f++;
} else if(c->type == _DT_ROUTINE) { } else if(c->type == _DT_ROUTINE) {
/* [calog fork] A routine identifier NOT followed by '(' is a first-class value
reference (like the array case below), not a call -- push it as an operand so
scripts can pass def/lambda routines to natives and store them in variables. */
if(ast && !_IS_FUNC((_object_t*)ast->data, _core_open_bracket)) {
if(f) {
_handle_error_on_obj(s, SE_RN_OPERATOR_EXPECTED, s->source_file, DON(ast), MB_FUNC_ERR, _error, result);
}
_ls_pushback(opnd, c);
f++;
} else {
_routine: _routine:
do { do {
#ifdef MB_ENABLE_CLASS #ifdef MB_ENABLE_CLASS
@ -4031,6 +4041,7 @@ _routine:
} }
_ls_pushback(opnd, c); _ls_pushback(opnd, c);
f++; f++;
}
} else if(c->type == _DT_VAR && c->data.variable->data->type == _DT_ARRAY) { } else if(c->type == _DT_VAR && c->data.variable->data->type == _DT_ARRAY) {
unsigned arr_idx = 0; unsigned arr_idx = 0;
mb_value_u arr_val; mb_value_u arr_val;
@ -19508,3 +19519,17 @@ _exit:
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif /* __cplusplus */ #endif /* __cplusplus */
/* [calog fork] Invoke a routine value from an IDLE interpreter (no live call frame).
Supplies the last AST node as a clean return landing so RETURN/ENDDEF have a valid
return point, and passes args directly. Used by calog to fire script callbacks
(pubsub/timer/export) that were registered earlier. See myBasic.h. */
int mb_eval_routine_cold(struct mb_interpreter_t* s, mb_value_t routine, mb_value_t* args, unsigned argc, mb_value_t* ret) {
_ls_node_t* n;
void* land;
if(!s) return MB_FUNC_ERR;
n = s->ast;
while(n && n->next) n = n->next;
land = n;
return mb_eval_routine(s, &land, routine, args, argc, ret);
}

View file

@ -23,8 +23,8 @@
** CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ** CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
#ifndef __MY_BASIC_H__ #ifndef __OUR_BASIC_H__
#define __MY_BASIC_H__ #define __OUR_BASIC_H__
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -663,6 +663,8 @@ MBAPI int mb_dispose_value(struct mb_interpreter_t* s, mb_value_t val);
MBAPI int mb_get_routine(struct mb_interpreter_t* s, void** l, const char* n, mb_value_t* val); MBAPI int mb_get_routine(struct mb_interpreter_t* s, void** l, const char* n, mb_value_t* val);
MBAPI int mb_set_routine(struct mb_interpreter_t* s, void** l, const char* n, mb_routine_func_t f, bool_t force); MBAPI int mb_set_routine(struct mb_interpreter_t* s, void** l, const char* n, mb_routine_func_t f, bool_t force);
MBAPI int mb_eval_routine(struct mb_interpreter_t* s, void** l, mb_value_t val, mb_value_t* args, unsigned argc, mb_value_t* ret/* = NULL*/); MBAPI int mb_eval_routine(struct mb_interpreter_t* s, void** l, mb_value_t val, mb_value_t* args, unsigned argc, mb_value_t* ret/* = NULL*/);
/* [calog fork] Invoke a routine value from an IDLE interpreter (no live call frame). */
MBAPI int mb_eval_routine_cold(struct mb_interpreter_t* s, mb_value_t routine, mb_value_t* args, unsigned argc, mb_value_t* ret);
MBAPI int mb_get_routine_type(struct mb_interpreter_t* s, mb_value_t val, mb_routine_type_e* y); MBAPI int mb_get_routine_type(struct mb_interpreter_t* s, mb_value_t val, mb_routine_type_e* y);
MBAPI int mb_load_string(struct mb_interpreter_t* s, const char* l, bool_t reset/* = true*/); MBAPI int mb_load_string(struct mb_interpreter_t* s, const char* l, bool_t reset/* = true*/);
@ -706,4 +708,4 @@ MBAPI char* mb_memdup(const char* val, unsigned size);
} }
#endif /* __cplusplus */ #endif /* __cplusplus */
#endif /* __MY_BASIC_H__ */ #endif /* __OUR_BASIC_H__ */