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; +}