Initial feature set complete.
This commit is contained in:
parent
3c39f48bde
commit
b5cf00b014
49 changed files with 2155 additions and 83 deletions
219
API.md
Normal file
219
API.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# calog script API reference
|
||||
|
||||
Every function below is a **native** that calog registers into each engine, so a script in
|
||||
any language can call it. This reference covers the natives the `calog` runner and its
|
||||
libraries expose. (For the C embedding API -- `calogCreate`, `calogRegister`, `calogFnInvoke`,
|
||||
etc. -- see the README and `src/calog.h`.)
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Calling syntax per engine.** Most engines call a native by its bare name --
|
||||
`cryptoUuid()`, `kvSet("k", 1)`. Two differ:
|
||||
- **Wren**: `Calog.call("name", [args])`, e.g. `Calog.call("kvSet", ["k", 1])`.
|
||||
- **Scheme (s7)**: s-expression form, `(kvSet "k" 1)`.
|
||||
- **Output & exit** (provided by the `calog` runner): `calogPrint(...)` writes to stdout;
|
||||
`calogExit([code])` tears everything down and exits. calog is event-driven, so a script
|
||||
must call `calogExit` (or be interrupted) to end -- a finished top level does not exit.
|
||||
- **Values.** Arguments and results marshal through one canonical type: nil, bool, integer,
|
||||
real, string, list, and map (keyed record). Strings are **binary-safe** (may contain
|
||||
embedded NULs) everywhere the underlying library allows it.
|
||||
- **Handles** (db connections, sockets, ssh sessions) are opaque values owned by the context
|
||||
that created them; do not share a live handle across contexts.
|
||||
- **Callbacks.** Natives that take a *function* argument (`psSubscribe`, `timerAfter`,
|
||||
`timerEvery`, `calogExport`) accept a first-class function value. All engines support this,
|
||||
including my-basic (a top-level `def` or `lambda`; see `vendor/ourbasic`).
|
||||
- **Availability.** Every library in this reference is compiled into `bin/calog`: crypto,
|
||||
json, kv, fs, time, timer, export, pubsub, task, net (TCP / UDP / ENet), db (SQLite /
|
||||
PostgreSQL / MySQL), http, and ssh. ssh needs a reachable server; http needs a reachable
|
||||
endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Runner (`calog` binary)
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `calogPrint(...)` | Write each argument to stdout, space-separated, with a trailing newline. |
|
||||
| `calogExit([code])` | Tear down the runtime and exit the process with `code` (default `0`). |
|
||||
|
||||
## crypto
|
||||
|
||||
Binary-safe cryptographic primitives over OpenSSL.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `cryptoHashSha256(data) -> hex` | SHA-256 as a 64-char lowercase hex digest. |
|
||||
| `cryptoHashSha1(data) -> hex` | SHA-1 as a 40-char lowercase hex digest. |
|
||||
| `cryptoHmacSha256(key, data) -> hex` | HMAC-SHA-256 as 64-char lowercase hex. |
|
||||
| `cryptoRandomBytes(count) -> bytes` | `count` cryptographically-random bytes (binary string). |
|
||||
| `cryptoBase64Encode(data) -> text` | Base64-encode. |
|
||||
| `cryptoBase64Decode(text) -> data` | Base64-decode (trailing whitespace tolerated). |
|
||||
| `cryptoHexEncode(data) -> text` | Lowercase hex encode. |
|
||||
| `cryptoHexDecode(hexText) -> data` | Hex decode (case-insensitive). |
|
||||
| `cryptoUuid() -> string` | A random RFC 4122 version-4 UUID string. |
|
||||
|
||||
## db
|
||||
|
||||
SQL over SQLite, PostgreSQL, and MySQL/MariaDB. Parameters are always **bound** (never
|
||||
string-spliced), so queries are injection-safe. Values marshal as: NULL <-> nil, integers/
|
||||
reals as-is, text and BLOBs as binary-safe strings.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `dbOpen(driver, conn) -> handle` | Open a connection. `driver` is `"sqlite"`, `"postgres"`, or `"mysql"`. `conn` is a SQLite path or `":memory:"`, a libpq conninfo, or a MySQL `key=value ...` string. |
|
||||
| `dbExec(handle, sql, ...params) -> rowsAffected` | Run a non-query statement with bound params; returns rows affected. |
|
||||
| `dbQuery(handle, sql, ...params) -> rows` | Run a query; returns a list of `{column: value}` row maps. |
|
||||
| `dbClose(handle)` | Close the connection. |
|
||||
|
||||
## export
|
||||
|
||||
Share a function by name across contexts and engines.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `calogExport(name, fn)` | Publish function `fn` under a global `name`. |
|
||||
| `calogUnexport(name)` | Remove an exported name. |
|
||||
| `calogCall(name, ...args) -> result` | Call an exported function by name -- works in **every** engine. (On hook engines -- Lua/JS/Squirrel/s7 -- an export is also reachable by its bare name.) |
|
||||
|
||||
## fs
|
||||
|
||||
POSIX filesystem access. A failed operation raises a catchable script error carrying
|
||||
`strerror(errno)`.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `fsRead(path) -> string` | Read a whole file (binary-safe). |
|
||||
| `fsWrite(path, data)` | Create/truncate and write `data`. |
|
||||
| `fsAppend(path, data)` | Create if absent, append at the end. |
|
||||
| `fsExists(path) -> bool` | Whether the path exists. |
|
||||
| `fsRemove(path)` | Unlink a file. |
|
||||
| `fsMkdir(path)` | Create one directory level (existing dir is OK). |
|
||||
| `fsList(path) -> list` | Entry name strings, excluding `.` and `..`. |
|
||||
| `fsStat(path) -> map` | `{size, isDir, isFile, mtime}`, or nil if the path is absent. |
|
||||
|
||||
## http
|
||||
|
||||
Minimal HTTP/1.1 client over `http://` and `https://`. Each call is its own connection
|
||||
(`Connection: close`); redirects are not followed. `https://` verifies the server certificate
|
||||
against the system CA store by default.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `httpGet(url) -> map` | GET a URL. Returns `{status, body, headers}` (headers keyed by lowercased name). |
|
||||
| `httpRequest(opts) -> map` | `opts` is `{method (default "GET"), url, headers (map), body, insecure (bool)}`. `insecure=true` skips TLS verification. Returns `{status, body, headers}`. |
|
||||
|
||||
## json
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `jsonParse(text) -> value` | Parse JSON: object -> map, array -> list, number -> int or real, string, true/false, null -> nil. |
|
||||
| `jsonStringify(value) -> text` | Serialize a value to compact JSON text. |
|
||||
|
||||
## kv
|
||||
|
||||
A process-wide, thread-safe store shared by every context and engine. Holds **data only** (a
|
||||
function value is rejected). Keys are binary-safe.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `kvSet(key, value)` | Store a deep copy of `value` under `key` (replaces any existing). |
|
||||
| `kvGet(key) -> value` | A deep copy of the stored value, or nil if absent. |
|
||||
| `kvHas(key) -> bool` | Whether the key is present. |
|
||||
| `kvDelete(key)` | Remove the key (no error if absent). |
|
||||
| `kvKeys() -> list` | A list of the stored keys (strings). |
|
||||
|
||||
## net
|
||||
|
||||
Three first-class transports -- **TCP**, **UDP**, and **ENet** (reliable/ordered delivery
|
||||
over UDP) -- all always available. Payloads are binary-safe strings. Blocking calls
|
||||
(`tcpAccept`/`tcpRecv`/`udpRecvFrom`/`enetService`) stall only the calling context's thread.
|
||||
|
||||
TCP and UDP:
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `tcpConnect(host, port) -> handle` | Connect to a TCP server. |
|
||||
| `tcpListen(port) -> handle` | Listen on a TCP port. |
|
||||
| `tcpAccept(handle) -> handle` | Block for a client; returns a connection handle. |
|
||||
| `tcpSend(handle, data) -> bytesSent` | Send all of `data`. |
|
||||
| `tcpRecv(handle, maxBytes) -> data` | Read up to `maxBytes`; nil at end of stream. |
|
||||
| `tcpClose(handle)` | Close a socket. |
|
||||
| `udpOpen(port) -> handle` | Open a UDP socket (`port` 0 = ephemeral). |
|
||||
| `udpSendTo(handle, host, port, data) -> bytesSent` | Send a datagram. |
|
||||
| `udpRecvFrom(handle, maxBytes) -> map` | Receive one datagram: `{data, host, port}`. |
|
||||
| `udpClose(handle)` | Close a UDP socket. |
|
||||
|
||||
ENet (reliable UDP -- ordered, reliable channels over UDP):
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `enetHost(port, maxPeers) -> hostHandle` | Create an ENet host. |
|
||||
| `enetConnect(hostHandle, host, port, channels) -> peerHandle` | Initiate a connection to a peer. |
|
||||
| `enetService(hostHandle, timeoutMs) -> event` | Poll for one event within `timeoutMs`. Returns `{type, ...}` where `type` is `"none"`, `"connect"`, `"receive"` (with `peer`, `channel`, `data`), or `"disconnect"`. |
|
||||
| `enetSend(peerHandle, channel, data, reliable)` | Queue a packet on a channel; `reliable` is a bool. |
|
||||
| `enetDisconnect(peerHandle)` | Begin disconnecting a peer. |
|
||||
| `enetClose(hostHandle)` | Destroy an ENet host. |
|
||||
|
||||
## pubsub
|
||||
|
||||
Deliver a message to every subscriber of a topic, across contexts and engines. Delivery is
|
||||
synchronous; each subscriber runs on its own context's thread and gets a deep copy of the
|
||||
message. Keep publish graphs acyclic.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `psSubscribe(topic, fn) -> id` | Register `fn` to receive messages published on `topic`. |
|
||||
| `psUnsubscribe(id)` | Drop the subscription with that id. |
|
||||
| `psPublish(topic, msg) -> count` | Deliver a copy of `msg` to every subscriber; returns how many were invoked. |
|
||||
|
||||
## ssh
|
||||
|
||||
SSH/SFTP over libssh2. Requires a reachable SSH server. Payloads are binary-safe.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `sshConnect(host[, port]) -> handle` | Connect (port defaults to 22). |
|
||||
| `sshAuthPassword(handle, user, password) -> bool` | Password authentication. |
|
||||
| `sshAuthKey(handle, user, privateKeyPath[, publicKeyPath, passphrase]) -> bool` | Public-key authentication. |
|
||||
| `sshExec(handle, command) -> map` | Run a remote command: `{stdout, stderr, exitCode}`. |
|
||||
| `sshClose(handle)` | Close the session. |
|
||||
| `sftpGet(handle, remotePath) -> data` | Read a remote file whole (binary-safe). |
|
||||
| `sftpPut(handle, remotePath, data)` | Create/truncate a remote file (mode 0644). |
|
||||
| `sftpList(handle, path) -> list` | `[{name, size, isDir}, ...]`. |
|
||||
| `sftpStat(handle, path) -> map` | `{size, isDir}`, or nil if the path is missing. |
|
||||
| `sftpRemove(handle, path)` | Remove a remote file. |
|
||||
| `sftpMkdir(handle, path)` | Create a remote directory (mode 0755). |
|
||||
|
||||
## task
|
||||
|
||||
Launch and manage other calog script contexts. Tasks are fire-and-forget: a spawned context
|
||||
runs on its own thread; results come back through host natives or the shared kv/pubsub. A
|
||||
task is owned by the context that spawned it, and only that owner may `taskEval`/`taskClose`.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `taskSpawn(engine, code) -> handle` | Run a code string on a named engine (`"lua"`, `"javascript"`, `"squirrel"`, `"mybasic"`, `"berry"`, `"scheme"`, `"wren"`). |
|
||||
| `taskLoad(baseName) -> handle` | Launch a script *file* (engine chosen by extension). |
|
||||
| `taskEval(handle, code)` | Feed more code into a running task (runs on its thread). |
|
||||
| `taskClose(handle)` | Stop a task (cooperative: waits for its thread to exit). |
|
||||
| `taskSelf() -> id` | The calling script's own context id. |
|
||||
| `taskCount() -> n` | Number of tasks this library currently holds open. |
|
||||
|
||||
## time
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `timeNow() -> real` | Wall-clock epoch seconds, fractional (CLOCK_REALTIME). |
|
||||
| `timeMonotonic() -> real` | Seconds from an unspecified origin (CLOCK_MONOTONIC); use for intervals. |
|
||||
| `timeSleep(ms)` | Block the calling context for `ms` milliseconds. |
|
||||
|
||||
## timer
|
||||
|
||||
One background thread drives every timer; each callback runs on the context that created the
|
||||
timer.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `timerAfter(ms, fn) -> id` | Fire `fn` once, `ms` milliseconds from now. |
|
||||
| `timerEvery(ms, fn) -> id` | Fire `fn` every `ms` milliseconds. |
|
||||
| `timerCancel(id)` | Stop a pending or repeating timer. |
|
||||
91
LICENSE.md
91
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.
|
||||
|
|
|
|||
37
Makefile
37
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.
|
||||
|
|
|
|||
26
README.md
26
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
|
||||
|
|
|
|||
22
design.md
22
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:
|
||||
|
|
|
|||
90
examples/scripts/README.md
Normal file
90
examples/scripts/README.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# calog example scripts
|
||||
|
||||
Small, runnable programs for the `calog` command-line runner (`bin/calog`). Each one is
|
||||
verified to run against the built binary. They double as a tour of the engines and the
|
||||
built-in libraries.
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
make # builds bin/calog (among others)
|
||||
|
||||
bin/calog examples/scripts/languages/lua.lua # run by extension
|
||||
bin/calog examples/scripts/apps/uuidGen # no extension -> searches .lua/.js/.nut/.bas/.be/.scm/.wren
|
||||
bin/calog examples/scripts/multifile/producer.js \
|
||||
examples/scripts/multifile/consumer.lua # several files share one runtime
|
||||
```
|
||||
|
||||
Conventions every example follows:
|
||||
|
||||
- **`calogPrint(...)`** writes to stdout (calog-prefixed so it never clashes with an
|
||||
engine's own `print`/keyword; on Wren it is `Calog.call("calogPrint", [...])`).
|
||||
- **`calogExit([code])`** ends the run. calog is event-driven -- a script's top level
|
||||
finishing does not exit the process (it may still have timers/subscriptions live), so a
|
||||
script asks to exit explicitly. `Ctrl-C` also tears things down cleanly.
|
||||
- Extensions map to engines: `.lua .js .nut .bas .be .scm .wren`.
|
||||
|
||||
## `languages/` -- one guided tour per engine
|
||||
|
||||
| file | shows |
|
||||
|---|---|
|
||||
| `lua.lua` | Lua 5.4: locals, a function, a numeric `for` building a table, then crypto + json natives |
|
||||
| `javascript.js` | JavaScript (QuickJS, ES2023): `const`/`let`, arrow fns, `Array.map`/`filter`, template literals, JSON round-trip |
|
||||
| `squirrel.nut` | Squirrel: locals, a function, `foreach` over an array, a table, a small class |
|
||||
| `mybasic.bas` | my-basic: variables, a `FOR..NEXT` sum, `IF..THEN..ELSE`, plus crypto + kv |
|
||||
| `berry.be` | Berry (Python-like): a `def`, a list built with a `for`, a map, then json + uuid |
|
||||
| `scheme.scm` | s7 Scheme: a recursive factorial, list build + map, string ops (all in one `(begin ...)`) |
|
||||
| `wren.wren` | Wren: a class, a `List`, and every native reached via `Calog.call(name, [args])` |
|
||||
|
||||
## `libraries/` -- one focused example per built-in library
|
||||
|
||||
| file | library / shows |
|
||||
|---|---|
|
||||
| `crypto.lua` | crypto: SHA-256/SHA-1, HMAC, base64 + hex round-trips, UUID, random bytes |
|
||||
| `cryptoInJs.js` | crypto from JavaScript |
|
||||
| `json.lua` | json: parse a nested doc, read fields, stringify a table |
|
||||
| `jsonInBerry.be` | json from Berry |
|
||||
| `kv.lua` | kv store: set / get / has / keys / delete |
|
||||
| `kvInScheme.scm` | kv from s7 Scheme |
|
||||
| `fs.lua` | fs: mkdir/write/append/read/stat/list/remove, all in a unique `/tmp` dir |
|
||||
| `time.lua` | time: `timeNow`, and measuring a `timeSleep` with `timeMonotonic` |
|
||||
| `timer.lua` | timer (Lua callbacks): a `timerAfter` one-shot + a self-cancelling `timerEvery` |
|
||||
| `timerInMyBasic.bas` | timer callbacks **in my-basic** -- the our-basic fork makes `def`/lambda first-class |
|
||||
| `database.lua` | db: an in-memory SQLite table, bound-parameter inserts, a query + an aggregate |
|
||||
| `export.lua` | export: publish a function, call it by name (and, on Lua, by bare name) |
|
||||
| `pubsub.lua` | pubsub: two handlers subscribe, publish delivers to both, then unsubscribe |
|
||||
| `task.lua` | task: `taskSelf`, spawn a sibling child, `taskCount` |
|
||||
| `net.lua` | net: a UDP echo over loopback between a spawned listener and a sender |
|
||||
| `http.lua` | http: self-contained -- a spawned child runs a one-shot HTTP/1.1 server, then `httpGet`s it |
|
||||
| `ssh.lua` | ssh/sftp: a documented, safe-to-run template (prints guidance if no server is configured) |
|
||||
|
||||
## `polyglot/` -- several languages sharing one runtime
|
||||
|
||||
| file | shows |
|
||||
|---|---|
|
||||
| `exportAcrossEngines.lua` | Lua exports a function; a spawned **JavaScript** task calls it |
|
||||
| `pubsubAcrossEngines.lua` | Lua subscribes to a topic; a spawned **JavaScript** task publishes to it |
|
||||
| `sharedKv.lua` | Lua seeds the shared kv store; a spawned **JavaScript** task reads it back and mutates it |
|
||||
| `taskFanout.lua` | one Lua script spawns a task on **five** different engines, each printing in its own language |
|
||||
|
||||
## `multifile/` -- multiple script files in one run
|
||||
|
||||
Files listed on the `calog` command line run **concurrently in one process** and share the
|
||||
same runtime (kv store, pubsub bus, exports). One `calogExit()` from any file ends the run.
|
||||
|
||||
```sh
|
||||
bin/calog examples/scripts/multifile/producer.js examples/scripts/multifile/consumer.lua
|
||||
```
|
||||
|
||||
| file | role |
|
||||
|---|---|
|
||||
| `producer.js` | (JavaScript) writes shared data to kv, then signals `"ready"` on the pubsub bus after a short delay |
|
||||
| `consumer.lua` | (Lua) subscribes to `"ready"`, and on the signal reads the shared kv data, prints it, and exits |
|
||||
|
||||
## `apps/` -- tiny end-to-end tools
|
||||
|
||||
| file | shows |
|
||||
|---|---|
|
||||
| `uuidGen.lua` | print five random UUIDs |
|
||||
| `hashText.lua` | write text to a temp file, read it back, print its SHA-256/SHA-1/length, clean up |
|
||||
| `wordCount.js` | word-count a paragraph (totals, uniques, top frequencies), emit the summary as JSON |
|
||||
19
examples/scripts/apps/hashText.lua
Normal file
19
examples/scripts/apps/hashText.lua
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- hashText.lua -- tiny hashing tool: write text to a temp file, read it back,
|
||||
-- and print its SHA-256, SHA-1, and byte length, then clean up.
|
||||
-- run: bin/calog examples/scripts/apps/hashText.lua
|
||||
|
||||
local path = "/tmp/hashText-" .. tostring(timeNow()) .. ".txt"
|
||||
local text = "The quick brown fox jumps over the lazy dog"
|
||||
|
||||
fsWrite(path, text)
|
||||
|
||||
local data = fsRead(path)
|
||||
|
||||
calogPrint("path: ", path)
|
||||
calogPrint("bytes: ", #data)
|
||||
calogPrint("sha256:", cryptoHashSha256(data))
|
||||
calogPrint("sha1: ", cryptoHashSha1(data))
|
||||
|
||||
fsRemove(path)
|
||||
|
||||
calogExit(0)
|
||||
8
examples/scripts/apps/uuidGen.lua
Normal file
8
examples/scripts/apps/uuidGen.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- uuidGen.lua -- generate five random UUIDs, one per line.
|
||||
-- run: bin/calog examples/scripts/apps/uuidGen.lua
|
||||
|
||||
for i = 1, 5 do
|
||||
calogPrint(cryptoUuid())
|
||||
end
|
||||
|
||||
calogExit(0)
|
||||
43
examples/scripts/apps/wordCount.js
Normal file
43
examples/scripts/apps/wordCount.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// wordCount.js -- tiny word-count app in JavaScript.
|
||||
// Splits a paragraph into words, counts totals, uniques, and top frequencies,
|
||||
// then emits a JSON summary via jsonStringify.
|
||||
// Run: bin/calog examples/scripts/apps/wordCount.js
|
||||
|
||||
const paragraph =
|
||||
"the quick brown fox jumps over the lazy dog " +
|
||||
"the dog was not amused and the fox did not care " +
|
||||
"the quick fox and the lazy dog became the best of friends";
|
||||
|
||||
// Normalize to lowercase and split on any run of non-letters.
|
||||
const words = paragraph
|
||||
.toLowerCase()
|
||||
.split(/[^a-z]+/)
|
||||
.filter((w) => w.length > 0);
|
||||
|
||||
// Tally frequency of each word.
|
||||
const freq = {};
|
||||
for (const w of words) {
|
||||
freq[w] = (freq[w] || 0) + 1;
|
||||
}
|
||||
|
||||
const uniqueWords = Object.keys(freq);
|
||||
|
||||
// Rank words by count (descending), then alphabetically for stable ties.
|
||||
const ranked = uniqueWords.slice().sort((a, b) => {
|
||||
if (freq[b] !== freq[a]) {
|
||||
return freq[b] - freq[a];
|
||||
}
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
});
|
||||
|
||||
const topFive = ranked.slice(0, 5).map((w) => ({ word: w, count: freq[w] }));
|
||||
|
||||
const summary = {
|
||||
totalWords: words.length,
|
||||
uniqueWords: uniqueWords.length,
|
||||
top: topFive,
|
||||
};
|
||||
|
||||
calogPrint(jsonStringify(summary));
|
||||
|
||||
calogExit(0);
|
||||
42
examples/scripts/languages/berry.be
Normal file
42
examples/scripts/languages/berry.be
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# A guided tour of Berry (Python-like) running on calog.
|
||||
# Demonstrates: a def function, a list built and iterated with a for loop, a map
|
||||
# iterated over its keys, then the jsonStringify and cryptoUuid natives.
|
||||
# Run: bin/calog examples/scripts/languages/berry.be
|
||||
|
||||
# A function.
|
||||
def square(n)
|
||||
return n * n
|
||||
end
|
||||
|
||||
# Build a list of squares with a numeric range for loop.
|
||||
var squares = []
|
||||
for i : 1..6
|
||||
squares.push(square(i))
|
||||
end
|
||||
|
||||
# Iterate the list, accumulating a sum.
|
||||
var total = 0
|
||||
for v : squares
|
||||
total = total + v
|
||||
end
|
||||
|
||||
# list.concat joins the elements into a readable string.
|
||||
calogPrint("squares 1..6 =", squares.concat(", "))
|
||||
calogPrint("sum of squares =", total)
|
||||
|
||||
# A map (Berry's associative type), iterated over its keys.
|
||||
var record = {
|
||||
"language": "Berry",
|
||||
"count": squares.size(),
|
||||
"total": total,
|
||||
"id": cryptoUuid()
|
||||
}
|
||||
for k : record.keys()
|
||||
calogPrint(" ", k, "=", record[k])
|
||||
end
|
||||
|
||||
# Serialize the map to JSON with the native.
|
||||
calogPrint("json:", jsonStringify(record))
|
||||
|
||||
# Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
calogExit(0)
|
||||
38
examples/scripts/languages/javascript.js
Normal file
38
examples/scripts/languages/javascript.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Tour of JavaScript (QuickJS ES2023) on calog: const/let, arrow functions,
|
||||
// Array.map/filter, template literals, and JSON round-tripping.
|
||||
// Run: bin/calog examples/scripts/languages/javascript.js
|
||||
|
||||
// const and let bindings.
|
||||
const languageName = "JavaScript";
|
||||
let engine = "QuickJS ES2023";
|
||||
|
||||
// An arrow function.
|
||||
const square = (n) => n * n;
|
||||
|
||||
// Array.filter then Array.map with an arrow function.
|
||||
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const evens = numbers.filter((n) => n % 2 === 0);
|
||||
const evenSquares = evens.map(square);
|
||||
|
||||
// Template literals.
|
||||
calogPrint(`Engine: ${languageName} (${engine})`);
|
||||
calogPrint(`Evens: ${evens.join(", ")}`);
|
||||
calogPrint(`Even squares: ${evenSquares.join(", ")}`);
|
||||
|
||||
// Compute a sum with reduce to show more of the standard library.
|
||||
const total = evenSquares.reduce((acc, n) => acc + n, 0);
|
||||
calogPrint(`Sum of even squares: ${total}`);
|
||||
|
||||
// jsonParse a nested object, mutate it, then jsonStringify it back out.
|
||||
const source = '{"user":{"name":"Ada","roles":["admin","dev"]},"active":true}';
|
||||
const parsed = jsonParse(source);
|
||||
parsed.user.roles.push("owner");
|
||||
parsed.stats = { evens: evens.length, total: total };
|
||||
|
||||
const roundTripped = jsonStringify(parsed);
|
||||
calogPrint(`User: ${parsed.user.name}`);
|
||||
calogPrint(`Roles: ${parsed.user.roles.join(", ")}`);
|
||||
calogPrint(`JSON: ${roundTripped}`);
|
||||
|
||||
// Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
calogExit(0);
|
||||
43
examples/scripts/languages/lua.lua
Normal file
43
examples/scripts/languages/lua.lua
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- A guided tour of Lua running on calog.
|
||||
-- Demonstrates: local variables, a function, a numeric for loop building a
|
||||
-- table, iterating that table, then cryptoHashSha256 and jsonStringify natives.
|
||||
-- Run: bin/calog examples/scripts/languages/lua.lua
|
||||
|
||||
-- A local variable.
|
||||
local squareCount = 6
|
||||
|
||||
-- A function.
|
||||
local function square(n)
|
||||
return n * n
|
||||
end
|
||||
|
||||
-- A numeric for loop that builds a table of squares.
|
||||
local squares = {}
|
||||
for i = 1, squareCount do
|
||||
squares[i] = square(i)
|
||||
end
|
||||
|
||||
-- Iterate the table and print each element.
|
||||
local total = 0
|
||||
for i, v in ipairs(squares) do
|
||||
calogPrint("square of", i, "is", v)
|
||||
total = total + v
|
||||
end
|
||||
calogPrint("sum of squares 1..6 =", total)
|
||||
|
||||
-- Hash a string with the crypto native.
|
||||
local message = "calog loves Lua"
|
||||
local digest = cryptoHashSha256(message)
|
||||
calogPrint("sha256(" .. message .. ") =", digest)
|
||||
|
||||
-- Build a small record and serialize it as JSON.
|
||||
local record = {
|
||||
language = "lua",
|
||||
version = "5.4",
|
||||
squares = squares,
|
||||
total = total,
|
||||
digest = digest
|
||||
}
|
||||
calogPrint("json:", jsonStringify(record))
|
||||
|
||||
calogExit(0)
|
||||
38
examples/scripts/languages/mybasic.bas
Normal file
38
examples/scripts/languages/mybasic.bas
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
' A guided tour of my-basic running on calog.
|
||||
' Demonstrates: variables, a FOR..NEXT loop that computes a running sum, an
|
||||
' IF..THEN..ELSE..ENDIF decision, then the cryptoHashSha256 and kvSet/kvGet
|
||||
' natives. my-basic has no first-class functions, so there are no callbacks.
|
||||
' Run: bin/calog examples/scripts/languages/mybasic.bas
|
||||
|
||||
' Plain variables. my-basic folds identifiers to a common case at parse time.
|
||||
language = "my-basic"
|
||||
upto = 6
|
||||
total = 0
|
||||
|
||||
' A FOR..NEXT loop building the sum 1 + 2 + ... + upto.
|
||||
for i = 1 to upto
|
||||
total = total + i
|
||||
calogPrint("added", i, "running total is", total)
|
||||
next i
|
||||
|
||||
calogPrint("sum of 1..", upto, "=", total)
|
||||
|
||||
' An IF..THEN..ELSE..ENDIF decision on the computed sum.
|
||||
if total > 20 then
|
||||
calogPrint("total", total, "is greater than 20")
|
||||
else
|
||||
calogPrint("total", total, "is 20 or less")
|
||||
endif
|
||||
|
||||
' Hash a string with the crypto native; it returns a hex digest.
|
||||
message = "calog loves BASIC"
|
||||
digest = cryptoHashSha256(message)
|
||||
calogPrint("sha256(" + message + ") =", digest)
|
||||
|
||||
' Store the digest in the process-wide key/value store, then read it back.
|
||||
kvSet("mybasic.digest", digest)
|
||||
roundTrip = kvGet("mybasic.digest")
|
||||
calogPrint("kv round-trip matches:", roundTrip = digest)
|
||||
|
||||
' Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
calogExit(0)
|
||||
36
examples/scripts/languages/scheme.scm
Normal file
36
examples/scripts/languages/scheme.scm
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
; A guided tour of s7 Scheme running on calog.
|
||||
; Demonstrates: a recursive factorial, a list built and mapped over, string
|
||||
; assembly, and the cryptoHashSha256 native -- all inside ONE (begin ...) form,
|
||||
; because s7's runSource reads a single top-level form per script.
|
||||
; Run: bin/calog examples/scripts/languages/scheme.scm
|
||||
|
||||
(begin
|
||||
|
||||
; A classic recursive function.
|
||||
(define (factorial n)
|
||||
(if (<= n 1)
|
||||
1
|
||||
(* n (factorial (- n 1)))))
|
||||
|
||||
; Build a list, then derive a second list by mapping factorial over it.
|
||||
(define nums (list 1 2 3 4 5 6))
|
||||
(define facts (map factorial nums))
|
||||
|
||||
; object->string turns an aggregate into a readable literal for printing
|
||||
; (calogPrint renders a raw list as "[aggregate]").
|
||||
(calogPrint "nums =" (object->string nums))
|
||||
(calogPrint "facts =" (object->string facts))
|
||||
|
||||
; Fold the mapped list into a running sum with a tail-recursive helper.
|
||||
(define (sum-list lst acc)
|
||||
(if (null? lst)
|
||||
acc
|
||||
(sum-list (cdr lst) (+ acc (car lst)))))
|
||||
(calogPrint "sum of factorials =" (sum-list facts 0))
|
||||
|
||||
; Hash a message with the crypto native; it returns a lowercase hex digest.
|
||||
(define message "calog + s7 scheme")
|
||||
(calogPrint "sha256(" message ") =" (cryptoHashSha256 message))
|
||||
|
||||
; Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
(calogExit 0))
|
||||
58
examples/scripts/languages/squirrel.nut
Normal file
58
examples/scripts/languages/squirrel.nut
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// A guided tour of Squirrel running on calog.
|
||||
// Demonstrates: local variables, a function, an array with foreach, a table
|
||||
// (map) with foreach, a simple class with a method, then the cryptoUuid native.
|
||||
// Run: bin/calog examples/scripts/languages/squirrel.nut
|
||||
|
||||
// A local variable.
|
||||
local engine = "Squirrel"
|
||||
|
||||
// A function.
|
||||
function square(n) {
|
||||
return n * n
|
||||
}
|
||||
|
||||
// An array, iterated with foreach to build a table of squares.
|
||||
local numbers = [1, 2, 3, 4, 5, 6]
|
||||
local squares = {}
|
||||
local total = 0
|
||||
foreach (idx, value in numbers) {
|
||||
local s = square(value)
|
||||
squares[value] <- s
|
||||
total = total + s
|
||||
calogPrint("square of", value, "is", s)
|
||||
}
|
||||
calogPrint("sum of squares 1..6 =", total)
|
||||
|
||||
// A table (map), iterated with foreach over key/value pairs.
|
||||
local record = {
|
||||
language = engine
|
||||
version = "3.x"
|
||||
total = total
|
||||
}
|
||||
foreach (key, value in record) {
|
||||
calogPrint("record[" + key + "] =", value)
|
||||
}
|
||||
|
||||
// A simple class with a constructor and a method.
|
||||
class Counter {
|
||||
count = 0
|
||||
constructor(start) {
|
||||
count = start
|
||||
}
|
||||
function bump(by) {
|
||||
count = count + by
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
local counter = Counter(10)
|
||||
counter.bump(5)
|
||||
counter.bump(7)
|
||||
calogPrint("counter after two bumps =", counter.count)
|
||||
|
||||
// Use the cryptoUuid native to mint a unique id.
|
||||
local id = cryptoUuid()
|
||||
calogPrint("generated uuid:", id)
|
||||
|
||||
// Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
calogExit(0)
|
||||
37
examples/scripts/languages/wren.wren
Normal file
37
examples/scripts/languages/wren.wren
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// A guided tour of Wren running on calog. Wren reaches natives only through
|
||||
// Calog.call(name, [args]), so print, crypto, and exit all go through it.
|
||||
// Demonstrates: a class with a method, a List transformed with map, and the
|
||||
// cryptoUuid / cryptoBase64Encode / calogPrint natives. Synchronous only.
|
||||
// Run: bin/calog examples/scripts/languages/wren.wren
|
||||
|
||||
// A small class with a constructor and a method.
|
||||
class Square {
|
||||
construct new(n) {
|
||||
_n = n
|
||||
}
|
||||
|
||||
value { _n * _n }
|
||||
}
|
||||
|
||||
// Build a List, then transform it with map (a Sequence) and materialize it.
|
||||
var numbers = [1, 2, 3, 4, 5, 6]
|
||||
var squares = numbers.map {|n| Square.new(n).value }.toList
|
||||
|
||||
// Reduce to a sum to show more of the List API.
|
||||
var total = squares.reduce(0) {|acc, v| acc + v }
|
||||
|
||||
// Natives are reached via Calog.call(name, [args]).
|
||||
Calog.call("calogPrint", ["squares 1..6 =", squares.join(", ")])
|
||||
Calog.call("calogPrint", ["sum of squares =", total])
|
||||
|
||||
// A generated UUID, then base64-encoded through two more natives.
|
||||
var id = Calog.call("cryptoUuid", [])
|
||||
var encoded = Calog.call("cryptoBase64Encode", [id])
|
||||
Calog.call("calogPrint", ["uuid =", id])
|
||||
Calog.call("calogPrint", ["base64 =", encoded])
|
||||
|
||||
// String interpolation works too; pass the finished string to the native.
|
||||
Calog.call("calogPrint", ["Wren squared %(numbers.count) numbers, total %(total)."])
|
||||
|
||||
// Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
Calog.call("calogExit", [0])
|
||||
20
examples/scripts/libraries/crypto.lua
Normal file
20
examples/scripts/libraries/crypto.lua
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- Showcase the calog crypto library: hashing, HMAC, base64/hex round-trips,
|
||||
-- UUID generation, and random bytes.
|
||||
-- Run: bin/calog examples/scripts/libraries/crypto.lua
|
||||
|
||||
calogPrint("sha256(abc):", cryptoHashSha256("abc"))
|
||||
calogPrint("sha1(abc):", cryptoHashSha1("abc"))
|
||||
calogPrint("hmacSha256(key,abc):", cryptoHmacSha256("key", "abc"))
|
||||
|
||||
local b64 = cryptoBase64Encode("hello world")
|
||||
calogPrint("base64 encode:", b64)
|
||||
calogPrint("base64 decode:", cryptoBase64Decode(b64))
|
||||
|
||||
local hex = cryptoHexEncode("hello world")
|
||||
calogPrint("hex encode:", hex)
|
||||
calogPrint("hex decode:", cryptoHexDecode(hex))
|
||||
|
||||
calogPrint("uuid:", cryptoUuid())
|
||||
calogPrint("randomBytes(16) length:", #cryptoRandomBytes(16))
|
||||
|
||||
calogExit(0)
|
||||
19
examples/scripts/libraries/cryptoInJs.js
Normal file
19
examples/scripts/libraries/cryptoInJs.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Crypto in JavaScript: SHA-256 hashing, a base64 encode/decode round-trip,
|
||||
// and UUID generation via the calog crypto natives.
|
||||
// Run: bin/calog examples/scripts/libraries/cryptoInJs.js
|
||||
|
||||
// NOTE: QuickJS has no console/print builtin -> use calogPrint.
|
||||
|
||||
const message = "hello world";
|
||||
|
||||
calogPrint("sha256(hello world):", cryptoHashSha256(message));
|
||||
|
||||
const encoded = cryptoBase64Encode(message);
|
||||
const decoded = cryptoBase64Decode(encoded);
|
||||
calogPrint("base64 encode:", encoded);
|
||||
calogPrint("base64 decode:", decoded);
|
||||
calogPrint("round-trip ok:", decoded === message);
|
||||
|
||||
calogPrint("uuid:", cryptoUuid());
|
||||
|
||||
calogExit(0);
|
||||
39
examples/scripts/libraries/database.lua
Normal file
39
examples/scripts/libraries/database.lua
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
-- Showcase the calog db library on SQLite: open an in-memory database, create a
|
||||
-- table, insert several rows using BOUND parameters (safe from injection), run a
|
||||
-- SELECT and print each returned row map field by field, run a COUNT aggregate,
|
||||
-- then close the handle.
|
||||
-- Run: bin/calog examples/scripts/libraries/database.lua
|
||||
|
||||
local db = dbOpen("sqlite", ":memory:")
|
||||
|
||||
dbExec(db, "CREATE TABLE fruit (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER, price REAL)")
|
||||
|
||||
-- Insert rows with bound parameters. dbExec returns rows affected.
|
||||
local stock = {
|
||||
{ "apple", 12, 0.35 },
|
||||
{ "banana", 30, 0.20 },
|
||||
{ "cherry", 8, 1.10 },
|
||||
{ "date", 15, 0.75 },
|
||||
}
|
||||
|
||||
local inserted = 0
|
||||
for _, item in ipairs(stock) do
|
||||
inserted = inserted + dbExec(db, "INSERT INTO fruit (name, qty, price) VALUES (?, ?, ?)", item[1], item[2], item[3])
|
||||
end
|
||||
calogPrint("rows inserted:", inserted)
|
||||
|
||||
-- SELECT returning a list of row maps. Print each field explicitly.
|
||||
local rows = dbQuery(db, "SELECT id, name, qty, price FROM fruit WHERE qty >= ? ORDER BY name", 10)
|
||||
calogPrint("rows with qty >= 10:", #rows)
|
||||
for _, row in ipairs(rows) do
|
||||
calogPrint(string.format(" #%d %-7s qty=%d price=%.2f", row.id, row.name, row.qty, row.price))
|
||||
end
|
||||
|
||||
-- Aggregate query: COUNT and SUM in a single row.
|
||||
local summary = dbQuery(db, "SELECT COUNT(*) AS n, SUM(qty) AS total FROM fruit")
|
||||
calogPrint("distinct fruits:", summary[1].n)
|
||||
calogPrint("total quantity:", summary[1].total)
|
||||
|
||||
dbClose(db)
|
||||
|
||||
calogExit(0)
|
||||
26
examples/scripts/libraries/export.lua
Normal file
26
examples/scripts/libraries/export.lua
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- Showcase the calog export library: publish a function with calogExport, then
|
||||
-- invoke it two ways -- via calogCall by name, and (a Lua convenience) by its bare
|
||||
-- name, since exports resolve as unknown globals. Finally calogUnexport it and show
|
||||
-- a guarded call fails cleanly instead of returning a stale result.
|
||||
-- Run: bin/calog examples/scripts/libraries/export.lua
|
||||
|
||||
calogExport("square", function(x)
|
||||
return x * x
|
||||
end)
|
||||
|
||||
-- Invoke through the broker by name.
|
||||
calogPrint("calogCall square 7:", calogCall("square", 7))
|
||||
|
||||
-- Invoke by bare name (Lua resolves the unknown global through the export registry).
|
||||
calogPrint("bare-name square 9:", square(9))
|
||||
|
||||
-- Retire the export; the name is no longer resolvable anywhere.
|
||||
calogUnexport("square")
|
||||
|
||||
local ok, err = pcall(calogCall, "square", 7)
|
||||
calogPrint("call after unexport ok:", ok)
|
||||
if not ok then
|
||||
calogPrint("call after unexport failed cleanly:", tostring(err))
|
||||
end
|
||||
|
||||
calogExit(0)
|
||||
34
examples/scripts/libraries/fs.lua
Normal file
34
examples/scripts/libraries/fs.lua
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- Showcase the calog fs library end to end inside a unique temp dir: create a
|
||||
-- directory, write and append a file, read it back, stat it, list the dir, then
|
||||
-- remove the file and directory so nothing is left behind.
|
||||
-- Run: bin/calog examples/scripts/libraries/fs.lua
|
||||
|
||||
local dir = "/tmp/calogFsExample." .. cryptoUuid()
|
||||
local file = dir .. "/notes.txt"
|
||||
|
||||
fsMkdir(dir)
|
||||
calogPrint("dir exists after mkdir:", fsExists(dir))
|
||||
|
||||
fsWrite(file, "line one\n")
|
||||
fsAppend(file, "line two\n")
|
||||
calogPrint("file exists after write:", fsExists(file))
|
||||
|
||||
local contents = fsRead(file)
|
||||
calogPrint("contents:", contents)
|
||||
|
||||
local info = fsStat(file)
|
||||
calogPrint("size:", info.size)
|
||||
calogPrint("isFile:", info.isFile)
|
||||
calogPrint("isDir:", info.isDir)
|
||||
|
||||
local entries = fsList(dir)
|
||||
calogPrint("entry count:", #entries)
|
||||
calogPrint("first entry:", entries[1])
|
||||
|
||||
fsRemove(file)
|
||||
calogPrint("file exists after remove:", fsExists(file))
|
||||
|
||||
fsRemove(dir)
|
||||
calogPrint("dir exists after remove:", fsExists(dir))
|
||||
|
||||
calogExit(0)
|
||||
50
examples/scripts/libraries/http.lua
Normal file
50
examples/scripts/libraries/http.lua
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- Showcase the calog http client end to end with NO external dependency: a
|
||||
-- spawned "lua" child task acts as a tiny one-shot HTTP/1.1 server -- it
|
||||
-- tcpListens on a fixed high port, accepts one connection, reads the request,
|
||||
-- and sends back a valid 200 response with a Content-Length body. The main
|
||||
-- context sleeps briefly so the child can bind, then httpGets that URL and
|
||||
-- prints the returned status and body before exiting.
|
||||
-- Run: bin/calog examples/scripts/libraries/http.lua
|
||||
|
||||
local PORT = 48080
|
||||
|
||||
-- The child runs as its own context. Its tcpAccept/tcpRecv block that child's
|
||||
-- thread, leaving the main context free to make the request. Inside this long
|
||||
-- string the \r\n sequences are literal text that the child parses as escapes.
|
||||
local serverCode = [[
|
||||
local PORT = 48080
|
||||
local listener = tcpListen(PORT)
|
||||
calogPrint("server: listening on port", PORT)
|
||||
|
||||
local client = tcpAccept(listener)
|
||||
local request = tcpRecv(client, 4096)
|
||||
local requestLine = request and string.match(request, "^[^\r\n]+") or "?"
|
||||
calogPrint("server: request line:", requestLine)
|
||||
|
||||
local body = "Hello from the calog TCP server!\n"
|
||||
local response =
|
||||
"HTTP/1.1 200 OK\r\n" ..
|
||||
"Content-Type: text/plain\r\n" ..
|
||||
"Content-Length: " .. #body .. "\r\n" ..
|
||||
"Connection: close\r\n" ..
|
||||
"\r\n" ..
|
||||
body
|
||||
tcpSend(client, response)
|
||||
tcpClose(client)
|
||||
tcpClose(listener)
|
||||
calogPrint("server: response sent")
|
||||
]]
|
||||
|
||||
taskSpawn("lua", serverCode)
|
||||
|
||||
-- Give the child a moment to reach its tcpListen before we connect.
|
||||
timeSleep(400)
|
||||
|
||||
local url = "http://127.0.0.1:" .. PORT .. "/"
|
||||
calogPrint("client: requesting", url)
|
||||
|
||||
local response = httpGet(url)
|
||||
calogPrint("client: status", response.status)
|
||||
calogPrint("client: body", response.body)
|
||||
|
||||
calogExit(0)
|
||||
35
examples/scripts/libraries/json.lua
Normal file
35
examples/scripts/libraries/json.lua
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- Showcase the calog json library: jsonParse a nested JSON document, read its
|
||||
-- fields, then build a Lua table and jsonStringify it back to text.
|
||||
-- Run: bin/calog examples/scripts/libraries/json.lua
|
||||
|
||||
local text = [[
|
||||
{
|
||||
"name": "calog",
|
||||
"version": 2,
|
||||
"stable": true,
|
||||
"notes": null,
|
||||
"engines": ["lua", "javascript", "scheme"],
|
||||
"limits": { "maxTasks": 64, "ratio": 1.5 }
|
||||
}
|
||||
]]
|
||||
|
||||
local doc = jsonParse(text)
|
||||
|
||||
calogPrint("name:", doc.name)
|
||||
calogPrint("version:", doc.version)
|
||||
calogPrint("stable:", doc.stable)
|
||||
calogPrint("notes is nil:", doc.notes == nil)
|
||||
calogPrint("first engine:", doc.engines[1])
|
||||
calogPrint("engine count:", #doc.engines)
|
||||
calogPrint("limits.maxTasks:", doc.limits.maxTasks)
|
||||
calogPrint("limits.ratio:", doc.limits.ratio)
|
||||
|
||||
local report = {}
|
||||
report.tool = "calog"
|
||||
report.ok = true
|
||||
report.count = doc.engines
|
||||
report.summary = { engines = #doc.engines, maxTasks = doc.limits.maxTasks }
|
||||
|
||||
calogPrint("stringified:", jsonStringify(report))
|
||||
|
||||
calogExit(0)
|
||||
26
examples/scripts/libraries/jsonInBerry.be
Normal file
26
examples/scripts/libraries/jsonInBerry.be
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Showcase the calog json library from Berry: jsonParse a small JSON document,
|
||||
# read fields out of the resulting map, then build a Berry map and jsonStringify
|
||||
# it back to text.
|
||||
# Run: bin/calog examples/scripts/libraries/jsonInBerry.be
|
||||
|
||||
# Parse a small JSON string into a Berry map.
|
||||
var text = '{ "name": "calog", "version": 2, "stable": true, "engines": ["lua", "berry", "scheme"] }'
|
||||
var doc = jsonParse(text)
|
||||
|
||||
# Read fields out of the parsed map.
|
||||
calogPrint("name:", doc["name"])
|
||||
calogPrint("version:", doc["version"])
|
||||
calogPrint("stable:", doc["stable"])
|
||||
calogPrint("first engine:", doc["engines"][0])
|
||||
calogPrint("engine count:", doc["engines"].size())
|
||||
|
||||
# Build a fresh map and serialize it to JSON text.
|
||||
var report = {
|
||||
"tool": doc["name"],
|
||||
"ok": doc["stable"],
|
||||
"engines": doc["engines"].size()
|
||||
}
|
||||
calogPrint("stringified:", jsonStringify(report))
|
||||
|
||||
# Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
calogExit(0)
|
||||
33
examples/scripts/libraries/kv.lua
Normal file
33
examples/scripts/libraries/kv.lua
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- Showcase the calog kv store: set string and number values, read them back,
|
||||
-- test presence with kvHas, list keys with kvKeys, then delete one key and
|
||||
-- confirm it is gone. The kv store is process-wide and shared across engines.
|
||||
-- Run: bin/calog examples/scripts/libraries/kv.lua
|
||||
|
||||
kvSet("tool", "calog")
|
||||
kvSet("version", 2)
|
||||
kvSet("ratio", 1.5)
|
||||
kvSet("stable", "yes")
|
||||
|
||||
calogPrint("tool:", kvGet("tool"))
|
||||
calogPrint("version:", kvGet("version"))
|
||||
calogPrint("ratio:", kvGet("ratio"))
|
||||
calogPrint("stable:", kvGet("stable"))
|
||||
|
||||
calogPrint("has tool:", kvHas("tool"))
|
||||
calogPrint("has missing:", kvHas("missing"))
|
||||
|
||||
local keys = kvKeys()
|
||||
table.sort(keys)
|
||||
calogPrint("key count:", #keys)
|
||||
calogPrint("keys:", table.concat(keys, ", "))
|
||||
|
||||
kvDelete("stable")
|
||||
calogPrint("has stable after delete:", kvHas("stable"))
|
||||
calogPrint("get stable after delete is nil:", kvGet("stable") == nil)
|
||||
|
||||
local remaining = kvKeys()
|
||||
table.sort(remaining)
|
||||
calogPrint("remaining count:", #remaining)
|
||||
calogPrint("remaining keys:", table.concat(remaining, ", "))
|
||||
|
||||
calogExit(0)
|
||||
22
examples/scripts/libraries/kvInScheme.scm
Normal file
22
examples/scripts/libraries/kvInScheme.scm
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
; The calog kv store from s7 Scheme, inside ONE (begin ...) form (s7's
|
||||
; runSource reads a single top-level form per script). Sets a couple of keys,
|
||||
; reads them back with kvGet, and checks presence with kvHas. The kv store is
|
||||
; process-wide and shared across every engine.
|
||||
; Run: bin/calog examples/scripts/libraries/kvInScheme.scm
|
||||
|
||||
(begin
|
||||
|
||||
; Store a string and a number under two keys.
|
||||
(kvSet "tool" "calog")
|
||||
(kvSet "answer" 42)
|
||||
|
||||
; Read the values back and print them.
|
||||
(calogPrint "tool =" (kvGet "tool"))
|
||||
(calogPrint "answer =" (kvGet "answer"))
|
||||
|
||||
; kvHas reports whether a key exists.
|
||||
(calogPrint "has tool =" (kvHas "tool"))
|
||||
(calogPrint "has missing =" (kvHas "missing"))
|
||||
|
||||
; Tear down and exit cleanly (required -- calog does not auto-exit).
|
||||
(calogExit 0))
|
||||
35
examples/scripts/libraries/net.lua
Normal file
35
examples/scripts/libraries/net.lua
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- Showcase the calog net library over loopback with UDP datagrams: a spawned
|
||||
-- "lua" listener task binds a fixed high port and blocks in udpRecvFrom, while
|
||||
-- the main context opens an ephemeral socket and sends one datagram to it. The
|
||||
-- listener's blocking recv runs on its own task thread, so the main thread stays
|
||||
-- free to send. A short delay lets the datagram arrive and print before we exit.
|
||||
-- Run: bin/calog examples/scripts/libraries/net.lua
|
||||
|
||||
local PORT = 47651
|
||||
|
||||
-- The listener runs in its own context/thread, so its blocking udpRecvFrom does
|
||||
-- not stall the main script. It prints the payload and sender it received.
|
||||
local listenerCode = string.format([[
|
||||
local sock = udpOpen(%d)
|
||||
calogPrint("listener bound on port", %d)
|
||||
local packet = udpRecvFrom(sock, 1024)
|
||||
calogPrint("listener received:", packet.data, "from", packet.host .. ":" .. packet.port)
|
||||
udpClose(sock)
|
||||
]], PORT, PORT)
|
||||
|
||||
local listener = taskSpawn("lua", listenerCode)
|
||||
calogPrint("spawned listener handle:", listener)
|
||||
|
||||
-- Give the listener a beat to bind its socket before we transmit.
|
||||
timeSleep(200)
|
||||
|
||||
local sender = udpOpen(0)
|
||||
local sent = udpSendTo(sender, "127.0.0.1", PORT, "hello over udp")
|
||||
calogPrint("sender transmitted", sent, "bytes to 127.0.0.1:" .. PORT)
|
||||
udpClose(sender)
|
||||
|
||||
-- Let the datagram land and the listener print, then tear everything down.
|
||||
timerAfter(300, function()
|
||||
taskClose(listener)
|
||||
calogExit(0)
|
||||
end)
|
||||
25
examples/scripts/libraries/pubsub.lua
Normal file
25
examples/scripts/libraries/pubsub.lua
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- Showcase the calog pubsub library within a single Lua context: two handlers
|
||||
-- subscribe to the "news" topic, a publish delivers to both (count 2), then one
|
||||
-- handler unsubscribes and a second publish delivers to just one (count 1).
|
||||
-- A short timer defers the exit so all deliveries land first.
|
||||
-- Run: bin/calog examples/scripts/libraries/pubsub.lua
|
||||
|
||||
local firstId = psSubscribe("news", function(msg)
|
||||
calogPrint("handler one got:", msg)
|
||||
end)
|
||||
|
||||
local secondId = psSubscribe("news", function(msg)
|
||||
calogPrint("handler two got:", msg)
|
||||
end)
|
||||
|
||||
local firstCount = psPublish("news", "hello subscribers")
|
||||
calogPrint("delivered to", firstCount, "handlers")
|
||||
|
||||
psUnsubscribe(firstId)
|
||||
|
||||
local secondCount = psPublish("news", "only one left")
|
||||
calogPrint("delivered to", secondCount, "handlers")
|
||||
|
||||
timerAfter(150, function()
|
||||
calogExit(0)
|
||||
end)
|
||||
57
examples/scripts/libraries/ssh.lua
Normal file
57
examples/scripts/libraries/ssh.lua
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
-- Showcase the calog ssh/sftp library as a safe-to-run, documented template:
|
||||
-- connect + password auth, run a remote command and print its stdout, then a
|
||||
-- round-trip through SFTP (upload a small payload with sftpPut, read it back
|
||||
-- with sftpGet, and verify it matches). The entire flow is wrapped in pcall,
|
||||
-- so with no reachable server it prints one clear hint and exits cleanly.
|
||||
-- Run: bin/calog examples/scripts/libraries/ssh.lua
|
||||
|
||||
-- EDIT THESE: point them at a real SSH server you control before running.
|
||||
-- The placeholder host below deliberately does not resolve, so the example is
|
||||
-- safe to run as-is: it prints a hint and exits 0 instead of touching a server.
|
||||
local HOST = "ssh.example.invalid"
|
||||
local USER = "youruser"
|
||||
local PASS = "yourpass"
|
||||
local PORT = 22
|
||||
|
||||
-- A remote path we may create and delete; keep it under /tmp so cleanup is safe.
|
||||
local REMOTE_PATH = "/tmp/calog_ssh_example.txt"
|
||||
local PAYLOAD = "hello from the calog ssh example\n"
|
||||
|
||||
-- Everything network-facing lives here so a single pcall can catch a missing
|
||||
-- server, refused connection, or auth failure without crashing the script.
|
||||
local function demo()
|
||||
local session = sshConnect(HOST, PORT)
|
||||
calogPrint("connected to", HOST .. ":" .. PORT)
|
||||
|
||||
if not sshAuthPassword(session, USER, PASS) then
|
||||
sshClose(session)
|
||||
error("password authentication was rejected for user " .. USER)
|
||||
end
|
||||
calogPrint("authenticated as", USER)
|
||||
|
||||
-- Run a command remotely and show its captured output.
|
||||
local run = sshExec(session, "uname -a")
|
||||
calogPrint("remote uname:", run.stdout)
|
||||
calogPrint("exit code:", run.exitCode)
|
||||
|
||||
-- SFTP round-trip: upload the payload, read it back, and compare.
|
||||
sftpPut(session, REMOTE_PATH, PAYLOAD)
|
||||
calogPrint("uploaded", #PAYLOAD, "bytes to", REMOTE_PATH)
|
||||
|
||||
local fetched = sftpGet(session, REMOTE_PATH)
|
||||
calogPrint("downloaded", #fetched, "bytes back")
|
||||
calogPrint("round-trip matches:", fetched == PAYLOAD)
|
||||
|
||||
-- Tidy up the remote file, then close the session.
|
||||
sftpRemove(session, REMOTE_PATH)
|
||||
sshClose(session)
|
||||
calogPrint("session closed")
|
||||
end
|
||||
|
||||
local ok, err = pcall(demo)
|
||||
if not ok then
|
||||
calogPrint("ssh example: set HOST/USER/PASS and point at a real SSH server")
|
||||
calogPrint(" (" .. tostring(err) .. ")")
|
||||
end
|
||||
|
||||
calogExit(0)
|
||||
21
examples/scripts/libraries/task.lua
Normal file
21
examples/scripts/libraries/task.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- Showcase the calog task library: taskSelf reports this context's id,
|
||||
-- taskSpawn launches a sibling "lua" child that runs its own code string and
|
||||
-- calogPrints a message, taskCount reports how many contexts are now alive.
|
||||
-- A short timer gives the child time to run before we exit, so its output lands.
|
||||
-- Run: bin/calog examples/scripts/libraries/task.lua
|
||||
|
||||
calogPrint("self id:", taskSelf())
|
||||
|
||||
local childCode = [[
|
||||
calogPrint("child says hello from task", taskSelf())
|
||||
]]
|
||||
|
||||
local child = taskSpawn("lua", childCode)
|
||||
calogPrint("spawned child handle:", child)
|
||||
|
||||
calogPrint("task count:", taskCount())
|
||||
|
||||
timerAfter(200, function()
|
||||
taskClose(child)
|
||||
calogExit(0)
|
||||
end)
|
||||
15
examples/scripts/libraries/time.lua
Normal file
15
examples/scripts/libraries/time.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- Showcase the calog time library: read the real wall-clock epoch with
|
||||
-- timeNow(), then measure a ~50ms timeSleep by sampling timeMonotonic()
|
||||
-- before and after (monotonic is immune to wall-clock adjustments).
|
||||
-- Run: bin/calog examples/scripts/libraries/time.lua
|
||||
|
||||
calogPrint("epoch now:", timeNow())
|
||||
|
||||
local start = timeMonotonic()
|
||||
timeSleep(50)
|
||||
local elapsed = timeMonotonic() - start
|
||||
|
||||
calogPrint("slept for (s):", elapsed)
|
||||
calogPrint("about 0.05s:", elapsed >= 0.04 and elapsed < 0.5)
|
||||
|
||||
calogExit(0)
|
||||
20
examples/scripts/libraries/timer.lua
Normal file
20
examples/scripts/libraries/timer.lua
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- Showcase the calog timer library with Lua callbacks: timerAfter schedules a
|
||||
-- one-shot, timerEvery fires a repeating tick, and the tick handler cancels its
|
||||
-- own timer after 3 ticks and then exits.
|
||||
-- Run: bin/calog examples/scripts/libraries/timer.lua
|
||||
|
||||
timerAfter(100, function()
|
||||
calogPrint("one-shot")
|
||||
end)
|
||||
|
||||
local ticks = 0
|
||||
|
||||
local tickId
|
||||
tickId = timerEvery(60, function()
|
||||
ticks = ticks + 1
|
||||
calogPrint("tick", ticks)
|
||||
if ticks >= 3 then
|
||||
timerCancel(tickId)
|
||||
calogExit(0)
|
||||
end
|
||||
end)
|
||||
20
examples/scripts/libraries/timerInMyBasic.bas
Normal file
20
examples/scripts/libraries/timerInMyBasic.bas
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
' Timer callbacks in my-basic. calog's my-basic fork (vendor/ourbasic) makes def/lambda
|
||||
' routines first-class values, so a BASIC script can hand a callback to a native just like
|
||||
' every other engine. Here a def is armed on a repeating timer; it counts ticks in a
|
||||
' global, and after the third tick cancels its own timer and ends the run.
|
||||
' run: bin/calog examples/scripts/libraries/timerInMyBasic.bas
|
||||
|
||||
n = 0
|
||||
tid = 0
|
||||
|
||||
def tick()
|
||||
n = n + 1
|
||||
calogPrint("my-basic tick", n)
|
||||
if n >= 3 then
|
||||
timerCancel(tid)
|
||||
calogExit(0)
|
||||
endif
|
||||
enddef
|
||||
|
||||
tid = timerEvery(80, tick)
|
||||
calogPrint("my-basic armed a repeating timer with a def callback")
|
||||
16
examples/scripts/multifile/consumer.lua
Normal file
16
examples/scripts/multifile/consumer.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- consumer.lua -- the other half of a MULTI-FILE calog run (see producer.js).
|
||||
--
|
||||
-- Run BOTH files together:
|
||||
-- bin/calog examples/scripts/multifile/producer.js examples/scripts/multifile/consumer.lua
|
||||
--
|
||||
-- Subscribes to the shared pubsub bus; when the producer (a separate .js file) signals,
|
||||
-- reads the values it left in the shared kv store, prints them, and ends the whole run.
|
||||
-- One calogExit() from any file tears the whole (multi-file) process down.
|
||||
|
||||
psSubscribe("ready", function()
|
||||
local greeting = kvGet("greeting")
|
||||
local from = kvGet("from")
|
||||
calogPrint("consumer.lua: got the signal ->", greeting, "from", from)
|
||||
calogExit(0)
|
||||
end)
|
||||
calogPrint("consumer.lua: subscribed to 'ready', waiting for the producer")
|
||||
18
examples/scripts/multifile/producer.js
Normal file
18
examples/scripts/multifile/producer.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// producer.js -- one half of a MULTI-FILE calog run (see consumer.lua).
|
||||
//
|
||||
// Run BOTH files together, in one process:
|
||||
// bin/calog examples/scripts/multifile/producer.js examples/scripts/multifile/consumer.lua
|
||||
//
|
||||
// Files listed on the command line run CONCURRENTLY and share one runtime -- so this
|
||||
// JavaScript file and the Lua file talk through the same kv store and pubsub bus.
|
||||
// This side writes the shared data, then signals "ready" after a short delay (giving the
|
||||
// consumer, loaded from the other file, time to subscribe first).
|
||||
|
||||
kvSet("greeting", "Hello");
|
||||
kvSet("from", "JavaScript (producer.js)");
|
||||
calogPrint("producer.js: wrote greeting + from into the shared kv store");
|
||||
|
||||
timerAfter(300, function () {
|
||||
calogPrint("producer.js: signalling 'ready'");
|
||||
psPublish("ready", "go");
|
||||
});
|
||||
26
examples/scripts/polyglot/exportAcrossEngines.lua
Normal file
26
examples/scripts/polyglot/exportAcrossEngines.lua
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- Cross-engine function call: Lua exports a "greet" function through the broker,
|
||||
-- then a spawned JavaScript task invokes that Lua function via calogCall and prints
|
||||
-- the string it returns. The export happens BEFORE the spawn so the child always
|
||||
-- sees the registered name; a short timer gives the JS task time to run, then exits.
|
||||
-- Run: bin/calog examples/scripts/polyglot/exportAcrossEngines.lua
|
||||
|
||||
-- Publish a Lua function into the process-wide export registry.
|
||||
calogExport("greet", function(name)
|
||||
return "hello, " .. name
|
||||
end)
|
||||
|
||||
-- The child runs on the JavaScript engine and reaches back into the Lua function.
|
||||
local jsCode = [[
|
||||
var msg = calogCall("greet", "world");
|
||||
calogPrint("javascript received:", msg);
|
||||
]]
|
||||
|
||||
local child = taskSpawn("js", jsCode)
|
||||
calogPrint("spawned javascript task:", child)
|
||||
|
||||
-- Give the JS task time to run its call, then tear everything down.
|
||||
timerAfter(300, function()
|
||||
taskClose(child)
|
||||
calogUnexport("greet")
|
||||
calogExit(0)
|
||||
end)
|
||||
29
examples/scripts/polyglot/pubsubAcrossEngines.lua
Normal file
29
examples/scripts/polyglot/pubsubAcrossEngines.lua
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- Cross-engine pubsub: Lua subscribes to the "events" topic through the broker,
|
||||
-- then a spawned JavaScript task publishes a message on that same topic. Because
|
||||
-- pubsub is process-wide, the Lua handler receives the JS-published message and
|
||||
-- prints it. The subscribe happens BEFORE the spawn so the child's publish is
|
||||
-- never missed; a short timer gives delivery time to land, then exits.
|
||||
-- Run: bin/calog examples/scripts/polyglot/pubsubAcrossEngines.lua
|
||||
|
||||
-- Register the Lua handler first so the topic has a listener before anyone publishes.
|
||||
local subId = psSubscribe("events", function(msg)
|
||||
calogPrint("lua handler received:", msg)
|
||||
end)
|
||||
|
||||
calogPrint("lua subscribed to 'events' with id:", subId)
|
||||
|
||||
-- The child runs on the JavaScript engine and publishes into the shared topic.
|
||||
local jsCode = [[
|
||||
var count = psPublish("events", "from JavaScript");
|
||||
calogPrint("javascript published to", count, "subscriber(s)");
|
||||
]]
|
||||
|
||||
local child = taskSpawn("js", jsCode)
|
||||
calogPrint("spawned javascript task:", child)
|
||||
|
||||
-- Give the JS publish time to be delivered to the Lua handler, then tear down.
|
||||
timerAfter(300, function()
|
||||
taskClose(child)
|
||||
psUnsubscribe(subId)
|
||||
calogExit(0)
|
||||
end)
|
||||
30
examples/scripts/polyglot/sharedKv.lua
Normal file
30
examples/scripts/polyglot/sharedKv.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-- Shared key/value store across engines: Lua seeds the process-wide kv with a JSON
|
||||
-- config blob and a numeric counter, then a spawned JavaScript task reads both back
|
||||
-- (parsing the JSON), prints them, and bumps the counter to 42. Because kv is shared
|
||||
-- across every engine, Lua sees the JS write: after a short timer it reads the counter
|
||||
-- again, confirms it is now 42, then tears everything down.
|
||||
-- Run: bin/calog examples/scripts/polyglot/sharedKv.lua
|
||||
|
||||
-- Seed shared state from Lua before spawning the child.
|
||||
kvSet("config", jsonStringify({ name = "calog", version = 1 }))
|
||||
kvSet("counter", 41)
|
||||
calogPrint("lua seeded config and counter =", kvGet("counter"))
|
||||
|
||||
-- The child runs on the JavaScript engine and reads/writes the same shared kv.
|
||||
local jsCode = [[
|
||||
var config = jsonParse(kvGet("config"));
|
||||
calogPrint("javascript read config name:", config.name, "version:", config.version);
|
||||
calogPrint("javascript read counter:", kvGet("counter"));
|
||||
kvSet("counter", 42);
|
||||
calogPrint("javascript set counter to 42");
|
||||
]]
|
||||
|
||||
local child = taskSpawn("js", jsCode)
|
||||
calogPrint("spawned javascript task:", child)
|
||||
|
||||
-- Give the JS task time to run its read/write, then observe the shared write from Lua.
|
||||
timerAfter(300, function()
|
||||
calogPrint("lua sees counter =", kvGet("counter"))
|
||||
taskClose(child)
|
||||
calogExit(0)
|
||||
end)
|
||||
34
examples/scripts/polyglot/taskFanout.lua
Normal file
34
examples/scripts/polyglot/taskFanout.lua
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- Polyglot task fan-out: one Lua script spawns a tiny task on FIVE different
|
||||
-- engines (JavaScript, Squirrel, Berry, Scheme/s7, Wren). Each child prints a
|
||||
-- one-line hello in its own language through the shared calogPrint native, so a
|
||||
-- single run shows all five runtimes executing side by side under one broker.
|
||||
-- The Scheme child is wrapped in (begin ...) because s7 evaluates one form; the
|
||||
-- Wren child reaches natives via Calog.call. A short timer lets every child run
|
||||
-- before we tear down and exit.
|
||||
-- Run: bin/calog examples/scripts/polyglot/taskFanout.lua
|
||||
|
||||
-- Each entry pairs a taskSpawn engine name with a minimal, idiomatic snippet.
|
||||
local children = {
|
||||
{ engine = "js", code = [[calogPrint("javascript: hello from JavaScript");]] },
|
||||
{ engine = "squirrel", code = [[calogPrint("squirrel: hello from Squirrel");]] },
|
||||
{ engine = "berry", code = [[calogPrint("berry: hello from Berry")]] },
|
||||
{ engine = "s7", code = [[(begin (calogPrint "scheme: hello from Scheme"))]] },
|
||||
{ engine = "wren", code = [[Calog.call("calogPrint", ["wren: hello from Wren"])]] },
|
||||
}
|
||||
|
||||
local handles = {}
|
||||
|
||||
for index = 1, #children do
|
||||
local child = children[index]
|
||||
local handle = taskSpawn(child.engine, child.code)
|
||||
handles[index] = handle
|
||||
calogPrint("spawned", child.engine, "task:", handle)
|
||||
end
|
||||
|
||||
-- Give all five children time to run their single print, then close and exit.
|
||||
timerAfter(400, function()
|
||||
for index = 1, #handles do
|
||||
taskClose(handles[index])
|
||||
end
|
||||
calogExit(0)
|
||||
end)
|
||||
|
|
@ -20,9 +20,7 @@
|
|||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef CALOG_WITH_ENET
|
||||
#include <enet/enet.h>
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
534
src/calogMain.c
Normal file
534
src/calogMain.c
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
// calogMain.c -- the `calog` command-line runner.
|
||||
//
|
||||
// Links every engine and every library into one binary, then executes the script files named
|
||||
// on the command line. A file named with a known extension runs on that engine (.lua .js .nut
|
||||
// .bas .be .scm .wren); a name with no recognized extension is searched for as <name>.<ext> in
|
||||
// engine-registration priority order (the first existing file wins), matching calogContextLoad.
|
||||
//
|
||||
// Each script is launched fire-and-forget on its own context, and the process pumps the host
|
||||
// thread so scripts can call back into the registered natives (print, the libraries). Because
|
||||
// calog is event-driven, a script finishing its top level does NOT end the program -- its
|
||||
// context stays alive to service timer/pubsub/task callbacks -- so there is no way to infer
|
||||
// "all processing is done". Instead a script calls calogExit([code]) to tear everything down
|
||||
// and exit; SIGINT/SIGTERM do the same. A script that ERRORS has its context retired (closed);
|
||||
// once no launched contexts remain live, calog exits on its own. Absent any of these, calog
|
||||
// runs until killed.
|
||||
//
|
||||
// Teardown order matters (see the library headers): the cross-context reference holders
|
||||
// (export/pubsub/timer/kv) are released while contexts are still alive, THEN calogDestroy joins
|
||||
// every context thread, THEN the libraries whose registries outlive the runtime (db/net/ssh/
|
||||
// task) are freed.
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "calog.h"
|
||||
|
||||
#include "calogCrypto.h"
|
||||
#include "calogDb.h"
|
||||
#include "calogExport.h"
|
||||
#include "calogFs.h"
|
||||
#include "calogHttp.h"
|
||||
#include "calogJson.h"
|
||||
#include "calogKv.h"
|
||||
#include "calogNet.h"
|
||||
#include "calogPubsub.h"
|
||||
#include "calogSsh.h"
|
||||
#include "calogTask.h"
|
||||
#include "calogTime.h"
|
||||
#include "calogTimer.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
|
||||
// One host-thread pump iteration parks this long between drains (0.5 ms), matching the test
|
||||
// harness -- long enough not to spin a core, short enough to feel responsive.
|
||||
#define PUMP_INTERVAL_NS 500000
|
||||
|
||||
// The base for a signal-derived exit code (128 + signal number), the shell convention.
|
||||
#define SIGNAL_EXIT_BASE 128
|
||||
|
||||
static const CalogEngineT *engineForExtension(const char *ext);
|
||||
static const char *extensionOf(const char *arg);
|
||||
static bool fileReadable(const char *path);
|
||||
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||
static int32_t nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||
static void onSignal(int sig);
|
||||
static void printUsage(FILE *stream, const char *program);
|
||||
static void printValue(const CalogValueT *value);
|
||||
static char *readFile(const char *path);
|
||||
static bool resolveArg(const char *arg, const CalogEngineT **outEngine, char **outSource);
|
||||
|
||||
// Requested by a script (calogExit), a signal, or the last live context erroring out; the host
|
||||
// pump loop watches gShutdown and exits with gExitCode.
|
||||
static _Atomic bool gShutdown = false;
|
||||
static _Atomic int32_t gExitCode = 0;
|
||||
|
||||
// One entry per script context the runner launched. onError flags `failed` for a context whose
|
||||
// script errors; the pump loop then closes that context and drops the live count. When the live
|
||||
// count reaches zero -- every launched context has errored out -- the runner exits.
|
||||
typedef struct LaunchedT {
|
||||
uint64_t id;
|
||||
CalogContextT *context; // NULL once the context has been closed
|
||||
_Atomic bool failed;
|
||||
} LaunchedT;
|
||||
|
||||
static LaunchedT *gLaunched = NULL;
|
||||
static int32_t gLaunchedCount = 0;
|
||||
|
||||
// The engines the runner knows, in load-search priority order (the same order
|
||||
// calogRegisterBuiltinEngines uses, so an extensionless name resolves identically here and via
|
||||
// taskLoad). Referencing the vtables directly pulls each engine archive into the link.
|
||||
static const CalogEngineT *const gEngines[] = {
|
||||
&calogLuaEngine,
|
||||
&calogJsEngine,
|
||||
&calogSquirrelEngine,
|
||||
&calogMyBasicEngine,
|
||||
&calogBerryEngine,
|
||||
&calogS7Engine,
|
||||
&calogWrenEngine
|
||||
};
|
||||
|
||||
// s7 interns a small, bounded set of "permanent" strings it never reclaims (an s7 trait, not a
|
||||
// leak); suppress exactly that site so an ASan/LSan build of the runner stays quiet. LSan calls
|
||||
// this weak hook automatically.
|
||||
const char *__lsan_default_suppressions(void);
|
||||
const char *__lsan_default_suppressions(void) {
|
||||
return "leak:make_permanent_string\n";
|
||||
}
|
||||
|
||||
// Keep the "Suppressions used" banner off stdout/stderr on a clean run.
|
||||
const char *__lsan_default_options(void);
|
||||
const char *__lsan_default_options(void) {
|
||||
return "print_suppressions=0";
|
||||
}
|
||||
|
||||
|
||||
// Find the engine that claims file extension ext (case-insensitive), or NULL if none does.
|
||||
static const CalogEngineT *engineForExtension(const char *ext) {
|
||||
size_t index;
|
||||
|
||||
for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
|
||||
const CalogEngineT *engine;
|
||||
int32_t which;
|
||||
|
||||
engine = gEngines[index];
|
||||
if (engine->extensions == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (which = 0; engine->extensions[which] != NULL; which++) {
|
||||
if (strcasecmp(engine->extensions[which], ext) == 0) {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Return a pointer to the extension of arg (the bytes after the last '.' of the final path
|
||||
// component), or NULL when there is none -- a name with no dot, or a leading-dot dotfile.
|
||||
static const char *extensionOf(const char *arg) {
|
||||
const char *base;
|
||||
const char *slash;
|
||||
const char *dot;
|
||||
|
||||
slash = strrchr(arg, '/');
|
||||
base = (slash != NULL) ? slash + 1 : arg;
|
||||
dot = strrchr(base, '.');
|
||||
if (dot == NULL || dot == base) {
|
||||
return NULL;
|
||||
}
|
||||
return dot + 1;
|
||||
}
|
||||
|
||||
|
||||
static bool fileReadable(const char *path) {
|
||||
FILE *file;
|
||||
|
||||
file = fopen(path, "rb");
|
||||
if (file == NULL) {
|
||||
return false;
|
||||
}
|
||||
fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// calogExit([code]) -- a script asks the runner to tear everything down and exit. Inline: it
|
||||
// only sets two atomics, which is thread-safe, so it needs no host hop.
|
||||
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||
(void)userData;
|
||||
calogValueNil(result);
|
||||
if (argCount >= 1) {
|
||||
if (args[0].type == calogIntE) {
|
||||
atomic_store(&gExitCode, (int32_t)args[0].as.i);
|
||||
} else if (args[0].type == calogRealE) {
|
||||
atomic_store(&gExitCode, (int32_t)args[0].as.r);
|
||||
} else {
|
||||
return calogFail(result, calogErrArgE, "calogExit expects an optional integer exit code");
|
||||
}
|
||||
}
|
||||
atomic_store(&gShutdown, true);
|
||||
return calogOkE;
|
||||
}
|
||||
|
||||
|
||||
// calogPrint(...) -- write each argument to stdout, space-separated, with a trailing newline. A
|
||||
// host native (runs on the host thread), so concurrent scripts' lines do not interleave. Named
|
||||
// with the calog prefix (like calogExit) so it never collides with an engine's built-in print
|
||||
// or the my-basic PRINT keyword.
|
||||
static int32_t nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||
int32_t index;
|
||||
|
||||
(void)userData;
|
||||
calogValueNil(result);
|
||||
for (index = 0; index < argCount; index++) {
|
||||
if (index > 0) {
|
||||
fputc(' ', stdout);
|
||||
}
|
||||
printValue(&args[index]);
|
||||
}
|
||||
fputc('\n', stdout);
|
||||
fflush(stdout);
|
||||
return calogOkE;
|
||||
}
|
||||
|
||||
|
||||
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||
int32_t index;
|
||||
|
||||
(void)userData;
|
||||
fprintf(stderr, "calog: script error: %s\n", (message != NULL) ? message : "(unknown)");
|
||||
// Flag the failing context so the pump loop retires it (a script error closes its context).
|
||||
for (index = 0; index < gLaunchedCount; index++) {
|
||||
if (gLaunched[index].id == contextId) {
|
||||
atomic_store(&gLaunched[index].failed, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Signal handler for SIGINT/SIGTERM: request the same orderly shutdown as calogExit. Only
|
||||
// async-signal-safe atomic stores happen here.
|
||||
static void onSignal(int sig) {
|
||||
atomic_store(&gExitCode, (int32_t)(SIGNAL_EXIT_BASE + sig));
|
||||
atomic_store(&gShutdown, true);
|
||||
}
|
||||
|
||||
|
||||
static void printUsage(FILE *stream, const char *program) {
|
||||
fprintf(stream, "usage: %s <script> [script ...]\n", program);
|
||||
fprintf(stream, "\n");
|
||||
fprintf(stream, "Run each script through the calog multi-language runtime.\n");
|
||||
fprintf(stream, "A name with a known extension runs on that engine:\n");
|
||||
fprintf(stream, " .lua .js .nut .bas .be .scm .wren\n");
|
||||
fprintf(stream, "A name with no recognized extension is searched for as <name>.<ext>\n");
|
||||
fprintf(stream, "in that priority order; the first existing file wins.\n");
|
||||
fprintf(stream, "\n");
|
||||
fprintf(stream, "Scripts share the crypto, db, export, fs, http, json, kv, net, pubsub,\n");
|
||||
fprintf(stream, "ssh, task, time, and timer libraries plus calogPrint(). Call calogExit([code])\n");
|
||||
fprintf(stream, "to tear everything down and exit; otherwise calog runs until interrupted.\n");
|
||||
}
|
||||
|
||||
|
||||
static void printValue(const CalogValueT *value) {
|
||||
switch (value->type) {
|
||||
case calogNilE:
|
||||
fputs("nil", stdout);
|
||||
break;
|
||||
case calogBoolE:
|
||||
fputs(value->as.b ? "true" : "false", stdout);
|
||||
break;
|
||||
case calogIntE:
|
||||
printf("%lld", (long long)value->as.i);
|
||||
break;
|
||||
case calogRealE:
|
||||
printf("%g", value->as.r);
|
||||
break;
|
||||
case calogStringE:
|
||||
fwrite(value->as.s.bytes, 1, (size_t)value->as.s.length, stdout);
|
||||
break;
|
||||
case calogAggE:
|
||||
fputs("[aggregate]", stdout);
|
||||
break;
|
||||
case calogFnE:
|
||||
fputs("[function]", stdout);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Read a whole file into a fresh, NUL-terminated buffer (binary content is preserved up to the
|
||||
// terminator; scripts are text). Returns NULL on failure, leaving errno set.
|
||||
static char *readFile(const char *path) {
|
||||
FILE *file;
|
||||
char *buffer;
|
||||
long size;
|
||||
size_t readCount;
|
||||
|
||||
file = fopen(path, "rb");
|
||||
if (file == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (fseek(file, 0, SEEK_END) != 0 || (size = ftell(file)) < 0) {
|
||||
fclose(file);
|
||||
return NULL;
|
||||
}
|
||||
rewind(file);
|
||||
buffer = (char *)malloc((size_t)size + 1);
|
||||
if (buffer == NULL) {
|
||||
fclose(file);
|
||||
return NULL;
|
||||
}
|
||||
readCount = fread(buffer, 1, (size_t)size, file);
|
||||
fclose(file);
|
||||
buffer[readCount] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
// Resolve one command-line argument to an engine + its script source (a fresh buffer the caller
|
||||
// frees). A recognized extension binds the exact file to that engine; otherwise the argument is
|
||||
// treated as a base name and searched as <arg>.<ext> across the engines in priority order. On
|
||||
// failure prints a diagnostic and returns false.
|
||||
static bool resolveArg(const char *arg, const CalogEngineT **outEngine, char **outSource) {
|
||||
const char *ext;
|
||||
const CalogEngineT *engine;
|
||||
char *source;
|
||||
size_t index;
|
||||
|
||||
*outEngine = NULL;
|
||||
*outSource = NULL;
|
||||
|
||||
ext = extensionOf(arg);
|
||||
if (ext != NULL) {
|
||||
engine = engineForExtension(ext);
|
||||
if (engine != NULL) {
|
||||
source = readFile(arg);
|
||||
if (source == NULL) {
|
||||
fprintf(stderr, "calog: %s: %s\n", arg, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
*outEngine = engine;
|
||||
*outSource = source;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
|
||||
int32_t which;
|
||||
|
||||
engine = gEngines[index];
|
||||
if (engine->extensions == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (which = 0; engine->extensions[which] != NULL; which++) {
|
||||
char *path;
|
||||
size_t pathSize;
|
||||
|
||||
pathSize = strlen(arg) + strlen(engine->extensions[which]) + 2; // '.' + '\0'
|
||||
path = (char *)malloc(pathSize);
|
||||
if (path == NULL) {
|
||||
fprintf(stderr, "calog: out of memory\n");
|
||||
return false;
|
||||
}
|
||||
snprintf(path, pathSize, "%s.%s", arg, engine->extensions[which]);
|
||||
if (!fileReadable(path)) {
|
||||
free(path);
|
||||
continue;
|
||||
}
|
||||
source = readFile(path);
|
||||
if (source == NULL) {
|
||||
fprintf(stderr, "calog: %s: %s\n", path, strerror(errno));
|
||||
free(path);
|
||||
return false;
|
||||
}
|
||||
free(path);
|
||||
*outEngine = engine;
|
||||
*outSource = source;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "calog: %s: no runnable script found\n", arg);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const CalogEngineT **engines;
|
||||
char **sources;
|
||||
const char *program;
|
||||
CalogT *calog;
|
||||
struct timespec tick = { 0, PUMP_INTERVAL_NS };
|
||||
int32_t scriptCount;
|
||||
int32_t liveCount;
|
||||
int32_t index;
|
||||
|
||||
program = strrchr(argv[0], '/');
|
||||
program = (program != NULL) ? program + 1 : argv[0];
|
||||
|
||||
if (argc < 2) {
|
||||
printUsage(stderr, program);
|
||||
return 2;
|
||||
}
|
||||
for (index = 1; index < argc; index++) {
|
||||
if (strcmp(argv[index], "-h") == 0 || strcmp(argv[index], "--help") == 0) {
|
||||
printUsage(stdout, program);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
scriptCount = argc - 1;
|
||||
engines = (const CalogEngineT **)malloc((size_t)scriptCount * sizeof(*engines));
|
||||
sources = (char **)malloc((size_t)scriptCount * sizeof(*sources));
|
||||
if (engines == NULL || sources == NULL) {
|
||||
fprintf(stderr, "calog: out of memory\n");
|
||||
free(engines);
|
||||
free(sources);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolve every argument before running any (fail fast): one bad name runs nothing.
|
||||
for (index = 0; index < scriptCount; index++) {
|
||||
if (!resolveArg(argv[index + 1], &engines[index], &sources[index])) {
|
||||
int32_t undo;
|
||||
for (undo = 0; undo < index; undo++) {
|
||||
free(sources[undo]);
|
||||
}
|
||||
free(engines);
|
||||
free(sources);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
calog = calogCreate();
|
||||
if (calog == NULL) {
|
||||
fprintf(stderr, "calog: failed to create runtime\n");
|
||||
for (index = 0; index < scriptCount; index++) {
|
||||
free(sources[index]);
|
||||
}
|
||||
free(engines);
|
||||
free(sources);
|
||||
return 1;
|
||||
}
|
||||
calogSetErrorHandler(calog, onError, NULL);
|
||||
|
||||
if (calogRegister(calog, "calogPrint", nativeCalogPrint, NULL) != calogOkE ||
|
||||
calogRegisterInline(calog, "calogExit", nativeCalogExit, NULL) != calogOkE ||
|
||||
calogCryptoRegister(calog) != calogOkE ||
|
||||
calogDbRegister(calog) != calogOkE ||
|
||||
calogExportRegister(calog) != calogOkE ||
|
||||
calogFsRegister(calog) != calogOkE ||
|
||||
calogHttpRegister(calog) != calogOkE ||
|
||||
calogJsonRegister(calog) != calogOkE ||
|
||||
calogKvRegister(calog) != calogOkE ||
|
||||
calogNetRegister(calog) != calogOkE ||
|
||||
calogPubsubRegister(calog) != calogOkE ||
|
||||
calogSshRegister(calog) != calogOkE ||
|
||||
calogTaskRegister(calog) != calogOkE ||
|
||||
calogTimeRegister(calog) != calogOkE ||
|
||||
calogTimerRegister(calog) != calogOkE) {
|
||||
fprintf(stderr, "calog: failed to register libraries\n");
|
||||
calogDestroy(calog);
|
||||
for (index = 0; index < scriptCount; index++) {
|
||||
free(sources[index]);
|
||||
}
|
||||
free(engines);
|
||||
free(sources);
|
||||
return 1;
|
||||
}
|
||||
calogRegisterBuiltinEngines(calog);
|
||||
|
||||
signal(SIGINT, onSignal);
|
||||
signal(SIGTERM, onSignal);
|
||||
|
||||
// Launch every script fire-and-forget on its own context, recording the live ones so a
|
||||
// later error can retire exactly that context. calogContextEval keeps its own copy of the
|
||||
// source, so the resolved buffer is freed right after.
|
||||
gLaunched = (LaunchedT *)calloc((size_t)scriptCount, sizeof(*gLaunched));
|
||||
if (gLaunched == NULL) {
|
||||
fprintf(stderr, "calog: out of memory\n");
|
||||
calogDestroy(calog);
|
||||
for (index = 0; index < scriptCount; index++) {
|
||||
free(sources[index]);
|
||||
}
|
||||
free(engines);
|
||||
free(sources);
|
||||
return 1;
|
||||
}
|
||||
for (index = 0; index < scriptCount; index++) {
|
||||
CalogContextT *context;
|
||||
|
||||
context = calogContextOpen(calog, engines[index]);
|
||||
if (context == NULL) {
|
||||
fprintf(stderr, "calog: %s: failed to open a context\n", argv[index + 1]);
|
||||
} else if (calogContextEval(context, sources[index]) != calogOkE) {
|
||||
fprintf(stderr, "calog: %s: failed to start script\n", argv[index + 1]);
|
||||
calogContextClose(context);
|
||||
} else {
|
||||
gLaunched[gLaunchedCount].id = calogContextId(context);
|
||||
gLaunched[gLaunchedCount].context = context;
|
||||
atomic_store(&gLaunched[gLaunchedCount].failed, false);
|
||||
gLaunchedCount++;
|
||||
}
|
||||
free(sources[index]);
|
||||
}
|
||||
free(engines);
|
||||
free(sources);
|
||||
|
||||
liveCount = gLaunchedCount;
|
||||
if (liveCount == 0) {
|
||||
atomic_store(&gShutdown, true); // nothing runnable launched
|
||||
}
|
||||
|
||||
// Service script->native calls until a script calls calogExit, we are signalled, or every
|
||||
// launched context has errored out. A script error retires (closes) its own context; when
|
||||
// the last live context is gone, the runner exits.
|
||||
while (!atomic_load(&gShutdown)) {
|
||||
calogPump(calog);
|
||||
for (index = 0; index < gLaunchedCount; index++) {
|
||||
if (gLaunched[index].context != NULL && atomic_load(&gLaunched[index].failed)) {
|
||||
int32_t expected;
|
||||
calogContextClose(gLaunched[index].context);
|
||||
gLaunched[index].context = NULL;
|
||||
liveCount--;
|
||||
expected = 0;
|
||||
atomic_compare_exchange_strong(&gExitCode, &expected, 1);
|
||||
}
|
||||
}
|
||||
if (liveCount == 0) {
|
||||
atomic_store(&gShutdown, true);
|
||||
break;
|
||||
}
|
||||
nanosleep(&tick, NULL);
|
||||
}
|
||||
calogPump(calog);
|
||||
|
||||
// Release cross-context references while the contexts are still alive, tear the runtime
|
||||
// down (joining every context thread), then free the registries that outlive it.
|
||||
calogExportShutdown();
|
||||
calogPubsubShutdown();
|
||||
calogTimerShutdown();
|
||||
calogKvShutdown();
|
||||
calogDestroy(calog);
|
||||
calogDbShutdown();
|
||||
calogNetShutdown();
|
||||
calogSshShutdown();
|
||||
calogTaskShutdown();
|
||||
|
||||
free(gLaunched);
|
||||
return (int)atomic_load(&gExitCode);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
// currentL so an exported BASIC routine can be evaluated through mb_eval_routine.
|
||||
|
||||
#include "mybasicAdapter.h"
|
||||
#include "myBasic.h"
|
||||
#include "ourBasic.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -423,16 +423,13 @@ static int32_t mbCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT
|
|||
routine = (MyBasicRoutineT *)userData;
|
||||
context = routine->context;
|
||||
calogValueNil(result);
|
||||
if (context->currentL == NULL) {
|
||||
return calogFail(result, calogErrUnsupportedE, "BASIC routine invoked outside a serving frame");
|
||||
}
|
||||
mbArgs = NULL;
|
||||
if (argCount > 0) {
|
||||
mbArgs = (mb_value_t *)calloc((size_t)argCount, sizeof(mb_value_t));
|
||||
// Always allocate a non-NULL args buffer (at least one slot): mb_eval_routine treats a NULL
|
||||
// args pointer as "parse the arguments from source", which fails for a cold / no-source call
|
||||
// -- so even a zero-argument callback needs a non-NULL (direct-args) buffer.
|
||||
mbArgs = (mb_value_t *)calloc((size_t)(argCount > 0 ? argCount : 1), sizeof(mb_value_t));
|
||||
if (mbArgs == NULL) {
|
||||
return calogFail(result, calogErrOomE, "out of memory marshalling routine args");
|
||||
}
|
||||
}
|
||||
for (index = 0; index < argCount; index++) {
|
||||
mb_make_nil(mbArgs[index]);
|
||||
status = mbFromValueDepth(context, context->currentL, &args[index], &mbArgs[index], 0);
|
||||
|
|
@ -446,7 +443,15 @@ static int32_t mbCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT
|
|||
}
|
||||
}
|
||||
mb_make_nil(ret);
|
||||
if (context->currentL != NULL) {
|
||||
// Synchronous: invoked while this context is serving a native call (e.g. an exported
|
||||
// routine reached via calogMyBasicExportRoutine) -- run it in that live frame.
|
||||
code = mb_eval_routine(context->bas, context->currentL, routine->routine, mbArgs, (unsigned)argCount, &ret);
|
||||
} else {
|
||||
// [calog fork] Deferred callback (timer/pubsub/export delivered to an idle interpreter):
|
||||
// invoke the routine cold -- mb_eval_routine_cold supplies its own return frame.
|
||||
code = mb_eval_routine_cold(context->bas, routine->routine, mbArgs, (unsigned)argCount, &ret);
|
||||
}
|
||||
if (code == MB_FUNC_OK) {
|
||||
// Marshal the return value BEFORE releasing the arguments: a routine may
|
||||
// return one of its (borrowed) arguments, so result must be deep-copied
|
||||
|
|
@ -586,6 +591,12 @@ static int mbDispatch(int32_t slot, struct mb_interpreter_t *s, void **l) {
|
|||
mb_dispose_value(context->bas, popped);
|
||||
}
|
||||
if (status != calogOkE) {
|
||||
// A refused routine argument (e.g. a lambda created just for this call) was popped
|
||||
// and is ours to free; dispose it so its scope is not leaked on the error path.
|
||||
// (Strings are borrowed interior pointers and collections are disposed just above.)
|
||||
if (popped.type == MB_DT_ROUTINE) {
|
||||
mb_dispose_value(context->bas, popped);
|
||||
}
|
||||
code = mb_raise_error(s, l, MB_NATIVE_ERROR, MB_FUNC_ERR);
|
||||
goto cleanupArgs;
|
||||
}
|
||||
|
|
@ -767,6 +778,12 @@ static int32_t mbToValueDepth(CalogMyBasicT *context, void **l, const mb_value_t
|
|||
case MB_DT_DICT:
|
||||
return mbDictToAggregate(context, l, *value, out, depth);
|
||||
case MB_DT_ROUTINE:
|
||||
// [calog fork] Marshal a def/lambda routine to a first-class calog callable, so a
|
||||
// BASIC script can hand a callback to a native (pubsub/timer/export) like any other
|
||||
// engine. The routine value is borrowed (owned by the interpreter's scope) and is
|
||||
// invoked later via mb_eval_routine_cold, so it must have a PERSISTENT scope -- a
|
||||
// top-level def or lambda. (A routine local to a function would dangle once that
|
||||
// function returns; callbacks are registered from the top level, as in every engine.)
|
||||
status = mbWrapRoutine(context, *value, &callable);
|
||||
if (status != calogOkE) {
|
||||
return status;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
// vtable, so a my-basic interpreter runs on its own context thread and is loadable by
|
||||
// file extension (.bas) via calogContextLoad.
|
||||
//
|
||||
// my-basic keeps a little process-global state, but only two things are shared across
|
||||
// interpreters: an allocation counter (patched to _Atomic in vendor/mybasic/myBasic.c,
|
||||
// preserved as myBasic.c.orig) and the mb_init singletons, which are built once and are
|
||||
// read-only thereafter. So interpreters EXECUTE in parallel; only lifecycle needs
|
||||
// The engine is vendor/ourbasic -- calog's fork of MY-BASIC (see vendor/ourbasic/NOTICE +
|
||||
// CHANGELOG; pristine upstream kept as ourBasic.c.upstream). my-basic keeps a little
|
||||
// process-global state, but only two things are shared across interpreters: an allocation
|
||||
// counter (patched to _Atomic in vendor/ourbasic/ourBasic.c) and the mb_init singletons,
|
||||
// which are built once and are read-only thereafter. So interpreters EXECUTE in parallel;
|
||||
// only lifecycle needs
|
||||
// serializing -- mb_init's lazy build on the first context, mb_dispose on the last, and
|
||||
// the adapter's shared context refcount. This lock covers just create and destroy;
|
||||
// runSource runs unlocked, so several my-basic scripts run at once.
|
||||
|
|
|
|||
40
vendor/ourbasic/CHANGELOG
vendored
Normal file
40
vendor/ourbasic/CHANGELOG
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
our-basic CHANGELOG -- calog's modifications to upstream MY-BASIC
|
||||
================================================================
|
||||
|
||||
Every change below is marked in-source with the comment tag "[calog fork]".
|
||||
Baseline: ourBasic.c.upstream (pristine upstream MY-BASIC). Run
|
||||
diff ourBasic.c.upstream ourBasic.c
|
||||
to see the full patch set.
|
||||
|
||||
Concurrency
|
||||
- The _mb_allocated allocation counter was changed from `volatile` to `_Atomic`,
|
||||
so independent interpreters (one per calog context thread) can be created and
|
||||
destroyed concurrently without racing the counter. Execution already runs
|
||||
unlocked; only lifecycle is serialized, in the adapter.
|
||||
|
||||
Memory
|
||||
- A my-basic routine (lambda) popped as a refused/errored native argument is now
|
||||
disposed on the error path so its scope is not leaked. (In the adapter's
|
||||
arg-marshal loop, src/mybasic/mybasicAdapter.c.)
|
||||
|
||||
First-class routines -- the reason for the fork
|
||||
- Bare-def-as-value: in _calc_expression, a routine identifier NOT followed by
|
||||
'(' now pushes the routine as an operand (a first-class value) instead of
|
||||
raising "Open bracket expected". A script can now pass a `def` (or a lambda) to
|
||||
a native and store it in a variable -- required for cross-engine callbacks
|
||||
(pubsub / timer / export). It mirrors the array-index branch beside it; calls,
|
||||
recursion, and closures are unchanged.
|
||||
- mb_eval_routine_cold(): a new public entry (declared in ourBasic.h) that invokes
|
||||
a routine value from an IDLE interpreter -- one with no live call frame. It
|
||||
supplies the last AST node as a clean return landing and passes arguments
|
||||
directly. calog uses it to fire script callbacks that were registered earlier
|
||||
and delivered when the interpreter is otherwise idle.
|
||||
|
||||
Constraint: a callback must be a TOP-LEVEL def/lambda (persistent scope); a routine
|
||||
local to a function would dangle once that function returns -- the same rule every
|
||||
engine's closures follow.
|
||||
|
||||
Verified: full calog `make test` (28 binaries, 0 failed) plus the my-basic engine
|
||||
suite (testMyBasic / testEngineMyBasic / testPolyglot / testLoad / testTask),
|
||||
ASan/UBSan-clean, with def and lambda callbacks firing via timer/pubsub/export and
|
||||
cross-engine (a Lua task calling a my-basic def).
|
||||
45
vendor/ourbasic/NOTICE
vendored
Normal file
45
vendor/ourbasic/NOTICE
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
our-basic -- calog's fork of MY-BASIC
|
||||
=====================================
|
||||
|
||||
This directory (vendor/ourbasic) is calog's vendored fork of MY-BASIC, a small
|
||||
embeddable BASIC interpreter by Tony Wang.
|
||||
|
||||
Upstream: MY-BASIC -- https://github.com/paladin-t/my_basic/
|
||||
License: MIT (the permission notice is reproduced in the header of every
|
||||
source file: ourBasic.c and ourBasic.h)
|
||||
Copyright (C) 2011 - 2026 Tony Wang
|
||||
|
||||
MY-BASIC is MIT-licensed, which permits this fork. calog's modifications are marked
|
||||
in-source with the comment tag "[calog fork]" and are catalogued in CHANGELOG (this
|
||||
directory).
|
||||
|
||||
ourBasic.c the forked interpreter (patched)
|
||||
ourBasic.h the forked public header (patched)
|
||||
ourBasic.c.upstream the pristine upstream MY-BASIC source, kept as the diff
|
||||
baseline: `diff ourBasic.c.upstream ourBasic.c` shows the fork
|
||||
|
||||
The internal C API keeps its upstream "mb_" prefix (renaming it would be pointless
|
||||
churn). The BASIC dialect calog exposes to scripts is still called "my-basic" -- the
|
||||
".bas" extension, the "mybasic" engine name, and CALOG_WITH_MYBASIC are unchanged.
|
||||
"our-basic" names only this modified implementation.
|
||||
|
||||
--- MIT License (MY-BASIC) ---
|
||||
|
||||
Copyright (C) 2011 - 2026 Tony Wang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
# endif /* _CRT_SECURE_NO_WARNINGS */
|
||||
#endif /* _MSC_VER */
|
||||
|
||||
#include "myBasic.h"
|
||||
#include "ourBasic.h"
|
||||
#if defined ARDUINO && !defined MB_CP_ARDUINO
|
||||
# define MB_CP_ARDUINO
|
||||
#endif /* ARDUINO && !MB_CP_ARDUINO */
|
||||
|
|
@ -3980,6 +3980,16 @@ _array:
|
|||
_ls_pushback(opnd, c);
|
||||
f++;
|
||||
} else if(c->type == _DT_ROUTINE) {
|
||||
/* [calog fork] A routine identifier NOT followed by '(' is a first-class value
|
||||
reference (like the array case below), not a call -- push it as an operand so
|
||||
scripts can pass def/lambda routines to natives and store them in variables. */
|
||||
if(ast && !_IS_FUNC((_object_t*)ast->data, _core_open_bracket)) {
|
||||
if(f) {
|
||||
_handle_error_on_obj(s, SE_RN_OPERATOR_EXPECTED, s->source_file, DON(ast), MB_FUNC_ERR, _error, result);
|
||||
}
|
||||
_ls_pushback(opnd, c);
|
||||
f++;
|
||||
} else {
|
||||
_routine:
|
||||
do {
|
||||
#ifdef MB_ENABLE_CLASS
|
||||
|
|
@ -4031,6 +4041,7 @@ _routine:
|
|||
}
|
||||
_ls_pushback(opnd, c);
|
||||
f++;
|
||||
}
|
||||
} else if(c->type == _DT_VAR && c->data.variable->data->type == _DT_ARRAY) {
|
||||
unsigned arr_idx = 0;
|
||||
mb_value_u arr_val;
|
||||
|
|
@ -19508,3 +19519,17 @@ _exit:
|
|||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
/* [calog fork] Invoke a routine value from an IDLE interpreter (no live call frame).
|
||||
Supplies the last AST node as a clean return landing so RETURN/ENDDEF have a valid
|
||||
return point, and passes args directly. Used by calog to fire script callbacks
|
||||
(pubsub/timer/export) that were registered earlier. See myBasic.h. */
|
||||
int mb_eval_routine_cold(struct mb_interpreter_t* s, mb_value_t routine, mb_value_t* args, unsigned argc, mb_value_t* ret) {
|
||||
_ls_node_t* n;
|
||||
void* land;
|
||||
if(!s) return MB_FUNC_ERR;
|
||||
n = s->ast;
|
||||
while(n && n->next) n = n->next;
|
||||
land = n;
|
||||
return mb_eval_routine(s, &land, routine, args, argc, ret);
|
||||
}
|
||||
|
|
@ -23,8 +23,8 @@
|
|||
** CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef __MY_BASIC_H__
|
||||
#define __MY_BASIC_H__
|
||||
#ifndef __OUR_BASIC_H__
|
||||
#define __OUR_BASIC_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
|
|
@ -663,6 +663,8 @@ MBAPI int mb_dispose_value(struct mb_interpreter_t* s, mb_value_t val);
|
|||
MBAPI int mb_get_routine(struct mb_interpreter_t* s, void** l, const char* n, mb_value_t* val);
|
||||
MBAPI int mb_set_routine(struct mb_interpreter_t* s, void** l, const char* n, mb_routine_func_t f, bool_t force);
|
||||
MBAPI int mb_eval_routine(struct mb_interpreter_t* s, void** l, mb_value_t val, mb_value_t* args, unsigned argc, mb_value_t* ret/* = NULL*/);
|
||||
/* [calog fork] Invoke a routine value from an IDLE interpreter (no live call frame). */
|
||||
MBAPI int mb_eval_routine_cold(struct mb_interpreter_t* s, mb_value_t routine, mb_value_t* args, unsigned argc, mb_value_t* ret);
|
||||
MBAPI int mb_get_routine_type(struct mb_interpreter_t* s, mb_value_t val, mb_routine_type_e* y);
|
||||
|
||||
MBAPI int mb_load_string(struct mb_interpreter_t* s, const char* l, bool_t reset/* = true*/);
|
||||
|
|
@ -706,4 +708,4 @@ MBAPI char* mb_memdup(const char* val, unsigned size);
|
|||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* __MY_BASIC_H__ */
|
||||
#endif /* __OUR_BASIC_H__ */
|
||||
Loading…
Add table
Reference in a new issue