1
0
Fork 0
forked from fun/fun

Added support for libSQL as an compatible alternative for SQLite. (0.33.0)

This commit is contained in:
Johannes Findeisen 2025-11-27 00:42:47 +01:00
commit 56712206d1
17 changed files with 418 additions and 5 deletions

2
.gitignore vendored
View file

@ -5,6 +5,7 @@ CMakeCache.txt
.venv
build*
cmake*
database.sqlite
demo_*
dist/
downloaded.png
@ -13,4 +14,3 @@ out/
src/*.o
*.swp
tmp*
todo.sqlite

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(fun VERSION 0.32.0 LANGUAGES C)
project(fun VERSION 0.33.0 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
@ -90,6 +90,40 @@ if(FUN_WITH_JSON)
endif()
endif()
# Optional libsql support (independent from SQLite)
option(FUN_WITH_LIBSQL "Enable libsql (Turso) client support" OFF)
set(LIBSQL_INCLUDE_DIRS "")
set(LIBSQL_LINK_LIBS "")
if(FUN_WITH_LIBSQL)
message(STATUS "Building with libsql support")
add_definitions(-DFUN_WITH_LIBSQL)
# Try pkg-config for libsql first; some systems expose libsql-client
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(LIBSQL QUIET libsql)
if(NOT LIBSQL_FOUND)
pkg_check_modules(LIBSQL_CLIENT QUIET libsql-client)
if(LIBSQL_CLIENT_FOUND)
set(LIBSQL_FOUND TRUE)
set(LIBSQL_INCLUDE_DIRS ${LIBSQL_CLIENT_INCLUDE_DIRS})
set(LIBSQL_LINK_LIBS ${LIBSQL_CLIENT_LIBRARIES})
endif()
endif()
endif()
if(LIBSQL_FOUND)
include_directories(${LIBSQL_INCLUDE_DIRS})
else()
# Fallback: many libsql deployments provide a sqlite3-compatible client lib
# so try linking against sqlite3 as a compatibility layer.
find_library(LIBSQL_LIB sqlite3)
if(LIBSQL_LIB)
list(APPEND LIBSQL_LINK_LIBS ${LIBSQL_LIB})
else()
message(FATAL_ERROR "libsql not found. Install libsql (or compatible sqlite3 client) or disable FUN_WITH_LIBSQL.")
endif()
endif()
endif()
# Optional PCRE2 support
option(FUN_WITH_PCRE2 "Enable PCRE2 (Perl Compatible Regular Expressions) support" OFF)
set(PCRE2_INCLUDE_DIRS "")
@ -201,6 +235,14 @@ if(SQLITE3_LINK_LIBS)
target_link_libraries(fun_core PUBLIC ${SQLITE3_LINK_LIBS})
endif()
# libsql include and link (if enabled)
if(LIBSQL_INCLUDE_DIRS)
target_include_directories(fun_core PRIVATE ${LIBSQL_INCLUDE_DIRS})
endif()
if(LIBSQL_LINK_LIBS)
target_link_libraries(fun_core PUBLIC ${LIBSQL_LINK_LIBS})
endif()
# Link threads if available on UNIX
if(Threads_FOUND)
target_link_libraries(fun_core PUBLIC Threads::Threads)

View file

@ -19,6 +19,7 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht
- [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑
- [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) ☑
- [libSQL](https://github.com/tursodatabase/libsql) support builtin (optional) ☑
- [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑
- [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑
- [SQLite](https://sqlite.org/) support builtin (optional) ☑

View file

@ -90,6 +90,7 @@ 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 support via libcurl (default OFF)
- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF)
- FUN_WITH_LIBSQL=ON|OFF — enable libSQL (Turso) client 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 support (default OFF)
- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default OFF)
@ -117,12 +118,33 @@ cmake -S . -B build -DFUN_WITH_SQLITE=ON
cmake --build build --target fun
# Create the sample database (requires the sqlite3 CLI):
sqlite3 ./todo.sqlite < ./examples/data/todo.sql
sqlite3 ./database.sqlite < ./examples/data/database.sql
# Run the example
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
### Install Fun to the OS (optional)
Not recommended during early development, but supported:

View file

@ -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 <you@hanez.org>
* 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
*/

View file

