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