diff --git a/.github/workflows/cmake-ubuntu-latest.yml b/.github/workflows/cmake-ubuntu-latest.yml index fc55f0f..6f62ae4 100644 --- a/.github/workflows/cmake-ubuntu-latest.yml +++ b/.github/workflows/cmake-ubuntu-latest.yml @@ -6,6 +6,7 @@ on: env: BUILD_TYPE: Release + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build: @@ -13,7 +14,6 @@ jobs: defaults: run: working-directory: ${{github.workspace}} - steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index cf923d8..73e7471 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,9 @@ Doxygen.in !.gitignore !.github !.gitkeep -!api/.gitkeep .venv api/ +!api/.gitkeep build/* build_debug/* build_release/* @@ -23,6 +23,8 @@ downloaded.png json.xml lib/*.so out/ +playground/* +!playground/.gitkeep src/*.o src/rust/Cargo.lock src/rust/target diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b2424..e8def14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (during 0.x.y development phase). +## [0.42.1] - 2026-06-08 +### Fixed +- Small test script issue. +### Changed +- Documentation and website updates. No runtime code changes. +- README, `make`, and `Doxyfile` updates. No runtime code changes. + +## [0.42.0] - 2026-06-03 +### Added +- Redis/Valkey extension named `redis` (see `src/extensions/redis.c` and `src/vm/redis/*`). +- Redis example programs: `basic_ping.fun`, `hash_ops.fun`, `kv_set_get.fun`, `list_ops.fun`, and `redis_test.fun`. +- Initial Redis documentation page under `web/documentation/extensions/redis/`. +### Changed +- Build system: Redis extension is not compiled by default; enable it explicitly if needed. No other code changes. +- GitHub workflow tweaks. No code changes. + +## [0.41.16] - 2026-05-29 +### Added +- `playground/` directory for local-only experiments. +### Changed +- `examples/features.fun` and website features page updates. +- README and API documentation updates. No code changes. +- Documentation and web fixes. No code changes. + ## [0.41.15] - 2026-05-26 ### Changed - More internal optimizations. diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c754fc..dc0523d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.41.15 LANGUAGES C) +project(fun VERSION 0.42.2 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -521,7 +521,7 @@ install(DIRECTORY lib/ # Optionally install example scripts option(FUN_INSTALL_EXAMPLES "Install example .fun scripts" ON) if(FUN_INSTALL_EXAMPLES) - install(DIRECTORY examples/ + install(DIRECTORY examples/ DESTINATION /usr/share/fun/examples FILES_MATCHING PATTERN "*.fun" ) diff --git a/Doxyfile b/Doxyfile index 39f0078..95ef2de 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "Fun API Documentation" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 0.41.15 +PROJECT_NUMBER = 0.42.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewers a diff --git a/README.md b/README.md index 3690522..1511d85 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ ## 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 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. -Fun is an experiment, just for fun, but Fun works! +Fun is dynamically typed with optional static type annotations, featuring first-class functions, classes with inheritance, pattern matching, and a rich standard library. It supports everything from basic scripting to TCP sockets, serial communication, threading, cryptography (MD5, SHA-1/256/384/512, CRC-32, AES-256), and a built-in debugger. -Fun is a highly strict programming language, but also highly simple. It looks like Python (My favorite language), but there are differences. +Fun is an experiment — just for fun — but Fun works! -Influenced by **[Bash](https://www.gnu.org/software/bash/)**, **[C](https://en.wikipedia.org/wiki/The_C_Programming_Language)**, **[Lua](https://www.lua.org/)**, PHP, **[Python](https://www.python.org/)**, and Rust (Most influences came from linked languages). +Influenced by **[Bash](https://www.gnu.org/software/bash/)**, **[C](https://en.wikipedia.org/wiki/The_C_Programming_Language)**, **[Lua](https://www.lua.org/)**, PHP, **[Python](https://www.python.org/)**, and **[Rust](https://www.rust-lang.org/)**. Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](https://opensource.org/license/apache-2-0). @@ -23,11 +23,19 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht ## Characteristics -- Dynamic and optionally statically typed -- Type safety -- Written in C (C99) and Fun -- Internal libs are written with no_camel_case even when written in Fun, except class names -- Only a minimal function set is written in C, and most other core functions and libraries are implemented in Fun +- **Dynamic typing** with optional **static type annotations** (`number`, `string`, `boolean`, `float`, `byte`, `uint8`–`uint64`, `int8`–`int64`) +- **Stack-based bytecode VM** written in C99 with ~220 opcodes +- **First-class functions**, anonymous functions (`fn`), and higher-order operations (`map`, `filter`, `reduce`) +- **Classes** with constructors, methods, and single inheritance +- **Exception handling** with `try`/`catch`/`finally` +- **Built-in data structures**: arrays (with slicing), maps/objects, strings +- **Concurrency**: threads (`thread_spawn`, `thread_join`) and cooperative async scheduler +- **Networking**: TCP and Unix domain sockets with non-blocking I/O polling +- **Serial communication**: full termios-based serial port control +- **Cryptography**: pure-Fun implementations of MD5, SHA-1/256/384/512, CRC-32/CRC-32C, AES-256 ECB +- **Built-in debugger** with breakpoints, step/next/finish, and stack inspection +- **Minimal C core** — most standard libraries are implemented in Fun itself +- **Internal style**: `snake_case` for functions and variables, `CamelCase` for class names ## The Fun Manifesto @@ -45,13 +53,13 @@ Coding should be enjoyable, elegant, and consistent. - **One Way to Do It**
No clutter, no 15 ways of writing the same thing. Simplicity means clarity. - **Hackable by Nature**
- Fun should be small and embeddable, like Lua. Easy to understand, extend, and tinker with — true to the hacker spirit. + Fun should be small and embeddable, like Lua. Easy to understand, extend, and tinker with — true to the hacker spirit. - **Beautiful Defaults**
- A language that doesn’t need linters, formatters, or style guides. Beauty is built in. + A language that doesn't need linters, formatters, or style guides. Beauty is built in. ## The Community -Fun is not about being the fastest or the most feature-rich. It’s about sharing joy in coding. The community should be: +Fun is not about being the fastest or the most feature-rich. It's about sharing joy in coding. The community should be: - Respectful - Curious @@ -70,63 +78,162 @@ A language that feels like home for developers who: - Believe consistency is freedom - Want to write code that looks good and feels good -Fun may not change the world — but it will make programming a little more fun. +Fun may not change the world — but it will make programming a little more fun. -## Features +## Language Features -### Core +### Core Syntax & Types -- functions/classes/objects -- if/else if/else -- try/catch/finally +- Indentation-based blocks (2-space), line and block comments +- Static type annotations with automatic runtime range clamping for fixed-width integers +- `typeof()`, `to_string()`, `to_number()`, `cast()` — type introspection and conversion +- `exit` statement with optional exit code, `#include` for source includes -And much more...! Look at the specs in [Spec](./spec/) for more detailed information. +### Operators -### Lib (./lib/) +- Arithmetic: `+`, `-`, `*`, `/`, `%` +- Comparison: `<`, `<=`, `>`, `>=`, `==`, `!=` +- Logical: `&&`, `||`, `!` (short-circuit) +- Bitwise: `band()`, `bor()`, `bxor()`, `bnot()`, `shl()`, `shr()`, `rol()`, `ror()` +- Ternary: `condition ? true_expr : false_expr` -See [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) for what the standard library provides. +### Control Flow -### Optional extensions (build-time selectable / only testing this on Linux actually): +- `if` / `else if` / `else` +- `while` with `break` and `continue` +- `for var in array` — iteration +- `for var in range(start, end)` — numeric range +- `for (key, value) in map` — map destructuring +- `match` expression (stdlib) +- `try` / `catch` / `finally` -- [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) — see [docs](./web/documentation/extensions/kcgi/kcgi.md) ☑ -- [cURL (libcurl)](./web/documentation/extensions/curl/curl.md) (optional) ☑ -- [INI (iniparser)](./web/documentation/extensions/ini/ini.md) (optional) ☑ -- [JSON (json-c)](./web/documentation/extensions/json/json.md) (optional) ☑ -- [PCRE2](./web/documentation/extensions/pcre2/pcre2.md) (optional) ☑ -- [PCSC (smart cards)](./web/documentation/extensions/pcsc/pcsc.md) (optional) ☑ -- [OpenSSL](./web/documentation/extensions/openssl/openssl.md) (optional) ☑ -- [SQLite](./web/documentation/extensions/sqlite/sqlite.md) (optional) ☑ -- [XML (libxml2)](./web/documentation/extensions/xml2/xml2.md) (optional) ☑ +### Functions -☑ = Done (or basics implemented) / ☐ = Planned or in progress. +- `fun name(params) body` — named functions +- `fn(params) body` — anonymous function literals +- First-class: pass as arguments, store in variables, recursion +- `return` with optional value (implicit nil) -Note: Not all of the above features will be implemented. Those who are marked "Done" will probaly remain in Fun, but I don't know actually... ;) +### Object-Oriented Programming -There are some libs written in Fun available in the [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) diretory. In the future most Fun enhancements should be written in Fun itself. +- `class Name(typed params) body` with `_construct(this, ...)` auto-invoked constructor +- `this` keyword, `obj.method(args)` method call sugar, `obj.field` dot property access +- `class Child(...) extends Parent` with method overriding -### OpenSSL quickstart (MD5) +### Data Structures -See the dedicated page: [./web/documentation/extensions/openssl/](./web/documentation/extensions/openssl/openssl.md) +- **Arrays**: literal `[1, 2, 3]`, index `arr[0]`, slice `arr[1:3]`, negative indices; `len()`, `push()`, `pop()`, `insert()`, `remove()`, `contains()`, `indexOf()`, `clear()`, `enumerate()`, `zip()`, `join()`, `map()`, `filter()`, `reduce()` +- **Maps**: literal `{key: value}`, bracket `map["key"]`, dot `map.key` access; `has()`, `keys()`, `values()` +- **Strings**: concatenation with `+`, `len()`, `substr()`, `find()`, `split()`, `join()` + +### Mathematics + +- Built-in: `abs`, `min`, `max`, `fmin`, `fmax`, `clamp`, `pow`, `sqrt`, `floor`, `ceil`, `trunc`, `round`, `sin`, `cos`, `tan`, `exp`, `log`, `log10`, `gcd`, `lcm`, `isqrt`, `sign` +- Random: `random_seed()`, `random_int()`, `random_number()` (cryptographic) +- Integer clamping: `sclamp()`, `uclamp()` + +### I/O & Platform + +- `print()`, `echo()` — output +- `read_file()`, `write_file()` — file I/O +- `input_line()` — stdin with optional prompt +- `env()`, `env_all()` — environment variables +- `proc_run()`, `system()` — process execution +- `os_list_dir()` — directory listing +- `time_now_ms()`, `clock_mono_ms()`, `date_format()`, `sleep()` + +### Networking (Built-in, Unix) + +- TCP: listen, accept, connect, send, recv, close +- Unix domain sockets: listen, connect +- Non-blocking I/O: `fd_set_nonblock()`, `fd_poll_read()`, `fd_poll_write()` + +### Serial Communication (Unix) + +- `serial_open()`, `serial_config()`, `serial_send()`, `serial_recv()`, `serial_close()` + +### Concurrency + +- `thread_spawn(fn, args)` returns thread ID; `thread_join(id)` returns result +- Cooperative async scheduler in stdlib + +### Debugging & Tooling + +- Built-in debugger with 64 breakpoints, step/next/finish/continue +- `--trace` / `-t` for opcode-level execution tracing +- `--repl-on-error`: enter REPL on runtime error with stack preserved +- Full-featured REPL with history, tab completion, multi-line input, commands (`:help`, `:load`, `:edit`, `:save`, `:debug`, `:trace`, `:type`, and more) +- `funstx` — syntax checker with optional `--fix` mode + +## Standard Library (lib/) + +Written primarily in Fun itself: + +- **Strings**: trim, starts/ends-with, split, replace-all, case conversion, repeat +- **Arrays**: slice, reverse, concat, unique, flatten +- **Math**: `abs`, `clamp`, `gcd`, `lcm`, `powi`, min3, max3, array min/max +- **Encoding**: hex encode/decode, base64 encode/decode +- **Cryptography** (pure Fun): MD5, SHA-1, SHA-256, SHA-384, SHA-512, CRC-32, CRC-32C, AES-256 ECB +- **Functional**: Option (Some/None), Result (Ok/Err), pattern matching +- **Ranges**: `range(n)`, `range2(start, end)`, `range3(start, end, step)` +- **Date/Time**: `DateTime` class with formatting, timers, sleep +- **CLI**: `argv()`, `parse_args()` with flag support +- **Console**: `Console` class with prompt, ask, hidden input, progress bar +- **Thread**: `Thread` class, **Process**: `Process` class +- **Socket classes**: `TcpClient`, `TcpServer`, `UnixClient` +- **Serial**: `Serial` class +- **Async**: Cooperative scheduler with I/O polling +- **HTTP**: Static file server, CGI-capable server +- **IRC**: `IRCClient` with full protocol support +- **CGI**: `CGI` class for web applications + +## Optional Extensions (Build-time) + +Enabled via CMake flags, wrapping mature C libraries: + +| Extension | Backend | +|---------------------------------------------------------------|-------------| +| [JSON](./web/documentation/extensions/json/json.md) | json-c | +| [cURL](./web/documentation/extensions/curl/curl.md) | libcurl | +| [SQLite](./web/documentation/extensions/sqlite/sqlite.md) | libsqlite3 | +| [PCRE2](./web/documentation/extensions/pcre2/pcre2.md) | libpcre2 | +| [OpenSSL](./web/documentation/extensions/openssl/openssl.md) | libcrypto | +| [INI](./web/documentation/extensions/ini/ini.md) | iniparser | +| [XML](./web/documentation/extensions/xml2/xml2.md) | libxml2 | +| [PC/SC](./web/documentation/extensions/pcsc/pcsc.md) | libpcsclite | +| [KCGI](./web/documentation/extensions/kcgi/kcgi.md) | libkcgi | +| [Redis/Valkey](./web/documentation/extensions/redis/redis.md) | hiredis | + +Some extensions also have a corresponding stdlib wrapper class (e.g., `JSON`, `INI`, `XML`, `PCSC`, `PCRE2`, `KCGI`). + +## Quick start + +
$ git clone https://git.xw3.org/fun/fun.git
+$ cd fun
+$ cmake -B build -DCMAKE_BUILD_TYPE=Release
+$ cmake --build build --target fun
+$ ./build/fun
+fun> print("Hello, World!")
+Hello, World!
+fun> :quit
+ +## Build Options + +- **`-DCMAKE_BUILD_TYPE=Debug`** — debug build with asserts +- **`-DCMAKE_BUILD_TYPE=Release`** — optimized build with LTO and stripping +- **`-DFUN_BUILD_MUSL=ON`** — static musl build +- Toggle each extension: `-DFUN_WITH_JSON=ON`, `-DFUN_WITH_CURL=ON`, etc. +- **`-DFUN_BUILD_DOXYGEN=ON`** — API reference ## Documentation -Looking for docs? Start here: - -- Local documentation index: [./web/documentation/documenatation.md](./web/documentation/documentation.md) - - Handbook: [./web/documentation/handbook/handbook.md](./web/documentation/handbook/handbook.md) - - Types overview: [./web/documentation/types/types.md](./web/documentation/types/types.md) - - REPL guide: [./web/documentation/repl/repl.md](./web/documentation/repl/repl.md) - - Testing: [./web/documentation/testing/testing.md](./web/documentation/testing/testing.md) - - Troubleshooting: [./web/documentation/troubleshooting/troubleshooting.md](./web/documentation/troubleshooting/troubleshooting.md) - -Additional references: - -- Specification: [./web/documentation/spec/v0.4.md](./web/documentation/spec/v0.4.md) (work in progress) +- Handbook: [./web/documentation/handbook/handbook.md](./web/documentation/handbook/handbook.md) +- Types: [./web/documentation/types/types.md](./web/documentation/types/types.md) +- REPL: [./web/documentation/repl/repl.md](./web/documentation/repl/repl.md) +- Testing: [./web/documentation/testing/testing.md](./web/documentation/testing/testing.md) +- Spec: [./web/documentation/spec/v0.4.md](./web/documentation/spec/v0.4.md) - Changelog: [CHANGELOG.md](./CHANGELOG.md) -- Examples demonstrating most features: [./examples/](./examples/) -- Internals and VM opcodes live in [./src/](./src/) (see [./src/vm/](./src/vm) for opcode implementations) - -Note: The project is evolving; some documents may lag behind. The docs index in [./web/documentation/documenatation.md](./web/documentation/documentation.md) is the most up‑to‑date entry point. +- Examples: [./examples/features.fun](./examples/features.fun) ## Author diff --git a/cmake/Extensions/Extensions.cmake b/cmake/Extensions/Extensions.cmake index 3e21523..5f79041 100644 --- a/cmake/Extensions/Extensions.cmake +++ b/cmake/Extensions/Extensions.cmake @@ -21,6 +21,7 @@ include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCSC.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/REPL.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/SQLITE.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/XML2.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/REDIS.cmake) # Summary of extension toggles message(STATUS "---- Fun extension summary ----") @@ -34,4 +35,5 @@ _fun_print_feature("PCRE2 (FUN_WITH_PCRE2)" FUN_WITH_PCRE2) _fun_print_feature("PCSC-Lite (FUN_WITH_PCSC)" FUN_WITH_PCSC) _fun_print_feature("REPL (FUN_WITH_REPL)" FUN_WITH_REPL) _fun_print_feature("SQLite (FUN_WITH_SQLITE)" FUN_WITH_SQLITE) +_fun_print_feature("Redis (FUN_WITH_REDIS)" FUN_WITH_REDIS) message(STATUS "--------------------------------") diff --git a/cmake/Extensions/REDIS.cmake b/cmake/Extensions/REDIS.cmake new file mode 100644 index 0000000..eb48b9e --- /dev/null +++ b/cmake/Extensions/REDIS.cmake @@ -0,0 +1,22 @@ +# Redis (hiredis) +option(FUN_WITH_REDIS "Enable Redis (hiredis) support" OFF) +set(HIREDIS_INCLUDE_DIRS "") +set(HIREDIS_LINK_LIBS "") +if(FUN_WITH_REDIS) + add_definitions(-DFUN_WITH_REDIS) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(HIREDIS QUIET hiredis) + endif() + if(HIREDIS_FOUND) + list(APPEND HIREDIS_INCLUDE_DIRS ${HIREDIS_INCLUDE_DIRS} ${HIREDIS_INCLUDE_DIRS}) + list(APPEND HIREDIS_LINK_LIBS ${HIREDIS_LINK_LIBS} ${HIREDIS_LIBRARIES}) + else() + find_library(HIREDIS_LIB hiredis) + if(HIREDIS_LIB) + list(APPEND HIREDIS_LINK_LIBS ${HIREDIS_LIB}) + else() + message(FATAL_ERROR "hiredis not found. Install hiredis (dev headers) or disable FUN_WITH_REDIS.") + endif() + endif() +endif() diff --git a/cmake/Targets.cmake b/cmake/Targets.cmake index 534d279..b8939d8 100644 --- a/cmake/Targets.cmake +++ b/cmake/Targets.cmake @@ -48,7 +48,8 @@ foreach(var_pair LIBSQL LIBXML2 OPENSSL - KCGI) + KCGI + HIREDIS) if(${var_pair}_INCLUDE_DIRS) target_include_directories(fun_core PRIVATE ${${var_pair}_INCLUDE_DIRS}) endif() @@ -94,6 +95,9 @@ if(FUN_WITH_SQLITE) target_compile_definitions(fun_core PUBLIC FUN_WITH_SQLITE=1) endif() +if(FUN_WITH_REDIS) + target_compile_definitions(fun_core PUBLIC FUN_WITH_REDIS=1) +endif() if(FUN_WITH_OPENSSL) target_compile_definitions(fun_core PUBLIC FUN_WITH_OPENSSL=1) diff --git a/examples/extensions/redis/README.md b/examples/extensions/redis/README.md new file mode 100644 index 0000000..88bace7 --- /dev/null +++ b/examples/extensions/redis/README.md @@ -0,0 +1,29 @@ +Redis extension examples (hiredis) + +This folder contains small Fun scripts that demonstrate how to use the Redis extension. + +Prerequisites +- Build Fun with Redis support enabled (FUN_WITH_REDIS=ON). This is ON by default in cmake/Extensions/REDIS.cmake. +- A Redis-compatible server reachable at 127.0.0.1:6379. + +How to run +- Using the Fun CLI from the repository root: + - Debug profile path: build_debug/fun + - Release profile path: build_release/fun + +Examples +1. basic_ping.fun + - Connects, PINGs, then closes. + +2. kv_set_get.fun + - SET/GET, EXISTS and DEL for a demo key. + +3. list_ops.fun + - Demonstrates LPUSH and LRANGE on a list. + +4. hash_ops.fun + - Demonstrates HSET, HGET and HGETALL on a hash. + +Note +- All examples use direct inline command strings with redis_cmd(handle, "COMMAND args..."). +- Close the connection with redis_close(handle) when finished. diff --git a/examples/extensions/redis/basic_ping.fun b/examples/extensions/redis/basic_ping.fun new file mode 100755 index 0000000..773d8bb --- /dev/null +++ b/examples/extensions/redis/basic_ping.fun @@ -0,0 +1,26 @@ +#!/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-06-03 + */ + +/* + * Simple Redis test using Fun builtins backed by hiredis. + * + * Basic PING + */ + +h = redis_connect('127.0.0.1', 6379) + +print('handle type: ' + typeof(h)) + +print(redis_cmd(h, 'PING')) + +redis_close(h) diff --git a/examples/extensions/redis/hash_ops.fun b/examples/extensions/redis/hash_ops.fun new file mode 100755 index 0000000..e5a8be9 --- /dev/null +++ b/examples/extensions/redis/hash_ops.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: 2026-06-03 + */ + +/* + * Simple Redis test using Fun builtins backed by hiredis. + * + * Hash operations (HSET/HGET/HGETALL) + */ + +h = redis_connect('127.0.0.1', 6379) +key = 'fun:examples:redis:hash:user1' + +// Start fresh +_ = redis_cmd(h, 'DEL ' + key) + +// Set a couple of fields +print(redis_cmd(h, 'HSET ' + key + ' name Alice')) +print(redis_cmd(h, 'HSET ' + key + ' age 30')) + +// Fetch a single field +print('HGET name -> ' + redis_cmd(h, 'HGET ' + key + ' name')) + +// Fetch all fields (returns a flat array [field, value, field, value, ...]) +all = redis_cmd(h, 'HGETALL ' + key) +print('HGETALL ->') +print(all) + +redis_close(h) diff --git a/examples/extensions/redis/kv_set_get.fun b/examples/extensions/redis/kv_set_get.fun new file mode 100755 index 0000000..e68b7a6 --- /dev/null +++ b/examples/extensions/redis/kv_set_get.fun @@ -0,0 +1,37 @@ +#!/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-06-03 + */ + +/* + * Simple Redis test using Fun builtins backed by hiredis. + * + * Simple key/value set-get-delete + */ + +h = redis_connect('127.0.0.1', 6379) +key = 'fun:examples:redis:key' + +// Clean slate +_ = redis_cmd(h, 'DEL ' + key) + +// Set and get +print(redis_cmd(h, 'SET ' + key + ' 42')) +print('GET -> ' + redis_cmd(h, 'GET ' + key)) + +// Check existence +print('EXISTS -> ' + to_string(redis_cmd(h, 'EXISTS ' + key))) + +// Delete +print('DEL -> ' + to_string(redis_cmd(h, 'DEL ' + key))) +print('EXISTS(after DEL) -> ' + to_string(redis_cmd(h, 'EXISTS ' + key))) + +redis_close(h) diff --git a/examples/extensions/redis/list_ops.fun b/examples/extensions/redis/list_ops.fun new file mode 100755 index 0000000..ba8d8ed --- /dev/null +++ b/examples/extensions/redis/list_ops.fun @@ -0,0 +1,36 @@ +#!/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-06-03 + */ + +/* + * Simple Redis test using Fun builtins backed by hiredis. + * + * List operations (LPUSH/LRANGE) + */ + +h = redis_connect('127.0.0.1', 6379) +key = 'fun:examples:redis:list' + +// Start fresh +_ = redis_cmd(h, 'DEL ' + key) + +// Push some values to the left +print(redis_cmd(h, 'LPUSH ' + key + ' a')) +print(redis_cmd(h, 'LPUSH ' + key + ' b')) +print(redis_cmd(h, 'LPUSH ' + key + ' c')) + +// Read entire list +vals = redis_cmd(h, 'LRANGE ' + key + ' 0 -1') +print('LRANGE 0 -1 -> ') +print(vals) + +redis_close(h) diff --git a/examples/extensions/redis/redis_test.fun b/examples/extensions/redis/redis_test.fun new file mode 100755 index 0000000..b1b8989 --- /dev/null +++ b/examples/extensions/redis/redis_test.fun @@ -0,0 +1,28 @@ +#!/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-06-03 + */ + +/* + * Simple Redis test using Fun builtins backed by hiredis. + */ + +h = redis_connect('127.0.0.1', 6379) + +print(typeof(h)) + +print(redis_cmd(h, 'PING')) + +_ = redis_cmd(h, 'SET fun_demo_key 42') + +print(redis_cmd(h, 'GET fun_demo_key')) + +redis_close(h) diff --git a/examples/features.fun b/examples/features.fun index 483219e..9ebce9d 100755 --- a/examples/features.fun +++ b/examples/features.fun @@ -10,48 +10,120 @@ print("") // ============================================ print("1. Strong Type System:") string name = "Fun Language" -float version = 0.3 +float version = 0.41 boolean is_awesome = true +number meaning = 42 items = [1, 2, 3, 4, 5] config = {"debug": true, "port": 8080} +nil_val = nil -print(" Language: " + name + " v" + to_string(version)) +print(" Language: " + name + " v" + fun_version()) print(" Awesome: " + to_string(is_awesome)) +print(" Nil: " + to_string(nil_val)) print("") // ============================================ -// 2. Modern Array Operations +// 2. Type Introspection // ============================================ -print("2. Array Operations:") +print("2. Type Introspection:") +number check_int = 42 +string check_str = "hello" +check_arr = [1, 2, 3] +check_map = {"key": "value"} +check_float = 3.14 + +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(" typeof(3.14) = " + typeof(check_float)) +print("") + +// ============================================ +// 3. Conversion & Casting +// ============================================ +print("3. Conversion & Casting:") +print(" to_string(42) = " + to_string(42)) +print(" to_number(\"99\") = " + to_string(to_number("99"))) +casted = cast(1, "boolean") +print(" cast(1, \"boolean\") = " + to_string(casted)) +casted2 = cast("42", "number") +print(" cast(\"42\", \"number\") = " + to_string(casted2)) +print("") + +// ============================================ +// 4. String Manipulation +// ============================================ +print("4. String Operations:") +string text = "Hello, Fun Language!" +print(" Original: " + text) +print(" Length: " + to_string(len(text))) +print(" Substr(0,5): " + substr(text, 0, 5)) +print(" Find(\"Fun\"): " + to_string(find(text, "Fun"))) +parts = split(text, " ") +print(" Split by space: " + to_string(parts)) +joined = join(parts, "-") +print(" Join with '-': " + joined) +print("") + +// ============================================ +// 5. Modern Array Operations +// ============================================ +print("5. Array Operations:") numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(" Original: " + to_string(numbers)) +print(" Length: " + to_string(len(numbers))) +print(" Index 0: " + to_string(numbers[0])) +print(" Index last: " + to_string(numbers[len(numbers) - 1])) -// Array helpers from spec: push, join, map, filter, reduce -joined = join([10, 20, 30], ", ") -print(" Joined: " + joined) +// Slice syntax arr[start:end] +sliced = numbers[2:7] +print(" Slice [2:7]: " + to_string(sliced)) -// Iterate arrays -print(" Iteration:") -for item in ["apple", "banana", "cherry"] - print(" " + item) +// Mutating array ops +arr = [10, 20, 30] +push(arr, 40) +print(" After push(40): " + to_string(arr)) +removed = pop(arr) +print(" Popped: " + to_string(removed) + ", arr: " + to_string(arr)) +insert(arr, 1, 15) +print(" After insert(1, 15): " + to_string(arr)) +removed = remove(arr, 0) +print(" Removed at 0: " + to_string(removed) + ", arr: " + to_string(arr)) + +// Search & utility +print(" Contains 20? " + to_string(contains(arr, 20))) +print(" Index of 20? " + to_string(indexOf(arr, 20))) +print(" Enumerate: " + to_string(enumerate(["a", "b", "c"]))) +print(" Zip: " + to_string(zip([1, 2, 3], ["x", "y", "z"]))) +clear(arr) +print(" After clear: " + to_string(arr)) print("") // ============================================ -// 3. Maps (Dictionaries) +// 6. Maps (Dictionaries) // ============================================ -print("3. Map Operations:") +print("6. 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))) + +// Bracket access and assignment +print(" person[\"name\"]: " + person["name"]) +person["age"] = 31 +print(" Updated age: " + to_string(person["age"])) + +// Object property access (dot notation) +print(" person.name: " + person.name) print("") // ============================================ -// 4. Object-Oriented Programming +// 7. Object-Oriented Programming // ============================================ -print("4. Classes & Objects:") +print("7. Classes & Objects:") class Counter(number initial, string label) count = 0 @@ -78,9 +150,9 @@ counter.display() print("") // ============================================ -// 5. Inheritance +// 8. Inheritance // ============================================ -print("5. Inheritance:") +print("8. Inheritance:") class Animal(string type) species = "" @@ -107,9 +179,9 @@ print(" Species: " + dog.species) print("") // ============================================ -// 6. Error Handling +// 9. Error Handling & Exceptions // ============================================ -print("6. Exception Handling:") +print("9. Exception Handling:") try print(" Attempting risky operation...") @@ -122,51 +194,63 @@ finally print("") // ============================================ -// 7. String Manipulation +// 10. Comparison & Logical Operators // ============================================ -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("10. Comparison & Logical Operators:") +number a = 10 +number b = 20 +print(" a=10, b=20") +print(" a < b: " + to_string(a < b)) +print(" a <= b: " + to_string(a <= b)) +print(" a > b: " + to_string(a > b)) +print(" a >= b: " + to_string(a >= b)) +print(" a == b: " + to_string(a == b)) +print(" a != b: " + to_string(a != b)) +print(" a < b && a > 0: " + to_string(a < b && a > 0)) +print(" a > b || a > 0: " + to_string(a > b || a > 0)) +print(" !true: " + to_string(!true)) +print(" Ternary (a < b ? \"yes\" : \"no\"): " + (a < b ? "yes" : "no")) print("") // ============================================ -// 8. Mathematical Operations +// 11. Control Flow: if/else if/else // ============================================ -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("11. If/Else If/Else:") +number score = 85 + +if score >= 90 + print(" Grade: A") +else if score >= 80 + print(" Grade: B") +else if score >= 70 + print(" Grade: C") +else + print(" Grade: F") print("") // ============================================ -// 9. Bitwise Operations +// 12. Control Flow: Loops // ============================================ -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("") +print("12. Loop Variants:") -// ============================================ -// 10. Control Flow -// ============================================ -print("10. Control Flow:") +// For-each with array +print(" For-each array:") +for item in ["apple", "banana", "cherry"] + print(" " + item) -// For loop with array -print(" Countdown:") -for i in [5, 4, 3, 2, 1] - print(" " + to_string(i) + "...") -print(" Liftoff!") +// For-range loop +print(" For-range 0..4:") +for i in range(0, 5) + print(" " + to_string(i)) + +// For-map loop +print(" For-map key, value:") +config_map = {"a": 1, "b": 2, "c": 3} +for (k, v) in config_map + print(" " + k + " = " + to_string(v)) // While with break/continue -print(" Skip evens:") +print(" While with break/continue:") number n = 0 while n < 10 n = n + 1 @@ -178,24 +262,55 @@ while n < 10 print("") // ============================================ -// 11. Type Introspection +// 13. Mathematical Operations // ============================================ -print("11. Type Introspection:") -number check_int = 42 -string check_str = "hello" -check_arr = [1, 2, 3] -check_map = {"key": "value"} +print("13. Math Functions:") +number mx = -16 +print(" abs(-16) = " + to_string(abs(mx))) +print(" min(10, 20) = " + to_string(min(10, 20))) +print(" max(10, 20) = " + to_string(max(10, 20))) +print(" clamp(50, 0, 10) = " + to_string(clamp(50, 0, 10))) +print(" pow(2, 10) = " + to_string(pow(2, 10))) +print(" sqrt(144) = " + to_string(sqrt(144))) +print(" floor(3.7) = " + to_string(floor(3.7))) +print(" ceil(3.2) = " + to_string(ceil(3.2))) +print(" round(3.5) = " + to_string(round(3.5))) +print(" trunc(3.9) = " + to_string(trunc(3.9))) +print(" sin(0) = " + to_string(sin(0))) +print(" cos(0) = " + to_string(cos(0))) +print(" gcd(12, 8) = " + to_string(gcd(12, 8))) +print(" lcm(12, 8) = " + to_string(lcm(12, 8))) +print(" isqrt(50) = " + to_string(isqrt(50))) +print(" sign(-42) = " + to_string(sign(-42))) +print(" fmin(3.1, 2.9) = " + to_string(fmin(3.1, 2.9))) +print(" fmax(3.1, 2.9) = " + to_string(fmax(3.1, 2.9))) -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)) +// Random (deterministic for demo) +random_seed(42) +print(" random_int(1, 100) = " + to_string(random_int(1, 100))) +print(" random_number(8) = " + random_number(8)) print("") // ============================================ -// 12. Functional Programming +// 14. Bitwise Operations // ============================================ -print("12. Higher-Order Functions:") +print("14. Bitwise Operations:") +number bits1 = 12 // 1100 +number bits2 = 10 // 1010 +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 = " + to_string(bnot(bits1))) +print(" 12 << 2 = " + to_string(shl(bits1, 2))) +print(" 12 >> 2 = " + to_string(shr(bits1, 2))) +print(" rol(0x80000001, 1) = " + to_string(rol(0x80000001, 1))) +print(" ror(0x80000001, 1) = " + to_string(ror(0x80000001, 1))) +print("") + +// ============================================ +// 15. Functional & Higher-Order Programming +// ============================================ +print("15. Higher-Order Functions:") fun double(n) return n * 2 @@ -208,28 +323,112 @@ print(" apply_twice(5, double) = " + to_string(result)) print("") // ============================================ -// 13. Array Operations with Spec Functions +// 16. Array Higher-Order Functions // ============================================ -print("13. Array Higher-Order Functions:") +print("16. 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)) +print(" map(nums, square): " + to_string(squared)) fun is_even(x) return x % 2 == 0 evens = filter(nums, is_even) -print(" Evens: " + to_string(evens)) +print(" filter(nums, is_even): " + to_string(evens)) fun sum(acc, x) return acc + x total = reduce(nums, 0, sum) -print(" Sum: " + to_string(total)) +print(" reduce(nums, 0, sum): " + to_string(total)) +print("") + +// ============================================ +// 17. File I/O +// ============================================ +print("17. File I/O:") +write_file("/tmp/fun_demo.txt", "Hello from Fun!") +content = read_file("/tmp/fun_demo.txt") +print(" Written and read back: " + content) +print("") + +// ============================================ +// 18. Environment & OS +// ============================================ +print("18. Environment & OS:") +print(" HOME: " + env("HOME")) +print(" Version: " + fun_version()) + +// List directory (non-empty /tmp assumed) +listing = os_list_dir("/tmp") +print(" /tmp has " + to_string(len(listing)) + " entries") + +// Run a command and capture output +proc_result = proc_run("echo hello from fun") +print(" proc_run output: " + proc_result["out"]) + +// System call exit code +sys_code = system("true") +print(" system(\"true\") exit: " + to_string(sys_code)) +print("") + +// ============================================ +// 19. Date, Time & Sleep +// ============================================ +print("19. Date, Time & Sleep:") +now = time_now_ms() +print(" Now (epoch ms): " + to_string(now)) +print(" Date formatted: " + date_format(now, "%Y-%m-%d %H:%M:%S")) + +mono = clock_mono_ms() +print(" Monotonic ms: " + to_string(mono)) + +// Short sleep to demonstrate +sleep(10) +after = clock_mono_ms() +diff = after - mono +print(" Slept 10 ms, elapsed: " + to_string(diff) + " ms") +print("") + +// ============================================ +// 20. Echo (print without newline) +// ============================================ +print("20. Echo (no newline):") +echo(" Hello, ") +echo("world") +print("!") +print("") + +// ============================================ +// 21. Threading +// ============================================ +print("21. Threading:") + +fun worker(id) + print(" Thread " + to_string(id) + " says hi!") + return id * 2 + +t1 = thread_spawn(worker, [1]) +t2 = thread_spawn(worker, [2]) +r1 = thread_join(t1) +r2 = thread_join(t2) +print(" Joined thread results: " + to_string(r1) + ", " + to_string(r2)) +print("") + +// ============================================ +// 22. Type-Safe Integer Clamping +// ============================================ +print("22. Integer Clamping:") +byte val = 300 +print(" byte val = 300 -> clamped to " + to_string(val)) +int8 signed = 200 +print(" int8 val = 200 -> clamped to " + to_string(signed)) +uint16 big = 70000 +print(" uint16 val = 70000 -> clamped to " + to_string(big)) print("") // ============================================ diff --git a/make b/make index c26a06a..8dcffd3 100755 --- a/make +++ b/make @@ -31,6 +31,7 @@ if [ "$target" = "all" ]; then -DFUN_WITH_CPP=ON \ -DFUN_WITH_KCGI=ON \ -DFUN_WITH_OPENSSL=ON \ + -DFUN_WITH_REDIS=ON \ && cmake --build build --target fun elif [ "$target" = "all_debug" ]; then rm -rf build \ @@ -45,6 +46,7 @@ elif [ "$target" = "all_debug" ]; then -DFUN_WITH_INI=ON \ -DFUN_WITH_CPP=ON \ -DFUN_WITH_OPENSSL=ON \ + -DFUN_WITH_REDIS=ON \ -DFUN_DEBUG=ON \ && cmake --build build --target fun elif [ "$target" = "alpine" ]; then diff --git a/playground/.gitkeep b/playground/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/check_op_includes.py b/scripts/check_op_includes.py index fbc58e9..be9b6e6 100755 --- a/scripts/check_op_includes.py +++ b/scripts/check_op_includes.py @@ -134,6 +134,9 @@ def parse_includes_from_vm(text: str) -> set[str]: if d == "sqlite": if n in {"open", "close", "exec", "query"}: return f"SQLITE_{n.upper()}" + if d == "redis": + if n in {"connect", "cmd", "close"}: + return f"REDIS_{n.upper()}" if d == "pcsc": if n in {"establish", "release", "list_readers", "connect", "disconnect", "transmit"}: return f"PCSC_{n.upper()}" diff --git a/src/bytecode.h b/src/bytecode.h index 42c4aea..88765a7 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -186,6 +186,11 @@ typedef enum { OP_SQLITE_EXEC, // pops sql, handle; pushes sqlite rc (0=OK) OP_SQLITE_QUERY, // pops sql, handle; pushes array + // Redis (optional, hiredis) + OP_REDIS_CONNECT, // pops port:int, host:string; pushes handle (>0) or 0 + OP_REDIS_CMD, // pops cmd:string, handle:int; pushes reply (string/int/array/nil/map) + OP_REDIS_CLOSE, // pops handle:int; pushes Nil + // PCSC (smart card) opcodes OP_PCSC_ESTABLISH, // returns context id (>0) or 0 OP_PCSC_RELEASE, // pops ctx id; returns 1/0 diff --git a/src/extensions/redis.c b/src/extensions/redis.c new file mode 100644 index 0000000..7c6ff1b --- /dev/null +++ b/src/extensions/redis.c @@ -0,0 +1,221 @@ +/* + * 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 + */ + +/** + * @file redis.c + * @brief Hiredis handle registry and reply mapping helpers for the Fun VM. + * + * This translation unit provides two small building blocks used by the Redis + * opcodes (implemented under src/vm/redis/*.c) and included from src/vm.c: + * + * 1) A process-local registry that assigns monotonically increasing positive + * integer identifiers to hiredis connection pointers (redisContext*). + * VM opcodes pass integer ids on the stack instead of raw pointers, keeping + * the bytecode portable and preventing accidental misuse of pointers. + * + * 2) Utilities to convert hiredis reply objects (redisReply) into Fun VM + * Value instances, recursively mapping arrays and supporting basic numeric + * and string types. + * + * Build-time feature flag + * ----------------------- + * The code is compiled only when the CMake option FUN_WITH_REDIS is enabled + * (i.e., the preprocessor symbol FUN_WITH_REDIS is defined). When disabled, + * this file contributes no symbols and the corresponding opcodes are compiled + * into stubs that return neutral values. + * + * Ownership and lifetime + * ---------------------- + * - The registry does NOT open or close Redis connections by itself; it merely + * stores pointers created elsewhere (e.g., via redisConnectWithTimeout()). + * - Adding an entry does not transfer ownership of the redisContext. Callers + * remain responsible for invoking redisFree() at the appropriate time. + * - Removing an entry from the registry does NOT free the connection; it only + * forgets the mapping between id and pointer. The connect/close opcodes take + * care of proper ownership transitions. + * - Integer identifiers are monotonically increasing per process. Once a + * handle id is deleted, it will not be reused within the same process + * lifetime. + * + * Error handling + * -------------- + * Functions here perform basic validation/allocations only. Allocation + * failures return NULL (for lookups/additions) or are silently ignored (for + * deletions of non-existent ids). No hiredis API calls are made here, so no + * hiredis error codes are produced by this module itself. + * + * Thread-safety + * ------------- + * The registry is a simple singly-linked list with no synchronization. It is + * NOT thread-safe. If the VM uses Redis from multiple threads, the caller must + * provide external synchronization around calls to these helpers. + * + * Example + * ------- + * @code{.c} + * // Open a hiredis connection elsewhere: + * struct timeval tv = { .tv_sec = 2, .tv_usec = 0 }; + * redisContext *ctx = redisConnectWithTimeout("127.0.0.1", 6379, tv); + * if (ctx && !ctx->err) { + * // Register and get an id: + * RedisHandle *h = redis_reg_add(ctx); + * int id = h ? h->id : -1; + * + * // Later look it up: + * RedisHandle *same = redis_reg_get(id); + * if (same) { + * // use same->ctx with hiredis APIs + * } + * + * // When finished, drop the registry entry and free manually: + * redis_reg_del(id); + * redisFree(ctx); + * } + * @endcode + */ + +#ifdef FUN_WITH_REDIS +#include +#include +#include + +/* Forward declarations from the VM (available in the same TU via includes) */ +static Value hiredis_reply_to_value(const redisReply *r); + +/** + * @brief Node in a singly-linked list of registered Redis handles. + * + * Associates a monotonically increasing positive integer identifier with a raw + * hiredis connection pointer. The list head is stored in a file-static global + * (g_redis_handles). + */ +typedef struct RedisHandle { + int id; /**< Positive identifier assigned by the registry. */ + redisContext *ctx; /**< Opaque pointer to a hiredis connection. */ + struct RedisHandle *next;/**< Next entry in the singly-linked list. */ +} RedisHandle; + +/** @brief Global head of the Redis handle list (NULL denotes empty list). */ +static RedisHandle *g_redis_handles = NULL; +/** @brief Next positive identifier to assign to a newly added handle. */ +static int g_redis_next_id = 1; + +/** + * @brief Add a hiredis connection handle to the registry. + * + * Allocates a new list node, assigns a fresh positive id, and prepends it to + * the internal registry list. Ownership of the redisContext remains with the + * caller; this registry does not free it during deletion. + * + * @param ctx Valid pointer to an opened hiredis connection. + * @return Pointer to the newly created RedisHandle on success; NULL on + * allocation failure. The returned pointer remains owned by the + * registry; do not free it directly. + */ +static RedisHandle *redis_reg_add(redisContext *ctx) { + RedisHandle *h = (RedisHandle *)calloc(1, sizeof(RedisHandle)); + if (!h) return NULL; + h->id = g_redis_next_id++; + h->ctx = ctx; + h->next = g_redis_handles; + g_redis_handles = h; + return h; +} + +/** + * @brief Look up a registered Redis handle by id. + * + * Performs a linear search over the internal list to find a matching id. + * + * @param id Positive identifier previously returned by redis_reg_add(). + * @return Pointer to the RedisHandle entry if found; NULL otherwise. + * + * @note The returned pointer is owned by the registry and must not be freed by + * the caller. + */ +static RedisHandle *redis_reg_get(int id) { + for (RedisHandle *p = g_redis_handles; p; p = p->next) + if (p->id == id) return p; + return NULL; +} + +/** + * @brief Remove a Redis handle entry from the registry. + * + * Deletes the list node associated with the given id. + * + * @param id Positive identifier of the entry to remove. + * + * @note This function does not free the underlying redisContext; the caller is + * responsible for calling redisFree() if appropriate. + * @note If the id does not exist, the function is a no-op. + */ +static void redis_reg_del(int id) { + RedisHandle **pp = &g_redis_handles; + while (*pp) { + if ((*pp)->id == id) { + RedisHandle *d = *pp; + *pp = d->next; + free(d); + return; + } + pp = &(*pp)->next; + } +} + +/** + * @brief Convert a hiredis reply to a Fun Value. + * + * Recursively maps hiredis reply types to the closest Fun representation: + * - REDIS_REPLY_STRING / STATUS -> string + * - REDIS_REPLY_INTEGER -> int + * - REDIS_REPLY_NIL -> nil + * - REDIS_REPLY_ARRAY -> array of recursively converted values + * - REDIS_REPLY_DOUBLE (if available) -> float + * - REDIS_REPLY_ERROR / default -> string (error text or "ERR") + * + * @param r Non-owning pointer to a redisReply. + * @return Value converted from the reply. For NULL replies, returns nil. + */ +static Value hiredis_reply_to_value(const redisReply *r) { + if (!r) return make_nil(); + switch (r->type) { + case REDIS_REPLY_STRING: + case REDIS_REPLY_STATUS: + return make_string(r->str ? r->str : ""); + case REDIS_REPLY_INTEGER: + return make_int((int64_t)r->integer); + case REDIS_REPLY_NIL: + return make_nil(); + case REDIS_REPLY_ARRAY: { + int n = (int)r->elements; + if (n <= 0) { + return make_array_from_values(NULL, 0); + } + Value *items = (Value *)calloc((size_t)n, sizeof(Value)); + if (!items) return make_array_from_values(NULL, 0); + for (int i = 0; i < n; i++) { + items[i] = hiredis_reply_to_value(r->element[i]); + } + Value arr = make_array_from_values(items, n); + for (int i = 0; i < n; i++) free_value(items[i]); + free(items); + return arr; + } +#ifdef REDIS_REPLY_DOUBLE + case REDIS_REPLY_DOUBLE: + return make_float(r->dval); +#endif + case REDIS_REPLY_ERROR: + default: + return make_string(r->str ? r->str : "ERR"); + } +} + +#endif /* FUN_WITH_REDIS */ diff --git a/src/fun.c b/src/fun.c index 127af48..12869de 100644 --- a/src/fun.c +++ b/src/fun.c @@ -142,11 +142,26 @@ int main(int argc, char **argv) { } char *joined = (char *)malloc(total); if (joined) { - joined[0] = '\0'; + size_t off = 0; for (int i = 0; i < sargc; ++i) { - strcat(joined, argv[sargi + i]); - if (i + 1 < sargc) strcat(joined, " "); + int written = snprintf(joined + off, (off < total ? total - off : 0), + "%s%s", + argv[sargi + i], + (i + 1 < sargc) ? " " : ""); + if (written < 0) { /* encoding error */ + off = total; /* force stop */ + break; + } + size_t w = (size_t)written; + if (off + w >= total) { /* ensure we don't advance beyond buffer */ + off = total ? total - 1 : 0; + joined[off] = '\0'; + break; + } + off += w; } + /* Ensure NUL termination even if loop didn't run */ + if (total > 0) joined[(off < total) ? off : (total - 1)] = '\0'; setenv("FUN_ARGS", joined, 1); free(joined); } diff --git a/src/parser.c b/src/parser.c index eee6572..aaa020d 100644 --- a/src/parser.c +++ b/src/parser.c @@ -2070,6 +2070,75 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* Redis builtins (hiredis) */ + if (strcmp(name, "redis_connect") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "redis_connect expects (host, port)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "redis_connect expects (host, port)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "redis_connect expects (host, port)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after redis_connect args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_REDIS_CONNECT, 0); + free(name); + return 1; + } + if (strcmp(name, "redis_cmd") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "redis_cmd expects (handle, cmd)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "redis_cmd expects (handle, cmd)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "redis_cmd expects (handle, cmd)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after redis_cmd args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_REDIS_CMD, 0); + free(name); + return 1; + } + if (strcmp(name, "redis_close") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "redis_close expects (handle)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after redis_close arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_REDIS_CLOSE, 0); + free(name); + return 1; + } if (strcmp(name, "sqlite_close") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { diff --git a/src/repl.c b/src/repl.c index 458886f..4d2ccb7 100644 --- a/src/repl.c +++ b/src/repl.c @@ -259,14 +259,16 @@ static int complete_load_path(char *buf, size_t *len_io) { if (slash) { size_t dlen = (size_t)(slash - expanded); if (dlen == 0) { - strcpy(dirpart, "/"); + /* use bounded copy to avoid potential overflow (even though "/" fits) */ + snprintf(dirpart, sizeof(dirpart), "%s", "/"); } else { memcpy(dirpart, expanded, dlen); dirpart[dlen] = '\0'; } snprintf(base, sizeof(base), "%s", slash + 1); } else { - strcpy(dirpart, "."); + /* use bounded copy to avoid potential overflow (even though "." fits) */ + snprintf(dirpart, sizeof(dirpart), "%s", "."); snprintf(base, sizeof(base), "%s", expanded); } diff --git a/src/vm.c b/src/vm.c index f5a3803..8841e36 100644 --- a/src/vm.c +++ b/src/vm.c @@ -73,6 +73,7 @@ #include "extensions/sqlite.c" #include "extensions/xml2.c" #include "extensions/kcgi.c" +#include "extensions/redis.c" /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); @@ -1230,6 +1231,13 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/sqlite/query.c" #endif +/* Redis ops */ +#ifdef FUN_WITH_REDIS +#include "vm/redis/connect.c" +#include "vm/redis/cmd.c" +#include "vm/redis/close.c" +#endif + /* C++ demo opcodes (guarded) */ #ifdef FUN_WITH_CPP case OP_CPP_ADD: { diff --git a/src/vm.h b/src/vm.h index c76de93..a3a864f 100644 --- a/src/vm.h +++ b/src/vm.h @@ -66,6 +66,8 @@ static const char *opcode_names[] = { "JSON_PARSE", "JSON_STRINGIFY", "JSON_FROM_FILE", "JSON_TO_FILE", "CURL_GET", "CURL_POST", "CURL_DOWNLOAD", "SQLITE_OPEN", "SQLITE_CLOSE", "SQLITE_EXEC", "SQLITE_QUERY", + /* Redis (hiredis) */ + "REDIS_CONNECT", "REDIS_CMD", "REDIS_CLOSE", "LIBSQL_OPEN", "LIBSQL_CLOSE", "LIBSQL_EXEC", "LIBSQL_QUERY", "PCSC_ESTABLISH", "PCSC_RELEASE", "PCSC_LIST_READERS", "PCSC_CONNECT", "PCSC_DISCONNECT", "PCSC_TRANSMIT", "PCRE2_TEST", "PCRE2_MATCH", "PCRE2_FINDALL", diff --git a/src/vm/os/proc_system.c b/src/vm/os/proc_system.c index 238b18f..90bc8db 100644 --- a/src/vm/os/proc_system.c +++ b/src/vm/os/proc_system.c @@ -27,6 +27,24 @@ case OP_PROC_SYSTEM: { push_value(vm, make_int(-1)); break; } + /* Security hardening: reject commands containing shell metacharacters or control chars + to reduce risk of command injection when using system(3). This preserves simple + command execution like "ls -l" but blocks dangerous constructs like pipes, redirects, + command substitution, etc. */ + const char *bad = "&;|$<>`\\\"'()*?[]{}~"; + int unsafe = 0; + for (const unsigned char *p = (const unsigned char *)cmd; *p; ++p) { + if (*p < 0x20 || strchr(bad, (int)*p)) { /* control or meta */ + unsafe = 1; + break; + } + } + if (unsafe) { + /* refuse to execute potentially unsafe shell command */ + push_value(vm, make_int(-1)); + free(cmd); + break; + } int status = system(cmd); int code = -1; #ifdef __unix__ diff --git a/src/vm/redis/close.c b/src/vm/redis/close.c new file mode 100644 index 0000000..4315ce3 --- /dev/null +++ b/src/vm/redis/close.c @@ -0,0 +1,45 @@ +/* + * 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 + */ + +/** + * @file close.c + * @brief Implements the OP_REDIS_CLOSE opcode (conditional build). + * + * Closes a previously opened Redis connection and removes its registry entry. + * + * Stack effect + * ------------ + * OP_REDIS_CLOSE: (handle:int) -> Nil + * + * Behavior + * -------- + * - When FUN_WITH_REDIS is enabled, looks up the handle, calls redisFree() on + * the underlying connection if present, clears the pointer, and removes the + * registry entry. Always pushes Nil. + * - When FUN_WITH_REDIS is disabled, pops the argument and pushes Nil. + */ + +case OP_REDIS_CLOSE: { +#ifdef FUN_WITH_REDIS + Value vh = pop_value(vm); + int hid = (int)vh.i; + free_value(vh); + RedisHandle *h = redis_reg_get(hid); + if (h && h->ctx) { + redisFree(h->ctx); + h->ctx = NULL; + } + redis_reg_del(hid); + push_value(vm, make_nil()); +#else + Value v1 = pop_value(vm); free_value(v1); + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/redis/cmd.c b/src/vm/redis/cmd.c new file mode 100644 index 0000000..8301a98 --- /dev/null +++ b/src/vm/redis/cmd.c @@ -0,0 +1,58 @@ +/* + * 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 + */ + +/** + * @file cmd.c + * @brief Implements the OP_REDIS_CMD opcode (conditional build). + * + * Executes a Redis command using the synchronous hiredis API on a previously + * opened handle and converts the reply to a Fun Value. + * + * Stack effect + * ------------ + * OP_REDIS_CMD: (handle:int, cmd:string) -> reply:Value + * + * Behavior + * -------- + * - The command string uses Redis inline protocol formatting (e.g., + * "PING", "SET key val", "LRANGE list 0 -1"). + * - When FUN_WITH_REDIS is enabled, the opcode looks up the connection by + * handle id, issues redisCommand(), and converts the resulting reply to a + * Fun Value: + * - status/string -> string + * - integer -> int + * - nil -> nil + * - array -> array of converted elements + * On errors or lookup failures, pushes Nil. + * - When FUN_WITH_REDIS is disabled, pops arguments and pushes Nil. + */ + +case OP_REDIS_CMD: { +#ifdef FUN_WITH_REDIS + Value vcmd = pop_value(vm); + Value vh = pop_value(vm); + int hid = (int)vh.i; + char *cmd = value_to_string_alloc(&vcmd); + free_value(vh); + free_value(vcmd); + RedisHandle *h = redis_reg_get(hid); + if (!h || !h->ctx || !cmd) { if (cmd) free(cmd); push_value(vm, make_nil()); break; } + redisReply *r = (redisReply *)redisCommand(h->ctx, cmd); + free(cmd); + if (!r) { push_value(vm, make_nil()); break; } + Value out = hiredis_reply_to_value(r); + freeReplyObject(r); + push_value(vm, out); +#else + Value v1 = pop_value(vm); free_value(v1); + Value v2 = pop_value(vm); free_value(v2); + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/redis/connect.c b/src/vm/redis/connect.c new file mode 100644 index 0000000..1fd0fd3 --- /dev/null +++ b/src/vm/redis/connect.c @@ -0,0 +1,65 @@ +/* + * 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 + */ + +/** + * @file connect.c + * @brief Implements the OP_REDIS_CONNECT opcode (conditional build). + * + * Establishes a synchronous TCP connection to a Redis-compatible server using + * hiredis and registers the connection handle in the internal registry. + * + * Stack effect + * ------------ + * OP_REDIS_CONNECT: (host:string, port:int) -> handle:int (>0) or 0 on error + * + * Behavior + * -------- + * - When FUN_WITH_REDIS is enabled, attempts a blocking connect with a + * 2-second timeout via redisConnectWithTimeout(). On success, the created + * redisContext* is stored in the registry and the assigned positive handle + * id is pushed. On failure, 0 is pushed. + * - When FUN_WITH_REDIS is disabled, arguments are popped and 0 is pushed. + * + * Notes + * ----- + * - The returned handle must be closed with OP_REDIS_CLOSE to release + * resources and delete the registry entry. + */ + +case OP_REDIS_CONNECT: { +#ifdef FUN_WITH_REDIS + Value vport = pop_value(vm); + Value vhost = pop_value(vm); + int port = (int)vport.i; + char *host = value_to_string_alloc(&vhost); + free_value(vport); + free_value(vhost); + if (!host) { push_value(vm, make_int(0)); break; } + struct timeval tv; tv.tv_sec = 2; tv.tv_usec = 0; + redisContext *ctx = redisConnectWithTimeout(host, port, tv); + free(host); + if (!ctx || ctx->err) { + if (ctx) redisFree(ctx); + push_value(vm, make_int(0)); + break; + } + RedisHandle *h = redis_reg_add(ctx); + if (!h) { + redisFree(ctx); + push_value(vm, make_int(0)); + break; + } + push_value(vm, make_int(h->id)); +#else + Value v1 = pop_value(vm); free_value(v1); + Value v2 = pop_value(vm); free_value(v2); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/web/_includes/footer.html b/web/_includes/footer.html index e323926..ea1bfcf 100644 --- a/web/_includes/footer.html +++ b/web/_includes/footer.html @@ -4,7 +4,7 @@

Navigation