@ -1,3 +1,5 @@
#!/usr/bin/env fun
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
@ -11,9 +13,9 @@
// Prepare sample DB from SQL if needed (requires sqlite3 CLI installed)
// Create it once with:
// sqlite3 todo.sqlite < ./examples/data/todo.sql
// sqlite3 ./database.sqlite < ./examples/data/database.sql
string db_path = "./todo.sqlite"
string db_path = "./database.sqlite"
number h = sqlite_open(db_path)
if h == 0
@ -35,3 +37,12 @@ rows2 = sqlite_query(h, "SELECT count(*) AS cnt FROM tasks;")
print("Total tasks now: " + to_string(rows2[0]["cnt"]))
sqlite_close(h)
/* Example output:
Tasks (3):
- [✔] (#1) Write Fun + SQLite example 2025-11-26 23:22:04
- [✘] (#2) Ship optional feature flag 2025-11-26 23:22:04
- [✘] (#3) Celebrate with coffee 2025-11-26 23:22:04
Insert rc=0
Total tasks now: 4
*/

View file

@ -152,6 +152,10 @@ static const char *opcode_name(OpCode op) {
case OP_SQLITE_CLOSE: return "SQLITE_CLOSE";
case OP_SQLITE_EXEC: 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: return "PCSC_RELEASE";
case OP_PCSC_LIST_READERS: return "PCSC_LIST_READERS";

View file

@ -158,6 +158,12 @@ typedef enum {
OP_SQLITE_EXEC, // pops sql, handle; pushes sqlite rc (0=OK)
OP_SQLITE_QUERY, // pops sql, handle; pushes array<map>
// 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<map>
// PCSC (smart card) opcodes
OP_PCSC_ESTABLISH, // returns context id (>0) or 0
OP_PCSC_RELEASE, // pops ctx id; returns 1/0

View file

@ -824,6 +824,43 @@ 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)) { parser_fail(*pos, "curl_post expects (url, body)"); free(name); return 0; }

View file

