From cbb81c0b93ead3280d2e0071934568a1cf805e7f Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 3 Jun 2026 21:48:45 +0200 Subject: [PATCH] Added a Redis/Valkey extension named redis. (0.42.0) --- CMakeLists.txt | 2 +- cmake/Extensions/Extensions.cmake | 2 + cmake/Extensions/REDIS.cmake | 22 ++ cmake/Targets.cmake | 6 +- examples/extensions/redis/README.md | 29 +++ examples/extensions/redis/basic_ping.fun | 26 +++ examples/extensions/redis/hash_ops.fun | 38 ++++ examples/extensions/redis/kv_set_get.fun | 37 ++++ examples/extensions/redis/list_ops.fun | 36 ++++ examples/extensions/redis/redis_test.fun | 28 +++ src/bytecode.h | 5 + src/extensions/redis.c | 221 ++++++++++++++++++++ src/parser.c | 69 ++++++ src/vm.c | 8 + src/vm.h | 2 + src/vm/redis/close.c | 45 ++++ src/vm/redis/cmd.c | 58 +++++ src/vm/redis/connect.c | 65 ++++++ web/_includes/footer.html | 2 +- web/_includes/header.html | 6 +- web/community/community.html | 2 +- web/contact/contact.html | 2 +- web/documentation/build/build.md | 11 +- web/documentation/documentation.md | 2 +- web/documentation/extensions/extensions.md | 1 + web/documentation/extensions/redis/redis.md | 81 +++++++ web/documentation/internals/internals.md | 4 +- web/features/features.md | 3 +- 28 files changed, 798 insertions(+), 15 deletions(-) create mode 100644 cmake/Extensions/REDIS.cmake create mode 100644 examples/extensions/redis/README.md create mode 100755 examples/extensions/redis/basic_ping.fun create mode 100755 examples/extensions/redis/hash_ops.fun create mode 100755 examples/extensions/redis/kv_set_get.fun create mode 100755 examples/extensions/redis/list_ops.fun create mode 100755 examples/extensions/redis/redis_test.fun create mode 100644 src/extensions/redis.c create mode 100644 src/vm/redis/close.c create mode 100644 src/vm/redis/cmd.c create mode 100644 src/vm/redis/connect.c create mode 100644 web/documentation/extensions/redis/redis.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 9936ea2..4d47084 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.41.16 LANGUAGES C) +project(fun VERSION 0.42.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) 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..a975621 --- /dev/null +++ b/cmake/Extensions/REDIS.cmake @@ -0,0 +1,22 @@ +# Redis (hiredis) +option(FUN_WITH_REDIS "Enable Redis (hiredis) support" ON) +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/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/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/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/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