From b5cf00b0145b891e97ae751dd41bdf1814b416ad Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 3 Jul 2026 20:00:29 -0500 Subject: [PATCH] Initial feature set complete. --- API.md | 219 +++++++ LICENSE.md | 91 ++- Makefile | 37 +- README.md | 26 +- design.md | 22 +- examples/scripts/README.md | 90 +++ examples/scripts/apps/hashText.lua | 19 + examples/scripts/apps/uuidGen.lua | 8 + examples/scripts/apps/wordCount.js | 43 ++ examples/scripts/languages/berry.be | 42 ++ examples/scripts/languages/javascript.js | 38 ++ examples/scripts/languages/lua.lua | 43 ++ examples/scripts/languages/mybasic.bas | 38 ++ examples/scripts/languages/scheme.scm | 36 ++ examples/scripts/languages/squirrel.nut | 58 ++ examples/scripts/languages/wren.wren | 37 ++ examples/scripts/libraries/crypto.lua | 20 + examples/scripts/libraries/cryptoInJs.js | 19 + examples/scripts/libraries/database.lua | 39 ++ examples/scripts/libraries/export.lua | 26 + examples/scripts/libraries/fs.lua | 34 ++ examples/scripts/libraries/http.lua | 50 ++ examples/scripts/libraries/json.lua | 35 ++ examples/scripts/libraries/jsonInBerry.be | 26 + examples/scripts/libraries/kv.lua | 33 ++ examples/scripts/libraries/kvInScheme.scm | 22 + examples/scripts/libraries/net.lua | 35 ++ examples/scripts/libraries/pubsub.lua | 25 + examples/scripts/libraries/ssh.lua | 57 ++ examples/scripts/libraries/task.lua | 21 + examples/scripts/libraries/time.lua | 15 + examples/scripts/libraries/timer.lua | 20 + examples/scripts/libraries/timerInMyBasic.bas | 20 + examples/scripts/multifile/consumer.lua | 16 + examples/scripts/multifile/producer.js | 18 + .../scripts/polyglot/exportAcrossEngines.lua | 26 + .../scripts/polyglot/pubsubAcrossEngines.lua | 29 + examples/scripts/polyglot/sharedKv.lua | 30 + examples/scripts/polyglot/taskFanout.lua | 34 ++ libs/calogNet.c | 14 - libs/calogNet.h | 22 +- src/calogMain.c | 534 ++++++++++++++++++ src/mybasic/mybasicAdapter.c | 41 +- src/mybasic/mybasicEngine.c | 10 +- vendor/ourbasic/CHANGELOG | 40 ++ vendor/ourbasic/NOTICE | 45 ++ .../myBasic.c => ourbasic/ourBasic.c} | 27 +- .../ourBasic.c.upstream} | 0 .../myBasic.h => ourbasic/ourBasic.h} | 8 +- 49 files changed, 2155 insertions(+), 83 deletions(-) create mode 100644 API.md create mode 100644 examples/scripts/README.md create mode 100644 examples/scripts/apps/hashText.lua create mode 100644 examples/scripts/apps/uuidGen.lua create mode 100644 examples/scripts/apps/wordCount.js create mode 100644 examples/scripts/languages/berry.be create mode 100644 examples/scripts/languages/javascript.js create mode 100644 examples/scripts/languages/lua.lua create mode 100644 examples/scripts/languages/mybasic.bas create mode 100644 examples/scripts/languages/scheme.scm create mode 100644 examples/scripts/languages/squirrel.nut create mode 100644 examples/scripts/languages/wren.wren create mode 100644 examples/scripts/libraries/crypto.lua create mode 100644 examples/scripts/libraries/cryptoInJs.js create mode 100644 examples/scripts/libraries/database.lua create mode 100644 examples/scripts/libraries/export.lua create mode 100644 examples/scripts/libraries/fs.lua create mode 100644 examples/scripts/libraries/http.lua create mode 100644 examples/scripts/libraries/json.lua create mode 100644 examples/scripts/libraries/jsonInBerry.be create mode 100644 examples/scripts/libraries/kv.lua create mode 100644 examples/scripts/libraries/kvInScheme.scm create mode 100644 examples/scripts/libraries/net.lua create mode 100644 examples/scripts/libraries/pubsub.lua create mode 100644 examples/scripts/libraries/ssh.lua create mode 100644 examples/scripts/libraries/task.lua create mode 100644 examples/scripts/libraries/time.lua create mode 100644 examples/scripts/libraries/timer.lua create mode 100644 examples/scripts/libraries/timerInMyBasic.bas create mode 100644 examples/scripts/multifile/consumer.lua create mode 100644 examples/scripts/multifile/producer.js create mode 100644 examples/scripts/polyglot/exportAcrossEngines.lua create mode 100644 examples/scripts/polyglot/pubsubAcrossEngines.lua create mode 100644 examples/scripts/polyglot/sharedKv.lua create mode 100644 examples/scripts/polyglot/taskFanout.lua create mode 100644 src/calogMain.c create mode 100644 vendor/ourbasic/CHANGELOG create mode 100644 vendor/ourbasic/NOTICE rename vendor/{mybasic/myBasic.c => ourbasic/ourBasic.c} (99%) rename vendor/{mybasic/myBasic.c.orig => ourbasic/ourBasic.c.upstream} (100%) rename vendor/{mybasic/myBasic.h => ourbasic/ourBasic.h} (98%) diff --git a/API.md b/API.md new file mode 100644 index 0000000..7622fc2 --- /dev/null +++ b/API.md @@ -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. | diff --git a/LICENSE.md b/LICENSE.md index d6511d9..4ad89ae 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -28,38 +28,79 @@ SOFTWARE. ## Third-party components -calog vendors the following scripting engines under `vendor/`, built from -source. Each is distributed under its own license (MIT unless noted), which -applies to its files; the full text ships alongside each engine's source. +calog vendors its dependencies under `vendor/`, each built from source. Every +dependency is distributed under its own license, and the full license text +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. - `vendor/lua/` (see `vendor/lua/README` and the notice in `src/lua.h`). +### Scripting engines -- **QuickJS-ng** (JavaScript) — Copyright (c) 2017-2024 Fabrice Bellard, Charlie - Gordon, and QuickJS-ng contributors. MIT License. `vendor/quickjs/quickjs.h`. +- **Lua 5.4** -- MIT License. Copyright (C) 1994-2023 Lua.org, PUC-Rio. + `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`. -- **MY-BASIC** — Copyright (C) 2011-2026 Tony Wang. MIT License. Notice in - `vendor/mybasic/myBasic.h`. +- **our-basic** -- calog's fork of MY-BASIC. MIT License. Copyright (C) + 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 - > global allocation counter `_Atomic` so interpreters are thread-safe); the - > unmodified original is preserved as `vendor/mybasic/myBasic.c.orig`. +- **s7 Scheme** -- BSD Zero-Clause License (0BSD; `SPDX-License-Identifier: 0BSD`). + Copyright (c) Bill Schottstaedt; derived from TinyScheme 1.39. `vendor/s7/s7.c`. -- **s7 Scheme** — Copyright (c) Bill Schottstaedt and s7 contributors. BSD-style - license (TinyScheme lineage: use for any purpose, no royalty). Notice in - `vendor/s7/s7.c`. +- **Wren 0.4.0** -- MIT License. Copyright (c) 2013-2021 Robert Nystrom and Wren + Contributors. Notice in `vendor/wren/wren.c` and `vendor/wren/wren.h`. -- **Wren** — Copyright (c) 2013-2021 Robert Nystrom and Wren Contributors. MIT - License. `vendor/wren/LICENSE` (amalgamated into `vendor/wren/wren.c`). + > Note: `vendor/wren/wren.c` and `wren.h` carry one small calog patch adding a + > 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 - > 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`). +- **Berry** -- MIT License. Copyright (c) 2018-2022 Guan Wenliang and Berry + contributors. Notice in `vendor/berry/src/berry.h`. -- **Berry** — Copyright (c) 2018-2026 Guan Wenliang and Berry contributors. MIT - License. `vendor/berry/` (see the upstream `LICENSE`). +### Database, network, and cryptography libraries + +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. diff --git a/Makefile b/Makefile index a6fb490..565b448 100644 --- a/Makefile +++ b/Makefile @@ -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 -# --- vendored my-basic (C) --- -MBDIR = vendor/mybasic +# --- vendored our-basic: calog's fork of MY-BASIC (see vendor/ourbasic/NOTICE) --- +MBDIR = vendor/ourbasic MBINC = -I$(MBDIR) 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 \ 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) @@ -215,7 +215,7 @@ $(DBADP): obj/%.o: %.c | obj # always enabled -- unlike the DB clients, ENet needs no server) NETADP = obj/calogNet.o $(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 # 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 $(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 obj/testPolyglot.o: testPolyglot.c | obj $(CC) $(ADPFLAGS) $(INC) $(LUAINC) $(MBINC) -DMB_DOUBLE_FLOAT -c -o $@ $< # ---- 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 $@ $< obj/%.o: $(LUADIR)/src/%.c | obj @@ -277,7 +282,7 @@ lib/libquickjs.a: $(QJSOBJ) | lib ar rcs $@ $^ lib/libsquirrel.a: $(SQOBJ) | lib ar rcs $@ $^ -lib/libmybasic.a: obj/myBasic.o | lib +lib/libmybasic.a: obj/ourBasic.o | lib ar rcs $@ $^ lib/libberry.a: $(BERRYOBJ) | lib 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 $(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 -------------------------------------------------------------- # 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 @@ -390,7 +407,7 @@ obj/rel: mkdir -p 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 $(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 # across the whole stack -- it races without the engine lock. 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 \ 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 - 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 # TSan (throwaway objs) so races are caught across the whole stack. diff --git a/README.md b/README.md index c816097..8125114 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`, | Lua 5.4 | `calogLuaEngine` | `.lua` | | | JavaScript | `calogJsEngine` | `.js` | QuickJS-ng (ES2023+, BigInt → int64) | | 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 | | Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call(…)` | | 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 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 -value also crosses *into* a script on every engine but MY-BASIC (which has no first-class -callable values). +value also crosses *into* a script on every engine but MY-BASIC (whose value model has no +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. @@ -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 ```c diff --git a/design.md b/design.md index 65c8746..62b5a43 100644 --- a/design.md +++ b/design.md @@ -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 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 `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 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, -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 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 @@ -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 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 -`_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 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 @@ -972,7 +985,10 @@ Per engine: `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 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. Added: diff --git a/examples/scripts/README.md b/examples/scripts/README.md new file mode 100644 index 0000000..778b1e8 --- /dev/null +++ b/examples/scripts/README.md @@ -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 | diff --git a/examples/scripts/apps/hashText.lua b/examples/scripts/apps/hashText.lua new file mode 100644 index 0000000..07f5070 --- /dev/null +++ b/examples/scripts/apps/hashText.lua @@ -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) diff --git a/examples/scripts/apps/uuidGen.lua b/examples/scripts/apps/uuidGen.lua new file mode 100644 index 0000000..6af219b --- /dev/null +++ b/examples/scripts/apps/uuidGen.lua @@ -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) diff --git a/examples/scripts/apps/wordCount.js b/examples/scripts/apps/wordCount.js new file mode 100644 index 0000000..446d1dd --- /dev/null +++ b/examples/scripts/apps/wordCount.js @@ -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); diff --git a/examples/scripts/languages/berry.be b/examples/scripts/languages/berry.be new file mode 100644 index 0000000..21af478 --- /dev/null +++ b/examples/scripts/languages/berry.be @@ -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) diff --git a/examples/scripts/languages/javascript.js b/examples/scripts/languages/javascript.js new file mode 100644 index 0000000..3c51b31 --- /dev/null +++ b/examples/scripts/languages/javascript.js @@ -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); diff --git a/examples/scripts/languages/lua.lua b/examples/scripts/languages/lua.lua new file mode 100644 index 0000000..c923b7b --- /dev/null +++ b/examples/scripts/languages/lua.lua @@ -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) diff --git a/examples/scripts/languages/mybasic.bas b/examples/scripts/languages/mybasic.bas new file mode 100644 index 0000000..4b2fb6c --- /dev/null +++ b/examples/scripts/languages/mybasic.bas @@ -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) diff --git a/examples/scripts/languages/scheme.scm b/examples/scripts/languages/scheme.scm new file mode 100644 index 0000000..2868e25 --- /dev/null +++ b/examples/scripts/languages/scheme.scm @@ -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)) diff --git a/examples/scripts/languages/squirrel.nut b/examples/scripts/languages/squirrel.nut new file mode 100644 index 0000000..c92dc1c --- /dev/null +++ b/examples/scripts/languages/squirrel.nut @@ -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) diff --git a/examples/scripts/languages/wren.wren b/examples/scripts/languages/wren.wren new file mode 100644 index 0000000..9bca381 --- /dev/null +++ b/examples/scripts/languages/wren.wren @@ -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]) diff --git a/examples/scripts/libraries/crypto.lua b/examples/scripts/libraries/crypto.lua new file mode 100644 index 0000000..74085d9 --- /dev/null +++ b/examples/scripts/libraries/crypto.lua @@ -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) diff --git a/examples/scripts/libraries/cryptoInJs.js b/examples/scripts/libraries/cryptoInJs.js new file mode 100644 index 0000000..19a797f --- /dev/null +++ b/examples/scripts/libraries/cryptoInJs.js @@ -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); diff --git a/examples/scripts/libraries/database.lua b/examples/scripts/libraries/database.lua new file mode 100644 index 0000000..f5daf8a --- /dev/null +++ b/examples/scripts/libraries/database.lua @@ -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) diff --git a/examples/scripts/libraries/export.lua b/examples/scripts/libraries/export.lua new file mode 100644 index 0000000..bc8a5f8 --- /dev/null +++ b/examples/scripts/libraries/export.lua @@ -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) diff --git a/examples/scripts/libraries/fs.lua b/examples/scripts/libraries/fs.lua new file mode 100644 index 0000000..e204382 --- /dev/null +++ b/examples/scripts/libraries/fs.lua @@ -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) diff --git a/examples/scripts/libraries/http.lua b/examples/scripts/libraries/http.lua new file mode 100644 index 0000000..71496f4 --- /dev/null +++ b/examples/scripts/libraries/http.lua @@ -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) diff --git a/examples/scripts/libraries/json.lua b/examples/scripts/libraries/json.lua new file mode 100644 index 0000000..27eef82 --- /dev/null +++ b/examples/scripts/libraries/json.lua @@ -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) diff --git a/examples/scripts/libraries/jsonInBerry.be b/examples/scripts/libraries/jsonInBerry.be new file mode 100644 index 0000000..4c512e8 --- /dev/null +++ b/examples/scripts/libraries/jsonInBerry.be @@ -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) diff --git a/examples/scripts/libraries/kv.lua b/examples/scripts/libraries/kv.lua new file mode 100644 index 0000000..4e3c251 --- /dev/null +++ b/examples/scripts/libraries/kv.lua @@ -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) diff --git a/examples/scripts/libraries/kvInScheme.scm b/examples/scripts/libraries/kvInScheme.scm new file mode 100644 index 0000000..2574045 --- /dev/null +++ b/examples/scripts/libraries/kvInScheme.scm @@ -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)) diff --git a/examples/scripts/libraries/net.lua b/examples/scripts/libraries/net.lua new file mode 100644 index 0000000..898656e --- /dev/null +++ b/examples/scripts/libraries/net.lua @@ -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) diff --git a/examples/scripts/libraries/pubsub.lua b/examples/scripts/libraries/pubsub.lua new file mode 100644 index 0000000..347dd2a --- /dev/null +++ b/examples/scripts/libraries/pubsub.lua @@ -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) diff --git a/examples/scripts/libraries/ssh.lua b/examples/scripts/libraries/ssh.lua new file mode 100644 index 0000000..8bd1c93 --- /dev/null +++ b/examples/scripts/libraries/ssh.lua @@ -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) diff --git a/examples/scripts/libraries/task.lua b/examples/scripts/libraries/task.lua new file mode 100644 index 0000000..e832206 --- /dev/null +++ b/examples/scripts/libraries/task.lua @@ -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) diff --git a/examples/scripts/libraries/time.lua b/examples/scripts/libraries/time.lua new file mode 100644 index 0000000..213fcd9 --- /dev/null +++ b/examples/scripts/libraries/time.lua @@ -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) diff --git a/examples/scripts/libraries/timer.lua b/examples/scripts/libraries/timer.lua new file mode 100644 index 0000000..6a631a4 --- /dev/null +++ b/examples/scripts/libraries/timer.lua @@ -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) diff --git a/examples/scripts/libraries/timerInMyBasic.bas b/examples/scripts/libraries/timerInMyBasic.bas new file mode 100644 index 0000000..6a9d35c --- /dev/null +++ b/examples/scripts/libraries/timerInMyBasic.bas @@ -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") diff --git a/examples/scripts/multifile/consumer.lua b/examples/scripts/multifile/consumer.lua new file mode 100644 index 0000000..8733148 --- /dev/null +++ b/examples/scripts/multifile/consumer.lua @@ -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") diff --git a/examples/scripts/multifile/producer.js b/examples/scripts/multifile/producer.js new file mode 100644 index 0000000..96ef9c8 --- /dev/null +++ b/examples/scripts/multifile/producer.js @@ -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"); +}); diff --git a/examples/scripts/polyglot/exportAcrossEngines.lua b/examples/scripts/polyglot/exportAcrossEngines.lua new file mode 100644 index 0000000..2201153 --- /dev/null +++ b/examples/scripts/polyglot/exportAcrossEngines.lua @@ -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) diff --git a/examples/scripts/polyglot/pubsubAcrossEngines.lua b/examples/scripts/polyglot/pubsubAcrossEngines.lua new file mode 100644 index 0000000..36eafc3 --- /dev/null +++ b/examples/scripts/polyglot/pubsubAcrossEngines.lua @@ -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) diff --git a/examples/scripts/polyglot/sharedKv.lua b/examples/scripts/polyglot/sharedKv.lua new file mode 100644 index 0000000..ff19218 --- /dev/null +++ b/examples/scripts/polyglot/sharedKv.lua @@ -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) diff --git a/examples/scripts/polyglot/taskFanout.lua b/examples/scripts/polyglot/taskFanout.lua new file mode 100644 index 0000000..1446856 --- /dev/null +++ b/examples/scripts/polyglot/taskFanout.lua @@ -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) diff --git a/libs/calogNet.c b/libs/calogNet.c index 0adb317..1eb9234 100644 --- a/libs/calogNet.c +++ b/libs/calogNet.c @@ -20,9 +20,7 @@ #include #include -#ifdef CALOG_WITH_ENET #include -#endif // 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). @@ -51,14 +49,12 @@ typedef struct NetLibT { static pthread_mutex_t gNetLibMutex = PTHREAD_MUTEX_INITIALIZER; static NetLibT *gNetLib = NULL; -#ifdef CALOG_WITH_ENET 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 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 enetSend(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 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); @@ -92,14 +88,12 @@ int32_t calogNetRegister(CalogT *calog) { pthread_mutex_unlock(&gNetLibMutex); return calogErrOomE; } -#ifdef CALOG_WITH_ENET if (enet_initialize() != 0) { calogHandleTableDestroy(lib->handles, NULL); free(lib); pthread_mutex_unlock(&gNetLibMutex); return calogErrOomE; } -#endif gNetLib = lib; } gNetLib->refCount++; @@ -114,14 +108,12 @@ int32_t calogNetRegister(CalogT *calog) { calogRegisterInline(calog, "udpSendTo", udpSendTo, gNetLib); calogRegisterInline(calog, "udpRecvFrom", udpRecvFrom, gNetLib); calogRegisterInline(calog, "udpClose", udpClose, gNetLib); -#ifdef CALOG_WITH_ENET calogRegisterInline(calog, "enetHost", enetHost, gNetLib); calogRegisterInline(calog, "enetConnect", enetConnect, gNetLib); calogRegisterInline(calog, "enetService", enetService, gNetLib); calogRegisterInline(calog, "enetSend", enetSend, gNetLib); calogRegisterInline(calog, "enetDisconnect", enetDisconnect, gNetLib); calogRegisterInline(calog, "enetClose", enetClose, gNetLib); -#endif return calogOkE; } @@ -135,9 +127,7 @@ void calogNetShutdown(void) { gNetLib->refCount--; if (gNetLib->refCount <= 0) { calogHandleTableDestroy(gNetLib->handles, netCloser); -#ifdef CALOG_WITH_ENET enet_deinitialize(); -#endif free(gNetLib); gNetLib = NULL; } @@ -156,14 +146,12 @@ static void netCloser(uint32_t type, void *resource) { free(sock); break; } -#ifdef CALOG_WITH_ENET case NET_TYPE_ENET_HOST: enet_host_destroy((ENetHost *)resource); break; case NET_TYPE_ENET_PEER: // Peers are owned by their host; enet_host_destroy frees them. break; -#endif default: 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) { NetLibT *lib; ENetHost *host; @@ -852,4 +839,3 @@ static int32_t enetService(CalogValueT *args, int32_t argCount, CalogValueT *res calogValueAgg(result, map); return calogOkE; } -#endif diff --git a/libs/calogNet.h b/libs/calogNet.h index 02f8597..09597d2 100644 --- a/libs/calogNet.h +++ b/libs/calogNet.h @@ -11,14 +11,20 @@ // udpSendTo(handle, host, port, data) -> bytesSent // udpRecvFrom(handle, maxBytes) -> { data, host, port } // udpClose(handle) -// Payloads are binary-safe strings. The blocking natives (accept/recv/recvFrom) 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). Backends: TCP/UDP always; ENet is -// added behind CALOG_WITH_ENET. +// enetHost(port, maxPeers) -> hostHandle +// enetConnect(hostHandle, host, port, channels) -> peerHandle +// enetService(hostHandle, timeoutMs) -> { type[, peer, channel, data] } +// enetSend(peerHandle, channel, data, reliable) +// enetDisconnect(peerHandle) +// enetClose(hostHandle) +// Payloads are binary-safe strings. The blocking natives (accept/recv/recvFrom/enetService) +// 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 #define CALOG_NET_H diff --git a/src/calogMain.c b/src/calogMain.c new file mode 100644 index 0000000..a97f91f --- /dev/null +++ b/src/calogMain.c @@ -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 . 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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