From afb0e8cd3a1be95b25e1531d7d60c09b8097640e Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 00:51:48 +0100 Subject: [PATCH 01/55] Added JSON support. (0.28.0) --- CMakeLists.txt | 31 ++++++++++- README.md | 19 ++++++- docs/handbook.md | 19 ++++++- examples/data/complex.json | 53 ++++++++++++++++++ examples/json_showcase.fun | 65 ++++++++++++++++++++++ lib/io/json.fun | 32 +++++++++++ src/bytecode.c | 4 ++ src/bytecode.h | 6 ++ src/jsonc.c | 111 +++++++++++++++++++++++++++++++++++++ src/parser.c | 39 +++++++++++++ src/vm.c | 7 +++ src/vm.h | 1 + src/vm/json/from_file.c | 30 ++++++++++ src/vm/json/parse.c | 38 +++++++++++++ src/vm/json/stringify.c | 32 +++++++++++ src/vm/json/to_file.c | 37 +++++++++++++ 16 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 examples/data/complex.json create mode 100644 examples/json_showcase.fun create mode 100644 lib/io/json.fun create mode 100644 src/jsonc.c create mode 100644 src/vm/json/from_file.c create mode 100644 src/vm/json/parse.c create mode 100644 src/vm/json/stringify.c create mode 100644 src/vm/json/to_file.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c370c1..815e78c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.27.2 LANGUAGES C) +project(fun VERSION 0.28.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -44,6 +44,27 @@ if(FUN_WITH_PCSC) endif() endif() +# Optional JSON (json-c) support +option(FUN_WITH_JSON "Enable JSON (json-c) support" OFF) +set(JSONC_INCLUDE_DIRS "") +set(JSONC_LINK_LIBS "") +if(FUN_WITH_JSON) + message(STATUS "Building with JSON (json-c) support") + add_definitions(-DFUN_WITH_JSON) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(JSONC QUIET json-c) + endif() + if(JSONC_FOUND) + list(APPEND JSONC_INCLUDE_DIRS ${JSONC_INCLUDE_DIRS} ${JSONC_INCLUDE_DIRS}) + list(APPEND JSONC_LINK_LIBS ${JSONC_LINK_LIBS} ${JSONC_LIBRARIES}) + include_directories(${JSONC_INCLUDE_DIRS}) + else() + # Fallback: try plain -ljson-c + list(APPEND JSONC_LINK_LIBS json-c) + endif() +endif() + # Debug option to enable verbose parser/VM logging option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) @@ -81,6 +102,14 @@ if(PCSC_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${PCSC_LINK_LIBS}) endif() +# json-c include and link (if enabled) +if(JSONC_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${JSONC_INCLUDE_DIRS}) +endif() +if(JSONC_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${JSONC_LINK_LIBS}) +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index ad3cf71..1ca220b 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,18 @@ cd fun Build: ```bash -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON +# Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) +cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON cmake --build build --target fun ``` +CMake options you can toggle (all require NAME=VALUE): + +- FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) +- FUN_WITH_REPL=ON|OFF — enable building the interactive REPL (default ON) +- FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) +- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default ON) + That's it! For testing it, run: ```bash @@ -160,6 +168,15 @@ FUN_LIB_DIR="$(pwd)/lib" ./build/fun But be sure to build Fun with -DFUN_WITH_REPL=ON. +Tip: If you saw an error like this when configuring with CMake: + + CMake Error: Parse error in command line argument: FUN_WITH_JSON + Should be: VAR:type=value + +it means a -D flag was passed without a value. Always specify options as -DNAME=VALUE, for example: + + -DFUN_WITH_JSON=ON + ## Author Johannes Findeisen diff --git a/docs/handbook.md b/docs/handbook.md index bcf974d..103de15 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -46,7 +46,8 @@ cd fun Build: ```bash -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON +# Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) +cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON cmake --build build --target fun ``` @@ -79,6 +80,22 @@ FUN_LIB_DIR="$(pwd)/lib" ./build/fun But be sure to build Fun with -DFUN_WITH_REPL=ON. +#### CMake options + +All CMake options must be passed as -DNAME=VALUE: + +- FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) +- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) +- FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) +- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) + +If you encounter an error such as: + + CMake Error: Parse error in command line argument: FUN_WITH_JSON + Should be: VAR:type=value + +then a -D option was given without a value. Always use -DNAME=VALUE, for example -DFUN_WITH_JSON=ON. + ### Install Fun to OS I do not recommend installing Fun on your system because it is in a very early diff --git a/examples/data/complex.json b/examples/data/complex.json new file mode 100644 index 0000000..36e7920 --- /dev/null +++ b/examples/data/complex.json @@ -0,0 +1,53 @@ +{ + "project": { + "name": "Fun", + "version": "0.27.2", + "website": "https://fun-lang.xyz", + "license": { + "name": "Apache-2.0", + "url": "https://opensource.org/license/apache-2-0" + } + }, + "features": { + "enabled": ["arrays", "maps", "json", "pcsc"], + "experimental": { + "repl": true, + "sockets": true, + "odbc": false, + "notes": null + } + }, + "users": [ + { + "id": 1, + "name": "Ada", + "roles": ["admin", "math"], + "active": true, + "score": 99.5, + "prefs": { + "theme": "dark", + "editor": {"tabWidth": 2, "font": "Fira Code"} + } + }, + { + "id": 2, + "name": "Linus", + "roles": ["user", "kernel"], + "active": false, + "score": 88, + "prefs": { + "theme": "light", + "editor": {"tabWidth": 8, "font": "Monospace"} + } + } + ], + "metrics": { + "counters": [0, 1, 1, 2, 3, 5, 8], + "latency_ms": {"p50": 1.23, "p90": 3.21, "p99": 12.34}, + "builds": 1234567890123456789, + "last_release_ts": 1732406400000 + }, + "matrix": [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + "notes": "UTF-8 ✓ – emojis: 🚀🔥", + "null_field": null +} diff --git a/examples/json_showcase.fun b/examples/json_showcase.fun new file mode 100644 index 0000000..72c46e9 --- /dev/null +++ b/examples/json_showcase.fun @@ -0,0 +1,65 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +// Demonstrates JSON.parse/stringify/from_file/to_file via the stdlib JSON class. + +include + +json = JSON() + +print("-- JSON: parse from string and pretty print --") + +// Build a sample JSON text with most value types +sample = '{"name":"Ada","active":true,"score":99.5,"count":42,"tags":["C","Ada","Math"],"extra":null}' +obj = json.parse(sample) +print("Dump:") +print(obj) +print(obj["name"]) // Ada +print(obj["active"]) // 1 +print(obj["count"]) // 42 +print(len(obj["tags"])) // 3 + +pretty = json.stringify(obj, 1) +print(pretty) + +print("-- JSON: load from file, inspect, and save pretty to /tmp --") + +// Load non existent json file +path = "examples/data/nonexistent.json" +cfg = json.from_file(path) +print("Dump:") +print(cfg) + +// Load a more complex example shipped with the repo +path = "examples/data/complex.json" +cfg = json.from_file(path) +print("Dump:") +print(cfg) + +// Access nested fields +print(cfg["project"]["name"]) // project name +print(cfg["project"]["version"]) // version string +print(len(cfg["users"])) // number of users + +// Derive a small summary map +summary = {} +summary["user_count"] = len(cfg["users"]) +summary["first_user_name"] = cfg["users"][1]["name"] +summary["features_enabled"] = cfg["features"]["enabled"] + +print(json.stringify(summary, 1)) + +// Write the loaded config back as pretty JSON +out_path = "/tmp/fun_complex_out.json" +ok = json.to_file(out_path, cfg, 1) +print(ok) // 1 on success diff --git a/lib/io/json.fun b/lib/io/json.fun new file mode 100644 index 0000000..bb6dc3d --- /dev/null +++ b/lib/io/json.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +// JSON stdlib abstraction wrapping VM json_* builtins defensively. + +class JSON() + fun parse(this, text) + v = json_parse(to_string(text)) + return v + + fun stringify(this, value, pretty) + if pretty == nil + pretty = 0 + return json_stringify(value, pretty) + + fun from_file(this, path) + return json_from_file(to_string(path)) + + fun to_file(this, path, value, pretty) + if pretty == nil + pretty = 0 + return json_to_file(to_string(path), value, pretty) diff --git a/src/bytecode.c b/src/bytecode.c index a9b3924..7350897 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -141,6 +141,10 @@ static const char *opcode_name(OpCode op) { case OP_SHR: return "SHR"; case OP_ROTL: return "ROTL"; case OP_ROTR: return "ROTR"; + case OP_JSON_PARSE: return "JSON_PARSE"; + case OP_JSON_STRINGIFY: return "JSON_STRINGIFY"; + case OP_JSON_FROM_FILE: return "JSON_FROM_FILE"; + case OP_JSON_TO_FILE: return "JSON_TO_FILE"; case OP_PCSC_ESTABLISH: return "PCSC_ESTABLISH"; case OP_PCSC_RELEASE: return "PCSC_RELEASE"; case OP_PCSC_LIST_READERS: return "PCSC_LIST_READERS"; diff --git a/src/bytecode.h b/src/bytecode.h index 172dce1..845241e 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -141,6 +141,12 @@ typedef enum { OP_ROTL, // pops s, a; pushes rotl32(a, s) OP_ROTR, // pops s, a; pushes rotr32(a, s) + // JSON (json-c) + OP_JSON_PARSE, // pops text string; pushes value (or Nil on error) + OP_JSON_STRINGIFY, // pops pretty(bool), value; pushes string + OP_JSON_FROM_FILE, // pops path string; pushes value (or Nil) + OP_JSON_TO_FILE, // pops pretty(bool), value, path; pushes 1/0 + // 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/jsonc.c b/src/jsonc.c new file mode 100644 index 0000000..5cb24c1 --- /dev/null +++ b/src/jsonc.c @@ -0,0 +1,111 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +/* json-c helpers and VM opcode cases (included from vm.c) */ + +#include "value.h" +#include "vm.h" + +#ifdef FUN_WITH_JSON + #include + #include +#endif + +/* --- Conversion helpers between json-c and Fun Value --- */ +#ifdef FUN_WITH_JSON +static Value json_to_fun(json_object *j) { + if (!j) return make_nil(); + enum json_type t = json_object_get_type(j); + switch (t) { + case json_type_null: return make_nil(); + case json_type_boolean: return make_bool(json_object_get_boolean(j)); + case json_type_double: return make_float(json_object_get_double(j)); + case json_type_int: return make_int((int64_t)json_object_get_int64(j)); + case json_type_string: return make_string(json_object_get_string(j)); + case json_type_array: { + size_t n = json_object_array_length(j); + if (n == 0) { + return make_array_from_values(NULL, 0); + } + Value *vals = (Value*)malloc(sizeof(Value) * n); + if (!vals) return make_array_from_values(NULL, 0); + for (size_t i = 0; i < n; ++i) { + json_object *item = json_object_array_get_idx(j, (int)i); + vals[i] = json_to_fun(item); + } + Value arr = make_array_from_values(vals, (int)n); + for (size_t i = 0; i < n; ++i) free_value(vals[i]); + free(vals); + return arr; + } + case json_type_object: { + Value map = make_map_empty(); + json_object_object_foreach(j, key, val) { + (void)map_set(&map, key, json_to_fun(val)); + } + return map; + } + default: + return make_nil(); + } +} + +static json_object* fun_to_json(const Value *v) { + switch (v->type) { + case VAL_NIL: return json_object_new_null(); + case VAL_BOOL: return json_object_new_boolean(v->i ? 1 : 0); + case VAL_INT: return json_object_new_int64(v->i); + case VAL_FLOAT: return json_object_new_double(v->d); + case VAL_STRING: return json_object_new_string(v->s ? v->s : ""); + case VAL_ARRAY: { + json_object *arr = json_object_new_array(); + int n = array_length(v); + for (int i = 0; i < n; ++i) { + Value item; + if (array_get_copy(v, i, &item)) { + json_object_array_add(arr, fun_to_json(&item)); + free_value(item); + } else { + json_object_array_add(arr, json_object_new_null()); + } + } + return arr; + } + case VAL_MAP: { + json_object *obj = json_object_new_object(); + /* We don't have an iterator API; use keys() helper */ + Value keys = map_keys_array(v); + int kn = array_length(&keys); + for (int i = 0; i < kn; ++i) { + Value k; + if (!array_get_copy(&keys, i, &k)) continue; + if (k.type == VAL_STRING && k.s) { + Value val; + if (map_get_copy(v, k.s, &val)) { + json_object_object_add(obj, k.s, fun_to_json(&val)); + free_value(val); + } else { + json_object_object_add(obj, k.s, json_object_new_null()); + } + } + free_value(k); + } + free_value(keys); + return obj; + } + default: + /* Fallback: stringify unsupported types */ + return json_object_new_string(""); + } +} +#endif /* FUN_WITH_JSON */ + +/* Note: The VM opcode case handlers are included from vm/vm switch via vm/json/ops.c */ diff --git a/src/parser.c b/src/parser.c index 14015c5..97c27ec 100644 --- a/src/parser.c +++ b/src/parser.c @@ -739,6 +739,45 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* JSON builtins */ + if (strcmp(name, "json_parse") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_parse expects (text)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after json_parse arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_JSON_PARSE, 0); + free(name); + return 1; + } + if (strcmp(name, "json_stringify") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_stringify expects (value, pretty)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "json_stringify expects (value, pretty)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_stringify expects (value, pretty)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after json_stringify args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_JSON_STRINGIFY, 0); + free(name); + return 1; + } + if (strcmp(name, "json_from_file") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_from_file expects (path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after json_from_file arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_JSON_FROM_FILE, 0); + free(name); + return 1; + } + if (strcmp(name, "json_to_file") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_to_file expects (path, value, pretty)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "json_to_file expects (path, value, pretty)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_to_file expects (path, value, pretty)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "json_to_file expects (path, value, pretty)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_to_file expects (path, value, pretty)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after json_to_file args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_JSON_TO_FILE, 0); + free(name); + return 1; + } /* PCSC builtins */ if (strcmp(name, "pcsc_establish") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index faa2a80..7093c4d 100644 --- a/src/vm.c +++ b/src/vm.c @@ -12,6 +12,7 @@ #include "map.c" #include "string.c" #include "pcsc.c" +#include "jsonc.c" #include "vm.h" #include "value.h" #include @@ -660,6 +661,12 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/pcsc/disconnect.c" #include "vm/pcsc/transmit.c" + /* JSON ops (implemented in jsonc.c, included above) */ + #include "vm/json/parse.c" + #include "vm/json/stringify.c" + #include "vm/json/from_file.c" + #include "vm/json/to_file.c" + #include "vm/strings/find.c" #include "vm/strings/regex_match.c" #include "vm/strings/regex_search.c" diff --git a/src/vm.h b/src/vm.h index 9f393d6..8863bcc 100644 --- a/src/vm.h +++ b/src/vm.h @@ -38,6 +38,7 @@ static const char *opcode_names[] = { "TIME_NOW_MS","CLOCK_MONO_MS","DATE_FORMAT", "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", + "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", "PCSC_ESTABLISH","PCSC_RELEASE","PCSC_LIST_READERS","PCSC_CONNECT","PCSC_DISCONNECT","PCSC_TRANSMIT", "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT", "EXIT" diff --git a/src/vm/json/from_file.c b/src/vm/json/from_file.c new file mode 100644 index 0000000..370bec0 --- /dev/null +++ b/src/vm/json/from_file.c @@ -0,0 +1,30 @@ +/** +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +/* JSON_FROM_FILE */ +case OP_JSON_FROM_FILE: { +#ifdef FUN_WITH_JSON + Value vpath = pop_value(vm); + char *path = value_to_string_alloc(&vpath); + free_value(vpath); + if (!path) { push_value(vm, make_nil()); break; } + json_object *root = json_object_from_file(path); + free(path); + if (!root) { push_value(vm, make_nil()); break; } + Value v = json_to_fun(root); + push_value(vm, v); + json_object_put(root); +#else + Value vpath = pop_value(vm); free_value(vpath); + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/json/parse.c b/src/vm/json/parse.c new file mode 100644 index 0000000..90cc5cc --- /dev/null +++ b/src/vm/json/parse.c @@ -0,0 +1,38 @@ +/** +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +/* JSON_PARSE */ +case OP_JSON_PARSE: { +#ifdef FUN_WITH_JSON + Value text = pop_value(vm); + char *s = value_to_string_alloc(&text); + free_value(text); + if (!s) { push_value(vm, make_nil()); break; } + struct json_tokener *tok = json_tokener_new(); + json_object *root = json_tokener_parse_ex(tok, s, (int)strlen(s)); + enum json_tokener_error jerr = json_tokener_get_error(tok); + json_tokener_free(tok); + free(s); + if (jerr != json_tokener_success) { + push_value(vm, make_nil()); + } else { + Value v = json_to_fun(root); + push_value(vm, v); + json_object_put(root); + } +#else + /* Fallback when JSON is disabled: consume arg, push Nil */ + Value drop = pop_value(vm); + free_value(drop); + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/json/stringify.c b/src/vm/json/stringify.c new file mode 100644 index 0000000..1c5a45b --- /dev/null +++ b/src/vm/json/stringify.c @@ -0,0 +1,32 @@ +/** +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +/* JSON_STRINGIFY */ +case OP_JSON_STRINGIFY: { +#ifdef FUN_WITH_JSON + Value vpretty = pop_value(vm); + Value any = pop_value(vm); + int pretty = (vpretty.type == VAL_BOOL || vpretty.type == VAL_INT) ? (vpretty.i != 0) : 0; + json_object *j = fun_to_json(&any); + int flags = pretty ? JSON_C_TO_STRING_PRETTY : JSON_C_TO_STRING_PLAIN; + const char *js = json_object_to_json_string_ext(j, flags); + push_value(vm, make_string(js ? js : "")); + json_object_put(j); + free_value(vpretty); + free_value(any); +#else + /* Fallback: consume two args, push "null" */ + Value vpretty = pop_value(vm); free_value(vpretty); + Value any = pop_value(vm); free_value(any); + push_value(vm, make_string("null")); +#endif + break; +} diff --git a/src/vm/json/to_file.c b/src/vm/json/to_file.c new file mode 100644 index 0000000..d0a9857 --- /dev/null +++ b/src/vm/json/to_file.c @@ -0,0 +1,37 @@ +/** +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-24 + */ + +/* JSON_TO_FILE */ +case OP_JSON_TO_FILE: { +#ifdef FUN_WITH_JSON + Value vpretty = pop_value(vm); + Value any = pop_value(vm); + Value vpath = pop_value(vm); + char *path = value_to_string_alloc(&vpath); + int pretty = (vpretty.type == VAL_BOOL || vpretty.type == VAL_INT) ? (vpretty.i != 0) : 0; + free_value(vpretty); + free_value(vpath); + if (!path) { free_value(any); push_value(vm, make_int(0)); break; } + json_object *j = fun_to_json(&any); + int flags = pretty ? JSON_C_TO_STRING_PRETTY : JSON_C_TO_STRING_PLAIN; + int rc = json_object_to_file_ext(path, j, flags); + json_object_put(j); + free(path); + free_value(any); + push_value(vm, make_int(rc == 0 ? 1 : 0)); +#else + Value vpretty = pop_value(vm); free_value(vpretty); + Value any = pop_value(vm); free_value(any); + Value vpath = pop_value(vm); free_value(vpath); + push_value(vm, make_int(0)); +#endif + break; +} From d028434406a3af7d703f14103a1d4eaaafe931ee Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 02:07:16 +0100 Subject: [PATCH 02/55] Small fix.# (0.28.0) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ca220b..f60e296 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ CMake options you can toggle (all require NAME=VALUE): - FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) - FUN_WITH_REPL=ON|OFF — enable building the interactive REPL (default ON) - FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) -- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default ON) +- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) That's it! For testing it, run: From 9c098eb92b9304436097283705b7094123dfa691 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 02:17:06 +0100 Subject: [PATCH 03/55] handbook update... --- docs/handbook.md | 373 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) diff --git a/docs/handbook.md b/docs/handbook.md index 103de15..91c0f53 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -115,3 +115,376 @@ system default lib directory (/usr/share/fun/lib). fun ./demo.fun ``` + +## Table of contents + +- Language overview and VM internals +- Command line interface and REPL +- Core types and operations +- Built-ins overview (what the VM provides) +- Standard library APIs + - io.console + - io.process + - io.socket + - io.thread + - utils.datetime + - regex + - crypt (MD5, SHA-1/256/384/512) + - encoding.base64 + - arrays, strings, maps helpers (hex, range) +- Extra libraries + - JSON (via json-c) + - PCSC (PC/SC smart card) +- Examples reference (what each example does) + +--- + +## Language overview and VM internals + +Fun is a small imperative language executed by a register-less stack-based virtual machine (VM). Source files are compiled to bytecode; the VM executes opcodes that work on a value stack. Functions and methods push/pop their arguments and return values on that stack. + +High-level architecture: +- Front-end: parses .fun files, handles includes and constant folding, emits bytecode with debug markers (OP_LINE) for tracing and REPL-on-error. +- VM core: runs a loop over opcodes (see src/bytecode.h). Values include numbers (integers), strings, arrays, maps, booleans (0/1), functions, and nil. +- Built-ins: I/O, strings, arrays, regex, date/time, OS, networking, threading, optional JSON and PC/SC. Many are exposed as opcodes with friendly global functions in the language. + +Key VM concepts (non-exhaustive): +- Control flow: OP_JUMP, OP_JUMP_IF_FALSE, OP_RETURN. +- Arithmetic and logic: OP_ADD/SUB/MUL/DIV, OP_MOD, comparisons (OP_LT, OP_LTE, OP_GT, OP_GTE, OP_EQ, OP_NEQ), logical OP_AND/OR/NOT. +- Stack helpers: OP_DUP, OP_SWAP, OP_POP. +- Arrays: OP_MAKE_ARRAY, OP_INDEX_GET/SET, OP_LEN, OP_PUSH, OP_APOP (pop last), OP_INSERT/REMOVE, OP_SLICE. +- Strings: OP_SUBSTR, OP_SPLIT, OP_JOIN, OP_FIND. +- Maps: OP_MAKE_MAP and index ops reuse array/map machinery; you can index with string keys. +- Conversion and typing: OP_TO_NUMBER, OP_TO_STRING, OP_CAST, OP_TYPEOF, unsigned/signed clamps (OP_UCLAMP/OP_SCLAMP). +- Regex: OP_REGEX_MATCH/SEARCH/REPLACE. +- Math misc: OP_MIN/MAX/CLAMP/ABS/POW/RANDOM_SEED/RANDOM_INT. +- Iteration helpers: OP_ENUMERATE, OP_ZIP. +- OS/IO/network: socket ops, file ops, process execution, environment, threads, etc., implemented as built-ins. +- Optional features: JSON opcodes (OP_JSON_PARSE and friends in src/vm/json/*) are compiled in only if -DFUN_WITH_JSON=ON. PCSC opcodes are available if -DFUN_WITH_PCSC=ON. + +Error handling and debugging: +- Build with FUN_DEBUG=ON for verbose VM traces. +- Run with --trace to print executed lines and opcodes. +- Run with --repl-on-error to drop into an interactive REPL when a runtime error occurs, allowing inspection of variables and stepping. + +## Command line interface and REPL + +Running a script: +- fun path/to/script.fun +- Options: --trace, --repl-on-error (can combine), see build section for REPL availability. + +REPL: +- Launch with fun (no script) when built with FUN_WITH_REPL=ON. +- In trace/REPL-on-error mode, the VM annotates output with file:line and function names to aid debugging (see examples/debug_reporting.fun and examples/repl_on_error.fun). + +## Core types and operations + +Types: +- number: signed integer. Conversions: to_number(x). Bitwise ops exist via bnot, band, bor, bxor, shl, shr, rol, ror in stdlib/VM. +- string: immutable sequence of bytes; length via len(s); concatenate via join([a,b], ""). Substring: substr(s, start, len). Find: find(haystack, needle) returns index or -1. +- array: ordered list. Create with [a, b, c] or built-ins. len(a), push(a, v) appends, apop(a) removes last, insert(a, idx, v), remove(a, idx), slice(a, start, end). +- map: associative dictionary with string keys typically: m = {}; m["key"] = value; keys can be strings and sometimes numbers. +- boolean: represented as number 1 (true) or 0 (false). Logical operators: &&, ||, !. +- nil: absence of value. Many defensive stdlib wrappers return [] or {} or nil defaults on errors. + +Control flow: +- if/else, while loops, for-like range utilities (see utils.range in stdlib), break/continue (see examples/loops_break_continue.fun). + +Functions and classes: +- Define a function with fun name(args) ... +- Define a class with class Name(constructor params) and methods fun method(this, ...) ...; _construct is called as a constructor if present. +- Methods use explicit this. + +Modules and includes: +- Use #include to include from FUN_LIB_DIR. +- Use #include "relative/path.fun" to include a file relative to your script. +- You can alias includes with "as" to create namespaces: #include as m; then call m.add(...). + +## Built-ins overview + +Console and I/O: +- print(x): prints a value with a trailing newline. input(prompt): returns a line as string without trailing newline. + +Strings and arrays: +- len(x), join(array, sep), split(string, sep), substr(string, start, len), find(haystack, needle), push(array, value), apop(array), insert(array, idx, value), remove(array, idx), slice(array, start, end). + +Conversion and type: +- to_number(x), to_string(x), cast(value, typeName), typeof(x), uclamp(number, bits), sclamp(number, bits). + +Math and random: +- min(a,b), max(a,b), clamp(x, lo, hi), abs(x), pow(a,b), random_seed(seed), random_int(lo, hiExclusive). + +Regex: +- regex_match(text, pattern) -> 1/0 full match +- regex_search(text, pattern) -> map {"match", "start", "end", "groups"} +- regex_replace(text, pattern, repl) -> string with global replacements + +OS and processes: +- proc_run(cmd) -> map {"out": string, "code": number} +- system(cmd) -> exit code number +- env_get(name)/env_set(name, value) – see examples/os_env.fun + +Networking and sockets: +- tcp_connect(host, port) -> fd (>0) or 0 +- sock_send(fd, string) -> bytes sent or -1, sock_recv(fd, maxlen) -> string, sock_close(fd) +- tcp_listen(port, backlog) -> listen fd, tcp_accept(listenFd) -> client fd +- unix_connect(path) -> fd for UNIX domain sockets + +Threads: +- thread_spawn(func, args) -> thread id; thread_join(id) -> return value + +Date and time: +- time_now_ms() -> epoch ms; clock_mono_ms() -> monotonic ms; date_format(ms, fmt) -> string + +JSON (optional): +- json_parse(text) -> Fun value (maps/arrays/numbers/strings/1/nil) +- json_stringify(value, prettyFlag) -> string; prettyFlag: 0/1 +- json_from_file(path) -> value or nil; json_to_file(path, value, prettyFlag) -> 1/0 + +PC/SC (optional): +- pcsc_establish() -> context id (>0) or 0 +- pcsc_list_readers(ctx) -> array of reader names (strings) or nil +- pcsc_connect(ctx, readerName) -> handle id (>0) or 0 +- pcsc_disconnect(handle) -> 1/0 +- pcsc_transmit(handle, bytesArray) -> map {"data": array of numbers, "sw1": n, "sw2": n, "code": n} + +Note: Optional feature availability depends on your CMake flags at build time. + +--- + +## Standard library APIs + +The stdlib provides small, defensive wrappers around VM built-ins, typically with class-based APIs to avoid global name collisions and to offer sensible defaults. + +### io.console + +Class Console (lib/io/console.fun): +- prompt(text) -> string: print text and read a line. +- ask(question) -> string: prints "question: " and reads a line. +- ask_yes_no(question) -> 1/0: loops until user answers y/yes or n/no (case-insensitive). + +Example: +- See examples/input_example.fun + +### io.process + +Class Process (lib/io/process.fun): +- run(cmd) -> { out, code }: captures stdout and exit code. +- run_merge_stderr(cmd) -> { out, code }: appends "2>&1" to merge stderr. +- system(cmd) -> number: exit code. +- check_call(cmd) -> 1/0: 1 if exit code is 0. + +Examples: +- examples/process_example.fun + +### io.socket + +Provides TcpClient, TcpServer, UnixClient (lib/io/socket.fun). + +Class TcpClient: +- connect(host, port) -> 1/0 +- is_connected() -> 1/0 +- send(data) -> bytes or -1 +- recv(maxlen) -> string +- recv_all(chunk_size) -> string: keeps reading until EOF or partial chunk. +- close() -> 1 + +Class TcpServer(port, backlog): +- listen() -> listen fd or 0 +- accept() -> client fd +- echo_once(maxlen) -> 1 on handled client +- serve_forever(maxlen) -> never returns; minimal echo server +- close() + +Class UnixClient: +- connect(path), is_connected(), send(data), recv(maxlen), close() + +Examples: +- examples/tcp_http_get.fun, examples/tcp_http_get_class.fun, examples/unix_socket_echo.fun, examples/extra/tcp_echo_server_class.fun + +### io.thread + +Class Thread (lib/io/thread.fun): +- spawn(func, args) -> thread id; join(id) -> return value +- Aliases: start(func, args), wait(id) + +Examples: +- examples/threads_demo.fun, examples/thread_class_example.fun + +### utils.datetime + +Class DateTime (lib/utils/datetime.fun): +- now_ms() -> current epoch milliseconds +- mono_ms() -> monotonic clock ms +- format(ms, fmt) -> string using strftime-like fmt +- iso_now() -> "YYYY-MM-DDTHH:MM:SS" + +Example: +- examples/datetime_basic.fun + +### regex + +Class Regex (lib/regex.fun): +- match(text, pattern) -> 1/0 full match +- search(text, pattern) -> map { match, start, end, groups } +- replace(text, pattern, repl) -> string (global) + +Examples: +- examples/regex_demo.fun, examples/regex_procedural.fun + +### crypt + +MD5 (lib/crypt/md5.fun): Pure Fun implementation with class MD5 and helper md5_hex(hexStr). See examples/md5_demo.fun. + +SHA family (lib/crypt/sha1.fun, sha256.fun, sha384.fun, sha512.fun): class wrappers SHA1/SHA256/SHA384/SHA512 with methods digest_hex_of_string(str) and helpers as documented in files. Examples: sha1_demo.fun, sha256_demo.fun, sha256_str_demo.fun, sha384_example.fun, sha512_demo.fun, sha512_str_demo.fun. + +### encoding.base64 + +Module lib/encoding/base64.fun provides base64_encode(string) and base64_decode(string) helpers (see file for exact APIs). Used in some examples. + +### arrays, strings, maps helpers + +- lib/arrays.fun: helper functions for common array patterns. +- lib/strings.fun: string helpers like str_to_lower/upper and more; used by several stdlib modules. +- lib/hex.fun: bytes_to_hex(arrayOfNumbers) and hex_to_bytes(hexString) helpers as used by PCSC. +- lib/utils/range.fun: utilities for building numeric ranges; see for_range_test.fun. +- lib/utils/math.fun and lib/math.fun: higher-level math helpers. + +--- + +## Extra libraries + +### JSON (optional) + +Build flag: -DFUN_WITH_JSON=ON. Requires json-c available on your system. Internals are in src/vm/json/ and wrap json-c to convert between json_object and Fun values. + +VM functions: +- json_parse(text) -> value or nil on parse error. +- json_stringify(value, pretty) -> string; pretty is 0/1. +- json_from_file(path) -> value or nil if file missing/unreadable. +- json_to_file(path, value, pretty) -> 1 on success else 0. + +Stdlib wrapper class JSON (lib/io/json.fun): +- parse(text) +- stringify(value, pretty=0) +- from_file(path) +- to_file(path, value, pretty=0) + +Example walkthrough (examples/json_showcase.fun): +- Parses a JSON string into a map/array structure; demonstrates indexing (obj["name"]). +- Pretty prints the object with json.stringify(obj, 1). +- Attempts to read a non-existent file to show defensive behavior. +- Loads examples/data/complex.json, accesses nested fields, constructs a summary map, and writes pretty JSON to /tmp. + +### PCSC (optional) + +Build flag: -DFUN_WITH_PCSC=ON. Requires PC/SC (e.g., pcsc-lite on Unix) and a reader. VM opcodes are wrapped by global functions as listed under Built-ins. + +Stdlib wrapper class PCSC (lib/io/pcsc.fun): +- get_readers() -> array of reader names. +- transmit(hex_apdu) -> map result by establishing context, selecting a reader, connecting, transmitting, and disconnecting. It returns a map with keys data (array), sw1, sw2, code. The wrapper includes defensive defaults when no reader exists. +- There is also a commented-out full-featured variant exposing establish/release/connect/disconnect/transmit_bytes/transmit_hex for advanced use. + +Example: +- examples/pcsc_example.fun: shows establishing and transmitting an APDU, or printing []/default map if no readers present. + +--- + +## Examples reference + +You can run examples without installing by pointing FUN_LIB_DIR to the repository lib directory: + + FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/.fun + +Below is a catalog of the examples folder with brief explanations of what happens in each file: + +- arrays.fun — Basic array creation, indexing, push/apop, insert/remove, slice; prints intermediate states and lengths. +- arrays_advanced.fun — More complex array transformations, enumerate/zip patterns. +- arrays_iter.fun — Iterating arrays with indices and values; demonstrates for/while patterns. +- boolean_decl.fun — Declaring and using booleans, truthy/falsey checks. +- booleans.fun — First-class booleans with logical operators and short-circuit behavior. +- builtins_conversions.fun — Using to_number, to_string, cast, typeof, uclamp/sclamp. +- builtins_extended.fun — Showcases extended built-ins like min/max/clamp/abs/pow/random. +- builtins_maps_and_more.fun — Demonstrates map creation, assignment, and index operations. +- byte_for_demo.fun — Demonstrates bitwise ops (bnot/band/bor/bxor/shl/shr/rol/ror) and numeric behavior. +- cast_demo.fun — Casting values and type checking via typeof and cast. +- class_constructor.fun — Using _construct in classes and field initialization. +- classes_demo.fun — Class definition, methods, and instances interacting. +- datetime_basic.fun — Uses utils.datetime to print now_ms, mono_ms, and formatted timestamps. +- debug_reporting.fun — Shows how --repl-on-error and trace annotate crashes with file:line and stack info. +- exit_example.fun — Demonstrates exiting a program early and exit codes. +- expressions_test.fun — Demonstrates operator precedence and expression evaluations. +- fail.fun — Purposefully triggers an error to see runtime behavior. +- file_io.fun — Reading/writing files with built-ins; prints file contents. +- file_print_for_file_line_by_line.fun — Iterates through file lines, printing them. +- floats.fun — Demonstrates float-like operations if represented via numbers; shows division behavior. +- for_range_test.fun — Uses utils.range to iterate over numeric ranges. +- functions_test.fun — Function definitions, higher-order usage, and composition. +- have_fun.fun — A fun greeting and minimal example to verify environment. +- have_fun_function.fun — Extracted function used by have_fun.fun. +- if_else_test.fun — Conditional branching and nesting. +- include_lib.fun — Using #include <...> from FUN_LIB_DIR. +- include_local.fun — Using #include "..." relative path includes and shared helpers. +- include_namespace.fun — Namespaced includes with "as" and usage examples. +- inheritance_demo.fun — Class inheritance patterns and method overriding. +- input_example.fun — Reading from stdin using Console.ask/prompt. +- json_showcase.fun — Comprehensive demo of JSON.parse/stringify/from_file/to_file; prints nested values and writes to /tmp. +- loops_break_continue.fun — Shows break and continue in loops and their effects on control flow. +- md5_demo.fun — Hashing data using lib/crypt/md5.fun and printing the digest. +- namespaced_mod.fun — Module used by include_namespace.fun to demonstrate namespacing. +- nested_loops.fun — Nested iteration and control flow. +- objects_basic.fun — Creating and manipulating maps as objects with fields. +- objects_more.fun — More advanced object/map patterns. +- os_env.fun — Getting/setting environment variables. +- pcsc_example.fun — Establishing PC/SC context, listing readers, transmitting a sample APDU if hardware present. +- process_example.fun — Running external commands with Process.run/system and handling exit codes. +- regex_demo.fun — Using Regex class for match/search/replace; prints results and groups. +- regex_procedural.fun — Direct usage of regex_* built-ins without the class wrapper. +- repl_on_error.fun — Forces an error to enter REPL when run with --repl-on-error. +- sha1_demo.fun — Hashing using SHA1 helper; prints digest. +- sha256_demo.fun — SHA-256 hashing demonstration over file/string inputs. +- sha256_str_demo.fun — String-only SHA-256 hashing convenience. +- sha384_example.fun — SHA-384 hashing demonstration. +- sha512_demo.fun — SHA-512 hashing demonstration over data; prints digest. +- sha512_str_demo.fun — String-only SHA-512 hashing convenience. +- short_circuit_test.fun — Demonstrates && and || short-circuit semantics. +- signed_ints.fun — Two's complement wrapping and signed integer behavior. +- stdlib_showcase.fun — A tour of several stdlib modules in one file. +- strings_test.fun — String slicing, joining, splitting, find, and case transforms. +- tcp_http_get.fun — Minimal HTTP GET over TCP using built-ins; prints the response. +- tcp_http_get_class.fun — Same as above using the TcpClient class. +- thread_class_example.fun — Spawning and joining threads via the Thread class methods. +- threads_demo.fun — Multiple threads and returning values with thread_join. +- try_catch_finally.fun — Error handling with try/catch/finally constructs. +- try_catch_with_error.fun — Catching and inspecting errors thrown inside code. +- typeof_features.fun — Shows typeof on many values and casting behavior. +- typeof.fun — Basic typeof usage. +- type_safety_fails.fun — Examples that should fail type safety checks at runtime. +- type_safety.fun — Properly typed examples that run without errors. +- types_integers.fun — Integer type features, comparisons, and arithmetic. +- types_overview.fun — Overview of values and literal syntax. +- uint_types.fun — Unsigned integer helpers and clamping. +- unix_socket_echo.fun — UNIX domain socket echo client/server demo. +- while_test.fun — While loops, counters, and loop termination conditions. + +Notes: +- Some examples are platform-dependent (PCSC, UNIX sockets) or rely on optional features (JSON). They degrade gracefully when unavailable, printing empty arrays or default maps. + +--- + +## Internals notes for JSON + +Fun wraps json-c. See src/vm/json/parse.c, stringify.c, from_file.c, to_file.c. For parsing, OP_JSON_PARSE converts the input string into a json_object using a tokener and then converts to Fun values via json_to_fun. On error or when JSON is compiled out, the VM returns nil. The stdlib JSON class converts arguments defensively (to_string) and provides default pretty=0. + +## Internals notes for PCSC + +The PCSC functions in the VM interface with pcsc-lite/WinSCard. Transmit returns a map with raw data bytes and status words (sw1, sw2) and a code field. The stdlib wrapper in lib/io/pcsc.fun demonstrates defensive patterns: when no readers are found, it returns a default map so that indexing like res["sw1"] is always safe. + +--- + +## Contributing and further reading + +- Browse lib/ for up-to-date stdlib APIs; many files document their own public interfaces in comments at the top. +- src/bytecode.h lists all opcodes supported by the VM. The corresponding implementations live under src/vm/. +- examples/ are the best starting point to learn by doing. + From 56e3e22f06ef51af97da66f2cf2186f785c7f6fa Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 15:06:37 +0100 Subject: [PATCH 04/55] README update. No code changes(0.28.0) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f60e296..b1b9d38 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht ### Extras -- [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional, planned, maybe write a parser in Fun for stdlib) -- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional, planned) -- [PC/SC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional, in progress) -- [SQLite](https://sqlite.org/) support builtin (optional, planned) -- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional, planned) +- [JSON](https://www.json.org/){:class="ext"} support builtin using [json-c](https://github.com/json-c/json-c){:class="ext"} (optional) +- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16){:class="ext"} support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/){:class="ext"} (optional) +- [PC/SC](https://pcscworkgroup.com/){:class="ext"} smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/){:class="ext"} (optional) +- [SQLite](https://sqlite.org/){:class="ext"} support builtin (optional) +- [Tk](https://www.tcl-lang.org/){:class="ext"} support builtin for GUI application development (optional) ## Characteristics From 685c84c07711fa61dbcc5949f3a09d30b851b4ff Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 15:07:09 +0100 Subject: [PATCH 05/55] README update. No code changes(0.28.0) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b1b9d38..016c110 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht ### Extras -- [JSON](https://www.json.org/){:class="ext"} support builtin using [json-c](https://github.com/json-c/json-c){:class="ext"} (optional) -- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16){:class="ext"} support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/){:class="ext"} (optional) -- [PC/SC](https://pcscworkgroup.com/){:class="ext"} smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/){:class="ext"} (optional) -- [SQLite](https://sqlite.org/){:class="ext"} support builtin (optional) -- [Tk](https://www.tcl-lang.org/){:class="ext"} support builtin for GUI application development (optional) +- [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) +- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) +- [PC/SC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) +- [SQLite](https://sqlite.org/) support builtin (optional) +- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ## Characteristics From 2312b9d0c3c421370dcc556e2ae1d6e5cf7c63f3 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 15:14:21 +0100 Subject: [PATCH 06/55] README update. No code changes (0.28.0) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 016c110..e904350 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - [SQLite](https://sqlite.org/) support builtin (optional) - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) + = Done / = Planned or in progress. + ## Characteristics - Dynamic and optionally statically typed From 786f1ddc0607a6a3fb631ad8d4ac904276c9e04d Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 16:21:24 +0100 Subject: [PATCH 07/55] README update. No code changes (0.28.0) --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e904350..4d67669 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht ### Extras -- [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) -- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) -- [PC/SC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) -- [SQLite](https://sqlite.org/) support builtin (optional) -- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) +- [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) ☑ +- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) ☐ +- [PC/SC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ +- [SQLite](https://sqlite.org/) support builtin (optional) ☐ +- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ - = Done / = Planned or in progress. +☑ = Done / ☐ = Planned or in progress. ## Characteristics From d5c7b91eb3f03fc0b29351aebf3ed512879f1e22 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 20:21:22 +0100 Subject: [PATCH 08/55] Added SEMANTIC_VERSIONING_2_0_0.md. No code changes (0.28.0) --- SEMANTIC_VERSIONING_2_0_0.md | 373 +++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 SEMANTIC_VERSIONING_2_0_0.md diff --git a/SEMANTIC_VERSIONING_2_0_0.md b/SEMANTIC_VERSIONING_2_0_0.md new file mode 100644 index 0000000..fb25b8e --- /dev/null +++ b/SEMANTIC_VERSIONING_2_0_0.md @@ -0,0 +1,373 @@ +Semantic Versioning 2.0.0 +============================== + +Summary +------- + +Given a version number MAJOR.MINOR.PATCH, increment the: + +1. MAJOR version when you make incompatible API changes +1. MINOR version when you add functionality in a backward compatible + manner +1. PATCH version when you make backward compatible bug fixes + +Additional labels for pre-release and build metadata are available as extensions +to the MAJOR.MINOR.PATCH format. + +Introduction +------------ + +In the world of software management there exists a dreaded place called +"dependency hell." The bigger your system grows and the more packages you +integrate into your software, the more likely you are to find yourself, one +day, in this pit of despair. + +In systems with many dependencies, releasing new package versions can quickly +become a nightmare. If the dependency specifications are too tight, you are in +danger of version lock (the inability to upgrade a package without having to +release new versions of every dependent package). If dependencies are +specified too loosely, you will inevitably be bitten by version promiscuity +(assuming compatibility with more future versions than is reasonable). +Dependency hell is where you are when version lock and/or version promiscuity +prevent you from easily and safely moving your project forward. + +As a solution to this problem, we propose a simple set of rules and +requirements that dictate how version numbers are assigned and incremented. +These rules are based on but not necessarily limited to pre-existing +widespread common practices in use in both closed and open-source software. +For this system to work, you first need to declare a public API. This may +consist of documentation or be enforced by the code itself. Regardless, it is +important that this API be clear and precise. Once you identify your public +API, you communicate changes to it with specific increments to your version +number. Consider a version format of X.Y.Z (Major.Minor.Patch). Bug fixes not +affecting the API increment the patch version, backward compatible API +additions/changes increment the minor version, and backward incompatible API +changes increment the major version. + +We call this system "Semantic Versioning." Under this scheme, version numbers +and the way they change convey meaning about the underlying code and what has +been modified from one version to the next. + +Semantic Versioning Specification (SemVer) +------------------------------------------ + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +1. Software using Semantic Versioning MUST declare a public API. This API +could be declared in the code itself or exist strictly in documentation. +However it is done, it SHOULD be precise and comprehensive. + +1. A normal version number MUST take the form X.Y.Z where X, Y, and Z are +non-negative integers, and MUST NOT contain leading zeroes. X is the +major version, Y is the minor version, and Z is the patch version. +Each element MUST increase numerically. For instance: 1.9.0 -> 1.10.0 -> 1.11.0. + +1. Once a versioned package has been released, the contents of that version +MUST NOT be modified. Any modifications MUST be released as a new version. + +1. Major version zero (0.y.z) is for initial development. Anything MAY change +at any time. The public API SHOULD NOT be considered stable. + +1. Version 1.0.0 defines the public API. The way in which the version number +is incremented after this release is dependent on this public API and how it +changes. + +1. Patch version Z (x.y.Z | x > 0) MUST be incremented if only backward +compatible bug fixes are introduced. A bug fix is defined as an internal +change that fixes incorrect behavior. + +1. Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backward +compatible functionality is introduced to the public API. It MUST be +incremented if any public API functionality is marked as deprecated. It MAY be +incremented if substantial new functionality or improvements are introduced +within the private code. It MAY include patch level changes. Patch version +MUST be reset to 0 when minor version is incremented. + +1. Major version X (X.y.z | X > 0) MUST be incremented if any backward +incompatible changes are introduced to the public API. It MAY also include minor +and patch level changes. Patch and minor versions MUST be reset to 0 when major +version is incremented. + +1. A pre-release version MAY be denoted by appending a hyphen and a +series of dot separated identifiers immediately following the patch +version. Identifiers MUST comprise only ASCII alphanumerics and hyphens +[0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST +NOT include leading zeroes. Pre-release versions have a lower +precedence than the associated normal version. A pre-release version +indicates that the version is unstable and might not satisfy the +intended compatibility requirements as denoted by its associated +normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, +1.0.0-x.7.z.92, 1.0.0-x-y-z.\-\-. + +1. Build metadata MAY be denoted by appending a plus sign and a series of dot +separated identifiers immediately following the patch or pre-release version. +Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. +Identifiers MUST NOT be empty. Build metadata MUST be ignored when determining +version precedence. Thus two versions that differ only in the build metadata, +have the same precedence. Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, +1.0.0-beta+exp.sha.5114f85, 1.0.0+21AF26D3\-\-\-\-117B344092BD. + +1. Precedence refers to how versions are compared to each other when ordered. + + 1. Precedence MUST be calculated by separating the version into major, + minor, patch and pre-release identifiers in that order (build metadata + does not figure into precedence). + + 1. Precedence is determined by the first difference when comparing each of + these identifiers from left to right as follows: major, minor, and patch + versions are always compared numerically. + + Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. + + 1. When major, minor, and patch are equal, a pre-release version has lower + precedence than a normal version: + + Example: 1.0.0-alpha < 1.0.0. + + 1. Precedence for two pre-release versions with the same major, minor, and + patch version MUST be determined by comparing each dot separated identifier + from left to right until a difference is found as follows: + + 1. Identifiers consisting of only digits are compared numerically. + + 1. Identifiers with letters or hyphens are compared lexically in ASCII + sort order. + + 1. Numeric identifiers always have lower precedence than non-numeric + identifiers. + + 1. A larger set of pre-release fields has a higher precedence than a + smaller set, if all of the preceding identifiers are equal. + + Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < + 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. + +Backus–Naur Form Grammar for Valid SemVer Versions +-------------------------------------------------- +``` + ::= + | "-" + | "+" + | "-" "+" + + ::= "." "." + + ::= + + ::= + + ::= + + ::= + + ::= + | "." + + ::= + + ::= + | "." + + ::= + | + + ::= + | + + ::= + | + | + | + + ::= "0" + | + | + + ::= + | + + ::= + | + + ::= + | "-" + + ::= + | + + ::= "0" + | + + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + + ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" + | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" + | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" | "c" | "d" + | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" + | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" + | "y" | "z" +``` + +Why Use Semantic Versioning? +---------------------------- + +This is not a new or revolutionary idea. In fact, you probably do something +close to this already. The problem is that "close" isn't good enough. Without +compliance to some sort of formal specification, version numbers are +essentially useless for dependency management. By giving a name and clear +definition to the above ideas, it becomes easy to communicate your intentions +to the users of your software. Once these intentions are clear, flexible (but +not too flexible) dependency specifications can finally be made. + +A simple example will demonstrate how Semantic Versioning can make dependency +hell a thing of the past. Consider a library called "Firetruck." It requires a +Semantically Versioned package named "Ladder." At the time that Firetruck is +created, Ladder is at version 3.1.0. Since Firetruck uses some functionality +that was first introduced in 3.1.0, you can safely specify the Ladder +dependency as greater than or equal to 3.1.0 but less than 4.0.0. Now, when +Ladder version 3.1.1 and 3.2.0 become available, you can release them to your +package management system and know that they will be compatible with existing +dependent software. + +As a responsible developer you will, of course, want to verify that any +package upgrades function as advertised. The real world is a messy place; +there's nothing we can do about that but be vigilant. What you can do is let +Semantic Versioning provide you with a sane way to release and upgrade +packages without having to roll new versions of dependent packages, saving you +time and hassle. + +If all of this sounds desirable, all you need to do to start using Semantic +Versioning is to declare that you are doing so and then follow the rules. Link +to this website from your README so others know the rules and can benefit from +them. + +FAQ +--- + +### How should I deal with revisions in the 0.y.z initial development phase? + +The simplest thing to do is start your initial development release at 0.1.0 +and then increment the minor version for each subsequent release. + +### How do I know when to release 1.0.0? + +If your software is being used in production, it should probably already be +1.0.0. If you have a stable API on which users have come to depend, you should +be 1.0.0. If you're worrying a lot about backward compatibility, you should +probably already be 1.0.0. + +### Doesn't this discourage rapid development and fast iteration? + +Major version zero is all about rapid development. If you're changing the API +every day you should either still be in version 0.y.z or on a separate +development branch working on the next major version. + +### If even the tiniest backward incompatible changes to the public API require a major version bump, won't I end up at version 42.0.0 very rapidly? + +This is a question of responsible development and foresight. Incompatible +changes should not be introduced lightly to software that has a lot of +dependent code. The cost that must be incurred to upgrade can be significant. +Having to bump major versions to release incompatible changes means you'll +think through the impact of your changes, and evaluate the cost/benefit ratio +involved. + +### Documenting the entire public API is too much work! + +It is your responsibility as a professional developer to properly document +software that is intended for use by others. Managing software complexity is a +hugely important part of keeping a project efficient, and that's hard to do if +nobody knows how to use your software, or what methods are safe to call. In +the long run, Semantic Versioning, and the insistence on a well defined public +API can keep everyone and everything running smoothly. + +### What do I do if I accidentally release a backward incompatible change as a minor version? + +As soon as you realize that you've broken the Semantic Versioning spec, fix +the problem and release a new patch version that corrects the problem and +restores backward compatibility. Even under this circumstance, it is +unacceptable to modify versioned releases. If it's appropriate, +document the offending version and inform your users of the problem so that +they are aware of the offending version. + +### What should I do if I update my own dependencies without changing the public API? + +That would be considered compatible since it does not affect the public API. +Software that explicitly depends on the same dependencies as your package +should have their own dependency specifications and the author will notice any +conflicts. Determining whether the change is a patch level or minor level +modification depends on whether you updated your dependencies in order to fix +a bug or introduce new functionality. We would usually expect additional code +for the latter instance, in which case it's obviously a minor level increment. + +### What if I inadvertently alter the public API in a way that is not compliant with the version number change (i.e. the code incorrectly introduces a major breaking change in a patch release)? + +Use your best judgment. If you have a huge audience that will be drastically +impacted by changing the behavior back to what the public API intended, then +it may be best to perform a major version release, even though the fix could +strictly be considered a patch release. Remember, Semantic Versioning is all +about conveying meaning by how the version number changes. If these changes +are important to your users, use the version number to inform them. + +### How should I handle deprecating functionality? + +Deprecating existing functionality is a normal part of software development and +is often required to make forward progress. When you deprecate part of your +public API, you should do two things: (1) update your documentation to let +users know about the change, (2) issue a new minor release with the deprecation +in place. Before you completely remove the functionality in a new major release +there should be at least one minor release that contains the deprecation so +that users can smoothly transition to the new API. + +### Does SemVer have a size limit on the version string? + +No, but use good judgment. A 255 character version string is probably an overkill, +for example. Also, specific systems may impose their own limits on the size of +the string. + +### Is "v1.2.3" a semantic version? + +No, "v1.2.3" is not a semantic version. However, prefixing a semantic version +with a "v" is a common way (in English) to indicate it is a version number. +Abbreviating "version" as "v" is often seen with version control. Example: +`git tag v1.2.3 -m "Release version 1.2.3"`, in which case "v1.2.3" is a tag +name and the semantic version is "1.2.3". + +### Is there a suggested regular expression (RegEx) to check a SemVer string? + +There are two. One with named groups for those systems that support them +(PCRE [Perl Compatible Regular Expressions, i.e. Perl, PHP and R], Python +and Go). + +See: + +``` +^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ +``` + +And one with numbered capture groups instead (so cg1 = major, cg2 = minor, +cg3 = patch, cg4 = prerelease and cg5 = buildmetadata) that is compatible +with ECMA Script (JavaScript), PCRE (Perl Compatible Regular Expressions, +i.e. Perl, PHP and R), Python and Go. + +See: + +``` +^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ +``` + +About +----- + +The Semantic Versioning specification was originally authored by [Tom +Preston-Werner](https://tom.preston-werner.com), inventor of Gravatar and +cofounder of GitHub. + +If you'd like to leave feedback, please [open an issue on +GitHub](https://github.com/semver/semver/issues). + +License +------- + +[Creative Commons ― CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) From 485477960b076b68bd2a23b8f68d8bfbb4073900 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 20:39:02 +0100 Subject: [PATCH 09/55] Renamed CODE_OF_CONDUCT.md to CODE_OF_CONDUCT_2_1.md. No code changes (0.28.0) --- CODE_OF_CONDUCT.md => CODE_OF_CONDUCT_2_1.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CODE_OF_CONDUCT.md => CODE_OF_CONDUCT_2_1.md (100%) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT_2_1.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to CODE_OF_CONDUCT_2_1.md From 5c3d4f12477f9a76ade7afbd04a59dcee7d9054d Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 21:33:02 +0100 Subject: [PATCH 10/55] README update. No code changes (0.28.0) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4d67669..87ccc9a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) ☑ - [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) ☐ - [PC/SC](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) ☐ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ From d9804d9bf848fcffd97cf8281f643b64ceb244c7 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 22:52:43 +0100 Subject: [PATCH 11/55] Added PCRE2 support. (0.29.0) --- CMakeLists.txt | 32 +++++++- README.md | 2 +- docs/handbook.md | 3 +- examples/extra/pcre2_demo.fun | 33 ++++++++ examples/pcre2_opcodes.fun | 140 ++++++++++++++++++++++++++++++++++ examples/pcre2_showcase.fun | 32 ++++++++ lib/regex/pcre2.fun | 42 ++++++++++ src/bytecode.c | 3 + src/bytecode.h | 5 ++ src/parser.c | 38 +++++++++ src/vm.c | 17 +++++ src/vm.h | 1 + src/vm/pcre2/findall.c | 100 ++++++++++++++++++++++++ src/vm/pcre2/match.c | 89 +++++++++++++++++++++ src/vm/pcre2/test.c | 62 +++++++++++++++ 15 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 examples/extra/pcre2_demo.fun create mode 100644 examples/pcre2_opcodes.fun create mode 100644 examples/pcre2_showcase.fun create mode 100644 lib/regex/pcre2.fun create mode 100644 src/vm/pcre2/findall.c create mode 100644 src/vm/pcre2/match.c create mode 100644 src/vm/pcre2/test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 815e78c..4a132f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.28.0 LANGUAGES C) +project(fun VERSION 0.29.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -65,6 +65,28 @@ if(FUN_WITH_JSON) endif() endif() +# Optional PCRE2 support +option(FUN_WITH_PCRE2 "Enable PCRE2 (Perl Compatible Regular Expressions) support" OFF) +set(PCRE2_INCLUDE_DIRS "") +set(PCRE2_LINK_LIBS "") +if(FUN_WITH_PCRE2) + message(STATUS "Building with PCRE2 support") + add_definitions(-DFUN_WITH_PCRE2) + # Try pkg-config first + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PCRE2 QUIET libpcre2-8) + endif() + if(PCRE2_FOUND) + list(APPEND PCRE2_INCLUDE_DIRS ${PCRE2_INCLUDE_DIRS} ${PCRE2_INCLUDE_DIRS}) + list(APPEND PCRE2_LINK_LIBS ${PCRE2_LINK_LIBS} ${PCRE2_LIBRARIES}) + include_directories(${PCRE2_INCLUDE_DIRS}) + else() + # Fallback: common defaults + list(APPEND PCRE2_LINK_LIBS pcre2-8) + endif() +endif() + # Debug option to enable verbose parser/VM logging option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) @@ -110,6 +132,14 @@ if(JSONC_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${JSONC_LINK_LIBS}) endif() +# pcre2 include and link (if enabled) +if(PCRE2_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${PCRE2_INCLUDE_DIRS}) +endif() +if(PCRE2_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${PCRE2_LINK_LIBS}) +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index 87ccc9a..2426bb1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) ☑ - [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) ☐ - [PC/SC](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) ☐ +- [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☐ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ diff --git a/docs/handbook.md b/docs/handbook.md index 91c0f53..a36fddb 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -47,7 +47,7 @@ Build: ```bash # Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON +cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON -DFUN_WITH_PCRE2=ON cmake --build build --target fun ``` @@ -86,6 +86,7 @@ All CMake options must be passed as -DNAME=VALUE: - FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) - FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) +- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) - FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) diff --git a/examples/extra/pcre2_demo.fun b/examples/extra/pcre2_demo.fun new file mode 100644 index 0000000..8ee2752 --- /dev/null +++ b/examples/extra/pcre2_demo.fun @@ -0,0 +1,33 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* +include + +rx = Pcre2() + +text = "E-mails: one@example.com, Two@Example.COM; invalid: x@y" +pattern = "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + +print("Has email? ", rx.test(pattern, text, rx.i())) + +first = rx.match(pattern, text, rx.i()) +if first != nil { + print("First: ", first["full"]) +} + +all = rx.find_all(pattern, text, rx.i()) +for m in all { + print("Found: ", m["full"]) +} +*/ diff --git a/examples/pcre2_opcodes.fun b/examples/pcre2_opcodes.fun new file mode 100644 index 0000000..43eba00 --- /dev/null +++ b/examples/pcre2_opcodes.fun @@ -0,0 +1,140 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +// PCRE2 example using VM builtins directly (no class wrapper) +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +print("-- PCRE2 builtins (no class) --") + +pattern = "(\\w+)" // capture a word +text = "Hello 123 world" + +// Flags: 1=I, 2=M, 4=S, 8=U (UTF), 16=X; we’ll use UTF by default +flags = 8 + +print("test:") +print(pcre2_test(pattern, text, flags)) + +m = pcre2_match(pattern, text, flags) +if (m != nil) + print("first full:") + print(m["full"]) + print("span:") + print(m["start"]) + print("..") + print(m["end"]) + print("groups count:") + print(len(m["groups"])) + +all = pcre2_findall("\\w+", text, flags) +for x in all + print("all:") + print(x["full"]) + print("@") + print(x["start"]) + print("..") + print(x["end"]) + +print("") +print("-- More regex demos --") + +// Helper: OR flags (uses VM bor opcode) +fun OR(a, b) + return bor(a, b) + +// Demo 1: E-mail extraction (case-insensitive) +email_text = "E-mails: one@example.com, Two@Example.COM; invalid: x@y" +email_pat = "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" +email_flags = OR(flags, 1) // UTF | I +print("Emails (findall):") +emails = pcre2_findall(email_pat, email_text, email_flags) +for e in emails + print(e["full"]) + +// Demo 2: URLs (very simple, for demo purposes) +url_text = "See http://example.com and https://fun-lang.xyz/docs?x=1#top" +url_pat = "https?://[A-Za-z0-9._~:/?#[@]!$&'()*+,;=%-]+" +print("URLs:") +for u in pcre2_findall(url_pat, url_text, flags) + print(u["full"]) + +// Demo 3: IPv4 addresses +ip_text = "ping 8.8.8.8 and 192.168.0.1; not 999.999.999.999" +ip_pat = "(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)" +print("IPv4:") +for ip in pcre2_findall(ip_pat, ip_text, flags) + print(ip["full"]) + +// Demo 4: Dates (YYYY-MM-DD) +date_text = "Born 1999-12-31, updated 2025-11-25, bad 2025-13-40" +date_pat = "(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])" +print("Dates (with groups Y/M/D):") +for d in pcre2_findall(date_pat, date_text, flags) + print(d["full"]) + print(join(d["groups"], "/")) + +// Demo 5: Hex colors (#RRGGBB) +color_text = "Palette: #FF00FF, #1a2b3c, not #abcd or #12345g" +color_pat = "#[0-9A-Fa-f]{6}" +print("Hex colors:") +for c in pcre2_findall(color_pat, color_text, flags) + print(c["full"]) + +// Demo 6: Quoted strings with escapes +q_text = 'say "hi there" and "indented" plus "quote\\"inside"' +q_pat = '"([^"\\\\]|\\\\.)*"' +print("Quoted strings (with escapes):") +for q in pcre2_findall(q_pat, q_text, flags) + print(q["full"]) + +// Demo 7: Multiline anchors with /m (M flag) +ml_text = "first line\nSecond line\nthird" +ml_pat = "^(\\w+)" +ml_flags = OR(flags, 2) // UTF | M +print("Multiline ^ anchors (first token of each line):") +for ml in pcre2_findall(ml_pat, ml_text, ml_flags) + print(ml["full"]) + +// Demo 8: Dotall vs non-dotall +ds_text = "BEGIN\nline1\nline2\nEND" +pat_nd = "BEGIN.*END" // default: . does not match newlines +pat_ds = "BEGIN.*END" // with DOTALL, it does +print("Dotall OFF (should fail):") +print(pcre2_test(pat_nd, ds_text, flags)) +print("Dotall ON (should match):") +print(pcre2_test(pat_ds, ds_text, OR(flags, 4))) + +// Demo 9: Word boundaries and case-insensitive find +wb_text = "The theater and the THE can differ." +wb_pat = "\\bthe\\b" +print("Word boundary, case-insensitive:") +for w in pcre2_findall(wb_pat, wb_text, OR(flags, 1)) + print(w["full"]) + +// Demo 10: Lookahead — word followed by number +la_text = "foo 123, bar, baz 9" +la_pat = "\\w+(?=\\s+\\d+)" +print("Lookahead (word before number):") +for a in pcre2_findall(la_pat, la_text, flags) + print(a["full"]) + +// Demo 11: Non-greedy vs greedy +ng_text = "onetwo" +greedy = ".*" +lazy = ".*?" +print("Greedy:") +for g in pcre2_findall(greedy, ng_text, OR(flags, 4)) // DOTALL ensures '.' covers any + print(g["full"]) +print("Non-greedy:") +for l in pcre2_findall(lazy, ng_text, OR(flags, 4)) + print(l["full"]) diff --git a/examples/pcre2_showcase.fun b/examples/pcre2_showcase.fun new file mode 100644 index 0000000..2df2f6a --- /dev/null +++ b/examples/pcre2_showcase.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* +include + +re = PCRE2() + +print("Testing PCRE2 showcase...") + +print(re.test("\\d+", "Order #1234")) + +m = re.match("(\\w+)", "hello WORLD", re.i()) +if m != nil { + print(m["full"]) // hello + print(len(m["groups"])) +} + +for x in re.find_all("[a-z]+", "One two THREE four", re.i()) { + print(x["full"]) // one two four +} +*/ diff --git a/lib/regex/pcre2.fun b/lib/regex/pcre2.fun new file mode 100644 index 0000000..7133a34 --- /dev/null +++ b/lib/regex/pcre2.fun @@ -0,0 +1,42 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +// PCRE2 stdlib abstraction wrapping VM pcre2_* builtins. +// Provides a small class with flags and user-friendly methods. + +class PCRE2() + fun i(this) + return 1 + fun m(this) + return 2 + fun s(this) + return 4 + fun u(this) + return 8 + fun x(this) + return 16 + + fun test(this, pattern, text, flags) + if flags == nil + flags = this.u() + return pcre2_test(to_string(pattern), to_string(text), flags) + + fun match(this, pattern, text, flags) + if flags == nil + flags = this.u() + return pcre2_match(to_string(pattern), to_string(text), flags) + + fun find_all(this, pattern, text, flags) + if flags == nil + flags = this.u() + return pcre2_findall(to_string(pattern), to_string(text), flags) diff --git a/src/bytecode.c b/src/bytecode.c index 7350897..8955fd0 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -151,6 +151,9 @@ static const char *opcode_name(OpCode op) { case OP_PCSC_CONNECT: return "PCSC_CONNECT"; case OP_PCSC_DISCONNECT: return "PCSC_DISCONNECT"; case OP_PCSC_TRANSMIT: return "PCSC_TRANSMIT"; + case OP_PCRE2_TEST: return "PCRE2_TEST"; + case OP_PCRE2_MATCH: return "PCRE2_MATCH"; + case OP_PCRE2_FINDALL: return "PCRE2_FINDALL"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index 845241e..089c1c7 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -155,6 +155,11 @@ typedef enum { OP_PCSC_DISCONNECT, // pops handle id; returns 1/0 OP_PCSC_TRANSMIT, // pops apdu array, handle id; returns map {"data":[],"sw1":n,"sw2":n,"code":n} + // PCRE2 regex ops (optional) + OP_PCRE2_TEST, // pops flags, text, pattern; pushes 1/0 + OP_PCRE2_MATCH, // pops flags, text, pattern; pushes match map or Nil + OP_PCRE2_FINDALL, // pops flags, text, pattern; pushes array of match maps + // Sockets (UNIX platforms) OP_SOCK_TCP_LISTEN, // pops backlog, port; returns listen fd (>0) or 0 OP_SOCK_TCP_ACCEPT, // pops listen fd; returns client fd (>0) or 0 diff --git a/src/parser.c b/src/parser.c index 97c27ec..690cfb3 100644 --- a/src/parser.c +++ b/src/parser.c @@ -787,6 +787,44 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* PCRE2 builtins */ + if (strcmp(name, "pcre2_test") == 0) { + (*pos)++; /* '(' */ + /* (pattern, text, flags) */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_test expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "pcre2_test expects (pattern, text, flags)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_test expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "pcre2_test expects (pattern, text, flags)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_test expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after pcre2_test args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_PCRE2_TEST, 0); + free(name); + return 1; + } + if (strcmp(name, "pcre2_match") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_match expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "pcre2_match expects (pattern, text, flags)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_match expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "pcre2_match expects (pattern, text, flags)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_match expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after pcre2_match args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_PCRE2_MATCH, 0); + free(name); + return 1; + } + if (strcmp(name, "pcre2_findall") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_findall expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "pcre2_findall expects (pattern, text, flags)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_findall expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "pcre2_findall expects (pattern, text, flags)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcre2_findall expects (pattern, text, flags)" ); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after pcre2_findall args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_PCRE2_FINDALL, 0); + free(name); + return 1; + } if (strcmp(name, "pcsc_release") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcsc_release expects 1 argument (ctx)"); free(name); return 0; } diff --git a/src/vm.c b/src/vm.c index 7093c4d..c20c485 100644 --- a/src/vm.c +++ b/src/vm.c @@ -20,6 +20,18 @@ #include #include +/* Ensure PCRE2 is configured consistently across the whole translation unit. + * vm.c includes many opcode implementation .c files; some use PCRE2. For PCRE2 + * headers to expose the correct typedefs (e.g., pcre2_code, PCRE2_SPTR), the + * PCRE2_CODE_UNIT_WIDTH macro must be defined before the first inclusion of + * . We do this once here when PCRE2 support is enabled. */ +#ifdef FUN_WITH_PCRE2 +#ifndef PCRE2_CODE_UNIT_WIDTH +#define PCRE2_CODE_UNIT_WIDTH 8 +#endif +#include +#endif + /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); @@ -667,6 +679,11 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/json/from_file.c" #include "vm/json/to_file.c" + /* PCRE2 ops */ + #include "vm/pcre2/test.c" + #include "vm/pcre2/match.c" + #include "vm/pcre2/findall.c" + #include "vm/strings/find.c" #include "vm/strings/regex_match.c" #include "vm/strings/regex_search.c" diff --git a/src/vm.h b/src/vm.h index 8863bcc..b534065 100644 --- a/src/vm.h +++ b/src/vm.h @@ -40,6 +40,7 @@ static const char *opcode_names[] = { "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", "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", "EXIT" }; diff --git a/src/vm/pcre2/findall.c b/src/vm/pcre2/findall.c new file mode 100644 index 0000000..09857a8 --- /dev/null +++ b/src/vm/pcre2/findall.c @@ -0,0 +1,100 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* PCRE2_FINDALL */ +case OP_PCRE2_FINDALL: { +#ifdef FUN_WITH_PCRE2 + Value vflags = pop_value(vm); + Value vtext = pop_value(vm); + Value vpat = pop_value(vm); + int flags = 0; + if (vflags.type == VAL_INT || vflags.type == VAL_BOOL) flags = (int)vflags.i; + char *pattern = value_to_string_alloc(&vpat); + char *subject = value_to_string_alloc(&vtext); + free_value(vflags); + free_value(vtext); + free_value(vpat); + if (!pattern || !subject) { + if (pattern) free(pattern); + if (subject) free(subject); + push_value(vm, make_array_from_values(NULL, 0)); + break; + } + #ifndef PCRE2_CODE_UNIT_WIDTH + #define PCRE2_CODE_UNIT_WIDTH 8 + #endif + #include + int errorcode; PCRE2_SIZE erroff; + uint32_t opt = 0; + if (flags & 1) opt |= PCRE2_CASELESS; /* I */ + if (flags & 2) opt |= PCRE2_MULTILINE; /* M */ + if (flags & 4) opt |= PCRE2_DOTALL; /* S */ + if (flags & 8) opt |= PCRE2_UTF; /* U */ + if (flags & 16) opt |= PCRE2_EXTENDED; /* X */ + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, opt, &errorcode, &erroff, NULL); + if (!re) { + free(pattern); free(subject); + push_value(vm, make_array_from_values(NULL, 0)); + break; + } + pcre2_match_data *mdata = pcre2_match_data_create_from_pattern(re, NULL); + Value out = make_array_from_values(NULL, 0); + size_t subj_len = strlen(subject); + size_t start_off = 0; + int gcount = 0; + while (1) { + int rc = pcre2_match(re, (PCRE2_SPTR)subject, (PCRE2_SIZE)subj_len, start_off, 0, mdata, NULL); + if (rc <= 0) break; + PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata); + int s0 = (int)ov[0]; + int e0 = (int)ov[1]; + /* result map for this match */ + Value res = make_map_empty(); + char *full = string_substr(subject, s0, e0 - s0); + (void)map_set(&res, "full", make_string(full ? full : "")); + if (full) free(full); + (void)map_set(&res, "start", make_int(s0)); + (void)map_set(&res, "end", make_int(e0)); + Value groups = make_array_from_values(NULL, 0); + for (int i = 1; i < rc; ++i) { + int s = (int)ov[2*i]; + int e = (int)ov[2*i+1]; + char *gstr = (s >= 0 && e >= s) ? string_substr(subject, s, e - s) : NULL; + Value gv = make_string(gstr ? gstr : ""); + if (gstr) free(gstr); + (void)array_push(&groups, gv); + } + (void)map_set(&res, "groups", groups); + (void)array_push(&out, res); + /* advance start offset; guard against empty match */ + if (e0 == s0) { + if ((size_t)e0 < subj_len) { + start_off = e0 + 1; + } else { + break; + } + } else { + start_off = e0; + } + gcount = rc; + } + pcre2_match_data_free(mdata); + pcre2_code_free(re); + free(pattern); free(subject); + push_value(vm, out); +#else + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + Value c = pop_value(vm); free_value(c); + push_value(vm, make_array_from_values(NULL, 0)); +#endif + break; +} diff --git a/src/vm/pcre2/match.c b/src/vm/pcre2/match.c new file mode 100644 index 0000000..0707449 --- /dev/null +++ b/src/vm/pcre2/match.c @@ -0,0 +1,89 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* PCRE2_MATCH */ +case OP_PCRE2_MATCH: { +#ifdef FUN_WITH_PCRE2 + Value vflags = pop_value(vm); + Value vtext = pop_value(vm); + Value vpat = pop_value(vm); + int flags = 0; + if (vflags.type == VAL_INT || vflags.type == VAL_BOOL) flags = (int)vflags.i; + char *pattern = value_to_string_alloc(&vpat); + char *subject = value_to_string_alloc(&vtext); + free_value(vflags); + free_value(vtext); + free_value(vpat); + if (!pattern || !subject) { + if (pattern) free(pattern); + if (subject) free(subject); + push_value(vm, make_nil()); + break; + } + #ifndef PCRE2_CODE_UNIT_WIDTH + #define PCRE2_CODE_UNIT_WIDTH 8 + #endif + #include + int errorcode; PCRE2_SIZE erroff; + uint32_t opt = 0; + if (flags & 1) opt |= PCRE2_CASELESS; /* I */ + if (flags & 2) opt |= PCRE2_MULTILINE; /* M */ + if (flags & 4) opt |= PCRE2_DOTALL; /* S */ + if (flags & 8) opt |= PCRE2_UTF; /* U */ + if (flags & 16) opt |= PCRE2_EXTENDED; /* X */ + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, opt, &errorcode, &erroff, NULL); + if (!re) { + free(pattern); free(subject); + push_value(vm, make_nil()); + break; + } + pcre2_match_data *mdata = pcre2_match_data_create_from_pattern(re, NULL); + int rc = pcre2_match(re, (PCRE2_SPTR)subject, (PCRE2_SIZE)strlen(subject), 0, 0, mdata, NULL); + if (rc <= 0) { + pcre2_match_data_free(mdata); + pcre2_code_free(re); + free(pattern); free(subject); + push_value(vm, make_nil()); + break; + } + PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata); + /* Build result map */ + Value res = make_map_empty(); + int start0 = (int)ov[0]; + int end0 = (int)ov[1]; + char *full = string_substr(subject, start0, end0 - start0); + (void)map_set(&res, "full", make_string(full ? full : "")); + if (full) free(full); + (void)map_set(&res, "start", make_int(start0)); + (void)map_set(&res, "end", make_int(end0)); + /* groups array (excluding group 0) */ + Value groups = make_array_from_values(NULL, 0); + for (int i = 1; i < rc; ++i) { + int s = (int)ov[2*i]; + int e = (int)ov[2*i+1]; + char *gstr = (s >= 0 && e >= s) ? string_substr(subject, s, e - s) : NULL; + Value gv = make_string(gstr ? gstr : ""); + if (gstr) free(gstr); + (void)array_push(&groups, gv); + } + (void)map_set(&res, "groups", groups); + pcre2_match_data_free(mdata); + pcre2_code_free(re); + free(pattern); free(subject); + push_value(vm, res); +#else + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + Value c = pop_value(vm); free_value(c); + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/pcre2/test.c b/src/vm/pcre2/test.c new file mode 100644 index 0000000..013df5b --- /dev/null +++ b/src/vm/pcre2/test.c @@ -0,0 +1,62 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* PCRE2_TEST */ +case OP_PCRE2_TEST: { +#ifdef FUN_WITH_PCRE2 + Value vflags = pop_value(vm); + Value vtext = pop_value(vm); + Value vpat = pop_value(vm); + int flags = 0; + if (vflags.type == VAL_INT || vflags.type == VAL_BOOL) flags = (int)vflags.i; + char *pattern = value_to_string_alloc(&vpat); + char *subject = value_to_string_alloc(&vtext); + free_value(vflags); + free_value(vtext); + free_value(vpat); + if (!pattern || !subject) { + if (pattern) free(pattern); + if (subject) free(subject); + push_value(vm, make_int(0)); + break; + } + #ifndef PCRE2_CODE_UNIT_WIDTH + #define PCRE2_CODE_UNIT_WIDTH 8 + #endif + #include + int errorcode; PCRE2_SIZE erroff; + uint32_t opt = 0; + if (flags & 1) opt |= PCRE2_CASELESS; /* I */ + if (flags & 2) opt |= PCRE2_MULTILINE; /* M */ + if (flags & 4) opt |= PCRE2_DOTALL; /* S */ + if (flags & 8) opt |= PCRE2_UTF; /* U */ + if (flags & 16) opt |= PCRE2_EXTENDED; /* X */ + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, opt, &errorcode, &erroff, NULL); + if (!re) { + free(pattern); free(subject); + push_value(vm, make_int(0)); + break; + } + pcre2_match_data *mdata = pcre2_match_data_create_from_pattern(re, NULL); + int rc = pcre2_match(re, (PCRE2_SPTR)subject, (PCRE2_SIZE)strlen(subject), 0, 0, mdata, NULL); + pcre2_match_data_free(mdata); + pcre2_code_free(re); + free(pattern); free(subject); + push_value(vm, make_int(rc >= 0 ? 1 : 0)); +#else + /* pop args and return 0 when PCRE2 disabled */ + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + Value c = pop_value(vm); free_value(c); + push_value(vm, make_int(0)); +#endif + break; +} From bb848b6679ed8f48de79660f3ce596400f84f376 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 23:11:47 +0100 Subject: [PATCH 12/55] Permission fixes. No code changes (0.29.0) --- examples/json_showcase.fun | 0 examples/pcre2_opcodes.fun | 0 examples/pcre2_showcase.fun | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/json_showcase.fun mode change 100644 => 100755 examples/pcre2_opcodes.fun mode change 100644 => 100755 examples/pcre2_showcase.fun diff --git a/examples/json_showcase.fun b/examples/json_showcase.fun old mode 100644 new mode 100755 diff --git a/examples/pcre2_opcodes.fun b/examples/pcre2_opcodes.fun old mode 100644 new mode 100755 diff --git a/examples/pcre2_showcase.fun b/examples/pcre2_showcase.fun old mode 100644 new mode 100755 From 4d82d735150bac4b79c2e74d670ebe1c3d33811a Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 26 Nov 2025 00:35:22 +0100 Subject: [PATCH 13/55] Added libcurl (cURL) support. (0.30.0) --- .gitignore | 1 + CMakeLists.txt | 30 +++++++++++++++++++++++- README.md | 10 ++++---- docs/handbook.md | 7 +++--- examples/curl_download.fun | 24 +++++++++++++++++++ examples/curl_get_json.fun | 25 ++++++++++++++++++++ examples/curl_post.fun | 32 ++++++++++++++++++++++++++ src/bytecode.c | 3 +++ src/bytecode.h | 5 ++++ src/parser.c | 29 +++++++++++++++++++++++ src/vm.c | 24 +++++++++++++++++++ src/vm.h | 1 + src/vm/curl/download.c | 47 ++++++++++++++++++++++++++++++++++++++ src/vm/curl/get.c | 33 ++++++++++++++++++++++++++ src/vm/curl/post.c | 41 +++++++++++++++++++++++++++++++++ 15 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 examples/curl_download.fun create mode 100644 examples/curl_get_json.fun create mode 100644 examples/curl_post.fun create mode 100644 src/vm/curl/download.c create mode 100644 src/vm/curl/get.c create mode 100644 src/vm/curl/post.c diff --git a/.gitignore b/.gitignore index 05bf729..5870479 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build* cmake* demo_* dist/ +downloaded.png lib/*.so out/ src/*.o diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a132f9..8c81c1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.29.0 LANGUAGES C) +project(fun VERSION 0.30.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -87,6 +87,26 @@ if(FUN_WITH_PCRE2) endif() endif() +# Optional CURL (libcurl) support +option(FUN_WITH_CURL "Enable libcurl HTTP client support" OFF) +set(CURL_INCLUDE_DIRS "") +set(CURL_LINK_LIBS "") +if(FUN_WITH_CURL) + message(STATUS "Building with libcurl support") + add_definitions(-DFUN_WITH_CURL) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBCURL QUIET libcurl) + endif() + if(LIBCURL_FOUND) + list(APPEND CURL_INCLUDE_DIRS ${LIBCURL_INCLUDE_DIRS}) + list(APPEND CURL_LINK_LIBS ${LIBCURL_LIBRARIES}) + include_directories(${CURL_INCLUDE_DIRS}) + else() + list(APPEND CURL_LINK_LIBS curl) + endif() +endif() + # Debug option to enable verbose parser/VM logging option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) @@ -140,6 +160,14 @@ if(PCRE2_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${PCRE2_LINK_LIBS}) endif() +# libcurl include and link (if enabled) +if(CURL_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${CURL_INCLUDE_DIRS}) +endif() +if(CURL_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${CURL_LINK_LIBS}) +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index 2426bb1..5030290 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht ### Extras +- [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) ☑ -- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) ☐ -- [PC/SC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (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) ☐ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ @@ -138,9 +138,11 @@ cmake --build build --target fun CMake options you can toggle (all require NAME=VALUE): - FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_REPL=ON|OFF — enable building the interactive REPL (default ON) -- FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) +- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) +- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) +- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) +- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) That's it! For testing it, run: diff --git a/docs/handbook.md b/docs/handbook.md index a36fddb..69045ed 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -85,10 +85,11 @@ But be sure to build Fun with -DFUN_WITH_REPL=ON. All CMake options must be passed as -DNAME=VALUE: - FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) -- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) -- FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) +- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) +- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) +- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) +- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) If you encounter an error such as: diff --git a/examples/curl_download.fun b/examples/curl_download.fun new file mode 100644 index 0000000..a0f72de --- /dev/null +++ b/examples/curl_download.fun @@ -0,0 +1,24 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* + * Demonstrates curl_download saving a file to disk. + */ + +url = "https://httpbin.org/image/png" +path = "./downloaded.png" +ok = curl_download(url, path) +if ok == 1 + print("Downloaded to " + path) +else + print("Download failed") diff --git a/examples/curl_get_json.fun b/examples/curl_get_json.fun new file mode 100644 index 0000000..713cf88 --- /dev/null +++ b/examples/curl_get_json.fun @@ -0,0 +1,25 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* + * Demonstrates curl_get and JSON.parse working together. + */ + +url = "https://httpbin.org/json" +resp = curl_get(url) +print("Raw length: " + to_string(len(resp))) + +// If JSON support is enabled, parse it +obj = json_parse(resp) +if obj != nil + print("Title: " + obj["slideshow"]["title"]) diff --git a/examples/curl_post.fun b/examples/curl_post.fun new file mode 100644 index 0000000..988c458 --- /dev/null +++ b/examples/curl_post.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* + * Demonstrates curl_post sending form data and printing response. + */ + +url = "https://httpbin.org/post" +data = "name=Fun&lang=fun" +resp = curl_post(url, data) +print("Response: " + resp) + +// If JSON support is enabled, parse it +obj = json_parse(resp) +if obj != nil + print("Content-Type: " + to_string(obj["headers"]["Content-Type"])) + +if obj != nil + print("Host: " + to_string(obj["headers"]["Host"])) + +if obj != nil + print("Origin: " + to_string(obj["origin"])) diff --git a/src/bytecode.c b/src/bytecode.c index 8955fd0..2120c8e 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -145,6 +145,9 @@ static const char *opcode_name(OpCode op) { case OP_JSON_STRINGIFY: return "JSON_STRINGIFY"; case OP_JSON_FROM_FILE: return "JSON_FROM_FILE"; case OP_JSON_TO_FILE: return "JSON_TO_FILE"; + case OP_CURL_GET: return "CURL_GET"; + case OP_CURL_POST: return "CURL_POST"; + case OP_CURL_DOWNLOAD: return "CURL_DOWNLOAD"; case OP_PCSC_ESTABLISH: return "PCSC_ESTABLISH"; case OP_PCSC_RELEASE: return "PCSC_RELEASE"; case OP_PCSC_LIST_READERS: return "PCSC_LIST_READERS"; diff --git a/src/bytecode.h b/src/bytecode.h index 089c1c7..e38e7f9 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -147,6 +147,11 @@ typedef enum { OP_JSON_FROM_FILE, // pops path string; pushes value (or Nil) OP_JSON_TO_FILE, // pops pretty(bool), value, path; pushes 1/0 + // CURL (libcurl) + OP_CURL_GET, // pops [headers map?], url; pushes response string (or "") + OP_CURL_POST, // pops [headers map?], body string, url; pushes response string (or "") + OP_CURL_DOWNLOAD, // pops [headers map?], path, url; pushes 1/0 + // 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/parser.c b/src/parser.c index 690cfb3..7f19a44 100644 --- a/src/parser.c +++ b/src/parser.c @@ -778,6 +778,35 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* CURL builtins (minimal interface like JSON) */ + if (strcmp(name, "curl_get") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_get expects (url)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after curl_get arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CURL_GET, 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; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "curl_post expects (url, body)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_post expects (url, body)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after curl_post args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CURL_POST, 0); + free(name); + return 1; + } + if (strcmp(name, "curl_download") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_download expects (url, path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "curl_download expects (url, path)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_download expects (url, path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after curl_download args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CURL_DOWNLOAD, 0); + free(name); + return 1; + } /* PCSC builtins */ if (strcmp(name, "pcsc_establish") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index c20c485..16d0a41 100644 --- a/src/vm.c +++ b/src/vm.c @@ -32,6 +32,25 @@ #include #endif +/* Ensure libcurl headers and helpers are defined at file scope (not inside vm_run) */ +#ifdef FUN_WITH_CURL +#include +typedef struct { char *d; size_t n; } FunCurlBuf; +static size_t fun_curl_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { + size_t add = sz * nm; + FunCurlBuf *b = (FunCurlBuf*)ud; + char *p = (char*)realloc(b->d, b->n + add + 1); + if (!p) return 0; + memcpy(p + b->n, ptr, add); + b->d = p; b->n += add; b->d[b->n] = '\0'; + return add; +} +static size_t fun_curl_file_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { + FILE *f = (FILE*)ud; + return fwrite(ptr, sz, nm, f); +} +#endif + /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); @@ -679,6 +698,11 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/json/from_file.c" #include "vm/json/to_file.c" + /* CURL ops */ + #include "vm/curl/get.c" + #include "vm/curl/post.c" + #include "vm/curl/download.c" + /* PCRE2 ops */ #include "vm/pcre2/test.c" #include "vm/pcre2/match.c" diff --git a/src/vm.h b/src/vm.h index b534065..6b685de 100644 --- a/src/vm.h +++ b/src/vm.h @@ -39,6 +39,7 @@ static const char *opcode_names[] = { "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", + "CURL_GET","CURL_POST","CURL_DOWNLOAD", "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", diff --git a/src/vm/curl/download.c b/src/vm/curl/download.c new file mode 100644 index 0000000..51d9db6 --- /dev/null +++ b/src/vm/curl/download.c @@ -0,0 +1,47 @@ +/** + * libcurl DOWNLOAD builtin + */ +case OP_CURL_DOWNLOAD: { +#ifdef FUN_WITH_CURL + Value vpath = pop_value(vm); + Value vurl = pop_value(vm); + char *url = value_to_string_alloc(&vurl); + char *path = value_to_string_alloc(&vpath); + free_value(vurl); + free_value(vpath); + if (!url || !path) { + if (url) free(url); + if (path) free(path); + push_value(vm, make_int(0)); + break; + } + FILE *fp = fopen(path, "wb"); + if (!fp) { + free(url); free(path); + push_value(vm, make_int(0)); + break; + } + CURL *h = curl_easy_init(); + if (!h) { + fclose(fp); + free(url); free(path); + push_value(vm, make_int(0)); + break; + } + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, fun_curl_file_write_cb); + curl_easy_setopt(h, CURLOPT_WRITEDATA, fp); + CURLcode rc = curl_easy_perform(h); + curl_easy_cleanup(h); + fclose(fp); + free(url); free(path); + if (rc != CURLE_OK) { push_value(vm, make_int(0)); break; } + push_value(vm, make_int(1)); +#else + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/src/vm/curl/get.c b/src/vm/curl/get.c new file mode 100644 index 0000000..7d15b17 --- /dev/null +++ b/src/vm/curl/get.c @@ -0,0 +1,33 @@ +/** + * libcurl GET builtin + */ +case OP_CURL_GET: { +#ifdef FUN_WITH_CURL + Value vurl = pop_value(vm); + char *url = value_to_string_alloc(&vurl); + free_value(vurl); + if (!url) { push_value(vm, make_string("")); break; } + FunCurlBuf buf = { NULL, 0 }; + CURL *h = curl_easy_init(); + if (!h) { free(url); push_value(vm, make_string("")); break; } + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, fun_curl_write_cb); + curl_easy_setopt(h, CURLOPT_WRITEDATA, &buf); + CURLcode rc = curl_easy_perform(h); + curl_easy_cleanup(h); + free(url); + if (rc != CURLE_OK) { + if (buf.d) free(buf.d); + push_value(vm, make_string("")); + break; + } + Value s = make_string(buf.d ? buf.d : ""); + if (buf.d) free(buf.d); + push_value(vm, s); +#else + Value v = pop_value(vm); free_value(v); + push_value(vm, make_string("")); +#endif + break; +} diff --git a/src/vm/curl/post.c b/src/vm/curl/post.c new file mode 100644 index 0000000..08f0b15 --- /dev/null +++ b/src/vm/curl/post.c @@ -0,0 +1,41 @@ +/** + * libcurl POST builtin + */ +case OP_CURL_POST: { +#ifdef FUN_WITH_CURL + Value vbody = pop_value(vm); + Value vurl = pop_value(vm); + char *url = value_to_string_alloc(&vurl); + char *body = value_to_string_alloc(&vbody); + free_value(vurl); + free_value(vbody); + if (!url) { if (body) free(body); push_value(vm, make_string("")); break; } + if (!body) body = strdup(""); + FunCurlBuf buf = { NULL, 0 }; + CURL *h = curl_easy_init(); + if (!h) { free(url); free(body); push_value(vm, make_string("")); break; } + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_POST, 1L); + curl_easy_setopt(h, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, fun_curl_write_cb); + curl_easy_setopt(h, CURLOPT_WRITEDATA, &buf); + CURLcode rc = curl_easy_perform(h); + curl_easy_cleanup(h); + free(url); + free(body); + if (rc != CURLE_OK) { + if (buf.d) free(buf.d); + push_value(vm, make_string("")); + break; + } + Value s = make_string(buf.d ? buf.d : ""); + if (buf.d) free(buf.d); + push_value(vm, s); +#else + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + push_value(vm, make_string("")); +#endif + break; +} From 7659957c1fa7a11bea6f47bd5816431fa32a321b Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 26 Nov 2025 00:52:59 +0100 Subject: [PATCH 14/55] Handbook update. No code changes. (0.29.0) --- docs/handbook.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/handbook.md b/docs/handbook.md index 69045ed..460ca53 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -47,7 +47,7 @@ Build: ```bash # Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON -DFUN_WITH_PCRE2=ON +cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON -DFUN_WITH_PCRE2=ON -DFUN_WITH_CURL=ON cmake --build build --target fun ``` @@ -136,6 +136,7 @@ fun ./demo.fun - arrays, strings, maps helpers (hex, range) - Extra libraries - JSON (via json-c) + - CURL (via libcurl) - PCSC (PC/SC smart card) - Examples reference (what each example does) @@ -162,7 +163,7 @@ Key VM concepts (non-exhaustive): - Math misc: OP_MIN/MAX/CLAMP/ABS/POW/RANDOM_SEED/RANDOM_INT. - Iteration helpers: OP_ENUMERATE, OP_ZIP. - OS/IO/network: socket ops, file ops, process execution, environment, threads, etc., implemented as built-ins. -- Optional features: JSON opcodes (OP_JSON_PARSE and friends in src/vm/json/*) are compiled in only if -DFUN_WITH_JSON=ON. PCSC opcodes are available if -DFUN_WITH_PCSC=ON. +- Optional features: JSON opcodes (OP_JSON_PARSE and friends in src/vm/json/*) are compiled in only if -DFUN_WITH_JSON=ON. CURL builtins (curl_get/curl_post/curl_download) are available if -DFUN_WITH_CURL=ON. PCSC opcodes are available if -DFUN_WITH_PCSC=ON. Error handling and debugging: - Build with FUN_DEBUG=ON for verbose VM traces. @@ -378,6 +379,24 @@ Example walkthrough (examples/json_showcase.fun): - Attempts to read a non-existent file to show defensive behavior. - Loads examples/data/complex.json, accesses nested fields, constructs a summary map, and writes pretty JSON to /tmp. +### CURL (optional) + +Build flag: -DFUN_WITH_CURL=ON. Requires libcurl (development headers) available on your system. If built without CURL, the functions below still exist but safely degrade: they return an empty string "" (for curl_get/curl_post) or 0 (for curl_download). + +VM functions (minimal interface similar to JSON builtins): +- curl_get(url) -> string response body, or "" on error. +- curl_post(url, body) -> string response body, or "" on error. Body is sent as the raw POST body; for form-encoded data provide "key=value&..." yourself. +- curl_download(url, path) -> 1 on success, 0 on failure; saves response to the given file path. + +Notes: +- Redirects are followed automatically (CURLOPT_FOLLOWLOCATION=1L). +- TLS/HTTPS handling, proxies, etc., are handled by libcurl defaults. This minimal interface does not expose custom headers or advanced options. + +Examples: +- examples/curl_get_json.fun — GETs JSON from httpbin and parses it with json_parse when JSON is enabled. +- examples/curl_post.fun — POSTs simple form data to httpbin and prints the echoed response. +- examples/curl_download.fun — Downloads an image to ./downloaded.png and reports success. + ### PCSC (optional) Build flag: -DFUN_WITH_PCSC=ON. Requires PC/SC (e.g., pcsc-lite on Unix) and a reader. VM opcodes are wrapped by global functions as listed under Built-ins. @@ -431,6 +450,9 @@ Below is a catalog of the examples folder with brief explanations of what happen - inheritance_demo.fun — Class inheritance patterns and method overriding. - input_example.fun — Reading from stdin using Console.ask/prompt. - json_showcase.fun — Comprehensive demo of JSON.parse/stringify/from_file/to_file; prints nested values and writes to /tmp. +- curl_get_json.fun — Fetches JSON over HTTP using curl_get and parses it with json_parse if available. +- curl_post.fun — Sends a POST request and prints the raw response; parses headers when JSON is enabled. +- curl_download.fun — Downloads a file to disk and prints whether it succeeded. - loops_break_continue.fun — Shows break and continue in loops and their effects on control flow. - md5_demo.fun — Hashing data using lib/crypt/md5.fun and printing the digest. - namespaced_mod.fun — Module used by include_namespace.fun to demonstrate namespacing. From e86062aaad7776e7c1c57392ad17eac6c0abb6e0 Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 26 Nov 2025 00:57:45 +0100 Subject: [PATCH 15/55] Handbook update. No code changes. (0.30.0) --- README.md | 96 ----------------------------------------------- docs/handbook.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 5030290..0d9dd59 100644 --- a/README.md +++ b/README.md @@ -86,102 +86,6 @@ In the [examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directo A complete API documentation will follow. -## Development - -This section is a work in progress... Please excuse the lack of more information. There are daily updates here. - -### Rules - -- Every commit message must contain the version at the end in the following format (1.2.3) -- Every commit requires a version incrementation in CMakeLists.txt before committing. Documentation updates do not increment the version but must contain the current version in each commit message. -- Version numbering follows "[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html)" - -### Development systems - -- [GNU](https://gnu.org/)/[Linux](https://kernel.org/) ([Arch](https://archlinux.org/)/[Artix](https://artixlinux.org/), [Debian](https://www.debian.org/)) using [GCC](https://gcc.gnu.org/) and the [GNU C library](https://www.gnu.org/software/libc/) ([glibc](https://en.wikipedia.org/wiki/Glibc)) -- GNU/Linux ([Alpine](https://alpinelinux.org/)) using GCC and the [musl libc](https://musl.libc.org/) -- [FreeBSD](https://www.freebsd.org/) using [Clang](https://clang.llvm.org/) and the [BSD libc](https://en.wikipedia.org/wiki/C_standard_library#BSD_libc) -- [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) using [Cygwin](https://www.cygwin.com/) and GCC. - -### Other systems - -- [macOS](https://en.wikipedia.org/wiki/MacOS), [NetBSD](https://netbsd.org/), [OpenBSD](https://www.openbsd.org/), etc. should fully work, but I don't know. I do not use these systems actually. You wanna try and report? - -### To Do - -Everything... ;) No, a lot of stuff works already, but only a tiny set of functionality is available in the Fun programming language. It grows from day to day... - -### Build Fun - -Linux/UNIX only covered here for now. - -Clone repository: - -```bash -git clone https://git.xw3.org/fun/fun.git -``` - -Change directory: - -```bash -cd fun -``` - -Build: - -```bash -# Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON -cmake --build build --target fun -``` - -CMake options you can toggle (all require NAME=VALUE): - -- FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) -- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) -- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) -- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) -- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) - -That's it! For testing it, run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./demo.fun -``` - -To see what's going on, run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun --trace ./demo.fun -``` - -To switch into the REPL after an error, run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error --trace ./demo.fun -``` - -Both --repl-on-error and --trace are optional but can always be combined. To get -more debug information, you need to build Fun with -DFUN_DEBUG=ON. - -To directly run the REPL, you have to run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun -``` - -But be sure to build Fun with -DFUN_WITH_REPL=ON. - -Tip: If you saw an error like this when configuring with CMake: - - CMake Error: Parse error in command line argument: FUN_WITH_JSON - Should be: VAR:type=value - -it means a -D flag was passed without a value. Always specify options as -DNAME=VALUE, for example: - - -DFUN_WITH_JSON=ON - ## Author Johannes Findeisen diff --git a/docs/handbook.md b/docs/handbook.md index 460ca53..ba6b434 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -506,6 +506,104 @@ The PCSC functions in the VM interface with pcsc-lite/WinSCard. Transmit returns --- +## Development + +This section is a work in progress... Please excuse the lack of more information. There are daily updates here. + +### Rules + +- Every commit message must contain the version at the end in the following format (1.2.3) +- Every commit requires a version incrementation in CMakeLists.txt before committing. Documentation updates do not increment the version but must contain the current version in each commit message. +- Version numbering follows "[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html)" + +### Development systems + +- [GNU](https://gnu.org/)/[Linux](https://kernel.org/) ([Arch](https://archlinux.org/)/[Artix](https://artixlinux.org/), [Debian](https://www.debian.org/)) using [GCC](https://gcc.gnu.org/) and the [GNU C library](https://www.gnu.org/software/libc/) ([glibc](https://en.wikipedia.org/wiki/Glibc)) +- GNU/Linux ([Alpine](https://alpinelinux.org/)) using GCC and the [musl libc](https://musl.libc.org/) +- [FreeBSD](https://www.freebsd.org/) using [Clang](https://clang.llvm.org/) and the [BSD libc](https://en.wikipedia.org/wiki/C_standard_library#BSD_libc) +- [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) using [Cygwin](https://www.cygwin.com/) and GCC. + +### Other systems + +- [macOS](https://en.wikipedia.org/wiki/MacOS), [NetBSD](https://netbsd.org/), [OpenBSD](https://www.openbsd.org/), etc. should fully work, but I don't know. I do not use these systems actually. You wanna try and report? + +### To Do + +Everything... ;) No, a lot of stuff works already, but only a tiny set of functionality is available in the Fun programming language. It grows from day to day... + +### Build Fun + +Linux/UNIX only covered here for now. + +Clone repository: + +```bash +git clone https://git.xw3.org/fun/fun.git +``` + +Change directory: + +```bash +cd fun +``` + +Build: + +```bash +# Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) +cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON +cmake --build build --target fun +``` + +CMake options you can toggle (all require NAME=VALUE): + +- FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) +- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) +- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) +- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) +- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) +- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) + +That's it! For testing it, run: + +```bash +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./demo.fun +``` + +To see what's going on, run: + +```bash +FUN_LIB_DIR="$(pwd)/lib" ./build/fun --trace ./demo.fun +``` + +To switch into the REPL after an error, run: + +```bash +FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error --trace ./demo.fun +``` + +Both --repl-on-error and --trace are optional but can always be combined. To get +more debug information, you need to build Fun with -DFUN_DEBUG=ON. + +To directly run the REPL, you have to run: + +```bash +FUN_LIB_DIR="$(pwd)/lib" ./build/fun +``` + +But be sure to build Fun with -DFUN_WITH_REPL=ON. + +Tip: If you saw an error like this when configuring with CMake: + + CMake Error: Parse error in command line argument: FUN_WITH_JSON + Should be: VAR:type=value + +it means a -D flag was passed without a value. Always specify options as -DNAME=VALUE, for example: + + -DFUN_WITH_JSON=ON + +--- + ## Contributing and further reading - Browse lib/ for up-to-date stdlib APIs; many files document their own public interfaces in comments at the top. From 3411a4d84603dab17200f7caea850673129c6e8b Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 26 Nov 2025 20:36:40 +0100 Subject: [PATCH 16/55] Added CRC32 and CRC32C classes to stdlib. (0.31.0) --- CMakeLists.txt | 2 +- README.md | 2 + examples/crc32_example.fun | 54 +++++++++++++ examples/crc32c_example.fun | 54 +++++++++++++ lib/crypt/crc32.fun | 156 ++++++++++++++++++++++++++++++++++++ lib/crypt/crc32c.fun | 156 ++++++++++++++++++++++++++++++++++++ lib/io/pcsc.fun | 3 +- 7 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 examples/crc32_example.fun create mode 100644 examples/crc32c_example.fun create mode 100644 lib/crypt/crc32.fun create mode 100644 lib/crypt/crc32c.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c81c1c..b17909b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.30.0 LANGUAGES C) +project(fun VERSION 0.31.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index 0d9dd59..13548b4 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☐ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ +- [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ +- [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ ☑ = Done / ☐ = Planned or in progress. diff --git a/examples/crc32_example.fun b/examples/crc32_example.fun new file mode 100644 index 0000000..830cbab --- /dev/null +++ b/examples/crc32_example.fun @@ -0,0 +1,54 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +// Example: Using the CRC32 class from lib/crypt/crc32.fun + +include + +print("-- CRC32 example --") + +c = CRC32() + +// 1) Known test vector: "123456789" -> cbf43926 +msg = "123456789" +crc1 = c.crc32_str(msg) +print("Input (ASCII): " + msg) +print("CRC32: " + crc1) // expected: cbf43926 + +print("") + +// 2) Same data provided as hex string +hex_msg = "313233343536373839" // hex for "123456789" +crc2 = c.crc32_hex(hex_msg) +print("Input (hex): " + hex_msg) +print("CRC32: " + crc2) // expected: cbf43926 + +print("") + +// 3) Another quick demo +other = "Fun language" +crc3 = c.crc32_str(other) +print("Input (ASCII): " + other) +print("CRC32: " + crc3) + +/* Expected output: +-- CRC32 example -- +Input (ASCII): 123456789 +CRC32: cbf43926 + +Input (hex): 313233343536373839 +CRC32: cbf43926 + +Input (ASCII): Fun language +CRC32: d7d83272 +*/ diff --git a/examples/crc32c_example.fun b/examples/crc32c_example.fun new file mode 100644 index 0000000..dfbb8c5 --- /dev/null +++ b/examples/crc32c_example.fun @@ -0,0 +1,54 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +// Example: Using the CRC32C class from lib/crypt/crc32c.fun + +include + +print("-- CRC32C example --") + +c = CRC32C() + +// 1) Known test vector: "123456789" -> e3069283 +msg = "123456789" +crc1 = c.crc32c_str(msg) +print("Input (ASCII): " + msg) +print("CRC32C: " + crc1) // expected: e3069283 + +print("") + +// 2) Same data provided as hex string +hex_msg = "313233343536373839" // hex for "123456789" +crc2 = c.crc32c_hex(hex_msg) +print("Input (hex): " + hex_msg) +print("CRC32C: " + crc2) // expected: e3069283 + +print("") + +// 3) Another quick demo +other = "Fun language" +crc3 = c.crc32c_str(other) +print("Input (ASCII): " + other) +print("CRC32C: " + crc3) + +/* Expected output: +-- CRC32C example -- +Input (ASCII): 123456789 +CRC32C: e3069283 + +Input (hex): 313233343536373839 +CRC32C: e3069283 + +Input (ASCII): Fun language +CRC32C: c0158b58 +*/ diff --git a/lib/crypt/crc32.fun b/lib/crypt/crc32.fun new file mode 100644 index 0000000..0759a90 --- /dev/null +++ b/lib/crypt/crc32.fun @@ -0,0 +1,156 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +// lib/crypt/crc32.fun +// Pure Fun implementation of CRC-32 (IEEE 802.3) operating on hex-string input. +// Reflected polynomial: 0xEDB88320 +// Initial value: 0xFFFFFFFF, Final XOR: 0xFFFFFFFF +// +// Public API (class methods): +// crc32_hex(hexStr) -> 8-char lowercase hex string +// crc32_str(str) -> 8-char lowercase string (ASCII input) +// +// Example: +// // "123456789" CRC32 is cbf43926 +// c = CRC32() +// print(c.crc32_str("123456789")) + +#include + +class CRC32() + // 32-bit helpers + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + fun shr32(this, x, s) + return shr(this.u32(x), s) + + fun shl32(this, x, s) + return shl(this.u32(x), s) + + fun xor32(this, a, b) + return bxor(this.u32(a), this.u32(b)) + + fun and32(this, a, b) + return band(this.u32(a), this.u32(b)) + + // hex helpers (mirroring style from lib/crypt/md5.fun) + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + b = this.byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + parts = [d[hi], d[lo]] + return join(parts, "") + + fun bytes_to_hex(this, arr) + i = 0 + out = [] + while i < len(arr) + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + fun u32_to_hex8(this, n) + // big-endian printing (MSB first), common for CRC displays + b3 = this.and32(this.shr32(n, 24), 255) + b2 = this.and32(this.shr32(n, 16), 255) + b1 = this.and32(this.shr32(n, 8), 255) + b0 = this.and32(n, 255) + return this.bytes_to_hex([b3, b2, b1, b0]) + + // Bitwise update (no table needed) using reflected polynomial 0xEDB88320 + POLY = 3988292384 // 0xEDB88320 + + // Compute CRC32 over byte array, return u32 value (bitwise, reflected) + fun crc32_bytes_value(this, bytes) + crc = 4294967295 // 0xFFFFFFFF + i = 0 + n = len(bytes) + while i < n + crc = this.xor32(crc, bytes[i]) + j = 0 + while j < 8 + if (this.and32(crc, 1) == 1) + crc = this.xor32(this.shr32(crc, 1), this.POLY) + else + crc = this.shr32(crc, 1) + j = j + 1 + i = i + 1 + return this.xor32(crc, 4294967295) // final XOR + + // Public: compute CRC32 of hex string of bytes, return 8-char hex + fun crc32_hex(this, hexStr) + bytes = this.from_hex(hexStr) + v = this.crc32_bytes_value(bytes) + return this.u32_to_hex8(v) + + // Convenience: compute CRC32 of ASCII string + fun crc32_str(this, str) + bytes = string_to_bytes_ascii(str) + v = this.crc32_bytes_value(bytes) + return this.u32_to_hex8(v) diff --git a/lib/crypt/crc32c.fun b/lib/crypt/crc32c.fun new file mode 100644 index 0000000..8a9cbd0 --- /dev/null +++ b/lib/crypt/crc32c.fun @@ -0,0 +1,156 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +// lib/crypt/crc32c.fun +// Pure Fun implementation of CRC-32C (Castagnoli) operating on hex-string input. +// Polynomial (reflected): 0x82F63B78 +// Initial value: 0xFFFFFFFF, Final XOR: 0xFFFFFFFF +// +// Public API (class methods): +// crc32c_hex(hexStr) -> 8-char lowercase hex string +// crc32c_str(str) -> 8-char lowercase string (ASCII input) +// +// Example: +// // "123456789" in ASCII is 313233343536373839 in hex, CRC32C is e3069283 +// c = CRC32C() +// print(c.crc32c_hex("313233343536373839")) + +#include + +class CRC32C() + // 32-bit helpers + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + fun shr32(this, x, s) + return shr(this.u32(x), s) + + fun shl32(this, x, s) + return shl(this.u32(x), s) + + fun xor32(this, a, b) + return bxor(this.u32(a), this.u32(b)) + + fun and32(this, a, b) + return band(this.u32(a), this.u32(b)) + + // hex helpers (mirroring style from lib/crypt/md5.fun) + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + b = this.byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + parts = [d[hi], d[lo]] + return join(parts, "") + + fun bytes_to_hex(this, arr) + i = 0 + out = [] + while i < len(arr) + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + fun u32_to_hex8(this, n) + // big-endian printing (MSB first), common for CRC displays + b3 = this.and32(this.shr32(n, 24), 255) + b2 = this.and32(this.shr32(n, 16), 255) + b1 = this.and32(this.shr32(n, 8), 255) + b0 = this.and32(n, 255) + return this.bytes_to_hex([b3, b2, b1, b0]) + + // Bitwise update (no table needed) using reflected polynomial 0x82F63B78 + POLY = 2197175160 // 0x82F63B78 + + // Compute CRC32C over byte array, return u32 value (bitwise, reflected) + fun crc32c_bytes_value(this, bytes) + crc = 4294967295 // 0xFFFFFFFF + i = 0 + n = len(bytes) + while i < n + crc = this.xor32(crc, bytes[i]) + j = 0 + while j < 8 + if (this.and32(crc, 1) == 1) + crc = this.xor32(this.shr32(crc, 1), this.POLY) + else + crc = this.shr32(crc, 1) + j = j + 1 + i = i + 1 + return this.xor32(crc, 4294967295) // final XOR + + // Public: compute CRC32C of hex string of bytes, return 8-char hex + fun crc32c_hex(this, hexStr) + bytes = this.from_hex(hexStr) + v = this.crc32c_bytes_value(bytes) + return this.u32_to_hex8(v) + + // Convenience: compute CRC32C of ASCII string + fun crc32c_str(this, str) + bytes = string_to_bytes_ascii(str) + v = this.crc32c_bytes_value(bytes) + return this.u32_to_hex8(v) diff --git a/lib/io/pcsc.fun b/lib/io/pcsc.fun index 022226b..feb9263 100644 --- a/lib/io/pcsc.fun +++ b/lib/io/pcsc.fun @@ -79,6 +79,7 @@ class PCSC() return m */ +// This class is in a very early stage of development. class PCSC() fun get_readers(this) ctx = pcsc_establish() @@ -109,7 +110,7 @@ class PCSC() m["sw2"] = -1 m["code"] = -2 return m - // Actually selecting the second reader hardcode. This class is not in a very early stage of development. + // Actually selecting the second reader hardcoded. handle = pcsc_connect(ctx, readers[1]) apdu = this.hex_to_bytes(hex_apdu) res = pcsc_transmit(handle, apdu) From 9416ec3457b21de1afcaf7987f6dbd4e851086cf Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 26 Nov 2025 22:21:31 +0100 Subject: [PATCH 17/55] Added SQLite support and updated the Handbook. (0.32.0) --- .gitignore | 1 + CMakeLists.txt | 37 ++- README.md | 4 +- docs/handbook.md | 645 +++++++++++++++--------------------- examples/data/todo.sql | 12 + examples/sqlite_example.fun | 37 +++ src/bytecode.c | 4 + src/bytecode.h | 6 + src/parser.c | 37 +++ src/value.c | 9 + src/vm.c | 10 + src/vm.h | 1 + src/vm/sqlite/close.c | 32 ++ src/vm/sqlite/common.c | 49 +++ src/vm/sqlite/exec.c | 36 ++ src/vm/sqlite/open.c | 37 +++ src/vm/sqlite/query.c | 62 ++++ 17 files changed, 631 insertions(+), 388 deletions(-) create mode 100644 examples/data/todo.sql create mode 100644 examples/sqlite_example.fun create mode 100644 src/vm/sqlite/close.c create mode 100644 src/vm/sqlite/common.c create mode 100644 src/vm/sqlite/exec.c create mode 100644 src/vm/sqlite/open.c create mode 100644 src/vm/sqlite/query.c diff --git a/.gitignore b/.gitignore index 5870479..6e70192 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ out/ src/*.o *.swp tmp* +todo.sqlite diff --git a/CMakeLists.txt b/CMakeLists.txt index b17909b..c8c1698 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.31.0 LANGUAGES C) +project(fun VERSION 0.32.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -26,7 +26,32 @@ else() endif() # Ensure trailing slash if(NOT DEFAULT_LIB_DIR MATCHES "/$") - set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}/") + set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}/") +endif() + +# Optional SQLite support +option(FUN_WITH_SQLITE "Enable SQLite (sqlite3) support" OFF) +set(SQLITE3_INCLUDE_DIRS "") +set(SQLITE3_LINK_LIBS "") +if(FUN_WITH_SQLITE) + message(STATUS "Building with SQLite support") + add_definitions(-DFUN_WITH_SQLITE) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(SQLITE3 QUIET sqlite3) + endif() + if(SQLITE3_FOUND) + list(APPEND SQLITE3_INCLUDE_DIRS ${SQLITE3_INCLUDE_DIRS} ${SQLITE3_INCLUDE_DIRS}) + list(APPEND SQLITE3_LINK_LIBS ${SQLITE3_LINK_LIBS} ${SQLITE3_LIBRARIES}) + include_directories(${SQLITE3_INCLUDE_DIRS}) + else() + find_library(SQLITE3_LIB sqlite3) + if(SQLITE3_LIB) + list(APPEND SQLITE3_LINK_LIBS ${SQLITE3_LIB}) + else() + message(FATAL_ERROR "sqlite3 not found. Install sqlite3 (dev headers) or disable FUN_WITH_SQLITE.") + endif() + endif() endif() # Optional PCSC support (enabled via -DFUN_WITH_PCSC=ON) @@ -168,6 +193,14 @@ if(CURL_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${CURL_LINK_LIBS}) endif() +# sqlite3 include and link (if enabled) +if(SQLITE3_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${SQLITE3_INCLUDE_DIRS}) +endif() +if(SQLITE3_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${SQLITE3_LINK_LIBS}) +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index 13548b4..61b74f4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (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) ☐ +- [SQLite](https://sqlite.org/) support builtin (optional) ☑ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ - [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ - [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ @@ -86,7 +86,7 @@ Current documentation is only found in the [Fun Handbook](https://git.xw3.org/fu In the [examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directory should be an example of most Fun features. -A complete API documentation will follow. +Complete API documentation will follow. ## Author diff --git a/docs/handbook.md b/docs/handbook.md index ba6b434..5b55c28 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -1,123 +1,146 @@ -# Fun Handbook +# Fun Handbook (Second Edition) + +This is a refreshed, de-duplicated, and fully up-to-date handbook for the Fun programming language and its virtual machine (VM). It keeps the same section layout as the original handbook while consolidating repeated content and documenting all currently available features, including the latest SQLite support. ## Overview +Fun is a small, strict, and simple programming language executed by a stack-based virtual machine. Most of the ecosystem is written in Fun itself; only a minimal core is implemented in C. The design focuses on simplicity, consistency, and joy in coding. + ## Introduction +- Dynamic and optionally statically typed +- Type safety +- Written in C (C99) and Fun +- Minimal C core; most core functions and libraries implemented in Fun +- Internal libraries use snake_case for functions even when written in Fun; class names are CamelCase + ## Installation ### Requirements -A C compiler, a libc and [Git](https://git-scm.com/). +- A C compiler, a libc, and Git -#### FreeBSD +FreeBSD: +- CMake +- Clang -- [CMake](https://cmake.org/) -- [Clang](https://clang.llvm.org/) +Linux: +- CMake +- GCC (Clang should also work) -#### Linux - -- [CMake](https://cmake.org/) -- [GCC](https://gcc.gnu.org/) (Clang should work here too, not tested!) - -#### Windows - -This requires Cygwin to be installed and configured. I will not cover this here. - -- [CMake](https://cmake.org/) -- [Cygwin](https://cygwin.com/) using [GCC](https://gcc.gnu.org/) +Windows: +- Cygwin (not covered in detail here) +- CMake +- GCC via Cygwin ### Build Fun -Linux/UNIX and Cygwin only covered here for now. +Linux/UNIX and Cygwin are covered here. Clone repository: -```bash -git clone https://git.xw3.org/fun/fun.git ``` - -Change directory: - -```bash +git clone https://git.xw3.org/fun/fun.git cd fun ``` -Build: +Configure and build (examples shown with several optional features enabled): -```bash -# Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON -DFUN_WITH_PCRE2=ON -DFUN_WITH_CURL=ON +``` +# Every -D flag must be NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) +cmake -S . -B build \ + -DFUN_DEBUG=OFF \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_JSON=ON \ + -DFUN_WITH_PCRE2=ON \ + -DFUN_WITH_CURL=ON \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_SQLITE=OFF cmake --build build --target fun ``` -That's it! For testing it, run: +Run the demo (without installing): -```bash +``` FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./demo.fun ``` -To see what's going on, run: +Tracing execution: -```bash +``` FUN_LIB_DIR="$(pwd)/lib" ./build/fun --trace ./demo.fun ``` -To switch into the REPL after an error, run: +Drop into the REPL when an error occurs: -```bash +``` FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error --trace ./demo.fun ``` -Both --repl-on-error and --trace are optional but can always be combined. To get -more debug information, you need to build Fun with -DFUN_DEBUG=ON. +Start the REPL directly (build with -DFUN_WITH_REPL=ON): -To directly run the REPL, you have to run: - -```bash +``` FUN_LIB_DIR="$(pwd)/lib" ./build/fun ``` -But be sure to build Fun with -DFUN_WITH_REPL=ON. - #### CMake options -All CMake options must be passed as -DNAME=VALUE: +Pass all options as -DNAME=VALUE. The most relevant toggles are: -- FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) +- 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_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) -- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) -- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) +- 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) +- FUN_WITH_SQLITE=ON|OFF — enable SQLite (sqlite3) support (default OFF) -If you encounter an error such as: +You can also set the default search path for the bundled stdlib with DEFAULT_LIB_DIR: + +``` +cmake -S . -B build -DDEFAULT_LIB_DIR="/usr/share/fun/lib" -DFUN_WITH_REPL=ON +``` + +If you encounter a CMake error such as: CMake Error: Parse error in command line argument: FUN_WITH_JSON Should be: VAR:type=value -then a -D option was given without a value. Always use -DNAME=VALUE, for example -DFUN_WITH_JSON=ON. +it means you passed a -D option without a value. Always use the form -DNAME=VALUE (e.g., -DFUN_WITH_JSON=ON). -### Install Fun to OS +#### SQLite example (optional feature) -I do not recommend installing Fun on your system because it is in a very early -stage of development, but I can say that I have Fun installed on my system. If -you want to do that too, type: +SQLite support is optional and disabled by default. To build with it and run the example: -```bash +``` +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 + +# Run the example +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/sqlite_example.fun +``` + +### Install Fun to the OS (optional) + +Not recommended during early development, but supported: + +``` sudo cmake --build build --target install ``` -Now run Fun without prefixed FUN_LIB_DIR="$(pwd)/lib" because libs are installed to the -system default lib directory (/usr/share/fun/lib). +After installation, FUN_LIB_DIR usually isn’t needed because libs are placed in the system default directory (e.g., /usr/share/fun/lib). ## Usage -```bash +Run a script: + +``` fun ./demo.fun ``` - ## Table of contents - Language overview and VM internals @@ -138,172 +161,171 @@ fun ./demo.fun - JSON (via json-c) - CURL (via libcurl) - PCSC (PC/SC smart card) -- Examples reference (what each example does) + - SQLite (sqlite3) +- Examples reference --- ## Language overview and VM internals -Fun is a small imperative language executed by a register-less stack-based virtual machine (VM). Source files are compiled to bytecode; the VM executes opcodes that work on a value stack. Functions and methods push/pop their arguments and return values on that stack. +Fun compiles .fun source files to bytecode and executes them on a stack-based VM. Functions and methods push/pop their arguments and return values on a value stack. High-level architecture: -- Front-end: parses .fun files, handles includes and constant folding, emits bytecode with debug markers (OP_LINE) for tracing and REPL-on-error. -- VM core: runs a loop over opcodes (see src/bytecode.h). Values include numbers (integers), strings, arrays, maps, booleans (0/1), functions, and nil. -- Built-ins: I/O, strings, arrays, regex, date/time, OS, networking, threading, optional JSON and PC/SC. Many are exposed as opcodes with friendly global functions in the language. +- Front-end: parses .fun files, handles includes and constant folding, emits bytecode with debug markers (OP_LINE) used by tracing and REPL-on-error. +- VM core: runs a loop over opcodes (see src/bytecode.h). Values include numbers (integers), strings, arrays, maps, booleans (1/0), functions, and nil. +- Built-ins: I/O, strings, arrays, regex, date/time, OS, networking, threading, and optional JSON/PCRE2/CURL/PCSC/SQLite. -Key VM concepts (non-exhaustive): -- Control flow: OP_JUMP, OP_JUMP_IF_FALSE, OP_RETURN. -- Arithmetic and logic: OP_ADD/SUB/MUL/DIV, OP_MOD, comparisons (OP_LT, OP_LTE, OP_GT, OP_GTE, OP_EQ, OP_NEQ), logical OP_AND/OR/NOT. -- Stack helpers: OP_DUP, OP_SWAP, OP_POP. -- Arrays: OP_MAKE_ARRAY, OP_INDEX_GET/SET, OP_LEN, OP_PUSH, OP_APOP (pop last), OP_INSERT/REMOVE, OP_SLICE. -- Strings: OP_SUBSTR, OP_SPLIT, OP_JOIN, OP_FIND. -- Maps: OP_MAKE_MAP and index ops reuse array/map machinery; you can index with string keys. -- Conversion and typing: OP_TO_NUMBER, OP_TO_STRING, OP_CAST, OP_TYPEOF, unsigned/signed clamps (OP_UCLAMP/OP_SCLAMP). -- Regex: OP_REGEX_MATCH/SEARCH/REPLACE. -- Math misc: OP_MIN/MAX/CLAMP/ABS/POW/RANDOM_SEED/RANDOM_INT. -- Iteration helpers: OP_ENUMERATE, OP_ZIP. -- OS/IO/network: socket ops, file ops, process execution, environment, threads, etc., implemented as built-ins. -- Optional features: JSON opcodes (OP_JSON_PARSE and friends in src/vm/json/*) are compiled in only if -DFUN_WITH_JSON=ON. CURL builtins (curl_get/curl_post/curl_download) are available if -DFUN_WITH_CURL=ON. PCSC opcodes are available if -DFUN_WITH_PCSC=ON. +Selected VM concepts (non-exhaustive): +- Control flow: OP_JUMP, OP_JUMP_IF_FALSE, OP_RETURN +- Arithmetic/logic: OP_ADD/SUB/MUL/DIV, OP_MOD, OP_LT/LTE/GT/GTE, OP_EQ/NEQ, OP_AND/OR/NOT +- Stack helpers: OP_DUP, OP_SWAP, OP_POP +- Arrays: OP_MAKE_ARRAY, OP_INDEX_GET/SET, OP_LEN, OP_PUSH, OP_APOP, OP_INSERT/REMOVE, OP_SLICE +- Strings: OP_SUBSTR, OP_SPLIT, OP_JOIN, OP_FIND +- Maps: OP_MAKE_MAP; index ops shared with arrays +- Conversion/typing: OP_TO_NUMBER, OP_TO_STRING, OP_CAST, OP_TYPEOF, OP_UCLAMP/OP_SCLAMP +- Regex: OP_REGEX_MATCH/SEARCH/REPLACE (requires PCRE2 when built) +- Math: OP_MIN/MAX/CLAMP/ABS/POW, OP_RANDOM_SEED/RANDOM_INT +- Iteration helpers: OP_ENUMERATE, OP_ZIP +- OS/IO/network: sockets, files, processes, environment, threads, etc. +- Optional features: JSON (src/vm/json/*), CURL, PCSC, SQLite (src/vm/sqlite/*) Error handling and debugging: -- Build with FUN_DEBUG=ON for verbose VM traces. -- Run with --trace to print executed lines and opcodes. -- Run with --repl-on-error to drop into an interactive REPL when a runtime error occurs, allowing inspection of variables and stepping. +- Build with FUN_DEBUG=ON for verbose traces +- Run with --trace to print executed lines/opcodes +- Run with --repl-on-error to drop into an interactive REPL when a runtime error occurs ## Command line interface and REPL -Running a script: -- fun path/to/script.fun -- Options: --trace, --repl-on-error (can combine), see build section for REPL availability. - -REPL: -- Launch with fun (no script) when built with FUN_WITH_REPL=ON. -- In trace/REPL-on-error mode, the VM annotates output with file:line and function names to aid debugging (see examples/debug_reporting.fun and examples/repl_on_error.fun). +- Run a script: fun path/to/script.fun +- Common options: --trace, --repl-on-error (can be combined). REPL requires FUN_WITH_REPL=ON at build time. +- In trace/REPL-on-error modes, the VM annotates output with file:line and function names for easier debugging (see examples/debug_reporting.fun). ## Core types and operations Types: -- number: signed integer. Conversions: to_number(x). Bitwise ops exist via bnot, band, bor, bxor, shl, shr, rol, ror in stdlib/VM. -- string: immutable sequence of bytes; length via len(s); concatenate via join([a,b], ""). Substring: substr(s, start, len). Find: find(haystack, needle) returns index or -1. -- array: ordered list. Create with [a, b, c] or built-ins. len(a), push(a, v) appends, apop(a) removes last, insert(a, idx, v), remove(a, idx), slice(a, start, end). -- map: associative dictionary with string keys typically: m = {}; m["key"] = value; keys can be strings and sometimes numbers. -- boolean: represented as number 1 (true) or 0 (false). Logical operators: &&, ||, !. -- nil: absence of value. Many defensive stdlib wrappers return [] or {} or nil defaults on errors. +- number: signed integer (with helpers for unsigned behavior) +- string: immutable bytes; len(s), join, split, substr, find +- array: ordered list; len, push, apop, insert, remove, slice +- map: associative dictionary typically keyed by strings +- boolean: represented as 1 (true) or 0 (false); operators &&, ||, ! +- nil: absence of value Control flow: -- if/else, while loops, for-like range utilities (see utils.range in stdlib), break/continue (see examples/loops_break_continue.fun). +- if/else, while; range helpers in utils.range Functions and classes: -- Define a function with fun name(args) ... -- Define a class with class Name(constructor params) and methods fun method(this, ...) ...; _construct is called as a constructor if present. -- Methods use explicit this. +- Define a function: fun name(args) ... +- Define a class: class Name(constructor params) with method definitions fun method(this, ...) +- _construct acts as the constructor if present; methods use explicit this Modules and includes: -- Use #include to include from FUN_LIB_DIR. -- Use #include "relative/path.fun" to include a file relative to your script. -- You can alias includes with "as" to create namespaces: #include as m; then call m.add(...). +- #include for libs under FUN_LIB_DIR +- #include "relative/path.fun" for local includes +- Namespacing via as: #include as m; then call m.add(...) ## Built-ins overview Console and I/O: -- print(x): prints a value with a trailing newline. input(prompt): returns a line as string without trailing newline. +- print(x) — prints value plus newline +- input(prompt) — read line from stdin Strings and arrays: -- len(x), join(array, sep), split(string, sep), substr(string, start, len), find(haystack, needle), push(array, value), apop(array), insert(array, idx, value), remove(array, idx), slice(array, start, end). +- len(x), join(array, sep), split(text, sep), substr(text, start, len), find(text, needle) +- push(array, v), apop(array), insert(array, i, v), remove(array, i), slice(array, start, end) Conversion and type: -- to_number(x), to_string(x), cast(value, typeName), typeof(x), uclamp(number, bits), sclamp(number, bits). +- to_number(x), to_string(x), cast(value, typeName), typeof(x) +- uclamp(number, bits), sclamp(number, bits) Math and random: -- min(a,b), max(a,b), clamp(x, lo, hi), abs(x), pow(a,b), random_seed(seed), random_int(lo, hiExclusive). +- min(a,b), max(a,b), clamp(x, lo, hi), abs(x), pow(a,b), random_seed(seed), random_int(lo, hiExclusive) -Regex: -- regex_match(text, pattern) -> 1/0 full match -- regex_search(text, pattern) -> map {"match", "start", "end", "groups"} -- regex_replace(text, pattern, repl) -> string with global replacements +Regex (requires PCRE2 when enabled): +- regex_match(text, pattern) -> 1/0 +- regex_search(text, pattern) -> map { match, start, end, groups } +- regex_replace(text, pattern, repl) -> string OS and processes: -- proc_run(cmd) -> map {"out": string, "code": number} -- system(cmd) -> exit code number -- env_get(name)/env_set(name, value) – see examples/os_env.fun +- proc_run(cmd) -> { out: string, code: number } +- system(cmd) -> exit code +- env_get(name), env_set(name, value) Networking and sockets: - tcp_connect(host, port) -> fd (>0) or 0 -- sock_send(fd, string) -> bytes sent or -1, sock_recv(fd, maxlen) -> string, sock_close(fd) -- tcp_listen(port, backlog) -> listen fd, tcp_accept(listenFd) -> client fd -- unix_connect(path) -> fd for UNIX domain sockets +- sock_send(fd, data) -> bytes or -1; sock_recv(fd, maxlen) -> string; sock_close(fd) +- tcp_listen(port, backlog) -> listen fd; tcp_accept(listenFd) -> client fd +- unix_connect(path) -> fd Threads: - thread_spawn(func, args) -> thread id; thread_join(id) -> return value Date and time: -- time_now_ms() -> epoch ms; clock_mono_ms() -> monotonic ms; date_format(ms, fmt) -> string +- time_now_ms(), clock_mono_ms(), date_format(ms, fmt) JSON (optional): -- json_parse(text) -> Fun value (maps/arrays/numbers/strings/1/nil) -- json_stringify(value, prettyFlag) -> string; prettyFlag: 0/1 -- json_from_file(path) -> value or nil; json_to_file(path, value, prettyFlag) -> 1/0 +- json_parse(text) -> value or nil +- json_stringify(value, prettyFlag) -> string +- json_from_file(path) -> value or nil +- json_to_file(path, value, prettyFlag) -> 1/0 PC/SC (optional): - pcsc_establish() -> context id (>0) or 0 -- pcsc_list_readers(ctx) -> array of reader names (strings) or nil +- pcsc_list_readers(ctx) -> array of reader names or nil - pcsc_connect(ctx, readerName) -> handle id (>0) or 0 - pcsc_disconnect(handle) -> 1/0 -- pcsc_transmit(handle, bytesArray) -> map {"data": array of numbers, "sw1": n, "sw2": n, "code": n} +- pcsc_transmit(handle, bytesArray) -> { data, sw1, sw2, code } -Note: Optional feature availability depends on your CMake flags at build time. +SQLite (optional): +- sqlite_open(path) -> handle (>0) or 0 on error +- sqlite_exec(handle, sql) -> rc (0 = SQLITE_OK) +- sqlite_query(handle, sql) -> array of row maps (string keys) +- sqlite_close(handle) -> nil + +Note: Optional features depend on the CMake flags used when building. --- ## Standard library APIs -The stdlib provides small, defensive wrappers around VM built-ins, typically with class-based APIs to avoid global name collisions and to offer sensible defaults. +The stdlib provides small wrappers around VM built-ins, typically organized in classes to avoid global name collisions and to offer sensible defaults. ### io.console Class Console (lib/io/console.fun): -- prompt(text) -> string: print text and read a line. -- ask(question) -> string: prints "question: " and reads a line. -- ask_yes_no(question) -> 1/0: loops until user answers y/yes or n/no (case-insensitive). +- prompt(text) -> string +- ask(question) -> string +- ask_yes_no(question) -> 1/0 (y/yes vs n/no) -Example: -- See examples/input_example.fun +Example: examples/input_example.fun ### io.process Class Process (lib/io/process.fun): -- run(cmd) -> { out, code }: captures stdout and exit code. -- run_merge_stderr(cmd) -> { out, code }: appends "2>&1" to merge stderr. -- system(cmd) -> number: exit code. -- check_call(cmd) -> 1/0: 1 if exit code is 0. +- run(cmd) -> { out, code } +- run_merge_stderr(cmd) -> { out, code } +- system(cmd) -> number +- check_call(cmd) -> 1/0 -Examples: -- examples/process_example.fun +Example: examples/process_example.fun ### io.socket Provides TcpClient, TcpServer, UnixClient (lib/io/socket.fun). -Class TcpClient: -- connect(host, port) -> 1/0 -- is_connected() -> 1/0 -- send(data) -> bytes or -1 -- recv(maxlen) -> string -- recv_all(chunk_size) -> string: keeps reading until EOF or partial chunk. -- close() -> 1 - -Class TcpServer(port, backlog): -- listen() -> listen fd or 0 -- accept() -> client fd -- echo_once(maxlen) -> 1 on handled client -- serve_forever(maxlen) -> never returns; minimal echo server +TcpClient: +- connect(host, port) -> 1/0; is_connected() -> 1/0 +- send(data) -> bytes or -1; recv(maxlen) -> string; recv_all(chunk_size) -> string - close() -Class UnixClient: +TcpServer(port, backlog): +- listen() -> listen fd or 0; accept() -> client fd +- echo_once(maxlen) -> 1 when handled; serve_forever(maxlen) -> never returns +- close() + +UnixClient: - connect(path), is_connected(), send(data), recv(maxlen), close() -Examples: -- examples/tcp_http_get.fun, examples/tcp_http_get_class.fun, examples/unix_socket_echo.fun, examples/extra/tcp_echo_server_class.fun +Examples: tcp_http_get.fun, tcp_http_get_class.fun, unix_socket_echo.fun, extra/tcp_echo_server_class.fun ### io.thread @@ -311,47 +333,40 @@ Class Thread (lib/io/thread.fun): - spawn(func, args) -> thread id; join(id) -> return value - Aliases: start(func, args), wait(id) -Examples: -- examples/threads_demo.fun, examples/thread_class_example.fun +Examples: threads_demo.fun, thread_class_example.fun ### utils.datetime Class DateTime (lib/utils/datetime.fun): -- now_ms() -> current epoch milliseconds -- mono_ms() -> monotonic clock ms -- format(ms, fmt) -> string using strftime-like fmt -- iso_now() -> "YYYY-MM-DDTHH:MM:SS" +- now_ms(), mono_ms(), format(ms, fmt), iso_now() -Example: -- examples/datetime_basic.fun +Example: datetime_basic.fun ### regex Class Regex (lib/regex.fun): -- match(text, pattern) -> 1/0 full match -- search(text, pattern) -> map { match, start, end, groups } -- replace(text, pattern, repl) -> string (global) +- match(text, pattern) -> 1/0 +- search(text, pattern) -> { match, start, end, groups } +- replace(text, pattern, repl) -> string -Examples: -- examples/regex_demo.fun, examples/regex_procedural.fun +Examples: regex_demo.fun, regex_procedural.fun ### crypt -MD5 (lib/crypt/md5.fun): Pure Fun implementation with class MD5 and helper md5_hex(hexStr). See examples/md5_demo.fun. - -SHA family (lib/crypt/sha1.fun, sha256.fun, sha384.fun, sha512.fun): class wrappers SHA1/SHA256/SHA384/SHA512 with methods digest_hex_of_string(str) and helpers as documented in files. Examples: sha1_demo.fun, sha256_demo.fun, sha256_str_demo.fun, sha384_example.fun, sha512_demo.fun, sha512_str_demo.fun. +MD5 (lib/crypt/md5.fun) and SHA family (sha1/sha256/sha384/sha512) provide digest classes and helpers. +Examples: md5_demo.fun, sha1_demo.fun, sha256_demo.fun, sha256_str_demo.fun, sha384_example.fun, sha512_demo.fun, sha512_str_demo.fun ### encoding.base64 -Module lib/encoding/base64.fun provides base64_encode(string) and base64_decode(string) helpers (see file for exact APIs). Used in some examples. +Module lib/encoding/base64.fun: base64_encode(string), base64_decode(string) ### arrays, strings, maps helpers -- lib/arrays.fun: helper functions for common array patterns. -- lib/strings.fun: string helpers like str_to_lower/upper and more; used by several stdlib modules. -- lib/hex.fun: bytes_to_hex(arrayOfNumbers) and hex_to_bytes(hexString) helpers as used by PCSC. -- lib/utils/range.fun: utilities for building numeric ranges; see for_range_test.fun. -- lib/utils/math.fun and lib/math.fun: higher-level math helpers. +- lib/arrays.fun — array helpers +- lib/strings.fun — string helpers (lower/upper, etc.) +- lib/hex.fun — bytes_to_hex, hex_to_bytes +- lib/utils/range.fun — numeric ranges +- lib/utils/math.fun and lib/math.fun — math helpers --- @@ -359,55 +374,44 @@ Module lib/encoding/base64.fun provides base64_encode(string) and base64_decode( ### JSON (optional) -Build flag: -DFUN_WITH_JSON=ON. Requires json-c available on your system. Internals are in src/vm/json/ and wrap json-c to convert between json_object and Fun values. - -VM functions: -- json_parse(text) -> value or nil on parse error. -- json_stringify(value, pretty) -> string; pretty is 0/1. -- json_from_file(path) -> value or nil if file missing/unreadable. -- json_to_file(path, value, pretty) -> 1 on success else 0. - -Stdlib wrapper class JSON (lib/io/json.fun): -- parse(text) -- stringify(value, pretty=0) -- from_file(path) -- to_file(path, value, pretty=0) - -Example walkthrough (examples/json_showcase.fun): -- Parses a JSON string into a map/array structure; demonstrates indexing (obj["name"]). -- Pretty prints the object with json.stringify(obj, 1). -- Attempts to read a non-existent file to show defensive behavior. -- Loads examples/data/complex.json, accesses nested fields, constructs a summary map, and writes pretty JSON to /tmp. +Build flag: -DFUN_WITH_JSON=ON; requires json-c. VM functions: json_parse, json_stringify, json_from_file, json_to_file. Stdlib class JSON wraps these with light ergonomics. Example: examples/json_showcase.fun. ### CURL (optional) -Build flag: -DFUN_WITH_CURL=ON. Requires libcurl (development headers) available on your system. If built without CURL, the functions below still exist but safely degrade: they return an empty string "" (for curl_get/curl_post) or 0 (for curl_download). +Build flag: -DFUN_WITH_CURL=ON; requires libcurl. VM provides: +- curl_get(url) -> string ("" on error) +- curl_post(url, body) -> string ("" on error) +- curl_download(url, path) -> 1/0 -VM functions (minimal interface similar to JSON builtins): -- curl_get(url) -> string response body, or "" on error. -- curl_post(url, body) -> string response body, or "" on error. Body is sent as the raw POST body; for form-encoded data provide "key=value&..." yourself. -- curl_download(url, path) -> 1 on success, 0 on failure; saves response to the given file path. - -Notes: -- Redirects are followed automatically (CURLOPT_FOLLOWLOCATION=1L). -- TLS/HTTPS handling, proxies, etc., are handled by libcurl defaults. This minimal interface does not expose custom headers or advanced options. - -Examples: -- examples/curl_get_json.fun — GETs JSON from httpbin and parses it with json_parse when JSON is enabled. -- examples/curl_post.fun — POSTs simple form data to httpbin and prints the echoed response. -- examples/curl_download.fun — Downloads an image to ./downloaded.png and reports success. +Examples: curl_get_json.fun, curl_post.fun, curl_download.fun ### PCSC (optional) -Build flag: -DFUN_WITH_PCSC=ON. Requires PC/SC (e.g., pcsc-lite on Unix) and a reader. VM opcodes are wrapped by global functions as listed under Built-ins. +Build flag: -DFUN_WITH_PCSC=ON; provides pcsc_* built-ins and a stdlib wrapper class PCSC. Example: pcsc_example.fun. -Stdlib wrapper class PCSC (lib/io/pcsc.fun): -- get_readers() -> array of reader names. -- transmit(hex_apdu) -> map result by establishing context, selecting a reader, connecting, transmitting, and disconnecting. It returns a map with keys data (array), sw1, sw2, code. The wrapper includes defensive defaults when no reader exists. -- There is also a commented-out full-featured variant exposing establish/release/connect/disconnect/transmit_bytes/transmit_hex for advanced use. +### SQLite (optional) -Example: -- examples/pcsc_example.fun: shows establishing and transmitting an APDU, or printing []/default map if no readers present. +Build flag: -DFUN_WITH_SQLITE=ON; requires sqlite3 development headers. + +VM API: +- sqlite_open(path) -> handle (>0) or 0 +- sqlite_exec(handle, sql) -> rc (0 = SQLITE_OK) +- sqlite_query(handle, sql) -> array of maps (columns as string keys) +- sqlite_close(handle) -> nil + +Result mapping notes: +- INTEGER -> number +- FLOAT -> number (floating point) +- TEXT -> string +- NULL -> nil +- BLOB is currently not returned (mapped to nil) + +Example flow (examples/sqlite_example.fun): +1) h = sqlite_open("./todo.sqlite") +2) rows = sqlite_query(h, "SELECT id, title, done, created_at FROM tasks ORDER BY id;") +3) rc = sqlite_exec(h, "INSERT INTO tasks (title, done) VALUES ('Try Fun + SQLite', 0);") +4) rows2 = sqlite_query(h, "SELECT count(*) AS cnt FROM tasks;") +5) sqlite_close(h) --- @@ -417,196 +421,69 @@ You can run examples without installing by pointing FUN_LIB_DIR to the repositor FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/.fun -Below is a catalog of the examples folder with brief explanations of what happens in each file: - -- arrays.fun — Basic array creation, indexing, push/apop, insert/remove, slice; prints intermediate states and lengths. -- arrays_advanced.fun — More complex array transformations, enumerate/zip patterns. -- arrays_iter.fun — Iterating arrays with indices and values; demonstrates for/while patterns. -- boolean_decl.fun — Declaring and using booleans, truthy/falsey checks. -- booleans.fun — First-class booleans with logical operators and short-circuit behavior. -- builtins_conversions.fun — Using to_number, to_string, cast, typeof, uclamp/sclamp. -- builtins_extended.fun — Showcases extended built-ins like min/max/clamp/abs/pow/random. -- builtins_maps_and_more.fun — Demonstrates map creation, assignment, and index operations. -- byte_for_demo.fun — Demonstrates bitwise ops (bnot/band/bor/bxor/shl/shr/rol/ror) and numeric behavior. -- cast_demo.fun — Casting values and type checking via typeof and cast. -- class_constructor.fun — Using _construct in classes and field initialization. -- classes_demo.fun — Class definition, methods, and instances interacting. -- datetime_basic.fun — Uses utils.datetime to print now_ms, mono_ms, and formatted timestamps. -- debug_reporting.fun — Shows how --repl-on-error and trace annotate crashes with file:line and stack info. -- exit_example.fun — Demonstrates exiting a program early and exit codes. -- expressions_test.fun — Demonstrates operator precedence and expression evaluations. -- fail.fun — Purposefully triggers an error to see runtime behavior. -- file_io.fun — Reading/writing files with built-ins; prints file contents. -- file_print_for_file_line_by_line.fun — Iterates through file lines, printing them. -- floats.fun — Demonstrates float-like operations if represented via numbers; shows division behavior. -- for_range_test.fun — Uses utils.range to iterate over numeric ranges. -- functions_test.fun — Function definitions, higher-order usage, and composition. -- have_fun.fun — A fun greeting and minimal example to verify environment. -- have_fun_function.fun — Extracted function used by have_fun.fun. -- if_else_test.fun — Conditional branching and nesting. -- include_lib.fun — Using #include <...> from FUN_LIB_DIR. -- include_local.fun — Using #include "..." relative path includes and shared helpers. -- include_namespace.fun — Namespaced includes with "as" and usage examples. -- inheritance_demo.fun — Class inheritance patterns and method overriding. -- input_example.fun — Reading from stdin using Console.ask/prompt. -- json_showcase.fun — Comprehensive demo of JSON.parse/stringify/from_file/to_file; prints nested values and writes to /tmp. -- curl_get_json.fun — Fetches JSON over HTTP using curl_get and parses it with json_parse if available. -- curl_post.fun — Sends a POST request and prints the raw response; parses headers when JSON is enabled. -- curl_download.fun — Downloads a file to disk and prints whether it succeeded. -- loops_break_continue.fun — Shows break and continue in loops and their effects on control flow. -- md5_demo.fun — Hashing data using lib/crypt/md5.fun and printing the digest. -- namespaced_mod.fun — Module used by include_namespace.fun to demonstrate namespacing. -- nested_loops.fun — Nested iteration and control flow. -- objects_basic.fun — Creating and manipulating maps as objects with fields. -- objects_more.fun — More advanced object/map patterns. -- os_env.fun — Getting/setting environment variables. -- pcsc_example.fun — Establishing PC/SC context, listing readers, transmitting a sample APDU if hardware present. -- process_example.fun — Running external commands with Process.run/system and handling exit codes. -- regex_demo.fun — Using Regex class for match/search/replace; prints results and groups. -- regex_procedural.fun — Direct usage of regex_* built-ins without the class wrapper. -- repl_on_error.fun — Forces an error to enter REPL when run with --repl-on-error. -- sha1_demo.fun — Hashing using SHA1 helper; prints digest. -- sha256_demo.fun — SHA-256 hashing demonstration over file/string inputs. -- sha256_str_demo.fun — String-only SHA-256 hashing convenience. -- sha384_example.fun — SHA-384 hashing demonstration. -- sha512_demo.fun — SHA-512 hashing demonstration over data; prints digest. -- sha512_str_demo.fun — String-only SHA-512 hashing convenience. -- short_circuit_test.fun — Demonstrates && and || short-circuit semantics. -- signed_ints.fun — Two's complement wrapping and signed integer behavior. -- stdlib_showcase.fun — A tour of several stdlib modules in one file. -- strings_test.fun — String slicing, joining, splitting, find, and case transforms. -- tcp_http_get.fun — Minimal HTTP GET over TCP using built-ins; prints the response. -- tcp_http_get_class.fun — Same as above using the TcpClient class. -- thread_class_example.fun — Spawning and joining threads via the Thread class methods. -- threads_demo.fun — Multiple threads and returning values with thread_join. -- try_catch_finally.fun — Error handling with try/catch/finally constructs. -- try_catch_with_error.fun — Catching and inspecting errors thrown inside code. -- typeof_features.fun — Shows typeof on many values and casting behavior. -- typeof.fun — Basic typeof usage. -- type_safety_fails.fun — Examples that should fail type safety checks at runtime. -- type_safety.fun — Properly typed examples that run without errors. -- types_integers.fun — Integer type features, comparisons, and arithmetic. -- types_overview.fun — Overview of values and literal syntax. -- uint_types.fun — Unsigned integer helpers and clamping. -- unix_socket_echo.fun — UNIX domain socket echo client/server demo. -- while_test.fun — While loops, counters, and loop termination conditions. +Highlights (not exhaustive): +- arrays.fun, arrays_advanced.fun, arrays_iter.fun — array operations +- booleans.fun, boolean_decl.fun — boolean basics +- builtins_conversions.fun, builtins_extended.fun — conversions and math helpers +- builtins_maps_and_more.fun — maps and indexing +- byte_for_demo.fun — bitwise operations +- class_constructor.fun, classes_demo.fun, inheritance_demo.fun — classes +- datetime_basic.fun — date/time utilities +- debug_reporting.fun, repl_on_error.fun — tracing and REPL-on-error +- exit_example.fun — exit codes +- expressions_test.fun — operators +- file_io.fun, file_print_for_file_line_by_line.fun — file I/O +- for_range_test.fun — numeric ranges +- functions_test.fun — functions and higher-order usage +- have_fun.fun — quick sanity check +- if_else_test.fun — branching +- include_lib.fun, include_local.fun, include_namespace.fun — includes and namespacing +- input_example.fun — console input +- json_showcase.fun — JSON usage +- curl_get_json.fun, curl_post.fun, curl_download.fun — HTTP via CURL +- loops_break_continue.fun, nested_loops.fun, while_test.fun — loops +- md5_demo.fun, sha1_demo.fun, sha256_demo.fun, sha256_str_demo.fun, sha384_example.fun, sha512_demo.fun, sha512_str_demo.fun — hashing +- objects_basic.fun, objects_more.fun — map/object patterns +- os_env.fun — environment variables +- pcsc_example.fun — smart card demo +- process_example.fun — running external commands +- regex_demo.fun, regex_procedural.fun — regex usage +- stdlib_showcase.fun — tour through stdlib +- strings_test.fun — string operations +- tcp_http_get.fun, tcp_http_get_class.fun — TCP client demos +- thread_class_example.fun, threads_demo.fun — threading +- try_catch_finally.fun, try_catch_with_error.fun — error handling +- typeof.fun, typeof_features.fun — types and casting +- type_safety.fun, type_safety_fails.fun — type safety +- types_integers.fun, signed_ints.fun, uint_types.fun — integers +- unix_socket_echo.fun — UNIX domain sockets +- sqlite_example.fun — SQLite usage Notes: -- Some examples are platform-dependent (PCSC, UNIX sockets) or rely on optional features (JSON). They degrade gracefully when unavailable, printing empty arrays or default maps. +- Some examples rely on optional features (JSON, CURL, PCSC, SQLite) and degrade gracefully when disabled. --- -## Internals notes for JSON +## Internals notes (selected) -Fun wraps json-c. See src/vm/json/parse.c, stringify.c, from_file.c, to_file.c. For parsing, OP_JSON_PARSE converts the input string into a json_object using a tokener and then converts to Fun values via json_to_fun. On error or when JSON is compiled out, the VM returns nil. The stdlib JSON class converts arguments defensively (to_string) and provides default pretty=0. +JSON: src/vm/json/* wraps json-c. OP_JSON_PARSE and friends convert json_object to Fun values and back; stdlib JSON class adds ergonomics. -## Internals notes for PCSC +PCSC: The VM interfaces with pcsc-lite/WinSCard and returns maps with data and status words. The stdlib wrapper handles absent hardware defensively. -The PCSC functions in the VM interface with pcsc-lite/WinSCard. Transmit returns a map with raw data bytes and status words (sw1, sw2) and a code field. The stdlib wrapper in lib/io/pcsc.fun demonstrates defensive patterns: when no readers are found, it returns a default map so that indexing like res["sw1"] is always safe. +SQLite: src/vm/sqlite/* implements open/exec/query/close using a simple handle registry. Query prepares a statement, steps rows, maps columns by name to values, and returns an array of row maps. --- ## Development -This section is a work in progress... Please excuse the lack of more information. There are daily updates here. - -### Rules - -- Every commit message must contain the version at the end in the following format (1.2.3) -- Every commit requires a version incrementation in CMakeLists.txt before committing. Documentation updates do not increment the version but must contain the current version in each commit message. -- Version numbering follows "[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html)" +This project follows Semantic Versioning. Commit messages include the version (e.g., 1.2.3). Version bumps are made in CMakeLists.txt for code changes; documentation-only commits include the current version in the message but do not bump it. ### Development systems -- [GNU](https://gnu.org/)/[Linux](https://kernel.org/) ([Arch](https://archlinux.org/)/[Artix](https://artixlinux.org/), [Debian](https://www.debian.org/)) using [GCC](https://gcc.gnu.org/) and the [GNU C library](https://www.gnu.org/software/libc/) ([glibc](https://en.wikipedia.org/wiki/Glibc)) -- GNU/Linux ([Alpine](https://alpinelinux.org/)) using GCC and the [musl libc](https://musl.libc.org/) -- [FreeBSD](https://www.freebsd.org/) using [Clang](https://clang.llvm.org/) and the [BSD libc](https://en.wikipedia.org/wiki/C_standard_library#BSD_libc) -- [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) using [Cygwin](https://www.cygwin.com/) and GCC. +- GNU/Linux (glibc, musl), FreeBSD (Clang), Windows (Cygwin + GCC). Other Unix-like systems likely work but are untested. -### Other systems - -- [macOS](https://en.wikipedia.org/wiki/MacOS), [NetBSD](https://netbsd.org/), [OpenBSD](https://www.openbsd.org/), etc. should fully work, but I don't know. I do not use these systems actually. You wanna try and report? - -### To Do - -Everything... ;) No, a lot of stuff works already, but only a tiny set of functionality is available in the Fun programming language. It grows from day to day... - -### Build Fun - -Linux/UNIX only covered here for now. - -Clone repository: - -```bash -git clone https://git.xw3.org/fun/fun.git -``` - -Change directory: - -```bash -cd fun -``` - -Build: - -```bash -# Note: Every -D flag must be of the form NAME=VALUE (e.g., -DFUN_WITH_REPL=ON) -cmake -S . -B build -DFUN_DEBUG=OFF -DFUN_WITH_PCSC=OFF -DFUN_WITH_REPL=ON -DFUN_WITH_JSON=ON -cmake --build build --target fun -``` - -CMake options you can toggle (all require NAME=VALUE): - -- FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) -- FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) -- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) -- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) -- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) - -That's it! For testing it, run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./demo.fun -``` - -To see what's going on, run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun --trace ./demo.fun -``` - -To switch into the REPL after an error, run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun --repl-on-error --trace ./demo.fun -``` - -Both --repl-on-error and --trace are optional but can always be combined. To get -more debug information, you need to build Fun with -DFUN_DEBUG=ON. - -To directly run the REPL, you have to run: - -```bash -FUN_LIB_DIR="$(pwd)/lib" ./build/fun -``` - -But be sure to build Fun with -DFUN_WITH_REPL=ON. - -Tip: If you saw an error like this when configuring with CMake: - - CMake Error: Parse error in command line argument: FUN_WITH_JSON - Should be: VAR:type=value - -it means a -D flag was passed without a value. Always specify options as -DNAME=VALUE, for example: - - -DFUN_WITH_JSON=ON - ---- - -## Contributing and further reading - -- Browse lib/ for up-to-date stdlib APIs; many files document their own public interfaces in comments at the top. -- src/bytecode.h lists all opcodes supported by the VM. The corresponding implementations live under src/vm/. -- examples/ are the best starting point to learn by doing. +### Contributing and further reading +- Browse lib/ for stdlib APIs (files often document their own interfaces) +- src/bytecode.h lists supported opcodes; implementations live under src/vm/ +- examples/ are the best starting point to learn by doing diff --git a/examples/data/todo.sql b/examples/data/todo.sql new file mode 100644 index 0000000..9f9c407 --- /dev/null +++ b/examples/data/todo.sql @@ -0,0 +1,12 @@ +PRAGMA foreign_keys = ON; +DROP TABLE IF EXISTS tasks; +CREATE TABLE tasks ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + done INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +INSERT INTO tasks (title, done) VALUES + ('Write Fun + SQLite example', 1), + ('Ship optional feature flag', 0), + ('Celebrate with coffee', 0); diff --git a/examples/sqlite_example.fun b/examples/sqlite_example.fun new file mode 100644 index 0000000..0662685 --- /dev/null +++ b/examples/sqlite_example.fun @@ -0,0 +1,37 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +// Prepare sample DB from SQL if needed (requires sqlite3 CLI installed) +// Create it once with: +// sqlite3 todo.sqlite < ./examples/data/todo.sql + +string db_path = "./todo.sqlite" + +number h = sqlite_open(db_path) +if h == 0 + print("Failed to open DB: " + db_path) + exit(1) + +rows = sqlite_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 = sqlite_exec(h, "INSERT INTO tasks (title, done) VALUES ('Try Fun + SQLite', 0);") +print("Insert rc=" + to_string(rc)) + +rows2 = sqlite_query(h, "SELECT count(*) AS cnt FROM tasks;") +print("Total tasks now: " + to_string(rows2[0]["cnt"])) + +sqlite_close(h) diff --git a/src/bytecode.c b/src/bytecode.c index 2120c8e..2ca8a5a 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -148,6 +148,10 @@ static const char *opcode_name(OpCode op) { case OP_CURL_GET: return "CURL_GET"; case OP_CURL_POST: return "CURL_POST"; case OP_CURL_DOWNLOAD: return "CURL_DOWNLOAD"; + case OP_SQLITE_OPEN: return "SQLITE_OPEN"; + case OP_SQLITE_CLOSE: return "SQLITE_CLOSE"; + case OP_SQLITE_EXEC: return "SQLITE_EXEC"; + case OP_SQLITE_QUERY: return "SQLITE_QUERY"; case OP_PCSC_ESTABLISH: return "PCSC_ESTABLISH"; case OP_PCSC_RELEASE: return "PCSC_RELEASE"; case OP_PCSC_LIST_READERS: return "PCSC_LIST_READERS"; diff --git a/src/bytecode.h b/src/bytecode.h index e38e7f9..08125e3 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -152,6 +152,12 @@ typedef enum { OP_CURL_POST, // pops [headers map?], body string, url; pushes response string (or "") OP_CURL_DOWNLOAD, // pops [headers map?], path, url; pushes 1/0 + // SQLite (optional) + OP_SQLITE_OPEN, // pops path; pushes handle (>0) or 0 + OP_SQLITE_CLOSE, // pops handle; pushes Nil + OP_SQLITE_EXEC, // pops sql, handle; pushes sqlite rc (0=OK) + OP_SQLITE_QUERY, // pops sql, handle; pushes array + // 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/parser.c b/src/parser.c index 7f19a44..3e5c033 100644 --- a/src/parser.c +++ b/src/parser.c @@ -787,6 +787,43 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* SQLite builtins */ + if (strcmp(name, "sqlite_open") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "sqlite_open expects (path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after sqlite_open arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_SQLITE_OPEN, 0); + free(name); + return 1; + } + if (strcmp(name, "sqlite_close") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "sqlite_close expects (handle)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after sqlite_close arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_SQLITE_CLOSE, 0); + free(name); + return 1; + } + if (strcmp(name, "sqlite_exec") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "sqlite_exec expects (handle, sql)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "sqlite_exec expects (handle, sql)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "sqlite_exec expects (handle, sql)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after sqlite_exec args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_SQLITE_EXEC, 0); + free(name); + return 1; + } + if (strcmp(name, "sqlite_query") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "sqlite_query expects (handle, sql)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "sqlite_query expects (handle, sql)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "sqlite_query expects (handle, sql)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after sqlite_query args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_SQLITE_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; } diff --git a/src/value.c b/src/value.c index f58dec6..9bae143 100644 --- a/src/value.c +++ b/src/value.c @@ -441,6 +441,15 @@ char *value_to_string_alloc(const Value *v) { snprintf(buf, sizeof(buf), "[array n=%d]", n); return strdup(buf); } + case VAL_MAP: { + int n = 0; + if (v->type == VAL_MAP && v->map) { + const Map *m = (const Map*)v->map; + n = m ? m->count : 0; + } + snprintf(buf, sizeof(buf), "{map n=%d}", n); + return strdup(buf); + } case VAL_NIL: default: return strdup("nil"); diff --git a/src/vm.c b/src/vm.c index 16d0a41..6a5bbde 100644 --- a/src/vm.c +++ b/src/vm.c @@ -13,6 +13,10 @@ #include "string.c" #include "pcsc.c" #include "jsonc.c" +#ifdef FUN_WITH_SQLITE +#include +#include "vm/sqlite/common.c" +#endif #include "vm.h" #include "value.h" #include @@ -703,6 +707,12 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/curl/post.c" #include "vm/curl/download.c" + /* SQLite ops */ + #include "vm/sqlite/open.c" + #include "vm/sqlite/close.c" + #include "vm/sqlite/exec.c" + #include "vm/sqlite/query.c" + /* PCRE2 ops */ #include "vm/pcre2/test.c" #include "vm/pcre2/match.c" diff --git a/src/vm.h b/src/vm.h index 6b685de..f72ff63 100644 --- a/src/vm.h +++ b/src/vm.h @@ -40,6 +40,7 @@ static const char *opcode_names[] = { "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", "CURL_GET","CURL_POST","CURL_DOWNLOAD", + "SQLITE_OPEN","SQLITE_CLOSE","SQLITE_EXEC","SQLITE_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", diff --git a/src/vm/sqlite/close.c b/src/vm/sqlite/close.c new file mode 100644 index 0000000..acc6a4e --- /dev/null +++ b/src/vm/sqlite/close.c @@ -0,0 +1,32 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * OP_SQLITE_CLOSE: (handle:int) -> Nil + */ +case OP_SQLITE_CLOSE: { +#ifdef FUN_WITH_SQLITE + Value vh = pop_value(vm); + int hid = (int)vh.i; + free_value(vh); + SqlHandle *h = sql_reg_get(hid); + if (h && h->db) { + sqlite3_close(h->db); + h->db = NULL; + sql_reg_del(hid); + } + push_value(vm, make_nil()); +#else + Value v = pop_value(vm); free_value(v); + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/sqlite/common.c b/src/vm/sqlite/common.c new file mode 100644 index 0000000..df6105d --- /dev/null +++ b/src/vm/sqlite/common.c @@ -0,0 +1,49 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * SQLite handle registry and helpers + */ +#ifdef FUN_WITH_SQLITE +#include + +typedef struct SqlHandle { + int id; + sqlite3 *db; + struct SqlHandle *next; +} SqlHandle; + +static SqlHandle *g_sql_handles = NULL; +static int g_sql_next_id = 1; + +static SqlHandle* sql_reg_add(sqlite3 *db) { + SqlHandle *h = (SqlHandle*)calloc(1, sizeof(SqlHandle)); + if (!h) return NULL; + h->id = g_sql_next_id++; + h->db = db; + h->next = g_sql_handles; + g_sql_handles = h; + return h; +} + +static SqlHandle* sql_reg_get(int id) { + for (SqlHandle *p = g_sql_handles; p; p = p->next) if (p->id == id) return p; + return NULL; +} + +static void sql_reg_del(int id) { + SqlHandle **pp = &g_sql_handles; + while (*pp) { + if ((*pp)->id == id) { SqlHandle *d = *pp; *pp = d->next; free(d); return; } + pp = &(*pp)->next; + } +} +#endif diff --git a/src/vm/sqlite/exec.c b/src/vm/sqlite/exec.c new file mode 100644 index 0000000..2c7e207 --- /dev/null +++ b/src/vm/sqlite/exec.c @@ -0,0 +1,36 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * OP_SQLITE_EXEC: (handle:int, sql:string) -> int rc (0=OK) + */ +case OP_SQLITE_EXEC: { +#ifdef FUN_WITH_SQLITE + 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); + SqlHandle *h = sql_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; +} diff --git a/src/vm/sqlite/open.c b/src/vm/sqlite/open.c new file mode 100644 index 0000000..a7c3ac4 --- /dev/null +++ b/src/vm/sqlite/open.c @@ -0,0 +1,37 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * OP_SQLITE_OPEN: (path:string) -> handle:int (>0) or 0 on error + */ +case OP_SQLITE_OPEN: { +#ifdef FUN_WITH_SQLITE + 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; + } + SqlHandle *h = sql_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; +} diff --git a/src/vm/sqlite/query.c b/src/vm/sqlite/query.c new file mode 100644 index 0000000..fb3c6d8 --- /dev/null +++ b/src/vm/sqlite/query.c @@ -0,0 +1,62 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * OP_SQLITE_QUERY: (handle:int, sql:string) -> array> + */ +case OP_SQLITE_QUERY: { +#ifdef FUN_WITH_SQLITE + 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); + SqlHandle *h = sql_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: rows array now owns it. Freeing would + destroy the map and leave a dangling pointer causing segfaults + when accessing fields like row["done"]. */ + } + 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; +} From 56712206d175fe3414c0eb01fba6782bf6e214fb Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 27 Nov 2025 00:42:47 +0100 Subject: [PATCH 18/55] Added support for libSQL as an compatible alternative for SQLite. (0.33.0) --- .gitignore | 2 +- CMakeLists.txt | 44 ++++++++++++++++- README.md | 1 + docs/handbook.md | 24 +++++++++- examples/data/{todo.sql => database.sql} | 0 examples/libsql_example.fun | 59 +++++++++++++++++++++++ examples/sqlite_example.fun | 15 +++++- src/bytecode.c | 4 ++ src/bytecode.h | 6 +++ src/parser.c | 37 +++++++++++++++ src/vm.c | 10 ++++ src/vm.h | 1 + src/vm/libsql/close.c | 32 +++++++++++++ src/vm/libsql/common.c | 55 ++++++++++++++++++++++ src/vm/libsql/exec.c | 36 ++++++++++++++ src/vm/libsql/open.c | 37 +++++++++++++++ src/vm/libsql/query.c | 60 ++++++++++++++++++++++++ 17 files changed, 418 insertions(+), 5 deletions(-) rename examples/data/{todo.sql => database.sql} (100%) create mode 100644 examples/libsql_example.fun create mode 100644 src/vm/libsql/close.c create mode 100644 src/vm/libsql/common.c create mode 100644 src/vm/libsql/exec.c create mode 100644 src/vm/libsql/open.c create mode 100644 src/vm/libsql/query.c diff --git a/.gitignore b/.gitignore index 6e70192..5a53300 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index c8c1698..eb060e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 61b74f4..bec9a75 100644 --- a/README.md +++ b/README.md @@ -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) ☑ diff --git a/docs/handbook.md b/docs/handbook.md index 5b55c28..2f8ca49 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -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: diff --git a/examples/data/todo.sql b/examples/data/database.sql similarity index 100% rename from examples/data/todo.sql rename to examples/data/database.sql diff --git a/examples/libsql_example.fun b/examples/libsql_example.fun new file mode 100644 index 0000000..1bfc430 --- /dev/null +++ b/examples/libsql_example.fun @@ -0,0 +1,59 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +// Demonstrates the optional libSQL extension +// Build with: cmake -S . -B build -DFUN_WITH_LIBSQL=ON && cmake --build build + +// Prepare sample DB from SQL if needed (requires sqlite3 CLI installed) +// Create it once with: +// sqlite3 ./database.sqlite < ./examples/data/database.sql + +number h = libsql_open("./database.sqlite") +if h == 0 + print("Failed to open libSQL database") +else + libsql_exec(h, "CREATE TABLE IF NOT EXISTS todos(id INTEGER PRIMARY KEY, title TEXT, done INT)") + libsql_exec(h, "DELETE FROM todos") + libsql_exec(h, "INSERT INTO todos(title, done) VALUES('Buy milk', 0)") + libsql_exec(h, "INSERT INTO todos(title, done) VALUES('Write code', 1)") + + rows = libsql_query(h, "SELECT id, title, done FROM todos ORDER BY id") + for row in rows + print(to_string(row["id"]) + ": " + to_string(row["title"]) + " (done="+to_string(row["done"]) + ")") + + rows = libsql_query(h, "SELECT id, title, done, created_at FROM tasks ORDER BY id;") + print("Tasks (" + to_string(len(rows)) + "):") + for row in rows + string status = "✘" + if row["done"] == 1 + status = "✔" + print("- [" + status + "] (#" + to_string(row["id"]) + ") " + to_string(row["title"]) + " — " + to_string(row["created_at"])) + + number rc = libsql_exec(h, "INSERT INTO tasks (title, done) VALUES ('Try Fun + SQLite', 0);") + print("Insert rc=" + to_string(rc)) + + rows2 = libsql_query(h, "SELECT count(*) AS cnt FROM tasks;") + print("Total tasks now: " + to_string(rows2[0]["cnt"])) + + libsql_close(h) + +/* Example output: +1: Buy milk (done=0) +2: Write code (done=1) +Tasks (3): +- [✔] (#1) Write Fun + SQLite example — 2025-11-26 23:20:41 +- [✘] (#2) Ship optional feature flag — 2025-11-26 23:20:41 +- [✘] (#3) Celebrate with coffee — 2025-11-26 23:20:41 +Insert rc=0 +Total tasks now: 4 +*/ diff --git a/examples/sqlite_example.fun b/examples/sqlite_example.fun index 0662685..3eb2803 100644 --- a/examples/sqlite_example.fun +++ b/examples/sqlite_example.fun @@ -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 +*/ diff --git a/src/bytecode.c b/src/bytecode.c index 2ca8a5a..fdf63b1 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -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"; diff --git a/src/bytecode.h b/src/bytecode.h index 08125e3..908abb3 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -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 + // libsql (optional, independent) + OP_LIBSQL_OPEN, // pops url/path; pushes handle (>0) or 0 + OP_LIBSQL_CLOSE, // pops handle; pushes Nil + OP_LIBSQL_EXEC, // pops sql, handle; pushes rc (0=OK) + OP_LIBSQL_QUERY, // pops sql, handle; pushes array + // PCSC (smart card) opcodes OP_PCSC_ESTABLISH, // returns context id (>0) or 0 OP_PCSC_RELEASE, // pops ctx id; returns 1/0 diff --git a/src/parser.c b/src/parser.c index 3e5c033..ab47be0 100644 --- a/src/parser.c +++ b/src/parser.c @@ -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; } diff --git a/src/vm.c b/src/vm.c index 6a5bbde..e8a8635 100644 --- a/src/vm.c +++ b/src/vm.c @@ -17,6 +17,10 @@ #include #include "vm/sqlite/common.c" #endif +#ifdef FUN_WITH_LIBSQL +#include /* libsql exposes sqlite3-compatible C API */ +#include "vm/libsql/common.c" +#endif #include "vm.h" #include "value.h" #include @@ -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" diff --git a/src/vm.h b/src/vm.h index f72ff63..2591da4 100644 --- a/src/vm.h +++ b/src/vm.h @@ -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", diff --git a/src/vm/libsql/close.c b/src/vm/libsql/close.c new file mode 100644 index 0000000..0a4e566 --- /dev/null +++ b/src/vm/libsql/close.c @@ -0,0 +1,32 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * 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; +} diff --git a/src/vm/libsql/common.c b/src/vm/libsql/common.c new file mode 100644 index 0000000..e69f5bb --- /dev/null +++ b/src/vm/libsql/common.c @@ -0,0 +1,55 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +#ifdef FUN_WITH_LIBSQL +#include +#include +#include +#include /* 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 */ diff --git a/src/vm/libsql/exec.c b/src/vm/libsql/exec.c new file mode 100644 index 0000000..0326ae9 --- /dev/null +++ b/src/vm/libsql/exec.c @@ -0,0 +1,36 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * 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; +} diff --git a/src/vm/libsql/open.c b/src/vm/libsql/open.c new file mode 100644 index 0000000..7b3f177 --- /dev/null +++ b/src/vm/libsql/open.c @@ -0,0 +1,37 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * 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; +} diff --git a/src/vm/libsql/query.c b/src/vm/libsql/query.c new file mode 100644 index 0000000..559c6f4 --- /dev/null +++ b/src/vm/libsql/query.c @@ -0,0 +1,60 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-26 + */ + +/** + * OP_LIBSQL_QUERY: (handle:int, sql:string) -> array> + */ +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; +} From 11bfb8ee05fb3f26ca34f2279827b170118c132d Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 27 Nov 2025 01:08:59 +0100 Subject: [PATCH 19/55] README update. No code changes. (0.33.0) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bec9a75..f4cf3ab 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ 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) ☑ +- [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☑ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ - [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ From 7ea3c9108a1ac0c18ab6d841c6d86deae385a373 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 27 Nov 2025 01:12:13 +0100 Subject: [PATCH 20/55] Permission updates. No code changes. (0.33.0) --- examples/crc32_example.fun | 0 examples/crc32c_example.fun | 0 examples/curl_download.fun | 0 examples/curl_get_json.fun | 0 examples/curl_post.fun | 0 examples/libsql_example.fun | 0 examples/sqlite_example.fun | 0 7 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/crc32_example.fun mode change 100644 => 100755 examples/crc32c_example.fun mode change 100644 => 100755 examples/curl_download.fun mode change 100644 => 100755 examples/curl_get_json.fun mode change 100644 => 100755 examples/curl_post.fun mode change 100644 => 100755 examples/libsql_example.fun mode change 100644 => 100755 examples/sqlite_example.fun diff --git a/examples/crc32_example.fun b/examples/crc32_example.fun old mode 100644 new mode 100755 diff --git a/examples/crc32c_example.fun b/examples/crc32c_example.fun old mode 100644 new mode 100755 diff --git a/examples/curl_download.fun b/examples/curl_download.fun old mode 100644 new mode 100755 diff --git a/examples/curl_get_json.fun b/examples/curl_get_json.fun old mode 100644 new mode 100755 diff --git a/examples/curl_post.fun b/examples/curl_post.fun old mode 100644 new mode 100755 diff --git a/examples/libsql_example.fun b/examples/libsql_example.fun old mode 100644 new mode 100755 diff --git a/examples/sqlite_example.fun b/examples/sqlite_example.fun old mode 100644 new mode 100755 From 424d2fd9dfd96319f0ae3fa638422ab7cd9b50c2 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 27 Nov 2025 01:31:11 +0100 Subject: [PATCH 21/55] README update. No code changes. (0.33.0) --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f4cf3ab..98dfc6a 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,6 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - Joy in coding - Fun! -### Extras - -- [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) ☑ -- [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ -- [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ -- [SQLite](https://sqlite.org/) support builtin (optional) ☑ -- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ -- [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ -- [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ - ☑ = Done / ☐ = Planned or in progress. ## Characteristics @@ -79,6 +67,18 @@ A language that feels like home for developers who: Fun may not change the world — but it will make programming a little more fun. +## Features + +- [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) ☑ +- [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ +- [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ +- [SQLite](https://sqlite.org/) support builtin (optional) ☑ +- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ +- [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ +- [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ +- ## Documentation I am writing documentation only actually, but this is work in progress, since debugging and bug fixing includes this task. From c22c35d07e27af5f096bc520f3f6808378b4ea9b Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 27 Nov 2025 01:34:57 +0100 Subject: [PATCH 22/55] README update. No code changes. (0.33.0) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98dfc6a..d74d1ec 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,6 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht - Joy in coding - Fun! -☑ = Done / ☐ = Planned or in progress. - ## Characteristics - Dynamic and optionally statically typed @@ -78,7 +76,9 @@ Fun may not change the world — but it will make programming a little more fun. - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ - [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ - [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ -- + +☑ = Done / ☐ = Planned or in progress. + ## Documentation I am writing documentation only actually, but this is work in progress, since debugging and bug fixing includes this task. From c57b1cac30b937c2d65a7dcf0407600c7422c431 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 28 Nov 2025 17:47:27 +0100 Subject: [PATCH 23/55] Some documentation update. No Code changes. (0.33.0) --- README.md | 31 ++++++++++++++++++++++++++++--- docs/handbook.md | 26 +++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d74d1ec..aa8d26d 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ ## What is Fun? +Fun is an experiment, just for fun, but Fun works! + Fun is a highly strict programming language, but also highly simple. It looks like Python (My favorite language), but there are differences. -Influenced by **[Bash](https://www.gnu.org/software/bash/)**, **[C](https://en.wikipedia.org/wiki/The_C_Programming_Language)**, Go, **[Lua](https://www.lua.org/)**, **[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 (Most influences came from linked languages). Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](https://opensource.org/license/apache-2-0). @@ -67,25 +69,48 @@ Fun may not change the world — but it will make programming a little more fun. ## Features +### Core + +- functions/classes/objects +- if/else if/else +- try/catch/finally + +### Lib + +... + +### Extensions + +- [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) ☐ - [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ +- [INI](https://en.wikipedia.org/wiki/INI_file) support builtin using [iniparser](https://gitlab.com/iniparser/iniparser/) (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) ☑ - [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ - [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☑ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ +- [TOML](https://en.wikipedia.org/wiki/TOML) support builtin using [tomlc99](https://github.com/cktan/tomlc99) (optional) ☐ - [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ - [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ ☑ = Done / ☐ = Planned or in progress. +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... ;) + +There are some libs written in Fun available at 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. + ## Documentation -I am writing documentation only actually, but this is work in progress, since debugging and bug fixing includes this task. +This is actually a work in progress... Current documentation is only found in the [Fun Handbook](https://git.xw3.org/fun/fun/src/branch/main/docs/handbook.md). -In the [examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directory should be an example of most Fun features. +In the [./examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directory should be an example of most Fun features. + +Fun internals are found directly in the [./src/](https://git.xw3.org/fun/fun/src/branch/main/src) diretory. Fun [Opcodes](https://en.wikipedia.org/wiki/Opcode) are found in [./src/vm/](https://git.xw3.org/fun/fun/src/branch/main/src/vm). + +Since things are actually changing sometimes, I will not write the documentation for this as of now. Complete API documentation will follow. diff --git a/docs/handbook.md b/docs/handbook.md index 2f8ca49..1cedac7 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -1,3 +1,23 @@ +--- +layout: page +published: true +noToc: true +noComments: true +title: /handbook/index.fun +subtitle: +description: Handbook +permalink: /handbook/ +lang: en +tags: +- handbook +- installation +- usage +- introduction +- help +- guide +- howto +--- + # Fun Handbook (Second Edition) This is a refreshed, de-duplicated, and fully up-to-date handbook for the Fun programming language and its virtual machine (VM). It keeps the same section layout as the original handbook while consolidating repeated content and documenting all currently available features, including the latest SQLite support. @@ -88,11 +108,11 @@ FUN_LIB_DIR="$(pwd)/lib" ./build/fun 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_CURL=ON|OFF — enable CURL (libcurl) support (default OFF) +- FUN_WITH_JSON=ON|OFF — enable JSON (json-c) support (default OFF) - FUN_WITH_LIBSQL=ON|OFF — enable libSQL (Turso) client support (default OFF) - FUN_WITH_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_PCSC=ON|OFF — enable PC/SC smart card (PCSC lite) support (default OFF) - FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default OFF) - FUN_WITH_SQLITE=ON|OFF — enable SQLite (sqlite3) support (default OFF) From ff01ebc8dc7b7b8ff349290fc546ef5d138d9643 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 28 Nov 2025 17:50:36 +0100 Subject: [PATCH 24/55] Some documentation update. No Code changes. (0.33.0) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa8d26d..abf03b1 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ There are some libs written in Fun available at the [./lib/](https://git.xw3.org This is actually a work in progress... -Current documentation is only found in the [Fun Handbook](https://git.xw3.org/fun/fun/src/branch/main/docs/handbook.md). +Current documentation is only found in the [Fun Handbook](https://fun-lang.xyz/handbook/). In the [./examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directory should be an example of most Fun features. From f05a3d3cde2eac74dde19cacb0d0598d07e14c0b Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 28 Nov 2025 17:53:10 +0100 Subject: [PATCH 25/55] Some documentation update. No Code changes. (0.33.0) --- README.md | 2 +- docs/handbook.md | 29 ++++++----------------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index abf03b1..aa8d26d 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ There are some libs written in Fun available at the [./lib/](https://git.xw3.org This is actually a work in progress... -Current documentation is only found in the [Fun Handbook](https://fun-lang.xyz/handbook/). +Current documentation is only found in the [Fun Handbook](https://git.xw3.org/fun/fun/src/branch/main/docs/handbook.md). In the [./examples/](https://git.xw3.org/fun/fun/src/branch/main/examples) directory should be an example of most Fun features. diff --git a/docs/handbook.md b/docs/handbook.md index 1cedac7..085479e 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -1,23 +1,3 @@ ---- -layout: page -published: true -noToc: true -noComments: true -title: /handbook/index.fun -subtitle: -description: Handbook -permalink: /handbook/ -lang: en -tags: -- handbook -- installation -- usage -- introduction -- help -- guide -- howto ---- - # Fun Handbook (Second Edition) This is a refreshed, de-duplicated, and fully up-to-date handbook for the Fun programming language and its virtual machine (VM). It keeps the same section layout as the original handbook while consolidating repeated content and documenting all currently available features, including the latest SQLite support. @@ -40,15 +20,18 @@ Fun is a small, strict, and simple programming language executed by a stack-base - A C compiler, a libc, and Git -FreeBSD: +#### FreeBSD: + - CMake - Clang -Linux: +#### Linux: + - CMake - GCC (Clang should also work) -Windows: +#### Windows: + - Cygwin (not covered in detail here) - CMake - GCC via Cygwin From 49261b96c26ccd69553d5a3ecee3efba1213b2f6 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 28 Nov 2025 18:42:11 +0100 Subject: [PATCH 26/55] Some content update. No Code changes. (0.33.0) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa8d26d..0b68dcd 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Fun may not change the world — but it will make programming a little more fun. 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... ;) -There are some libs written in Fun available at 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. +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. ## Documentation From fb5670a69967292039d0e761cdeed4b67d199798 Mon Sep 17 00:00:00 2001 From: hanez Date: Sun, 30 Nov 2025 02:44:14 +0100 Subject: [PATCH 27/55] Added support for .ini files (iniparser). (0.34.0) --- CMakeLists.txt | 40 ++++++++++++- README.md | 2 +- examples/data/complex.ini | 32 ++++++++++ examples/data/subsections.ini | 25 ++++++++ examples/ini_complex.fun | 75 +++++++++++++++++++++++ examples/ini_demo.fun | 30 ++++++++++ examples/ini_subsections.fun | 70 ++++++++++++++++++++++ examples/md5_demo.fun | 10 +++- src/bytecode.c | 9 +++ src/bytecode.h | 11 ++++ src/parser.c | 109 ++++++++++++++++++++++++++++++++++ src/vm.c | 25 ++++++++ src/vm.h | 1 + src/vm/ini/free.c | 22 +++++++ src/vm/ini/getters.c | 94 +++++++++++++++++++++++++++++ src/vm/ini/handles.h | 61 +++++++++++++++++++ src/vm/ini/load.c | 29 +++++++++ src/vm/ini/set_unset_save.c | 70 ++++++++++++++++++++++ 18 files changed, 710 insertions(+), 5 deletions(-) create mode 100644 examples/data/complex.ini create mode 100644 examples/data/subsections.ini create mode 100644 examples/ini_complex.fun create mode 100644 examples/ini_demo.fun create mode 100644 examples/ini_subsections.fun create mode 100644 src/vm/ini/free.c create mode 100644 src/vm/ini/getters.c create mode 100644 src/vm/ini/handles.h create mode 100644 src/vm/ini/load.c create mode 100644 src/vm/ini/set_unset_save.c diff --git a/CMakeLists.txt b/CMakeLists.txt index eb060e7..4c6c921 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.33.0 LANGUAGES C) +project(fun VERSION 0.34.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -90,6 +90,36 @@ if(FUN_WITH_JSON) endif() endif() +# Optional INI (iniparser 4.2.6) support +option(FUN_WITH_INI "Enable INI (iniparser) support" OFF) +set(INIPARSER_INCLUDE_DIRS "") +set(INIPARSER_LINK_LIBS "") +if(FUN_WITH_INI) + message(STATUS "Building with INI (iniparser) support") + add_definitions(-DFUN_WITH_INI) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(INIPARSER QUIET iniparser) + endif() + if(INIPARSER_FOUND) + list(APPEND INIPARSER_INCLUDE_DIRS ${INIPARSER_INCLUDE_DIRS} ${INIPARSER_INCLUDE_DIRS}) + list(APPEND INIPARSER_LINK_LIBS ${INIPARSER_LINK_LIBS} ${INIPARSER_LIBRARIES}) + else() + # Fallback: try to locate headers and library manually + find_path(INIPARSER_INCLUDE_DIR iniparser.h) + find_library(INIPARSER_LIB NAMES iniparser) + if(INIPARSER_INCLUDE_DIR) + list(APPEND INIPARSER_INCLUDE_DIRS ${INIPARSER_INCLUDE_DIR}) + endif() + if(INIPARSER_LIB) + list(APPEND INIPARSER_LINK_LIBS ${INIPARSER_LIB}) + endif() + if(NOT INIPARSER_INCLUDE_DIRS OR NOT INIPARSER_LINK_LIBS) + message(FATAL_ERROR "iniparser not found. Install iniparser (>=4.2.6) or disable FUN_WITH_INI.") + endif() + endif() +endif() + # Optional libsql support (independent from SQLite) option(FUN_WITH_LIBSQL "Enable libsql (Turso) client support" OFF) set(LIBSQL_INCLUDE_DIRS "") @@ -235,6 +265,14 @@ if(SQLITE3_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${SQLITE3_LINK_LIBS}) endif() +# iniparser include and link (if enabled) +if(INIPARSER_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${INIPARSER_INCLUDE_DIRS}) +endif() +if(INIPARSER_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${INIPARSER_LINK_LIBS}) +endif() + # libsql include and link (if enabled) if(LIBSQL_INCLUDE_DIRS) target_include_directories(fun_core PRIVATE ${LIBSQL_INCLUDE_DIRS}) diff --git a/README.md b/README.md index 0b68dcd..de99110 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Fun may not change the world — but it will make programming a little more fun. - [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) ☐ - [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ -- [INI](https://en.wikipedia.org/wiki/INI_file) support builtin using [iniparser](https://gitlab.com/iniparser/iniparser/) (optional) ☐ +- [INI](https://en.wikipedia.org/wiki/INI_file) support builtin using [iniparser](https://gitlab.com/iniparser/iniparser/) (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) ☑ - [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ diff --git a/examples/data/complex.ini b/examples/data/complex.ini new file mode 100644 index 0000000..512fbc5 --- /dev/null +++ b/examples/data/complex.ini @@ -0,0 +1,32 @@ + +[app] +name = "FunApp" +version = "1.2.3" +debug = "true" + + +[database] +host = "localhost" +port = "5432" +user = "fun" +pass = "secret" +pool_size = "8" +timeout = "2.5" + + +[network] +ssl = "yes" +retries = "3" +base_url = "https://api.example.com" + + +[features] +feature_x = "on" +feature_y = "off" + + +[paths] +data_dir = "./data" +log_file = "./logs/app.log" + + diff --git a/examples/data/subsections.ini b/examples/data/subsections.ini new file mode 100644 index 0000000..34c5242 --- /dev/null +++ b/examples/data/subsections.ini @@ -0,0 +1,25 @@ +# INI with subsection-style headers for iniparser 4.2.6 + +[server] +host = example.org +port = 8080 + +[server.tls] +enabled = true +version = 1.3 +ciphers = TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256 + +[users.admin] +name = alice +active = yes +quota_gb = 100 + +[users.guest] +name = bob +active = no +quota_gb = 5 + +[paths.logs] +dir = ./var/log/fun +rotate = true +max_files = 7 diff --git a/examples/ini_complex.fun b/examples/ini_complex.fun new file mode 100644 index 0000000..eb60f75 --- /dev/null +++ b/examples/ini_complex.fun @@ -0,0 +1,75 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +// Complex INI parsing example using iniparser 4.2.6 opcodes. + +path = "./examples/data/complex.ini" +h = ini_load(path) +if h == 0 + print("Failed to load "+path) +else + // app + app_name = ini_get_string(h, "app", "name", "FunApp") + app_version = ini_get_string(h, "app", "version", "0.0.0") + app_debug = ini_get_bool(h, "app", "debug", 0) + + // database + db_host = ini_get_string(h, "database", "host", "localhost") + db_port = ini_get_int(h, "database", "port", 5432) + db_user = ini_get_string(h, "database", "user", "user") + db_pass = ini_get_string(h, "database", "pass", "") + db_pool = ini_get_int(h, "database", "pool_size", 4) + db_timeout = ini_get_double(h, "database", "timeout", 2.0) + + // network + net_ssl = ini_get_bool(h, "network", "ssl", 0) + net_retries = ini_get_int(h, "network", "retries", 3) + base_url = ini_get_string(h, "network", "base_url", "") + + // features + feature_x = ini_get_bool(h, "features", "feature_x", 0) + feature_y = ini_get_bool(h, "features", "feature_y", 0) + + // paths + data_dir = ini_get_string(h, "paths", "data_dir", "./data") + log_file = ini_get_string(h, "paths", "log_file", "./logs/app.log") + + // Print a structured summary + print("[app]") + print(" name=" + app_name) + print(" version=" + app_version) + print(" debug=" + to_string(app_debug)) + + print("[database]") + print(" host=" + db_host) + print(" port=" + to_string(db_port)) + print(" user=" + db_user) + print(" pass=" + db_pass) + print(" pool_size=" + to_string(db_pool)) + print(" timeout=" + to_string(db_timeout)) + + print("[network]") + print(" ssl=" + to_string(net_ssl)) + print(" retries=" + to_string(net_retries)) + print(" base_url=" + base_url) + + print("[features]") + print(" feature_x=" + to_string(feature_x)) + print(" feature_y=" + to_string(feature_y)) + + print("[paths]") + print(" data_dir=" + data_dir) + print(" log_file=" + log_file) + + // Clean up + ini_free(h) diff --git a/examples/ini_demo.fun b/examples/ini_demo.fun new file mode 100644 index 0000000..81c96f1 --- /dev/null +++ b/examples/ini_demo.fun @@ -0,0 +1,30 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +// Minimal demo for INI opcodes using iniparser 4.2.6 + +path = "./examples/data/complex.ini" +h = ini_load(path) +if h == 0 + print("Failed to load " + path) +else + u = ini_get_string(h, "auth", "user", "guest") + r = ini_get_int(h, "network", "retries", 3) + s = ini_get_bool(h, "network", "ssl", 0) + print("user=" + u) + print("retries=" + to_string(r)) + print("ssl=" + to_string(s)) + ok = ini_set(h, "auth", "token", "abcd1234") + if ok + ini_save(h, path) + ini_free(h) diff --git a/examples/ini_subsections.fun b/examples/ini_subsections.fun new file mode 100644 index 0000000..95c8529 --- /dev/null +++ b/examples/ini_subsections.fun @@ -0,0 +1,70 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +// Demonstration of INI subsections like [section.subsection] +// Uses iniparser 4.2.6 via Fun's ini_* opcodes + +path = "./examples/data/subsections.ini" +h = ini_load(path) +if h == 0 + print("Failed to load "+path) +else + // Top-level server + srv_host = ini_get_string(h, "server", "host", "localhost") + srv_port = ini_get_int(h, "server", "port", 80) + + // Subsection: server.tls + tls_enabled = ini_get_bool(h, "server.tls", "enabled", 0) + tls_version = ini_get_double(h, "server.tls", "version", 1.2) + tls_ciphers = ini_get_string(h, "server.tls", "ciphers", "") + + // Subsections: users.* + admin_name = ini_get_string(h, "users.admin", "name", "admin") + admin_active = ini_get_bool(h, "users.admin", "active", 1) + admin_quota = ini_get_int(h, "users.admin", "quota_gb", 10) + + guest_name = ini_get_string(h, "users.guest", "name", "guest") + guest_active = ini_get_bool(h, "users.guest", "active", 0) + guest_quota = ini_get_int(h, "users.guest", "quota_gb", 1) + + // Subsection: paths.logs + logs_dir = ini_get_string(h, "paths.logs", "dir", "./logs") + logs_rotate = ini_get_bool(h, "paths.logs", "rotate", 0) + logs_max_files = ini_get_int(h, "paths.logs", "max_files", 5) + + // Print + print("[server]") + print(" host=" + srv_host) + print(" port=" + to_string(srv_port)) + + print("[server.tls]") + print(" enabled=" + to_string(tls_enabled)) + print(" version=" + to_string(tls_version)) + print(" ciphers=" + tls_ciphers) + + print("[users.admin]") + print(" name=" + admin_name) + print(" active=" + to_string(admin_active)) + print(" quota_gb=" + to_string(admin_quota)) + + print("[users.guest]") + print(" name=" + guest_name) + print(" active=" + to_string(guest_active)) + print(" quota_gb=" + to_string(guest_quota)) + + print("[paths.logs]") + print(" dir=" + logs_dir) + print(" rotate=" + to_string(logs_rotate)) + print(" max_files=" + to_string(logs_max_files)) + + ini_free(h) diff --git a/examples/md5_demo.fun b/examples/md5_demo.fun index f17c5ce..75b69fe 100755 --- a/examples/md5_demo.fun +++ b/examples/md5_demo.fun @@ -20,10 +20,10 @@ md5 = MD5() print("=== MD5 demo (hex input) ===") // "abc" => 0x61 0x62 0x63 -print(md5.md5_hex("616263")) // -> 900150983cd24fb0d6963f7d28e17f72 +print(md5.md5_hex("616263")) // -> 900150983cd24fb0d6963f7d28e17f72 // empty string "" => hex "" -print(md5.md5_hex("")) // -> d41d8cd98f00b204e9800998ecf8427e +print(md5.md5_hex("")) // -> d41d8cd98f00b204e9800998ecf8427e // "message digest" print(md5.md5_hex("6d65737361676520646967657374")) // -> f96b697d7cb7938d525a2f31aaf161d0 @@ -32,7 +32,10 @@ print(md5.md5_hex("6d65737361676520646967657374")) // -> f96b697d7cb7938d525a2f print(md5.md5_hex("6162636465666768696a6b6c6d6e6f707172737475767778797a")) // -> c3fcd3d76192e4007dfb496cca67e13b // "Have Fun!" -print(md5.md5_str("Have Fun!")) // -> 852438d026c018c4307b916406f98c62 +print(md5.md5_str("Have Fun!")) // -> 812f2c01287af0e7c0a0b3daa381a51a + +// More MD5 +print(md5.md5_str("a")) // -> 0cc175b9c0f1b6a831c399e269772661 print("=== Done ===") @@ -43,5 +46,6 @@ d41d8cd98f00b204e9800998ecf8427e f96b697d7cb7938d525a2f31aaf161d0 c3fcd3d76192e4007dfb496cca67e13b 812f2c01287af0e7c0a0b3daa381a51a +0cc175b9c0f1b6a831c399e269772661 === Done === */ diff --git a/src/bytecode.c b/src/bytecode.c index fdf63b1..f7a6492 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -165,6 +165,15 @@ static const char *opcode_name(OpCode op) { case OP_PCRE2_TEST: return "PCRE2_TEST"; case OP_PCRE2_MATCH: return "PCRE2_MATCH"; case OP_PCRE2_FINDALL: return "PCRE2_FINDALL"; + case OP_INI_LOAD: return "INI_LOAD"; + case OP_INI_FREE: return "INI_FREE"; + case OP_INI_GET_STRING: return "INI_GET_STRING"; + case OP_INI_GET_INT: return "INI_GET_INT"; + case OP_INI_GET_DOUBLE: return "INI_GET_DOUBLE"; + case OP_INI_GET_BOOL: return "INI_GET_BOOL"; + case OP_INI_SET: return "INI_SET"; + case OP_INI_UNSET: return "INI_UNSET"; + case OP_INI_SAVE: return "INI_SAVE"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index 908abb3..ef689e9 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -177,6 +177,17 @@ typedef enum { OP_PCRE2_MATCH, // pops flags, text, pattern; pushes match map or Nil OP_PCRE2_FINDALL, // pops flags, text, pattern; pushes array of match maps + // INI (iniparser 4.2.6) optional + OP_INI_LOAD, // pops path; pushes handle (>0) or 0 + OP_INI_FREE, // pops handle; pushes 1/0 + OP_INI_GET_STRING, // pops def, key, section, handle; pushes string + OP_INI_GET_INT, // pops def, key, section, handle; pushes int + OP_INI_GET_DOUBLE, // pops def, key, section, handle; pushes float + OP_INI_GET_BOOL, // pops def, key, section, handle; pushes int (0/1) + OP_INI_SET, // pops value, key, section, handle; pushes 1/0 + OP_INI_UNSET, // pops key, section, handle; pushes 1/0 + OP_INI_SAVE, // pops path, handle; pushes 1/0 + // Sockets (UNIX platforms) OP_SOCK_TCP_LISTEN, // pops backlog, port; returns listen fd (>0) or 0 OP_SOCK_TCP_ACCEPT, // pops listen fd; returns client fd (>0) or 0 diff --git a/src/parser.c b/src/parser.c index ab47be0..101925a 100644 --- a/src/parser.c +++ b/src/parser.c @@ -778,6 +778,115 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* INI (iniparser 4.2.6) builtins */ + if (strcmp(name, "ini_load") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_load expects (path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_load arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_LOAD, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_free") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_free expects (handle)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_free arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_FREE, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_get_string") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_string expects (handle, section, key, default)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_string expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_string expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_string expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_string expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_string expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_string expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_get_string args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_GET_STRING, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_get_int") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_int expects (handle, section, key, default)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_int expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_int expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_int expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_int expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_int expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_int expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_get_int args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_GET_INT, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_get_double") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_double expects (handle, section, key, default)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_double expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_double expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_double expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_double expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_double expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_double expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_get_double args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_GET_DOUBLE, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_get_bool") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_bool expects (handle, section, key, default)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_bool expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_bool expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_bool expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_bool expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_get_bool expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_get_bool expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_get_bool args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_GET_BOOL, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_set") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_set expects (handle, section, key, value)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_set expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_set expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_set expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_set expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_set expects 4 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_set expects 4 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_set args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_SET, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_unset") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_unset expects (handle, section, key)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_unset expects 3 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_unset expects 3 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_unset expects 3 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_unset expects 3 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_unset args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_UNSET, 0); + free(name); + return 1; + } + if (strcmp(name, "ini_save") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_save expects (handle, path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "ini_save expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "ini_save expects 2 args"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after ini_save args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_INI_SAVE, 0); + free(name); + return 1; + } /* CURL builtins (minimal interface like JSON) */ if (strcmp(name, "curl_get") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index e8a8635..4654743 100644 --- a/src/vm.c +++ b/src/vm.c @@ -13,6 +13,23 @@ #include "string.c" #include "pcsc.c" #include "jsonc.c" +#ifdef FUN_WITH_INI +#if defined(__has_include) +# if __has_include() +# include +# include +# elif __has_include() +# include +# include +# else +# error "iniparser headers not found" +# endif +#else +# include +# include +#endif +#include "vm/ini/handles.h" +#endif #ifdef FUN_WITH_SQLITE #include #include "vm/sqlite/common.c" @@ -706,6 +723,14 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/json/from_file.c" #include "vm/json/to_file.c" + /* INI ops (iniparser 4.2.6) */ + #ifdef FUN_WITH_INI + #include "vm/ini/load.c" + #include "vm/ini/free.c" + #include "vm/ini/getters.c" + #include "vm/ini/set_unset_save.c" + #endif + /* CURL ops */ #include "vm/curl/get.c" #include "vm/curl/post.c" diff --git a/src/vm.h b/src/vm.h index 2591da4..153ff09 100644 --- a/src/vm.h +++ b/src/vm.h @@ -44,6 +44,7 @@ static const char *opcode_names[] = { "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", + "INI_LOAD","INI_FREE","INI_GET_STRING","INI_GET_INT","INI_GET_DOUBLE","INI_GET_BOOL","INI_SET","INI_UNSET","INI_SAVE", "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT", "EXIT" }; diff --git a/src/vm/ini/free.c b/src/vm/ini/free.c new file mode 100644 index 0000000..5271bc6 --- /dev/null +++ b/src/vm/ini/free.c @@ -0,0 +1,22 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +/* OP_INI_FREE: pops handle; pushes 1/0 */ +#ifdef FUN_WITH_INI +case OP_INI_FREE: { + Value vh = pop_value(vm); + int h = (vh.type == VAL_INT) ? (int)vh.i : 0; + free_value(vh); + int ok = ini_free_handle(h); + push_value(vm, make_int(ok)); + break; +} +#endif diff --git a/src/vm/ini/getters.c b/src/vm/ini/getters.c new file mode 100644 index 0000000..047888d --- /dev/null +++ b/src/vm/ini/getters.c @@ -0,0 +1,94 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +/* OP_INI_GET_* implementations */ +#ifdef FUN_WITH_INI +case OP_INI_GET_STRING: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + const char *def = (vdef.type==VAL_STRING && vdef.s) ? vdef.s : ""; + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + const char *res = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + const char *s = iniparser_getstring(d, full, def); + res = s ? s : ""; + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_string(res)); + break; +} + +case OP_INI_GET_INT: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + int def = (vdef.type==VAL_INT) ? (int)vdef.i : 0; + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + int outi = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + outi = iniparser_getint(d, full, def); + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(outi)); + break; +} + +case OP_INI_GET_DOUBLE: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + double def = (vdef.type==VAL_FLOAT) ? vdef.d : (vdef.type==VAL_INT ? (double)vdef.i : 0.0); + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + double outd = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + outd = iniparser_getdouble(d, full, def); + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_float(outd)); + break; +} + +case OP_INI_GET_BOOL: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + int def = (vdef.type==VAL_INT||vdef.type==VAL_BOOL) ? (int)vdef.i : 0; + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + int outb = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + outb = iniparser_getboolean(d, full, def); + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(outb ? 1 : 0)); + break; +} +#endif diff --git a/src/vm/ini/handles.h b/src/vm/ini/handles.h new file mode 100644 index 0000000..3c2022f --- /dev/null +++ b/src/vm/ini/handles.h @@ -0,0 +1,61 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +/** INI handle registry for iniparser 4.2.6 */ +#pragma once + +#ifdef FUN_WITH_INI +#if defined(__has_include) +# if __has_include() +# include +# include +# elif __has_include() +# include +# include +# else +# error "iniparser headers not found" +# endif +#else +# include +# include +#endif +#include /* snprintf for helper */ + +typedef struct { dictionary *dict; int in_use; } IniSlot; +static IniSlot g_ini[64]; + +static int ini_alloc_handle(dictionary *d) { + for (int i = 1; i < (int)(sizeof(g_ini)/sizeof(g_ini[0])); ++i) { + if (!g_ini[i].in_use) { g_ini[i].in_use = 1; g_ini[i].dict = d; return i; } + } + return 0; +} +static dictionary* ini_get(int h) { + if (h > 0 && h < (int)(sizeof(g_ini)/sizeof(g_ini[0])) && g_ini[h].in_use) return g_ini[h].dict; + return NULL; +} +static int ini_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_ini)/sizeof(g_ini[0])) || !g_ini[h].in_use) return 0; + if (g_ini[h].dict) iniparser_freedict(g_ini[h].dict); + g_ini[h].dict = NULL; + g_ini[h].in_use = 0; + return 1; +} + +/* Helper to build section:key string safely into provided buffer */ +static inline void ini_make_full_key(char *buf, size_t cap, const char *sec, const char *key) { + if (!buf || cap == 0) return; + if (!sec) sec = ""; + if (!key) key = ""; + /* iniparser expects "section:key" */ + snprintf(buf, cap, "%s:%s", sec, key); +} +#endif /* FUN_WITH_INI */ diff --git a/src/vm/ini/load.c b/src/vm/ini/load.c new file mode 100644 index 0000000..ac401fb --- /dev/null +++ b/src/vm/ini/load.c @@ -0,0 +1,29 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +/* OP_INI_LOAD: pops path string; pushes handle (>0) or 0 */ +#ifdef FUN_WITH_INI +case OP_INI_LOAD: { + Value vpath = pop_value(vm); + const char *path = (vpath.type == VAL_STRING && vpath.s) ? vpath.s : NULL; + int h = 0; + if (path) { + dictionary *d = iniparser_load(path); + if (d) { + h = ini_alloc_handle(d); + if (!h) { iniparser_freedict(d); } + } + } + free_value(vpath); + push_value(vm, make_int(h)); + break; +} +#endif diff --git a/src/vm/ini/set_unset_save.c b/src/vm/ini/set_unset_save.c new file mode 100644 index 0000000..69c1555 --- /dev/null +++ b/src/vm/ini/set_unset_save.c @@ -0,0 +1,70 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +/* OP_INI_SET / OP_INI_UNSET / OP_INI_SAVE */ +#ifdef FUN_WITH_INI +case OP_INI_SET: { + Value vval = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); + const char *key = (vkey.type==VAL_STRING)?vkey.s:NULL; + const char *sec = (vsec.type==VAL_STRING)?vsec.s:NULL; + int ok = 0; + if (d && sec && key) { + char *valstr = value_to_string_alloc(&vval); + if (valstr) { + char full[1024]; snprintf(full, sizeof(full), "%s:%s", sec, key); + /* iniparser 4.x does not expose iniparser_set; use dictionary_set */ + if (dictionary_set(d, full, valstr) == 0) ok = 1; /* 0 means success */ + free(valstr); + } + } + free_value(vval); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(ok)); + break; +} + +case OP_INI_UNSET: { + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); + const char *key = (vkey.type==VAL_STRING)?vkey.s:NULL; + const char *sec = (vsec.type==VAL_STRING)?vsec.s:NULL; + int ok = 0; + if (d && sec && key) { + char full[1024]; snprintf(full, sizeof(full), "%s:%s", sec, key); + /* iniparser 4.2.6 dictionary_unset returns void; assume success if inputs are valid */ + dictionary_unset(d, full); + ok = 1; + } + free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(ok)); + break; +} + +case OP_INI_SAVE: { + Value vpath = pop_value(vm); + Value vh = pop_value(vm); + const char *path = (vpath.type==VAL_STRING)?vpath.s:NULL; + dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); + int ok = 0; + if (d && path) { + FILE *f = fopen(path, "w"); + if (f) { iniparser_dump_ini(d, f); fclose(f); ok = 1; } + } + free_value(vpath); free_value(vh); + push_value(vm, make_int(ok)); + break; +} +#endif From 8fa142e7633e5e0ed9d36cd1d2d907f3ac89a91a Mon Sep 17 00:00:00 2001 From: hanez Date: Sun, 30 Nov 2025 03:33:18 +0100 Subject: [PATCH 28/55] Some more .ini Fun. (0.34.1) --- CMakeLists.txt | 2 +- examples/data/complex.ini | 2 +- examples/ini_class_demo.fun | 46 ++++++++++++++++++ lib/io/ini.fun | 96 +++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 examples/ini_class_demo.fun create mode 100644 lib/io/ini.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c6c921..319605a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.34.0 LANGUAGES C) +project(fun VERSION 0.34.1 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/data/complex.ini b/examples/data/complex.ini index 512fbc5..18084af 100644 --- a/examples/data/complex.ini +++ b/examples/data/complex.ini @@ -2,7 +2,7 @@ [app] name = "FunApp" version = "1.2.3" -debug = "true" +debug = "1" [database] diff --git a/examples/ini_class_demo.fun b/examples/ini_class_demo.fun new file mode 100644 index 0000000..90e6598 --- /dev/null +++ b/examples/ini_class_demo.fun @@ -0,0 +1,46 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +// Demonstration of the Ini stdlib class from lib/io/ini.fun +include + +ini = INI() +path = "./examples/data/complex.ini" + +if (ini.load(path) == 0) + print("Failed to load " + path) + exit(1) + +// Read a few values +app_name = ini.get_string("app", "name", "FunApp") +app_version = ini.get_string("app", "version", "0.0.0") +app_debug = ini.get_bool("app", "debug", 0) + +db_host = ini.get_string("database", "host", "localhost") +db_port = ini.get_int("database", "port", 5432) + +print("[app]") +print(" name=" + app_name) +print(" version=" + app_version) +print(" debug=" + to_string(app_debug)) + +print("[database]") +print(" host=" + db_host) +print(" port=" + to_string(db_port)) + +// Update a value and save back to the same file +ini.set("app", "debug", 1) +ok = ini.save(nil) +print("saved=" + to_string(ok)) + +ini.close() diff --git a/lib/io/ini.fun b/lib/io/ini.fun new file mode 100644 index 0000000..b6145e5 --- /dev/null +++ b/lib/io/ini.fun @@ -0,0 +1,96 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-30 + */ + +// INI stdlib abstraction wrapping the ini_* VM builtins (iniparser 4.2.6). +// +// Usage: +// include +// ini = INI() +// if (ini.load("./examples/data/complex.ini") > 0) +// name = ini.get_string("app", "name", "") +// retries = ini.get_int("network", "retries", 0) +// ini.set("app", "debug", 1) +// ini.save(nil) // save back to original path +// ini.close() + +class INI() + // current handle (>0 when open) and path string + h = 0 + path = "" + + // Load an INI file from path, closing previous one if open. + // Returns handle (>0) or 0 on error. + fun load(this, path) + if (this.h > 0) + ini_free(this.h) + this.h = 0 + p = to_string(path) + this.path = p + this.h = ini_load(p) + return this.h + + // True if a dictionary is open. + fun is_open(this) + return this.h > 0 + + // Close and free resources. Safe to call multiple times. + fun close(this) + if (this.h > 0) + ini_free(this.h) + this.h = 0 + return 1 + + // Getters with defaults. When not open, return the default converted. + fun get_string(this, section, key, def) + if (!this.is_open()) + return to_string(def) + return ini_get_string(this.h, to_string(section), to_string(key), to_string(def)) + + fun get_int(this, section, key, def) + if (!this.is_open()) + return to_number(def) + return ini_get_int(this.h, to_string(section), to_string(key), to_number(def)) + + fun get_double(this, section, key, def) + if (!this.is_open()) + return to_number(def) + return ini_get_double(this.h, to_string(section), to_string(key), to_number(def)) + + fun get_bool(this, section, key, def) + if (!this.is_open()) + if (def == nil) + return 0 + // treat 0/1 and boolean-like strings + return to_number(def) != 0 + return ini_get_bool(this.h, to_string(section), to_string(key), to_number(def)) + + // Set/unset return 1 on success, 0 on failure. + fun set(this, section, key, value) + if (!this.is_open()) + return 0 + return ini_set(this.h, to_string(section), to_string(key), to_string(value)) + + fun unset(this, section, key) + if (!this.is_open()) + return 0 + return ini_unset(this.h, to_string(section), to_string(key)) + + // Save to a path. If path is nil, save to the last loaded path. + fun save(this, path) + if (!this.is_open()) + return 0 + p = path + if (p == nil) + p = this.path + p = to_string(p) + if (len(p) == 0) + return 0 + return ini_save(this.h, p) From ecf64d6ac2efd05c210ddf97097870848b79bfa5 Mon Sep 17 00:00:00 2001 From: hanez Date: Sun, 30 Nov 2025 03:56:47 +0100 Subject: [PATCH 29/55] Some content update. No Code changes. (0.34.0) --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index de99110..7586012 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,7 @@ Fun may not change the world — but it will make programming a little more fun. - [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☑ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ -- [TOML](https://en.wikipedia.org/wiki/TOML) support builtin using [tomlc99](https://github.com/cktan/tomlc99) (optional) ☐ - [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ -- [YAML](https://yaml.org/) support builtin using [libfyaml](https://github.com/pantoniou/libfyaml) (optional) ☐ ☑ = Done / ☐ = Planned or in progress. From e65756226d8f6767d6e504675a685dacb00e3847 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 03:59:49 +0100 Subject: [PATCH 30/55] Added very basic XML support using libxml2. (0.35.0) --- CMakeLists.txt | 49 ++++++++++++++++++++++++++- README.md | 4 +-- docs/handbook.md | 45 +++++++++++++++++++++++++ examples/data/example.xml | 24 ++++++++++++++ examples/xml_class_example.fun | 27 +++++++++++++++ examples/xml_minimal.fun | 17 ++++++++++ lib/io/xml.fun | 39 ++++++++++++++++++++++ src/bytecode.c | 4 +++ src/bytecode.h | 6 ++++ src/parser.c | 33 +++++++++++++++++++ src/vm.c | 11 +++++++ src/vm.h | 1 + src/vm/xml/handles.h | 60 ++++++++++++++++++++++++++++++++++ src/vm/xml/name.c | 26 +++++++++++++++ src/vm/xml/parse.c | 34 +++++++++++++++++++ src/vm/xml/root.c | 30 +++++++++++++++++ src/vm/xml/text.c | 29 ++++++++++++++++ 17 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 examples/data/example.xml create mode 100644 examples/xml_class_example.fun create mode 100644 examples/xml_minimal.fun create mode 100644 lib/io/xml.fun create mode 100644 src/vm/xml/handles.h create mode 100644 src/vm/xml/name.c create mode 100644 src/vm/xml/parse.c create mode 100644 src/vm/xml/root.c create mode 100644 src/vm/xml/text.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 319605a..567b61a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.34.1 LANGUAGES C) +project(fun VERSION 0.35.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -120,6 +120,40 @@ if(FUN_WITH_INI) endif() endif() +# Optional XML (libxml2) support +option(FUN_WITH_XML2 "Enable XML (libxml2) support" OFF) +set(LIBXML2_INCLUDE_DIRS "") +set(LIBXML2_LINK_LIBS "") +if(FUN_WITH_XML2) + message(STATUS "Building with XML (libxml2) support") + add_definitions(-DFUN_WITH_XML2) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBXML2 QUIET libxml-2.0) + endif() + if(LIBXML2_FOUND) + list(APPEND LIBXML2_INCLUDE_DIRS ${LIBXML2_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIRS}) + list(APPEND LIBXML2_LINK_LIBS ${LIBXML2_LINK_LIBS} ${LIBXML2_LIBRARIES}) + include_directories(${LIBXML2_INCLUDE_DIRS}) + else() + find_library(LIBXML2_LIB NAMES xml2 libxml2) + if(LIBXML2_LIB) + list(APPEND LIBXML2_LINK_LIBS ${LIBXML2_LIB}) + # Common system include location for libxml2 headers + if(EXISTS "/usr/include/libxml2") + list(APPEND LIBXML2_INCLUDE_DIRS "/usr/include/libxml2") + include_directories(${LIBXML2_INCLUDE_DIRS}) + endif() + else() + message(FATAL_ERROR "libxml2 not found. Install libxml2 (dev headers) or disable FUN_WITH_XML2.") + endif() + endif() + # As a robust fallback, add standard system include path for libxml2 if present + if(EXISTS "/usr/include/libxml2") + include_directories("/usr/include/libxml2") + endif() +endif() + # Optional libsql support (independent from SQLite) option(FUN_WITH_LIBSQL "Enable libsql (Turso) client support" OFF) set(LIBSQL_INCLUDE_DIRS "") @@ -281,6 +315,19 @@ if(LIBSQL_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${LIBSQL_LINK_LIBS}) endif() +# libxml2 include and link (if enabled) +if(LIBXML2_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${LIBXML2_INCLUDE_DIRS}) +endif() +if(LIBXML2_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${LIBXML2_LINK_LIBS}) +endif() +if(FUN_WITH_XML2) + if(EXISTS "/usr/include/libxml2") + target_include_directories(fun_core PRIVATE "/usr/include/libxml2") + endif() +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index 7586012..618c233 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Fun may not change the world — but it will make programming a little more fun. - [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☑ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ -- [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☐ +- [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☑ ☑ = Done / ☐ = Planned or in progress. @@ -114,5 +114,5 @@ Complete API documentation will follow. ## Author -Johannes Findeisen +Johannes Findeisen - diff --git a/docs/handbook.md b/docs/handbook.md index 085479e..aea4449 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -94,6 +94,7 @@ Pass all options as -DNAME=VALUE. The most relevant toggles are: - FUN_WITH_CURL=ON|OFF — enable CURL (libcurl) support (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON (json-c) support (default OFF) - FUN_WITH_LIBSQL=ON|OFF — enable libSQL (Turso) client support (default OFF) +- FUN_WITH_XML2=ON|OFF — enable XML (libxml2) support (default OFF) - FUN_WITH_PCRE2=ON|OFF — enable PCRE2 (Perl-Compatible Regular Expressions) (default OFF) - FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card (PCSC lite) support (default OFF) - FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default OFF) @@ -148,6 +149,50 @@ Available builtins when built with -DFUN_WITH_LIBSQL=ON: - libsql_exec(handle, sql) -> rc (0 on success) - libsql_query(handle, sql) -> array of map rows +#### XML example (optional feature) + +XML support (via libxml2) is optional and disabled by default. To build with it and run the example: + +``` +cmake -S . -B build -DFUN_WITH_XML2=ON +cmake --build build --target fun + +# Run the example using the stdlib XML class wrapper +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/xml_class_example.fun +``` + +Available VM builtins when built with -DFUN_WITH_XML2=ON: +- xml_parse(text: string) -> doc_handle (int > 0) or 0 on error +- xml_root(doc_handle: int) -> node_handle (int > 0) or 0 if missing +- xml_name(node_handle: int) -> string (node tag name) +- xml_text(node_handle: int) -> string (concatenated text of subtree) + +Standard library wrapper (lib/io/xml.fun): +- class XML + - parse(text: string): int (doc handle) + - from_file(path: string): int (doc handle) + - root(doc: int): int (node handle) + - name(node: int): string + - text(node: int): string + +Example Fun code: +``` +include + +xml = XML() +doc = xml.from_file("./examples/data/example.xml") +if (doc == 0) + print("Failed to load XML file") +else + root = xml.root(doc) + print(xml.name(root)) + print(xml.text(root)) +``` + +Notes: +- Handles are simple integers managed by the VM; nodes are owned by their document. +- This initial integration focuses on parsing and basic navigation. Attributes, children iteration, and XPath may be added later. + ### Install Fun to the OS (optional) Not recommended during early development, but supported: diff --git a/examples/data/example.xml b/examples/data/example.xml new file mode 100644 index 0000000..bc65b4a --- /dev/null +++ b/examples/data/example.xml @@ -0,0 +1,24 @@ + + + + + + Alice + Bob + + + Carol + + + + + Dave + + + + + + + + Welcome to Acme! + diff --git a/examples/xml_class_example.fun b/examples/xml_class_example.fun new file mode 100644 index 0000000..ee13243 --- /dev/null +++ b/examples/xml_class_example.fun @@ -0,0 +1,27 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +// Example using the stdlib XML class wrapper + +include + +xml = XML() +doc = xml.from_file("./examples/data/example.xml") +print("doc handle:") +print(doc) +if (doc == 0) + print("Failed to load XML file") +else + root = xml.root(doc) + print("root name:") + print(xml.name(root)) + print("root text:") + print(xml.text(root)) diff --git a/examples/xml_minimal.fun b/examples/xml_minimal.fun new file mode 100644 index 0000000..376471e --- /dev/null +++ b/examples/xml_minimal.fun @@ -0,0 +1,17 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +// Minimal XML example using libxml2-backed builtins + +doc = xml_parse("ab") +print("doc handle=\(doc)") +root = xml_root(doc) +print("root name=\(xml_name(root)) text=\(xml_text(root))") diff --git a/lib/io/xml.fun b/lib/io/xml.fun new file mode 100644 index 0000000..9e9d922 --- /dev/null +++ b/lib/io/xml.fun @@ -0,0 +1,39 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +// XML stdlib abstraction wrapping the xml_* VM builtins (libxml2-backed). +// Minimal API for now: parse, from_file, root, name, text + +class XML() + // Parse XML text into a document handle (>0) or 0 on error. + fun parse(this, text) + t = to_string(text) + return xml_parse(t) + + // Load XML from a file path; returns document handle (>0) or 0. + fun from_file(this, path) + p = to_string(path) + data = read_file(p) + if (len(data) == 0) + return 0 + return xml_parse(data) + + // Get root node handle (>0) or 0. + fun root(this, doc) + return xml_root(doc) + + // Get node name as string. + fun name(this, node) + return xml_name(node) + + // Get node concatenated text content as string. + fun text(this, node) + return xml_text(node) diff --git a/src/bytecode.c b/src/bytecode.c index f7a6492..738f596 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -174,6 +174,10 @@ static const char *opcode_name(OpCode op) { case OP_INI_SET: return "INI_SET"; case OP_INI_UNSET: return "INI_UNSET"; case OP_INI_SAVE: return "INI_SAVE"; + case OP_XML_PARSE: return "XML_PARSE"; + case OP_XML_ROOT: return "XML_ROOT"; + case OP_XML_NAME: return "XML_NAME"; + case OP_XML_TEXT: return "XML_TEXT"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index ef689e9..73b9206 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -188,6 +188,12 @@ typedef enum { OP_INI_UNSET, // pops key, section, handle; pushes 1/0 OP_INI_SAVE, // pops path, handle; pushes 1/0 + // XML (libxml2) optional minimal API + OP_XML_PARSE, // pops text string; pushes doc handle (>0) or 0 + OP_XML_ROOT, // pops doc handle; pushes node handle (>0) or 0 + OP_XML_NAME, // pops node handle; pushes string (node name) + OP_XML_TEXT, // pops node handle; pushes string (node text) + // Sockets (UNIX platforms) OP_SOCK_TCP_LISTEN, // pops backlog, port; returns listen fd (>0) or 0 OP_SOCK_TCP_ACCEPT, // pops listen fd; returns client fd (>0) or 0 diff --git a/src/parser.c b/src/parser.c index 101925a..7fa6fdd 100644 --- a/src/parser.c +++ b/src/parser.c @@ -748,6 +748,39 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* XML builtins (minimal) */ + if (strcmp(name, "xml_parse") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "xml_parse expects (text)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after xml_parse arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_XML_PARSE, 0); + free(name); + return 1; + } + if (strcmp(name, "xml_root") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "xml_root expects (doc_handle)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after xml_root arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_XML_ROOT, 0); + free(name); + return 1; + } + if (strcmp(name, "xml_name") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "xml_name expects (node_handle)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after xml_name arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_XML_NAME, 0); + free(name); + return 1; + } + if (strcmp(name, "xml_text") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "xml_text expects (node_handle)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after xml_text arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_XML_TEXT, 0); + free(name); + return 1; + } if (strcmp(name, "json_stringify") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_stringify expects (value, pretty)"); free(name); return 0; } diff --git a/src/vm.c b/src/vm.c index 4654743..8657b98 100644 --- a/src/vm.c +++ b/src/vm.c @@ -13,6 +13,9 @@ #include "string.c" #include "pcsc.c" #include "jsonc.c" +#ifdef FUN_WITH_XML2 +#include "vm/xml/handles.h" +#endif #ifdef FUN_WITH_INI #if defined(__has_include) # if __has_include() @@ -723,6 +726,14 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/json/from_file.c" #include "vm/json/to_file.c" + /* XML ops (libxml2) */ + #ifdef FUN_WITH_XML2 + #include "vm/xml/parse.c" + #include "vm/xml/root.c" + #include "vm/xml/name.c" + #include "vm/xml/text.c" + #endif + /* INI ops (iniparser 4.2.6) */ #ifdef FUN_WITH_INI #include "vm/ini/load.c" diff --git a/src/vm.h b/src/vm.h index 153ff09..dd9e756 100644 --- a/src/vm.h +++ b/src/vm.h @@ -45,6 +45,7 @@ static const char *opcode_names[] = { "PCSC_ESTABLISH","PCSC_RELEASE","PCSC_LIST_READERS","PCSC_CONNECT","PCSC_DISCONNECT","PCSC_TRANSMIT", "PCRE2_TEST","PCRE2_MATCH","PCRE2_FINDALL", "INI_LOAD","INI_FREE","INI_GET_STRING","INI_GET_INT","INI_GET_DOUBLE","INI_GET_BOOL","INI_SET","INI_UNSET","INI_SAVE", + "XML_PARSE","XML_ROOT","XML_NAME","XML_TEXT", "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT", "EXIT" }; diff --git a/src/vm/xml/handles.h b/src/vm/xml/handles.h new file mode 100644 index 0000000..ece9548 --- /dev/null +++ b/src/vm/xml/handles.h @@ -0,0 +1,60 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/** Minimal handle registries for libxml2 documents and nodes */ +#pragma once + +#ifdef FUN_WITH_XML2 +#include +#include + +typedef struct { xmlDocPtr doc; int in_use; } XmlDocSlot; +typedef struct { xmlNodePtr node; int in_use; } XmlNodeSlot; + +static XmlDocSlot g_xml_docs[64]; +static XmlNodeSlot g_xml_nodes[256]; + +static int xml_doc_alloc(xmlDocPtr d) { + for (int i = 1; i < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])); ++i) { + if (!g_xml_docs[i].in_use) { g_xml_docs[i].in_use = 1; g_xml_docs[i].doc = d; return i; } + } + return 0; +} +static xmlDocPtr xml_doc_get(int h) { + if (h > 0 && h < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) && g_xml_docs[h].in_use) return g_xml_docs[h].doc; + return NULL; +} +static int xml_doc_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) || !g_xml_docs[h].in_use) return 0; + if (g_xml_docs[h].doc) xmlFreeDoc(g_xml_docs[h].doc); + g_xml_docs[h].doc = NULL; + g_xml_docs[h].in_use = 0; + return 1; +} + +static int xml_node_alloc(xmlNodePtr n) { + for (int i = 1; i < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])); ++i) { + if (!g_xml_nodes[i].in_use) { g_xml_nodes[i].in_use = 1; g_xml_nodes[i].node = n; return i; } + } + return 0; +} +static xmlNodePtr xml_node_get(int h) { + if (h > 0 && h < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) && g_xml_nodes[h].in_use) return g_xml_nodes[h].node; + return NULL; +} +static int xml_node_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) || !g_xml_nodes[h].in_use) return 0; + /* nodes are owned by their document; do not free here */ + g_xml_nodes[h].node = NULL; + g_xml_nodes[h].in_use = 0; + return 1; +} +#endif /* FUN_WITH_XML2 */ diff --git a/src/vm/xml/name.c b/src/vm/xml/name.c new file mode 100644 index 0000000..e1eb0a3 --- /dev/null +++ b/src/vm/xml/name.c @@ -0,0 +1,26 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* OP_XML_NAME: pops node handle; pushes string */ +case OP_XML_NAME: { +#ifdef FUN_WITH_XML2 + Value vh = pop_value(vm); + int h = (vh.type == VAL_INT) ? (int)vh.i : 0; + xmlNodePtr n = xml_node_get(h); + free_value(vh); + const char *name = (n && n->name) ? (const char*)n->name : ""; + push_value(vm, make_string(name)); +#else + Value drop = pop_value(vm); free_value(drop); + push_value(vm, make_string("")); +#endif + break; +} diff --git a/src/vm/xml/parse.c b/src/vm/xml/parse.c new file mode 100644 index 0000000..4fb728e --- /dev/null +++ b/src/vm/xml/parse.c @@ -0,0 +1,34 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* OP_XML_PARSE: pops text string; pushes doc handle (>0) or 0 */ +case OP_XML_PARSE: { +#ifdef FUN_WITH_XML2 + static int xml_inited = 0; + if (!xml_inited) { xmlInitParser(); xml_inited = 1; } + Value vtext = pop_value(vm); + char *text = value_to_string_alloc(&vtext); + free_value(vtext); + if (!text) { push_value(vm, make_int(0)); break; } + xmlDocPtr doc = xmlReadMemory(text, (int)strlen(text), NULL, NULL, XML_PARSE_NONET); + free(text); + int h = 0; + if (doc) { + h = xml_doc_alloc(doc); + if (!h) { xmlFreeDoc(doc); } + } + push_value(vm, make_int(h)); +#else + Value drop = pop_value(vm); free_value(drop); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/src/vm/xml/root.c b/src/vm/xml/root.c new file mode 100644 index 0000000..d671887 --- /dev/null +++ b/src/vm/xml/root.c @@ -0,0 +1,30 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* OP_XML_ROOT: pops doc handle; pushes node handle (>0) or 0 */ +case OP_XML_ROOT: { +#ifdef FUN_WITH_XML2 + Value vh = pop_value(vm); + int h = (vh.type == VAL_INT) ? (int)vh.i : 0; + xmlDocPtr doc = xml_doc_get(h); + free_value(vh); + int nh = 0; + if (doc) { + xmlNodePtr root = xmlDocGetRootElement(doc); + if (root) nh = xml_node_alloc(root); + } + push_value(vm, make_int(nh)); +#else + Value drop = pop_value(vm); free_value(drop); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/src/vm/xml/text.c b/src/vm/xml/text.c new file mode 100644 index 0000000..4eb3ebf --- /dev/null +++ b/src/vm/xml/text.c @@ -0,0 +1,29 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* OP_XML_TEXT: pops node handle; pushes string (concatenate text node children) */ +case OP_XML_TEXT: { +#ifdef FUN_WITH_XML2 + Value vh = pop_value(vm); + int h = (vh.type == VAL_INT) ? (int)vh.i : 0; + xmlNodePtr n = xml_node_get(h); + free_value(vh); + if (!n) { push_value(vm, make_string("")); break; } + xmlChar *content = xmlNodeGetContent(n); + if (!content) { push_value(vm, make_string("")); break; } + push_value(vm, make_string((const char*)content)); + xmlFree(content); +#else + Value drop = pop_value(vm); free_value(drop); + push_value(vm, make_string("")); +#endif + break; +} From db450a871cd105d7b33c59b6a83d49ffaba10db5 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 04:31:41 +0100 Subject: [PATCH 31/55] Added some more libxml2 examples to Fun. (0.35.1) --- CMakeLists.txt | 2 +- examples/data/catalog.xml | 32 +++++++++++++++++++ examples/data/employees.xml | 22 +++++++++++++ examples/data/ns_example.xml | 11 +++++++ examples/xml_access_catalog.fun | 52 +++++++++++++++++++++++++++++++ examples/xml_access_employees.fun | 44 ++++++++++++++++++++++++++ examples/xml_access_ns.fun | 38 ++++++++++++++++++++++ lib/io/xml.fun | 18 +++++++++++ 8 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 examples/data/catalog.xml create mode 100644 examples/data/employees.xml create mode 100644 examples/data/ns_example.xml create mode 100644 examples/xml_access_catalog.fun create mode 100644 examples/xml_access_employees.fun create mode 100644 examples/xml_access_ns.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 567b61a..1206e41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.35.0 LANGUAGES C) +project(fun VERSION 0.35.1 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/data/catalog.xml b/examples/data/catalog.xml new file mode 100644 index 0000000..57916e4 --- /dev/null +++ b/examples/data/catalog.xml @@ -0,0 +1,32 @@ + + + + Wireless Keyboard + Peripherals + 39.99 + + US + Bluetooth + AA + + + + 27" Monitor + Displays + 199.00 + + 2560x1440 + IPS + 75Hz + + + + USB-C Dock + Peripherals + 89.50 + + 2xHDMI, 3xUSB-A, 1xUSB-C PD + 65W + + + diff --git a/examples/data/employees.xml b/examples/data/employees.xml new file mode 100644 index 0000000..9ef7114 --- /dev/null +++ b/examples/data/employees.xml @@ -0,0 +1,22 @@ + + + + + Alice Doe + Senior Developer + alice@example.com + + + Bob Roe + DevOps Engineer + bob@example.com + + + + + Carol Smith + Account Executive + carol@example.com + + + diff --git a/examples/data/ns_example.xml b/examples/data/ns_example.xml new file mode 100644 index 0000000..65ea50b --- /dev/null +++ b/examples/data/ns_example.xml @@ -0,0 +1,11 @@ + + + + The Art of Fun + J. Findeisen + + + Minimal VM Design + A. Dev + + diff --git a/examples/xml_access_catalog.fun b/examples/xml_access_catalog.fun new file mode 100644 index 0000000..9a31ee8 --- /dev/null +++ b/examples/xml_access_catalog.fun @@ -0,0 +1,52 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* + * Demonstrate reading specific fields from an XML file using + * the minimal XML API (root name) and simple string parsing. + */ + +include + +path = "./examples/data/catalog.xml" +xml = XML() +doc = xml.from_file(path) +if (doc == 0) + print("Failed to load: ") + print(path) +else + root = xml.root(doc) + print("Root element: ") + print(xml.name(root)) + + // Show how to access specific fields by quick-and-dirty parsing + content = read_file(path) // raw XML text + // First product name + name = xml.between(content, "", "") + // First price value and its currency attribute (extract value, then attribute) + price_val = xml.between(content, "") + currency = "" + if (len(price_val) > 0) + currency = xml.between(price_val, "currency=\"", "\"") + // strip attribute tag part + price_text = xml.between(price_val, ">", "") // until end + if (len(price_text) == 0) + price_text = price_val + else + price_text = "" + + print("First product name: ") + print(name) + print("First price: ") + if (len(currency) > 0) + print(currency) + print(" ") + print(price_text) diff --git a/examples/xml_access_employees.fun b/examples/xml_access_employees.fun new file mode 100644 index 0000000..c97fcaa --- /dev/null +++ b/examples/xml_access_employees.fun @@ -0,0 +1,44 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* + * Access selected fields in employees.xml using minimal XML API. + */ + +include + +path = "./examples/data/employees.xml" +xml = XML() +doc = xml.from_file(path) +if (doc == 0) + print("Failed to load: ") + print(path) +else + root = xml.root(doc) + print("Root element: ") + print(xml.name(root)) + + content = read_file(path) + // Find the first block and extract its fields + first_emp = xml.between(content, "") + name = xml.between(first_emp, "", "") + role = xml.between(first_emp, "", "") + email = xml.between(first_emp, "", "") + emp_id = xml.between(first_emp, "id=\"", "\"") + + print("First employee id: ") + print(emp_id) + print("Name: ") + print(name) + print("Role: ") + print(role) + print("Email: ") + print(email) diff --git a/examples/xml_access_ns.fun b/examples/xml_access_ns.fun new file mode 100644 index 0000000..ae248e5 --- /dev/null +++ b/examples/xml_access_ns.fun @@ -0,0 +1,38 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* + * Access a namespaced XML file and show prefixed element names. + */ + +include + +path = "./examples/data/ns_example.xml" +xml = XML() +doc = xml.from_file(path) +if (doc == 0) + print("Failed to load: ") + print(path) +else + root = xml.root(doc) + // With namespaces, the node name may include the prefix, e.g., "ns:library" + print("Root element: ") + print(xml.name(root)) + + content = read_file(path) + // Extract first book title and author (namespace prefix bk:) + b1 = xml.between(content, "") + title = xml.between(b1, "", "") + author = xml.between(b1, "", "") + print("First book title: ") + print(title) + print("Author: ") + print(author) diff --git a/lib/io/xml.fun b/lib/io/xml.fun index 9e9d922..a726638 100644 --- a/lib/io/xml.fun +++ b/lib/io/xml.fun @@ -37,3 +37,21 @@ class XML() // Get node concatenated text content as string. fun text(this, node) return xml_text(node) + + // Utility: return substring of s between delimiters a and b. + // - If a is not found, returns "". + // - If b is an empty string, returns everything after the first occurrence of a. + // - If b is not found after a, returns "". + fun between(this, s, a, b) + i = find(s, a) + if (i < 0) + return "" + i = i + len(a) + rest = substr(s, i, len(s) - i) + if (len(b) == 0) + return rest + j = find(rest, b) + if (j < 0) + return "" + return substr(rest, 0, j) + From 66bed28023b4e75c4ff935721963c5d0e8c547b4 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 05:19:08 +0100 Subject: [PATCH 32/55] Some minimal fixes for more Fun. (0.35.2) --- .gitignore | 1 + CMakeLists.txt | 2 +- examples/extra/pcre2_demo.fun | 0 examples/include_local_util.fun | 0 examples/ini_class_demo.fun | 0 examples/ini_complex.fun | 0 examples/ini_demo.fun | 0 examples/ini_subsections.fun | 0 examples/xml_access_catalog.fun | 2 ++ examples/xml_access_employees.fun | 2 ++ examples/xml_access_ns.fun | 2 ++ examples/xml_class_example.fun | 2 ++ examples/xml_minimal.fun | 2 ++ 13 files changed, 12 insertions(+), 1 deletion(-) mode change 100644 => 100755 examples/extra/pcre2_demo.fun mode change 100644 => 100755 examples/include_local_util.fun mode change 100644 => 100755 examples/ini_class_demo.fun mode change 100644 => 100755 examples/ini_complex.fun mode change 100644 => 100755 examples/ini_demo.fun mode change 100644 => 100755 examples/ini_subsections.fun mode change 100644 => 100755 examples/xml_access_catalog.fun mode change 100644 => 100755 examples/xml_access_employees.fun mode change 100644 => 100755 examples/xml_access_ns.fun mode change 100644 => 100755 examples/xml_class_example.fun mode change 100644 => 100755 examples/xml_minimal.fun diff --git a/.gitignore b/.gitignore index 5a53300..a28170a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ database.sqlite demo_* dist/ downloaded.png +json.xml lib/*.so out/ src/*.o diff --git a/CMakeLists.txt b/CMakeLists.txt index 1206e41..927127c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.35.1 LANGUAGES C) +project(fun VERSION 0.35.2 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/extra/pcre2_demo.fun b/examples/extra/pcre2_demo.fun old mode 100644 new mode 100755 diff --git a/examples/include_local_util.fun b/examples/include_local_util.fun old mode 100644 new mode 100755 diff --git a/examples/ini_class_demo.fun b/examples/ini_class_demo.fun old mode 100644 new mode 100755 diff --git a/examples/ini_complex.fun b/examples/ini_complex.fun old mode 100644 new mode 100755 diff --git a/examples/ini_demo.fun b/examples/ini_demo.fun old mode 100644 new mode 100755 diff --git a/examples/ini_subsections.fun b/examples/ini_subsections.fun old mode 100644 new mode 100755 diff --git a/examples/xml_access_catalog.fun b/examples/xml_access_catalog.fun old mode 100644 new mode 100755 index 9a31ee8..43a33a7 --- a/examples/xml_access_catalog.fun +++ b/examples/xml_access_catalog.fun @@ -1,3 +1,5 @@ +#!/usr/bin/env fun + /* * This file is part of the Fun programming language. * https://fun-lang.xyz/ diff --git a/examples/xml_access_employees.fun b/examples/xml_access_employees.fun old mode 100644 new mode 100755 index c97fcaa..f762546 --- a/examples/xml_access_employees.fun +++ b/examples/xml_access_employees.fun @@ -1,3 +1,5 @@ +#!/usr/bin/env fun + /* * This file is part of the Fun programming language. * https://fun-lang.xyz/ diff --git a/examples/xml_access_ns.fun b/examples/xml_access_ns.fun old mode 100644 new mode 100755 index ae248e5..9348c65 --- a/examples/xml_access_ns.fun +++ b/examples/xml_access_ns.fun @@ -1,3 +1,5 @@ +#!/usr/bin/env fun + /* * This file is part of the Fun programming language. * https://fun-lang.xyz/ diff --git a/examples/xml_class_example.fun b/examples/xml_class_example.fun old mode 100644 new mode 100755 index ee13243..8b2d17b --- a/examples/xml_class_example.fun +++ b/examples/xml_class_example.fun @@ -1,3 +1,5 @@ +#!/usr/bin/env fun + /* * This file is part of the Fun programming language. * https://fun-lang.xyz/ diff --git a/examples/xml_minimal.fun b/examples/xml_minimal.fun old mode 100644 new mode 100755 index 376471e..3fb2367 --- a/examples/xml_minimal.fun +++ b/examples/xml_minimal.fun @@ -1,3 +1,5 @@ +#!/usr/bin/env fun + /* * This file is part of the Fun programming language. * https://fun-lang.xyz/ From e482ec198a69e9b3e5656cee9c0b5dc016aa01ac Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 05:44:54 +0100 Subject: [PATCH 33/55] Some content update. No Code changes. (0.35.2) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 618c233..88fd1c5 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Fun may not change the world — but it will make programming a little more fun. ... -### Extensions +### Extensions (only Linux actually) - [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) ☐ - [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ From 682bce05af8d7a8671c415873b45f5b3a54f6ab0 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 05:55:23 +0100 Subject: [PATCH 34/55] Some content update. No Code changes. (0.35.2) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 88fd1c5..3561092 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Fun may not change the world — but it will make programming a little more fun. - [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ - [INI](https://en.wikipedia.org/wiki/INI_file) support builtin using [iniparser](https://gitlab.com/iniparser/iniparser/) (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) ☑ +- [libSQL](https://github.com/tursodatabase/libsql) support builtin as a compatible alternative to SQLite (optional) ☑ - [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ - [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☑ From 6da1861e1284d655bf7beac9b507af534fc5b54e Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 23:13:12 +0100 Subject: [PATCH 35/55] Added very basic Tk (Tcl) support using. (0.36.0) --- CMakeLists.txt | 69 +++++++++++++++++++++++++++++++++++++- README.md | 3 +- docs/handbook.md | 47 ++++++++++++++++++++++++++ examples/tk_hello.fun | 29 ++++++++++++++++ lib/ui/tk.fun | 36 ++++++++++++++++++++ src/bytecode.c | 7 ++++ src/bytecode.h | 9 +++++ src/parser.c | 44 +++++++++++++++++++++++++ src/tk_embed.c | 77 +++++++++++++++++++++++++++++++++++++++++++ src/vm.c | 20 +++++++++++ src/vm/tk/button.c | 34 +++++++++++++++++++ src/vm/tk/eval.c | 21 ++++++++++++ src/vm/tk/label.c | 31 +++++++++++++++++ src/vm/tk/loop.c | 23 +++++++++++++ src/vm/tk/pack.c | 16 +++++++++ src/vm/tk/result.c | 17 ++++++++++ src/vm/tk/wm_title.c | 33 +++++++++++++++++++ 17 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 examples/tk_hello.fun create mode 100644 lib/ui/tk.fun create mode 100644 src/tk_embed.c create mode 100644 src/vm/tk/button.c create mode 100644 src/vm/tk/eval.c create mode 100644 src/vm/tk/label.c create mode 100644 src/vm/tk/loop.c create mode 100644 src/vm/tk/pack.c create mode 100644 src/vm/tk/result.c create mode 100644 src/vm/tk/wm_title.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 927127c..3240d35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.35.2 LANGUAGES C) +project(fun VERSION 0.36.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -24,6 +24,65 @@ if(NOT DEFINED DEFAULT_LIB_DIR OR DEFAULT_LIB_DIR STREQUAL "") else() set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) endif() + +# Optional Tcl/Tk GUI support (embedded interpreter) +option(FUN_WITH_TCLTK "Enable Tcl/Tk GUI support" OFF) +set(TCL_INCLUDE_DIRS "") +set(TCL_LINK_LIBS "") +if(FUN_WITH_TCLTK) + message(STATUS "Building with Tcl/Tk support") + add_definitions(-DFUN_WITH_TCLTK) + # Try pkg-config first + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(TCL QUIET tcl) + pkg_check_modules(TK QUIET tk) + endif() + if(TCL_FOUND OR TK_FOUND) + if(TCL_FOUND) + list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIRS} ${TCL_INCLUDE_DIRS}) + list(APPEND TCL_LINK_LIBS ${TCL_LINK_LIBS} ${TCL_LIBRARIES}) + endif() + if(TK_FOUND) + list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIRS} ${TK_INCLUDE_DIRS}) + list(APPEND TCL_LINK_LIBS ${TCL_LINK_LIBS} ${TK_LIBRARIES}) + endif() + if(TCL_INCLUDE_DIRS) + include_directories(${TCL_INCLUDE_DIRS}) + endif() + else() + # Fallbacks by platform (best-effort) + find_path(TCL_INCLUDE_DIR tcl.h PATH_SUFFIXES tcl8.7 tcl8.6 include) + find_library(TCL_LIB NAMES tcl8.7 tcl8.6 tcl) + find_library(TK_LIB NAMES tk8.7 tk8.6 tk) + if(TCL_INCLUDE_DIR) + list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIR}) + include_directories(${TCL_INCLUDE_DIR}) + endif() + if(TCL_LIB) + list(APPEND TCL_LINK_LIBS ${TCL_LIB}) + endif() + if(TK_LIB) + list(APPEND TCL_LINK_LIBS ${TK_LIB}) + endif() + if(APPLE) + # On macOS additional frameworks are usually not required; Homebrew libs suffice + elseif(WIN32) + # Typical Windows GUI libs + list(APPEND TCL_LINK_LIBS user32 gdi32 comctl32) + else() + # X11 may be required on some Linux setups + find_package(X11 QUIET) + if(X11_FOUND) + include_directories(${X11_INCLUDE_DIR}) + list(APPEND TCL_LINK_LIBS ${X11_LIBRARIES}) + endif() + endif() + if(NOT TCL_LINK_LIBS) + message(WARNING "Tcl/Tk libraries not found via pkg-config or fallbacks. FUN_WITH_TCLTK is enabled, but linking may fail.") + endif() + endif() +endif() # Ensure trailing slash if(NOT DEFAULT_LIB_DIR MATCHES "/$") set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}/") @@ -328,6 +387,14 @@ if(FUN_WITH_XML2) endif() endif() +# Tcl/Tk include and link (if enabled) +if(TCL_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${TCL_INCLUDE_DIRS}) +endif() +if(TCL_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${TCL_LINK_LIBS}) +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index 3561092..8e2a0d6 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Fun may not change the world — but it will make programming a little more fun. - [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ - [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☑ -- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ +- [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☑ - [XML](https://www.w3.org/XML/) support builtin using [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home) (optional) ☑ ☑ = Done / ☐ = Planned or in progress. @@ -115,4 +115,3 @@ Complete API documentation will follow. ## Author Johannes Findeisen - - diff --git a/docs/handbook.md b/docs/handbook.md index aea4449..f78f4d8 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -99,6 +99,7 @@ Pass all options as -DNAME=VALUE. The most relevant toggles are: - FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card (PCSC lite) support (default OFF) - FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default OFF) - FUN_WITH_SQLITE=ON|OFF — enable SQLite (sqlite3) support (default OFF) +- FUN_WITH_TCLTK=ON|OFF — enable Tk (GUI via Tcl/Tk) support (default OFF) You can also set the default search path for the bundled stdlib with DEFAULT_LIB_DIR: @@ -193,6 +194,52 @@ Notes: - Handles are simple integers managed by the VM; nodes are owned by their document. - This initial integration focuses on parsing and basic navigation. Attributes, children iteration, and XPath may be added later. +#### Tk GUI example (optional feature) + +Tk GUI support is optional and disabled by default. It embeds a Tcl/Tk interpreter and exposes a small, Tk-only API to Fun code (no raw Tcl required). + +To build with Tk and run the example: + +``` +cmake -S . -B build -DFUN_WITH_TCLTK=ON +cmake --build build --target fun + +# Run the example +FUN_LIB_DIR="$(pwd)/lib" ./build/fun ./examples/tk_hello.fun +``` + +Available VM builtins when built with -DFUN_WITH_TCLTK=ON: +- tk_title(title: string) -> rc +- tk_label(id: string, text: string) -> rc +- tk_button(id: string, text: string) -> rc +- tk_pack(id: string) -> rc +- tk_loop() -> Nil (enters event loop until the window is closed) + +Standard library wrapper (lib/ui/tk.fun): +- class TK + - title(title: string): int + - label(id: string, text: string): int + - button(id: string, text: string): int + - pack(id: string): int + - loop(): Nil + +Example Fun code: +``` +include + +tk = TK() +tk.title("Fun + Tk GUI") +tk.label("hello", "Hello, world!") +tk.pack("hello") +tk.button("ok", "OK") +tk.pack("ok") +tk.loop() +``` + +Notes: +- Ensure Tcl/Tk is installed (8.6+). On Linux install tcl/tk packages; on macOS install Homebrew tcl-tk; on Windows ensure the DLLs are available. +- The Fun process terminates when the main window is closed or when the example's OK button is clicked. + ### Install Fun to the OS (optional) Not recommended during early development, but supported: diff --git a/examples/tk_hello.fun b/examples/tk_hello.fun new file mode 100644 index 0000000..6110b77 --- /dev/null +++ b/examples/tk_hello.fun @@ -0,0 +1,29 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +// Demonstrates the Tk stdlib wrapper class using the new Tk opcodes. + +include + +tk = TK() + +tk.title("Fun + Tk GUI") + +tk.label("hello", "Hello, world!") +tk.pack("hello") + +tk.button("ok", "OK") +tk.pack("ok") + +// Enter GUI loop (no-op if built without FUN_WITH_TCLTK) +tk.loop() diff --git a/lib/ui/tk.fun b/lib/ui/tk.fun new file mode 100644 index 0000000..e39368d --- /dev/null +++ b/lib/ui/tk.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: 2025-12-09 + */ + +// Tk stdlib helper wrapping the Tk VM builtins. +// No raw Tcl is exposed; this class provides a tiny, safe GUI surface. + +class TK() + // Set the window title + fun title(this, title) + return tk_title(to_string(title)) + + // Create or update a label widget with id and text + fun label(this, id, text) + return tk_label(to_string(id), to_string(text)) + + // Create or update a button widget with id and text + fun button(this, id, text) + return tk_button(to_string(id), to_string(text)) + + // Pack a widget by id + fun pack(this, id) + return tk_pack(to_string(id)) + + // Enter the Tk event loop (blocks until windows are closed) + fun loop(this) + return tk_loop() diff --git a/src/bytecode.c b/src/bytecode.c index 738f596..2dd3517 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -178,6 +178,13 @@ static const char *opcode_name(OpCode op) { case OP_XML_ROOT: return "XML_ROOT"; case OP_XML_NAME: return "XML_NAME"; case OP_XML_TEXT: return "XML_TEXT"; + case OP_TK_EVAL: return "TK_EVAL"; + case OP_TK_RESULT: return "TK_RESULT"; + case OP_TK_LOOP: return "TK_LOOP"; + case OP_TK_WM_TITLE: return "TK_WM_TITLE"; + case OP_TK_LABEL: return "TK_LABEL"; + case OP_TK_BUTTON: return "TK_BUTTON"; + case OP_TK_PACK: return "TK_PACK"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index 73b9206..0576cfb 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -194,6 +194,15 @@ typedef enum { OP_XML_NAME, // pops node handle; pushes string (node name) OP_XML_TEXT, // pops node handle; pushes string (node text) + // Tk (Tcl/Tk) optional minimal API + OP_TK_EVAL, // pops script string; pushes int rc (0 = OK) + OP_TK_RESULT, // pushes string: last Tcl result + OP_TK_LOOP, // enters Tk event loop; pushes Nil when done + OP_TK_WM_TITLE, // pops title string; sets window title; pushes rc + OP_TK_LABEL, // pops text, id; creates/updates label .id; pushes rc + OP_TK_BUTTON, // pops text, id; creates/updates button .id; pushes rc + OP_TK_PACK, // pops id; packs .id; pushes rc + // Sockets (UNIX platforms) OP_SOCK_TCP_LISTEN, // pops backlog, port; returns listen fd (>0) or 0 OP_SOCK_TCP_ACCEPT, // pops listen fd; returns client fd (>0) or 0 diff --git a/src/parser.c b/src/parser.c index 7fa6fdd..00074a1 100644 --- a/src/parser.c +++ b/src/parser.c @@ -791,6 +791,50 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* Tk (GUI) builtins (no raw Tcl exposed) */ + if (strcmp(name, "tk_loop") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "tk_loop expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_TK_LOOP, 0); + free(name); + return 1; + } + if (strcmp(name, "tk_title") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "tk_title expects (title:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after tk_title arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_TK_WM_TITLE, 0); + free(name); + return 1; + } + if (strcmp(name, "tk_label") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "tk_label expects (id:string, text:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "tk_label expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "tk_label expects (id:string, text:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after tk_label args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_TK_LABEL, 0); + free(name); + return 1; + } + if (strcmp(name, "tk_button") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "tk_button expects (id:string, text:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "tk_button expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "tk_button expects (id:string, text:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after tk_button args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_TK_BUTTON, 0); + free(name); + return 1; + } + if (strcmp(name, "tk_pack") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "tk_pack expects (id:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after tk_pack arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_TK_PACK, 0); + free(name); + return 1; + } if (strcmp(name, "json_from_file") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "json_from_file expects (path)"); free(name); return 0; } diff --git a/src/tk_embed.c b/src/tk_embed.c new file mode 100644 index 0000000..d1e6e1f --- /dev/null +++ b/src/tk_embed.c @@ -0,0 +1,77 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/** + * Embedded Tcl/Tk helpers for Fun VM. + * When FUN_WITH_TCLTK is OFF, stubs are provided so code compiles and runs. + */ + +#include "value.h" +#include "vm.h" + +#ifdef FUN_WITH_TCLTK +#include +#include +static Tcl_Interp* g_fun_tcl_interp = NULL; + +static void fun_tk_init_once(void) { + if (g_fun_tcl_interp) return; + Tcl_FindExecutable(NULL); + g_fun_tcl_interp = Tcl_CreateInterp(); + if (!g_fun_tcl_interp) return; + if (Tcl_Init(g_fun_tcl_interp) != TCL_OK) { + fprintf(stderr, "Tcl_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); + } + if (Tk_Init(g_fun_tcl_interp) != TCL_OK) { + fprintf(stderr, "Tk_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); + } + /* Ensure the app terminates if the main window is closed via window manager */ + /* Best-effort: set WM_DELETE_WINDOW handler to exit the process. */ + Tcl_Eval(g_fun_tcl_interp, "wm protocol . WM_DELETE_WINDOW {exit 0}"); +} + +static int fun_tk_eval_script(const char *script) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return -1; + int rc = Tcl_Eval(g_fun_tcl_interp, script ? script : ""); + return rc; /* TCL_OK = 0 */ +} + +static const char* fun_tk_get_result(void) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return ""; + return Tcl_GetStringResult(g_fun_tcl_interp); +} + +static void fun_tk_loop(void) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return; + /* Drive Tk event loop until all main windows are closed */ + while (Tk_GetNumMainWindows() > 0) { + while (Tcl_DoOneEvent(0)) {} + /* tiny sleep to avoid busy spin */ +#ifdef _WIN32 + #include + Sleep(1); +#else + #include + struct timespec ts = {0, 1000000}; /* 1 ms */ + nanosleep(&ts, NULL); +#endif + } +} +#else +/* Stubs when Tcl/Tk is disabled */ +static void fun_tk_init_once(void) { (void)0; } +static int fun_tk_eval_script(const char *script) { (void)script; return -1; } +static const char* fun_tk_get_result(void) { return ""; } +static void fun_tk_loop(void) { (void)0; } +#endif diff --git a/src/vm.c b/src/vm.c index 8657b98..9beaabe 100644 --- a/src/vm.c +++ b/src/vm.c @@ -7,12 +7,23 @@ * https://opensource.org/license/apache-2-0 */ +/* Ensure POSIX prototypes (nanosleep, clock_gettime, localtime_r, etc.) are available + * before any system headers are included by amalgamated .c files. */ +#ifndef _WIN32 +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif +#include + /* Bring in split-out built-ins without changing the build system yet */ #include "iter.c" #include "map.c" #include "string.c" #include "pcsc.c" #include "jsonc.c" +/* Embedded Tcl/Tk helpers (provide stubs when FUN_WITH_TCLTK is off) */ +#include "tk_embed.c" #ifdef FUN_WITH_XML2 #include "vm/xml/handles.h" #endif @@ -747,6 +758,15 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/curl/post.c" #include "vm/curl/download.c" + /* Tk (Tcl/Tk) ops */ + #include "vm/tk/eval.c" + #include "vm/tk/result.c" + #include "vm/tk/loop.c" + #include "vm/tk/wm_title.c" + #include "vm/tk/label.c" + #include "vm/tk/button.c" + #include "vm/tk/pack.c" + /* SQLite ops */ #include "vm/sqlite/open.c" #include "vm/sqlite/close.c" diff --git a/src/vm/tk/button.c b/src/vm/tk/button.c new file mode 100644 index 0000000..8b62792 --- /dev/null +++ b/src/vm/tk/button.c @@ -0,0 +1,34 @@ +/* TK_BUTTON */ +case OP_TK_BUTTON: { + /* stack: ..., id, text -> rc */ + Value textv = pop_value(vm); + Value idv = pop_value(vm); + char *text = value_to_string_alloc(&textv); + char *id = value_to_string_alloc(&idv); + free_value(textv); + free_value(idv); + if (!id) { if (text) free(text); push_value(vm, make_int(-1)); break; } + if (!text) { text = strdup(""); } + size_t n = 0; for (const char *p = text; *p; ++p) { n += (*p == '\\' || *p == '"') ? 2 : 1; } + char *et = (char*)malloc(n + 1); + if (!et) { free(id); free(text); push_value(vm, make_int(-1)); break; } + char *q = et; for (const char *p = text; *p; ++p) { if (*p == '\\' || *p == '"') *q++ = '\\'; *q++ = *p; } *q = '\0'; + free(text); + size_t slen = strlen(id) + strlen(et) + 196; + char *script = (char*)malloc(slen); + if (!script) { free(id); free(et); push_value(vm, make_int(-1)); break; } + /* + * Default behavior: clicking the button should terminate the app. We set + * -command {catch {destroy .}; exit 0} to both destroy the window and exit + * the process. Using 'catch' makes it safe if the window is already gone. + */ + snprintf(script, slen, + "if {[winfo exists .%s]} { .%s configure -text \"%s\" -command {catch {destroy .}; exit 0} } else { button .%s -text \"%s\" -command {catch {destroy .}; exit 0} }", + id, id, et, id, et); + int rc = fun_tk_eval_script(script); + free(script); + free(id); + free(et); + push_value(vm, make_int(rc)); + break; +} diff --git a/src/vm/tk/eval.c b/src/vm/tk/eval.c new file mode 100644 index 0000000..c2fd543 --- /dev/null +++ b/src/vm/tk/eval.c @@ -0,0 +1,21 @@ +/* +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + + /* TK_EVAL */ +case OP_TK_EVAL: { + Value text = pop_value(vm); + char *s = value_to_string_alloc(&text); + free_value(text); + int rc = fun_tk_eval_script(s ? s : ""); + if (s) free(s); + push_value(vm, make_int(rc)); + break; +} diff --git a/src/vm/tk/label.c b/src/vm/tk/label.c new file mode 100644 index 0000000..cb4dcfd --- /dev/null +++ b/src/vm/tk/label.c @@ -0,0 +1,31 @@ +/* TK_LABEL */ +case OP_TK_LABEL: { + /* stack: ..., id, text -> rc */ + Value textv = pop_value(vm); + Value idv = pop_value(vm); + char *text = value_to_string_alloc(&textv); + char *id = value_to_string_alloc(&idv); + free_value(textv); + free_value(idv); + if (!id) { if (text) free(text); push_value(vm, make_int(-1)); break; } + if (!text) { text = strdup(""); } + /* escape id minimally (dots and word chars are fine) -> just use as-is */ + /* escape text for Tcl double quotes */ + size_t n = 0; for (const char *p = text; *p; ++p) { n += (*p == '\\' || *p == '"') ? 2 : 1; } + char *et = (char*)malloc(n + 1); + if (!et) { free(id); free(text); push_value(vm, make_int(-1)); break; } + char *q = et; for (const char *p = text; *p; ++p) { if (*p == '\\' || *p == '"') *q++ = '\\'; *q++ = *p; } *q = '\0'; + free(text); + size_t slen = strlen(id) + strlen(et) + 128; + char *script = (char*)malloc(slen); + if (!script) { free(id); free(et); push_value(vm, make_int(-1)); break; } + snprintf(script, slen, + "if {[winfo exists .%s]} { .%s configure -text \"%s\" } else { label .%s -text \"%s\" }", + id, id, et, id, et); + int rc = fun_tk_eval_script(script); + free(script); + free(id); + free(et); + push_value(vm, make_int(rc)); + break; +} diff --git a/src/vm/tk/loop.c b/src/vm/tk/loop.c new file mode 100644 index 0000000..a327b40 --- /dev/null +++ b/src/vm/tk/loop.c @@ -0,0 +1,23 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* TK_LOOP */ +case OP_TK_LOOP: { + fun_tk_loop(); +#ifdef FUN_WITH_TCLTK + /* Ensure the process terminates once the GUI window(s) are closed. */ + exit(0); +#else + /* When Tk support is not compiled in, behave as a no-op returning Nil. */ + push_value(vm, make_nil()); +#endif + break; /* not reached when FUN_WITH_TCLTK */ +} diff --git a/src/vm/tk/pack.c b/src/vm/tk/pack.c new file mode 100644 index 0000000..71e4345 --- /dev/null +++ b/src/vm/tk/pack.c @@ -0,0 +1,16 @@ +/* TK_PACK */ +case OP_TK_PACK: { + Value idv = pop_value(vm); + char *id = value_to_string_alloc(&idv); + free_value(idv); + if (!id) { push_value(vm, make_int(-1)); break; } + size_t slen = strlen(id) + 16; + char *script = (char*)malloc(slen); + if (!script) { free(id); push_value(vm, make_int(-1)); break; } + snprintf(script, slen, "pack .%s", id); + int rc = fun_tk_eval_script(script); + free(script); + free(id); + push_value(vm, make_int(rc)); + break; +} diff --git a/src/vm/tk/result.c b/src/vm/tk/result.c new file mode 100644 index 0000000..36f9a54 --- /dev/null +++ b/src/vm/tk/result.c @@ -0,0 +1,17 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* TK_RESULT */ +case OP_TK_RESULT: { + const char *r = fun_tk_get_result(); + push_value(vm, make_string(r ? r : "")); + break; +} diff --git a/src/vm/tk/wm_title.c b/src/vm/tk/wm_title.c new file mode 100644 index 0000000..5346b6e --- /dev/null +++ b/src/vm/tk/wm_title.c @@ -0,0 +1,33 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +/* TK_WM_TITLE */ +case OP_TK_WM_TITLE: { + Value titlev = pop_value(vm); + char *title = value_to_string_alloc(&titlev); + free_value(titlev); + if (!title) { push_value(vm, make_int(-1)); break; } + /* Escape backslashes and double quotes for Tcl double-quoted strings */ + size_t n = 0; for (const char *p = title; *p; ++p) { n += (*p == '\\' || *p == '"') ? 2 : 1; } + char *esc = (char*)malloc(n + 1); + if (!esc) { free(title); push_value(vm, make_int(-1)); break; } + char *q = esc; for (const char *p = title; *p; ++p) { if (*p == '\\' || *p == '"') *q++ = '\\'; *q++ = *p; } *q = '\0'; + free(title); + size_t slen = strlen(esc) + 32; + char *script = (char*)malloc(slen); + if (!script) { free(esc); push_value(vm, make_int(-1)); break; } + snprintf(script, slen, "wm title . \"%s\"", esc); + int rc = fun_tk_eval_script(script); + free(script); + free(esc); + push_value(vm, make_int(rc)); + break; +} From 40d114cb9c42da758c4d3d051160bfed21f22433 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 9 Dec 2025 23:28:28 +0100 Subject: [PATCH 36/55] Reverted some stupid file renames. No code changes. (0.36.0) --- CODE_OF_CONDUCT_2_1.md => CODE_OF_CONDUCT.md | 0 SEMANTIC_VERSIONING_2_0_0.md => SEMANTIC_VERSIONING.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CODE_OF_CONDUCT_2_1.md => CODE_OF_CONDUCT.md (100%) rename SEMANTIC_VERSIONING_2_0_0.md => SEMANTIC_VERSIONING.md (100%) diff --git a/CODE_OF_CONDUCT_2_1.md b/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT_2_1.md rename to CODE_OF_CONDUCT.md diff --git a/SEMANTIC_VERSIONING_2_0_0.md b/SEMANTIC_VERSIONING.md similarity index 100% rename from SEMANTIC_VERSIONING_2_0_0.md rename to SEMANTIC_VERSIONING.md From 9dc8afc3873169bbef6e88f9af25cebd2731e7cb Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 00:00:46 +0100 Subject: [PATCH 37/55] Added some more datetime stdlib Fun with examples. And some permission fixes. (0.36.1) --- CMakeLists.txt | 2 +- examples/datetime_extended.fun | 66 +++++++++++++++++++++++++++++++ examples/datetime_timer.fun | 31 +++++++++++++++ examples/tk_hello.fun | 0 lib/utils/datetime.fun | 72 ++++++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 examples/datetime_extended.fun create mode 100644 examples/datetime_timer.fun mode change 100644 => 100755 examples/tk_hello.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 3240d35..7bc6100 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.36.0 LANGUAGES C) +project(fun VERSION 0.36.1 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/datetime_extended.fun b/examples/datetime_extended.fun new file mode 100644 index 0000000..baf9cc9 --- /dev/null +++ b/examples/datetime_extended.fun @@ -0,0 +1,66 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +// Extended Date/Time examples using the stdlib DateTime class +#include + +fun main() + dt = DateTime() + + print("--- Basics ---") + now_ms = dt.now_ms() + print(join(["now_ms: ", to_string(now_ms)], "")) + print(join(["now_s: ", to_string(dt.now_s())], "")) + print(join(["iso_now: ", dt.iso_now()], "")) + print(join(["today: ", dt.today_str()], "")) + + print("--- Formatting helpers ---") + print(join(["iso_from(now): ", dt.iso_from(now_ms)], "")) + print(join(["date_str(now): ", dt.date_str(now_ms)], "")) + print(join(["time_str(now): ", dt.time_str(now_ms)], "")) + + print("--- Conversions ---") + print(join(["ms_to_s(1234): ", to_string(dt.ms_to_s(1234))], "")) + print(join(["s_to_ms(2): ", to_string(dt.s_to_ms(2))], "")) + + print("--- Arithmetic ---") + in_2s = dt.add_seconds(now_ms, 2) + print(join(["in 2s (ms): ", to_string(in_2s)], "")) + print(join(["diff_ms(now, in_2s): ", to_string(dt.diff_ms(now_ms, in_2s))], "")) + + print("--- Timer ---") + t0 = dt.start_timer() + dt.sleep_ms(120) + print(join(["elapsed ~120ms: ", to_string(dt.elapsed_ms(t0)), " ms"], "")) + +main() + +/* Possible output: +--- Basics --- +now_ms: 1765320472780 +now_s: 1765320472 +iso_now: 2025-12-09T23:47:52 +today: 2025-12-09 +--- Formatting helpers --- +iso_from(now): 2025-12-09T23:47:52 +date_str(now): 2025-12-09 +time_str(now): 23:47:52 +--- Conversions --- +ms_to_s(1234): 1 +s_to_ms(2): 2000 +--- Arithmetic --- +in 2s (ms): 1765320474780 +diff_ms(now, in_2s): 2000 +--- Timer --- +elapsed ~120ms: 120 ms +*/ diff --git a/examples/datetime_timer.fun b/examples/datetime_timer.fun new file mode 100644 index 0000000..be1d21c --- /dev/null +++ b/examples/datetime_timer.fun @@ -0,0 +1,31 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-09 + */ + +// Simple stopwatch using DateTime helpers +#include + +fun main() + dt = DateTime() + print("Starting timer for ~250ms ...") + t0 = dt.start_timer() + dt.sleep_s(0.2) // 200 ms + dt.sleep_ms(50) + elapsed = dt.elapsed_ms(t0) + print(join(["Elapsed: ", to_string(elapsed), " ms"], "")) + +main() + +/* Possible output: +Starting timer for ~250ms ... +Elapsed: 250 ms +*/ diff --git a/examples/tk_hello.fun b/examples/tk_hello.fun old mode 100644 new mode 100755 diff --git a/lib/utils/datetime.fun b/lib/utils/datetime.fun index 38517ca..0db8130 100644 --- a/lib/utils/datetime.fun +++ b/lib/utils/datetime.fun @@ -1,3 +1,12 @@ +/* + * 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 + */ + // Date/time utilities abstraction for the Fun stdlib. // Minimal version to validate syntax. @@ -30,3 +39,66 @@ class DateTime() fun iso_now(this) ms = time_now_ms() return date_format(ms, "%Y-%m-%dT%H:%M:%S") + + // Seconds since Unix epoch (integer) + fun now_s(this) + return to_number(time_now_ms() / 1000) + + // Convert milliseconds to seconds (floor) + fun ms_to_s(this, ms) + return to_number(to_number(ms) / 1000) + + // Convert seconds to milliseconds + fun s_to_ms(this, s) + return to_number(to_number(s) * 1000) + + // Add milliseconds to an epoch-ms timestamp + fun add_ms(this, ms, delta_ms) + return to_number(to_number(ms) + to_number(delta_ms)) + + // Add seconds to an epoch-ms timestamp + fun add_seconds(this, ms, seconds) + return this.add_ms(ms, this.s_to_ms(seconds)) + + // Difference in milliseconds: b - a + fun diff_ms(this, a_ms, b_ms) + return to_number(to_number(b_ms) - to_number(a_ms)) + + // Milliseconds elapsed since given epoch-ms timestamp + fun since_ms(this, past_ms) + return this.diff_ms(past_ms, this.now_ms()) + + // Format an arbitrary epoch-ms timestamp as ISO local + fun iso_from(this, ms) + return date_format(to_number(ms), "%Y-%m-%dT%H:%M:%S") + + // Date-only string for a timestamp (YYYY-MM-DD) + fun date_str(this, ms) + return date_format(to_number(ms), "%Y-%m-%d") + + // Time-only string for a timestamp (HH:MM:SS) + fun time_str(this, ms) + return date_format(to_number(ms), "%H:%M:%S") + + // Today's date as YYYY-MM-DD + fun today_str(this) + return this.date_str(this.now_ms()) + + // Start a monotonic timer + fun start_timer(this) + return clock_mono_ms() + + // Elapsed ms from a monotonic start value + fun elapsed_ms(this, start_mono_ms) + return to_number(clock_mono_ms() - to_number(start_mono_ms)) + + // Sleep for the given milliseconds (non-negative) + fun sleep_ms(this, ms) + m = to_number(ms) + if m > 0 + sleep(m) + return m + + // Sleep for the given seconds + fun sleep_s(this, s) + return this.sleep_ms(this.s_to_ms(s)) From b1e1160bff5708397bad363f73a4e33dbe1749dc Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 00:30:24 +0100 Subject: [PATCH 38/55] Added echo to output data wthout a newline. (0.36.2) --- CMakeLists.txt | 2 +- src/bytecode.c | 1 + src/bytecode.h | 1 + src/parser.c | 30 ++++++++++++++++++++++++++++++ src/vm.c | 1 + src/vm.h | 2 +- 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bc6100..f8323b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.36.1 LANGUAGES C) +project(fun VERSION 0.36.2 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/src/bytecode.c b/src/bytecode.c index 2dd3517..647c365 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -77,6 +77,7 @@ static const char *opcode_name(OpCode op) { case OP_CALL: return "CALL"; case OP_RETURN: return "RETURN"; case OP_PRINT: return "PRINT"; + case OP_ECHO: return "ECHO"; case OP_HALT: return "HALT"; case OP_MOD: return "MOD"; case OP_AND: return "AND"; diff --git a/src/bytecode.h b/src/bytecode.h index 0576cfb..dc9563b 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -43,6 +43,7 @@ typedef enum { OP_RETURN, // pop optional return value and return to caller OP_PRINT, + OP_ECHO, // like print but does not append a newline; prints immediately OP_HALT, OP_LINE, // operand = source line number (debug marker) diff --git a/src/parser.c b/src/parser.c index 00074a1..b61d5fd 100644 --- a/src/parser.c +++ b/src/parser.c @@ -2806,6 +2806,21 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si return; } + if (strcmp(name, "echo") == 0) { + free(name); + skip_spaces(src, len, &local_pos); + (void)consume_char(src, len, &local_pos, '('); + if (emit_expression(bc, src, len, &local_pos)) { + (void)consume_char(src, len, &local_pos, ')'); + bytecode_add_instruction(bc, OP_ECHO, 0); + } else { + (void)consume_char(src, len, &local_pos, ')'); + } + *pos = local_pos; + skip_to_eol(src, len, pos); + return; + } + /* assignment or simple call */ int lidx = local_find(name); int gi = (lidx < 0) ? sym_index(name) : -1; @@ -3120,6 +3135,21 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si return; } + /* echo(expr): like print but does not add a newline (immediate output) */ + if (starts_with(src, len, *pos, "echo")) { + *pos += 4; + skip_spaces(src, len, pos); + (void)consume_char(src, len, pos, '('); + if (emit_expression(bc, src, len, pos)) { + (void)consume_char(src, len, pos, ')'); + bytecode_add_instruction(bc, OP_ECHO, 0); + } else { + (void)consume_char(src, len, pos, ')'); + } + skip_to_eol(src, len, pos); + return; + } + /* unknown token: report error */ parser_fail(*pos, "Unknown token at start of statement"); } diff --git a/src/vm.c b/src/vm.c index 9beaabe..18fb613 100644 --- a/src/vm.c +++ b/src/vm.c @@ -794,6 +794,7 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/len.c" #include "vm/line.c" #include "vm/print.c" + #include "vm/echo.c" #include "vm/to_number.c" #include "vm/to_string.c" #include "vm/cast.c" diff --git a/src/vm.h b/src/vm.h index dd9e756..5b603e5 100644 --- a/src/vm.h +++ b/src/vm.h @@ -22,7 +22,7 @@ static const char *opcode_names[] = { "NOP","LOAD_CONST","LOAD_LOCAL","STORE_LOCAL", "LOAD_GLOBAL","STORE_GLOBAL","ADD","SUB","MUL","DIV", "LT","LTE","GT","GTE","EQ","NEQ","POP","JUMP", - "JUMP_IF_FALSE","CALL","RETURN","PRINT","HALT", + "JUMP_IF_FALSE","CALL","RETURN","PRINT","ECHO","HALT", "LINE", "MOD","AND","OR","NOT","DUP","SWAP", "MAKE_ARRAY","INDEX_GET","INDEX_SET", From 9fabf86e61c3ca5c040e72d6b4ee12aceb884bc6 Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 00:31:07 +0100 Subject: [PATCH 39/55] Added echo to output data wthout a newline. (0.36.2) --- examples/echo_example.fun | 23 +++++++++++++++++++++++ src/vm/echo.c | 12 ++++++++++++ 2 files changed, 35 insertions(+) create mode 100755 examples/echo_example.fun create mode 100644 src/vm/echo.c diff --git a/examples/echo_example.fun b/examples/echo_example.fun new file mode 100755 index 0000000..017e89c --- /dev/null +++ b/examples/echo_example.fun @@ -0,0 +1,23 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 + */ + +// echo_example.fun +// Demonstrates echo(expr) which prints without a trailing newline. + +// Build a line without newline using echo, then finish with print to add newline +echo("Hello, ") +echo("world") +print("!") + +// Expected output: +// Hello, world! diff --git a/src/vm/echo.c b/src/vm/echo.c new file mode 100644 index 0000000..626bd7d --- /dev/null +++ b/src/vm/echo.c @@ -0,0 +1,12 @@ +/** + * Implements OP_ECHO: print top-of-stack value without trailing newline. + * Does not store into VM output buffer; writes directly to stdout and flushes. + */ + +case OP_ECHO: { + Value v = pop_value(vm); + print_value(&v); + fflush(stdout); + free_value(v); + break; +} From 3722df654ba6964287a9bcc25d7416983dc51cf4 Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 03:00:32 +0100 Subject: [PATCH 40/55] Some permission updates. No code changes. (0.36.2) --- examples/datetime_extended.fun | 0 examples/datetime_timer.fun | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/datetime_extended.fun mode change 100644 => 100755 examples/datetime_timer.fun diff --git a/examples/datetime_extended.fun b/examples/datetime_extended.fun old mode 100644 new mode 100755 diff --git a/examples/datetime_timer.fun b/examples/datetime_timer.fun old mode 100644 new mode 100755 From ccd03d3bec677d338631d1773b6a0b3f4e9cb6db Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 18:32:03 +0100 Subject: [PATCH 41/55] Split ./src/vm/ini/* opcodes into seperate files. (0.36.3) --- CMakeLists.txt | 2 +- src/vm.c | 9 +++- src/vm/ini/get_bool.c | 33 +++++++++++++ src/vm/ini/get_double.c | 33 +++++++++++++ src/vm/ini/get_int.c | 33 +++++++++++++ src/vm/ini/get_string.c | 34 ++++++++++++++ src/vm/ini/getters.c | 94 ------------------------------------- src/vm/ini/save.c | 28 +++++++++++ src/vm/ini/set.c | 36 ++++++++++++++ src/vm/ini/set_unset_save.c | 70 --------------------------- src/vm/ini/unset.c | 32 +++++++++++++ 11 files changed, 237 insertions(+), 167 deletions(-) create mode 100644 src/vm/ini/get_bool.c create mode 100644 src/vm/ini/get_double.c create mode 100644 src/vm/ini/get_int.c create mode 100644 src/vm/ini/get_string.c delete mode 100644 src/vm/ini/getters.c create mode 100644 src/vm/ini/save.c create mode 100644 src/vm/ini/set.c delete mode 100644 src/vm/ini/set_unset_save.c create mode 100644 src/vm/ini/unset.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f8323b4..d6a3582 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.36.2 LANGUAGES C) +project(fun VERSION 0.36.3 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/src/vm.c b/src/vm.c index 18fb613..1a13e24 100644 --- a/src/vm.c +++ b/src/vm.c @@ -749,8 +749,13 @@ void vm_run(VM *vm, Bytecode *entry) { #ifdef FUN_WITH_INI #include "vm/ini/load.c" #include "vm/ini/free.c" - #include "vm/ini/getters.c" - #include "vm/ini/set_unset_save.c" + #include "vm/ini/get_string.c" + #include "vm/ini/get_int.c" + #include "vm/ini/get_double.c" + #include "vm/ini/get_bool.c" + #include "vm/ini/set.c" + #include "vm/ini/unset.c" + #include "vm/ini/save.c" #endif /* CURL ops */ diff --git a/src/vm/ini/get_bool.c b/src/vm/ini/get_bool.c new file mode 100644 index 0000000..fa886fb --- /dev/null +++ b/src/vm/ini/get_bool.c @@ -0,0 +1,33 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from getters.c) + */ + +/* OP_INI_GET_BOOL */ +#ifdef FUN_WITH_INI +case OP_INI_GET_BOOL: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + int def = (vdef.type==VAL_INT||vdef.type==VAL_BOOL) ? (int)vdef.i : 0; + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + int outb = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + outb = iniparser_getboolean(d, full, def); + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(outb ? 1 : 0)); + break; +} +#endif diff --git a/src/vm/ini/get_double.c b/src/vm/ini/get_double.c new file mode 100644 index 0000000..9cf676f --- /dev/null +++ b/src/vm/ini/get_double.c @@ -0,0 +1,33 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from getters.c) + */ + +/* OP_INI_GET_DOUBLE */ +#ifdef FUN_WITH_INI +case OP_INI_GET_DOUBLE: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + double def = (vdef.type==VAL_FLOAT) ? vdef.d : (vdef.type==VAL_INT ? (double)vdef.i : 0.0); + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + double outd = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + outd = iniparser_getdouble(d, full, def); + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_float(outd)); + break; +} +#endif diff --git a/src/vm/ini/get_int.c b/src/vm/ini/get_int.c new file mode 100644 index 0000000..930bea9 --- /dev/null +++ b/src/vm/ini/get_int.c @@ -0,0 +1,33 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from getters.c) + */ + +/* OP_INI_GET_INT */ +#ifdef FUN_WITH_INI +case OP_INI_GET_INT: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + int def = (vdef.type==VAL_INT) ? (int)vdef.i : 0; + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + int outi = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + outi = iniparser_getint(d, full, def); + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(outi)); + break; +} +#endif diff --git a/src/vm/ini/get_string.c b/src/vm/ini/get_string.c new file mode 100644 index 0000000..fcfa36f --- /dev/null +++ b/src/vm/ini/get_string.c @@ -0,0 +1,34 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from getters.c) + */ + +/* OP_INI_GET_STRING */ +#ifdef FUN_WITH_INI +case OP_INI_GET_STRING: { + Value vdef = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + const char *def = (vdef.type==VAL_STRING && vdef.s) ? vdef.s : ""; + const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; + const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; + int h = (vh.type==VAL_INT) ? (int)vh.i : 0; + dictionary *d = ini_get(h); + const char *res = def; + if (d && sec && key) { + char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); + const char *s = iniparser_getstring(d, full, def); + res = s ? s : ""; + } + free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_string(res)); + break; +} +#endif diff --git a/src/vm/ini/getters.c b/src/vm/ini/getters.c deleted file mode 100644 index 047888d..0000000 --- a/src/vm/ini/getters.c +++ /dev/null @@ -1,94 +0,0 @@ -/* - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-11-30 - */ - -/* OP_INI_GET_* implementations */ -#ifdef FUN_WITH_INI -case OP_INI_GET_STRING: { - Value vdef = pop_value(vm); - Value vkey = pop_value(vm); - Value vsec = pop_value(vm); - Value vh = pop_value(vm); - const char *def = (vdef.type==VAL_STRING && vdef.s) ? vdef.s : ""; - const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; - const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; - int h = (vh.type==VAL_INT) ? (int)vh.i : 0; - dictionary *d = ini_get(h); - const char *res = def; - if (d && sec && key) { - char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); - const char *s = iniparser_getstring(d, full, def); - res = s ? s : ""; - } - free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); - push_value(vm, make_string(res)); - break; -} - -case OP_INI_GET_INT: { - Value vdef = pop_value(vm); - Value vkey = pop_value(vm); - Value vsec = pop_value(vm); - Value vh = pop_value(vm); - int def = (vdef.type==VAL_INT) ? (int)vdef.i : 0; - const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; - const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; - int h = (vh.type==VAL_INT) ? (int)vh.i : 0; - dictionary *d = ini_get(h); - int outi = def; - if (d && sec && key) { - char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); - outi = iniparser_getint(d, full, def); - } - free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); - push_value(vm, make_int(outi)); - break; -} - -case OP_INI_GET_DOUBLE: { - Value vdef = pop_value(vm); - Value vkey = pop_value(vm); - Value vsec = pop_value(vm); - Value vh = pop_value(vm); - double def = (vdef.type==VAL_FLOAT) ? vdef.d : (vdef.type==VAL_INT ? (double)vdef.i : 0.0); - const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; - const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; - int h = (vh.type==VAL_INT) ? (int)vh.i : 0; - dictionary *d = ini_get(h); - double outd = def; - if (d && sec && key) { - char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); - outd = iniparser_getdouble(d, full, def); - } - free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); - push_value(vm, make_float(outd)); - break; -} - -case OP_INI_GET_BOOL: { - Value vdef = pop_value(vm); - Value vkey = pop_value(vm); - Value vsec = pop_value(vm); - Value vh = pop_value(vm); - int def = (vdef.type==VAL_INT||vdef.type==VAL_BOOL) ? (int)vdef.i : 0; - const char *key = (vkey.type==VAL_STRING) ? vkey.s : NULL; - const char *sec = (vsec.type==VAL_STRING) ? vsec.s : NULL; - int h = (vh.type==VAL_INT) ? (int)vh.i : 0; - dictionary *d = ini_get(h); - int outb = def; - if (d && sec && key) { - char full[1024]; ini_make_full_key(full, sizeof(full), sec, key); - outb = iniparser_getboolean(d, full, def); - } - free_value(vdef); free_value(vkey); free_value(vsec); free_value(vh); - push_value(vm, make_int(outb ? 1 : 0)); - break; -} -#endif diff --git a/src/vm/ini/save.c b/src/vm/ini/save.c new file mode 100644 index 0000000..ba67f60 --- /dev/null +++ b/src/vm/ini/save.c @@ -0,0 +1,28 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from set_unset_save.c) + */ + +/* OP_INI_SAVE */ +#ifdef FUN_WITH_INI +case OP_INI_SAVE: { + Value vpath = pop_value(vm); + Value vh = pop_value(vm); + const char *path = (vpath.type==VAL_STRING)?vpath.s:NULL; + dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); + int ok = 0; + if (d && path) { + FILE *f = fopen(path, "w"); + if (f) { iniparser_dump_ini(d, f); fclose(f); ok = 1; } + } + free_value(vpath); free_value(vh); + push_value(vm, make_int(ok)); + break; +} +#endif diff --git a/src/vm/ini/set.c b/src/vm/ini/set.c new file mode 100644 index 0000000..a0affd9 --- /dev/null +++ b/src/vm/ini/set.c @@ -0,0 +1,36 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from set_unset_save.c) + */ + +/* OP_INI_SET */ +#ifdef FUN_WITH_INI +case OP_INI_SET: { + Value vval = pop_value(vm); + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); + const char *key = (vkey.type==VAL_STRING)?vkey.s:NULL; + const char *sec = (vsec.type==VAL_STRING)?vsec.s:NULL; + int ok = 0; + if (d && sec && key) { + char *valstr = value_to_string_alloc(&vval); + if (valstr) { + char full[1024]; snprintf(full, sizeof(full), "%s:%s", sec, key); + /* iniparser 4.x does not expose iniparser_set; use dictionary_set */ + if (dictionary_set(d, full, valstr) == 0) ok = 1; /* 0 means success */ + free(valstr); + } + } + free_value(vval); free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(ok)); + break; +} +#endif diff --git a/src/vm/ini/set_unset_save.c b/src/vm/ini/set_unset_save.c deleted file mode 100644 index 69c1555..0000000 --- a/src/vm/ini/set_unset_save.c +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-11-30 - */ - -/* OP_INI_SET / OP_INI_UNSET / OP_INI_SAVE */ -#ifdef FUN_WITH_INI -case OP_INI_SET: { - Value vval = pop_value(vm); - Value vkey = pop_value(vm); - Value vsec = pop_value(vm); - Value vh = pop_value(vm); - dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); - const char *key = (vkey.type==VAL_STRING)?vkey.s:NULL; - const char *sec = (vsec.type==VAL_STRING)?vsec.s:NULL; - int ok = 0; - if (d && sec && key) { - char *valstr = value_to_string_alloc(&vval); - if (valstr) { - char full[1024]; snprintf(full, sizeof(full), "%s:%s", sec, key); - /* iniparser 4.x does not expose iniparser_set; use dictionary_set */ - if (dictionary_set(d, full, valstr) == 0) ok = 1; /* 0 means success */ - free(valstr); - } - } - free_value(vval); free_value(vkey); free_value(vsec); free_value(vh); - push_value(vm, make_int(ok)); - break; -} - -case OP_INI_UNSET: { - Value vkey = pop_value(vm); - Value vsec = pop_value(vm); - Value vh = pop_value(vm); - dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); - const char *key = (vkey.type==VAL_STRING)?vkey.s:NULL; - const char *sec = (vsec.type==VAL_STRING)?vsec.s:NULL; - int ok = 0; - if (d && sec && key) { - char full[1024]; snprintf(full, sizeof(full), "%s:%s", sec, key); - /* iniparser 4.2.6 dictionary_unset returns void; assume success if inputs are valid */ - dictionary_unset(d, full); - ok = 1; - } - free_value(vkey); free_value(vsec); free_value(vh); - push_value(vm, make_int(ok)); - break; -} - -case OP_INI_SAVE: { - Value vpath = pop_value(vm); - Value vh = pop_value(vm); - const char *path = (vpath.type==VAL_STRING)?vpath.s:NULL; - dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); - int ok = 0; - if (d && path) { - FILE *f = fopen(path, "w"); - if (f) { iniparser_dump_ini(d, f); fclose(f); ok = 1; } - } - free_value(vpath); free_value(vh); - push_value(vm, make_int(ok)); - break; -} -#endif diff --git a/src/vm/ini/unset.c b/src/vm/ini/unset.c new file mode 100644 index 0000000..d264f72 --- /dev/null +++ b/src/vm/ini/unset.c @@ -0,0 +1,32 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 (split from set_unset_save.c) + */ + +/* OP_INI_UNSET */ +#ifdef FUN_WITH_INI +case OP_INI_UNSET: { + Value vkey = pop_value(vm); + Value vsec = pop_value(vm); + Value vh = pop_value(vm); + dictionary *d = ini_get((vh.type==VAL_INT)?(int)vh.i:0); + const char *key = (vkey.type==VAL_STRING)?vkey.s:NULL; + const char *sec = (vsec.type==VAL_STRING)?vsec.s:NULL; + int ok = 0; + if (d && sec && key) { + char full[1024]; snprintf(full, sizeof(full), "%s:%s", sec, key); + /* iniparser 4.2.6 dictionary_unset returns void; assume success if inputs are valid */ + dictionary_unset(d, full); + ok = 1; + } + free_value(vkey); free_value(vsec); free_value(vh); + push_value(vm, make_int(ok)); + break; +} +#endif From 26ddaf161e33370cbb768324e3059358eb35480b Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 23:27:06 +0100 Subject: [PATCH 42/55] Added more exception handling after debugging ./examples/byte_for_demo.fun. (0.37.0) --- CMakeLists.txt | 2 +- examples/byte_for_demo.fun | 14 +++ examples/byte_overflow_try_catch.fun | 41 ++++++++ src/bytecode.h | 7 +- src/parser.c | 148 ++++++++++++++++++++++++--- src/tk_embed.c | 77 -------------- src/vm.c | 135 +++++++++++++++++++++++- src/vm.h | 8 +- src/vm/core/throw.c | 33 ++++++ src/vm/core/try_pop.c | 13 +++ src/vm/core/try_push.c | 18 ++++ src/vm/xml/handles.h | 60 ----------- 12 files changed, 398 insertions(+), 158 deletions(-) create mode 100755 examples/byte_overflow_try_catch.fun create mode 100644 src/vm/core/throw.c create mode 100644 src/vm/core/try_pop.c create mode 100644 src/vm/core/try_push.c delete mode 100644 src/vm/xml/handles.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d6a3582..d5aa6ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.36.3 LANGUAGES C) +project(fun VERSION 0.37.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/byte_for_demo.fun b/examples/byte_for_demo.fun index 90e1980..500b352 100755 --- a/examples/byte_for_demo.fun +++ b/examples/byte_for_demo.fun @@ -1,5 +1,14 @@ #!/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 + */ + // Byte + for-loop demonstration print("=== byte with hex literal and clamping ===") @@ -32,6 +41,11 @@ print(typeof(x)) // -> "String" /* Expected output: === byte with hex literal and clamping === +OverflowError: value out of range for uint8 +*/ + +/* Expected output (OLD): +=== byte with hex literal and clamping === 255 255 255 diff --git a/examples/byte_overflow_try_catch.fun b/examples/byte_overflow_try_catch.fun new file mode 100755 index 0000000..1ac7caf --- /dev/null +++ b/examples/byte_overflow_try_catch.fun @@ -0,0 +1,41 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-10 + */ + +// Demonstrate byte overflow with try/catch +// Note: Runtime exceptions are not yet implemented; overflow emits an error and halts. +// This example shows intended usage once exceptions are supported. + +print("=== byte overflow with try/catch demo ===") +try + byte b = 0 + print("assign 255 -> ok") + b = 255 + print(b) + print("assign 256 -> should overflow and be caught") + b = 256 // will trigger OverflowError: value out of range for uint8 + print("this line will not execute if overflow occurs") +catch err + print("caught error:") + print(err) +finally + print("finally block executed") + +/* Expected output: +=== byte overflow with try/catch demo === +assign 255 -> ok +255 +assign 256 -> should overflow and be caught +caught error: +OverflowError: value out of range for uint8 +finally block executed +*/ diff --git a/src/bytecode.h b/src/bytecode.h index dc9563b..12986e8 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -215,7 +215,12 @@ typedef enum { OP_SOCK_UNIX_CONNECT, // pops path; returns fd (>0) or 0 // process control - OP_EXIT // pops code (or uses operand) and terminates script with exit code + OP_EXIT, // pops code (or uses operand) and terminates script with exit code + + // exceptions (minimal) + OP_TRY_PUSH, // operand = handler ip; push handler onto try-stack + OP_TRY_POP, // pop current handler + OP_THROW // pops error value; if handler -> jump to it (push err), else print and terminate } OpCode; typedef struct { diff --git a/src/parser.c b/src/parser.c index b61d5fd..17f1eca 100644 --- a/src/parser.c +++ b/src/parser.c @@ -2727,7 +2727,7 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si } bytecode_set_operand(bc, j_skip_err, bc->instr_count); } else { - /* integer widths: expect Number then clamp */ + /* integer widths: expect Number then range-check */ int abs_bits = decl_bits < 0 ? -decl_bits : decl_bits; if (abs_bits > 0) { /* typeof == Number */ @@ -2747,7 +2747,53 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si } bytecode_set_operand(bc, j_skip_err, bc->instr_count); - bytecode_add_instruction(bc, (decl_bits < 0) ? OP_SCLAMP : OP_UCLAMP, abs_bits); + /* range check instead of clamp */ + int64_t minV = 0, maxV = 0; + if (decl_bits < 0) { + /* signed */ + if (abs_bits >= 64) { minV = INT64_MIN; maxV = INT64_MAX; } + else { maxV = (1LL << (abs_bits - 1)) - 1; minV = - (1LL << (abs_bits - 1)); } + } else { + /* unsigned */ + if (abs_bits >= 63) { minV = 0; maxV = INT64_MAX; } + else { minV = 0; maxV = (1LL << abs_bits) - 1; } + } + int ciMin = bytecode_add_constant(bc, make_int(minV)); + int ciMax = bytecode_add_constant(bc, make_int(maxV)); + + /* if (v < min) -> error */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMin); + bytecode_add_instruction(bc, OP_LT, 0); + int j_after_min = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + { + const char *tname = (decl_bits < 0) + ? (abs_bits==64? "int64" : (abs_bits==32? "int32" : (abs_bits==16? "int16" : "int8"))) + : (abs_bits==64? "uint64" : (abs_bits==32? "uint32" : (abs_bits==16? "uint16" : "uint8"))); + char buf[128]; + snprintf(buf, sizeof(buf), "OverflowError: value out of range for %s", tname); + int ciMsg = bytecode_add_constant(bc, make_string(buf)); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_THROW, 0); + } + bytecode_set_operand(bc, j_after_min, bc->instr_count); + + /* if (v > max) -> error */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMax); + bytecode_add_instruction(bc, OP_GT, 0); + int j_after_max = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + { + const char *tname = (decl_bits < 0) + ? (abs_bits==64? "int64" : (abs_bits==32? "int32" : (abs_bits==16? "int16" : "int8"))) + : (abs_bits==64? "uint64" : (abs_bits==32? "uint32" : (abs_bits==16? "uint16" : "uint8"))); + char buf[128]; + snprintf(buf, sizeof(buf), "OverflowError: value out of range for %s", tname); + int ciMsg = bytecode_add_constant(bc, make_string(buf)); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_THROW, 0); + } + bytecode_set_operand(bc, j_after_max, bc->instr_count); } } @@ -3071,7 +3117,7 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si } bytecode_set_operand(bc, j_skip_err, bc->instr_count); } else if (meta != 0) { - /* integer widths: expect Number then clamp to declared width */ + /* integer widths: expect Number then range-check to declared width */ int abs_bits = meta < 0 ? -meta : meta; /* typeof == Number */ bytecode_add_instruction(bc, OP_DUP, 0); @@ -3090,7 +3136,49 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si } bytecode_set_operand(bc, j_skip_err, bc->instr_count); - bytecode_add_instruction(bc, (meta < 0) ? OP_SCLAMP : OP_UCLAMP, abs_bits); + /* range check instead of clamp */ + int64_t minV = 0, maxV = 0; + if (meta < 0) { + if (abs_bits >= 64) { minV = INT64_MIN; maxV = INT64_MAX; } + else { maxV = (1LL << (abs_bits - 1)) - 1; minV = - (1LL << (abs_bits - 1)); } + } else { + if (abs_bits >= 63) { minV = 0; maxV = INT64_MAX; } + else { minV = 0; maxV = (1LL << abs_bits) - 1; } + } + int ciMin = bytecode_add_constant(bc, make_int(minV)); + int ciMax = bytecode_add_constant(bc, make_int(maxV)); + + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMin); + bytecode_add_instruction(bc, OP_LT, 0); + int j_after_min = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + { + const char *tname = (meta < 0) + ? (abs_bits==64? "int64" : (abs_bits==32? "int32" : (abs_bits==16? "int16" : "int8"))) + : (abs_bits==64? "uint64" : (abs_bits==32? "uint32" : (abs_bits==16? "uint16" : "uint8"))); + char buf[128]; + snprintf(buf, sizeof(buf), "OverflowError: value out of range for %s", tname); + int ciMsg2 = bytecode_add_constant(bc, make_string(buf)); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg2); + bytecode_add_instruction(bc, OP_THROW, 0); + } + bytecode_set_operand(bc, j_after_min, bc->instr_count); + + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMax); + bytecode_add_instruction(bc, OP_GT, 0); + int j_after_max = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + { + const char *tname = (meta < 0) + ? (abs_bits==64? "int64" : (abs_bits==32? "int32" : (abs_bits==16? "int16" : "int8"))) + : (abs_bits==64? "uint64" : (abs_bits==32? "uint32" : (abs_bits==16? "uint16" : "uint8"))); + char buf[128]; + snprintf(buf, sizeof(buf), "OverflowError: value out of range for %s", tname); + int ciMsg3 = bytecode_add_constant(bc, make_string(buf)); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg3); + bytecode_add_instruction(bc, OP_THROW, 0); + } + bytecode_set_operand(bc, j_after_max, bc->instr_count); } /* dynamic (meta==0): no enforcement */ @@ -4288,13 +4376,16 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, continue; } - /* try/catch/finally (syntax support; runtime exceptions not yet implemented) */ + /* try/catch/finally */ if (starts_with(src, len, *pos, "try")) { /* consume 'try' */ *pos += 3; /* end of header line */ skip_to_eol(src, len, pos); + /* Install a handler placeholder; will be patched to catch label (or a rethrow stub) */ + int try_push_idx = bytecode_add_instruction(bc, OP_TRY_PUSH, 0); + /* parse try body at increased indent (if any) */ int try_body_indent = 0; size_t look_try = *pos; @@ -4304,9 +4395,16 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, /* empty try body allowed */ } + /* After try body, pop handler for normal (non-exceptional) flow */ + bytecode_add_instruction(bc, OP_TRY_POP, 0); + + /* on normal completion, jump over catch body */ + int jmp_over_catch_finally = bytecode_add_instruction(bc, OP_JUMP, 0); + /* Optional: catch and/or finally clauses at same indentation */ int seen_catch = 0; int seen_finally = 0; + int catch_label = -1; for (;;) { size_t look = *pos; int look_indent = 0; @@ -4320,15 +4418,34 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, skip_spaces(src, len, pos); char *ex_name = NULL; size_t tmp = *pos; + int have_name = 0; if (read_identifier_into(src, len, &tmp, &ex_name)) { *pos = tmp; - free(ex_name); + have_name = 1; } /* end of header line */ skip_to_eol(src, len, pos); - /* We currently don't have runtime exceptions: emit an unconditional jump over the catch body (so it's parsed but never executed) */ - int j_over = bytecode_add_instruction(bc, OP_JUMP, 0); + /* Mark catch label and patch try handler target */ + catch_label = bc->instr_count; + bytecode_set_operand(bc, try_push_idx, catch_label); + + /* On entering catch, the thrown error is on stack. Bind to name if provided, else pop. */ + if (have_name) { + int lidx = -1, gi = -1; + if (g_locals) { + int existing = local_find(ex_name); + if (existing >= 0) lidx = existing; else lidx = local_add(ex_name); + } else { + gi = sym_index(ex_name); + } + if (lidx >= 0) bytecode_add_instruction(bc, OP_STORE_LOCAL, lidx); + else if (gi >= 0) bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi); + else bytecode_add_instruction(bc, OP_POP, 0); + } else { + bytecode_add_instruction(bc, OP_POP, 0); + } + if (ex_name) free(ex_name); /* parse catch body at increased indent (if any) */ int catch_indent = 0; @@ -4338,10 +4455,6 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, } else { /* empty catch body allowed */ } - - /* patch jump to here (after catch body) */ - bytecode_set_operand(bc, j_over, bc->instr_count); - seen_catch = 1; continue; } @@ -4368,6 +4481,17 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, /* no recognized clause at this indentation */ break; } + + /* If no catch clause was present, make handler rethrow */ + if (!seen_catch) { + int rethrow_label = bc->instr_count; + bytecode_set_operand(bc, try_push_idx, rethrow_label); + /* at handler: immediately rethrow the incoming error */ + bytecode_add_instruction(bc, OP_THROW, 0); + } + + /* patch normal-flow jump to here (after catch/finally) */ + bytecode_set_operand(bc, jmp_over_catch_finally, bc->instr_count); continue; } diff --git a/src/tk_embed.c b/src/tk_embed.c index d1e6e1f..e69de29 100644 --- a/src/tk_embed.c +++ b/src/tk_embed.c @@ -1,77 +0,0 @@ -/* - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-12-09 - */ - -/** - * Embedded Tcl/Tk helpers for Fun VM. - * When FUN_WITH_TCLTK is OFF, stubs are provided so code compiles and runs. - */ - -#include "value.h" -#include "vm.h" - -#ifdef FUN_WITH_TCLTK -#include -#include -static Tcl_Interp* g_fun_tcl_interp = NULL; - -static void fun_tk_init_once(void) { - if (g_fun_tcl_interp) return; - Tcl_FindExecutable(NULL); - g_fun_tcl_interp = Tcl_CreateInterp(); - if (!g_fun_tcl_interp) return; - if (Tcl_Init(g_fun_tcl_interp) != TCL_OK) { - fprintf(stderr, "Tcl_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); - } - if (Tk_Init(g_fun_tcl_interp) != TCL_OK) { - fprintf(stderr, "Tk_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); - } - /* Ensure the app terminates if the main window is closed via window manager */ - /* Best-effort: set WM_DELETE_WINDOW handler to exit the process. */ - Tcl_Eval(g_fun_tcl_interp, "wm protocol . WM_DELETE_WINDOW {exit 0}"); -} - -static int fun_tk_eval_script(const char *script) { - fun_tk_init_once(); - if (!g_fun_tcl_interp) return -1; - int rc = Tcl_Eval(g_fun_tcl_interp, script ? script : ""); - return rc; /* TCL_OK = 0 */ -} - -static const char* fun_tk_get_result(void) { - fun_tk_init_once(); - if (!g_fun_tcl_interp) return ""; - return Tcl_GetStringResult(g_fun_tcl_interp); -} - -static void fun_tk_loop(void) { - fun_tk_init_once(); - if (!g_fun_tcl_interp) return; - /* Drive Tk event loop until all main windows are closed */ - while (Tk_GetNumMainWindows() > 0) { - while (Tcl_DoOneEvent(0)) {} - /* tiny sleep to avoid busy spin */ -#ifdef _WIN32 - #include - Sleep(1); -#else - #include - struct timespec ts = {0, 1000000}; /* 1 ms */ - nanosleep(&ts, NULL); -#endif - } -} -#else -/* Stubs when Tcl/Tk is disabled */ -static void fun_tk_init_once(void) { (void)0; } -static int fun_tk_eval_script(const char *script) { (void)script; return -1; } -static const char* fun_tk_get_result(void) { return ""; } -static void fun_tk_loop(void) { (void)0; } -#endif diff --git a/src/vm.c b/src/vm.c index 1a13e24..62f4f6f 100644 --- a/src/vm.c +++ b/src/vm.c @@ -22,11 +22,69 @@ #include "string.c" #include "pcsc.c" #include "jsonc.c" -/* Embedded Tcl/Tk helpers (provide stubs when FUN_WITH_TCLTK is off) */ -#include "tk_embed.c" -#ifdef FUN_WITH_XML2 -#include "vm/xml/handles.h" + +#include "value.h" +#include "vm.h" + +#ifdef FUN_WITH_TCLTK +#include +#include +static Tcl_Interp* g_fun_tcl_interp = NULL; + +static void fun_tk_init_once(void) { + if (g_fun_tcl_interp) return; + Tcl_FindExecutable(NULL); + g_fun_tcl_interp = Tcl_CreateInterp(); + if (!g_fun_tcl_interp) return; + if (Tcl_Init(g_fun_tcl_interp) != TCL_OK) { + fprintf(stderr, "Tcl_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); + } + if (Tk_Init(g_fun_tcl_interp) != TCL_OK) { + fprintf(stderr, "Tk_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); + } + /* Ensure the app terminates if the main window is closed via window manager */ + /* Best-effort: set WM_DELETE_WINDOW handler to exit the process. */ + Tcl_Eval(g_fun_tcl_interp, "wm protocol . WM_DELETE_WINDOW {exit 0}"); +} + +static int fun_tk_eval_script(const char *script) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return -1; + int rc = Tcl_Eval(g_fun_tcl_interp, script ? script : ""); + return rc; /* TCL_OK = 0 */ +} + +static const char* fun_tk_get_result(void) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return ""; + return Tcl_GetStringResult(g_fun_tcl_interp); +} + +static void fun_tk_loop(void) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return; + /* Drive Tk event loop until all main windows are closed */ + while (Tk_GetNumMainWindows() > 0) { + while (Tcl_DoOneEvent(0)) {} + /* tiny sleep to avoid busy spin */ +#ifdef _WIN32 + #include + Sleep(1); +#else + #include + struct timespec ts = {0, 1000000}; /* 1 ms */ + nanosleep(&ts, NULL); #endif + } +} +#else +/* Stubs when Tcl/Tk is disabled */ +static void fun_tk_init_once(void) { (void)0; } +static int fun_tk_eval_script(const char *script) { (void)script; return -1; } +static const char* fun_tk_get_result(void) { return ""; } +static void fun_tk_loop(void) { (void)0; } +#endif + #ifdef FUN_WITH_INI #if defined(__has_include) # if __has_include() @@ -44,10 +102,12 @@ #endif #include "vm/ini/handles.h" #endif + #ifdef FUN_WITH_SQLITE #include #include "vm/sqlite/common.c" #endif + #ifdef FUN_WITH_LIBSQL #include /* libsql exposes sqlite3-compatible C API */ #include "vm/libsql/common.c" @@ -90,6 +150,53 @@ static size_t fun_curl_file_write_cb(void *ptr, size_t sz, size_t nm, void *ud) } #endif +#ifdef FUN_WITH_XML2 +#include +#include + +typedef struct { xmlDocPtr doc; int in_use; } XmlDocSlot; +typedef struct { xmlNodePtr node; int in_use; } XmlNodeSlot; + +static XmlDocSlot g_xml_docs[64]; +static XmlNodeSlot g_xml_nodes[256]; + +static int xml_doc_alloc(xmlDocPtr d) { + for (int i = 1; i < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])); ++i) { + if (!g_xml_docs[i].in_use) { g_xml_docs[i].in_use = 1; g_xml_docs[i].doc = d; return i; } + } + return 0; +} +static xmlDocPtr xml_doc_get(int h) { + if (h > 0 && h < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) && g_xml_docs[h].in_use) return g_xml_docs[h].doc; + return NULL; +} +static int xml_doc_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) || !g_xml_docs[h].in_use) return 0; + if (g_xml_docs[h].doc) xmlFreeDoc(g_xml_docs[h].doc); + g_xml_docs[h].doc = NULL; + g_xml_docs[h].in_use = 0; + return 1; +} + +static int xml_node_alloc(xmlNodePtr n) { + for (int i = 1; i < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])); ++i) { + if (!g_xml_nodes[i].in_use) { g_xml_nodes[i].in_use = 1; g_xml_nodes[i].node = n; return i; } + } + return 0; +} +static xmlNodePtr xml_node_get(int h) { + if (h > 0 && h < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) && g_xml_nodes[h].in_use) return g_xml_nodes[h].node; + return NULL; +} +static int xml_node_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) || !g_xml_nodes[h].in_use) return 0; + /* nodes are owned by their document; do not free here */ + g_xml_nodes[h].node = NULL; + g_xml_nodes[h].in_use = 0; + return 1; +} +#endif /* FUN_WITH_XML2 */ + /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); @@ -464,6 +571,7 @@ static void frame_init(Frame *f) { f->fn = NULL; f->ip = 0; for (int i = 0; i < MAX_FRAME_LOCALS; ++i) f->locals[i] = make_nil(); + f->try_sp = -1; } void vm_init(VM *vm) { @@ -662,8 +770,8 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/core/call.c" #include "vm/core/dup.c" - #include "vm/core/halt.c" #include "vm/core/exit.c" + #include "vm/core/halt.c" #include "vm/core/jump.c" #include "vm/core/jump_if_false.c" #include "vm/core/load_const.c" @@ -675,6 +783,9 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/core/store_global.c" #include "vm/core/store_local.c" #include "vm/core/swap.c" + #include "vm/core/throw.c" + #include "vm/core/try_pop.c" + #include "vm/core/try_push.c" #include "vm/io/read_file.c" #include "vm/io/write_file.c" @@ -724,18 +835,22 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/socket_unix_listen.c" #include "vm/os/socket_unix_connect.c" + #ifdef FUN_WITH_PCSC #include "vm/pcsc/establish.c" #include "vm/pcsc/release.c" #include "vm/pcsc/list_readers.c" #include "vm/pcsc/connect.c" #include "vm/pcsc/disconnect.c" #include "vm/pcsc/transmit.c" + #endif /* JSON ops (implemented in jsonc.c, included above) */ + #ifdef FUN_WITH_JSON #include "vm/json/parse.c" #include "vm/json/stringify.c" #include "vm/json/from_file.c" #include "vm/json/to_file.c" + #endif /* XML ops (libxml2) */ #ifdef FUN_WITH_XML2 @@ -759,11 +874,14 @@ void vm_run(VM *vm, Bytecode *entry) { #endif /* CURL ops */ + #ifdef FUN_WITH_CURL #include "vm/curl/get.c" #include "vm/curl/post.c" #include "vm/curl/download.c" + #endif /* Tk (Tcl/Tk) ops */ + #ifdef FUN_WITH_TCLTK #include "vm/tk/eval.c" #include "vm/tk/result.c" #include "vm/tk/loop.c" @@ -771,23 +889,30 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/tk/label.c" #include "vm/tk/button.c" #include "vm/tk/pack.c" + #endif /* SQLite ops */ + #ifdef FUN_WITH_SQLITE #include "vm/sqlite/open.c" #include "vm/sqlite/close.c" #include "vm/sqlite/exec.c" #include "vm/sqlite/query.c" + #endif /* libsql ops (independent) */ + #ifdef FUN_WITH_LIBSQL #include "vm/libsql/open.c" #include "vm/libsql/close.c" #include "vm/libsql/exec.c" #include "vm/libsql/query.c" + #endif /* PCRE2 ops */ + #ifdef FUN_WITH_PCRE2 #include "vm/pcre2/test.c" #include "vm/pcre2/match.c" #include "vm/pcre2/findall.c" + #endif #include "vm/strings/find.c" #include "vm/strings/regex_match.c" diff --git a/src/vm.h b/src/vm.h index 5b603e5..df2c0b7 100644 --- a/src/vm.h +++ b/src/vm.h @@ -47,13 +47,17 @@ static const char *opcode_names[] = { "INI_LOAD","INI_FREE","INI_GET_STRING","INI_GET_INT","INI_GET_DOUBLE","INI_GET_BOOL","INI_SET","INI_UNSET","INI_SAVE", "XML_PARSE","XML_ROOT","XML_NAME","XML_TEXT", "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT", - "EXIT" + "EXIT", + "TRY_PUSH","TRY_POP","THROW" }; typedef struct { Bytecode *fn; int ip; Value locals[MAX_FRAME_LOCALS]; + /* exception handling (per-frame) */ + int try_stack[16]; + int try_sp; /* -1 when empty */ } Frame; struct VM { @@ -122,7 +126,7 @@ void vm_debug_request_finish(VM *vm); void vm_debug_request_continue(VM *vm); static inline int opcode_is_valid(int op) { - return op >= OP_NOP && op <= OP_EXIT; // all current opcodes + return op >= OP_NOP && op <= OP_THROW; // all current opcodes } #endif diff --git a/src/vm/core/throw.c b/src/vm/core/throw.c new file mode 100644 index 0000000..f734bfa --- /dev/null +++ b/src/vm/core/throw.c @@ -0,0 +1,33 @@ +/** + * 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 + */ + +case OP_THROW: { + Value err = pop_value(vm); + /* if there is a handler in this frame, jump to it and push err for catch */ + if (f->try_sp >= 0) { + int try_idx = f->try_stack[f->try_sp--]; + int target = f->fn->instructions[try_idx].operand; + /* push error for catch block */ + push_value(vm, err); /* transfer ownership to stack */ + f->ip = target; + break; + } + /* Unhandled: print error and terminate */ + char *s = value_to_string_alloc(&err); + if (s) { + fprintf(stdout, "%s\n", s); + free(s); + } else { + fprintf(stdout, "\n"); + } + free_value(err); + /* clear frames to stop execution */ + vm->fp = -1; + break; +} diff --git a/src/vm/core/try_pop.c b/src/vm/core/try_pop.c new file mode 100644 index 0000000..d30dfd2 --- /dev/null +++ b/src/vm/core/try_pop.c @@ -0,0 +1,13 @@ +/** + * 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 + */ + +case OP_TRY_POP: { + if (f->try_sp >= 0) f->try_sp--; + break; +} diff --git a/src/vm/core/try_push.c b/src/vm/core/try_push.c new file mode 100644 index 0000000..a224383 --- /dev/null +++ b/src/vm/core/try_push.c @@ -0,0 +1,18 @@ +/** + * 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 + */ + +case OP_TRY_PUSH: { + /* push index of this TRY instruction; handler ip is in its operand (may be patched later) */ + if (f->try_sp >= (int)(sizeof(f->try_stack)/sizeof(f->try_stack[0])) - 1) { + fprintf(stderr, "Runtime error: try depth exceeded\n"); + exit(1); + } + f->try_stack[++f->try_sp] = f->ip - 1; /* index of TRY_PUSH instruction */ + break; +} diff --git a/src/vm/xml/handles.h b/src/vm/xml/handles.h deleted file mode 100644 index ece9548..0000000 --- a/src/vm/xml/handles.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-12-09 - */ - -/** Minimal handle registries for libxml2 documents and nodes */ -#pragma once - -#ifdef FUN_WITH_XML2 -#include -#include - -typedef struct { xmlDocPtr doc; int in_use; } XmlDocSlot; -typedef struct { xmlNodePtr node; int in_use; } XmlNodeSlot; - -static XmlDocSlot g_xml_docs[64]; -static XmlNodeSlot g_xml_nodes[256]; - -static int xml_doc_alloc(xmlDocPtr d) { - for (int i = 1; i < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])); ++i) { - if (!g_xml_docs[i].in_use) { g_xml_docs[i].in_use = 1; g_xml_docs[i].doc = d; return i; } - } - return 0; -} -static xmlDocPtr xml_doc_get(int h) { - if (h > 0 && h < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) && g_xml_docs[h].in_use) return g_xml_docs[h].doc; - return NULL; -} -static int xml_doc_free_handle(int h) { - if (h <= 0 || h >= (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) || !g_xml_docs[h].in_use) return 0; - if (g_xml_docs[h].doc) xmlFreeDoc(g_xml_docs[h].doc); - g_xml_docs[h].doc = NULL; - g_xml_docs[h].in_use = 0; - return 1; -} - -static int xml_node_alloc(xmlNodePtr n) { - for (int i = 1; i < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])); ++i) { - if (!g_xml_nodes[i].in_use) { g_xml_nodes[i].in_use = 1; g_xml_nodes[i].node = n; return i; } - } - return 0; -} -static xmlNodePtr xml_node_get(int h) { - if (h > 0 && h < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) && g_xml_nodes[h].in_use) return g_xml_nodes[h].node; - return NULL; -} -static int xml_node_free_handle(int h) { - if (h <= 0 || h >= (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) || !g_xml_nodes[h].in_use) return 0; - /* nodes are owned by their document; do not free here */ - g_xml_nodes[h].node = NULL; - g_xml_nodes[h].in_use = 0; - return 1; -} -#endif /* FUN_WITH_XML2 */ From b2986075cb0dba5836c7039b2c75678286e3ac80 Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 10 Dec 2025 23:42:00 +0100 Subject: [PATCH 43/55] Some XML examples documentation fixes. (0.37.1) --- CMakeLists.txt | 2 +- examples/xml_access_catalog.fun | 43 ++++++++++++++++ examples/xml_access_employees.fun | 35 +++++++++++++ examples/xml_access_ns.fun | 20 ++++++++ examples/xml_class_example.fun | 81 +++++++++++++++++++++++++++++++ examples/xml_minimal.fun | 5 ++ 6 files changed, 185 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d5aa6ea..cd001ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.0 LANGUAGES C) +project(fun VERSION 0.37.1 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/xml_access_catalog.fun b/examples/xml_access_catalog.fun index 43a33a7..1efb87a 100755 --- a/examples/xml_access_catalog.fun +++ b/examples/xml_access_catalog.fun @@ -52,3 +52,46 @@ else print(currency) print(" ") print(price_text) + +/* Expected output: +Root element: + + + + Wireless Keyboard + Peripherals + 39.99 + + US + Bluetooth + AA + + + + 27" Monitor + Displays + 199.00 + + 2560x1440 + IPS + 75Hz + + + + USB-C Dock + Peripherals + 89.50 + + 2xHDMI, 3xUSB-A, 1xUSB-C PD + 65W + + + + +First product name: +Wireless Keyboard +First price: +USD + +39.99 +*/ diff --git a/examples/xml_access_employees.fun b/examples/xml_access_employees.fun index f762546..5f44e3c 100755 --- a/examples/xml_access_employees.fun +++ b/examples/xml_access_employees.fun @@ -44,3 +44,38 @@ else print(role) print("Email: ") print(email) + +/* Expected output: +Root element: + + + + + Alice Doe + Senior Developer + alice@example.com + + + Bob Roe + DevOps Engineer + bob@example.com + + + + + Carol Smith + Account Executive + carol@example.com + + + + +First employee id: +E-100 +Name: +Alice Doe +Role: +Senior Developer +Email: +alice@example.com +*/ diff --git a/examples/xml_access_ns.fun b/examples/xml_access_ns.fun index 9348c65..a061065 100755 --- a/examples/xml_access_ns.fun +++ b/examples/xml_access_ns.fun @@ -38,3 +38,23 @@ else print(title) print("Author: ") print(author) + +/* Expected output: +Root element: + + + + The Art of Fun + J. Findeisen + + + Minimal VM Design + A. Dev + + + +First book title: +The Art of Fun +Author: +J. Findeisen +*/ diff --git a/examples/xml_class_example.fun b/examples/xml_class_example.fun index 8b2d17b..042bdc1 100755 --- a/examples/xml_class_example.fun +++ b/examples/xml_class_example.fun @@ -27,3 +27,84 @@ else print(xml.name(root)) print("root text:") print(xml.text(root)) + +/* Expected output: +doc handle: + + + + + + Alice + Bob + + + Carol + + + + + Dave + + + + + + + + Welcome to Acme! + + +root name: + + + + + + Alice + Bob + + + Carol + + + + + Dave + + + + + + + + Welcome to Acme! + + +root text: + + + + + + Alice + Bob + + + Carol + + + + + Dave + + + + + + + + Welcome to Acme! + + +*/ diff --git a/examples/xml_minimal.fun b/examples/xml_minimal.fun index 3fb2367..38e7db4 100755 --- a/examples/xml_minimal.fun +++ b/examples/xml_minimal.fun @@ -17,3 +17,8 @@ doc = xml_parse("ab") print("doc handle=\(doc)") root = xml_root(doc) print("root name=\(xml_name(root)) text=\(xml_text(root))") + +/* Expected output: +doc handle=(doc) +root name=(xml_name(root)) text=(xml_text(root)) +*/ From ebae80cf44f543ece1549c03a11d8388a918d23a Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 11 Dec 2025 01:28:38 +0100 Subject: [PATCH 44/55] Some housekeeping. (0.37.2) --- CMakeLists.txt | 2 +- src/jsonc.c | 111 --------------------------------- src/pcsc.c | 74 ---------------------- src/tk_embed.c | 0 src/vm.c | 163 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 188 deletions(-) delete mode 100644 src/jsonc.c delete mode 100644 src/pcsc.c delete mode 100644 src/tk_embed.c diff --git a/CMakeLists.txt b/CMakeLists.txt index cd001ab..ad5119f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.1 LANGUAGES C) +project(fun VERSION 0.37.2 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/src/jsonc.c b/src/jsonc.c deleted file mode 100644 index 5cb24c1..0000000 --- a/src/jsonc.c +++ /dev/null @@ -1,111 +0,0 @@ -/** - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-11-24 - */ - -/* json-c helpers and VM opcode cases (included from vm.c) */ - -#include "value.h" -#include "vm.h" - -#ifdef FUN_WITH_JSON - #include - #include -#endif - -/* --- Conversion helpers between json-c and Fun Value --- */ -#ifdef FUN_WITH_JSON -static Value json_to_fun(json_object *j) { - if (!j) return make_nil(); - enum json_type t = json_object_get_type(j); - switch (t) { - case json_type_null: return make_nil(); - case json_type_boolean: return make_bool(json_object_get_boolean(j)); - case json_type_double: return make_float(json_object_get_double(j)); - case json_type_int: return make_int((int64_t)json_object_get_int64(j)); - case json_type_string: return make_string(json_object_get_string(j)); - case json_type_array: { - size_t n = json_object_array_length(j); - if (n == 0) { - return make_array_from_values(NULL, 0); - } - Value *vals = (Value*)malloc(sizeof(Value) * n); - if (!vals) return make_array_from_values(NULL, 0); - for (size_t i = 0; i < n; ++i) { - json_object *item = json_object_array_get_idx(j, (int)i); - vals[i] = json_to_fun(item); - } - Value arr = make_array_from_values(vals, (int)n); - for (size_t i = 0; i < n; ++i) free_value(vals[i]); - free(vals); - return arr; - } - case json_type_object: { - Value map = make_map_empty(); - json_object_object_foreach(j, key, val) { - (void)map_set(&map, key, json_to_fun(val)); - } - return map; - } - default: - return make_nil(); - } -} - -static json_object* fun_to_json(const Value *v) { - switch (v->type) { - case VAL_NIL: return json_object_new_null(); - case VAL_BOOL: return json_object_new_boolean(v->i ? 1 : 0); - case VAL_INT: return json_object_new_int64(v->i); - case VAL_FLOAT: return json_object_new_double(v->d); - case VAL_STRING: return json_object_new_string(v->s ? v->s : ""); - case VAL_ARRAY: { - json_object *arr = json_object_new_array(); - int n = array_length(v); - for (int i = 0; i < n; ++i) { - Value item; - if (array_get_copy(v, i, &item)) { - json_object_array_add(arr, fun_to_json(&item)); - free_value(item); - } else { - json_object_array_add(arr, json_object_new_null()); - } - } - return arr; - } - case VAL_MAP: { - json_object *obj = json_object_new_object(); - /* We don't have an iterator API; use keys() helper */ - Value keys = map_keys_array(v); - int kn = array_length(&keys); - for (int i = 0; i < kn; ++i) { - Value k; - if (!array_get_copy(&keys, i, &k)) continue; - if (k.type == VAL_STRING && k.s) { - Value val; - if (map_get_copy(v, k.s, &val)) { - json_object_object_add(obj, k.s, fun_to_json(&val)); - free_value(val); - } else { - json_object_object_add(obj, k.s, json_object_new_null()); - } - } - free_value(k); - } - free_value(keys); - return obj; - } - default: - /* Fallback: stringify unsupported types */ - return json_object_new_string(""); - } -} -#endif /* FUN_WITH_JSON */ - -/* Note: The VM opcode case handlers are included from vm/vm switch via vm/json/ops.c */ diff --git a/src/pcsc.c b/src/pcsc.c deleted file mode 100644 index 1f72f45..0000000 --- a/src/pcsc.c +++ /dev/null @@ -1,74 +0,0 @@ -/** - * This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-10-02 - */ - - /* PCSC helpers: registries and helper functions. - * Included at file scope from vm.c. - */ -#ifdef FUN_WITH_PCSC - #if defined(__has_include) - #if __has_include() - #include - #include - #elif __has_include() - #include - #else - #error "FUN_WITH_PCSC is enabled but PCSC headers were not found" - #endif - #else - #include - #include - #endif - #include - - typedef struct { - SCARDCONTEXT ctx; - int in_use; - } pcsc_ctx_entry; - - typedef struct { - SCARDHANDLE h; - DWORD proto; - int in_use; - } pcsc_card_entry; - - static pcsc_ctx_entry g_pcsc_ctx[8]; - static pcsc_card_entry g_pcsc_card[32]; - - static int pcsc_alloc_ctx_slot(void) { - for (int i = 0; i < (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0])); ++i) { - if (!g_pcsc_ctx[i].in_use) { g_pcsc_ctx[i].in_use = 1; g_pcsc_ctx[i].ctx = 0; return i + 1; } - } - return 0; - } - - static int pcsc_alloc_card_slot(void) { - for (int i = 0; i < (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0])); ++i) { - if (!g_pcsc_card[i].in_use) { g_pcsc_card[i].in_use = 1; g_pcsc_card[i].h = 0; g_pcsc_card[i].proto = 0; return i + 1; } - } - return 0; - } - - static pcsc_ctx_entry* pcsc_get_ctx(int id) { - if (id <= 0) return NULL; - int idx = id - 1; - if (idx < 0 || idx >= (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0]))) return NULL; - if (!g_pcsc_ctx[idx].in_use) return NULL; - return &g_pcsc_ctx[idx]; - } - - static pcsc_card_entry* pcsc_get_card(int id) { - if (id <= 0) return NULL; - int idx = id - 1; - if (idx < 0 || idx >= (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0]))) return NULL; - if (!g_pcsc_card[idx].in_use) return NULL; - return &g_pcsc_card[idx]; - } -#endif /* FUN_WITH_PCSC */ diff --git a/src/tk_embed.c b/src/tk_embed.c deleted file mode 100644 index e69de29..0000000 diff --git a/src/vm.c b/src/vm.c index 62f4f6f..7ecc8fb 100644 --- a/src/vm.c +++ b/src/vm.c @@ -20,12 +20,171 @@ #include "iter.c" #include "map.c" #include "string.c" -#include "pcsc.c" -#include "jsonc.c" +#include "value.h" +#include "vm.h" + +#ifdef FUN_WITH_PCSC + /* PCSC helpers: registries and helper functions. + * Included at file scope from vm.c. */ + #if defined(__has_include) + #if __has_include() + #include + #include + #elif __has_include() + #include + #else + #error "FUN_WITH_PCSC is enabled but PCSC headers were not found" + #endif + #else + #include + #include + #endif + #include + + typedef struct { + SCARDCONTEXT ctx; + int in_use; + } pcsc_ctx_entry; + + typedef struct { + SCARDHANDLE h; + DWORD proto; + int in_use; + } pcsc_card_entry; + + static pcsc_ctx_entry g_pcsc_ctx[8]; + static pcsc_card_entry g_pcsc_card[32]; + + static int pcsc_alloc_ctx_slot(void) { + for (int i = 0; i < (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0])); ++i) { + if (!g_pcsc_ctx[i].in_use) { g_pcsc_ctx[i].in_use = 1; g_pcsc_ctx[i].ctx = 0; return i + 1; } + } + return 0; + } + + static int pcsc_alloc_card_slot(void) { + for (int i = 0; i < (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0])); ++i) { + if (!g_pcsc_card[i].in_use) { g_pcsc_card[i].in_use = 1; g_pcsc_card[i].h = 0; g_pcsc_card[i].proto = 0; return i + 1; } + } + return 0; + } + + static pcsc_ctx_entry* pcsc_get_ctx(int id) { + if (id <= 0) return NULL; + int idx = id - 1; + if (idx < 0 || idx >= (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0]))) return NULL; + if (!g_pcsc_ctx[idx].in_use) return NULL; + return &g_pcsc_ctx[idx]; + } + + static pcsc_card_entry* pcsc_get_card(int id) { + if (id <= 0) return NULL; + int idx = id - 1; + if (idx < 0 || idx >= (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0]))) return NULL; + if (!g_pcsc_card[idx].in_use) return NULL; + return &g_pcsc_card[idx]; + } +#endif + +#ifdef FUN_WITH_JSON +/* json-c helpers and VM opcode cases (included from vm.c) */ #include "value.h" #include "vm.h" +#include +#include + +/* --- Conversion helpers between json-c and Fun Value --- */ +static Value json_to_fun(json_object *j) { + if (!j) return make_nil(); + enum json_type t = json_object_get_type(j); + switch (t) { + case json_type_null: return make_nil(); + case json_type_boolean: return make_bool(json_object_get_boolean(j)); + case json_type_double: return make_float(json_object_get_double(j)); + case json_type_int: return make_int((int64_t)json_object_get_int64(j)); + case json_type_string: return make_string(json_object_get_string(j)); + case json_type_array: { + size_t n = json_object_array_length(j); + if (n == 0) { + return make_array_from_values(NULL, 0); + } + Value *vals = (Value*)malloc(sizeof(Value) * n); + if (!vals) return make_array_from_values(NULL, 0); + for (size_t i = 0; i < n; ++i) { + json_object *item = json_object_array_get_idx(j, (int)i); + vals[i] = json_to_fun(item); + } + Value arr = make_array_from_values(vals, (int)n); + for (size_t i = 0; i < n; ++i) free_value(vals[i]); + free(vals); + return arr; + } + case json_type_object: { + Value map = make_map_empty(); + json_object_object_foreach(j, key, val) { + (void)map_set(&map, key, json_to_fun(val)); + } + return map; + } + default: + return make_nil(); + } +} + +static json_object* fun_to_json(const Value *v) { + switch (v->type) { + case VAL_NIL: return json_object_new_null(); + case VAL_BOOL: return json_object_new_boolean(v->i ? 1 : 0); + case VAL_INT: return json_object_new_int64(v->i); + case VAL_FLOAT: return json_object_new_double(v->d); + case VAL_STRING: return json_object_new_string(v->s ? v->s : ""); + case VAL_ARRAY: { + json_object *arr = json_object_new_array(); + int n = array_length(v); + for (int i = 0; i < n; ++i) { + Value item; + if (array_get_copy(v, i, &item)) { + json_object_array_add(arr, fun_to_json(&item)); + free_value(item); + } else { + json_object_array_add(arr, json_object_new_null()); + } + } + return arr; + } + case VAL_MAP: { + json_object *obj = json_object_new_object(); + /* We don't have an iterator API; use keys() helper */ + Value keys = map_keys_array(v); + int kn = array_length(&keys); + for (int i = 0; i < kn; ++i) { + Value k; + if (!array_get_copy(&keys, i, &k)) continue; + if (k.type == VAL_STRING && k.s) { + Value val; + if (map_get_copy(v, k.s, &val)) { + json_object_object_add(obj, k.s, fun_to_json(&val)); + free_value(val); + } else { + json_object_object_add(obj, k.s, json_object_new_null()); + } + } + free_value(k); + } + free_value(keys); + return obj; + } + default: + /* Fallback: stringify unsupported types */ + return json_object_new_string(""); + } +} + +/* Note: The VM opcode case handlers are included from vm/vm switch via vm/json/ops.c */ +#endif + #ifdef FUN_WITH_TCLTK #include #include From 2927f883403c8bfb230e5db7c93de9e3144a8661 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 11 Dec 2025 03:14:20 +0100 Subject: [PATCH 45/55] Some more housekeeping. Destroys nothing, but makes some things easier. (0.37.3) --- CMakeLists.txt | 2 +- demo.fun | 325 ++++++++---------- examples/{ => error}/debug_reporting.fun | 0 examples/{ => error}/exit_example.fun | 0 examples/{ => error}/fail.fun | 0 examples/{ => error}/repl_on_error.fun | 0 examples/{ => error}/try_catch_with_error.fun | 0 scripts/run_examples.sh | 16 + 8 files changed, 156 insertions(+), 187 deletions(-) rename examples/{ => error}/debug_reporting.fun (100%) rename examples/{ => error}/exit_example.fun (100%) rename examples/{ => error}/fail.fun (100%) rename examples/{ => error}/repl_on_error.fun (100%) rename examples/{ => error}/try_catch_with_error.fun (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad5119f..5cd1750 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.2 LANGUAGES C) +project(fun VERSION 0.37.3 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/demo.fun b/demo.fun index e7a9324..c8153e3 100755 --- a/demo.fun +++ b/demo.fun @@ -1,200 +1,153 @@ #!/usr/bin/env fun /* -* This file is part of the Fun programming language. - * https://fun-lang.xyz/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-09-30 + * Interactive demo runner for all examples in ./examples + * - Asks y/n before running each feature demo + * - Executes each example as a subprocess */ -// Fun Interactive Demo -// Run: FUN_LIB_DIR="$(pwd)/lib" ./build/fun demo.fun (Linux/macOS/FreeBSD) -// set FUN_LIB_DIR=%CD%\lib && build-debug\fun.exe demo.fun (Windows CMD) -// $env:FUN_LIB_DIR="$PWD\lib"; .\build\fun.exe demo.fun (Windows PowerShell) +#include -// Use a stdlib helper from the repository (fallback to ./lib via preprocessor) -#include +fun pick_fun_bin() + // Prefer an explicit FUN_BIN override; otherwise rely on PATH + b = env("FUN_BIN") + if b != "" + return b + return "fun" -print("") -print("=== Fun Interactive Demo ===") +fun run_example(bin, path) + // Ensure examples can locate stdlib when run from repo root. + // We execute via the shell so env assignment + redirection works. + cmd = join(["sh -c '\nFUN_LIB_DIR=./lib ", bin, " ", path, " 2>&1\n'"], "") + print("-- output begin --") + code = system(cmd) + print("-- output end --") + print(join(["exit code: ", to_string(code)], "")) + return code -print("") -print("== Basics: dynamic vs typed variables and typeof ==") -x = 123 -print("x=" + to_string(x) + " typeof=" + typeof(x)) -x = "hello" -print("x=" + to_string(x) + " typeof=" + typeof(x)) +fun main() + c = Console() + bin = pick_fun_bin() -number n = 42 -print("n=" + to_string(n) + " typeof=" + typeof(n)) -n = n + 8 -print("n=" + to_string(n) + " typeof=" + typeof(n)) -// Uncomment to see a runtime type error and halt the program: -// n = "oops" + print("=== Fun language feature showcase (interactive) ===") + print(join(["Using interpreter: ", bin], "")) + print("Tip: set FUN_BIN=/path/to/fun to override. Stdlib is passed via FUN_LIB_DIR=./lib\n") -boolean flag = 0 -print("flag=" + to_string(flag) + " typeof=" + typeof(flag)) -flag = 2 // gets clamped to 1 -print("flag(after clamp 2)=" + to_string(flag)) + // List of example scripts. Keep paths relative to repo root where this demo resides. + // If you add/remove examples, update this list. + files = [ + "examples/arrays.fun", + "examples/arrays_advanced.fun", + "examples/arrays_iter.fun", + "examples/boolean_decl.fun", + "examples/booleans.fun", + "examples/builtins_conversions.fun", + "examples/builtins_extended.fun", + "examples/builtins_maps_and_more.fun", + "examples/byte_for_demo.fun", + "examples/byte_overflow_try_catch.fun", + "examples/cast_demo.fun", + "examples/class_constructor.fun", + "examples/classes_demo.fun", + "examples/crc32_example.fun", + "examples/crc32c_example.fun", + "examples/curl_download.fun", + "examples/curl_get_json.fun", + "examples/curl_post.fun", + "examples/datetime_basic.fun", + "examples/datetime_extended.fun", + "examples/datetime_timer.fun", + "examples/debug_reporting.fun", + "examples/echo_example.fun", + "examples/exit_example.fun", + "examples/expressions_test.fun", + "examples/fail.fun", + "examples/file_io.fun", + "examples/file_print_for_file_line_by_line.fun", + "examples/floats.fun", + "examples/for_range_test.fun", + "examples/functions_test.fun", + "examples/have_fun.fun", + "examples/have_fun_function.fun", + "examples/if_else_test.fun", + "examples/include_lib.fun", + "examples/include_local.fun", + "examples/include_local_util.fun", + "examples/include_namespace.fun", + "examples/inheritance_demo.fun", + "examples/ini_class_demo.fun", + "examples/ini_complex.fun", + "examples/ini_demo.fun", + "examples/ini_subsections.fun", + "examples/input_example.fun", + "examples/json_showcase.fun", + "examples/libsql_example.fun", + "examples/loops_break_continue.fun", + "examples/md5_demo.fun", + "examples/namespaced_mod.fun", + "examples/nested_loops.fun", + "examples/objects_basic.fun", + "examples/objects_more.fun", + "examples/os_env.fun", + "examples/pcre2_opcodes.fun", + "examples/pcre2_showcase.fun", + "examples/pcsc_example.fun", + "examples/process_example.fun", + "examples/regex_demo.fun", + "examples/regex_procedural.fun", + "examples/repl_on_error.fun", + "examples/sha1_demo.fun", + "examples/sha256_demo.fun", + "examples/sha256_str_demo.fun", + "examples/sha384_example.fun", + "examples/sha512_demo.fun", + "examples/sha512_str_demo.fun", + "examples/short_circuit_test.fun", + "examples/signed_ints.fun", + "examples/sqlite_example.fun", + "examples/stdlib_showcase.fun", + "examples/strings_test.fun", + "examples/tcp_http_get.fun", + "examples/tcp_http_get_class.fun", + "examples/thread_class_example.fun", + "examples/threads_demo.fun", + "examples/tk_hello.fun", + "examples/try_catch_finally.fun", + "examples/try_catch_with_error.fun", + "examples/typeof.fun", + "examples/typeof_features.fun", + "examples/type_safety.fun", + "examples/type_safety_fails.fun", + "examples/types_integers.fun", + "examples/types_overview.fun", + "examples/uint_types.fun", + "examples/unix_socket_echo.fun", + "examples/while_test.fun", + "examples/xml_access_catalog.fun", + "examples/xml_access_employees.fun", + "examples/xml_access_ns.fun", + "examples/xml_class_example.fun", + "examples/xml_minimal.fun" + ] -int8 si = 130 // clamps to 127 -uint8 u8 = 300 // clamps to 255 -print("int8 si=" + to_string(si) + ", uint8 u8=" + to_string(u8)) + failures = [] -nil nothing = nil -print("nothing typeof=" + typeof(nothing)) + for f in files + q = join(["Run ", f, "?"], "") + if c.ask_yes_no(q) + print(join(["=== Running: ", f, " ==="], "")) + code = run_example(bin, f) + if code != 0 + failures.push(f) + print("") + else + print(join(["Skipped: ", f], "")) -print("") -print("== Arithmetic, comparisons, and logic ==") -print("3+4=" + to_string(3 + 4)) -print("10-3=" + to_string(10 - 3)) -print("6*7=" + to_string(6 * 7)) -print("20/3=" + to_string(20 / 3)) -print("20%3=" + to_string(20 % 3)) -print("2<3=" + to_string(2 < 3) + ", 3<=3=" + to_string(3 <= 3)) -print("5>2=" + to_string(5 > 2) + ", 2>=2=" + to_string(2 >= 2)) -print("2==2=" + to_string(2 == 2) + ", 2!=3=" + to_string(2 != 3)) -print("(1 && 0)=" + to_string(1 && 0) + ", (0 || 1)=" + to_string(0 || 1) + ", !0=" + to_string(!0)) + if len(failures) == 0 + print("All selected examples completed successfully.") + else + print("Some selected examples failed:") + for ff in failures + print(join([" - ", ff], "")) -print("") -print("== Strings and arrays ==") -s = "alpha,beta,gamma" -parts = split(s, ",") -print("split -> len=" + to_string(len(parts))) -print("join(parts,'|')=" + join(parts, "|")) -print("substr('abcdef', 2, 3)=" + substr("abcdef", 2, 3)) -print("find('hello world','world')=" + to_string(find("hello world", "world"))) - -arr = [1, 2, 3] -print("arr len=" + to_string(len(arr))) -push(arr, 4) -print("after push 4 -> len=" + to_string(len(arr))) -v = pop(arr) -print("popped=" + to_string(v) + " len=" + to_string(len(arr))) -insert(arr, 1, 99) // [1,99,2,3] -print("after insert(1,99) arr[1]=" + to_string(arr[1])) -set(arr, 2, 55) // [1,99,55,3] -print("after set(2,55) arr[2]=" + to_string(arr[2])) -print("contains(arr, 55)=" + to_string(contains(arr, 55)) + ", indexOf(arr, 99)=" + to_string(indexOf(arr, 99))) -print("slice arr[1:3] len=" + to_string(len(arr[1:3]))) -print("join(arr, ',')=" + join(arr, ",")) - -print("") -print("== Enumerate and zip ==") -for p in enumerate(["a", "b", "c"]) - print("idx=" + to_string(p[0]) + " val=" + to_string(p[1])) -z = zip([1, 2], ["x", "y"]) -for pair in z - print("(" + to_string(pair[0]) + "," + to_string(pair[1]) + ")") - -print("") -print("== Maps (dictionaries) ==") -m = {"name": "Alice", "age": 30} -print("has(m,'age')=" + to_string(has(m, "age"))) -print("m['name']=" + to_string(m["name"])) -m.age = 31 -print("m.age after = " + to_string(m.age)) -print("keys: " + join(keys(m), ",")) -print("values count=" + to_string(len(values(m)))) - -print("") -print("== Functions and higher-order ops (map/filter/reduce) ==") -fun greet(name) - print("Hello, " + to_string(name) + "!") -greet("Fun") - -fun double(x) - return x * 2 - -nums = [1, 2, 3, 4, 5] -twice = map(nums, double) -print("len(map)=" + to_string(len(twice)) + " first=" + to_string(twice[0])) - -fun isEven(x) - return (x % 2) == 0 -evens = filter(nums, isEven) -print("filter evens len=" + to_string(len(evens))) - -fun sum(acc, x) - return acc + x -total = reduce(nums, 0, sum) -print("reduce sum=" + to_string(total)) - -print("") -print("== If / else-if / else and loops ==") -val = 7 -if (val < 0) - print("neg") -else if (val == 0) - print("zero") -else - print("pos") - -print("for range(0, 5):") -for i in range(0, 5) - print(i) - -print("for in array:") -for x in ["h", "i", "!"] - print(x) - -print("while loop (count to 3):") -c = 0 -while (c < 3) - print(c) - c = c + 1 - -print("") -print("== Classes (with 'this' and methods) ==") -class Person(string name, number age) - // default field values (can be overridden by constructor params) - full = name + " (" + to_string(age) + ")" - - // required: first param is 'this' - fun say(this) - print("I am " + to_string(this.full)) - - // typeof(instance) will return this string for Maps tagged with __class - fun toString(this) - return "Person" - -p = Person("Alice", 30) -p.say() -print("typeof p: " + typeof(p)) - -print("") -print("== Math and bitwise helpers ==") -print("min(3,9)=" + to_string(min(3, 9)) + ", max(3,9)=" + to_string(max(3, 9))) -print("clamp(15,0,10)=" + to_string(clamp(15, 0, 10)) + ", abs(-5)=" + to_string(abs(-5))) -print("pow(2,10)=" + to_string(pow(2, 10))) -print("band(0xF0,0x3C)=" + to_string(band(0xF0, 0x3C))) -print("bor(0x0F,0x30)=" + to_string(bor(0x0F, 0x30))) -print("bxor(0xFF,0x0F)=" + to_string(bxor(0xFF, 0x0F))) -print("bnot(0x0F)=" + to_string(bnot(0x0F))) -print("shl(1,4)=" + to_string(shl(1, 4)) + ", shr(128,3)=" + to_string(shr(128, 3))) -print("rol(0x12,1)=" + to_string(rol(0x12, 1)) + ", ror(0x12,1)=" + to_string(ror(0x12, 1))) - -print("") -print("== Random numbers ==") -random(12345) // seed -print("randomInt(1,10) -> " + to_string(randomInt(1, 10))) - -print("") -print("== File IO and environment ==") -write_ok = write_file("demo_tmp.txt", "Hello from Fun!\n") -print("write_file ok=" + to_string(write_ok)) -content = read_file("demo_tmp.txt") -print("read_file len=" + to_string(len(content))) -print("PATH starts with: " + substr(env("PATH"), 0, 24)) - -print("") -print("== Library include demo ==") -print("add(2,3) from utils/math.fun -> " + to_string(add(2, 3))) -print("times(4,5) from utils/math.fun -> " + to_string(times(4, 5))) - -print("") -print("=== Demo complete. Have Fun! ===") +main() diff --git a/examples/debug_reporting.fun b/examples/error/debug_reporting.fun similarity index 100% rename from examples/debug_reporting.fun rename to examples/error/debug_reporting.fun diff --git a/examples/exit_example.fun b/examples/error/exit_example.fun similarity index 100% rename from examples/exit_example.fun rename to examples/error/exit_example.fun diff --git a/examples/fail.fun b/examples/error/fail.fun similarity index 100% rename from examples/fail.fun rename to examples/error/fail.fun diff --git a/examples/repl_on_error.fun b/examples/error/repl_on_error.fun similarity index 100% rename from examples/repl_on_error.fun rename to examples/error/repl_on_error.fun diff --git a/examples/try_catch_with_error.fun b/examples/error/try_catch_with_error.fun similarity index 100% rename from examples/try_catch_with_error.fun rename to examples/error/try_catch_with_error.fun diff --git a/scripts/run_examples.sh b/scripts/run_examples.sh index 9d814cd..1f7f5cd 100755 --- a/scripts/run_examples.sh +++ b/scripts/run_examples.sh @@ -50,6 +50,14 @@ if [[ ! -x "$BIN" ]]; then exit 2 fi +# Ensure stdlib is discoverable for examples unless user already set it +if [[ -z "${FUN_LIB_DIR:-}" ]]; then + export FUN_LIB_DIR="$ROOT/lib" +fi + +# Ensure error bucket exists +mkdir -p "$EX_DIR/error" + shopt -s nullglob files=("$EX_DIR"/*.fun) shopt -u nullglob @@ -64,6 +72,14 @@ for f in "${files[@]}"; do echo "=== Running: ${f#$ROOT/} ===" if ! "$BIN" "$f"; then echo "FAILED: ${f#$ROOT/}" + base="$(basename "$f")" + dest="$EX_DIR/error/$base" + # Move the failing example to the error folder + if mv -f "$f" "$dest"; then + echo "Moved to: examples/error/$base" + else + echo "warning: failed to move $base to examples/error/" >&2 + fi rc=1 fi done From 8b9b9249fdfd32463af07622dd501c9aef9d8cc8 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 11 Dec 2025 03:31:27 +0100 Subject: [PATCH 46/55] Just more Fun. (0.37.4) --- CMakeLists.txt | 2 +- demo.fun | 153 ------------------------------------- play.fun | 199 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 140 insertions(+), 214 deletions(-) delete mode 100755 demo.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cd1750..4bc9feb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.3 LANGUAGES C) +project(fun VERSION 0.37.4 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/demo.fun b/demo.fun deleted file mode 100755 index c8153e3..0000000 --- a/demo.fun +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env fun - -/* - * Interactive demo runner for all examples in ./examples - * - Asks y/n before running each feature demo - * - Executes each example as a subprocess - */ - -#include - -fun pick_fun_bin() - // Prefer an explicit FUN_BIN override; otherwise rely on PATH - b = env("FUN_BIN") - if b != "" - return b - return "fun" - -fun run_example(bin, path) - // Ensure examples can locate stdlib when run from repo root. - // We execute via the shell so env assignment + redirection works. - cmd = join(["sh -c '\nFUN_LIB_DIR=./lib ", bin, " ", path, " 2>&1\n'"], "") - print("-- output begin --") - code = system(cmd) - print("-- output end --") - print(join(["exit code: ", to_string(code)], "")) - return code - -fun main() - c = Console() - bin = pick_fun_bin() - - print("=== Fun language feature showcase (interactive) ===") - print(join(["Using interpreter: ", bin], "")) - print("Tip: set FUN_BIN=/path/to/fun to override. Stdlib is passed via FUN_LIB_DIR=./lib\n") - - // List of example scripts. Keep paths relative to repo root where this demo resides. - // If you add/remove examples, update this list. - files = [ - "examples/arrays.fun", - "examples/arrays_advanced.fun", - "examples/arrays_iter.fun", - "examples/boolean_decl.fun", - "examples/booleans.fun", - "examples/builtins_conversions.fun", - "examples/builtins_extended.fun", - "examples/builtins_maps_and_more.fun", - "examples/byte_for_demo.fun", - "examples/byte_overflow_try_catch.fun", - "examples/cast_demo.fun", - "examples/class_constructor.fun", - "examples/classes_demo.fun", - "examples/crc32_example.fun", - "examples/crc32c_example.fun", - "examples/curl_download.fun", - "examples/curl_get_json.fun", - "examples/curl_post.fun", - "examples/datetime_basic.fun", - "examples/datetime_extended.fun", - "examples/datetime_timer.fun", - "examples/debug_reporting.fun", - "examples/echo_example.fun", - "examples/exit_example.fun", - "examples/expressions_test.fun", - "examples/fail.fun", - "examples/file_io.fun", - "examples/file_print_for_file_line_by_line.fun", - "examples/floats.fun", - "examples/for_range_test.fun", - "examples/functions_test.fun", - "examples/have_fun.fun", - "examples/have_fun_function.fun", - "examples/if_else_test.fun", - "examples/include_lib.fun", - "examples/include_local.fun", - "examples/include_local_util.fun", - "examples/include_namespace.fun", - "examples/inheritance_demo.fun", - "examples/ini_class_demo.fun", - "examples/ini_complex.fun", - "examples/ini_demo.fun", - "examples/ini_subsections.fun", - "examples/input_example.fun", - "examples/json_showcase.fun", - "examples/libsql_example.fun", - "examples/loops_break_continue.fun", - "examples/md5_demo.fun", - "examples/namespaced_mod.fun", - "examples/nested_loops.fun", - "examples/objects_basic.fun", - "examples/objects_more.fun", - "examples/os_env.fun", - "examples/pcre2_opcodes.fun", - "examples/pcre2_showcase.fun", - "examples/pcsc_example.fun", - "examples/process_example.fun", - "examples/regex_demo.fun", - "examples/regex_procedural.fun", - "examples/repl_on_error.fun", - "examples/sha1_demo.fun", - "examples/sha256_demo.fun", - "examples/sha256_str_demo.fun", - "examples/sha384_example.fun", - "examples/sha512_demo.fun", - "examples/sha512_str_demo.fun", - "examples/short_circuit_test.fun", - "examples/signed_ints.fun", - "examples/sqlite_example.fun", - "examples/stdlib_showcase.fun", - "examples/strings_test.fun", - "examples/tcp_http_get.fun", - "examples/tcp_http_get_class.fun", - "examples/thread_class_example.fun", - "examples/threads_demo.fun", - "examples/tk_hello.fun", - "examples/try_catch_finally.fun", - "examples/try_catch_with_error.fun", - "examples/typeof.fun", - "examples/typeof_features.fun", - "examples/type_safety.fun", - "examples/type_safety_fails.fun", - "examples/types_integers.fun", - "examples/types_overview.fun", - "examples/uint_types.fun", - "examples/unix_socket_echo.fun", - "examples/while_test.fun", - "examples/xml_access_catalog.fun", - "examples/xml_access_employees.fun", - "examples/xml_access_ns.fun", - "examples/xml_class_example.fun", - "examples/xml_minimal.fun" - ] - - failures = [] - - for f in files - q = join(["Run ", f, "?"], "") - if c.ask_yes_no(q) - print(join(["=== Running: ", f, " ==="], "")) - code = run_example(bin, f) - if code != 0 - failures.push(f) - print("") - else - print(join(["Skipped: ", f], "")) - - if len(failures) == 0 - print("All selected examples completed successfully.") - else - print("Some selected examples failed:") - for ff in failures - print(join([" - ", ff], "")) - -main() diff --git a/play.fun b/play.fun index 7070ba3..c8153e3 100755 --- a/play.fun +++ b/play.fun @@ -1,74 +1,153 @@ #!/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 + * Interactive demo runner for all examples in ./examples + * - Asks y/n before running each feature demo + * - Executes each example as a subprocess */ -include +#include -fun foo() - print("Have fun!") - print("Having fun... forever.") +fun pick_fun_bin() + // Prefer an explicit FUN_BIN override; otherwise rely on PATH + b = env("FUN_BIN") + if b != "" + return b + return "fun" -print("Typeof foo(): " + typeof(foo)) +fun run_example(bin, path) + // Ensure examples can locate stdlib when run from repo root. + // We execute via the shell so env assignment + redirection works. + cmd = join(["sh -c '\nFUN_LIB_DIR=./lib ", bin, " ", path, " 2>&1\n'"], "") + print("-- output begin --") + code = system(cmd) + print("-- output end --") + print(join(["exit code: ", to_string(code)], "")) + return code -print("Yay, the playground for having fun... ;)") +fun main() + c = Console() + bin = pick_fun_bin() -print(string_to_bytes_ascii("Have Fun!")) + print("=== Fun language feature showcase (interactive) ===") + print(join(["Using interpreter: ", bin], "")) + print("Tip: set FUN_BIN=/path/to/fun to override. Stdlib is passed via FUN_LIB_DIR=./lib\n") -number n = 23 -// Every type MUST be lowercase. Sint* must be sint*. -print(n) + // List of example scripts. Keep paths relative to repo root where this demo resides. + // If you add/remove examples, update this list. + files = [ + "examples/arrays.fun", + "examples/arrays_advanced.fun", + "examples/arrays_iter.fun", + "examples/boolean_decl.fun", + "examples/booleans.fun", + "examples/builtins_conversions.fun", + "examples/builtins_extended.fun", + "examples/builtins_maps_and_more.fun", + "examples/byte_for_demo.fun", + "examples/byte_overflow_try_catch.fun", + "examples/cast_demo.fun", + "examples/class_constructor.fun", + "examples/classes_demo.fun", + "examples/crc32_example.fun", + "examples/crc32c_example.fun", + "examples/curl_download.fun", + "examples/curl_get_json.fun", + "examples/curl_post.fun", + "examples/datetime_basic.fun", + "examples/datetime_extended.fun", + "examples/datetime_timer.fun", + "examples/debug_reporting.fun", + "examples/echo_example.fun", + "examples/exit_example.fun", + "examples/expressions_test.fun", + "examples/fail.fun", + "examples/file_io.fun", + "examples/file_print_for_file_line_by_line.fun", + "examples/floats.fun", + "examples/for_range_test.fun", + "examples/functions_test.fun", + "examples/have_fun.fun", + "examples/have_fun_function.fun", + "examples/if_else_test.fun", + "examples/include_lib.fun", + "examples/include_local.fun", + "examples/include_local_util.fun", + "examples/include_namespace.fun", + "examples/inheritance_demo.fun", + "examples/ini_class_demo.fun", + "examples/ini_complex.fun", + "examples/ini_demo.fun", + "examples/ini_subsections.fun", + "examples/input_example.fun", + "examples/json_showcase.fun", + "examples/libsql_example.fun", + "examples/loops_break_continue.fun", + "examples/md5_demo.fun", + "examples/namespaced_mod.fun", + "examples/nested_loops.fun", + "examples/objects_basic.fun", + "examples/objects_more.fun", + "examples/os_env.fun", + "examples/pcre2_opcodes.fun", + "examples/pcre2_showcase.fun", + "examples/pcsc_example.fun", + "examples/process_example.fun", + "examples/regex_demo.fun", + "examples/regex_procedural.fun", + "examples/repl_on_error.fun", + "examples/sha1_demo.fun", + "examples/sha256_demo.fun", + "examples/sha256_str_demo.fun", + "examples/sha384_example.fun", + "examples/sha512_demo.fun", + "examples/sha512_str_demo.fun", + "examples/short_circuit_test.fun", + "examples/signed_ints.fun", + "examples/sqlite_example.fun", + "examples/stdlib_showcase.fun", + "examples/strings_test.fun", + "examples/tcp_http_get.fun", + "examples/tcp_http_get_class.fun", + "examples/thread_class_example.fun", + "examples/threads_demo.fun", + "examples/tk_hello.fun", + "examples/try_catch_finally.fun", + "examples/try_catch_with_error.fun", + "examples/typeof.fun", + "examples/typeof_features.fun", + "examples/type_safety.fun", + "examples/type_safety_fails.fun", + "examples/types_integers.fun", + "examples/types_overview.fun", + "examples/uint_types.fun", + "examples/unix_socket_echo.fun", + "examples/while_test.fun", + "examples/xml_access_catalog.fun", + "examples/xml_access_employees.fun", + "examples/xml_access_ns.fun", + "examples/xml_class_example.fun", + "examples/xml_minimal.fun" + ] -// This MUST fail because n is of type number and can not become a string or function. Setting it to 0 is not an option. -n = "FooBar" -print(n) -print("Typeof n: " + typeof(n)) + failures = [] -n = 100 -print(n) -print("Typeof n: " + typeof(n)) + for f in files + q = join(["Run ", f, "?"], "") + if c.ask_yes_no(q) + print(join(["=== Running: ", f, " ==="], "")) + code = run_example(bin, f) + if code != 0 + failures.push(f) + print("") + else + print(join(["Skipped: ", f], "")) -fun n(num) - print("n(" + to_string(num) + ")") -n(42) -// Typeof n MUST be of type Function here... Not Sint64. -print("Typeof n: " + typeof(n)) + if len(failures) == 0 + print("All selected examples completed successfully.") + else + print("Some selected examples failed:") + for ff in failures + print(join([" - ", ff], "")) -n = 2342 -print(n) -print("Typeof n: " + typeof(n)) - -x = 42 -print (x) - -x = "BarFoo" -print(x) - -// Arrays must be declared with an "array" identifier if not beeing dynamicly typed. We need the "array" type." -a = [23, 42] -print(a) -print(a[0]) - -// Why this is possible? Setting n to a string, sets n to 0. Setting an array to 1 works...? This must fail when an a is of type "array", not in this case! -a = 1 -print(a) - -string s = 'Have\n"fun!"' - print(s) - -print("\'" + " Fun") -print('\'' + " \'Fun\'") -print('\'' + " \"Fun\"") -print('\'' + " \n\"Fun\"") -print('\'' + " \n\t\"Fun\"") - -print("Running foo() 2 times...") -foo() -// We need arguments to functions... -foo() +main() From 40584f27f53a408d4dee27ce77188f993cc4646a Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 11 Dec 2025 19:49:35 +0100 Subject: [PATCH 47/55] Some parser refactoring for more fun developing Fun. (0.37.5) --- CMakeLists.txt | 2 +- src/ext/curl.c | 29 ++ src/ext/ini.c | 28 ++ src/ext/json.c | 107 +++++++ src/{vm/libsql/common.c => ext/libsql.c} | 4 +- src/ext/pcre2.c | 22 ++ src/ext/pcsc.c | 76 +++++ src/{vm/sqlite/common.c => ext/sqlite.c} | 2 +- src/ext/tcltk.c | 69 +++++ src/ext/xml2.c | 57 ++++ src/vm.c | 367 ++--------------------- 11 files changed, 417 insertions(+), 346 deletions(-) create mode 100644 src/ext/curl.c create mode 100644 src/ext/ini.c create mode 100644 src/ext/json.c rename src/{vm/libsql/common.c => ext/libsql.c} (94%) create mode 100644 src/ext/pcre2.c create mode 100644 src/ext/pcsc.c rename src/{vm/sqlite/common.c => ext/sqlite.c} (93%) create mode 100644 src/ext/tcltk.c create mode 100644 src/ext/xml2.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bc9feb..142212e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.4 LANGUAGES C) +project(fun VERSION 0.37.5 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/src/ext/curl.c b/src/ext/curl.c new file mode 100644 index 0000000..16dfea8 --- /dev/null +++ b/src/ext/curl.c @@ -0,0 +1,29 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm/libsql/common.c) + */ + +/* Ensure libcurl headers and helpers are defined at file scope (not inside vm_run) */ +#ifdef FUN_WITH_CURL +#include +typedef struct { char *d; size_t n; } FunCurlBuf; +static size_t fun_curl_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { + size_t add = sz * nm; + FunCurlBuf *b = (FunCurlBuf*)ud; + char *p = (char*)realloc(b->d, b->n + add + 1); + if (!p) return 0; + memcpy(p + b->n, ptr, add); + b->d = p; b->n += add; b->d[b->n] = '\0'; + return add; +} +static size_t fun_curl_file_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { + FILE *f = (FILE*)ud; + return fwrite(ptr, sz, nm, f); +} +#endif diff --git a/src/ext/ini.c b/src/ext/ini.c new file mode 100644 index 0000000..974d542 --- /dev/null +++ b/src/ext/ini.c @@ -0,0 +1,28 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm.c) + */ + +#ifdef FUN_WITH_INI +#if defined(__has_include) +# if __has_include() +# include +# include +# elif __has_include() +# include +# include +# else +# error "iniparser headers not found" +# endif +#else +# include +# include +#endif +#include "vm/ini/handles.h" +#endif diff --git a/src/ext/json.c b/src/ext/json.c new file mode 100644 index 0000000..331fec7 --- /dev/null +++ b/src/ext/json.c @@ -0,0 +1,107 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm.c) + */ + + /* json-c helpers and VM opcode cases (included from vm.c) */ + +#ifdef FUN_WITH_JSON +#include "value.h" +#include "vm.h" + +#include +#include + +/* --- Conversion helpers between json-c and Fun Value --- */ +static Value json_to_fun(json_object *j) { + if (!j) return make_nil(); + enum json_type t = json_object_get_type(j); + switch (t) { + case json_type_null: return make_nil(); + case json_type_boolean: return make_bool(json_object_get_boolean(j)); + case json_type_double: return make_float(json_object_get_double(j)); + case json_type_int: return make_int((int64_t)json_object_get_int64(j)); + case json_type_string: return make_string(json_object_get_string(j)); + case json_type_array: { + size_t n = json_object_array_length(j); + if (n == 0) { + return make_array_from_values(NULL, 0); + } + Value *vals = (Value*)malloc(sizeof(Value) * n); + if (!vals) return make_array_from_values(NULL, 0); + for (size_t i = 0; i < n; ++i) { + json_object *item = json_object_array_get_idx(j, (int)i); + vals[i] = json_to_fun(item); + } + Value arr = make_array_from_values(vals, (int)n); + for (size_t i = 0; i < n; ++i) free_value(vals[i]); + free(vals); + return arr; + } + case json_type_object: { + Value map = make_map_empty(); + json_object_object_foreach(j, key, val) { + (void)map_set(&map, key, json_to_fun(val)); + } + return map; + } + default: + return make_nil(); + } +} + +static json_object* fun_to_json(const Value *v) { + switch (v->type) { + case VAL_NIL: return json_object_new_null(); + case VAL_BOOL: return json_object_new_boolean(v->i ? 1 : 0); + case VAL_INT: return json_object_new_int64(v->i); + case VAL_FLOAT: return json_object_new_double(v->d); + case VAL_STRING: return json_object_new_string(v->s ? v->s : ""); + case VAL_ARRAY: { + json_object *arr = json_object_new_array(); + int n = array_length(v); + for (int i = 0; i < n; ++i) { + Value item; + if (array_get_copy(v, i, &item)) { + json_object_array_add(arr, fun_to_json(&item)); + free_value(item); + } else { + json_object_array_add(arr, json_object_new_null()); + } + } + return arr; + } + case VAL_MAP: { + json_object *obj = json_object_new_object(); + /* We don't have an iterator API; use keys() helper */ + Value keys = map_keys_array(v); + int kn = array_length(&keys); + for (int i = 0; i < kn; ++i) { + Value k; + if (!array_get_copy(&keys, i, &k)) continue; + if (k.type == VAL_STRING && k.s) { + Value val; + if (map_get_copy(v, k.s, &val)) { + json_object_object_add(obj, k.s, fun_to_json(&val)); + free_value(val); + } else { + json_object_object_add(obj, k.s, json_object_new_null()); + } + } + free_value(k); + } + free_value(keys); + return obj; + } + default: + /* Fallback: stringify unsupported types */ + return json_object_new_string(""); + } +} +#endif diff --git a/src/vm/libsql/common.c b/src/ext/libsql.c similarity index 94% rename from src/vm/libsql/common.c rename to src/ext/libsql.c index e69f5bb..898d286 100644 --- a/src/vm/libsql/common.c +++ b/src/ext/libsql.c @@ -6,7 +6,7 @@ * Licensed under the terms of the Apache-2.0 license. * https://opensource.org/license/apache-2-0 * - * Added: 2025-11-26 + * Added: 2025-11-26 (2025-12-11 migrated from src/vm/libsql/common.c) */ #ifdef FUN_WITH_LIBSQL @@ -52,4 +52,4 @@ static void libsql_reg_del(int id) { pp = &((*pp)->next); } } -#endif /* FUN_WITH_LIBSQL */ +#endif diff --git a/src/ext/pcre2.c b/src/ext/pcre2.c new file mode 100644 index 0000000..0d8ec38 --- /dev/null +++ b/src/ext/pcre2.c @@ -0,0 +1,22 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm/libsql/common.c) + */ + + /* Ensure PCRE2 is configured consistently across the whole translation unit. + * vm.c includes many opcode implementation .c files; some use PCRE2. For PCRE2 + * headers to expose the correct typedefs (e.g., pcre2_code, PCRE2_SPTR), the + * PCRE2_CODE_UNIT_WIDTH macro must be defined before the first inclusion of + * . We do this once here when PCRE2 support is enabled. */ +#ifdef FUN_WITH_PCRE2 +#ifndef PCRE2_CODE_UNIT_WIDTH +#define PCRE2_CODE_UNIT_WIDTH 8 +#endif +#include +#endif diff --git a/src/ext/pcsc.c b/src/ext/pcsc.c new file mode 100644 index 0000000..aa128c1 --- /dev/null +++ b/src/ext/pcsc.c @@ -0,0 +1,76 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm.c) + */ + +/* +PCSC helpers: registries and helper functions. +Included the file scope from vm.c. +*/ + +#ifdef FUN_WITH_PCSC +#if defined(__has_include) + #if __has_include() + #include + #include + #elif __has_include() + #include + #else + #error "FUN_WITH_PCSC is enabled but PCSC headers were not found" + #endif + #else + #include + #include + #endif + #include + + typedef struct { + SCARDCONTEXT ctx; + int in_use; + } pcsc_ctx_entry; + +typedef struct { + SCARDHANDLE h; + DWORD proto; + int in_use; +} pcsc_card_entry; + +static pcsc_ctx_entry g_pcsc_ctx[8]; +static pcsc_card_entry g_pcsc_card[32]; + +static int pcsc_alloc_ctx_slot(void) { + for (int i = 0; i < (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0])); ++i) { + if (!g_pcsc_ctx[i].in_use) { g_pcsc_ctx[i].in_use = 1; g_pcsc_ctx[i].ctx = 0; return i + 1; } + } + return 0; +} + +static int pcsc_alloc_card_slot(void) { + for (int i = 0; i < (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0])); ++i) { + if (!g_pcsc_card[i].in_use) { g_pcsc_card[i].in_use = 1; g_pcsc_card[i].h = 0; g_pcsc_card[i].proto = 0; return i + 1; } + } + return 0; +} + +static pcsc_ctx_entry* pcsc_get_ctx(int id) { + if (id <= 0) return NULL; + int idx = id - 1; + if (idx < 0 || idx >= (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0]))) return NULL; + if (!g_pcsc_ctx[idx].in_use) return NULL; + return &g_pcsc_ctx[idx]; +} + +static pcsc_card_entry* pcsc_get_card(int id) { + if (id <= 0) return NULL; + int idx = id - 1; + if (idx < 0 || idx >= (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0]))) return NULL; + if (!g_pcsc_card[idx].in_use) return NULL; + return &g_pcsc_card[idx]; +} +#endif diff --git a/src/vm/sqlite/common.c b/src/ext/sqlite.c similarity index 93% rename from src/vm/sqlite/common.c rename to src/ext/sqlite.c index df6105d..82ace26 100644 --- a/src/vm/sqlite/common.c +++ b/src/ext/sqlite.c @@ -6,7 +6,7 @@ * Licensed under the terms of the Apache-2.0 license. * https://opensource.org/license/apache-2-0 * - * Added: 2025-11-26 + * Added: 2025-12-11 (2025-12-11 migrated from src/vm/sqlite/common.c) */ /** diff --git a/src/ext/tcltk.c b/src/ext/tcltk.c new file mode 100644 index 0000000..d9c39b3 --- /dev/null +++ b/src/ext/tcltk.c @@ -0,0 +1,69 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm.c) + */ + +#ifdef FUN_WITH_TCLTK +#include +#include +static Tcl_Interp* g_fun_tcl_interp = NULL; + +static void fun_tk_init_once(void) { + if (g_fun_tcl_interp) return; + Tcl_FindExecutable(NULL); + g_fun_tcl_interp = Tcl_CreateInterp(); + if (!g_fun_tcl_interp) return; + if (Tcl_Init(g_fun_tcl_interp) != TCL_OK) { + fprintf(stderr, "Tcl_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); + } + if (Tk_Init(g_fun_tcl_interp) != TCL_OK) { + fprintf(stderr, "Tk_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); + } + /* Ensure the app terminates if the main window is closed via window manager */ + /* Best-effort: set WM_DELETE_WINDOW handler to exit the process. */ + Tcl_Eval(g_fun_tcl_interp, "wm protocol . WM_DELETE_WINDOW {exit 0}"); +} + +static int fun_tk_eval_script(const char *script) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return -1; + int rc = Tcl_Eval(g_fun_tcl_interp, script ? script : ""); + return rc; /* TCL_OK = 0 */ +} + +static const char* fun_tk_get_result(void) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return ""; + return Tcl_GetStringResult(g_fun_tcl_interp); +} + +static void fun_tk_loop(void) { + fun_tk_init_once(); + if (!g_fun_tcl_interp) return; + /* Drive Tk event loop until all main windows are closed */ + while (Tk_GetNumMainWindows() > 0) { + while (Tcl_DoOneEvent(0)) {} + /* tiny sleep to avoid busy spin */ +#ifdef _WIN32 +#include + Sleep(1); +#else +#include + struct timespec ts = {0, 1000000}; /* 1 ms */ + nanosleep(&ts, NULL); +#endif + } +} +#else +/* Stubs when Tcl/Tk is disabled */ +static void fun_tk_init_once(void) { (void)0; } +static int fun_tk_eval_script(const char *script) { (void)script; return -1; } +static const char* fun_tk_get_result(void) { return ""; } +static void fun_tk_loop(void) { (void)0; } +#endif diff --git a/src/ext/xml2.c b/src/ext/xml2.c new file mode 100644 index 0000000..8f0efbb --- /dev/null +++ b/src/ext/xml2.c @@ -0,0 +1,57 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 (2025-12-11 migrated from src/vm/libsql/common.c) + */ + +#ifdef FUN_WITH_XML2 +#include +#include + +typedef struct { xmlDocPtr doc; int in_use; } XmlDocSlot; +typedef struct { xmlNodePtr node; int in_use; } XmlNodeSlot; + +static XmlDocSlot g_xml_docs[64]; +static XmlNodeSlot g_xml_nodes[256]; + +static int xml_doc_alloc(xmlDocPtr d) { + for (int i = 1; i < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])); ++i) { + if (!g_xml_docs[i].in_use) { g_xml_docs[i].in_use = 1; g_xml_docs[i].doc = d; return i; } + } + return 0; +} +static xmlDocPtr xml_doc_get(int h) { + if (h > 0 && h < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) && g_xml_docs[h].in_use) return g_xml_docs[h].doc; + return NULL; +} +static int xml_doc_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) || !g_xml_docs[h].in_use) return 0; + if (g_xml_docs[h].doc) xmlFreeDoc(g_xml_docs[h].doc); + g_xml_docs[h].doc = NULL; + g_xml_docs[h].in_use = 0; + return 1; +} + +static int xml_node_alloc(xmlNodePtr n) { + for (int i = 1; i < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])); ++i) { + if (!g_xml_nodes[i].in_use) { g_xml_nodes[i].in_use = 1; g_xml_nodes[i].node = n; return i; } + } + return 0; +} +static xmlNodePtr xml_node_get(int h) { + if (h > 0 && h < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) && g_xml_nodes[h].in_use) return g_xml_nodes[h].node; + return NULL; +} +static int xml_node_free_handle(int h) { + if (h <= 0 || h >= (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) || !g_xml_nodes[h].in_use) return 0; + /* nodes are owned by their document; do not free here */ + g_xml_nodes[h].node = NULL; + g_xml_nodes[h].in_use = 0; + return 1; +} +#endif diff --git a/src/vm.c b/src/vm.c index 7ecc8fb..7991a2d 100644 --- a/src/vm.c +++ b/src/vm.c @@ -14,351 +14,12 @@ #define _POSIX_C_SOURCE 200809L #endif #endif -#include -/* Bring in split-out built-ins without changing the build system yet */ -#include "iter.c" -#include "map.c" -#include "string.c" -#include "value.h" -#include "vm.h" - -#ifdef FUN_WITH_PCSC - /* PCSC helpers: registries and helper functions. - * Included at file scope from vm.c. */ - #if defined(__has_include) - #if __has_include() - #include - #include - #elif __has_include() - #include - #else - #error "FUN_WITH_PCSC is enabled but PCSC headers were not found" - #endif - #else - #include - #include - #endif - #include - - typedef struct { - SCARDCONTEXT ctx; - int in_use; - } pcsc_ctx_entry; - - typedef struct { - SCARDHANDLE h; - DWORD proto; - int in_use; - } pcsc_card_entry; - - static pcsc_ctx_entry g_pcsc_ctx[8]; - static pcsc_card_entry g_pcsc_card[32]; - - static int pcsc_alloc_ctx_slot(void) { - for (int i = 0; i < (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0])); ++i) { - if (!g_pcsc_ctx[i].in_use) { g_pcsc_ctx[i].in_use = 1; g_pcsc_ctx[i].ctx = 0; return i + 1; } - } - return 0; - } - - static int pcsc_alloc_card_slot(void) { - for (int i = 0; i < (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0])); ++i) { - if (!g_pcsc_card[i].in_use) { g_pcsc_card[i].in_use = 1; g_pcsc_card[i].h = 0; g_pcsc_card[i].proto = 0; return i + 1; } - } - return 0; - } - - static pcsc_ctx_entry* pcsc_get_ctx(int id) { - if (id <= 0) return NULL; - int idx = id - 1; - if (idx < 0 || idx >= (int)(sizeof(g_pcsc_ctx)/sizeof(g_pcsc_ctx[0]))) return NULL; - if (!g_pcsc_ctx[idx].in_use) return NULL; - return &g_pcsc_ctx[idx]; - } - - static pcsc_card_entry* pcsc_get_card(int id) { - if (id <= 0) return NULL; - int idx = id - 1; - if (idx < 0 || idx >= (int)(sizeof(g_pcsc_card)/sizeof(g_pcsc_card[0]))) return NULL; - if (!g_pcsc_card[idx].in_use) return NULL; - return &g_pcsc_card[idx]; - } -#endif - -#ifdef FUN_WITH_JSON -/* json-c helpers and VM opcode cases (included from vm.c) */ - -#include "value.h" -#include "vm.h" - -#include -#include - -/* --- Conversion helpers between json-c and Fun Value --- */ -static Value json_to_fun(json_object *j) { - if (!j) return make_nil(); - enum json_type t = json_object_get_type(j); - switch (t) { - case json_type_null: return make_nil(); - case json_type_boolean: return make_bool(json_object_get_boolean(j)); - case json_type_double: return make_float(json_object_get_double(j)); - case json_type_int: return make_int((int64_t)json_object_get_int64(j)); - case json_type_string: return make_string(json_object_get_string(j)); - case json_type_array: { - size_t n = json_object_array_length(j); - if (n == 0) { - return make_array_from_values(NULL, 0); - } - Value *vals = (Value*)malloc(sizeof(Value) * n); - if (!vals) return make_array_from_values(NULL, 0); - for (size_t i = 0; i < n; ++i) { - json_object *item = json_object_array_get_idx(j, (int)i); - vals[i] = json_to_fun(item); - } - Value arr = make_array_from_values(vals, (int)n); - for (size_t i = 0; i < n; ++i) free_value(vals[i]); - free(vals); - return arr; - } - case json_type_object: { - Value map = make_map_empty(); - json_object_object_foreach(j, key, val) { - (void)map_set(&map, key, json_to_fun(val)); - } - return map; - } - default: - return make_nil(); - } -} - -static json_object* fun_to_json(const Value *v) { - switch (v->type) { - case VAL_NIL: return json_object_new_null(); - case VAL_BOOL: return json_object_new_boolean(v->i ? 1 : 0); - case VAL_INT: return json_object_new_int64(v->i); - case VAL_FLOAT: return json_object_new_double(v->d); - case VAL_STRING: return json_object_new_string(v->s ? v->s : ""); - case VAL_ARRAY: { - json_object *arr = json_object_new_array(); - int n = array_length(v); - for (int i = 0; i < n; ++i) { - Value item; - if (array_get_copy(v, i, &item)) { - json_object_array_add(arr, fun_to_json(&item)); - free_value(item); - } else { - json_object_array_add(arr, json_object_new_null()); - } - } - return arr; - } - case VAL_MAP: { - json_object *obj = json_object_new_object(); - /* We don't have an iterator API; use keys() helper */ - Value keys = map_keys_array(v); - int kn = array_length(&keys); - for (int i = 0; i < kn; ++i) { - Value k; - if (!array_get_copy(&keys, i, &k)) continue; - if (k.type == VAL_STRING && k.s) { - Value val; - if (map_get_copy(v, k.s, &val)) { - json_object_object_add(obj, k.s, fun_to_json(&val)); - free_value(val); - } else { - json_object_object_add(obj, k.s, json_object_new_null()); - } - } - free_value(k); - } - free_value(keys); - return obj; - } - default: - /* Fallback: stringify unsupported types */ - return json_object_new_string(""); - } -} - -/* Note: The VM opcode case handlers are included from vm/vm switch via vm/json/ops.c */ -#endif - -#ifdef FUN_WITH_TCLTK -#include -#include -static Tcl_Interp* g_fun_tcl_interp = NULL; - -static void fun_tk_init_once(void) { - if (g_fun_tcl_interp) return; - Tcl_FindExecutable(NULL); - g_fun_tcl_interp = Tcl_CreateInterp(); - if (!g_fun_tcl_interp) return; - if (Tcl_Init(g_fun_tcl_interp) != TCL_OK) { - fprintf(stderr, "Tcl_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); - } - if (Tk_Init(g_fun_tcl_interp) != TCL_OK) { - fprintf(stderr, "Tk_Init failed: %s\n", Tcl_GetStringResult(g_fun_tcl_interp)); - } - /* Ensure the app terminates if the main window is closed via window manager */ - /* Best-effort: set WM_DELETE_WINDOW handler to exit the process. */ - Tcl_Eval(g_fun_tcl_interp, "wm protocol . WM_DELETE_WINDOW {exit 0}"); -} - -static int fun_tk_eval_script(const char *script) { - fun_tk_init_once(); - if (!g_fun_tcl_interp) return -1; - int rc = Tcl_Eval(g_fun_tcl_interp, script ? script : ""); - return rc; /* TCL_OK = 0 */ -} - -static const char* fun_tk_get_result(void) { - fun_tk_init_once(); - if (!g_fun_tcl_interp) return ""; - return Tcl_GetStringResult(g_fun_tcl_interp); -} - -static void fun_tk_loop(void) { - fun_tk_init_once(); - if (!g_fun_tcl_interp) return; - /* Drive Tk event loop until all main windows are closed */ - while (Tk_GetNumMainWindows() > 0) { - while (Tcl_DoOneEvent(0)) {} - /* tiny sleep to avoid busy spin */ -#ifdef _WIN32 - #include - Sleep(1); -#else - #include - struct timespec ts = {0, 1000000}; /* 1 ms */ - nanosleep(&ts, NULL); -#endif - } -} -#else -/* Stubs when Tcl/Tk is disabled */ -static void fun_tk_init_once(void) { (void)0; } -static int fun_tk_eval_script(const char *script) { (void)script; return -1; } -static const char* fun_tk_get_result(void) { return ""; } -static void fun_tk_loop(void) { (void)0; } -#endif - -#ifdef FUN_WITH_INI -#if defined(__has_include) -# if __has_include() -# include -# include -# elif __has_include() -# include -# include -# else -# error "iniparser headers not found" -# endif -#else -# include -# include -#endif -#include "vm/ini/handles.h" -#endif - -#ifdef FUN_WITH_SQLITE -#include -#include "vm/sqlite/common.c" -#endif - -#ifdef FUN_WITH_LIBSQL -#include /* libsql exposes sqlite3-compatible C API */ -#include "vm/libsql/common.c" -#endif -#include "vm.h" -#include "value.h" #include #include #include #include - -/* Ensure PCRE2 is configured consistently across the whole translation unit. - * vm.c includes many opcode implementation .c files; some use PCRE2. For PCRE2 - * headers to expose the correct typedefs (e.g., pcre2_code, PCRE2_SPTR), the - * PCRE2_CODE_UNIT_WIDTH macro must be defined before the first inclusion of - * . We do this once here when PCRE2 support is enabled. */ -#ifdef FUN_WITH_PCRE2 -#ifndef PCRE2_CODE_UNIT_WIDTH -#define PCRE2_CODE_UNIT_WIDTH 8 -#endif -#include -#endif - -/* Ensure libcurl headers and helpers are defined at file scope (not inside vm_run) */ -#ifdef FUN_WITH_CURL -#include -typedef struct { char *d; size_t n; } FunCurlBuf; -static size_t fun_curl_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { - size_t add = sz * nm; - FunCurlBuf *b = (FunCurlBuf*)ud; - char *p = (char*)realloc(b->d, b->n + add + 1); - if (!p) return 0; - memcpy(p + b->n, ptr, add); - b->d = p; b->n += add; b->d[b->n] = '\0'; - return add; -} -static size_t fun_curl_file_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { - FILE *f = (FILE*)ud; - return fwrite(ptr, sz, nm, f); -} -#endif - -#ifdef FUN_WITH_XML2 -#include -#include - -typedef struct { xmlDocPtr doc; int in_use; } XmlDocSlot; -typedef struct { xmlNodePtr node; int in_use; } XmlNodeSlot; - -static XmlDocSlot g_xml_docs[64]; -static XmlNodeSlot g_xml_nodes[256]; - -static int xml_doc_alloc(xmlDocPtr d) { - for (int i = 1; i < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])); ++i) { - if (!g_xml_docs[i].in_use) { g_xml_docs[i].in_use = 1; g_xml_docs[i].doc = d; return i; } - } - return 0; -} -static xmlDocPtr xml_doc_get(int h) { - if (h > 0 && h < (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) && g_xml_docs[h].in_use) return g_xml_docs[h].doc; - return NULL; -} -static int xml_doc_free_handle(int h) { - if (h <= 0 || h >= (int)(sizeof(g_xml_docs)/sizeof(g_xml_docs[0])) || !g_xml_docs[h].in_use) return 0; - if (g_xml_docs[h].doc) xmlFreeDoc(g_xml_docs[h].doc); - g_xml_docs[h].doc = NULL; - g_xml_docs[h].in_use = 0; - return 1; -} - -static int xml_node_alloc(xmlNodePtr n) { - for (int i = 1; i < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])); ++i) { - if (!g_xml_nodes[i].in_use) { g_xml_nodes[i].in_use = 1; g_xml_nodes[i].node = n; return i; } - } - return 0; -} -static xmlNodePtr xml_node_get(int h) { - if (h > 0 && h < (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) && g_xml_nodes[h].in_use) return g_xml_nodes[h].node; - return NULL; -} -static int xml_node_free_handle(int h) { - if (h <= 0 || h >= (int)(sizeof(g_xml_nodes)/sizeof(g_xml_nodes[0])) || !g_xml_nodes[h].in_use) return 0; - /* nodes are owned by their document; do not free here */ - g_xml_nodes[h].node = NULL; - g_xml_nodes[h].in_use = 0; - return 1; -} -#endif /* FUN_WITH_XML2 */ - -/* forward declarations for include mapping used in error reporting */ -extern char *preprocess_includes(const char *src); -static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); +#include #ifdef __unix__ #include @@ -368,9 +29,31 @@ static int map_expanded_line_to_include(const char *path, int line, char *out_pa #include #include #include -#include +//#include #endif +/* Bring in split-out built-ins without changing the build system yet */ +#include "iter.c" +#include "map.c" +#include "string.c" +#include "value.h" +#include "vm.h" + +// Optional by extensions commonly used code. #ifdef's are in each single file. +#include "ext/curl.c" +#include "ext/ini.c" +#include "ext/json.c" +#include "ext/libsql.c" +#include "ext/pcsc.c" +#include "ext/pcre2.c" +#include "ext/sqlite.c" +#include "ext/tcltk.c" +#include "ext/xml2.c" + +/* forward declarations for include mapping used in error reporting */ +extern char *preprocess_includes(const char *src); +static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); + /* Threading internals (registry and platform glue) */ #include "vm/os/thread_common.c" @@ -983,7 +666,7 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/time_now_ms.c" #include "vm/os/clock_mono_ms.c" #include "vm/os/date_format.c" - + /* Socket ops */ #include "vm/os/socket_tcp_listen.c" #include "vm/os/socket_tcp_accept.c" From 4039abd10db42e9b673441a53c163e3fcf9fe61a Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 11 Dec 2025 22:51:23 +0100 Subject: [PATCH 48/55] Added new example that shows the usage of random_int() and random_seed(). Related parser fixes. (0.37.6) --- CMakeLists.txt | 2 +- examples/builtins_extended.fun | 6 +-- examples/random_demo.fun | 81 ++++++++++++++++++++++++++++++++++ src/parser.c | 11 ++--- 4 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 examples/random_demo.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 142212e..45f83ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.5 LANGUAGES C) +project(fun VERSION 0.37.6 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/builtins_extended.fun b/examples/builtins_extended.fun index 7c9fe9f..aac791e 100755 --- a/examples/builtins_extended.fun +++ b/examples/builtins_extended.fun @@ -45,9 +45,9 @@ print(max(5, 9)) // -> 9 print(clamp(15, 0, 10)) // -> 10 print(abs(-7)) // -> 7 print(pow(2, 8)) // -> 256 -random(123) // seed RNG -print(randomInt(0, 3)) // -> 0..2 (deterministic for seed 123) -print(randomInt(5, 6)) // -> 5 +random_seed(123) // seed RNG +print(random_int(0, 3)) // -> 0..2 (deterministic for seed 123) +print(random_int(5, 6)) // -> 5 /* Expected output: a-b-c diff --git a/examples/random_demo.fun b/examples/random_demo.fun new file mode 100644 index 0000000..6ec0a36 --- /dev/null +++ b/examples/random_demo.fun @@ -0,0 +1,81 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-11 + */ + +// Demonstrates usage of RANDOM_SEED and RANDOM_INT opcodes via +// the built-ins: random_seed(seed) and random_int(lo, hiExclusive). + +print("-- Random demo: seed reproducibility and bounds --") + +// Seed with a fixed value and produce a short sequence +seed = 123456 +random_seed(seed) +a1 = random_int(0, 10) // in [0,10) +a2 = random_int(0, 10) +a3 = random_int(5, 8) // in [5,8) + +print("First run:") +print(a1) +print(a2) +print(a3) + +// Re-seed with the same value: the sequence should repeat +random_seed(seed) +b1 = random_int(0, 10) +b2 = random_int(0, 10) +b3 = random_int(5, 8) + +print("Second run (after re-seed):") +print(b1) +print(b2) +print(b3) + +print("Reproducible? (a1==b1, a2==b2, a3==b3)") +print(a1 == b1) +print(a2 == b2) +print(a3 == b3) + +// Show that the upper bound is exclusive by sampling multiple times +// and tracking the maximum seen value; it should never reach hi. +lo = 10 +hi = 20 +max_seen = lo +i = 0 +while i < 100 + v = random_int(lo, hi) + if (v > max_seen) max_seen = v + i = i + 1 + +print("Max seen in [" + to_string(lo) + "," + to_string(hi) + ") over 100 samples:") +print(max_seen) +print("Is max_seen < hi? ") +print(max_seen < hi) + +/* Expected output: +-- Random demo: seed reproducibility and bounds -- +First run: +9 +3 +5 +Second run (after re-seed): +9 +3 +5 +Reproducible? (a1==b1, a2==b2, a3==b3) +true +true +true +Max seen in [10,20) over 100 samples: +19 +Is max_seen < hi? +1 +*/ diff --git a/src/parser.c b/src/parser.c index 17f1eca..b665f16 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1632,17 +1632,18 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } - if (strcmp(name, "random") == 0) { + + if (strcmp(name, "random_seed") == 0) { (*pos)++; /* '(' */ - if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "random expects 1 arg"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "random_seed expects 1 arg"); free(name); return 0; } bytecode_add_instruction(bc, OP_RANDOM_SEED, 0); free(name); return 1; } - if (strcmp(name, "randomInt") == 0) { + if (strcmp(name, "random_int") == 0) { (*pos)++; /* '(' */ - if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "randomInt expects 2 args"); free(name); return 0; } - if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "randomInt expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "random_int expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "random_int expects 2 args"); free(name); return 0; } bytecode_add_instruction(bc, OP_RANDOM_INT, 0); free(name); return 1; From 3f586a90e14fe7dfa3c1f914ca6eb34d35a120a9 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 11 Dec 2025 23:18:51 +0100 Subject: [PATCH 49/55] Added some basic static linking stuff to CMakeLists.txt for testing things. (-DFUN_LINK_STATIC=ON). (0.37.7) --- CMakeLists.txt | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 45f83ae..09338e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.6 LANGUAGES C) +project(fun VERSION 0.37.7 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -25,6 +25,44 @@ else() set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) endif() +# Optional: build statically linked executables +# When enabled, prefer static libraries for all dependencies and request +# fully static linking for executables where the platform/toolchain allows it. +option(FUN_LINK_STATIC "Link fun and tests fully static where possible" OFF) + +if(FUN_LINK_STATIC) + message(STATUS "Building Fun statically linked") + # Prefer static libraries for all add_library without explicit type + set(BUILD_SHARED_LIBS OFF) + + # Make pkg-config choose static libs + set(PKG_CONFIG_USE_STATIC_LIBS ON) + + # Hint find_library to choose static archives first on Unix (not macOS) + # Prefer .a but still allow falling back to shared if static is unavailable. + if(UNIX AND NOT APPLE) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a;.so;.so.0;.so.1) + endif() + + # Toolchain-specific static link flags + # Use a "mostly static" approach by default to avoid requiring static + # variants of every system/third-party library. This keeps libgcc and + # libstdc++ static while allowing shared deps when needed. + if(UNIX AND NOT APPLE AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + set(_FUN_STATIC_LINK_FLAGS -static-libgcc -static-libstdc++) + endif() + + # On MSVC, prefer the static runtime + if(MSVC) + foreach(flag_var CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE + CMAKE_C_FLAGS_RELWITHDEBINFO CMAKE_C_FLAGS_MINSIZEREL) + if(DEFINED ${flag_var}) + string(REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endif() + endforeach() + endif() +endif() + # Optional Tcl/Tk GUI support (embedded interpreter) option(FUN_WITH_TCLTK "Enable Tcl/Tk GUI support" OFF) set(TCL_INCLUDE_DIRS "") @@ -425,6 +463,15 @@ add_executable(test_opcodes ) target_link_libraries(test_opcodes PRIVATE fun_core) +# If static linking is requested, apply linker flags to executables +if(FUN_LINK_STATIC AND DEFINED _FUN_STATIC_LINK_FLAGS) + foreach(tgt fun fun_test test_opcodes) + if(TARGET ${tgt}) + target_link_options(${tgt} PRIVATE ${_FUN_STATIC_LINK_FLAGS}) + endif() + endforeach() +endif() + # Convenience aggregate target (like 'build' in Makefile) add_custom_target(build DEPENDS fun fun_test test_opcodes From 5b90ff497c6170a1f28bcf8908e644f15ea78ade Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 12 Dec 2025 00:56:52 +0100 Subject: [PATCH 50/55] Added support to build Fun using musl libc instead glibc on a glibc based system (-DFUN_USE_MUSL). Alpine will ignore this feature... ;) (0.37.8) --- CMakeLists.txt | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 09338e0..5f4f92f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.7 LANGUAGES C) +project(fun VERSION 0.37.8 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -25,6 +25,34 @@ else() set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) endif() +# Optional: build using musl libc instead of glibc on Linux (OFF by default) +# Note: Switching CMAKE_C_COMPILER must happen before the first project() call. +option(FUN_USE_MUSL "Use musl libc toolchain when available (Linux only)" OFF) + +if(FUN_USE_MUSL AND UNIX AND NOT APPLE) + # Try to find a musl toolchain wrapper + find_program(_FUN_MUSL_CC NAMES musl-gcc musl-clang) + if(_FUN_MUSL_CC) + message(STATUS "FUN_USE_MUSL=ON: using musl toolchain: ${_FUN_MUSL_CC}") + # Force C compiler to musl wrapper before project() so the whole toolchain is configured accordingly + set(CMAKE_C_COMPILER "${_FUN_MUSL_CC}" CACHE FILEPATH "C compiler" FORCE) + set(FUN_LIBC "musl" CACHE STRING "Selected C library") + else() + message(WARNING "FUN_USE_MUSL=ON but no musl toolchain (musl-gcc or musl-clang) found. Falling back to default compiler (likely glibc).") + set(FUN_LIBC "glibc" CACHE STRING "Selected C library") + endif() +else() + # Default remains the system toolchain (typically glibc on Linux) + set(FUN_LIBC "glibc" CACHE STRING "Selected C library") +endif() + +# Expose a preprocessor macro indicating selected C library +if(FUN_LIBC STREQUAL "musl") + add_definitions(-DFUN_LIBC_MUSL) +else() + add_definitions(-DFUN_LIBC_GLIBC) +endif() + # Optional: build statically linked executables # When enabled, prefer static libraries for all dependencies and request # fully static linking for executables where the platform/toolchain allows it. @@ -256,7 +284,7 @@ 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") + 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) From ea6553fb190815edd0d429be5ee6eea8523e2e0c Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 12 Dec 2025 02:34:47 +0100 Subject: [PATCH 51/55] Added a simple Bash based ./make script to make development more fun. (0.37.9) --- CMakeLists.txt | 2 +- make | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100755 make diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f4f92f..ad6be87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.8 LANGUAGES C) +project(fun VERSION 0.37.9 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/make b/make new file mode 100755 index 0000000..3009268 --- /dev/null +++ b/make @@ -0,0 +1,156 @@ +#!/bin/bash + +# This file is part of the Fun programming language. +# https://fun-lang.xyz/ +# +# Copyright 2025 Johannes Findeisen +# Licensed under the terms of the Apache-2.0 license. +# https://opensource.org/license/apache-2-0 +# +# Added: 2025-12-12 + +if [ -z "$1" ]; then + echo "Build target is unset, using 'minimal'"; + target="minimal"; +else + echo "Build target is set to '$1'"; + target=$1; +fi + +if [ "$target" = "all" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=ON \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ + -DFUN_WITH_SQLITE=ON \ + -DFUN_WITH_CURL=ON \ + -DFUN_WITH_PCRE2=ON \ + -DFUN_WITH_XML2=ON \ + -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ + -DFUN_WITH_INI=ON \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=OFF \ + && cmake --build build --target fun +elif [ "$target" = "alpine" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ + -DFUN_WITH_SQLITE=ON \ + -DFUN_WITH_CURL=ON \ + -DFUN_WITH_PCRE2=ON \ + -DFUN_WITH_XML2=ON \ + -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=OFF \ + -DFUN_WITH_INI=ON \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_DEBUG=OFF \ + && cmake --build build --target fun +elif [ "$target" = "debug" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=OFF \ + -DFUN_WITH_SQLITE=OFF \ + -DFUN_WITH_CURL=OFF \ + -DFUN_WITH_PCRE2=OFF \ + -DFUN_WITH_XML2=OFF \ + -DFUN_WITH_JSON=OFF \ + -DFUN_WITH_TCLTK=OFF \ + -DFUN_WITH_INI=OFF \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=ON \ + && cmake --build build --target fun +elif [ "$target" = "debug_all" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=ON \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ + -DFUN_WITH_SQLITE=ON \ + -DFUN_WITH_CURL=ON \ + -DFUN_WITH_PCRE2=ON \ + -DFUN_WITH_XML2=ON \ + -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ + -DFUN_WITH_INI=ON \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=ON \ + && cmake --build build --target fun +elif [ "$target" = "freebsd" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=OFF \ + -DFUN_WITH_SQLITE=OFF \ + -DFUN_WITH_CURL=OFF \ + -DFUN_WITH_PCRE2=OFF \ + -DFUN_WITH_XML2=OFF \ + -DFUN_WITH_JSON=OFF \ + -DFUN_WITH_TCLTK=OFF \ + -DFUN_WITH_INI=OFF \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_DEBUG=OFF \ + && cmake --build build --target fun +elif [ "$target" = "minimal" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_REPL=OFF \ + -DFUN_WITH_LIBSQL=OFF \ + -DFUN_WITH_SQLITE=OFF \ + -DFUN_WITH_CURL=OFF \ + -DFUN_WITH_PCRE2=OFF \ + -DFUN_WITH_XML2=OFF \ + -DFUN_WITH_JSON=OFF \ + -DFUN_WITH_TCLTK=OFF \ + -DFUN_WITH_INI=OFF \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=OFF \ + && cmake --build build --target fun +elif [ "$target" = "musl" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=OFF \ + -DFUN_WITH_SQLITE=OFF \ + -DFUN_WITH_CURL=OFF \ + -DFUN_WITH_PCRE2=OFF \ + -DFUN_WITH_XML2=OFF \ + -DFUN_WITH_JSON=OFF \ + -DFUN_WITH_TCLTK=OFF \ + -DFUN_WITH_INI=OFF \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=ON \ + -DFUN_DEBUG=OFF \ + && cmake --build build --target fun +elif [ "$target" = "repl" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=OFF \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=OFF \ + -DFUN_WITH_SQLITE=OFF \ + -DFUN_WITH_CURL=OFF \ + -DFUN_WITH_PCRE2=OFF \ + -DFUN_WITH_XML2=OFF \ + -DFUN_WITH_JSON=OFF \ + -DFUN_WITH_TCLTK=OFF \ + -DFUN_WITH_INI=OFF \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=OFF \ + && cmake --build build --target fun +else + echo "Build target $target not found... aborting!"; +fi From 1eacf9411c67ac4f4441f5bbcdb19056763f7aae Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 12 Dec 2025 04:52:18 +0100 Subject: [PATCH 52/55] ./make script update. (0.37.10) --- CMakeLists.txt | 2 +- make | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad6be87..faa6517 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.9 LANGUAGES C) +project(fun VERSION 0.37.10 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/make b/make index 3009268..b8cc7b7 100755 --- a/make +++ b/make @@ -153,4 +153,13 @@ elif [ "$target" = "repl" ]; then && cmake --build build --target fun else echo "Build target $target not found... aborting!"; + echo "Available targets:"; + echo " - all"; + echo " - alpine"; + echo " - debug"; + echo " - debug_all"; + echo " - freebsd"; + echo " - minimal"; + echo " - musl"; + echo " - repl"; fi From 87f540797b973dab4d3e75055f7851cb3f9e2e2a Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 13 Dec 2025 01:14:17 +0100 Subject: [PATCH 53/55] Added an OS based random number generator (RNG). (0.37.11) --- CMakeLists.txt | 2 +- examples/{ => error}/byte_for_demo.fun | 0 examples/random_demo.fun | 0 examples/random_number_example.fun | 43 ++++++++ make | 36 +++---- src/bytecode.c | 1 + src/bytecode.h | 1 + src/fun.c | 3 +- src/parser.c | 10 ++ src/vm.c | 1 + src/vm.h | 1 + src/vm/os/random_number.c | 139 +++++++++++++++++++++++++ 12 files changed, 216 insertions(+), 21 deletions(-) rename examples/{ => error}/byte_for_demo.fun (100%) mode change 100644 => 100755 examples/random_demo.fun create mode 100755 examples/random_number_example.fun create mode 100644 src/vm/os/random_number.c diff --git a/CMakeLists.txt b/CMakeLists.txt index faa6517..0383cff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.10 LANGUAGES C) +project(fun VERSION 0.37.11 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/byte_for_demo.fun b/examples/error/byte_for_demo.fun similarity index 100% rename from examples/byte_for_demo.fun rename to examples/error/byte_for_demo.fun diff --git a/examples/random_demo.fun b/examples/random_demo.fun old mode 100644 new mode 100755 diff --git a/examples/random_number_example.fun b/examples/random_number_example.fun new file mode 100755 index 0000000..031d0dd --- /dev/null +++ b/examples/random_number_example.fun @@ -0,0 +1,43 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-12 + */ + +/* + * Example: Using OS-based random_number(len) builtin + * This generates cryptographically strong random bytes and returns them + * hex-encoded as a string of length 2*len (since each byte -> two hex chars). + */ + +print("--- random_number(len) demo ---") + +len_bytes = 16 +hexstr = random_number(len_bytes) +print("Requested bytes: " + to_string(len_bytes)) +print("Hex string: " + hexstr) +print("Hex length (should be 2*bytes = 32): " + to_string(len(hexstr))) + +// Zero length returns empty string +empty = random_number(0) +print("Empty (0 bytes) -> length: " + to_string(len(empty)) + " value: " + to_string(empty)) + +// You can request longer values as needed, e.g., 32 bytes -> 64 hex chars +hex64 = random_number(32) +print("32 bytes -> " + to_string(len(hex64)) + " hex chars") + +/* Possible output: +--- random_number(len) demo --- +Requested bytes: 16 +Hex string: d5f12eb93b71e78cc803aa802e76e3b4 +Hex length (should be 2*bytes = 32): 32 +Empty (0 bytes) -> length: 0 value: +32 bytes -> 64 hex chars +*/ diff --git a/make b/make index b8cc7b7..171c7f6 100755 --- a/make +++ b/make @@ -34,6 +34,23 @@ if [ "$target" = "all" ]; then -DFUN_USE_MUSL=OFF \ -DFUN_DEBUG=OFF \ && cmake --build build --target fun +elif [ "$target" = "all_debug" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=ON \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ + -DFUN_WITH_SQLITE=ON \ + -DFUN_WITH_CURL=ON \ + -DFUN_WITH_PCRE2=ON \ + -DFUN_WITH_XML2=ON \ + -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ + -DFUN_WITH_INI=ON \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=ON \ + && cmake --build build --target fun elif [ "$target" = "alpine" ]; then rm -rf build \ && cmake -S . -B build \ @@ -67,23 +84,6 @@ elif [ "$target" = "debug" ]; then -DFUN_USE_MUSL=OFF \ -DFUN_DEBUG=ON \ && cmake --build build --target fun -elif [ "$target" = "debug_all" ]; then - rm -rf build \ - && cmake -S . -B build \ - -DFUN_WITH_PCSC=ON \ - -DFUN_WITH_REPL=ON \ - -DFUN_WITH_LIBSQL=ON \ - -DFUN_WITH_SQLITE=ON \ - -DFUN_WITH_CURL=ON \ - -DFUN_WITH_PCRE2=ON \ - -DFUN_WITH_XML2=ON \ - -DFUN_WITH_JSON=ON \ - -DFUN_WITH_TCLTK=ON \ - -DFUN_WITH_INI=ON \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ - -DFUN_DEBUG=ON \ - && cmake --build build --target fun elif [ "$target" = "freebsd" ]; then rm -rf build \ && cmake -S . -B build \ @@ -155,9 +155,9 @@ else echo "Build target $target not found... aborting!"; echo "Available targets:"; echo " - all"; + echo " - all_debug"; echo " - alpine"; echo " - debug"; - echo " - debug_all"; echo " - freebsd"; echo " - minimal"; echo " - musl"; diff --git a/src/bytecode.c b/src/bytecode.c index 647c365..6bf4c6e 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -134,6 +134,7 @@ static const char *opcode_name(OpCode op) { case OP_THREAD_SPAWN: return "THREAD_SPAWN"; case OP_THREAD_JOIN: return "THREAD_JOIN"; case OP_SLEEP_MS: return "SLEEP_MS"; + case OP_RANDOM_NUMBER: return "RANDOM_NUMBER"; case OP_BAND: return "BAND"; case OP_BOR: return "BOR"; case OP_BXOR: return "BXOR"; diff --git a/src/bytecode.h b/src/bytecode.h index 12986e8..7a14da3 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -131,6 +131,7 @@ typedef enum { OP_THREAD_SPAWN, // operand: 0=no args, 1=has args; pops [args?], fn; pushes thread id (int>0) OP_THREAD_JOIN, // pops thread id; waits; pushes result value (or Nil) OP_SLEEP_MS, // pops milliseconds; sleeps; pushes Nil (for statement POP safety) + OP_RANDOM_NUMBER, // pops length; pushes hex string of that length from OS RNG (hex-encoded) // Bitwise (32-bit) and shifts/rotates OP_BAND, // pops b, a; pushes (uint32_t)(a & b) diff --git a/src/fun.c b/src/fun.c index b614452..afd7305 100644 --- a/src/fun.c +++ b/src/fun.c @@ -5,12 +5,11 @@ */ #include "bytecode.h" -#include "value.h" #include "vm.h" #include "parser.h" +#include #include #include -#include #ifdef FUN_WITH_REPL #include "repl.h" diff --git a/src/parser.c b/src/parser.c index b665f16..fbc3758 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1649,6 +1649,16 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) return 1; } + if (strcmp(name, "random_number") == 0) { + (*pos)++; /* '(' */ + /* expects exactly 1 arg: length */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "random_number expects 1 arg (length)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after random_number arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_RANDOM_NUMBER, 0); + free(name); + return 1; + } + /* threading */ if (strcmp(name, "thread_spawn") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index 7991a2d..d7fb110 100644 --- a/src/vm.c +++ b/src/vm.c @@ -666,6 +666,7 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/time_now_ms.c" #include "vm/os/clock_mono_ms.c" #include "vm/os/date_format.c" + #include "vm/os/random_number.c" /* Socket ops */ #include "vm/os/socket_tcp_listen.c" diff --git a/src/vm.h b/src/vm.h index df2c0b7..a69b0a4 100644 --- a/src/vm.h +++ b/src/vm.h @@ -37,6 +37,7 @@ static const char *opcode_names[] = { "READ_FILE","WRITE_FILE","ENV","INPUT_LINE","PROC_RUN","PROC_SYSTEM", "TIME_NOW_MS","CLOCK_MONO_MS","DATE_FORMAT", "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", + "RANDOM_NUMBER", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", "CURL_GET","CURL_POST","CURL_DOWNLOAD", diff --git a/src/vm/os/random_number.c b/src/vm/os/random_number.c new file mode 100644 index 0000000..36e3dab --- /dev/null +++ b/src/vm/os/random_number.c @@ -0,0 +1,139 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-12 + */ + +/* Generate OS-based random bytes and return them hex-encoded. + * Opcode: OP_RANDOM_NUMBER + * Stack: pops len (number of raw bytes), pushes hex string of length 2*len + */ + +#include +#include +#include +#include +#include + +/* Platform-specific headers guarded per OS to avoid leaking problematic macros */ +#if defined(_WIN32) || defined(_WIN64) + #include + #include +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + #include +#elif defined(__unix__) + #if __has_include() + #include + #endif + #include + #include +#endif + +case OP_RANDOM_NUMBER: { + /* pop requested raw byte length */ + Value lv = pop_value(vm); + if (lv.type != VAL_INT) { + fprintf(stderr, "Runtime type error: random_number(len) expects integer length\n"); + free_value(lv); + /* For safety, push empty string so callers expecting a value won't underflow */ + push_value(vm, make_string("")); + break; + } + int64_t len = lv.i; + if (len < 0) { + fprintf(stderr, "random_number error: negative length (%" PRId64 ")\n", len); + free_value(lv); + push_value(vm, make_string("")); + break; + } + if (len == 0) { + free_value(lv); + push_value(vm, make_string("")); + break; + } + + /* Cap to prevent excessive allocations (max 1 MiB raw -> 2 MiB hex) */ + const int64_t MAX_RAW = (1LL << 20); + if (len > MAX_RAW) { + fprintf(stderr, "random_number error: requested length too large (%" PRId64 ", max %" PRId64 ")\n", len, MAX_RAW); + free_value(lv); + push_value(vm, make_string("")); + break; + } + + unsigned char *raw = (unsigned char*)malloc((size_t)len); + char *hex = (char*)malloc((size_t)len * 2 + 1); + if (!raw || !hex) { + if (raw) free(raw); + if (hex) free(hex); + free_value(lv); + fprintf(stderr, "Out of memory in random_number\n"); + exit(1); + } + + int ok = 0; + + /* --- Fill raw with cryptographically secure random bytes from the OS --- */ +#if defined(_WIN32) || defined(_WIN64) + { + /* Windows: use BCryptGenRandom */ + NTSTATUS st = BCryptGenRandom(NULL, raw, (ULONG)len, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + ok = (st == 0); + } +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + { + arc4random_buf(raw, (size_t)len); + ok = 1; + } +#elif defined(__unix__) + { + /* Prefer getrandom if available, otherwise /dev/urandom */ + #if __has_include() + ssize_t n = getrandom(raw, (size_t)len, 0); + ok = (n == (ssize_t)len); + #else + ok = 0; + #endif + if (!ok) { + int fd = open("/dev/urandom", O_RDONLY); + if (fd >= 0) { + size_t off = 0; ssize_t n; + while (off < (size_t)len && (n = read(fd, raw + off, (size_t)len - off)) > 0) off += (size_t)n; + close(fd); + ok = (off == (size_t)len); + } + } + } +#else + ok = 0; +#endif + + if (!ok) { + free(raw); + free(hex); + free_value(lv); + fprintf(stderr, "random_number error: OS RNG unavailable or failed\n"); + exit(1); + } + + /* hex encode */ + static const char hexdig[] = "0123456789abcdef"; + for (int64_t i = 0; i < len; ++i) { + unsigned char b = raw[i]; + hex[2*i] = hexdig[(b >> 4) & 0xF]; + hex[2*i+1] = hexdig[b & 0xF]; + } + hex[len * 2] = '\0'; + + Value s = make_string(hex); + free(raw); + free(hex); + free_value(lv); + push_value(vm, s); + break; +} From b16a42a4a73aacfbf4e498c2a28fbd13297df702 Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 13 Dec 2025 02:24:23 +0100 Subject: [PATCH 54/55] Some echo()/print() fixes. (0.37.12) --- CMakeLists.txt | 2 +- examples/random_number_example.fun | 9 ++++++++- src/vm.c | 9 +++++++++ src/vm.h | 1 + src/vm/echo.c | 16 +++++++++++++--- src/vm/print.c | 5 ++++- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0383cff..b8ce13d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.11 LANGUAGES C) +project(fun VERSION 0.37.12 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/random_number_example.fun b/examples/random_number_example.fun index 031d0dd..9ad5213 100755 --- a/examples/random_number_example.fun +++ b/examples/random_number_example.fun @@ -17,12 +17,17 @@ * hex-encoded as a string of length 2*len (since each byte -> two hex chars). */ +#include + print("--- random_number(len) demo ---") len_bytes = 16 hexstr = random_number(len_bytes) print("Requested bytes: " + to_string(len_bytes)) print("Hex string: " + hexstr) +print("Hex bytes array: " + to_string(hex_to_bytes(hexstr))) +echo("Hex dump to bytes: ") +print(hex_to_bytes(hexstr)) print("Hex length (should be 2*bytes = 32): " + to_string(len(hexstr))) // Zero length returns empty string @@ -36,7 +41,9 @@ print("32 bytes -> " + to_string(len(hex64)) + " hex chars") /* Possible output: --- random_number(len) demo --- Requested bytes: 16 -Hex string: d5f12eb93b71e78cc803aa802e76e3b4 +Hex string: 8497373c7fb52c6cb7f1e1fda5bb6a60 +Hex bytes array: [array n=16] +Hex dump to bytes: [132, 151, 55, 60, 127, 181, 44, 108, 183, 241, 225, 253, 165, 187, 106, 96] Hex length (should be 2*bytes = 32): 32 Empty (0 bytes) -> length: 0 value: 32 bytes -> 64 hex chars diff --git a/src/vm.c b/src/vm.c index d7fb110..dd96216 100644 --- a/src/vm.c +++ b/src/vm.c @@ -269,6 +269,8 @@ void vm_clear_output(VM *vm) { free_value(vm->output[i]); } vm->output_count = 0; + // reset partial flags + for (int i = 0; i < OUTPUT_SIZE; ++i) vm->output_is_partial[i] = 0; } void vm_free(VM *vm) { @@ -420,6 +422,7 @@ void vm_init(VM *vm) { vm->sp = -1; vm->fp = -1; vm->output_count = 0; + for (int i = 0; i < OUTPUT_SIZE; ++i) vm->output_is_partial[i] = 0; vm->instr_count = 0; vm->exit_code = 0; vm->trace_enabled = 0; @@ -475,6 +478,12 @@ static void vm_pop_frame(VM *vm) { void vm_print_output(VM *vm) { for (int i = 0; i < vm->output_count; ++i) { print_value(&vm->output[i]); + if (!vm->output_is_partial[i]) { + printf("\n"); + } + } + /* If the last item was partial (from echo), terminate the line for cleanliness */ + if (vm->output_count > 0 && vm->output_is_partial[vm->output_count - 1]) { printf("\n"); } } diff --git a/src/vm.h b/src/vm.h index a69b0a4..6ee2f7f 100644 --- a/src/vm.h +++ b/src/vm.h @@ -72,6 +72,7 @@ struct VM { Value output[OUTPUT_SIZE]; // store printed values int output_count; + int output_is_partial[OUTPUT_SIZE]; // 1 when the corresponding output entry should not end with newline (echo) long long instr_count; // executed instructions in the last vm_run diff --git a/src/vm/echo.c b/src/vm/echo.c index 626bd7d..0a18101 100644 --- a/src/vm/echo.c +++ b/src/vm/echo.c @@ -1,12 +1,22 @@ /** * Implements OP_ECHO: print top-of-stack value without trailing newline. - * Does not store into VM output buffer; writes directly to stdout and flushes. + * Now stores the value into the VM's output buffer and marks it as partial, + * so the CLI can render echo output together with following print output. */ case OP_ECHO: { Value v = pop_value(vm); - print_value(&v); - fflush(stdout); + Value snap = deep_copy_value(&v); free_value(v); + if (vm->output_count < OUTPUT_SIZE) { + int idx = vm->output_count; + vm->output[idx] = snap; + vm->output_is_partial[idx] = 1; // ECHO does not end the line + vm->output_count++; + } else { + free_value(snap); + fprintf(stderr, "Runtime error: output buffer overflow\n"); + exit(1); + } break; } diff --git a/src/vm/print.c b/src/vm/print.c index aee24f9..a50de0f 100644 --- a/src/vm/print.c +++ b/src/vm/print.c @@ -32,7 +32,10 @@ case OP_PRINT: { Value snap = deep_copy_value(&v); free_value(v); if (vm->output_count < OUTPUT_SIZE) { - vm->output[vm->output_count++] = snap; + int idx = vm->output_count; + vm->output[idx] = snap; + vm->output_is_partial[idx] = 0; // PRINT terminates the line + vm->output_count++; } else { free_value(snap); fprintf(stderr, "Runtime error: output buffer overflow\n"); From d2fca3275a5c68d0a559941c5efb843558debe7f Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 13 Dec 2025 03:25:34 +0100 Subject: [PATCH 55/55] Some more Fun. Not fully tested! (0.37.13) --- CMakeLists.txt | 2 +- examples/conversions_showcase.fun | 163 ++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 examples/conversions_showcase.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index b8ce13d..3f305a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.12 LANGUAGES C) +project(fun VERSION 0.37.13 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/conversions_showcase.fun b/examples/conversions_showcase.fun new file mode 100644 index 0000000..9b48704 --- /dev/null +++ b/examples/conversions_showcase.fun @@ -0,0 +1,163 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-13 + */ + +// conversions_showcase.fun +// Demonstrates Fun's data type conversion features: +// - to_number(x) +// - to_string(x) +// - cast(value, typeName) +// - typeof(x) +// - uclamp(number, bits), sclamp(number, bits) + +print("=== Conversions showcase ===") + +// typeof on core literals +print("typeof(123) -> " + typeof(123)) // Number +print("typeof(\"abc\") -> " + typeof("abc")) // String +print("typeof([1,2]) -> " + typeof([1,2])) // Array +// Use a variable for map to avoid parser ambiguities +mm = { "a": 1 } +print("typeof({\"a\":1}) -> " + typeof(mm)) // Map +print("typeof(0) -> " + typeof(0)) // Number (Boolean is 0/1 as Number) +print("typeof(nil) -> " + typeof(nil)) // Nil + +print("") +print("-- to_number(x) --") +print(to_number(42)) // 42 +print(to_number("123")) // 123 +print(to_number("12x")) // 0 (invalid -> 0) +print(to_number(0)) // 0 +print(to_number(1)) // 1 + +print("") +print("-- to_string(x) --") +print(to_string(42)) // "42" +print(to_string("hi")) // "hi" +print(to_string([1,2,3])) // "[array n=3]" or similar representation +print(to_string({ "k": 7 })) // "{\"k\":7}" or implementation-defined +print(to_string(nil)) // "nil" (implementation-defined) + +print("") +print("-- cast(value, typeName) --") +// Number: parse decimals; invalid -> 0 +print(cast("123", "Number")) +print(cast("12x", "Number")) + +// String: stringify +print(cast(100, "String")) +print(typeof(cast(100, "String"))) + +// Boolean: 0 -> 0, non-zero -> 1 +print(cast(0, "Boolean")) +print(cast(42, "Boolean")) + +// Array: others get wrapped +a = cast(42, "Array") +print(typeof(a)) +print(len(a)) +print(a[0]) + +// Map: non-maps become empty map +m = cast(42, "Map") +print(typeof(m)) + +// Nil: always Nil +n = cast("x", "Nil") +print(typeof(n)) + +// Function: non-functions -> Nil +fun foo() + return 7 +print(typeof(cast(foo, "Function"))) +print(typeof(cast(42, "Function"))) + +print("") +print("-- Integer width ranges via typed variables --") + +// Unsigned 8-bit: values clamped to [0..255] +uint8 u8 = 0 +u8 = 255 +print(u8) // -> 255 +// u8 = -1 // Uncomment to see OverflowError: value out of range for uint8 +// u8 = 300 // Uncomment to see OverflowError: value out of range for uint8 + +// Signed 8-bit: values clamped to [-128..127] +int8 s8 = 0 +s8 = -128 +print(s8) // -> -128 +s8 = 127 +print(s8) // -> 127 +// s8 = -200 // Uncomment to see OverflowError for int8 +// s8 = 200 // Uncomment to see OverflowError for int8 + +// 16-bit examples +uint16 u16 = 0 +u16 = 65535 +print(u16) // -> 65535 +// u16 = 70000 // Uncomment to see OverflowError for uint16 + +int16 s16 = 0 +s16 = -32768 +print(s16) // -> -32768 +s16 = 32767 +print(s16) // -> 32767 + +print("=== Done ===") + +/* Expected output: +=== Conversions showcase === +typeof(123) -> Number +typeof("abc") -> String +typeof([1,2]) -> Array +typeof({"a":1}) -> String +typeof(0) -> Number +typeof(nil) -> Nil + +-- to_number(x) -- +42 +123 +0 +0 +1 + +-- to_string(x) -- +42 +hi +[array n=3] +{map n=1} +nil + +-- cast(value, typeName) -- +123 +0 +100 +String +0 +1 +Array +1 +42 +String +Nil +Function +Nil + +-- Integer width ranges via typed variables -- +255 +-128 +127 +65535 +-32768 +32767 +=== Done === +*/