From de50e111484eeb243b4d14618b979787c0053c2c Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 19 Jan 2026 20:45:09 +0100 Subject: [PATCH 01/17] Handbook update. No code changes. (0.37.57) --- docs/handbook.md | 283 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 269 insertions(+), 14 deletions(-) diff --git a/docs/handbook.md b/docs/handbook.md index f78f4d8..006d60a 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -100,6 +100,8 @@ Pass all options as -DNAME=VALUE. The most relevant toggles are: - FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default OFF) - FUN_WITH_SQLITE=ON|OFF — enable SQLite (sqlite3) support (default OFF) - FUN_WITH_TCLTK=ON|OFF — enable Tk (GUI via Tcl/Tk) support (default OFF) +- FUN_WITH_INI=ON|OFF — enable INI (iniparser) support (default OFF) +- FUN_WITH_NOTCURSES=ON|OFF — enable Notcurses TUI support (default OFF) You can also set the default search path for the bundled stdlib with DEFAULT_LIB_DIR: @@ -321,20 +323,29 @@ Error handling and debugging: Types: - number: signed integer (with helpers for unsigned behavior) +- float: 64-bit IEEE-754 floating point - string: immutable bytes; len(s), join, split, substr, find - array: ordered list; len, push, apop, insert, remove, slice - map: associative dictionary typically keyed by strings - boolean: represented as 1 (true) or 0 (false); operators &&, ||, ! - nil: absence of value +Numeric variants: +- byte (uint8) +- Fixed width ints: int8/16/32/64 and uint8/16/32/64 + Control flow: -- if/else, while; range helpers in utils.range +- if/else, while; break, continue; range helpers in utils.range Functions and classes: - Define a function: fun name(args) ... - Define a class: class Name(constructor params) with method definitions fun method(this, ...) - _construct acts as the constructor if present; methods use explicit this +Exceptions: +- try { ... } catch (e) { ... } finally { ... } +- Throwing/catching is defined by the spec; some runtimes may implement handling progressively. Examples: try_catch_finally.fun + Modules and includes: - #include for libs under FUN_LIB_DIR - #include "relative/path.fun" for local includes @@ -357,6 +368,10 @@ Conversion and type: Math and random: - min(a,b), max(a,b), clamp(x, lo, hi), abs(x), pow(a,b), random_seed(seed), random_int(lo, hiExclusive) +Bitwise (uint32 operations): +- band(a,b), bor(a,b), bxor(a,b), bnot(a) +- shl(a, s), shr(a, s) + Regex (requires PCRE2 when enabled): - regex_match(text, pattern) -> 1/0 - regex_search(text, pattern) -> map { match, start, end, groups } @@ -491,20 +506,99 @@ Module lib/encoding/base64.fun: base64_encode(string), base64_decode(string) ### JSON (optional) -Build flag: -DFUN_WITH_JSON=ON; requires json-c. VM functions: json_parse, json_stringify, json_from_file, json_to_file. Stdlib class JSON wraps these with light ergonomics. Example: examples/json_showcase.fun. +Build flag: -DFUN_WITH_JSON=ON; requires json-c. + +VM API: +- json_parse(text) -> value or nil +- json_stringify(value, prettyFlag) -> string +- json_from_file(path) -> value or nil +- json_to_file(path, value, prettyFlag) -> 1/0 + +Stdlib wrapper: +- class JSON (lib/io/json.fun) — convenience methods mirroring the VM API. + +Example: +``` +include + +j = JSON() +data = j.parse('{"name":"Fun","year":2026,"ok":true,"tags":["vm","lang"]}') +print(typeof(data)) // map +print(data["name"]) + +ok = j.to_file("./tmp/out.json", data, 1) // pretty = 1 +print("saved:", ok) +``` + +Notes: +- JSON types map to Fun types: object -> map, array -> array, string -> string, number -> number/float, true/false -> 1/0, null -> nil. +- When writing, prettyFlag=1 enables pretty printing. ### CURL (optional) -Build flag: -DFUN_WITH_CURL=ON; requires libcurl. VM provides: -- curl_get(url) -> string ("" on error) -- curl_post(url, body) -> string ("" on error) +Build flag: -DFUN_WITH_CURL=ON; requires libcurl. + +VM API: +- curl_get(url) -> string (empty string on error) +- curl_post(url, body) -> string (empty string on error) - curl_download(url, path) -> 1/0 -Examples: curl_get_json.fun, curl_post.fun, curl_download.fun +Stdlib wrapper: +- None (call built-ins directly). See examples in examples/extra/. + +Example: +``` +url = "https://httpbin.org/get" +resp = curl_get(url) +if (len(resp) == 0) + print("GET failed") +else + print(substr(resp, 0, 60), "...") +``` + +Examples: +- curl_get_json.fun, curl_post.fun, curl_download.fun ### PCSC (optional) -Build flag: -DFUN_WITH_PCSC=ON; provides pcsc_* built-ins and a stdlib wrapper class PCSC. Example: pcsc_example.fun. +Build flag: -DFUN_WITH_PCSC=ON; requires PC/SC (pcsc-lite). + +VM API: +- pcsc_establish() -> context id (>0) or 0 +- pcsc_list_readers(ctx) -> array of reader names or nil +- pcsc_connect(ctx, readerName) -> handle id (>0) or 0 +- pcsc_disconnect(handle) -> 1/0 +- pcsc_transmit(handle, bytesArray) -> { data: array, sw1: number, sw2: number, code: number } + +Stdlib wrapper: +- class PCSC (lib/io/pcsc.fun) — higher-level helpers for listing readers, connecting, and APDU I/O. + +Example: +``` +include + +sc = PCSC() +ctx = pcsc_establish() +if (ctx == 0) + print("PCSC not available") +else + readers = pcsc_list_readers(ctx) + if (readers == nil || len(readers) == 0) + print("No readers found") + else + h = pcsc_connect(ctx, readers[0]) + if (h) + // Example APDU: GET RESPONSE (illustrative only) + res = pcsc_transmit(h, [0x00, 0xC0, 0x00, 0x00, 0x00]) + print("SW:", res["sw1"], res["sw2"], "code:", res["code"]) + pcsc_disconnect(h) +``` + +Examples: +- examples/extra/pcsc_example.fun, examples/extra/pcsc_demo.fun + +Notes: +- Smart card operations depend on the reader, card, and drivers installed. Ensure pcscd/service is running. ### SQLite (optional) @@ -530,13 +624,174 @@ Example flow (examples/sqlite_example.fun): 4) rows2 = sqlite_query(h, "SELECT count(*) AS cnt FROM tasks;") 5) sqlite_close(h) +### INI (optional) + +Build flag: -DFUN_WITH_INI=ON; requires iniparser (>= 4.2.6). + +VM API: +- ini_load(path) -> handle (>0) or 0 on error +- ini_free(handle) -> 1/0 +- ini_get_string(handle, section, key, default) -> string +- ini_get_int(handle, section, key, defaultNumber) -> number +- ini_get_double(handle, section, key, defaultFloat) -> float +- ini_get_bool(handle, section, key, defaultBool) -> 1/0 +- ini_set(handle, section, key, valueString) -> 1/0 +- ini_unset(handle, section, key) -> 1/0 +- ini_save(handle, path) -> 1/0 + +Notes: +- Section/key may be looked up flexibly ("section:key" and "section.key"). +- Always free handles with ini_free when done. + +Example: +``` +h = ini_load("./examples/data/example.ini") +if (h == 0) + print("Failed to load INI") +else + host = ini_get_string(h, "server", "host", "localhost") + port = ini_get_int(h, "server", "port", 8080) + print("server:", host, port) + ok = ini_set(h, "server", "host", "127.0.0.1") + if (ok) + ini_save(h, "./tmp/updated.ini") + ini_free(h) +``` + +### Notcurses (optional) + +Build flag: -DFUN_WITH_NOTCURSES=ON; requires notcurses (pkg-config: notcurses or notcurses-core). + +VM API: +- nc_init() -> 1/0 (initialize TUI; call once) +- nc_draw_text(x, y, text) -> 0 on success, -1 on error +- nc_clear() -> 0 on success, -1 on error +- nc_getch(timeout_ms) -> key code (int), -1 if timeout/no input +- nc_shutdown() -> 1/0 (restore terminal) + +Example: +``` +if (nc_init()) + nc_draw_text(2, 1, "Fun + Notcurses") + print("Press any key...") + k = nc_getch(0) // 0 = blocking + nc_shutdown() +else + print("Notcurses init failed") +``` + +### XML (optional) + +Build flag: -DFUN_WITH_XML2=ON; requires libxml2. + +VM API: +- xml_parse(text) -> doc_handle (>0) or 0 on error +- xml_root(doc_handle) -> node_handle (>0) or 0 if missing +- xml_name(node_handle) -> string +- xml_text(node_handle) -> string + +Stdlib wrapper: +- class XML (lib/io/xml.fun) + - parse(text): int + - from_file(path): int + - root(doc): int + - name(node): string + - text(node): string + +Example: +``` +include + +xml = XML() +doc = xml.from_file("./examples/data/example.xml") +if (doc == 0) + print("Failed to load XML file") +else + root = xml.root(doc) + print(xml.name(root)) + print(xml.text(root)) +``` + +Notes: +- Handles are integers managed by the VM; nodes belong to their document. + +### libSQL (optional) + +Build flag: -DFUN_WITH_LIBSQL=ON; requires libSQL client (compatible with sqlite3 C API). + +VM API: +- libsql_open(path_or_url) -> handle (>0) or 0 on error +- libsql_close(handle) -> nil +- libsql_exec(handle, sql) -> rc (0 on success) +- libsql_query(handle, sql) -> array of row maps + +Example flow: +1) h = libsql_open("./database.sqlite") +2) rows = libsql_query(h, "SELECT id, title FROM tasks;") +3) rc = libsql_exec(h, "INSERT INTO tasks (title) VALUES ('Try libSQL');") +4) libsql_close(h) + +### PCRE2 / Regex (optional) + +Build flag: -DFUN_WITH_PCRE2=ON; requires PCRE2 (8-bit API). + +VM API: +- regex_match(text, pattern) -> 1/0 +- regex_search(text, pattern) -> map { match, start, end, groups } +- regex_replace(text, pattern, repl) -> string + +Stdlib wrapper: +- class Regex (lib/regex.fun) providing match/search/replace helpers. + +Examples: regex_demo.fun, regex_procedural.fun + +Notes: +- Patterns use PCRE2 syntax. + +### Tcl/Tk GUI (optional) + +Build flag: -DFUN_WITH_TCLTK=ON; requires Tcl/Tk (8.6+). + +VM API: +- tk_title(title) -> rc +- tk_label(id, text) -> rc +- tk_button(id, text) -> rc +- tk_pack(id) -> rc +- tk_loop() -> nil (enters event loop) + +Stdlib wrapper: +- class TK (lib/ui/tk.fun) mirrors the VM API. + +Example: +``` +include + +tk = TK() +tk.title("Fun + Tk GUI") +tk.label("hello", "Hello, world!") +tk.pack("hello") +tk.button("ok", "OK") +tk.pack("ok") +tk.loop() +``` + +Notes: +- Ensure Tcl/Tk runtime libraries are available on your system. + +### REPL (optional) + +Build flag: -DFUN_WITH_REPL=ON. + +Description: +- Enables the interactive Read–Eval–Print Loop and the --repl-on-error mode. No additional VM API functions; this feature is part of the executable. + --- ## Examples reference You can run examples without installing by pointing FUN_LIB_DIR to the repository lib directory: - FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/.fun + FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/.fun Highlights (not exhaustive): - arrays.fun, arrays_advanced.fun, arrays_iter.fun — array operations @@ -555,14 +810,14 @@ Highlights (not exhaustive): - have_fun.fun — quick sanity check - if_else_test.fun — branching - include_lib.fun, include_local.fun, include_namespace.fun — includes and namespacing -- input_example.fun — console input -- json_showcase.fun — JSON usage -- curl_get_json.fun, curl_post.fun, curl_download.fun — HTTP via CURL +- interactive/input_example.fun — console input +- extra/json_showcase.fun — JSON usage +- extra/curl_get_json.fun, extra/curl_post.fun, extra/curl_download.fun — HTTP via CURL - loops_break_continue.fun, nested_loops.fun, while_test.fun — loops -- md5_demo.fun, sha1_demo.fun, sha256_demo.fun, sha256_str_demo.fun, sha384_example.fun, sha512_demo.fun, sha512_str_demo.fun — hashing +- crypto/md5_demo.fun, crypto/sha1_demo.fun, crypto/sha256_demo.fun, crypto/sha256_str_demo.fun, crypto/sha384_example.fun, crypto/sha512_demo.fun, crypto/sha512_str_demo.fun — hashing - objects_basic.fun, objects_more.fun — map/object patterns - os_env.fun — environment variables -- pcsc_example.fun — smart card demo +- extra/pcsc_example.fun — smart card demo - process_example.fun — running external commands - regex_demo.fun, regex_procedural.fun — regex usage - stdlib_showcase.fun — tour through stdlib @@ -574,7 +829,7 @@ Highlights (not exhaustive): - type_safety.fun, type_safety_fails.fun — type safety - types_integers.fun, signed_ints.fun, uint_types.fun — integers - unix_socket_echo.fun — UNIX domain sockets -- sqlite_example.fun — SQLite usage +- extra/sqlite_example.fun — SQLite usage Notes: - Some examples rely on optional features (JSON, CURL, PCSC, SQLite) and degrade gracefully when disabled. From 8478c49240d325b9f922737a3d4e65734e9556a6 Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 19 Jan 2026 20:52:00 +0100 Subject: [PATCH 02/17] Directory layout changes. No code changes. (0.37.57) --- examples/{extra => blocking}/http_server.fun | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{extra => blocking}/http_server.fun (100%) diff --git a/examples/extra/http_server.fun b/examples/blocking/http_server.fun similarity index 100% rename from examples/extra/http_server.fun rename to examples/blocking/http_server.fun From b513cf0463021135f1bade5f2583e16a0bcf0f33 Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 19 Jan 2026 23:24:36 +0100 Subject: [PATCH 03/17] Added a basic TCP server that provides access to an sqlite database. (0.37.58) --- CMakeLists.txt | 2 +- examples/sqlited/README.md | 42 ++++++ examples/sqlited/client.fun | 142 ++++++++++++++++++ examples/sqlited/protocol.md | 35 +++++ examples/sqlited/server.fun | 282 +++++++++++++++++++++++++++++++++++ 5 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 examples/sqlited/README.md create mode 100755 examples/sqlited/client.fun create mode 100644 examples/sqlited/protocol.md create mode 100755 examples/sqlited/server.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index a8847f4..0033b44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.57 LANGUAGES C) +project(fun VERSION 0.37.58 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/sqlited/README.md b/examples/sqlited/README.md new file mode 100644 index 0000000..4be87fc --- /dev/null +++ b/examples/sqlited/README.md @@ -0,0 +1,42 @@ +Fun SQL TCP Demo (sqlited) + +This example provides a minimal TCP server that executes SQL against a local SQLite database and a matching client. + +Files +- server.fun — TCP server daemon +- client.fun — simple CLI client +- protocol.md — wire protocol specification (line-based TSV) + +Prerequisites +- Build Fun with SQLite support enabled: configure with -DFUN_WITH_SQLITE=ON +- Ensure the sqlite3 development headers and runtime are installed + +Create a sample database +- A schema is provided at examples/data/database.sql +- Create ./database.sqlite at the repository root using the sqlite3 CLI: + sqlite3 ./database.sqlite < ./examples/data/database.sql + +Run the server +- Set FUN_LIB_DIR to the repo’s lib directory or install Fun libs system-wide +- Example (Debug profile path may differ): + FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlited/server.fun 127.0.0.1 5555 + +Run the client +- Query: + FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlited/client.fun 127.0.0.1 5555 "SELECT id, title FROM tasks;" +- Exec/DDL: + FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlited/client.fun 127.0.0.1 5555 "UPDATE tasks SET done=1 WHERE id=1;" + +Protocol summary +- Client sends one line with SQL ended by a newline (\n) +- Server responds with either: + - RESULT block (header + rows as TSV) ending with END + - OK rc (for exec/DDL) + - ERROR message (on error) +See protocol.md for details. + +Notes and limitations +- Demo only; do not expose to untrusted networks (no auth/TLS; arbitrary SQL) +- BLOBs and binary data are not specially handled in this v1 +- Very long SQL lines are capped at 64 KiB +- The server handles one client at a time (simple model); extend with threads if desired diff --git a/examples/sqlited/client.fun b/examples/sqlited/client.fun new file mode 100755 index 0000000..d524bc9 --- /dev/null +++ b/examples/sqlited/client.fun @@ -0,0 +1,142 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-19 + */ + +// Simple TCP SQL client for Fun +// Connects to host:port, sends a single-line SQL (from CLI args or default), +// prints the server response, and exits. + +// Run the server: +// FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlited/server.fun 127.0.0.1 5555 + +// Run the client: +// FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error ./examples/sqlited/client.fun 127.0.0.1 5555 "SELECT * FROM tasks" + +#include + +fun arg_or_default(args, i, d) + if (len(args) > i) + return args[i] + else + return d + +fun read_all(fd) + buf = "" + while (true) + chunk = sock_recv(fd, 1024) + if (chunk == nil || len(chunk) == 0) + break + buf = buf + chunk + return buf + +fun main() + args = argv() + host = arg_or_default(args, 0, "127.0.0.1") + port = to_number(arg_or_default(args, 1, 5555)) + sql = arg_or_default(args, 2, "SELECT 1 AS one;") + + fd = tcp_connect(host, port) + if (fd == 0) + print("Connect failed to " + host + " " + to_string(port)) + return 1 + + // Ensure a single line terminated by \n + if (len(sql) == 0 || substr(sql, len(sql)-1, 1) != "\n") + sql = sql + "\n" + + sent = sock_send(fd, sql) + if (sent < 0) + print("Send failed") + sock_close(fd) + return 1 + + resp = read_all(fd) + sock_close(fd) + if (resp == nil) + resp = "" + print(resp) + +// Explicitly invoke main when the script is run +main() + +/* Possible result with 67 entries in the tasks table: +RESULT +value +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +{map n=4} +END +*/ diff --git a/examples/sqlited/protocol.md b/examples/sqlited/protocol.md new file mode 100644 index 0000000..c7075c1 --- /dev/null +++ b/examples/sqlited/protocol.md @@ -0,0 +1,35 @@ +Fun SQL TCP Demo Protocol (TSV, line-based) + +- Client sends exactly one line with the SQL text terminated by a newline ("\n"). The server reads up to 64 KiB. + +Responses + +1) Query returning rows (e.g., SELECT): + RESULT + col1\tcol2\t...\n + v11\tv12\t...\n + ... + END + + Notes: + - First line is the literal word RESULT followed by a newline. + - Second line is a header with column names separated by a single tab ("\t"). + - Each subsequent line is one row; fields are tab-separated. Nil/NULL are encoded as empty strings. + - The block terminates with a line containing the literal END. + +2) Exec/DDL (e.g., INSERT/UPDATE/CREATE): + OK rc + + Notes: + - rc is the sqlite3 result code (0 indicates success). + +3) Error: + ERROR message + + Notes: + - The error message is human-readable and not machine-stable. + +General +- Newlines are Unix style ("\n"). +- Tabs and newlines in data are replaced with spaces for TSV safety. +- The server closes the connection after sending the response. diff --git a/examples/sqlited/server.fun b/examples/sqlited/server.fun new file mode 100755 index 0000000..316aafc --- /dev/null +++ b/examples/sqlited/server.fun @@ -0,0 +1,282 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-19 + */ + +// Simple TCP SQL server for Fun +// Listens on a TCP port, opens ./database.sqlite, executes one-line SQL per connection, +// and returns results over the socket in a simple TSV protocol. + +// Run the server: +// FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlited/server.fun 127.0.0.1 5555 + +// Run the client: +// FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error ./examples/sqlited/client.fun 127.0.0.1 5555 "SELECT * FROM tasks" + +// Protocol (per protocol.md): +// - Client sends a single line of SQL ending with \n +// - If query returns rows: respond with +// RESULT\n +// \t\t...\n +// \t\t...\n +// ... +// END\n +// - If exec/DDL: respond with +// OK \n +// - On error: respond with +// ERROR \n + +// Helper: CLI args via stdlib +#include +#include + +fun arg_or_default(args, i, d) + if (len(args) > i) + return args[i] + else + return d + +// Helper: send a string (no newline added) +fun send(fd, s) + // sock_send returns bytes or -1 + return sock_send(fd, s) + +// Helper: read a single line (up to max_len) ending with \n; returns string without trailing \r?\n or nil on EOF +fun read_line(fd) + max_len = 65536 + buf = "" + while (len(buf) < max_len) + chunk = sock_recv(fd, 256) + if (chunk == nil || len(chunk) == 0) + break + buf = buf + chunk + pos = find(buf, "\n") + if (pos >= 0) + line = substr(buf, 0, pos) + // trim trailing \r if present + if (len(line) > 0 && substr(line, len(line)-1, 1) == "\r") + line = substr(line, 0, len(line)-1) + return line + if (len(buf) == 0) + return nil + // no newline; return whole buffer (trim any trailing CR) + if (len(buf) > 0 && substr(buf, len(buf)-1, 1) == "\r") + buf = substr(buf, 0, len(buf)-1) + return buf + +// Replace tab/newline with spaces for TSV safety +fun sanitize_tsv(s) + if (s == nil) + return "" + out = "" + i = 0 + while (i < len(s)) + ch = substr(s, i, 1) + if (ch == "\t" || ch == "\n" || ch == "\r") + out = out + " " + else + out = out + ch + i = i + 1 + return out + +fun trim(s) + // trim spaces and tabs + i = 0 + j = len(s) + while (i < j && (substr(s, i, 1) == " " || substr(s, i, 1) == "\t")) + i = i + 1 + while (j > i && (substr(s, j-1, 1) == " " || substr(s, j-1, 1) == "\t" || substr(s, j-1, 1) == ";")) + j = j - 1 + return substr(s, i, j - i) + +fun split_on_comma(s) + parts = [] + cur = "" + i = 0 + while (i < len(s)) + ch = substr(s, i, 1) + if (ch == ",") + push(parts, trim(cur)) + cur = "" + else + cur = cur + ch + i = i + 1 + push(parts, trim(cur)) + return parts + +// Parse header from SQL SELECT list; for SELECT * tries PRAGMA table_info(table) +fun parse_header_from_sql(sql, dbh) + // Use stdlib helper for lowercase + lower_sql = str_to_lower(sql) + psel = find(lower_sql, "select ") + pfrom = find(lower_sql, " from ") + if (psel < 0 || pfrom < 0 || pfrom <= psel) + return nil + cols_str = substr(sql, psel + 7, pfrom - (psel + 7)) + cols_str = trim(cols_str) + if (find(cols_str, "*") >= 0) + // Attempt to detect table name after FROM + rest = substr(sql, pfrom + 6, len(sql) - (pfrom + 6)) + rest = trim(rest) + // table name is up to next space or semicolon + sp = find(rest, " ") + tname = rest + if (sp > 0) + tname = substr(rest, 0, sp) + // remove trailing semicolon if any + tname = trim(tname) + if (len(tname) > 0) + pragma_sql = "PRAGMA table_info(" + tname + ");" + ti = sqlite_query(dbh, pragma_sql) + if (ti != nil && len(ti) > 0) + cols = [] + i = 0 + while (i < len(ti)) + nm = ti[i]["name"] + if (nm != nil) + push(cols, to_string(nm)) + i = i + 1 + if (len(cols) > 0) + return cols + // Parse explicit column list + parts = split_on_comma(cols_str) + cols = [] + i = 0 + while (i < len(parts)) + p = parts[i] + pl = lower(p) + // handle AS alias + aspos = find(pl, " as ") + if (aspos >= 0) + alias = trim(substr(p, aspos + 4, len(p) - (aspos + 4))) + push(cols, alias) + else + // take last token after dot + dot = find(p, ".") + if (dot >= 0) + push(cols, trim(substr(p, dot + 1, len(p) - (dot + 1)))) + else + push(cols, trim(p)) + i = i + 1 + if (len(cols) > 0) + return cols + return nil + +// Attempt to build a deterministic header and row order using enumerate(row). +// Falls back to attempting common column names if enumerate is unavailable. +fun extract_header(row) + // Build a header by probing a set of common keys present in many queries. + // If none are present, fall back to a single synthetic column "value" and + // the caller will print the entire row using to_string(row). + hdr_candidates = [ + "id", "name", "title", "value", "count", "cnt", + "done", "created_at", "updated_at", "rowid" + ] + cols = [] + found = 0 + i = 0 + while (i < len(hdr_candidates)) + k = hdr_candidates[i] + v = row[k] + if (v != nil) + push(cols, k) + found = 1 + i = i + 1 + if (found == 1) + return [cols, 0] // is_synthetic = 0 + else + return [["value"], 1] // is_synthetic = 1 + +// Try to obtain map keys via enumerate(row). Returns [keys, is_synthetic] +fun header_from_enumerate(row) + keys = [] + pairs = enumerate(row) + if (pairs == nil) + return [["value"], 1] + i = 0 + while (i < len(pairs)) + p = pairs[i] + // Expect pair to be [key, value] + if (p != nil && len(p) >= 1) + push(keys, p[0]) + i = i + 1 + if (len(keys) == 0) + return [["value"], 1] + return [keys, 0] + +fun handle_client(fd, dbh) + print("[sqlited] client connected: fd=" + to_string(fd)) + sql = read_line(fd) + print("[sqlited] received SQL: '" + (sql == nil ? "" : sql) + "'") + if (sql == nil || len(sql) == 0) + send(fd, "ERROR empty\n") + sock_close(fd) + return 0 + + // Try query first + rows = sqlite_query(dbh, sql) + if (rows != nil) + print("[sqlited] query path; rows array obtained") + // Build response in the stable synthetic format used in the 5558 build: + // RESULT\n + // value\n + // {map n=...}\n (per row) + resp = "RESULT\n" + // Always emit single-column header 'value' for compatibility + resp = resp + "value\n" + // Emit rows + r = 0 + while (r < len(rows)) + row = rows[r] + print("[sqlited] sending row #" + to_string(r)) + resp = resp + sanitize_tsv(to_string(row)) + "\n" + print("[sqlited] row #" + to_string(r) + " appended (synth)") + r = r + 1 + // Terminate block + print("[sqlited] finished building response; sending END and closing") + resp = resp + "END\n" + sb = send(fd, resp) + print("[sqlited] total bytes sent=" + to_string(sb)) + sock_close(fd) + return 1 + else + // Exec path + print("[sqlited] exec/DDL path") + rc = sqlite_exec(dbh, sql) + print("[sqlited] exec rc=" + to_string(rc)) + send(fd, "OK " + to_string(rc) + "\n") + sock_close(fd) + return 1 + +fun main() + args = argv() + host = arg_or_default(args, 0, "127.0.0.1") + port = to_number(arg_or_default(args, 1, 5555)) + + dbh = sqlite_open("./database.sqlite") + if (dbh == 0) + print("Failed to open ./database.sqlite; create it first (sqlite3 ./database.sqlite < ./examples/data/database.sql)") + return 1 + + lfd = tcp_listen(port, 16) + if (lfd == 0) + print("Failed to listen on port " + to_string(port)) + return 1 + + print("sqlited: listening on " + host + " " + to_string(port)) + while (true) + cfd = tcp_accept(lfd) + if (cfd > 0) + // Handle sequentially to keep it simple for a demo + handle_client(cfd, dbh) + +// Explicitly invoke main when the script is run +main() From f073df040cad8152286a77b79ec53fa2f28503b4 Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 19 Jan 2026 23:40:39 +0100 Subject: [PATCH 04/17] Added a maps usage example. (0.37.59) --- CMakeLists.txt | 2 +- examples/maps.fun | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100755 examples/maps.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 0033b44..e6bfc41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.58 LANGUAGES C) +project(fun VERSION 0.37.59 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/maps.fun b/examples/maps.fun new file mode 100755 index 0000000..c8425a4 --- /dev/null +++ b/examples/maps.fun @@ -0,0 +1,62 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-19 + */ + +/* + * Maps datatype — focused examples + * + * This script exclusively demonstrates the built-in maps datatype: + * - Creating maps with literals and empty maps + * - Reading and writing entries via [key] + * - Checking for key existence with has(map, key) + * - Getting keys(map) and values(map) + * - Using nested maps + */ + +// Create an empty map and add entries +user = {} +user["name"] = "Ada" +user["age"] = 37 +print(user) // -> {"name": "Ada", "age": 37} + +// Read an entry by key +print(user["name"]) // -> Ada + +// Update an existing entry +user["age"] = 38 +print(user["age"]) // -> 38 + +// Check if a key exists +print(has(user, "age")) // -> 1 (true) +print(has(user, "email")) // -> 0 (false) + +// Keys and values (order may vary) +print(keys(user)) // -> [name, age] +print(values(user)) // -> ["Ada", 38] + +// Nested maps +address = { "city": "London", "zip": "E1" } +user["address"] = address +print(user["address"]) // -> {"city": "London", "zip": "E1"} +print(user["address"]["city"]) // -> London + +/* Expected output: +{"name": Ada, "age": 37} +Ada +38 +1 +0 +[name, age] +[Ada, 38] +{"zip": E1, "city": London} +London +*/ From 97cfc74a013dd916626412676f3102b0cb870f48 Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 19 Jan 2026 23:45:17 +0100 Subject: [PATCH 05/17] Small README update. No code changes. (0.37.59) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c26be1..fcc8f94 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ There are some libs written in Fun available in the [./lib/](https://git.xw3.org This is actually a work in progress... -Current documentation is only found in the [Fun Handbook](https://git.xw3.org/fun/fun/src/branch/main/docs/handbook.md). +Current documentation is only found in the [Fun Handbook](https://git.xw3.org/fun/fun/src/branch/main/docs/handbook.md) and the [specification](https://git.xw3.org/fun/fun/src/branch/main/spec/v0.3.md). In the [./examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directory should be an example of most Fun features. From 29db3bc63e8974debff91e6eadbe71f3a8f08a16 Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 24 Jan 2026 19:57:39 +0100 Subject: [PATCH 06/17] Some array type fixes in the parser. Needs more investigation. (0.37.60) --- CMakeLists.txt | 2 +- examples/arrays.fun | 1 + examples/features.fun | 240 ++++++++++++++++++++++++++++++++++++++++++ src/parser.c | 69 +++++++++--- 4 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 examples/features.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index e6bfc41..618cc69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.59 LANGUAGES C) +project(fun VERSION 0.37.60 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/arrays.fun b/examples/arrays.fun index ba28939..c6fd414 100755 --- a/examples/arrays.fun +++ b/examples/arrays.fun @@ -22,6 +22,7 @@ // Arrays basics arr = [1, 2, 3] +print(typeof(arr)) print(arr) // -> [1, 2, 3] print(arr[0] + arr[1]) // -> 3 diff --git a/examples/features.fun b/examples/features.fun new file mode 100644 index 0000000..483219e --- /dev/null +++ b/examples/features.fun @@ -0,0 +1,240 @@ +#!/usr/bin/env fun +// features.fun - Showcase of Fun's neat features +// Demonstrates modern language capabilities in pure Fun + +print("=== Fun Language Feature Showcase ===") +print("") + +// ============================================ +// 1. Type System & Type Safety +// ============================================ +print("1. Strong Type System:") +string name = "Fun Language" +float version = 0.3 +boolean is_awesome = true +items = [1, 2, 3, 4, 5] +config = {"debug": true, "port": 8080} + +print(" Language: " + name + " v" + to_string(version)) +print(" Awesome: " + to_string(is_awesome)) +print("") + +// ============================================ +// 2. Modern Array Operations +// ============================================ +print("2. Array Operations:") +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +print(" Original: " + to_string(numbers)) + +// Array helpers from spec: push, join, map, filter, reduce +joined = join([10, 20, 30], ", ") +print(" Joined: " + joined) + +// Iterate arrays +print(" Iteration:") +for item in ["apple", "banana", "cherry"] + print(" " + item) +print("") + +// ============================================ +// 3. Maps (Dictionaries) +// ============================================ +print("3. Map Operations:") +person = {"name": "Alice", "age": 30, "role": "Developer"} + +print(" Person: " + to_string(person)) +print(" Has 'age' key: " + to_string(has(person, "age"))) +print(" Keys: " + to_string(keys(person))) +print(" Values: " + to_string(values(person))) +print("") + +// ============================================ +// 4. Object-Oriented Programming +// ============================================ +print("4. Classes & Objects:") + +class Counter(number initial, string label) + count = 0 + name = "" + + fun _construct(this, initial, label) + this.count = initial + this.name = label + + fun increment(this) + this.count = this.count + 1 + + fun get_value(this) + return this.count + + fun display(this) + print(" " + this.name + ": " + to_string(this.count)) + +counter = Counter(100, "Score") +counter.display() +counter.increment() +counter.increment() +counter.display() +print("") + +// ============================================ +// 5. Inheritance +// ============================================ +print("5. Inheritance:") + +class Animal(string type) + species = "" + + fun _construct(this, type) + this.species = type + + fun speak(this) + return "Some sound" + +class Dog(string dog_name) extends Animal + name = "" + + fun _construct(this, dog_name) + this.species = "Canine" + this.name = dog_name + + fun speak(this) + return "Woof! I'm " + this.name + +dog = Dog("Buddy") +print(" " + dog.speak()) +print(" Species: " + dog.species) +print("") + +// ============================================ +// 6. Error Handling +// ============================================ +print("6. Exception Handling:") + +try + print(" Attempting risky operation...") + result = 42 / 2 + print(" Result: " + to_string(result)) +catch err + print(" Caught error! Handled gracefully.") +finally + print(" Cleanup completed.") +print("") + +// ============================================ +// 7. String Manipulation +// ============================================ +print("7. String Features:") +string text = "Hello, Fun Language!" +print(" Original: " + text) +// Note: Using stdlib functions (assumed to exist in utils modules) +// len, substr, find, split would come from stdlib +print("") + +// ============================================ +// 8. Mathematical Operations +// ============================================ +print("8. Math Functions:") +float x = 16.7 +print(" x = " + to_string(x)) +// Note: Math functions like sqrt, floor, ceil, abs, gcd, lcm +// would come from stdlib or similar +print("") + +// ============================================ +// 9. Bitwise Operations +// ============================================ +print("9. Bitwise Operations:") +number bits1 = 12 +number bits2 = 10 +print(" 12 & 10 = " + to_string(band(bits1, bits2))) +print(" 12 | 10 = " + to_string(bor(bits1, bits2))) +print(" 12 ^ 10 = " + to_string(bxor(bits1, bits2))) +print(" 12 << 2 = " + to_string(shl(bits1, 2))) +print(" ~12 = " + to_string(bnot(bits1))) +print("") + +// ============================================ +// 10. Control Flow +// ============================================ +print("10. Control Flow:") + +// For loop with array +print(" Countdown:") +for i in [5, 4, 3, 2, 1] + print(" " + to_string(i) + "...") +print(" Liftoff!") + +// While with break/continue +print(" Skip evens:") +number n = 0 +while n < 10 + n = n + 1 + if n % 2 == 0 + continue + print(" " + to_string(n)) + if n >= 7 + break +print("") + +// ============================================ +// 11. Type Introspection +// ============================================ +print("11. Type Introspection:") +number check_int = 42 +string check_str = "hello" +check_arr = [1, 2, 3] +check_map = {"key": "value"} + +print(" typeof(42) = " + typeof(check_int)) +print(" typeof(\"hello\") = " + typeof(check_str)) +print(" typeof([1,2,3]) = " + typeof(check_arr)) +print(" typeof(map) = " + typeof(check_map)) +print("") + +// ============================================ +// 12. Functional Programming +// ============================================ +print("12. Higher-Order Functions:") + +fun double(n) + return n * 2 + +fun apply_twice(x, func) + return func(func(x)) + +result = apply_twice(5, double) +print(" apply_twice(5, double) = " + to_string(result)) +print("") + +// ============================================ +// 13. Array Operations with Spec Functions +// ============================================ +print("13. Array Higher-Order Functions:") +nums = [1, 2, 3, 4, 5] + +fun square(x) + return x * x + +squared = map(nums, square) +print(" Squared: " + to_string(squared)) + +fun is_even(x) + return x % 2 == 0 + +evens = filter(nums, is_even) +print(" Evens: " + to_string(evens)) + +fun sum(acc, x) + return acc + x + +total = reduce(nums, 0, sum) +print(" Sum: " + to_string(total)) +print("") + +// ============================================ +// Conclusion +// ============================================ +print("=== Feature Showcase Complete! ===") +print("Fun combines modern language features with simplicity.") +print("Explore more examples in ./examples/ directory!") diff --git a/src/parser.c b/src/parser.c index eae29c6..47b727a 100644 --- a/src/parser.c +++ b/src/parser.c @@ -73,6 +73,7 @@ static int g_temp_counter = 0; #define TYPE_META_NIL 10003 #define TYPE_META_CLASS 10004 #define TYPE_META_FLOAT 10005 +#define TYPE_META_ARRAY 10006 static void parser_fail(size_t pos, const char *fmt, ...) { g_has_error = 1; @@ -2845,21 +2846,23 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si Note: 'number' maps to signed 64-bit here. 'byte' is an alias of unsigned 8-bit. 'Class' restricts to class instances. */ if (strcmp(name, "number") == 0 || strcmp(name, "string") == 0 || strcmp(name, "boolean") == 0 || strcmp(name, "nil") == 0 - || strcmp(name, "Class") == 0 || strcmp(name, "float") == 0 + || strcmp(name, "class") == 0 || strcmp(name, "float") == 0 + || strcmp(name, "array") == 0 || strcmp(name, "byte") == 0 || strcmp(name, "uint8") == 0 || strcmp(name, "uint16") == 0 || strcmp(name, "uint32") == 0 || strcmp(name, "uint64") == 0 || strcmp(name, "int8") == 0 || strcmp(name, "int16") == 0 || strcmp(name, "int32") == 0 || strcmp(name, "int64") == 0) { - int is_number = (strcmp(name, "number") == 0); - int is_string = (strcmp(name, "string") == 0); - int is_boolean = (strcmp(name, "boolean") == 0); - int is_nil = (strcmp(name, "nil") == 0); - int is_class_tkn = (strcmp(name, "Class") == 0); - int is_float_tkn = (strcmp(name, "float") == 0); - int is_byte = (strcmp(name, "byte") == 0); - int is_u8 = (strcmp(name, "uint8") == 0) || is_byte; - int is_u16 = (strcmp(name, "uint16") == 0); - int is_u32 = (strcmp(name, "uint32") == 0); - int is_u64 = (strcmp(name, "uint64") == 0); + int is_number = (strcmp(name, "number") == 0); + int is_string = (strcmp(name, "string") == 0); + int is_boolean = (strcmp(name, "boolean") == 0); + int is_nil = (strcmp(name, "nil") == 0); + int is_class = (strcmp(name, "class") == 0); + int is_float = (strcmp(name, "float") == 0); + int is_array = (strcmp(name, "array") == 0); + int is_byte = (strcmp(name, "byte") == 0); + int is_u8 = (strcmp(name, "uint8") == 0) || is_byte; + int is_u16 = (strcmp(name, "uint16") == 0); + int is_u32 = (strcmp(name, "uint32") == 0); + int is_u64 = (strcmp(name, "uint64") == 0); int is_s8 = (strcmp(name, "int8") == 0); int is_s16 = (strcmp(name, "int16") == 0); int is_s32 = (strcmp(name, "int32") == 0); @@ -2870,7 +2873,7 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si /* store decl bits with sign encoded: negative means signed (number is signed 64-bit) */ if (decl_signed) decl_bits = -decl_bits; - /* declared type metadata: integers use decl_bits; string/boolean/nil/Class/float use special markers */ + /* declared type metadata: integers use decl_bits; string/boolean/nil/Class/float/array use special markers */ int decl_meta = decl_bits; if (is_string) { decl_meta = TYPE_META_STRING; @@ -2878,10 +2881,12 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si decl_meta = TYPE_META_BOOLEAN; } else if (is_nil) { decl_meta = TYPE_META_NIL; - } else if (is_class_tkn) { + } else if (is_class) { decl_meta = TYPE_META_CLASS; - } else if (is_float_tkn) { + } else if (is_float) { decl_meta = TYPE_META_FLOAT; + } else if (is_array) { + decl_meta = TYPE_META_ARRAY; } free(name); @@ -2989,6 +2994,23 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si bytecode_add_instruction(bc, OP_HALT, 0); } bytecode_set_operand(bc, j_skip_err, bc->instr_count); + } else if (decl_meta == TYPE_META_ARRAY) { + /* expect Array */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciArr = bytecode_add_constant(bc, make_string("Array")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciArr); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Array")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); } else if (decl_meta == TYPE_META_BOOLEAN) { /* accept Boolean literal or Number; if Number, clamp to 0/1 */ /* check if value is Boolean */ @@ -3349,6 +3371,23 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si bytecode_add_instruction(bc, OP_HALT, 0); } bytecode_set_operand(bc, j_skip_err, bc->instr_count); + } else if (meta == TYPE_META_ARRAY) { + /* expect Array */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciArr = bytecode_add_constant(bc, make_string("Array")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciArr); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Array")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); } else if (meta == TYPE_META_BOOLEAN) { /* expect Number then clamp to 1 bit (unsigned) */ bytecode_add_instruction(bc, OP_DUP, 0); From 6648fa1e27425eb51957ccec8f739455fe85ca82 Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 24 Jan 2026 20:15:25 +0100 Subject: [PATCH 07/17] Some code style fixes in the parser and bug fixes. (0.37.61) --- CMakeLists.txt | 2 +- examples/types_overview.fun | 2 +- src/parser.c | 30 +++++++++++++++--------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 618cc69..e16325b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.60 LANGUAGES C) +project(fun VERSION 0.37.61 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/types_overview.fun b/examples/types_overview.fun index 859f64d..ff02f12 100755 --- a/examples/types_overview.fun +++ b/examples/types_overview.fun @@ -71,7 +71,7 @@ class Person(number age, string name) // typeof(instance) will call this and return it return "Class" -Class p = Person(33, "Jo") +p = Person(33, "Jo") print(typeof(p)) // -> "Class" // p = "nope" // Uncomment to see TypeError: expected Class diff --git a/src/parser.c b/src/parser.c index 47b727a..e110f68 100644 --- a/src/parser.c +++ b/src/parser.c @@ -2851,26 +2851,26 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si || strcmp(name, "byte") == 0 || strcmp(name, "uint8") == 0 || strcmp(name, "uint16") == 0 || strcmp(name, "uint32") == 0 || strcmp(name, "uint64") == 0 || strcmp(name, "int8") == 0 || strcmp(name, "int16") == 0 || strcmp(name, "int32") == 0 || strcmp(name, "int64") == 0) { - int is_number = (strcmp(name, "number") == 0); - int is_string = (strcmp(name, "string") == 0); - int is_boolean = (strcmp(name, "boolean") == 0); - int is_nil = (strcmp(name, "nil") == 0); - int is_class = (strcmp(name, "class") == 0); - int is_float = (strcmp(name, "float") == 0); - int is_array = (strcmp(name, "array") == 0); - int is_byte = (strcmp(name, "byte") == 0); - int is_u8 = (strcmp(name, "uint8") == 0) || is_byte; - int is_u16 = (strcmp(name, "uint16") == 0); - int is_u32 = (strcmp(name, "uint32") == 0); - int is_u64 = (strcmp(name, "uint64") == 0); + int is_number = (strcmp(name, "number") == 0); + int is_string = (strcmp(name, "string") == 0); + int is_boolean = (strcmp(name, "boolean") == 0); + int is_nil = (strcmp(name, "nil") == 0); + int is_class = (strcmp(name, "class") == 0); + int is_float = (strcmp(name, "float") == 0); + int is_array = (strcmp(name, "array") == 0); + int is_byte = (strcmp(name, "byte") == 0); + int is_u8 = (strcmp(name, "uint8") == 0) || is_byte; + int is_u16 = (strcmp(name, "uint16") == 0); + int is_u32 = (strcmp(name, "uint32") == 0); + int is_u64 = (strcmp(name, "uint64") == 0); int is_s8 = (strcmp(name, "int8") == 0); int is_s16 = (strcmp(name, "int16") == 0); int is_s32 = (strcmp(name, "int32") == 0); int is_s64 = (strcmp(name, "int64") == 0) || is_number; /* number maps to int64 (signed) */ int decl_bits = is_u8 ? 8 : is_u16 ? 16 : is_u32 ? 32 : is_u64 ? 64 - : is_s8 ? 8 : is_s16 ? 16 : is_s32 ? 32 : is_s64 ? 64 : 0; + : is_s8 ? 8 : is_s16 ? 16 : is_s32 ? 32 : is_s64 ? 64 : 0; int decl_signed = (is_s8 || is_s16 || is_s32 || is_s64) ? 1 : 0; - /* store decl bits with sign encoded: negative means signed (number is signed 64-bit) */ + /* store decl bits with sign encoded: negative means signed (number is signed 64-bit) */ if (decl_signed) decl_bits = -decl_bits; /* declared type metadata: integers use decl_bits; string/boolean/nil/Class/float/array use special markers */ @@ -3099,7 +3099,7 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si ci = bytecode_add_constant(bc, make_string("")); } else if (is_nil) { ci = bytecode_add_constant(bc, make_nil()); - } else if (is_class_tkn) { + } else if (is_class) { /* Class-typed variable defaults to Nil until assigned an instance */ ci = bytecode_add_constant(bc, make_nil()); } else if (is_boolean) { From 869d68b992eb312f40de0dd552afbfb57bcfb17b Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 24 Jan 2026 21:17:40 +0100 Subject: [PATCH 08/17] Fixed try/catch to run finally again. exit() in catch if you want to stop execution. (0.37.62) --- CMakeLists.txt | 2 +- src/vm.c | 32 ++++++++++++++++++++++++++++++++ src/vm.h | 6 ++++++ src/vm/arithmetic/div.c | 8 ++++---- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e16325b..a6448c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.61 LANGUAGES C) +project(fun VERSION 0.37.62 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/src/vm.c b/src/vm.c index 01864f8..16ac20b 100644 --- a/src/vm.c +++ b/src/vm.c @@ -235,6 +235,38 @@ static void fun_vm_exit(int code) { /* Redirect exit inside this TU (affects included opcode handlers) */ #define exit(code) fun_vm_exit(code) +/* Forward decl for stack push used by vm_raise_error */ +static void push_value(VM *vm, Value v); + +/* Raise a runtime error that respects try/catch/finally semantics. + * If a handler is installed for the current frame, jump to it and push + * an error string for the catch clause. Otherwise, print and stop VM. */ +void vm_raise_error(VM *vm, const char *msg) { + if (!vm || vm->fp < 0) { + fprintf(stderr, "Runtime error: %s\n", msg ? msg : ""); + return; + } + Frame *f = &vm->frames[vm->fp]; + if (f->try_sp >= 0) { + char buf[256]; + if (msg) { + snprintf(buf, sizeof(buf), "Runtime error: %s", msg); + } else { + snprintf(buf, sizeof(buf), "Runtime error"); + } + /* push error value and transfer control to handler target */ + Value err = make_string(buf); + push_value(vm, err); + int try_idx = f->try_stack[f->try_sp--]; + int target = f->fn->instructions[try_idx].operand; + f->ip = target; + return; + } + /* No handler: print annotated message and terminate VM */ + fprintf(stderr, "Runtime error: %s\n", msg ? msg : ""); + vm->fp = -1; /* stop execution */ +} + /* Opcode case include index (vm_case_*.inc): - Core/stack/frame: diff --git a/src/vm.h b/src/vm.h index 13ad548..a08a891 100644 --- a/src/vm.h +++ b/src/vm.h @@ -122,6 +122,12 @@ void vm_dump_globals(VM *vm); // run entry Bytecode (pushes first frame) void vm_run(VM *vm, Bytecode *entry); +/* Raise a runtime error that respects try/catch/finally. + * If a try handler is active in the current frame, control jumps to it + * with an error string pushed on the stack. Otherwise, prints the error + * (annotated with location) and terminates execution. */ +void vm_raise_error(VM *vm, const char *msg); + /* --- Debugger API --- */ void vm_debug_reset(VM *vm); int vm_debug_add_breakpoint(VM *vm, const char *file, int line); // returns id >=0 or -1 diff --git a/src/vm/arithmetic/div.c b/src/vm/arithmetic/div.c index 8c821ef..fa37615 100644 --- a/src/vm/arithmetic/div.c +++ b/src/vm/arithmetic/div.c @@ -40,8 +40,8 @@ case OP_DIV: { double da = (a.type == VAL_FLOAT) ? a.d : (double)a.i; double db = (b.type == VAL_FLOAT) ? b.d : (double)b.i; if (db == 0.0) { - fprintf(stderr, "Runtime error: division by zero\n"); - exit(1); + vm_raise_error(vm, "division by zero"); + break; } Value res = make_float(da / db); free_value(a); @@ -49,8 +49,8 @@ case OP_DIV: { push_value(vm, res); } else { if (b.i == 0) { - fprintf(stderr, "Runtime error: division by zero\n"); - exit(1); + vm_raise_error(vm, "division by zero"); + break; } Value res = make_int(a.i / b.i); free_value(a); From 14c85c030fd135c0e49e8533853eefc57bec8f7c Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 26 Jan 2026 03:45:35 +0100 Subject: [PATCH 09/17] Added REPL documentation to ./docs/. No code changes. (0.37.62) --- docs/repl.md | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 docs/repl.md diff --git a/docs/repl.md b/docs/repl.md new file mode 100644 index 0000000..e918406 --- /dev/null +++ b/docs/repl.md @@ -0,0 +1,241 @@ +# Fun REPL Guide + +This document describes the interactive Read–Eval–Print Loop (REPL) for the Fun programming language: how to build/launch it, how input and execution work, line editing and completion, the REPL buffer workflow, commands, debugging helpers, and tips. + +The REPL is optional at build time. It provides a fast feedback loop for experimenting with Fun code, inspecting VM state, and debugging scripts interactively. + +## Enable and build + +- Build flag: -DFUN_WITH_REPL=ON +- Typical CMake configuration example: + +``` +cmake -S . -B build \ + -DFUN_WITH_REPL=ON +cmake --build build --target fun +``` + +You can also set a default search path for the bundled stdlib using DEFAULT_LIB_DIR at configure time (used for completions and library loading): + +``` +cmake -S . -B build -DFUN_WITH_REPL=ON -DDEFAULT_LIB_DIR="/usr/share/fun/lib" +``` + +## Launching the REPL + +- Directly run the main executable (ensure FUN_WITH_REPL=ON): + +``` +FUN_LIB_DIR="$(pwd)/lib" ./build/fun +``` + +- With the CMake “repl” convenience target (available only if built with FUN_WITH_REPL=ON): + +``` +cmake --build build --target repl +``` + +On startup, you should see something like: + +``` +Fun X.Y.Z REPL +Type :help for commands. Submit an empty line to run. +``` + +Environment variable FUN_LIB_DIR can be used to point the REPL to the standard library directory for symbol completion and library loading. If not set, a compile-time DEFAULT_LIB_DIR (if provided) or "lib" is used. + +## Running scripts and REPL-on-error + +- Run a script file normally: + +``` +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./demo.fun +``` + +- Enable tracing, and drop into a REPL automatically when a runtime error occurs: + +``` +FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error --trace ./demo.fun +``` + +Inside REPL-on-error, you can inspect frames, locals, disassembly, set breakpoints, and continue or step. Use :help to see available commands. + +## Prompts and input model + +- Primary prompt: `fun> ` when the input buffer is empty. +- Continuation prompt: + - `... ` when text exists but no extra indentation is required. + - `...N> ` (e.g., `...1> `, `...2> `) when the REPL detects open blocks needing additional indentation (2 spaces per level). This helps write multi-line constructs. +- Execution trigger: submit an empty line (press Enter on a blank line). The REPL parses the current buffer and executes it if complete; otherwise it will tell you the input looks incomplete and keep the buffer. + +Notes on completeness detection: +- The REPL considers code incomplete if there are unbalanced blocks or the last significant line ends with an operator/comma. +- If incomplete, it prints a hint like “(incomplete, open block indent +N)” or “(incomplete, continue typing)”. + +## Line editing and history + +The REPL includes an integrated line editor (on UNIX-like systems) with support for: + +- Left/Right arrows: move cursor (single-line mode). In multi-line input the cursor stays at end for simplicity. +- Up/Down arrows: navigate input history. Your current line is saved/restored while browsing. +- Ctrl+Left / Ctrl+Right: jump by words. +- Backspace/Delete: erase characters. +- Ctrl+O: insert a newline at the cursor (quick multi-line editing). +- Enter: accept line (adds a newline to the buffer; an empty line executes the buffer). + +History is persisted in a file named .fun_history in your home directory (HOME/USERPROFILE) when available, otherwise in the current directory. + +## Tab completion + +Tab provides two kinds of completions: + +1) :load path completion + - When typing a :load command, Tab completes filesystem paths, including directories. A trailing slash is handled as expected. Completion attempts to compute common suffixes across candidates. + +2) Standard library identifier completion + - For general input, Tab attempts to complete identifiers from the standard library symbols scanned from FUN_LIB_DIR (or DEFAULT_LIB_DIR/lib). If there are multiple matches, a menu of candidates is printed; otherwise the identifier is completed in place. + +If Tab is pressed in the middle of the line (not at end), the REPL beeps instead of completing. + +## Buffer workflow + +You typically build code incrementally: + +1) Type lines; they accumulate in an internal buffer. +2) Press Enter on a blank line to parse and execute the buffer. +3) Output is printed and the buffer is cleared. + +Alternatively, use the :run command to execute the buffer immediately (without needing a blank line), or :run to execute a file’s contents. + +## Timing and profiling + +- Toggle a simple elapsed time measurement for executions with :time on|off|toggle. +- Use :profile to parse+run and report parse time, run time, total, and instruction count. + +## Command reference + +Type :help to print the built-in command summary. Full list with clarifications: + +- :help | :h + Show the help. + +- :quit | :q | :exit + Exit the REPL. + +- :reset | :re + Reset VM state (clears globals). + +- :dump | :du | :globals | :gl + Dump current globals (indexes and stringified values). + +- :globals [pattern] / :vars | :v [pattern] + Dump globals, filtering by substring match on the value when a pattern is provided. + +- :clear | :cl + Clear the current input buffer. + +- :print | :pr + Show the current buffer content. + +- :run | :ru [file] + Execute current buffer, or execute the specified file immediately. Parsing errors are reported with caret highlighting. + +- :profile | :pf + Execute buffer and show timing for parse and run plus instruction count. + +- :save | :sa + Save the current buffer to a file. + +- :load | :lo + Load a file into the buffer (does not run). Use :run or a blank line to execute afterward. + +- :paste | :pa [run] + Enter paste mode to insert multiple lines verbatim. Finish with a single dot line: `.`. If the optional argument `run` (or `exec`) is given, the REPL will run the pasted buffer immediately. + +- :history | :hi [N] + Show the last N lines of persistent history (default 50). + +- :time | :ti on|off|toggle + Toggle/enable/disable timing for subsequent runs. + +- :env | :en [NAME[=VALUE]] + Get or set an environment variable. With NAME only, prints NAME=value. With NAME=VALUE, sets the variable for the current process. + +- :backtrace | :bt | :ba + Show a backtrace of VM frames (most recent first), including function name, source file, IP, and line. + +- :frame | :fr N + Select a frame N (0..top) to target with :locals, :list, :disasm and value inspections. By default, the top frame is used. + +- :list | :li [±K] + Show K lines of source around the current frame’s line (default 5). The current line is marked with `>`. + +- :disasm | :di [±N] + Disassemble around current frame’s instruction pointer (default 5 on each side). Shows index, opcode name, and operand. + +- :mdump | :md WHAT [offset [len]] [raw] [to ] + Dump a VM memory region. WHAT is one of: code | stack | globals | consts. Offset and length are optional; if omitted, a sensible default (up to 256 bytes) is used. With `raw`, write binary bytes. With `to `, write output to a file; otherwise print to stdout as a formatted hexdump. + +- :stack | :st [N] + Show top N (or all) stack values, stringified. + +- :top | :to + Show the value at the top of the VM stack. + +- :locals | :lc [FRAME] + Show non-nil locals for the selected frame (or the provided frame index). + +- :printv | :pv WHAT + Print a specific value: `local[i]`, `stack[i]`, or `global[i]`. + +- :break | :br [file:]line + Set a breakpoint. If file is omitted, the current frame’s source file is used. Prints a numeric breakpoint ID on success. + +- :info | :in breaks + List breakpoints. + +- :delete | :de ID + Delete a breakpoint by ID. + +- :clear breaks | :cb + Remove all breakpoints. + +- :cont | :co + Continue execution. In REPL-on-error/debug stops, this exits the REPL and resumes the program. + +- :step | :sp + Step a single instruction (REPL-on-error/debug mode). + +- :next | :ne + Step over in the current frame (REPL-on-error/debug mode). + +- :finish | :fi + Run until the current frame returns (REPL-on-error/debug mode). + +If an unknown command is entered, the REPL prints a hint to use :help. + +## Output handling + +Program output produced by VM execution is collected and then printed after each run. After printing, the VM’s output buffer is cleared. + +## Errors and diagnostics + +- Parse errors are reported with file:line and a caret pointing to the column. When tracing is enabled during normal execution, the VM annotates output with file/line and function names to aid debugging. +- In FUN_DEBUG builds, parse errors are also appended as comments to the history file to assist in later review. + +## Environment and library path + +- FUN_LIB_DIR: Overrides the standard library search path and the directory scanned for identifier completion. +- DEFAULT_LIB_DIR: Compile-time fallback path used when FUN_LIB_DIR is not set. If neither is available, the REPL defaults to the relative path "lib". + +## Tips + +- Use :paste run to quickly paste and execute multi-line code from the clipboard. +- :profile is handy to compare parser vs. VM time and to observe instruction counts for larger snippets. +- Combine --repl-on-error with --trace when running scripts to drop into an interactive diagnostic session at the point of failure. +- Use :mdump code/consts/stack/globals to inspect raw VM data, and :disasm to view bytecode in a human-readable form. + +## See also + +- docs/handbook.md — full language and VM handbook, including build options and ecosystem overview. +- examples/error/repl_on_error.fun — example showing the REPL-on-error workflow. From 592f6d2215a58b834201a28a5fabedf938dc978c Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 26 Jan 2026 04:18:48 +0100 Subject: [PATCH 10/17] Only some ./play.fun updates. Some code changes. (0.37.62) --- play.fun | 73 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/play.fun b/play.fun index c8153e3..3801b78 100755 --- a/play.fun +++ b/play.fun @@ -6,7 +6,10 @@ * - Executes each example as a subprocess */ +#include + #include +#include fun pick_fun_bin() // Prefer an explicit FUN_BIN override; otherwise rely on PATH @@ -49,18 +52,42 @@ fun main() "examples/cast_demo.fun", "examples/class_constructor.fun", "examples/classes_demo.fun", - "examples/crc32_example.fun", - "examples/crc32c_example.fun", - "examples/curl_download.fun", - "examples/curl_get_json.fun", - "examples/curl_post.fun", + "examples/crypto/crc32_example.fun", + "examples/crypto/crc32c_example.fun", + "examples/crypto/md5_demo.fun", + "examples/crypto/sha1_demo.fun", + "examples/crypto/sha256_demo.fun", + "examples/crypto/sha256_str_demo.fun", + "examples/crypto/sha384_example.fun", + "examples/crypto/sha512_demo.fun", + "examples/crypto/sha512_str_demo.fun", "examples/datetime_basic.fun", "examples/datetime_extended.fun", "examples/datetime_timer.fun", - "examples/debug_reporting.fun", "examples/echo_example.fun", - "examples/exit_example.fun", + "examples/error/debug_reporting.fun", + "examples/error/exit_example.fun", + "examples/error/repl_on_error.fun", "examples/expressions_test.fun", + "examples/extra/curl_download.fun", + "examples/extra/curl_get_json.fun", + "examples/extra/curl_post.fun", + "examples/extra/ini_class_demo.fun", + "examples/extra/ini_complex.fun", + "examples/extra/ini_demo.fun", + "examples/extra/ini_subsections.fun", + "examples/extra/json_showcase.fun", + "examples/extra/libsql_example.fun", + "examples/extra/pcre2_opcodes.fun", + "examples/extra/pcre2_showcase.fun", + "examples/extra/pcsc_example.fun", + "examples/extra/sqlite_example.fun", + "examples/extra/tk_hello.fun", + "examples/extra/xml_access_catalog.fun", + "examples/extra/xml_access_employees.fun", + "examples/extra/xml_access_ns.fun", + "examples/extra/xml_class_example.fun", + "examples/extra/xml_minimal.fun", "examples/fail.fun", "examples/file_io.fun", "examples/file_print_for_file_line_by_line.fun", @@ -75,59 +102,35 @@ fun main() "examples/include_local_util.fun", "examples/include_namespace.fun", "examples/inheritance_demo.fun", - "examples/ini_class_demo.fun", - "examples/ini_complex.fun", - "examples/ini_demo.fun", - "examples/ini_subsections.fun", - "examples/input_example.fun", - "examples/json_showcase.fun", - "examples/libsql_example.fun", + "examples/interactive/input_example.fun", "examples/loops_break_continue.fun", - "examples/md5_demo.fun", "examples/namespaced_mod.fun", "examples/nested_loops.fun", "examples/objects_basic.fun", "examples/objects_more.fun", "examples/os_env.fun", - "examples/pcre2_opcodes.fun", - "examples/pcre2_showcase.fun", - "examples/pcsc_example.fun", "examples/process_example.fun", "examples/regex_demo.fun", "examples/regex_procedural.fun", - "examples/repl_on_error.fun", - "examples/sha1_demo.fun", - "examples/sha256_demo.fun", - "examples/sha256_str_demo.fun", - "examples/sha384_example.fun", - "examples/sha512_demo.fun", - "examples/sha512_str_demo.fun", "examples/short_circuit_test.fun", "examples/signed_ints.fun", - "examples/sqlite_example.fun", "examples/stdlib_showcase.fun", "examples/strings_test.fun", "examples/tcp_http_get.fun", "examples/tcp_http_get_class.fun", "examples/thread_class_example.fun", "examples/threads_demo.fun", - "examples/tk_hello.fun", "examples/try_catch_finally.fun", "examples/try_catch_with_error.fun", - "examples/typeof.fun", - "examples/typeof_features.fun", "examples/type_safety.fun", "examples/type_safety_fails.fun", "examples/types_integers.fun", "examples/types_overview.fun", + "examples/typeof.fun", + "examples/typeof_features.fun", "examples/uint_types.fun", "examples/unix_socket_echo.fun", - "examples/while_test.fun", - "examples/xml_access_catalog.fun", - "examples/xml_access_employees.fun", - "examples/xml_access_ns.fun", - "examples/xml_class_example.fun", - "examples/xml_minimal.fun" + "examples/while_test.fun" ] failures = [] From c95134669a9cb53e1a34a798bd9cc60cce1eaa2b Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 26 Jan 2026 12:41:54 +0100 Subject: [PATCH 11/17] Added documentation for internals to ./docs/internals.md. No code changes. (0.37.62) --- docs/internals.md | 255 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 docs/internals.md diff --git a/docs/internals.md b/docs/internals.md new file mode 100644 index 0000000..7194603 --- /dev/null +++ b/docs/internals.md @@ -0,0 +1,255 @@ +# Fun Internals + +This document describes how Fun is implemented under the hood: the bytecode format, the virtual machine (VM) execution model, how opcodes are organized, error handling and debugging, and how the parser translates source code into bytecode. + +It complements the Handbook and Spec by focusing on implementation details in the C core, with pointers to the relevant source files. + +- VM implementation: src/vm.c (and many small opcode handlers in src/vm/...) +- Parser and compiler: src/parser.c +- Bytecode and values: src/bytecode.h, src/value.h +- VM API/structure: src/vm.h + +## High‑level architecture + +Fun programs are compiled from source text to a compact bytecode representation (Bytecode). The VM executes this bytecode using a simple stack machine with call frames, locals, and globals. + +- Parser produces a Bytecode object per compiled unit (e.g., a module or function). +- Bytecode is a linear array of Instruction { op, operand } entries with a parallel constants table. +- The VM maintains: + - A value stack for computation + - A frame stack for function calls (locals, instruction pointer, try/catch state) + - A globals array + - An output buffer (captures printed values), plus tracing/debugger state + +Almost all operations are implemented as small, focused opcode handlers. The VM’s main interpreter loop dispatches these opcodes and performs type‑aware operations on Value instances. + +## Values, bytecode, and instructions + +Relevant headers: +- src/value.h — the tagged value type used by the VM (ints, floats, strings, arrays, maps, nil, booleans, etc.). +- src/bytecode.h — the instruction set and bytecode container types. + +Instruction set: +- enum OpCode defines all opcodes (OP_NOP, OP_LOAD_CONST, OP_ADD, …). See src/bytecode.h. +- Instruction is a pair { OpCode op; int32_t operand; }. + - Some opcodes encode immediate arguments in operand (e.g., local slot index, constant index, jump target, arg count). + +Bytecode container (Bytecode): +- instructions: dynamic array of Instruction +- constants: dynamic array of Value copies (literals, strings, numbers, etc.) +- debug metadata: name (function or module), source_file (for error mapping) + +Utilities: +- bytecode_add_constant, bytecode_add_instruction, bytecode_set_operand, bytecode_dump… + +## VM structure and execution model + +See src/vm.h for the main VM and Frame shapes. + +Frame: +- fn: pointer to the current Bytecode (function or module entry) +- ip: instruction pointer (index into instructions) +- locals[MAX_FRAME_LOCALS]: local slots for this frame +- try_stack[16], try_sp: per‑frame exception handler stack (see exceptions section) + +VM: +- stack[STACK_SIZE], sp: data stack and stack pointer +- frames[MAX_FRAMES], fp: call frame stack and frame pointer +- globals[MAX_GLOBALS]: global slots +- output[OUTPUT_SIZE], output_count, output_is_partial[]: captures output of OP_PRINT/OP_ECHO +- instr_count: instructions executed during the last vm_run +- current_line: last known source line (maintained via OP_LINE) +- exit_code: set by OP_EXIT +- tracing flags and REPL‑on‑error hook +- debugger state (step/next/finish, breakpoints) + +Initialization and lifecycle: +- vm_init(VM*): zeroes state and prepares stacks +- vm_reset(VM*): frees/clears dynamic state while keeping the VM instance +- vm_free(VM*): tear‑down helper +- vm_run(VM*, Bytecode* entry): pushes an initial frame and enters the interpreter loop + +## The interpreter loop and opcode dispatch + +The interpreter loop lives in src/vm.c: vm_run. Opcodes are executed in a tight loop that: +- Fetches the current instruction (op, operand) from the active frame (frames[fp]) +- Optionally updates debug/tracing state (e.g., OP_LINE updates VM.current_line) +- Executes the handler for the opcode +- Advances ip, or jumps/returns/halts as needed + +Opcode handlers organization: +- To keep vm.c readable, most opcode implementations are factored into small .c files included directly into vm.c (e.g., vm/core/load_const.c, vm/logic/and.c, vm/arrays/push.c, vm/math/abs.c, vm/os/thread_spawn.c, etc.). +- This is a deliberate “amalgamation” style: small single‑purpose C units compiled as part of vm.c. +- Optional subsystems (JSON, PCRE2, CURL, SQLite, libSQL, PC/SC, XML2, Tcl/Tk, Notcurses, INI, sockets, serial, OS helpers) are grouped under src/external and src/vm//. + +Dispatch naming and visibility: +- Human‑readable names for opcodes live in vm.h: opcode_names[]. These are used in debug prints and error messages. + +## Stacks, frames, locals, globals + +Data stack: +- push_value/pop_value manage stack items of type Value. +- Most opcodes pop their arguments (right‑to‑left) and push a result. + +Call frames: +- vm_push_frame sets up a new frame for a function call (OP_CALL), transferring arguments into the callee’s local slots per the calling convention implemented by the compiler. +- vm_pop_frame unwinds one frame, restoring caller context and optionally leaving a return value on the data stack. + +Locals and globals: +- OP_LOAD_LOCAL/STORE_LOCAL address MAX_FRAME_LOCALS slots in the current frame. +- OP_LOAD_GLOBAL/STORE_GLOBAL access the VM‑wide globals table. + +## Control flow, calls, and returns + +- OP_JUMP and OP_JUMP_IF_FALSE implement structured control flow compiled by the parser (if/elif/else, loops, conditionals). +- OP_CALL pops function + N args, sets up a callee frame, and transfers control. +- OP_RETURN unwinds the current frame. The interpreter returns from vm_run when the entry frame is popped or a HALT/EXIT is executed. + +## Type system and operations + +The VM is dynamically typed. Values carry a tag; operations check types at runtime and coerce where sensible (e.g., number parsing in OP_TO_NUMBER). Representative groups: + +- Core arithmetic: OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, plus float‑aware rounding and transcendental ops (FLOOR/CEIL/TRUNC/ROUND, SIN/COS/TAN, EXP/LOG/LOG10/SQRT, FMIN/FMAX, GCD/LCM/ISQRT/SIGN). +- Logic and comparisons: OP_LT/LTE/GT/GTE/EQ/NEQ, OP_AND/OR/NOT. +- Stack utils: OP_DUP/OP_SWAP/OP_POP. +- Arrays: OP_MAKE_ARRAY/INDEX_GET/INDEX_SET/LEN/PUSH/APOP/SET/INSERT/REMOVE/SLICE/CONTAINS/INDEX_OF/CLEAR/ENUMERATE/ZIP. +- Strings and regex: OP_SPLIT/JOIN/SUBSTR/FIND and OP_REGEX_MATCH/SEARCH/REPLACE (and PCRE2 variants if enabled). +- Maps: OP_MAKE_MAP/KEYS/VALUES/HAS_KEY. +- Conversions/reflection: OP_TO_NUMBER/TO_STRING/CAST/TYPEOF, OP_UCLAMP/SCLAMP. +- I/O and OS: OP_READ_FILE/WRITE_FILE/INPUT_LINE/ENV/PROC_RUN/PROC_SYSTEM/TIME_NOW_MS/CLOCK_MONO_MS/DATE_FORMAT/OS_LIST_DIR/RANDOM_NUMBER, sockets, serial. +- External integrations (optional): JSON, CURL, SQLite, libSQL, PC/SC, XML2, Tcl/Tk, Notcurses, INI. + +Each handler enforces argument types and returns clear error messages via vm_raise_error on misuse. + +## Source lines, tracing, and error reporting + +Source line tracking: +- The compiler emits OP_LINE markers carrying 1‑based source line numbers. vm_run updates VM.current_line when these execute. + +Tracing: +- When VM.trace_enabled is set, vm_run prints each opcode and stack state. This is helpful for debugging compiled programs. + +Enhanced error messages: +- vm.c wraps fprintf for stderr to append context: source file, line, function name, opcode, and ip of the last executed instruction. +- For sources expanded via include preprocessing, vm.c maps the line back to the included file using preprocess_includes and inline markers (see below). + +## Exceptions: try/catch/finally + +Minimal structured exceptions are implemented with three opcodes and a per‑frame try stack: +- OP_TRY_PUSH operand = handler ip; pushes a handler location to Frame.try_stack +- OP_TRY_POP pops the current handler +- OP_THROW pops an error value; if a handler exists in the current frame, control jumps to it with the error value available on the stack; otherwise, vm_raise_error terminates execution (or triggers REPL if configured) + +This design keeps exception metadata strictly per frame and avoids VM‑global unwind state. + +## REPL‑on‑error and debugger + +Runtime error path: +- vm_raise_error consults the current frame’s try handlers. If none match, it formats a message with location, prints it, and sets exit_code. +- If VM.repl_on_error is enabled and a REPL hook is installed, the VM drops into the REPL to inspect state. + +Debugger state (vm.h): +- Step/Next/Finish modes, breakpoints stored in VM.breakpoints[]. +- vm_debug_* helpers manage breakpoints and stepping requests. vm_run consults this state at loop boundaries to pause execution. + +## Include preprocessing and source mapping + +Fun supports a lightweight include mechanism at the source text level (handled before/around compilation) to allow composing modules. vm.c provides two helpers used for error mapping: + +- preprocess_includes(const char* src) expands the source by inlining included files and injecting marker comments of the form: + // __include_begin__: : + …included lines… + // __include_end__: : + +- map_expanded_line_to_include(path, line, out_path, out_line) uses these markers to map a line in the expanded text back to the original file:line that contributed it. + +When stderr output is produced by the VM, fun_vm_vfprintf annotates messages with the mapped file and line if possible. + +## Parser and compiler pipeline (src/parser.c) + +The parser compiles directly to bytecode in a single pass with localized backpatching. The code is organized by precedence levels and statement/block parsing. + +Key components: + +- Namespaces and aliases: ns_aliases_scan detects alias directives at the top of the source (for module resolution) before parsing proper. +- Symbol tables: sym_index for globals; LocalEnv tracks locals in the current function scope. local_find/local_add manage local slots. +- Expression parser: a classic precedence‑climbing/recursive‑descent set of emit_* functions: + - emit_primary: literals, identifiers, grouping, array/map literals, function literals, calls, indexing + - emit_unary: prefix ops like !, unary -, type conversions + - emit_multiplicative/additive/relational/equality/and/or: binary operators by precedence + - emit_conditional: ternary/conditional constructs if supported by the grammar + - emit_expression: entry point that threads all the above + +- Statement and block parsing: + - read_line_start and skip_to_eol implement indentation and line/whitespace/comment handling (Fun uses indentation‑based blocks). + - parse_simple_statement emits bytecode for assignments, declarations, expression statements, control flow (if/elif/else, while/for), returns, breaks/continues, try/catch/finally constructs, print/echo, etc. + - parse_block handles nested blocks, indentation tracking, and emits OP_LINE markers for accurate source positioning. + +- Control‑flow codegen: + - Conditional and loop constructs emit OP_JUMP/OP_JUMP_IF_FALSE with forward jump placeholders patched later via bytecode_set_operand. + - try/catch/finally: emit OP_TRY_PUSH/OP_TRY_POP and arrange handler ips; OP_THROW for explicit throw. + +- Functions and calls: + - Functions compile to their own Bytecode with a fresh LocalEnv; callers use OP_CALL with operand = arg count. Arguments are pushed left‑to‑right; the callee consumes them from the stack into local slots as per the compiler’s calling convention. + +- Constants and literals: + - String/number/boolean/nil literals are interned into the Bytecode.constants table. OP_LOAD_CONST references them by index. + +- Line information and files: + - The parser emits OP_LINE as it advances through source lines. Bytecode.source_file is set so the VM can report accurate errors. + +Front‑end entry points: +- parse_string_to_bytecode(const char* source) +- parse_file_to_bytecode(const char* path) +- compile_minimal for very small snippets/tests + +These return an owned Bytecode* that the VM can execute. + +## Libraries and built‑ins layout + +The VM includes small, standalone C files for each feature group under src/vm/… + +- Core: src/vm/core/*.c (load/store, jumps, call/return, stack ops, halt/exit, try/throw) +- Numbers and logic: src/vm/arithmetic/*.c, src/vm/logic/*.c, src/vm/bitwise/*.c, src/vm/math/*.c +- Collections and strings: src/vm/arrays/*.c, src/vm/maps/*.c, src/vm/strings/*.c +- Conversions/reflection: src/vm/*.c (to_number, to_string, cast, typeof, uclamp, sclamp) +- OS and I/O: src/vm/io/*.c, src/vm/os/*.c, sockets and serial +- External integrations: src/external/*.c glue with opcode handlers in src/vm/ when enabled by CMake options + +Feature flags (CMake): +- Many subsystems are guarded by -DFUN_WITH_… options (JSON, PCRE2, CURL, PCSC, SQLITE, LIBSQL, XML2, TCLTK, NOTCURSES, INI, REPL). See CMake options in the Handbook. + +## Debugging and development tips + +- Use the --trace flag (or VM.trace_enabled) to inspect execution step‑by‑step. +- Use OP_LINE markers (visible via bytecode_dump) to correlate bytecode with source lines. +- vm_dump_globals helps inspect non‑nil globals at runtime. +- When adding a new opcode: + 1) Extend enum OpCode and opcode_names[] + 2) Implement a handler (small C file) and include it from src/vm.c + 3) Teach the parser/emitter to generate the opcode + 4) Update docs/spec and examples + +## Data limits and sizes + +From vm.h defaults (tuned for simplicity; adjust if needed): +- STACK_SIZE = 1024 +- MAX_FRAMES = 128 +- MAX_FRAME_LOCALS = 64 +- MAX_GLOBALS = 128 +- OUTPUT_SIZE = 1024 + +## Entry points recap + +- VM execution: vm_init → vm_run(entry) → vm_print_output/vm_clear_output → vm_reset/vm_free. +- Parsing: parse_string_to_bytecode / parse_file_to_bytecode → Bytecode*. +- Bytecode helpers: bytecode_add_instruction/constant, bytecode_set_operand, bytecode_dump. + +## Where to look in the source + +- src/vm.c — interpreter loop, error/trace, and amalgamated opcode includes +- src/vm.h — VM/Frame definitions and debugger API +- src/bytecode.h — instruction set and bytecode container +- src/parser.c — compiler, expression/statement/block parsing, emission, indentation handling +- src/vm/* — small focused opcode handlers by domain +- src/external/* — integration shims for optional dependencies From 08620d935979691830fb0972e535411eda97adef Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 26 Jan 2026 13:03:26 +0100 Subject: [PATCH 12/17] Added documentation for opcodes to ./docs/opcodes.md. No code changes. (0.37.62) --- docs/opcodes.md | 250 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 docs/opcodes.md diff --git a/docs/opcodes.md b/docs/opcodes.md new file mode 100644 index 0000000..15c742d --- /dev/null +++ b/docs/opcodes.md @@ -0,0 +1,250 @@ +# Fun VM Opcodes Overview + +This document provides an overview of the available VM opcodes implemented under src/vm and its subdirectories. Opcodes are grouped by module. For each opcode, we briefly note its purpose and, where available, the expected stack arguments and return value. + +## Notes + +- Arguments are pushed on the stack before the opcode executes; results are pushed back on the stack. +- Types are indicative; many opcodes accept integers and/or strings depending on context. See code for exact type checks and error behavior. + +## Core + +- OP_NOP: No operation. +- OP_HALT: Stop the VM. +- OP_EXIT: Terminate script with an exit code; pops int exitCode. +- OP_LOAD_CONST: Push a constant (by index embedded in bytecode). +- OP_LOAD_LOCAL: Push local variable value (by slot index). +- OP_STORE_LOCAL: Pop value and store into local (by slot index). +- OP_LOAD_GLOBAL: Push global variable value (by slot index/name id). +- OP_STORE_GLOBAL: Pop value and store into global (by slot index/name id). +- OP_CALL: Call a function; pops N args and a callable; pushes return value. +- OP_RETURN: Return from current function; optionally pops return value and pushes it to caller frame. +- OP_POP: Pop and discard the top of the stack. +- OP_DUP: Duplicate the top stack value. +- OP_SWAP: Swap the top two stack values. +- OP_JUMP: Unconditional jump to bytecode offset. +- OP_JUMP_IF_FALSE: Pop condition; jump if falsey. +- OP_THROW: Pop error and raise; unwinds to nearest try. +- OP_TRY_PUSH: Begin try handler (internal to exception handling). +- OP_TRY_POP: End try handler (internal to exception handling). + +## Arithmetic + +- OP_ADD: Add two numbers or concatenate two strings; pops b, a; pushes a+b or a..b. +- OP_SUB: Integer subtraction; pops b, a; pushes a-b. +- OP_MUL: Integer multiplication; pops b, a; pushes a*b. +- OP_DIV: Integer division; pops b, a; pushes a/b. +- OP_MOD: Integer modulus; pops b, a; pushes a%b. (math/mod.c) + +## Logic and Comparison + +- OP_NOT: Logical NOT; pops v; pushes bool. +- OP_AND: Logical AND; pops b, a; pushes bool. +- OP_OR: Logical OR; pops b, a; pushes bool. +- OP_EQ: Equality; pops b, a; pushes bool. +- OP_NEQ: Inequality; pops b, a; pushes bool. +- OP_LT: Less than; pops b, a; pushes bool. +- OP_LTE: Less than or equal; pops b, a; pushes bool. +- OP_GT: Greater than; pops b, a; pushes bool. +- OP_GTE: Greater than or equal; pops b, a; pushes bool. + +## Arrays and Indexing + +- OP_MAKE_ARRAY: Create array from N values; pops N values (count encoded in bytecode); pushes array. +- OP_INDEX_GET: Indexing into array or map; pops index/key, container; pushes value or Nil on missing. +- OP_INDEX_SET: Assign into array or map; pops value, index/key, container; pushes 1/0 for success (see code for exact behavior). +- OP_INSERT: Insert value at index in array; pops value, index, array; pushes 1/0. +- OP_REMOVE: Remove element at index in array; pops index, array; pushes removed value or Nil. +- OP_PUSH: Append element to end of array; pops value, array; pushes 1/0. +- OP_APOP: Pop last element from array; pops array; pushes removed value or Nil. +- OP_SLICE: Slice array; pops end, start, array; pushes new array slice. +- OP_INDEX_OF: Find index of value in array; pops value, array; pushes index or -1. +- OP_CONTAINS: Membership test for arrays; pops value, array; pushes bool. +- OP_JOIN: Join array of strings; pops separator, array; pushes string. +- OP_CLEAR: Clear all elements in array; pops array; pushes 1/0. +- OP_ENUMERATE: Produce array of [index, value] pairs from array; pops array; pushes array of pairs. + +## Maps + +- OP_MAKE_MAP: Create a map from N key-value pairs; pops 2*N values (val, key ...); pushes map. +- OP_HAS_KEY: Check if key exists; pops key, map; pushes bool. +- OP_KEYS: Return array of keys; pops map; pushes array. +- OP_VALUES: Return array of values; pops map; pushes array. + +## Strings and Regex + +- OP_SUBSTR: Substring; pops len, start, string; pushes substring. +- OP_FIND: Find substring; pops needle, haystack; pushes index or -1. +- OP_SPLIT: Split string; pops separator, string; pushes array of strings. +- OP_REGEX_MATCH: Regex full match; pops pattern, string; pushes bool or captures (see strings/regex_match.c). +- OP_REGEX_SEARCH: Regex search/find; pops pattern, string; pushes match details or -1. +- OP_REGEX_REPLACE: Regex replace; pops replacement, pattern, string; pushes new string. + +## Bitwise (uint32) + +- OP_BAND: Bitwise AND; pops b, a; pushes a & b. +- OP_BOR: Bitwise OR; pops b, a; pushes a | b. +- OP_BXOR: Bitwise XOR; pops b, a; pushes a ^ b. +- OP_BNOT: Bitwise NOT; pops a; pushes ~a. +- OP_SHL: Logical left shift; pops shift, value; pushes value << shift. +- OP_SHR: Logical right shift; pops shift, value; pushes value >> shift. +- OP_ROTL: Rotate left; pops shift, value; pushes rotl(value, shift). +- OP_ROTR: Rotate right; pops shift, value; pushes rotr(value, shift). + +## Math + +- OP_ABS: Absolute value; pops x; pushes |x|. +- OP_CEIL: Ceiling; pops x; pushes ceil(x). +- OP_FLOOR: Floor; pops x; pushes floor(x). +- OP_ROUND: Round; pops x; pushes round(x). +- OP_TRUNC: Truncate; pops x; pushes trunc(x). +- OP_SIN: Sine; pops x; pushes sin(x). +- OP_COS: Cosine; pops x; pushes cos(x). +- OP_TAN: Tangent; pops x; pushes tan(x). +- OP_SQRT: Square root; pops x; pushes sqrt(x) (float). +- OP_ISQRT: Integer square root; pops x; pushes isqrt(x) (int). +- OP_LOG: Natural logarithm; pops x; pushes log(x). +- OP_LOG10: Base-10 logarithm; pops x; pushes log10(x). +- OP_EXP: Exponential; pops x; pushes exp(x). +- OP_MAX: Max of two ints; pops b, a; pushes max(a, b). +- OP_MIN: Min of two ints; pops b, a; pushes min(a, b). +- OP_FMAX: Max of two floats; pops b, a; pushes max(a, b). +- OP_FMIN: Min of two floats; pops b, a; pushes min(a, b). +- OP_CLAMP: Clamp; pops hi, lo, x; pushes clamp(x, lo, hi). +- OP_SIGN: Sign; pops x; pushes -1, 0, or 1. +- OP_POW: Power; pops exp, base; pushes base^exp. +- OP_GCD: Greatest common divisor; pops b, a; pushes gcd(a, b). +- OP_LCM: Least common multiple; pops b, a; pushes lcm(a, b). +- OP_RANDOM_SEED: Seed RNG; pops int seed; pushes 1/0. +- OP_RANDOM_INT: Random integer in [lo, hi]; pops hi, lo; pushes int. + +## I/O + +- OP_READ_FILE: Read file contents; pops path:string; pushes data:string or Nil. +- OP_WRITE_FILE: Write data to file; pops data:string, path:string; pushes 1/0. +- OP_INPUT_LINE: Read a line from stdin; optional prompt on stack; pushes string (may be empty) or Nil. + +## JSON + +- OP_JSON_PARSE: Parse JSON text; pops text:string; pushes value (map/array/number/string/bool/Nil) or Nil on error. +- OP_JSON_STRINGIFY: Stringify a value; pops pretty:int(0/1), any; pushes json:string. +- OP_JSON_TO_FILE: Write value as JSON to file; pops pretty:int(0/1), any, path; pushes 1/0. +- OP_JSON_FROM_FILE: Read and parse JSON file; pops path; pushes value or Nil. + +## XML + +- OP_XML_PARSE: Parse XML text; pops text:string; pushes doc handle (>0) or 0 on error. +- OP_XML_ROOT: Get root node; pops doc handle; pushes node handle (>0) or 0. +- OP_XML_NAME: Get node name; pops node handle; pushes string. +- OP_XML_TEXT: Get node text (concatenated); pops node handle; pushes string. + +## INI + +- OP_INI_LOAD: Load INI; pops path:string; pushes handle (>0) or 0. +- OP_INI_FREE: Free INI handle; pops handle; pushes 1/0. +- OP_INI_GET_STRING: Get string; pops default, key, section, handle; pushes string. +- OP_INI_GET_INT: Get integer; pops default, key, section, handle; pushes int. +- OP_INI_GET_DOUBLE: Get double; pops default, key, section, handle; pushes float. +- OP_INI_GET_BOOL: Get bool; pops default, key, section, handle; pushes 1/0. +- OP_INI_SET: Set value; pops value, key, section, handle; pushes 1/0. +- OP_INI_UNSET: Remove key; pops key, section, handle; pushes 1/0. +- OP_INI_SAVE: Save to file; pops path, handle; pushes 1/0. + +## SQLite + +- OP_SQLITE_OPEN: Open database; pops path:string; pushes handle (>0) or 0. +- OP_SQLITE_CLOSE: Close database; pops handle; pushes Nil. +- OP_SQLITE_EXEC: Execute statement; pops handle:int, sql:string; pushes rc:int (0=OK). +- OP_SQLITE_QUERY: Run query; pops handle:int, sql:string; pushes array>. + +## libSQL + +- OP_LIBSQL_OPEN: Open database (url or path); pops string; pushes handle (>0) or 0. +- OP_LIBSQL_CLOSE: Close database; pops handle; pushes Nil. +- OP_LIBSQL_EXEC: Execute statement; pops handle:int, sql:string; pushes rc:int (0=OK). +- OP_LIBSQL_QUERY: Run query; pops handle:int, sql:string; pushes array>. + +## OS, Time, Processes, Threads, Sockets, Serial + +- OP_ENV: Get environment variable; pops key:string; pushes value:string or Nil. +- OP_ENV_ALL: Get all environment variables; pushes map. +- OP_FUN_VERSION: Push Fun version string; no args. +- OP_SLEEP_MS: Sleep; pops ms:int; pushes Nil. +- OP_TIME_NOW_MS: Current wall-clock time in ms since epoch; pushes int. +- OP_CLOCK_MONO_MS: Monotonic clock in ms; pushes int. +- OP_DATE_FORMAT: Format epoch ms with strftime; pops format:string, ms:int; pushes string. +- OP_RANDOM_NUMBER: Random float in [0,1); optional lower/upper bound handling; see os/random_number.c. +- OP_PROC_SYSTEM: Run command via system(); pops cmd:string; pushes exit code:int. +- OP_PROC_RUN: Run command and capture stdout/stderr; pops cmd:string; pushes map or string (see os/proc_run.c). +- OP_LIST_DIR / OP_OS_LIST_DIR: List directory; pops path; pushes array of file names. +- Threads: + - OP_THREAD_SPAWN: Spawn a thread to run a function; pops args (array or scalar), fn; pushes thread id. + - OP_THREAD_JOIN: Join thread; pops thread id; pushes thread result. +- Sockets (TCP/Unix): + - OP_SOCK_TCP_LISTEN: Listen on TCP port; pops backlog:int, port:int; pushes fd:int or -1. + - OP_SOCK_TCP_ACCEPT: Accept a connection; pops fd:int; pushes client fd:int or -1. + - OP_SOCK_TCP_CONNECT: Connect to host:port; pops port:int, host:string; pushes fd:int or -1. + - OP_SOCK_UNIX_LISTEN: Listen on Unix domain socket; pops backlog:int, path:string; pushes fd:int or -1. + - OP_SOCK_UNIX_CONNECT: Connect to Unix domain socket; pops path:string; pushes fd:int or -1. + - OP_SOCK_SEND: Send bytes; pops data:string, fd:int; pushes bytesSent:int or -1. + - OP_SOCK_RECV: Receive bytes; pops max:int, fd:int; pushes data:string or Nil. + - OP_SOCK_CLOSE: Close socket; pops fd:int; pushes 1/0. +- Serial (TTY): + - OP_SERIAL_OPEN: Open serial port; pops baud:int, path:string; pushes fd:int or -1. + - OP_SERIAL_CONFIG: Configure port; pops flow_ctrl, stop_bits, parity, data_bits, fd; pushes 1/0. + - OP_SERIAL_SEND: Send bytes; pops data:string, fd:int; pushes bytesSent:int or -1. + - OP_SERIAL_RECV: Receive bytes; pops max:int, fd:int; pushes data:string or Nil. + - OP_SERIAL_CLOSE: Close; pops fd:int; pushes 1/0. + +## Curl (HTTP) + +- OP_CURL_GET: HTTP GET; pops url:string; pushes body:string or Nil. +- OP_CURL_POST: HTTP POST; pops body:string, url:string; pushes response:string or Nil. +- OP_CURL_DOWNLOAD: Download URL to file; pops path:string, url:string; pushes 1/0. + +## PCRE2 (Regex) + +- OP_PCRE2_TEST: Test pattern; pops flags:int, text:string, pattern:string; pushes 1/0. +- OP_PCRE2_MATCH: Match pattern; pops flags:int, text:string, pattern:string; pushes array/map with groups or 0. +- OP_PCRE2_FINDALL: Find all matches; pops flags:int, text:string, pattern:string; pushes array of matches. + +## PC/SC (Smart cards) + +- OP_PCSC_ESTABLISH: Establish context; pushes ctx handle (>0) or 0. +- OP_PCSC_LIST_READERS: List readers; pops scope/id; pushes array of strings. +- OP_PCSC_CONNECT: Connect to reader; pops reader:string, ctx; pushes handle or 0. +- OP_PCSC_TRANSMIT: Send APDU; pops apdu:array/string, handle; pushes response bytes or map incl. SW. +- OP_PCSC_DISCONNECT: Disconnect; pops handle; pushes 1/0. +- OP_PCSC_RELEASE: Release context; pops scope/id; pushes 1/0. + +## Notcurses (Terminal UI) + +- OP_NC_INIT: Initialize notcurses; pushes handle or 0. +- OP_NC_SHUTDOWN: Shutdown; no args; pushes 1/0. +- OP_NC_CLEAR: Clear screen; pushes 1/0. +- OP_NC_DRAW_TEXT: Draw text at (x,y); pops text, x, y; pushes 1/0. +- OP_NC_GETCH: Get key with timeout; pops timeout_ms:int; pushes int key or -1. + +## SQLite-compatible lib (libSQL) + +- See “libSQL” section above; identical opcode set with different backend. + +## TK (Tcl/Tk UI) + +- OP_TK_EVAL: Evaluate Tcl code; pops text:string; pushes result string or error. +- OP_TK_LABEL: Create/update label; pops text, id; pushes 1/0. +- OP_TK_BUTTON: Create/update button; pops text, id; pushes 1/0. +- OP_TK_PACK: Pack widget; pops id; pushes 1/0. +- OP_TK_BIND: Bind event; pops command, event, id; pushes 1/0. +- OP_TK_WM_TITLE: Set window title; pops title; pushes 1/0. +- OP_TK_LOOP: Enter main event loop; no args; blocks until exit. +- OP_TK_RESULT: Retrieve last Tcl result; pushes string. + +## Miscellaneous + +- OP_KEYS / OP_VALUES: Map utilities (see Maps). +- Additional opcodes may exist for modules under src/vm that are stubbed or platform-dependent. Refer to the corresponding C file for precise semantics and edge cases. + +## How to explore + +- Each opcode implementation lives in its own file and begins with a comment documenting its behavior and stack contract. Grep for “case OP_” or open files under src/vm//*.c to see details and error handling. From c164bb80ae8ff139fcd43c70170a0e5d5f2bbafe Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 27 Jan 2026 01:30:25 +0100 Subject: [PATCH 13/17] README update. No code changes. (0.37.62) --- README.md | 47 +++++++---------------------------------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fcc8f94..d136a5e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ## What is Fun? +Fun is a small, strict, and simple programming language that runs on a compact stack-based virtual machine. The C core is intentionally minimal; most functionality and standard libraries are implemented in Fun itself. The language emphasizes simplicity, consistency, and joy in coding. + Fun is an experiment, just for fun, but Fun works! Fun is a highly strict programming language, but also highly simple. It looks like Python (My favorite language), but there are differences. @@ -14,6 +16,8 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - Simplicity - Consistency +- Simple to extend +- Hackable - Joy in coding - Fun! @@ -52,6 +56,7 @@ Fun is not about being the fastest or the most feature-rich. It’s about sharin - Respectful - Curious - Creative +- Open for everyone Like the name says: Fun Unites Nerds. @@ -77,47 +82,9 @@ Fun may not change the world — but it will make programming a little more fun. ### Lib (./lib/) -``` -arrays.fun -crypt/ - crc32c.fun - crc32.fun - md5.fun - md5_legacy.fun - ripemd160.fun - Broken! - sha1.fun - sha256.fun - sha384.fun - sha512.fun -encoding/ - base64.fun -hello.fun -hex.fun -io/ - console.fun - ini.fun - json.fun - pcsc2.fun - pcsc.fun - process.fun - serial.fun - socket.fun - thread.fun - xml.fun -math.fun -regex - pcre2.fun -regex.fun -strings.fun -ui/ - tk.fun -utils/ - datetime.fun - math.fun - range.fun -``` +See [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) for what the standard library provides. -### Extensions (only Linux actually) +Optional extensions (build-time selectable / only testing this on Linux actually): - [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) ☐ - [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ From 8aa150f8f74b358d5523a9ed2b8082df955b50fe Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 27 Jan 2026 01:31:07 +0100 Subject: [PATCH 14/17] README update. No code changes. (0.37.62) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d136a5e..bd30bf4 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Fun may not change the world — but it will make programming a little more fun. See [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) for what the standard library provides. -Optional extensions (build-time selectable / only testing this on Linux actually): +### Optional extensions (build-time selectable / only testing this on Linux actually): - [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) ☐ - [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ From 149e1353186faa8deaafb6ccda59d39d34eddb8b Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 27 Jan 2026 04:26:54 +0100 Subject: [PATCH 15/17] Added Rust support to the Fun core. (0.38.0) --- .gitignore | 2 + CMakeLists.txt | 105 +++++++++++++++++++++++++++++++++++++++- examples/rust_hello.fun | 32 ++++++++++++ src/bytecode.c | 1 + src/bytecode.h | 3 ++ src/parser.c | 7 +++ src/rust/Cargo.toml | 16 ++++++ src/rust/src/lib.rs | 44 +++++++++++++++++ src/test_opcodes.c | 23 +++++++++ src/vm.c | 25 ++++++++++ src/vm.h | 15 ++++++ src/vm/rust/hello.c | 27 +++++++++++ 12 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 examples/rust_hello.fun create mode 100644 src/rust/Cargo.toml create mode 100644 src/rust/src/lib.rs create mode 100644 src/vm/rust/hello.c diff --git a/.gitignore b/.gitignore index ceee666..48a69a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,7 @@ json.xml lib/*.so out/ src/*.o +src/rust/Cargo.lock +src/rust/target *.swp tmp* diff --git a/CMakeLists.txt b/CMakeLists.txt index a6448c9..fbfca89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.62 LANGUAGES C) +project(fun VERSION 0.38.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -11,11 +11,114 @@ include(${CMAKE_SOURCE_DIR}/cmake/Dependencies.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/Extensions.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Targets.cmake) +# --- Always show key build toggles --- +# Normalize to ENABLED/DISABLED like the "Fun extension summary" +if(FUN_DEBUG) + set(_FUN_DEBUG_STATE "ENABLED") +else() + set(_FUN_DEBUG_STATE "DISABLED") +endif() + +if(FUN_WITH_RUST) + set(_FUN_WITH_RUST_STATE "ENABLED") +else() + set(_FUN_WITH_RUST_STATE "DISABLED") +endif() + +message(STATUS "==== Fun build options ====") +message(STATUS " FUN_DEBUG: ${_FUN_DEBUG_STATE}") +message(STATUS " FUN_WITH_RUST: ${_FUN_WITH_RUST_STATE}") +message(STATUS "===========================") + # Convenience aggregate target (like 'build' in Makefile) add_custom_target(build DEPENDS fun fun_test test_opcodes ) +# --- Rust (Cargo) integration: optionally build and link a staticlib with opcode examples --- +option(FUN_WITH_RUST "Build and link Rust opcode library" OFF) + +if(FUN_WITH_RUST) + find_program(CARGO_EXECUTABLE cargo) + if(NOT CARGO_EXECUTABLE) + message(FATAL_ERROR "FUN_WITH_RUST=ON but 'cargo' not found in PATH") + endif() + + set(RUST_CRATE_DIR ${CMAKE_SOURCE_DIR}/src/rust) + + # Map CMake build type to Cargo profile and output path + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(RUST_PROFILE "debug") + set(RUST_BUILD_ARGS) + else() + set(RUST_PROFILE "release") + set(RUST_BUILD_ARGS --release) + endif() + + # Crate name as per Cargo.toml + # Crate name on-disk replaces '-' with '_' in artifact names + set(RUST_CRATE_NAME hello-c-world) + string(REPLACE "-" "_" RUST_CRATE_BASENAME "${RUST_CRATE_NAME}") + set(RUST_LIB_NAME lib${RUST_CRATE_BASENAME}.a) + set(RUST_LIB_PATH ${RUST_CRATE_DIR}/target/${RUST_PROFILE}/${RUST_LIB_NAME}) + + add_custom_command( + OUTPUT ${RUST_LIB_PATH} + COMMAND ${CARGO_EXECUTABLE} build ${RUST_BUILD_ARGS} + WORKING_DIRECTORY ${RUST_CRATE_DIR} + COMMENT "Building Rust static library (${RUST_PROFILE})" + VERBATIM + ) + + add_custom_target(rust_ops_build DEPENDS ${RUST_LIB_PATH}) + + add_library(fun_ops STATIC IMPORTED GLOBAL) + set_target_properties(fun_ops PROPERTIES + IMPORTED_LOCATION ${RUST_LIB_PATH} + IMPORTED_LINK_INTERFACE_LANGUAGES C + ) + add_dependencies(fun_ops rust_ops_build) + + # Link Rust ops into the core so executables can call them + if(TARGET fun_core) + add_dependencies(fun_core rust_ops_build) + target_link_libraries(fun_core PRIVATE fun_ops) + target_compile_definitions(fun_core PRIVATE FUN_WITH_RUST) + endif() + + # Propagate define to test targets that might call Rust + if(TARGET test_opcodes) + add_dependencies(test_opcodes rust_ops_build) + target_link_libraries(test_opcodes PRIVATE fun_ops) + target_compile_definitions(test_opcodes PRIVATE FUN_WITH_RUST) + endif() + + if(TARGET fun) + target_compile_definitions(fun PRIVATE FUN_WITH_RUST) + endif() +endif() + +# --- Size optimization for final binaries (Release) --- +# Enable function/data sectioning for better GC; safe for all configs. +add_compile_options(-ffunction-sections -fdata-sections) + +# Link-time garbage collection and stripping for the main executable in Release +if(TARGET fun) + # Garbage-collect unused sections; also strip symbols (-s) in Release + target_link_options(fun PRIVATE + $<$:-Wl,--gc-sections -s> + ) + # Prefer enabling LTO/IPO for Release builds + set_property(TARGET fun PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) +endif() + +if(TARGET fun_core) + # LTO/IPO for the core library helps the final link DCE more Rust/C glue + set_property(TARGET fun_core PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + # Ensure objects compiled with sectioning (added globally above) benefit from GC + target_link_options(fun_core PRIVATE $<$:-Wl,--gc-sections>) +endif() + # Convenience targets: repl, run, threads-demo, ops, examples set(FUN_RUN_SCRIPT "" CACHE STRING "Script to run with the 'run' target, e.g. -DFUN_RUN_SCRIPT=examples/strings_test.fun") diff --git a/examples/rust_hello.fun b/examples/rust_hello.fun new file mode 100644 index 0000000..5cd23d2 --- /dev/null +++ b/examples/rust_hello.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + +/* + * Rust-backed opcode demo: rust_hello() + * + * Build instructions: + * - Default builds disable Rust integration. + * - Enable it via: cmake -S . -B build_debug -DFUN_WITH_RUST=ON + * - Then: cmake --build build_debug --target fun + * + * Run: + * build_debug/fun examples/rust_hello.fun + * + * Expected output (with FUN_WITH_RUST=ON): + * Hello from Rust ops! + * + * If built without Rust, calling rust_hello() will raise a runtime error + * explaining that Rust integration is disabled. + */ + +print(rust_hello()) diff --git a/src/bytecode.c b/src/bytecode.c index 80a4ce7..58b62a8 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -206,6 +206,7 @@ static const char *opcode_name(OpCode op) { case OP_SIGN: return "SIGN"; case OP_FMIN: return "FMIN"; case OP_FMAX: return "FMAX"; + case OP_RUST_HELLO: return "RUST_HELLO"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index c1b9434..0cb4908 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -263,6 +263,9 @@ typedef enum { OP_FMIN, // pops b, a (int/float); pushes fmin(a,b) (NaN handling per C99) OP_FMAX, // pops b, a (int/float); pushes fmax(a,b) (NaN handling per C99) + // Rust FFI demo opcode(s) + OP_RUST_HELLO, // pushes string returned from Rust (hello world) + /* Notcurses TUI (optional) */ OP_NC_INIT, // initializes Notcurses; returns 1 on success, 0 on failure OP_NC_SHUTDOWN, // shuts down Notcurses; returns 0 diff --git a/src/parser.c b/src/parser.c index e110f68..78da293 100644 --- a/src/parser.c +++ b/src/parser.c @@ -786,6 +786,13 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "rust_hello") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "rust_hello expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_RUST_HELLO, 0); + free(name); + return 1; + } if (strcmp(name, "os_list_dir") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "os_list_dir expects (path)"); free(name); return 0; } diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000..a6c7500 --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "hello-c-world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[profile.release] +panic = "abort" +# Optimize for minimal size +opt-level = "z" +codegen-units = 1 +lto = true +# If Cargo is new enough, this strips symbols from Rust objects +strip = "symbols" diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs new file mode 100644 index 0000000..12121ed --- /dev/null +++ b/src/rust/src/lib.rs @@ -0,0 +1,44 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + +#![no_std] + +#[repr(C)] +pub struct Vm; + +extern "C" { + fn vm_pop_i64(vm: *mut Vm) -> i64; + fn vm_push_i64(vm: *mut Vm, v: i64); +} + +// Submodule with additional Rust VM math ops (exported via C ABI) +pub mod vm; + +#[no_mangle] +pub extern "C" fn fun_op_radd(vm: *mut Vm) -> i32 { + unsafe { + let b = vm_pop_i64(vm); + let a = vm_pop_i64(vm); + vm_push_i64(vm, a + b); + } + 0 +} + +#[no_mangle] +pub extern "C" fn fun_rust_get_string() -> *const core::ffi::c_char { + b"Hello from Rust ops!\0".as_ptr() as *const _ +} + +// Minimal panic handler for no_std; abort behavior requested via Cargo profile +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/src/test_opcodes.c b/src/test_opcodes.c index c6c6e3a..db79283 100644 --- a/src/test_opcodes.c +++ b/src/test_opcodes.c @@ -46,6 +46,29 @@ int main() { } vm_clear_output(&vm); + + /* --- Rust FFI demo: call a Rust opcode and string function --- */ +#ifdef FUN_WITH_RUST + extern int fun_op_radd(VM *vm); + extern const char *fun_rust_get_string(void); + + printf("=== Rust FFI demo ===\n"); + const char *rs = fun_rust_get_string(); + if (rs) { + printf("Rust says: %s\n", rs); + } + + /* prepare stack: push 10 and 32, then call Rust add -> expect 42 */ + vm_push_i64(&vm, 10); + vm_push_i64(&vm, 32); + int rc = fun_op_radd(&vm); + printf("fun_op_radd rc=%d\n", rc); + long long sum = (long long)vm_pop_i64(&vm); + printf("Rust op result: %lld\n", sum); +#else + printf("=== Rust FFI demo (disabled; build with -DFUN_WITH_RUST=ON) ===\n"); +#endif + vm_free(&vm); bytecode_free(bc); return 0; diff --git a/src/vm.c b/src/vm.c index 16ac20b..4c78db9 100644 --- a/src/vm.c +++ b/src/vm.c @@ -466,6 +466,28 @@ static Value pop_value(VM *vm) { return vm->stack[vm->sp--]; /* caller owns returned Value */ } +/* --- C ABI helpers for Rust FFI --- */ +int64_t vm_pop_i64(VM *vm) { + Value v = pop_value(vm); + int64_t out = 0; + if (v.type == VAL_INT) { + out = v.i; + } else if (v.type == VAL_FLOAT) { + out = (int64_t) v.d; + } else { + fprintf(stderr, "Runtime type error: expected int/float on stack, got %s\n", value_type_name(v.type)); + free_value(v); + exit(1); + } + /* free any dynamic payload (no-op for int/float) */ + free_value(v); + return out; +} + +void vm_push_i64(VM *vm, int64_t v) { + push_value(vm, make_int(v)); +} + static void frame_init(Frame *f) { f->fn = NULL; f->ip = 0; @@ -734,6 +756,9 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/math/isqrt.c" #include "vm/math/sign.c" + /* Rust FFI demo opcode(s) */ + #include "vm/rust/hello.c" + #include "vm/os/env.c" #include "vm/os/env_all.c" #include "vm/os/fun_version.c" diff --git a/src/vm.h b/src/vm.h index a08a891..0e1d528 100644 --- a/src/vm.h +++ b/src/vm.h @@ -54,6 +54,9 @@ static const char *opcode_names[] = { "SERIAL_OPEN","SERIAL_CONFIG","SERIAL_SEND","SERIAL_RECV","SERIAL_CLOSE", "TK_EVAL","TK_RESULT","TK_LOOP","TK_WM_TITLE","TK_LABEL","TK_BUTTON","TK_PACK", "TRY_PUSH","TRY_POP","THROW", + "FMIN","FMAX", + /* Rust FFI demo */ + "RUST_HELLO", /* Notcurses TUI (optional) */ "NC_INIT","NC_SHUTDOWN","NC_CLEAR","NC_DRAW_TEXT","NC_GETCH" }; @@ -143,4 +146,16 @@ static inline int opcode_is_valid(int op) { return op >= OP_NOP && op <= OP_NC_GETCH; // all current opcodes (including optional NC_*) } +/* --- Minimal C ABI helpers for FFI (Rust opcode experiments) --- */ +/* Pop an int64 from VM stack (errors if not an int/float); returns integer-converted value. */ +int64_t vm_pop_i64(VM *vm); +/* Push an int64 onto VM stack. */ +void vm_push_i64(VM *vm, int64_t v); + +/* Example Rust-implemented opcode (adds top two ints on stack) */ +int fun_op_radd(VM *vm); + +/* Example Rust function returning a demo C string (null-terminated). */ +const char *fun_rust_get_string(void); + #endif diff --git a/src/vm/rust/hello.c b/src/vm/rust/hello.c new file mode 100644 index 0000000..709d8c6 --- /dev/null +++ b/src/vm/rust/hello.c @@ -0,0 +1,27 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + + /** + * Rust FFI demo opcode: OP_RUST_HELLO + * When executed, it pushes a hello string returned by Rust onto the VM stack. + */ + +case OP_RUST_HELLO: { +#ifdef FUN_WITH_RUST + const char *s = fun_rust_get_string(); + if (!s) s = ""; + push_value(vm, make_string(s)); +#else + vm_raise_error(vm, "RUST_HELLO requires FUN_WITH_RUST=ON at build time"); + push_value(vm, make_nil()); +#endif + break; +} From da183962d42d864bb72ea1f4a0ddbf29ee801876 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 27 Jan 2026 04:46:37 +0100 Subject: [PATCH 16/17] Missed a file in the last commit. Sorry! (0.38.0) --- src/rust/src/vm/mod.rs | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/rust/src/vm/mod.rs diff --git a/src/rust/src/vm/mod.rs b/src/rust/src/vm/mod.rs new file mode 100644 index 0000000..aec43a4 --- /dev/null +++ b/src/rust/src/vm/mod.rs @@ -0,0 +1,70 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + +//! Rust VM helpers and opcode handlers exposed via C ABI. +//! +//! Functions in this module are compiled into the Rust static library and can +//! be called from the C VM. They operate on the VM stack using the minimal C +//! ABI helpers declared on the C side (`vm_pop_i64`, `vm_push_i64`). + +use super::Vm; + +extern "C" { + fn vm_pop_i64(vm: *mut Vm) -> i64; + fn vm_push_i64(vm: *mut Vm, v: i64); +} + +/// Multiply the top two integers on the VM stack. +/// +/// Before: [..., a, b] +/// After: [..., a*b] +#[no_mangle] +pub extern "C" fn fun_op_rmul(vm: *mut Vm) -> i32 { + unsafe { + let b = vm_pop_i64(vm); + let a = vm_pop_i64(vm); + vm_push_i64(vm, a.saturating_mul(b)); + } + 0 +} + +/// Subtract the top two integers on the VM stack. +/// +/// Before: [..., a, b] +/// After: [..., a-b] +#[no_mangle] +pub extern "C" fn fun_op_rsub(vm: *mut Vm) -> i32 { + unsafe { + let b = vm_pop_i64(vm); + let a = vm_pop_i64(vm); + vm_push_i64(vm, a.wrapping_sub(b)); + } + 0 +} + +/// Integer division of the top two integers on the VM stack (a / b). +/// Pushes 0 and returns 1 when dividing by zero. +/// +/// Before: [..., a, b] +/// After: [..., a/b] +#[no_mangle] +pub extern "C" fn fun_op_rdiv(vm: *mut Vm) -> i32 { + unsafe { + let b = vm_pop_i64(vm); + let a = vm_pop_i64(vm); + if b == 0 { + vm_push_i64(vm, 0); + return 1; // indicate error (div by zero) + } + vm_push_i64(vm, a / b); + } + 0 +} From 6fd349703271a496f964d35a84d8699d644d4bd5 Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 28 Jan 2026 02:02:19 +0100 Subject: [PATCH 17/17] More documentation about internals. No code changes (0.38.0) --- docs/internals.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/internals.md b/docs/internals.md index 7194603..9995968 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -253,3 +253,80 @@ From vm.h defaults (tuned for simplicity; adjust if needed): - src/parser.c — compiler, expression/statement/block parsing, emission, indentation handling - src/vm/* — small focused opcode handlers by domain - src/external/* — integration shims for optional dependencies + +## Concurrency, isolates, and garbage collection + +### Short answer + +We chose isolated state (like Lua) rather than a single global lock (like Python’s GIL). Each VM has its own heap, scheduler, and GC. There are no cross‑VM pointers. Concurrency and data exchange happen via message passing and a few carefully scoped shared‑memory primitives for high‑throughput use cases. This keeps the C API simple, predictable, and safe to embed in multi‑threaded hosts. + +### Concurrency model + +- Isolates/VMs: Each `fun_vm_t*` is an isolate with its own heap, bytecode, scheduler, and GC. Multiple VMs can run truly in parallel on different OS threads/cores. +- Intra‑VM concurrency: User‑space tasks (green threads/fibers) scheduled by the VM. Within a single VM, you get structured concurrency; the VM is single‑owner from the host’s perspective. +- Inter‑VM concurrency: Use message passing (channels/ports) and zero‑copy shared buffers where explicitly opted in. + +### Memory and sharing + +- Per‑VM heaps: Objects are allocated, owned, and collected per VM. No object from VM A may be referenced by VM B. +- No global runtime lock: There is no GIL. VMs never contend on a global lock and can scale across cores. +- Message passing: `fun_send(port, value)` and `fun_recv(port, timeout)` copy values across VM boundaries using a compact, GC‑safe serialization format. +- Zero‑copy fast path (opt‑in): For large payloads, hosts can create `fun_shared_buffer` objects which are reference‑counted, immutable within VMs, and can be shared across VMs without copying. Mutating a shared buffer requires creating a new buffer (copy‑on‑write style), preserving safety. + +### Thread‑safety rules for the C API + +- VM affinity: A `fun_vm_t*` has thread affinity. Host code must interact with a VM from its owning thread. If you need to call into the same VM from multiple OS threads, you post work to the VM’s run loop via `fun_vm_post(vm, callback, user_data)`. +- Opaque handles: All handles (`fun_vm_t*`, `fun_value_t`, `fun_port_t`, `fun_shared_buffer_t`) are opaque. There are no raw pointers to VM heap objects in the API. +- No cross‑VM objects: You cannot pass `fun_value_t` directly across VMs. Use `fun_send`/`fun_serialize`/`fun_deserialize` or `fun_shared_buffer`. +- Optional locking helpers: For the rare case where a host wants shared mutable state outside the VM, we expose thin wrappers over atomics and locks (`fun_atomic_*`, `fun_mutex_t`, `fun_rwlock_t`) so extension code doesn’t need to mix threading libraries. These do not participate in GC and are outside VM heaps. + +### Scheduling and GC + +- Per‑VM scheduler: Green threads are multiplexed within a VM. Preemption is cooperative with periodic safe points; an optional time‑slice can yield between bytecode instruction groups when `FUN_DEBUG` or tracing is enabled. +- Per‑VM GC: Stop‑the‑world, per‑VM. No global stop‑the‑world across VMs. A GC in one VM does not pause others. + +### Embedding patterns + +- Parallelism: Create N VMs for N cores, wire them with channels or shared buffers. No global lock contention. +- UI/game loop: Keep one VM on the main thread; background workers operate their own VMs and communicate via ports. +- Native callbacks/FFI: Callbacks into a VM must occur on its owning thread (use `fun_vm_post`). For bulk data (e.g., images, tensors), pass `fun_shared_buffer` to avoid copies. + +### Why not a GIL? + +- A GIL simplifies internal invariants but serializes all CPU‑bound work and penalizes embedded hosts with existing thread pools. Our isolate + message‑passing design preserves safety while scaling with cores. + +### When you must share state + +- Prefer `fun_shared_buffer` (immutable) or message passing. +- If you truly need shared mutability from native code, use `fun_atomic_*` or `fun_mutex_t`/`fun_rwlock_t` around your own data structures outside the VM. The VM treats these as external resources. + +### Minimal API surface (illustrative) + +```c +// Create/destroy VMs +fun_vm_t* vm = fun_vm_create(const fun_vm_config_t*); +void fun_vm_destroy(fun_vm_t*); + +// Thread-affine execution and posting work +int fun_vm_run(fun_vm_t*, const fun_script_t*); +int fun_vm_post(fun_vm_t*, void (*cb)(fun_vm_t*, void*), void* user); + +// Ports/channels for inter-VM comms +fun_port_t* fun_port_create(fun_vm_t*); +int fun_send(fun_port_t*, fun_value_t value); +int fun_recv(fun_port_t*, fun_value_t* out, uint64_t timeout_ms); + +// Serialization for cross-VM values +int fun_serialize(fun_value_t v, fun_buffer_t* out); +int fun_deserialize(fun_vm_t*, const fun_buffer_t*, fun_value_t* out); + +// Zero-copy shared buffers (immutable inside VMs) +fun_shared_buffer_t* fun_shared_buffer_new(size_t n); +void* fun_shared_buffer_data(fun_shared_buffer_t*); +void fun_shared_buffer_retain(fun_shared_buffer_t*); +void fun_shared_buffer_release(fun_shared_buffer_t*); +``` + +### Glossary + +- GC: garbage collection. In our context, each `fun_vm_t` isolate has its own GC (stop‑the‑world, per‑VM). There is no global stop‑the‑world and no global lock; a GC pause in one VM does not affect others.