diff --git a/.editorconfig b/.editorconfig index 1acfb67..e7b467c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,7 +45,7 @@ ij_shell_use_unix_line_separator = true [*.c] indent_style = space -indent_size = 2 +indent_size = 4 tab_width = 1 [*.csv] diff --git a/.github/workflows/cmake-ubuntu-latest.yml b/.github/workflows/cmake-ubuntu-latest.yml deleted file mode 100644 index 041956d..0000000 --- a/.github/workflows/cmake-ubuntu-latest.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Fun with CMake on Ubuntu-Slim - -on: - push: - branches: [ "main" ] - -env: - BUILD_TYPE: Release - -jobs: - build: - runs-on: ubuntu-slim - defaults: - run: - working-directory: ${{github.workspace}} - - steps: - - uses: actions/checkout@v4 - - - name: Update apt - run: sudo apt update - - #- name: Upgrade apt - # run: sudo apt upgrade -y - - - name: Install dependencies - run: sudo apt install cmake libjson-c-dev libssl-dev -y - - - name: Configure CMake - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DFUN_DEBUG=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_PCSC=OFF -DFUN_WITH_LIBSQL=OFF -DFUN_WITH_SQLITE=OFF -DFUN_WITH_CURL=OFF -DFUN_WITH_PCRE2=OFF -DFUN_WITH_XML2=OFF -DFUN_WITH_JSON=ON -DFUN_WITH_TCLTK=OFF -DFUN_WITH_INI=OFF -DFUN_WITH_NOTCURSES=OFF -DFUN_WITH_CPP=ON -DFUN_WITH_OPENSSL=ON -DFUN_WITH_LIBRESSL=OFF -DFUN_WITH_YAML=OFF -DFUN_USE_MUSL=OFF -DFUN_WITH_RUST=OFF - - - name: Build - run: cmake --build build --config ${{env.BUILD_TYPE}} --target all - - - name: Test - working-directory: ${{github.workspace}}/build - run: ctest -C ${{env.BUILD_TYPE}} - - - name: Run Examples - run: ./scripts/run_examples.sh - - - name: Run Examples (algos) - run: ./scripts/run_examples.sh algos - - - name: Run Examples (arrays) - run: ./scripts/run_examples.sh arrays - - - name: Run Examples (basics) - run: ./scripts/run_examples.sh basics - - - name: Run Examples (compose) - run: ./scripts/run_examples.sh compose - - - name: Run Examples (crypto) - run: ./scripts/run_examples.sh crypto - - #- name: Run Examples (external/curl) - # run: ./scripts/run_examples.sh external/curl - - #- name: Run Examples (external/ini) - # run: ./scripts/run_examples.sh external/ini - - - name: Run Examples (external/json) - run: ./scripts/run_examples.sh external/json - - #- name: Run Examples (external/libressl) - # run: ./scripts/run_examples.sh external/libressl - - #- name: Run Examples (external/libsql) - # run: ./scripts/run_examples.sh external/libsql - - #- name: Run Examples (external/notcurses) - # run: ./scripts/run_examples.sh external/notcurses - - - name: Run Examples (external/openssl) - run: ./scripts/run_examples.sh external/openssl - - #- name: Run Examples (external/pcre2) - # run: ./scripts/run_examples.sh external/pcre2 - - #- name: Run Examples (external/pcsc) - # run: ./scripts/run_examples.sh external/pcsc - - #- name: Run Examples (external/sqlite) - # run: ./scripts/run_examples.sh external/sqlite - - #- name: Run Examples (external/tcltk) - # run: ./scripts/run_examples.sh external/tcltk - - #- name: Run Examples (external/xml2) - # run: ./scripts/run_examples.sh external/xml2 - - - name: Run Examples (functions) - run: ./scripts/run_examples.sh functions - - - name: Run Examples (io) - run: ./scripts/run_examples.sh io - - - name: Run Examples (math) - run: ./scripts/run_examples.sh math - - - name: Run Examples (patterns) - run: ./scripts/run_examples.sh patterns - - - name: Run Examples (strings) - run: ./scripts/run_examples.sh strings diff --git a/.gitignore b/.gitignore index 063129a..48a69a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ CMakeCache.txt .* !.editorconfig !.gitignore -!.github .venv build* cmake_install.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 96d5229..34d9fb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.40.5 LANGUAGES C) +project(fun VERSION 0.39.15 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index fd335c9..dcfc6fd 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ See [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) for what the stand - [PCSC (smart cards)](./docs/external/pcsc.md) (optional) ☑ - [OpenSSL](./docs/external/openssl.md) (optional) ☑ - [SQLite](./docs/external/sqlite.md) (optional) ☑ +- [Tk (Tcl/Tk GUI)](./docs/external/tcltk.md) (optional) ☑ - [XML (libxml2)](./docs/external/xml2.md) (optional) ☑ ☑ = Done / ☐ = Planned or in progress. diff --git a/cmake/Extensions/Extensions.cmake b/cmake/Extensions/Extensions.cmake index a60fdca..92843b6 100644 --- a/cmake/Extensions/Extensions.cmake +++ b/cmake/Extensions/Extensions.cmake @@ -10,27 +10,34 @@ function(_fun_print_feature name flag) endif() endfunction() -# Include each extension module (Tcl/Tk removed) +# Include each extension module +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/TCLTK.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/SQLITE.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCSC.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/JSON.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/INI.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/XML2.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/LIBSQL.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCRE2.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/CURL.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/NOTCURSES.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/REPL.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/OPENSSL.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/LIBRESSL.cmake) # Summary of extension toggles message(STATUS "---- Fun extension summary ----") +_fun_print_feature("Tcl/Tk (FUN_WITH_TCLTK)" FUN_WITH_TCLTK) _fun_print_feature("SQLite (FUN_WITH_SQLITE)" FUN_WITH_SQLITE) _fun_print_feature("PCSC (FUN_WITH_PCSC)" FUN_WITH_PCSC) _fun_print_feature("JSON-C (FUN_WITH_JSON)" FUN_WITH_JSON) _fun_print_feature("INI/iniparser (FUN_WITH_INI)" FUN_WITH_INI) _fun_print_feature("libxml2 (FUN_WITH_XML2)" FUN_WITH_XML2) -## libsql extension removed +_fun_print_feature("libsql (FUN_WITH_LIBSQL)" FUN_WITH_LIBSQL) _fun_print_feature("PCRE2 (FUN_WITH_PCRE2)" FUN_WITH_PCRE2) _fun_print_feature("libcurl (FUN_WITH_CURL)" FUN_WITH_CURL) +_fun_print_feature("Notcurses (FUN_WITH_NOTCURSES)" FUN_WITH_NOTCURSES) _fun_print_feature("REPL (FUN_WITH_REPL)" FUN_WITH_REPL) _fun_print_feature("OpenSSL (FUN_WITH_OPENSSL)" FUN_WITH_OPENSSL) +_fun_print_feature("LibreSSL (FUN_WITH_LIBRESSL)" FUN_WITH_LIBRESSL) message(STATUS "--------------------------------") diff --git a/cmake/Targets.cmake b/cmake/Targets.cmake index 0e98eab..2184b30 100644 --- a/cmake/Targets.cmake +++ b/cmake/Targets.cmake @@ -35,7 +35,10 @@ foreach(var_pair INIPARSER LIBSQL LIBXML2 - OPENSSL) + TCL + NOTCURSES + OPENSSL + LIBRESSL) if(${var_pair}_INCLUDE_DIRS) target_include_directories(fun_core PRIVATE ${${var_pair}_INCLUDE_DIRS}) endif() @@ -76,11 +79,17 @@ if(FUN_WITH_CURL) target_compile_definitions(fun_core PUBLIC FUN_WITH_CURL=1) endif() +if(FUN_WITH_NOTCURSES) + target_compile_definitions(fun_core PUBLIC FUN_WITH_NOTCURSES=1) +endif() if(FUN_WITH_SQLITE) target_compile_definitions(fun_core PUBLIC FUN_WITH_SQLITE=1) endif() +if(FUN_WITH_TCLTK) + target_compile_definitions(fun_core PUBLIC FUN_WITH_TCLTK=1) +endif() if(FUN_WITH_OPENSSL) target_compile_definitions(fun_core PUBLIC FUN_WITH_OPENSSL=1) @@ -88,6 +97,9 @@ endif() # LibreSSL toggle: ensure compile definitions are applied to fun_core so # the LibreSSL code paths are compiled, and linking vars are handled above. +if(FUN_WITH_LIBRESSL) + target_compile_definitions(fun_core PUBLIC FUN_WITH_LIBRESSL=1) +endif() # Workaround: provide a dummy rust_eh_personality to satisfy linker when using # no_std Rust staticlib with panic abort (some toolchains still reference it). diff --git a/docs/README.md b/docs/README.md index 75c135a..d0cbc19 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ This file serves as an index of the documents in this directory. Links are relat ## New and supplemental guides -- [build.md](./build.md) - How to build Fun with CMake, available targets, and build options (FUN_DEBUG, FUN_USE_MUSL, FUN_WITH_CPP, FUN_WITH_RUST, FUN_WITH_OPENSSL). +- [build.md](./build.md) - How to build Fun with CMake, available targets, and build options (FUN_DEBUG, FUN_USE_MUSL, FUN_WITH_CPP, FUN_WITH_RUST, FUN_WITH_OPENSSL, FUN_WITH_LIBRESSL). - [cli.md](./cli.md) - Command-line usage of the `fun` executable: synopsis, options, exit codes, includes and library paths. - [fun.md](./fun.md) - Full usage guide for the `fun` executable: invocation patterns, REPL, env vars, include paths, examples, and install locations. - [asyncio.md](./asyncio.md) - Async I/O primitives and patterns: non-blocking sockets, fd polling, examples, and best practices. @@ -47,7 +47,7 @@ This file serves as an index of the documents in this directory. Links are relat Documentation for optional, build-time selectable integrations lives in [external/](./external/): - [Index of extensions](./external/README.md) - - Highlights: [cURL](./external/curl.md), [INI](./external/ini.md), [JSON](./external/json.md), [XML (libxml2)](./external/xml2.md), [SQLite](./external/sqlite.md), [PCRE2](./external/pcre2.md), [PC/SC](./external/pcsc.md), [OpenSSL](./external/openssl.md) + - Highlights: [cURL](./external/curl.md), [INI](./external/ini.md), [JSON](./external/json.md), [XML (libxml2)](./external/xml2.md), [SQLite](./external/sqlite.md), [libSQL](./external/libsql.md), [PCRE2](./external/pcre2.md), [PC/SC](./external/pcsc.md), [Notcurses](./external/notcurses.md), [Tcl/Tk](./external/tcltk.md), [OpenSSL](./external/openssl.md), [LibreSSL](./external/libressl.md) ## Tips @@ -55,3 +55,4 @@ Documentation for optional, build-time selectable integrations lives in [externa - For a broader project overview and quickstart, see the repository root [README.md](../README.md). - Crypto examples: - If built with `-DFUN_WITH_OPENSSL=ON`, try `examples/crypto/openssl_md5.fun`. + - If built with `-DFUN_WITH_LIBRESSL=ON`, try `examples/crypto/libressl_md5.fun`. diff --git a/docs/build.md b/docs/build.md index 9fb92d6..04c45a0 100644 --- a/docs/build.md +++ b/docs/build.md @@ -23,6 +23,7 @@ Fun exposes several options you can toggle at configure time: - `FUN_WITH_CPP` (ON/OFF) - Enable C++-based opcode/examples support - `FUN_WITH_RUST` (ON/OFF) - Build and link Rust staticlib from `src/rust/` - `FUN_WITH_OPENSSL` (ON/OFF) - Enable OpenSSL-backed helpers (MD5/SHA-256/SHA-512/RIPEMD-160) +- `FUN_WITH_LIBRESSL` (ON/OFF) - Enable LibreSSL-backed helpers (MD5/SHA-256/SHA-512/RIPEMD-160) When configuring, the build prints a summary like: @@ -55,7 +56,7 @@ cmake --build build_release --target build ### Enabling optional extensions ``` cmake -S . -B build_release -DCMAKE_BUILD_TYPE=Release \ - -DFUN_WITH_CPP=ON -DFUN_WITH_RUST=ON -DFUN_WITH_OPENSSL=ON + -DFUN_WITH_CPP=ON -DFUN_WITH_RUST=ON -DFUN_WITH_OPENSSL=ON -DFUN_WITH_LIBRESSL=ON cmake --build build_release --target build ``` @@ -63,6 +64,7 @@ If `FUN_WITH_RUST` is enabled, ensure `cargo` is available in PATH; the build wi If `FUN_WITH_OPENSSL` is enabled, CMake must detect your system OpenSSL (libcrypto). +If `FUN_WITH_LIBRESSL` is enabled, CMake detects LibreSSL directly (via pkg-config or standard include/lib locations) and links to LibreSSL's `libcrypto` — no OpenSSL installation is required. Both extensions use the EVP interface. Note: On OpenSSL 3.x, RIPEMD-160 may require the legacy provider; if unavailable, the helper returns an empty string. ## Running - CLI: run the `fun` executable from your build directory. diff --git a/docs/external/README.md b/docs/external/README.md index c9986d0..5219cbd 100644 --- a/docs/external/README.md +++ b/docs/external/README.md @@ -13,9 +13,13 @@ This section documents Fun's optional, build-time selectable extensions. Each pa - [JSON (json-c)](./json.md) - [libxml2 (XML)](./xml2.md) - [SQLite](./sqlite.md) +- [libSQL (SQLite-compatible)](./libsql.md) - [PCRE2 (Perl-compatible regex)](./pcre2.md) - [PC/SC (Smart cards)](./pcsc.md) +- [Notcurses (TUI)](./notcurses.md) +- [Tcl/Tk (GUI)](./tcltk.md) - [OpenSSL](./openssl.md) +- [LibreSSL](./libressl.md) ## Notes: diff --git a/docs/external/libressl.md b/docs/external/libressl.md new file mode 100644 index 0000000..388c69a --- /dev/null +++ b/docs/external/libressl.md @@ -0,0 +1,45 @@ +# LibreSSL extension (optional) + +- CMake option: FUN_WITH_LIBRESSL=ON +- Purpose: provide small crypto helpers backed by LibreSSL's libcrypto. Includes md5, sha256, sha512, ripemd160 helpers under libressl_* names. +- Homepage: https://www.libressl.org/ + +## Build notes: + +- Requires system LibreSSL development headers and libcrypto. The build detects LibreSSL directly (via pkg-config or standard include/lib locations) and links to libcrypto from LibreSSL — it does not require OpenSSL to be installed. +- When disabled, the builtins below evaluate to empty strings to mirror optionality behavior across extensions. + +## Provided helper functions/opcodes: + +- Function: libressl_md5(data:string) -> string (lowercase hex). +- Function: libressl_sha256(data:string) -> string (lowercase hex). +- Function: libressl_sha512(data:string) -> string (lowercase hex). +- Function: libressl_ripemd160(data:string) -> string (lowercase hex). +- Opcodes: OP_LIBRESSL_MD5, OP_LIBRESSL_SHA256, OP_LIBRESSL_SHA512, OP_LIBRESSL_RIPEMD160 (internal mappings for the functions above). + +## Quickstart: + +- Configure: `cmake -S . -B build -DFUN_WITH_LIBRESSL=ON` +- Build: `cmake --build build --target fun` +- Run examples: + - `./build/fun examples/crypto/libressl_md5.fun` + - `./build/fun examples/crypto/libressl_sha256.fun` + - `./build/fun examples/crypto/libressl_sha512.fun` + - `./build/fun examples/crypto/libressl_ripemd160.fun` + +## Example output: + +- md5(abc) = 900150983cd24fb0d6963f7d28e17f72 +- md5("") = d41d8cd98f00b204e9800998ecf8427e +- sha256(abc) = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad +- sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +- sha512(abc) = ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f +- sha512("") = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e +- ripemd160(abc) = 8eb208f7e05d987a9b044a8e98c6b087f15a0bfc +- ripemd160("") = 9c1185a5c5e9fc54612808977ee8f548b2258d31 + +## Notes: + +- The functions accept any Fun value; non-strings are coerced via `to_string` semantics by the VM before hashing. +- On some platforms, RIPEMD-160 may not be available; in that case the helper returns an empty string. +- If your system installs LibreSSL headers under `/usr/include/libressl`, CMake will automatically add this directory to the include path so that `#include ` resolves correctly. diff --git a/docs/external/libsql.md b/docs/external/libsql.md new file mode 100644 index 0000000..43662a7 --- /dev/null +++ b/docs/external/libsql.md @@ -0,0 +1,17 @@ +# libSQL extension (optional) + +- CMake option: FUN_WITH_LIBSQL=ON +- Purpose: SQLite-compatible client using the libSQL (Turso) library. +- Homepage: https://libsql.org/ + +## Opcodes: + +- OP_LIBSQL_OPEN: pops url_or_path; pushes handle (>0) or 0 +- OP_LIBSQL_CLOSE: pops handle; pushes Nil +- OP_LIBSQL_EXEC: pops sql, handle; pushes rc:int (0=OK) +- OP_LIBSQL_QUERY: pops sql, handle; pushes array + +## Notes: + +- Uses a SQLite-compatible C API provided by libSQL; behavior is similar to the SQLite backend. +- This module is independent from the SQLite extension; you may enable either or both. diff --git a/docs/external/notcurses.md b/docs/external/notcurses.md new file mode 100644 index 0000000..f845843 --- /dev/null +++ b/docs/external/notcurses.md @@ -0,0 +1,27 @@ +# Notcurses (TUI) extension (optional) + +- CMake option: FUN_WITH_NOTCURSES=ON +- Purpose: Terminal UI capabilities via the Notcurses library. +- Homepage: https://notcurses.com/ + +## Opcodes: + +- OP_NC_INIT: initialize Notcurses; returns 1 on success, 0 on failure +- OP_NC_SHUTDOWN: shutdown; returns 0 +- OP_NC_CLEAR: clear screen/plane; returns 0 +- OP_NC_DRAW_TEXT: pops text, x, y; draws; returns 0 +- OP_NC_GETCH: pops timeout_ms; returns codepoint or -1 on timeout/error +- OP_NC_GET_SIZE: push [rows, cols] of the stdplane, or -1 if unavailable +- OP_NC_SET_STYLE: pops style, bg_rgb, fg_rgb; sets colors/styles for stdplane; returns 0/-1 +- OP_NC_DRAW_CHAR: pops ch, x, y; draws a single Unicode codepoint; returns 0/-1 +- OP_NC_DRAW_HLINE: pops len, x, y, ch; draws a horizontal line; returns 0/-1 +- OP_NC_DRAW_VLINE: pops len, x, y, ch; draws a vertical line; returns 0/-1 +- OP_NC_BOX: pops x, y, w, h, style; draws a rectangle using ASCII/Unicode box glyphs; returns 0/-1 +- OP_NC_FILL: pops x, y, w, h, ch; fills an area with a codepoint; returns 0/-1 +- OP_NC_RENDER: forces a render; returns 0/-1 + +## Notes: + +- Requires Notcurses development headers/libs. +- Behavior may vary across terminals; see the implementation for details. +- When Fun is built without Notcurses (FUN_WITH_NOTCURSES=OFF), these opcodes still exist but perform no-ops and return -1/0 as documented, allowing scripts to degrade gracefully. diff --git a/docs/external/tcltk.md b/docs/external/tcltk.md new file mode 100644 index 0000000..f39d66a --- /dev/null +++ b/docs/external/tcltk.md @@ -0,0 +1,21 @@ +# Tcl/Tk (GUI) extension (optional) + +- CMake option: FUN_WITH_TCLTK=ON +- Purpose: Basic GUI functionality via Tcl/Tk. +- Homepage: https://www.tcl.tk/ + +## Opcodes: + +- OP_TK_EVAL: pops script string; pushes rc (0=OK) +- OP_TK_RESULT: pushes last Tcl result string +- OP_TK_LOOP: enters main event loop; pushes Nil when done +- OP_TK_WM_TITLE: pops title string; sets window title; pushes rc +- OP_TK_LABEL: pops text, id; creates/updates label .id; pushes rc +- OP_TK_BUTTON: pops text, id; creates/updates button .id; pushes rc +- OP_TK_PACK: pops id; packs .id; pushes rc +- OP_TK_BIND: pops command, event, id; binds; pushes rc + +## Notes: + +- Requires Tcl/Tk development headers/libs. +- GUI behavior depends on your desktop environment/window manager. diff --git a/docs/handbook.md b/docs/handbook.md index 3e6508a..006d60a 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -93,12 +93,15 @@ Pass all options as -DNAME=VALUE. The most relevant toggles are: - FUN_DEBUG=ON|OFF — verbose VM debug logging (default OFF) - FUN_WITH_CURL=ON|OFF — enable CURL (libcurl) support (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON (json-c) support (default OFF) +- FUN_WITH_LIBSQL=ON|OFF — enable libSQL (Turso) client support (default OFF) - FUN_WITH_XML2=ON|OFF — enable XML (libxml2) support (default OFF) - FUN_WITH_PCRE2=ON|OFF — enable PCRE2 (Perl-Compatible Regular Expressions) (default OFF) - FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card (PCSC lite) support (default OFF) - 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: @@ -128,6 +131,27 @@ sqlite3 ./database.sqlite < ./examples/data/database.sql FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlite_example.fun ``` +#### libSQL example (optional feature) + +libSQL support is optional and disabled by default. It is implemented as an independent extension and can coexist with SQLite. To build with it and run the example: + +``` +cmake -S . -B build -DFUN_WITH_LIBSQL=ON +cmake --build build --target fun + +# Create the sample database using the sqlite3 CLI (libSQL implements the sqlite C API) +sqlite3 ./database.sqlite < ./examples/data/database.sql + +# Run the libSQL example +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/libsql_example.fun +``` + +Available builtins when built with -DFUN_WITH_LIBSQL=ON: +- 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 map rows + #### XML example (optional feature) XML support (via libxml2) is optional and disabled by default. To build with it and run the example: @@ -172,6 +196,52 @@ Notes: - Handles are simple integers managed by the VM; nodes are owned by their document. - This initial integration focuses on parsing and basic navigation. Attributes, children iteration, and XPath may be added later. +#### Tk GUI example (optional feature) + +Tk GUI support is optional and disabled by default. It embeds a Tcl/Tk interpreter and exposes a small, Tk-only API to Fun code (no raw Tcl required). + +To build with Tk and run the example: + +``` +cmake -S . -B build -DFUN_WITH_TCLTK=ON +cmake --build build --target fun + +# Run the example +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/tk_hello.fun +``` + +Available VM builtins when built with -DFUN_WITH_TCLTK=ON: +- tk_title(title: string) -> rc +- tk_label(id: string, text: string) -> rc +- tk_button(id: string, text: string) -> rc +- tk_pack(id: string) -> rc +- tk_loop() -> Nil (enters event loop until the window is closed) + +Standard library wrapper (lib/ui/tk.fun): +- class TK + - title(title: string): int + - label(id: string, text: string): int + - button(id: string, text: string): int + - pack(id: string): int + - loop(): Nil + +Example Fun code: +``` +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 is installed (8.6+). On Linux install tcl/tk packages; on macOS install Homebrew tcl-tk; on Windows ensure the DLLs are available. +- The Fun process terminates when the main window is closed or when the example's OK button is clicked. + ### Install Fun to the OS (optional) Not recommended during early development, but supported: @@ -464,7 +534,7 @@ 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) +### CURL (optional) Build flag: -DFUN_WITH_CURL=ON; requires libcurl. @@ -588,6 +658,28 @@ else 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. @@ -623,6 +715,22 @@ else 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). @@ -640,6 +748,35 @@ 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) diff --git a/docs/opcodes.md b/docs/opcodes.md index 5b04a27..3d512e9 100644 --- a/docs/opcodes.md +++ b/docs/opcodes.md @@ -157,7 +157,12 @@ This document provides an overview of the available VM opcodes implemented under - 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 @@ -240,7 +245,9 @@ This document provides an overview of the available VM opcodes implemented under - OP_NC_FILL: Fill rectangle with codepoint; pops x:int, y:int, w:int, h:int, ch:int; pushes 0/-1. - OP_NC_RENDER: Force a render; pushes 0/-1. - +## SQLite-compatible lib (libSQL) + +- See “libSQL” section above; identical opcode set with different backend. ## TK (Tcl/Tk UI) diff --git a/examples/blocking/tk_file_manager.fun b/examples/blocking/tk_file_manager.fun new file mode 100755 index 0000000..1614c45 --- /dev/null +++ b/examples/blocking/tk_file_manager.fun @@ -0,0 +1,59 @@ +#!/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: 2025-12-23 + */ + +#include + +print("Initializing Fun File Manager...") +tk = TK() +tk.title("Fun File Manager") + +// Current directory +dir = env("PWD") +if (dir == "") + dir = "." +print("Current directory: " + dir) + +tk.label("path", "Current Dir: " + dir) +tk.pack("path") + +// Files listbox +tk.listbox("files") +tk.pack("files") + +// Populate listbox +fun refresh(tk, dir) + print("Refreshing file list for: " + dir) + tk.clear("files") + files = os_list_dir(dir) + print("Found " + to_string(len(files)) + " entries.") + for f in files + tk.insert("files", "end", f) + +refresh(tk, dir) + +// Refresh button +// Using tk.eval for button because tk.button currently exits the app. +tk.eval("button .refresh -text {Refresh} -command {puts {Refresh requested}}") +tk.pack("refresh") + +// Exit button +tk.button("exit", "Exit") +tk.pack("exit") + +print("Entering Tk loop...") +tk.loop() +print("Tk loop exited.") + +/* Expected output: +A GUI... ;) +*/ diff --git a/examples/blocking/tk_hello.fun b/examples/blocking/tk_hello.fun new file mode 100755 index 0000000..88d2129 --- /dev/null +++ b/examples/blocking/tk_hello.fun @@ -0,0 +1,33 @@ +#!/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: 2025-12-09 + */ + +// Demonstrates the Tk stdlib wrapper class using the new Tk opcodes. + +include + +tk = TK() + +tk.title("Fun + Tk GUI") + +tk.label("hello", "Hello, world!") +tk.pack("hello") + +tk.button("ok", "OK") +tk.pack("ok") + +// Enter GUI loop (no-op if built without FUN_WITH_TCLTK) +tk.loop() + +/* Expected output: +A GUI... ;) +*/ diff --git a/examples/blocking/tk_testing.fun b/examples/blocking/tk_testing.fun new file mode 100755 index 0000000..9bff362 --- /dev/null +++ b/examples/blocking/tk_testing.fun @@ -0,0 +1,38 @@ +#!/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: 2025-12-23 + */ + +#include + +print("Testing os_list_dir...") +files = os_list_dir(".") +print("Found " + to_string(len(files)) + " files.") +if len(files) > 0 + print("First file: " + files[0]) + +print("Testing tk_bind parsing...") +// We can't easily test Tk without an X server, but we can see if it crashes. +// If built with FUN_WITH_TCLTK, it should at least initialize. +// We use tk_eval to avoid full loop. +rc = tk_eval("set x 1") +print("tk_eval rc: " + to_string(rc)) +if rc == 0 + print("Tcl Result: " + tk_result()) + +/* Expected output: +Testing os_list_dir... +Found 23 files. +First file: build +Testing tk_bind parsing... +tk_eval rc: 0 +Tcl Result: 1 +*/ diff --git a/examples/broken/complex_nested_no_capture.fun b/examples/broken/complex_nested_no_capture.fun deleted file mode 100644 index 648bb1d..0000000 --- a/examples/broken/complex_nested_no_capture.fun +++ /dev/null @@ -1,177 +0,0 @@ -#!/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-04-02 - */ - -/* - * Complex demonstration of nested functions that are local to their - * enclosing function, without any variable capture (no closures). - * All state is passed explicitly through parameters at each call. - * - * The example showcases: - * - Deeply nested helpers and multi-stage orchestration - * - Returning a nested function and using it later - * - Recursion implemented via a nested helper - * - Working with maps while threading state explicitly - */ - -print("=== Complex nested functions (no captures) ===") - -// 1) A small data pipeline on a user map, using local helpers -fun process_user(user) - // Ensure a key is present; if missing, fill with default - fun ensure_has(m, key, def) - if has(m, key) - return m - m[key] = def - return m - - // Normalize optional fields - fun normalize_city(m) - if has(m, "city") - return m - m["city"] = "Unknown" - return m - - // Add derived attributes explicitly via parameters (no function calls on RHS) - fun annotate(m) - a = m["age"] - // Inline age grouping without relying on an additional helper - g = "" - if a < 13 - g = "child" - else if a < 20 - g = "teen" - else if a < 65 - g = "adult" - else - g = "senior" - m["group"] = g - return m - - m1 = ensure_has(user, "name", "N/A") - m2 = ensure_has(m1, "age", 0) - m3 = normalize_city(m2) - m4 = annotate(m3) - return m4 - -u1 = {"name": "Ada", "age": 37} -u2 = process_user(u1) -print("process_user -> " + to_string(u2)) - -print("") -print("=== Returning a nested function (no captures) ===") - -// 2) Return an inner function and use it later. The returned function -// still requires all the data it needs as parameters (no implicit capture). -fun math_suite() - fun addk(x, k) - return x + k - - fun mulk(x, k) - return x * k - - // Integer power via nested recursion helper - fun powi(x, n) - fun loop(acc, base, exp) - if exp == 0 - return acc - return loop(acc * base, base, exp - 1) - return loop(1, x, n) - - // Compose a small arithmetic chain explicitly. Break into simple steps to - // match parser expectations and avoid nested calls on the right-hand side. - fun apply_chain(x, k_a, k_b, n) - t1 = x + k_a - t2 = t1 * k_b - t3 = powi(t2, n) - return t3 - - return apply_chain - -chain = math_suite() -print("chain(2, 3, 4, 2) -> expected ((2+3)*4)^2 = 400") -print(chain(2, 3, 4, 2)) - -print("") -print("=== Deep orchestration with 3-level nesting ===") - -// 3) Multi-stage pipeline with explicit parameter threading through each level -fun orchestrate(a, b, c) - fun stage1(x, a_, b_, c_) - fun stage2(y, b2, c2) - fun stage3(z, c3) - // No capture: every value needed arrives via parameters - tmp = z + c3 - return tmp * 2 - return stage3(y + b2, c2) - return stage2(x + a_, b_, c_) - - // Kick off with x = 0 and thread a, b, c explicitly - return stage1(0, a, b, c) - -print("orchestrate(1, 2, 3) -> stage3((0+1)+2, 3) * 2 = (3+3)*2 = 12") -print(orchestrate(1, 2, 3)) - -print("") -print("=== Nested recursion: factorial via inner loop ===") - -// 4) Factorial using an inner tail-recursive helper (no captures) -fun fact(n) - fun go(i, acc) - if i <= 1 - return acc - return go(i - 1, acc * i) - return go(n, 1) - -print("fact(6) -> expected 720") -print(fact(6)) - -print("") -print("=== Higher-order style without captures ===") - -// 5) Higher-order-like usage where the "strategy" function receives -// all needed parameters explicitly. -fun reducer_sum_with_limit(x, limit) - if x > limit - return 0 - return x - -fun fold3(a, b, c, f, p1, p2, p3) - // Apply f to each and sum; f must accept (value, param) - s1 = f(a, p1) - s2 = f(b, p2) - s3 = f(c, p3) - return s1 + s2 + s3 - -print("fold3 with limit: (5<=10 ? 5 : 0) + (12<=10 ? 0 : 0) + (7<=10 ? 7 : 0) = 12") -print(fold3(5, 12, 7, reducer_sum_with_limit, 10, 10, 10)) - -/* Expected output: -=== Complex nested functions (no captures) === -process_user -> {"age": 37, "city": "Unknown", "group": "adult", "name": "Ada"} - -=== Returning a nested function (no captures) === -chain(2, 3, 4, 2) -> expected ((2+3)*4)^2 = 400 -400 - -=== Deep orchestration with 3-level nesting === -orchestrate(1, 2, 3) -> stage3((0+1)+2, 3) * 2 = (3+3)*2 = 12 -12 - -=== Nested recursion: factorial via inner loop === -fact(6) -> expected 720 -720 - -=== Higher-order style without captures === -fold3 with limit: (5<=10 ? 5 : 0) + (12<=10 ? 0 : 0) + (7<=10 ? 7 : 0) = 12 -12 -*/ diff --git a/examples/crypto/libressl_md5.fun b/examples/crypto/libressl_md5.fun new file mode 100755 index 0000000..24b3740 --- /dev/null +++ b/examples/crypto/libressl_md5.fun @@ -0,0 +1,28 @@ +#!/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-02-19 + */ + +// LibreSSL MD5 example +// Enable with -DFUN_WITH_LIBRESSL=ON during build for real hashing. + +s = "abc" +d = libressl_md5(s) +print("md5(abc) = " + d) + +// Another quick check (empty string) +e = "" +print("md5(\"\") = " + libressl_md5(e)) + +/* Expected output: +md5(abc) = 900150983cd24fb0d6963f7d28e17f72 +md5("") = d41d8cd98f00b204e9800998ecf8427e +*/ diff --git a/examples/crypto/libressl_ripemd160.fun b/examples/crypto/libressl_ripemd160.fun new file mode 100755 index 0000000..437e6e1 --- /dev/null +++ b/examples/crypto/libressl_ripemd160.fun @@ -0,0 +1,28 @@ +#!/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-02-19 + */ + +// LibreSSL RIPEMD-160 example +// Enable with -DFUN_WITH_LIBRESSL=ON during build for real hashing. + +s = "abc" +d = libressl_ripemd160(s) +print("ripemd160(abc) = " + d) + +// Another quick check (empty string) +e = "" +print("ripemd160(\"\") = " + libressl_ripemd160(e)) + +/* Expected output: +ripemd160(abc) = 8eb208f7e05d987a9b049a9a5c0c2b74e07e6a5d +ripemd160("") = 9c1185a5c5e9fc54612808977ee8f548b2258d31 +*/ diff --git a/examples/crypto/libressl_sha256.fun b/examples/crypto/libressl_sha256.fun new file mode 100755 index 0000000..a51baed --- /dev/null +++ b/examples/crypto/libressl_sha256.fun @@ -0,0 +1,28 @@ +#!/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-02-19 + */ + +// LibreSSL SHA-256 example +// Enable with -DFUN_WITH_LIBRESSL=ON during build for real hashing. + +s = "abc" +d = libressl_sha256(s) +print("sha256(abc) = " + d) + +// Another quick check (empty string) +e = "" +print("sha256(\"\") = " + libressl_sha256(e)) + +/* Expected output: +sha256(abc) = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad +sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +*/ diff --git a/examples/crypto/libressl_sha512.fun b/examples/crypto/libressl_sha512.fun new file mode 100755 index 0000000..525dbcf --- /dev/null +++ b/examples/crypto/libressl_sha512.fun @@ -0,0 +1,28 @@ +#!/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-02-19 + */ + +// LibreSSL SHA-512 example +// Enable with -DFUN_WITH_LIBRESSL=ON during build for real hashing. + +s = "abc" +d = libressl_sha512(s) +print("sha512(abc) = " + d) + +// Another quick check (empty string) +e = "" +print("sha512(\"\") = " + libressl_sha512(e)) + +/* Expected output: +sha512(abc) = ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f +sha512("") = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e +*/ diff --git a/examples/external/openssl/openssl_md5.fun b/examples/crypto/openssl_md5.fun similarity index 100% rename from examples/external/openssl/openssl_md5.fun rename to examples/crypto/openssl_md5.fun diff --git a/examples/external/openssl/openssl_ripemd160.fun b/examples/crypto/openssl_ripemd160.fun similarity index 100% rename from examples/external/openssl/openssl_ripemd160.fun rename to examples/crypto/openssl_ripemd160.fun diff --git a/examples/external/openssl/openssl_sha256.fun b/examples/crypto/openssl_sha256.fun similarity index 100% rename from examples/external/openssl/openssl_sha256.fun rename to examples/crypto/openssl_sha256.fun diff --git a/examples/external/openssl/openssl_sha512.fun b/examples/crypto/openssl_sha512.fun similarity index 100% rename from examples/external/openssl/openssl_sha512.fun rename to examples/crypto/openssl_sha512.fun diff --git a/examples/external/curl/curl_download.fun b/examples/extra/curl_download.fun similarity index 100% rename from examples/external/curl/curl_download.fun rename to examples/extra/curl_download.fun diff --git a/examples/external/curl/curl_get_json.fun b/examples/extra/curl_get_json.fun similarity index 100% rename from examples/external/curl/curl_get_json.fun rename to examples/extra/curl_get_json.fun diff --git a/examples/external/curl/curl_post.fun b/examples/extra/curl_post.fun similarity index 100% rename from examples/external/curl/curl_post.fun rename to examples/extra/curl_post.fun diff --git a/examples/external/ini/ini_class_demo.fun b/examples/extra/ini_class_demo.fun similarity index 100% rename from examples/external/ini/ini_class_demo.fun rename to examples/extra/ini_class_demo.fun diff --git a/examples/external/ini/ini_complex.fun b/examples/extra/ini_complex.fun similarity index 100% rename from examples/external/ini/ini_complex.fun rename to examples/extra/ini_complex.fun diff --git a/examples/external/ini/ini_demo.fun b/examples/extra/ini_demo.fun similarity index 100% rename from examples/external/ini/ini_demo.fun rename to examples/extra/ini_demo.fun diff --git a/examples/external/ini/ini_diag.fun b/examples/extra/ini_diag.fun similarity index 100% rename from examples/external/ini/ini_diag.fun rename to examples/extra/ini_diag.fun diff --git a/examples/external/ini/ini_subsections.fun b/examples/extra/ini_subsections.fun similarity index 100% rename from examples/external/ini/ini_subsections.fun rename to examples/extra/ini_subsections.fun diff --git a/examples/external/json/json_showcase.fun b/examples/extra/json_showcase.fun similarity index 100% rename from examples/external/json/json_showcase.fun rename to examples/extra/json_showcase.fun diff --git a/examples/extra/libsql_example.fun b/examples/extra/libsql_example.fun new file mode 100755 index 0000000..1bfc430 --- /dev/null +++ b/examples/extra/libsql_example.fun @@ -0,0 +1,59 @@ +#!/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: 2025-11-26 + */ + +// Demonstrates the optional libSQL extension +// Build with: cmake -S . -B build -DFUN_WITH_LIBSQL=ON && cmake --build build + +// Prepare sample DB from SQL if needed (requires sqlite3 CLI installed) +// Create it once with: +// sqlite3 ./database.sqlite < ./examples/data/database.sql + +number h = libsql_open("./database.sqlite") +if h == 0 + print("Failed to open libSQL database") +else + libsql_exec(h, "CREATE TABLE IF NOT EXISTS todos(id INTEGER PRIMARY KEY, title TEXT, done INT)") + libsql_exec(h, "DELETE FROM todos") + libsql_exec(h, "INSERT INTO todos(title, done) VALUES('Buy milk', 0)") + libsql_exec(h, "INSERT INTO todos(title, done) VALUES('Write code', 1)") + + rows = libsql_query(h, "SELECT id, title, done FROM todos ORDER BY id") + for row in rows + print(to_string(row["id"]) + ": " + to_string(row["title"]) + " (done="+to_string(row["done"]) + ")") + + rows = libsql_query(h, "SELECT id, title, done, created_at FROM tasks ORDER BY id;") + print("Tasks (" + to_string(len(rows)) + "):") + for row in rows + string status = "✘" + if row["done"] == 1 + status = "✔" + print("- [" + status + "] (#" + to_string(row["id"]) + ") " + to_string(row["title"]) + " — " + to_string(row["created_at"])) + + number rc = libsql_exec(h, "INSERT INTO tasks (title, done) VALUES ('Try Fun + SQLite', 0);") + print("Insert rc=" + to_string(rc)) + + rows2 = libsql_query(h, "SELECT count(*) AS cnt FROM tasks;") + print("Total tasks now: " + to_string(rows2[0]["cnt"])) + + libsql_close(h) + +/* Example output: +1: Buy milk (done=0) +2: Write code (done=1) +Tasks (3): +- [✔] (#1) Write Fun + SQLite example — 2025-11-26 23:20:41 +- [✘] (#2) Ship optional feature flag — 2025-11-26 23:20:41 +- [✘] (#3) Celebrate with coffee — 2025-11-26 23:20:41 +Insert rc=0 +Total tasks now: 4 +*/ diff --git a/examples/extra/notcurses_hello.fun b/examples/extra/notcurses_hello.fun new file mode 100755 index 0000000..ba0c493 --- /dev/null +++ b/examples/extra/notcurses_hello.fun @@ -0,0 +1,46 @@ +#!/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-03 + */ + +/* + * Minimal Notcurses hello example. + */ + +include + +n = Notcurses() + +if n.init() == 0 + print("Notcurses not available. Rebuild with -DFUN_WITH_NOTCURSES=ON.") + exit(0) + +n.clear() +n.draw_text(2, 0, "Fun + Notcurses") +n.draw_text(4, 0, "Press any key to exit...") + +// blocking until key +_ = n.getch(0) +n.shutdown() + +/* Possible output: +A TUI. + +After exit: +3 renders, 991,14µs (223,57µs min, 330,38µs avg, 531,91µs max) +3 rasters, 320,76µs (106,45µs min, 106,92µs avg, 107,72µs max) +3 writes, 198,26µs (62,54µs min, 66,09µs avg, 69,94µs max) +59B (0B min, 19B avg, 30B max) 1 input Ghpa: 0 +0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors +RGB emits:elides: def 1:38 fg 0:0 bg 0:0 +Cell emits:elides: 39:74805 (99,95%) 97,44% 0,00% 0,00% +Bmap emits:elides: 0:0 (0,00%) 0B (0,00%) SuM: 0 (0,00%) +*/ diff --git a/examples/extra/notcurses_menu.fun b/examples/extra/notcurses_menu.fun new file mode 100755 index 0000000..8c17449 --- /dev/null +++ b/examples/extra/notcurses_menu.fun @@ -0,0 +1,33 @@ +#!/usr/bin/env fun + +include + +n = Notcurses() +if n.init() == 0 + print("Notcurses not available. Build with -DFUN_WITH_NOTCURSES=ON.") + exit(1) + +items = ["First", "Second", "Third", "Fourth"] +sel = 0 +while true + n.clear() + // Title + n.set_style(0xFFFF00, 0x000000, 1) // bold yellow + n.draw_text(1, 2, "Demo Menu (j/k to move, Enter to select, ESC to quit)") + + // Render menu list + n.menu(3, 4, items, sel, 30, 0, 32) + + key = n.getch(0) + if key == 27 // ESC + break + if key == 106 // 'j' + sel = (sel + 1) % len(items) + if key == 107 // 'k' + sel = (sel - 1 + len(items)) % len(items) + if key == 10 // Enter + n.status_bar("Selected: " + items[sel], 32) + // brief wait + sleep_ms(300) + +n.shutdown() diff --git a/examples/extra/notcurses_progress.fun b/examples/extra/notcurses_progress.fun new file mode 100755 index 0000000..8176685 --- /dev/null +++ b/examples/extra/notcurses_progress.fun @@ -0,0 +1,38 @@ +#!/usr/bin/env fun + +include + +n = Notcurses() +if n.init() == 0 + print("Notcurses not available. Build with -DFUN_WITH_NOTCURSES=ON.") + exit(1) + +n.clear() +sz = n.size() +rows = 24 +cols = 80 +if typeof(sz) == "array" + if len(sz) >= 2 + rows = sz[0] + cols = sz[1] + +y = to_number(rows) / 2 +w = cols - 10 +if w < 20 + w = 20 + +i = 0 +while i <= 100 + // styles handled inside progress(); keep plane sane + rc = n.set_style(0x00FF00, 0x000000, 1) + frac = to_number(i) / 100 + rc = n.progress(y, 5, w, frac, 0) + k = n.getch(10) + i = i + 1 + +// final message +rc = n.set_style(0xFFFFFF, 0x000000, 1) +rc = n.draw_text(y + 2, 5, "Done!") +rc = n.render() + +rc = n.shutdown() diff --git a/examples/external/pcre2/pcre2_demo.fun b/examples/extra/pcre2_demo.fun similarity index 100% rename from examples/external/pcre2/pcre2_demo.fun rename to examples/extra/pcre2_demo.fun diff --git a/examples/external/pcre2/pcre2_opcodes.fun b/examples/extra/pcre2_opcodes.fun similarity index 100% rename from examples/external/pcre2/pcre2_opcodes.fun rename to examples/extra/pcre2_opcodes.fun diff --git a/examples/external/pcre2/pcre2_showcase.fun b/examples/extra/pcre2_showcase.fun similarity index 100% rename from examples/external/pcre2/pcre2_showcase.fun rename to examples/extra/pcre2_showcase.fun diff --git a/examples/external/pcre2/pcsc2_example.fun b/examples/extra/pcsc2_example.fun similarity index 100% rename from examples/external/pcre2/pcsc2_example.fun rename to examples/extra/pcsc2_example.fun diff --git a/examples/external/pcsc/pcsc_demo.fun b/examples/extra/pcsc_demo.fun similarity index 100% rename from examples/external/pcsc/pcsc_demo.fun rename to examples/extra/pcsc_demo.fun diff --git a/examples/external/pcsc/pcsc_example.fun b/examples/extra/pcsc_example.fun similarity index 100% rename from examples/external/pcsc/pcsc_example.fun rename to examples/extra/pcsc_example.fun diff --git a/examples/external/xml2/xml_access_catalog.fun b/examples/extra/xml_access_catalog.fun similarity index 100% rename from examples/external/xml2/xml_access_catalog.fun rename to examples/extra/xml_access_catalog.fun diff --git a/examples/external/xml2/xml_access_employees.fun b/examples/extra/xml_access_employees.fun similarity index 100% rename from examples/external/xml2/xml_access_employees.fun rename to examples/extra/xml_access_employees.fun diff --git a/examples/external/xml2/xml_access_ns.fun b/examples/extra/xml_access_ns.fun similarity index 100% rename from examples/external/xml2/xml_access_ns.fun rename to examples/extra/xml_access_ns.fun diff --git a/examples/external/xml2/xml_class_example.fun b/examples/extra/xml_class_example.fun similarity index 100% rename from examples/external/xml2/xml_class_example.fun rename to examples/extra/xml_class_example.fun diff --git a/examples/external/xml2/xml_minimal.fun b/examples/extra/xml_minimal.fun similarity index 100% rename from examples/external/xml2/xml_minimal.fun rename to examples/extra/xml_minimal.fun diff --git a/examples/functions/nested_functions.fun b/examples/functions/nested_functions.fun deleted file mode 100755 index 7d2da30..0000000 --- a/examples/functions/nested_functions.fun +++ /dev/null @@ -1,67 +0,0 @@ -#!/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-04-02 - */ - -/* - * Demonstrates nested functions that are only visible inside the - * outer function where they are defined. This example avoids - * capturing outer variables (closures) and instead passes values - * explicitly, which works with the current implementation. - */ - -print("=== Nested functions (local to outer function) ===") - -// Outer function defines two inner helpers that are only usable inside outer -fun outer(a, b) - // Defined inside outer; not visible as a global symbol - fun times3(x) - return x * 3 - - // Another local helper; also only visible within outer - fun sum_plus1(x, y) - return x + y + 1 - - // Use the local helpers - t1 = times3(a) - t2 = times3(b) - return sum_plus1(t1, t2) - -print("outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22") -print(outer(2, 5)) - -print("") -print("=== Deeper nesting without variable capture ===") - -// Demonstrates function-inside-function-inside-function. -// Each level avoids referencing outer locals directly; values -// are threaded through parameters instead. -fun demo_deep(n) - fun one(x) - fun two(y) - fun three(z) - return z + 1 - return three(y) + 1 - return two(x) + 1 - return one(n) - -print("demo_deep(4) -> expected 7") -print(demo_deep(4)) - -/* Expected output: -=== Nested functions (local to outer function) === -outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22 -22 - -=== Deeper nesting without variable capture === -demo_deep(4) -> expected 7 -7 -*/ diff --git a/examples/functions/nested_no_capture.fun b/examples/functions/nested_no_capture.fun deleted file mode 100755 index 5820340..0000000 --- a/examples/functions/nested_no_capture.fun +++ /dev/null @@ -1,41 +0,0 @@ -#!/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-04-02 - */ - -/* - * Demonstrates nested functions that are only visible inside the - * outer function where they are defined. This example avoids - * capturing outer variables (closures) and instead passes values - * explicitly, which works with the current implementation. - */ - -print("=== Nested (no captures), explicit parameters ===") - -fun outer(a, b) - fun mul3(x) - return x * 3 - - fun combine(x, y) - return x + y + 1 - - ax = mul3(a) - by = mul3(b) - return combine(ax, by) - -print("outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22") -print(outer(2, 5)) - -/* Expected output: -=== Nested (no captures), explicit parameters === -outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22 -22 -*/ diff --git a/examples/functions/functions_test.fun b/examples/functions_test.fun similarity index 100% rename from examples/functions/functions_test.fun rename to examples/functions_test.fun diff --git a/examples/io/word_count.fun b/examples/io/word_count.fun index 3445239..96f9810 100755 --- a/examples/io/word_count.fun +++ b/examples/io/word_count.fun @@ -320,6 +320,9 @@ using: 1 cards)](./docs/external/pcsc.md): 1 [openssl](./docs/external/openssl.md): 1 [sqlite](./docs/external/sqlite.md): 1 +[tk: 1 +(tcl/tk: 1 +gui)](./docs/external/tcltk.md): 1 [xml: 1 (libxml2)](./docs/external/xml2.md): 1 =: 2 diff --git a/examples/rust/rust_hello.fun b/examples/rust_hello.fun similarity index 100% rename from examples/rust/rust_hello.fun rename to examples/rust_hello.fun diff --git a/examples/rust/rust_hello_args.fun b/examples/rust_hello_args.fun similarity index 100% rename from examples/rust/rust_hello_args.fun rename to examples/rust_hello_args.fun diff --git a/examples/rust/rust_hello_args_return.fun b/examples/rust_hello_args_return.fun similarity index 100% rename from examples/rust/rust_hello_args_return.fun rename to examples/rust_hello_args_return.fun diff --git a/examples/snippets/nested.fun b/examples/snippets/nested.fun deleted file mode 100644 index c31aa47..0000000 --- a/examples/snippets/nested.fun +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env fun - -/* - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Demonstrates nested functions that are only visible inside the - * outer function where they are defined. This example avoids - * capturing outer variables (closures) and instead passes values - * explicitly, which works with the current implementation. - * - * Added: 2026-04-02 - */ - -fun outer(x) - fun inner(y) - return y * 2 - return inner(x) + inner(3) - -print(outer(5)) diff --git a/examples/snippets/parse_test2.fun b/examples/snippets/parse_test2.fun new file mode 100755 index 0000000..248a940 --- /dev/null +++ b/examples/snippets/parse_test2.fun @@ -0,0 +1,12 @@ +#!/usr/bin/env fun + +#include + +cgi = CGI() +pairs = cgi._parse_urlencoded("a=1&b=2&c=3") +i = 0 +n = len(pairs) +while (i < n) + p = pairs[i] + print(to_string(p[0]) + "=" + to_string(p[1])) + i = i + 1 diff --git a/examples/snippets/test_parse.fun b/examples/snippets/test_parse.fun index 4772b4b..248a940 100755 --- a/examples/snippets/test_parse.fun +++ b/examples/snippets/test_parse.fun @@ -1,17 +1,5 @@ #!/usr/bin/env fun -/* - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Demonstrates nested functions that are only visible inside the - * outer function where they are defined. This example avoids - * capturing outer variables (closures) and instead passes values - * explicitly, which works with the current implementation. - * - * Added: 2026-04-02 - */ - #include cgi = CGI() diff --git a/examples/external/sqlite/sqlited/README.md b/examples/sqlited/README.md similarity index 100% rename from examples/external/sqlite/sqlited/README.md rename to examples/sqlited/README.md diff --git a/examples/external/sqlite/sqlited/client.fun b/examples/sqlited/client.fun similarity index 100% rename from examples/external/sqlite/sqlited/client.fun rename to examples/sqlited/client.fun diff --git a/examples/external/sqlite/sqlited/protocol.md b/examples/sqlited/protocol.md similarity index 100% rename from examples/external/sqlite/sqlited/protocol.md rename to examples/sqlited/protocol.md diff --git a/examples/external/sqlite/sqlited/server.fun b/examples/sqlited/server.fun similarity index 100% rename from examples/external/sqlite/sqlited/server.fun rename to examples/sqlited/server.fun diff --git a/lib/ui/notcurses.fun b/lib/ui/notcurses.fun new file mode 100644 index 0000000..e4b352c --- /dev/null +++ b/lib/ui/notcurses.fun @@ -0,0 +1,172 @@ +#!/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-03 + */ + +// Notcurses stdlib abstraction wrapping VM NC_* builtins. +// Provides a tiny OO-style helper around nc_init/nc_clear/nc_draw_text/nc_getch/nc_shutdown. +// If Fun was built without Notcurses support, init() returns 0 and methods are no-ops. + +class Notcurses() + // Initialize Notcurses core. Returns 1 on success, 0 on failure. + fun init(this) + return nc_init() + + // Clear the screen and render. Returns 0 on success, -1 on error. + fun clear(this) + return nc_clear() + + // Draw text at y, x and render. Returns 0 on success, -1 on error. + fun draw_text(this, y, x, text) + // allow flexible types + return nc_draw_text(to_number(y), to_number(x), to_string(text)) + + // Get a Unicode codepoint (int). timeout_ms<=0 blocks; >0 returns -1 if no key ready. + fun getch(this, timeout_ms) + if timeout_ms == nil + timeout_ms = 0 + return nc_getch(to_number(timeout_ms)) + + // Shut down Notcurses, restoring the terminal. + fun shutdown(this) + return nc_shutdown() + + // (banner helper removed temporarily due to parser limitations) + + // --- Extended primitives --- + fun size(this) + return nc_get_size() + + fun render(this) + return nc_render() + + // fg/bg are 0xRRGGBB, style bitmask: bold(1), dim(2), italic(4), underline(8), strike(16), reverse(32) + fun set_style(this, fg, bg, style) + // parser order for builtin is (style,bg,fg) + return nc_set_style(to_number(style), to_number(bg), to_number(fg)) + + fun draw_char(this, y, x, ch) + return nc_draw_char(to_number(y), to_number(x), to_number(ch)) + + fun hline(this, y, x, len, ch) + return nc_draw_hline(to_number(y), to_number(x), to_number(len), to_number(ch)) + + fun vline(this, y, x, len, ch) + return nc_draw_vline(to_number(y), to_number(x), to_number(len), to_number(ch)) + + fun box(this, y, x, h, w, style) + // builtin expects (x,y,w,h,style) + return nc_box(to_number(x), to_number(y), to_number(w), to_number(h), to_number(style)) + + fun fill(this, y, x, h, w, ch) + // builtin expects (x,y,w,h,ch) + return nc_fill(to_number(x), to_number(y), to_number(w), to_number(h), to_number(ch)) + + // --- Widgets as methods --- + // Status bar on the last row + fun status_bar(this, text, style) + sz = this.size() + if style == nil + style = 32 + rc = this.set_style(0x000000, 0xFFFFFF, style) + rc = this.fill(sz[0] - 1, 0, 1, sz[1], 32) + rc = this.draw_text(sz[0] - 1, 1, text) + rc = this.render() + return 0 + + // Draw styled label + fun label(this, y, x, text, style) + if style != nil + rc = this.set_style(0xFFFFFF, 0x000000, style) + rc = this.draw_text(y, x, text) + rc = this.render() + return 0 + + // Panel with optional title + fun box_panel(this, y, x, h, w, title, style) + if style == nil + style = 0 + rc = this.box(y, x, h, w, style) + if title != nil + rc = this.draw_text(y, x + 2, to_string(title)) + rc = this.render() + return 0 + + // Progress bar (frac in 0..1) + fun progress(this, y, x, w, frac, style) + if style == nil + style = 0 + if frac < 0 + frac = 0 + if frac > 1 + frac = 1 + // frame and title + rc = nc_box(to_number(x - 2), to_number(y - 1), to_number(w + 4), 3, to_number(style)) + // background in dim gray + rc = this.set_style(0x606060, 0x000000, 2) + rc = nc_fill(to_number(y), to_number(x), to_number(w), 1, 46) // '.' + // filled portion in bold green + rc = this.set_style(0x00FF00, 0x000000, 1) + fw = to_number(frac) * w + if fw < 0 + fw = 0 + if fw > w + fw = w + if fw > 0 + rc = nc_fill(to_number(y), to_number(x), to_number(fw), 1, 35) // '#' + rc = nc_draw_text(to_number(y - 1), to_number(x), "Progress") + rc = nc_render() + return 0 + + // Simple vertical menu, highlight selected index + fun menu(this, y, x, items, selected_idx, w, style, sel_style) + if style == nil + style = 0 + if sel_style == nil + sel_style = 32 + i = 0 + while i < len(items) + if i == selected_idx + rc = this.set_style(0x000000, 0xFFFFFF, sel_style) + else + rc = this.set_style(0xFFFFFF, 0x000000, style) + if w != nil + if len(to_string(items[i])) > w + rc = this.draw_text(y + i, x, substr(to_string(items[i]), 0, w)) + else + rc = this.draw_text(y + i, x, to_string(items[i])) + else + rc = this.draw_text(y + i, x, to_string(items[i])) + i = i + 1 + rc = this.render() + return 0 + + // Input line echo with cursor + fun input_line(this, y, x, prompt, buffer, cursor_idx, w, style) + if style == nil + style = 0 + if w == nil + w = len(buffer) + len(prompt) + 2 + rc = this.set_style(0xFFFFFF, 0x000000, style) + rc = this.draw_text(y, x, to_string(prompt)) + rc = this.fill(y, x + len(prompt), 1, w, 32) + rc = this.draw_text(y, x + len(prompt), to_string(buffer)) + this.set_style(0x000000, 0xFFFFFF, 32) + ci = cursor_idx + if ci < 0 + ci = 0 + if ci > len(buffer) + ci = len(buffer) + rc = this.draw_char(y, x + len(prompt) + ci, 32) + rc = this.render() + return 0 + +// --- End of class Notcurses --- diff --git a/make b/make index 3c49fac..a11600c 100755 --- a/make +++ b/make @@ -22,26 +22,33 @@ if [ "$target" = "all" ]; then && cmake -S . -B build \ -DFUN_WITH_PCSC=ON \ -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ -DFUN_WITH_SQLITE=ON \ -DFUN_WITH_CURL=ON \ -DFUN_WITH_PCRE2=ON \ -DFUN_WITH_XML2=ON \ -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ -DFUN_WITH_INI=ON \ + -DFUN_WITH_NOTCURSES=ON \ -DFUN_WITH_CPP=ON \ -DFUN_WITH_OPENSSL=ON \ + -DFUN_WITH_LIBRESSL=ON \ && cmake --build build --target fun elif [ "$target" = "all_debug" ]; then rm -rf build \ && cmake -S . -B build \ -DFUN_WITH_PCSC=ON \ -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ -DFUN_WITH_SQLITE=ON \ -DFUN_WITH_CURL=ON \ -DFUN_WITH_PCRE2=ON \ -DFUN_WITH_XML2=ON \ -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ -DFUN_WITH_INI=ON \ + -DFUN_WITH_NOTCURSES=ON \ -DFUN_WITH_CPP=ON \ -DFUN_WITH_OPENSSL=ON \ -DFUN_DEBUG=ON \ @@ -50,6 +57,7 @@ elif [ "$target" = "alpine" ]; then rm -rf build \ && cmake -S . -B build \ -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ -DFUN_WITH_SQLITE=ON \ -DFUN_WITH_CURL=ON \ -DFUN_WITH_PCRE2=ON \ @@ -64,12 +72,15 @@ elif [ "$target" = "cpp_all" ]; then && cmake -S . -B build \ -DFUN_WITH_PCSC=ON \ -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ -DFUN_WITH_SQLITE=ON \ -DFUN_WITH_CURL=ON \ -DFUN_WITH_PCRE2=ON \ -DFUN_WITH_XML2=ON \ -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ -DFUN_WITH_INI=ON \ + -DFUN_WITH_NOTCURSES=ON \ -DFUN_WITH_CPP=ON \ -DFUN_WITH_OPENSSL=ON \ && cmake --build build --target fun @@ -117,11 +128,13 @@ elif [ "$target" = "rust_all" ]; then && cmake -S . -B build \ -DFUN_WITH_PCSC=ON \ -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ -DFUN_WITH_SQLITE=ON \ -DFUN_WITH_CURL=ON \ -DFUN_WITH_PCRE2=ON \ -DFUN_WITH_XML2=ON \ -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ -DFUN_WITH_INI=ON \ -DFUN_WITH_RUST=ON \ -DFUN_WITH_CPP=ON \ diff --git a/scripts/run_examples.sh b/scripts/run_examples.sh index cc38060..f6fced6 100755 --- a/scripts/run_examples.sh +++ b/scripts/run_examples.sh @@ -19,8 +19,9 @@ pick_bin() { local cands=( "$ROOT/fun" "$ROOT/build/fun" - "$ROOT/build_debug/fun" - "$ROOT/build_release/fun" + "$ROOT/build/bin/fun" + "$ROOT/cmake-build-debug/fun" + "$ROOT/cmake-build-release/fun" ) for p in "${cands[@]}"; do [[ -x "$p" ]] && { echo "$p"; return 0; } diff --git a/src/bytecode.c b/src/bytecode.c index dfbd7b5..7173f91 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -257,6 +257,14 @@ static const char *opcode_name(OpCode op) { return "SQLITE_EXEC"; case OP_SQLITE_QUERY: return "SQLITE_QUERY"; + case OP_LIBSQL_OPEN: + return "LIBSQL_OPEN"; + case OP_LIBSQL_CLOSE: + return "LIBSQL_CLOSE"; + case OP_LIBSQL_EXEC: + return "LIBSQL_EXEC"; + case OP_LIBSQL_QUERY: + return "LIBSQL_QUERY"; case OP_PCSC_ESTABLISH: return "PCSC_ESTABLISH"; case OP_PCSC_RELEASE: @@ -283,6 +291,14 @@ static const char *opcode_name(OpCode op) { return "OPENSSL_SHA512"; case OP_OPENSSL_RIPEMD160: return "OPENSSL_RIPEMD160"; + case OP_LIBRESSL_MD5: + return "LIBRESSL_MD5"; + case OP_LIBRESSL_SHA256: + return "LIBRESSL_SHA256"; + case OP_LIBRESSL_SHA512: + return "LIBRESSL_SHA512"; + case OP_LIBRESSL_RIPEMD160: + return "LIBRESSL_RIPEMD160"; case OP_INI_LOAD: return "INI_LOAD"; case OP_INI_FREE: diff --git a/src/bytecode.h b/src/bytecode.h index 0171a22..cacf59f 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -162,6 +162,11 @@ typedef enum { OP_SQLITE_EXEC, // pops sql, handle; pushes sqlite rc (0=OK) OP_SQLITE_QUERY, // pops sql, handle; pushes array + // libsql (optional, independent) + OP_LIBSQL_OPEN, // pops url/path; pushes handle (>0) or 0 + OP_LIBSQL_CLOSE, // pops handle; pushes Nil + OP_LIBSQL_EXEC, // pops sql, handle; pushes rc (0=OK) + OP_LIBSQL_QUERY, // pops sql, handle; pushes array // PCSC (smart card) opcodes OP_PCSC_ESTABLISH, // returns context id (>0) or 0 @@ -182,6 +187,11 @@ typedef enum { OP_OPENSSL_SHA512, // pops data string; pushes sha512 hex string OP_OPENSSL_RIPEMD160, // pops data string; pushes ripemd160 hex string + // LibreSSL (optional; same API as OpenSSL but different toggle) + OP_LIBRESSL_MD5, // pops data string; pushes md5 hex string + OP_LIBRESSL_SHA256, // pops data string; pushes sha256 hex string + OP_LIBRESSL_SHA512, // pops data string; pushes sha512 hex string + OP_LIBRESSL_RIPEMD160, // pops data string; pushes ripemd160 hex string // INI (iniparser 4.2.6) optional OP_INI_LOAD, // pops path; pushes handle (>0) or 0 diff --git a/src/parser.c b/src/parser.c index af40c75..3b1cae2 100644 --- a/src/parser.c +++ b/src/parser.c @@ -54,11 +54,6 @@ /* from parser_utils.c */ extern char *preprocess_includes_with_path(const char *src, const char *current_path); -/* Forward declarations for helpers used before their definitions */ -static void skip_to_eol(const char *src, size_t len, size_t *pos); -static int read_line_start(const char *src, size_t len, size_t *pos, int *out_indent); -static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, int current_indent); - /* ---- parser error state ---- */ static const char *g_current_source_path = NULL; /* for propagating filename into nested bytecodes */ static int g_has_error = 0; @@ -224,22 +219,6 @@ typedef struct { static LocalEnv *g_locals = NULL; -/* --- nested function environment tracking (for no-capture enforcement) --- */ -static LocalEnv *g_func_env_stack[64]; -static int g_func_env_depth = 0; /* number of valid outer env entries */ - -static int name_in_outer_envs(const char *name) { - if (g_func_env_depth <= 0) return 0; - for (int d = g_func_env_depth - 1; d >= 0; --d) { - LocalEnv *e = g_func_env_stack[d]; - if (!e) continue; - for (int i = 0; i < e->count; ++i) { - if (e->names[i] && strcmp(e->names[i], name) == 0) return 1; - } - } - return 0; -} - /* loop context for break/continue patching */ typedef struct LoopCtx { int break_jumps[64]; @@ -277,94 +256,6 @@ static int emit_expression(Bytecode *bc, const char *src, size_t len, size_t *po static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) { skip_spaces(src, len, pos); - /* anonymous function literal: fn(arg, ...) indented-body end */ - if (*pos + 2 < len && starts_with(src, len, *pos, "fn") && (src[*pos + 2] == '(' || src[*pos + 2] == ' ' || src[*pos + 2] == '\t')) { - *pos += 2; - skip_spaces(src, len, pos); - if (!consume_char(src, len, pos, '(')) { - parser_fail(*pos, "Expected '(' after 'fn'"); - return 0; - } - - /* build locals from parameters */ - LocalEnv env = {{0}, {0}, 0}; - LocalEnv *prev = g_locals; - int saved_depth = g_func_env_depth; - /* push outer env for no-capture checks */ - if (prev != NULL) { - if (g_func_env_depth >= (int)(sizeof(g_func_env_stack) / sizeof(g_func_env_stack[0]))) { - parser_fail(*pos, "Too many nested functions (env stack overflow)"); - return 0; - } - g_func_env_stack[g_func_env_depth++] = prev; - } - g_locals = &env; - - skip_spaces(src, len, pos); - if (*pos < len && src[*pos] != ')') { - for (;;) { - char *pname = NULL; - if (!read_identifier_into(src, len, pos, &pname)) { - parser_fail(*pos, "Expected parameter name"); - g_locals = prev; - g_func_env_depth = saved_depth; - return 0; - } - if (local_find(pname) >= 0) { - parser_fail(*pos, "Duplicate parameter name '%s'", pname); - free(pname); - g_locals = prev; - g_func_env_depth = saved_depth; - return 0; - } - local_add(pname); - free(pname); - skip_spaces(src, len, pos); - if (*pos < len && src[*pos] == ',') { - (*pos)++; - skip_spaces(src, len, pos); - continue; - } - break; - } - } - if (!consume_char(src, len, pos, ')')) { - parser_fail(*pos, "Expected ')' after parameter list"); - g_locals = prev; - g_func_env_depth = saved_depth; - return 0; - } - /* end header line */ - skip_to_eol(src, len, pos); - - /* compile body into separate Bytecode */ - Bytecode *fn_bc = bytecode_new(); - if (fn_bc) { - if (fn_bc->name) free((void *)fn_bc->name); - fn_bc->name = strdup(""); - if (fn_bc->source_file) free((void *)fn_bc->source_file); - if (g_current_source_path) fn_bc->source_file = strdup(g_current_source_path); - } - - /* parse body at increased indent if present */ - int body_indent = 0; - size_t look_body = *pos; - if (read_line_start(src, len, &look_body, &body_indent) && body_indent > 0) { - parse_block(fn_bc, src, len, pos, body_indent); - } else { - /* empty body ok */ - } - bytecode_add_instruction(fn_bc, OP_RETURN, 0); - - /* restore env stack */ - g_locals = prev; - g_func_env_depth = saved_depth; - - int fci = bytecode_add_constant(bc, make_function(fn_bc)); - bytecode_add_instruction(bc, OP_LOAD_CONST, fci); - return 1; - } - /* parenthesized */ if (*pos < len && src[*pos] == '(') { (*pos)++; /* '(' */ @@ -2093,6 +1984,91 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* libsql builtins (independent extension) */ + if (strcmp(name, "libsql_open") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libsql_open expects (url_or_path)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libsql_open arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBSQL_OPEN, 0); + free(name); + return 1; + } + if (strcmp(name, "libsql_close") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libsql_close expects (handle)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libsql_close arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBSQL_CLOSE, 0); + free(name); + return 1; + } + if (strcmp(name, "libsql_exec") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libsql_exec expects (handle, sql)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "libsql_exec expects (handle, sql)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libsql_exec expects (handle, sql)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libsql_exec args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBSQL_EXEC, 0); + free(name); + return 1; + } + if (strcmp(name, "libsql_query") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libsql_query expects (handle, sql)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "libsql_query expects (handle, sql)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libsql_query expects (handle, sql)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libsql_query args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBSQL_QUERY, 0); + free(name); + return 1; + } if (strcmp(name, "curl_post") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { @@ -2210,7 +2186,71 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } - /* LibreSSL builtins removed */ + /* LibreSSL (md5/sha256/sha512/ripemd160) */ + if (strcmp(name, "libressl_md5") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libressl_md5 expects (data)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libressl_md5 arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBRESSL_MD5, 0); + free(name); + return 1; + } + if (strcmp(name, "libressl_sha256") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libressl_sha256 expects (data)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libressl_sha256 arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBRESSL_SHA256, 0); + free(name); + return 1; + } + if (strcmp(name, "libressl_sha512") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libressl_sha512 expects (data)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libressl_sha512 arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBRESSL_SHA512, 0); + free(name); + return 1; + } + if (strcmp(name, "libressl_ripemd160") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "libressl_ripemd160 expects (data)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after libressl_ripemd160 arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_LIBRESSL_RIPEMD160, 0); + free(name); + return 1; + } /* PCSC builtins */ if (strcmp(name, "pcsc_establish") == 0) { (*pos)++; /* '(' */ @@ -4346,12 +4386,6 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) } /* push function value first */ - if (local_idx < 0 && name_in_outer_envs(name)) { - const char *fname = (bc && bc->name) ? bc->name : ""; - parser_fail(*pos, "Nested function '%s' cannot access outer local '%s'. Pass it as a parameter instead.", fname, name); - free(name); - return 0; - } if (local_idx >= 0) { bytecode_add_instruction(bc, OP_LOAD_LOCAL, local_idx); } else { @@ -4507,12 +4541,6 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } else { - if (local_idx < 0 && name_in_outer_envs(name)) { - const char *fname = (bc && bc->name) ? bc->name : ""; - parser_fail(*pos, "Nested function '%s' cannot access outer local '%s'. Pass it as a parameter instead.", fname, name); - free(name); - return 0; - } if (local_idx >= 0) { bytecode_add_instruction(bc, OP_LOAD_LOCAL, local_idx); } else { @@ -6593,17 +6621,8 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, } /* build locals from parameters */ - LocalEnv env = {{0}, {0}, 0}; + LocalEnv env = {{0}, 0}; LocalEnv *prev = g_locals; - int saved_depth = g_func_env_depth; - if (prev != NULL) { - if (g_func_env_depth >= (int)(sizeof(g_func_env_stack) / sizeof(g_func_env_stack[0]))) { - parser_fail(*pos, "Too many nested functions (env stack overflow)"); - free(fname); - return; - } - g_func_env_stack[g_func_env_depth++] = prev; - } g_locals = &env; skip_spaces(src, len, pos); @@ -6673,46 +6692,12 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, } #endif - /* If we are inside another function (prev != NULL), bind this function as a local - * so that it is only visible within the current function scope. Otherwise, bind - * it as a global (top-level behavior unchanged). */ + /* bind function to global: LOAD_CONST ; STORE_GLOBAL fgi */ int fci = bytecode_add_constant(bc, make_function(fn_bc)); bytecode_add_instruction(bc, OP_LOAD_CONST, fci); - if (prev != NULL) { - /* Bind into the OUTER function's local env (prev), not the inner temp env. */ - LocalEnv *save_env = g_locals; - g_locals = prev; - /* declare a local with the function name if not present */ - int lidx = local_find(fname); - if (lidx < 0) { - lidx = local_add(fname); - } - if (lidx < 0) { - /* failed to allocate local slot */ - g_locals = save_env; /* restore before returning */ - g_locals = prev; - free(fname); - return; - } - /* We want sibling inner functions to be able to call this nested function - * without closures. Strategy: also expose it as a global symbol so that - * inner functions (compiled into separate frames) can resolve the name - * via global lookup. Keep the local binding for the outer function body. */ - /* Duplicate the function value so we can store to both local and global. */ - bytecode_add_instruction(bc, OP_DUP, 0); - bytecode_add_instruction(bc, OP_STORE_LOCAL, lidx); - /* Bind a global under the same name for call resolution inside deeper nested functions. */ - int gsym = sym_index(fname); - bytecode_add_instruction(bc, OP_STORE_GLOBAL, gsym); - /* restore current env (will be reset to prev below) */ - g_locals = save_env; - } else { - /* top-level: global binding (backward compatible) */ - bytecode_add_instruction(bc, OP_STORE_GLOBAL, fgi); - } + bytecode_add_instruction(bc, OP_STORE_GLOBAL, fgi); g_locals = prev; - g_func_env_depth = saved_depth; free(fname); continue; } diff --git a/src/vm.c b/src/vm.c index de841e0..51468ce 100644 --- a/src/vm.c +++ b/src/vm.c @@ -53,7 +53,12 @@ #include "value.h" #include "vm.h" -/* Notcurses support removed */ +/* Shared Notcurses state (optional) */ +#ifdef FUN_WITH_NOTCURSES +#include +#include /* ensure wcwidth/wcswidth prototypes present before notcurses.h on some systems */ +#endif +#include "vm/notcurses/common.h" // Optional by extensions commonly used code. #ifdef's are in each single file. #include "external/curl.c" @@ -65,10 +70,13 @@ /* Note: INI opcode handlers are included below; changes in vm/ini/ .c files * require vm.c to rebuild. */ #include "external/json.c" +#include "external/libressl.c" +#include "external/libsql.c" #include "external/openssl.c" #include "external/pcre2.c" #include "external/pcsc.c" #include "external/sqlite.c" +#include "external/tcltk.c" #include "external/xml2.c" /* forward declarations for include mapping used in error reporting */ @@ -843,10 +851,42 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/openssl/sha512.c" #endif +/* LibreSSL ops (md5/sha256/sha512/ripemd160) */ +#ifdef FUN_WITH_LIBRESSL +#include "vm/libressl/md5.c" +#include "vm/libressl/ripemd160.c" +#include "vm/libressl/sha256.c" +#include "vm/libressl/sha512.c" +#endif -/* Tcl/Tk support removed */ +/* Tk (Tcl/Tk) ops */ +#ifdef FUN_WITH_TCLTK +#include "vm/tk/bind.c" +#include "vm/tk/button.c" +#include "vm/tk/eval.c" +#include "vm/tk/label.c" +#include "vm/tk/loop.c" +#include "vm/tk/pack.c" +#include "vm/tk/result.c" +#include "vm/tk/wm_title.c" +#endif -/* Notcurses TUI ops removed */ +/* Notcurses TUI ops (optional) */ +#ifdef FUN_WITH_NOTCURSES +#include "vm/notcurses/clear.c" +#include "vm/notcurses/draw_text.c" +#include "vm/notcurses/getch.c" +#include "vm/notcurses/init.c" +#include "vm/notcurses/shutdown.c" +#include "vm/notcurses/get_size.c" +#include "vm/notcurses/set_style.c" +#include "vm/notcurses/draw_char.c" +#include "vm/notcurses/hline.c" +#include "vm/notcurses/vline.c" +#include "vm/notcurses/box.c" +#include "vm/notcurses/fill.c" +#include "vm/notcurses/render.c" +#endif /* SQLite ops */ #ifdef FUN_WITH_SQLITE @@ -872,7 +912,13 @@ void vm_run(VM *vm, Bytecode *entry) { } #endif -/* libsql support removed */ +/* libsql ops (independent) */ +#ifdef FUN_WITH_LIBSQL +#include "vm/libsql/close.c" +#include "vm/libsql/exec.c" +#include "vm/libsql/open.c" +#include "vm/libsql/query.c" +#endif /* PCRE2 ops */ #ifdef FUN_WITH_PCRE2