@ -17,6 +17,10 @@
#include <sqlite3.h>
#include "vm/sqlite/common.c"
#endif
#ifdef FUN_WITH_LIBSQL
#include <sqlite3.h> /* libsql exposes sqlite3-compatible C API */
#include "vm/libsql/common.c"
#endif
#include "vm.h"
#include "value.h"
#include <stdio.h>
@ -713,6 +717,12 @@ void vm_run(VM *vm, Bytecode *entry) {
#include "vm/sqlite/exec.c"
#include "vm/sqlite/query.c"
/* libsql ops (independent) */
#include "vm/libsql/open.c"
#include "vm/libsql/close.c"
#include "vm/libsql/exec.c"
#include "vm/libsql/query.c"
/* PCRE2 ops */
#include "vm/pcre2/test.c"
#include "vm/pcre2/match.c"

View file

@ -41,6 +41,7 @@ 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",
"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",
"SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT",

32
src/vm/libsql/close.c Normal file
View file

@ -0,0 +1,32 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*
* Added: 2025-11-26
*/
/**
* OP_LIBSQL_CLOSE: (handle:int) -> Nil
*/
case OP_LIBSQL_CLOSE: {
#ifdef FUN_WITH_LIBSQL
Value vh = pop_value(vm);
int hid = (int)vh.i;
free_value(vh);
LibSqlHandle *h = libsql_reg_get(hid);
if (h && h->db) {
sqlite3_close(h->db);
h->db = NULL;
libsql_reg_del(hid);
}
push_value(vm, make_nil());
#else
Value v = pop_value(vm); free_value(v);
push_value(vm, make_nil());
#endif
break;
}

55
src/vm/libsql/common.c Normal file
View file

@ -0,0 +1,55 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*
* Added: 2025-11-26
*/
#ifdef FUN_WITH_LIBSQL
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sqlite3.h> /* libsql provides a sqlite3-compatible C API */
typedef struct LibSqlHandle {
int id;
sqlite3 *db;
struct LibSqlHandle *next;
} LibSqlHandle;
static LibSqlHandle *g_libsql_handles = NULL;
static int g_libsql_next_id = 1;
static LibSqlHandle *libsql_reg_add(sqlite3 *db) {
LibSqlHandle *h = (LibSqlHandle*)malloc(sizeof(LibSqlHandle));
if (!h) return NULL;
h->id = g_libsql_next_id++;
h->db = db;
h->next = g_libsql_handles;
g_libsql_handles = h;
return h;
}
static LibSqlHandle *libsql_reg_get(int id) {
LibSqlHandle *p = g_libsql_handles;
while (p) { if (p->id == id) return p; p = p->next; }
return NULL;
}
static void libsql_reg_del(int id) {
LibSqlHandle **pp = &g_libsql_handles;
while (*pp) {
if ((*pp)->id == id) {
LibSqlHandle *dead = *pp;
*pp = (*pp)->next;
free(dead);
return;
}
pp = &((*pp)->next);
}
}
#endif /* FUN_WITH_LIBSQL */

36
src/vm/libsql/exec.c Normal file
View file

@ -0,0 +1,36 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*
* Added: 2025-11-26
*/
/**
* OP_LIBSQL_EXEC: (handle:int, sql:string) -> int rc (0=OK)
*/
case OP_LIBSQL_EXEC: {
#ifdef FUN_WITH_LIBSQL
Value vsql = pop_value(vm);
Value vh = pop_value(vm);
int hid = (int)vh.i;
char *sql = value_to_string_alloc(&vsql);
free_value(vh);
free_value(vsql);
LibSqlHandle *h = libsql_reg_get(hid);
if (!h || !h->db || !sql) { if (sql) free(sql); push_value(vm, make_int(SQLITE_MISUSE)); break; }
char *errmsg = NULL;
int rc = sqlite3_exec(h->db, sql, NULL, NULL, &errmsg);
if (errmsg) sqlite3_free(errmsg);
free(sql);
push_value(vm, make_int(rc));
#else
Value v1 = pop_value(vm); free_value(v1);
Value v2 = pop_value(vm); free_value(v2);
push_value(vm, make_int(-1));
#endif
break;
}

37
src/vm/libsql/open.c Normal file
View file

@ -0,0 +1,37 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*
* Added: 2025-11-26
*/
/**
* OP_LIBSQL_OPEN: (url_or_path:string) -> handle:int (>0) or 0 on error
*/
case OP_LIBSQL_OPEN: {
#ifdef FUN_WITH_LIBSQL
Value vpath = pop_value(vm);
char *path = value_to_string_alloc(&vpath);
free_value(vpath);
if (!path) { push_value(vm, make_int(0)); break; }
sqlite3 *db = NULL;
int rc = sqlite3_open(path, &db);
free(path);
if (rc != SQLITE_OK || !db) {
if (db) sqlite3_close(db);
push_value(vm, make_int(0));
break;
}
LibSqlHandle *h = libsql_reg_add(db);
if (!h) { sqlite3_close(db); push_value(vm, make_int(0)); break; }
push_value(vm, make_int(h->id));
#else
Value v = pop_value(vm); free_value(v);
push_value(vm, make_int(0));
#endif
break;
}

60
src/vm/libsql/query.c Normal file
View file

@ -0,0 +1,60 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*
* Added: 2025-11-26
*/
/**
* OP_LIBSQL_QUERY: (handle:int, sql:string) -> array<map<string,any>>
*/
case OP_LIBSQL_QUERY: {
#ifdef FUN_WITH_LIBSQL
Value vsql = pop_value(vm);
Value vh = pop_value(vm);
int hid = (int)vh.i;
char *sql = value_to_string_alloc(&vsql);
free_value(vh);
free_value(vsql);
LibSqlHandle *h = libsql_reg_get(hid);
if (!h || !h->db || !sql) { if (sql) free(sql); push_value(vm, make_array_from_values(NULL, 0)); break; }
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(h->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
free(sql);
push_value(vm, make_array_from_values(NULL, 0));
break;
}
free(sql);
Value rows = make_array_from_values(NULL, 0);
int ncols = sqlite3_column_count(stmt);
while (sqlite3_step(stmt) == SQLITE_ROW) {
Value row = make_map_empty();
for (int i = 0; i < ncols; i++) {
const char *name = sqlite3_column_name(stmt, i);
int type = sqlite3_column_type(stmt, i);
Value kv;
switch (type) {
case SQLITE_INTEGER: kv = make_int((int64_t)sqlite3_column_int64(stmt, i)); break;
case SQLITE_FLOAT: kv = make_float(sqlite3_column_double(stmt, i)); break;
case SQLITE_TEXT: kv = make_string((const char*)sqlite3_column_text(stmt, i)); break;
case SQLITE_NULL: kv = make_nil(); break;
default: kv = make_nil(); break; /* ignore blobs for now */
}
(void)map_set(&row, name ? name : "", kv);
}
(void)array_push(&rows, row);
/* Do NOT free 'row' here; owned by rows array. */
}
sqlite3_finalize(stmt);
push_value(vm, rows);
#else
Value v1 = pop_value(vm); free_value(v1);
Value v2 = pop_value(vm); free_value(v2);
push_value(vm, make_array_from_values(NULL, 0));
#endif
break;
}