From d9804d9bf848fcffd97cf8281f643b64ceb244c7 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 25 Nov 2025 22:52:43 +0100 Subject: [PATCH] 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; +}