From be590414af8fbac4c3e837ff152d4f4e234e1ad3 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 7 May 2026 21:54:05 +0200 Subject: [PATCH] Refactored the PCRE2 extension plus adding some regex examples using this extension. (0.41.7) --- CMakeLists.txt | 2 +- Doxyfile | 2 +- examples/extensions/pcre2/pcre2_demo.fun | 208 +++++++++++++++-- examples/extensions/pcre2/pcre2_findall.fun | 34 +++ .../extensions/pcre2/pcre2_lib_findall.fun | 38 +++ examples/extensions/pcre2/pcre2_lib_match.fun | 43 ++++ examples/extensions/pcre2/pcre2_lib_test.fun | 36 +++ examples/extensions/pcre2/pcre2_match.fun | 39 ++++ examples/extensions/pcre2/pcre2_opcodes.fun | 214 ----------------- examples/extensions/pcre2/pcre2_showcase.fun | 32 --- examples/extensions/pcre2/pcre2_test.fun | 29 +++ .../{pcre2 => pcsc}/pcsc2_example.fun | 0 lib/regex/pcre2.fun | 5 + src/extensions/pcre2.c | 217 +++++++++++++++++- src/vm/pcre2/findall.c | 63 +---- src/vm/pcre2/match.c | 53 +---- src/vm/pcre2/test.c | 26 +-- 17 files changed, 637 insertions(+), 404 deletions(-) create mode 100644 examples/extensions/pcre2/pcre2_findall.fun create mode 100644 examples/extensions/pcre2/pcre2_lib_findall.fun create mode 100644 examples/extensions/pcre2/pcre2_lib_match.fun create mode 100644 examples/extensions/pcre2/pcre2_lib_test.fun create mode 100644 examples/extensions/pcre2/pcre2_match.fun delete mode 100755 examples/extensions/pcre2/pcre2_opcodes.fun delete mode 100755 examples/extensions/pcre2/pcre2_showcase.fun create mode 100644 examples/extensions/pcre2/pcre2_test.fun rename examples/extensions/{pcre2 => pcsc}/pcsc2_example.fun (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index c7850dd..48704bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.41.6 LANGUAGES C) +project(fun VERSION 0.41.7 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/Doxyfile b/Doxyfile index cc4e508..d2e7c9b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "Fun API Documentation" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 0.41.5 +PROJECT_NUMBER = 0.41.7 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewers a diff --git a/examples/extensions/pcre2/pcre2_demo.fun b/examples/extensions/pcre2/pcre2_demo.fun index 8ee2752..66404d0 100755 --- a/examples/extensions/pcre2/pcre2_demo.fun +++ b/examples/extensions/pcre2/pcre2_demo.fun @@ -11,23 +11,203 @@ * Added: 2025-11-25 */ -/* -include +// PCRE2 example using VM builtins directly (no class wrapper) +// Requires building Fun with -DFUN_WITH_PCRE2=ON -rx = Pcre2() +print("-- PCRE2 builtins (no class) --") -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,}" +pattern = "(\\w+)" // capture a word +text = "Hello 123 world" -print("Has email? ", rx.test(pattern, text, rx.i())) +// Flags: 1=I, 2=M, 4=S, 8=U (UTF), 16=X; we’ll use UTF by default +flags = 8 -first = rx.match(pattern, text, rx.i()) -if first != nil { - print("First: ", first["full"]) -} +print("test:") +print(pcre2_test(pattern, text, flags)) -all = rx.find_all(pattern, text, rx.i()) -for m in all { - print("Found: ", m["full"]) -} +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"]) + +/* Expected output: +-- PCRE2 builtins (no class) -- +test: +1 +first full: +Hello +span: +0 +.. +5 +groups count: +1 +all: +Hello +@ +0 +.. +5 +all: +123 +@ +6 +.. +9 +all: +world +@ +10 +.. +15 + +-- More regex demos -- +Emails (findall): +one@example.com +Two@Example.COM +URLs: +IPv4: +8.8.8.8 +192.168.0.1 +Dates (with groups Y/M/D): +1999-12-31 +1999/12/31 +2025-11-25 +2025/11/25 +Hex colors: +#FF00FF +#1a2b3c +Quoted strings (with escapes): +"hi there" +"indented" +"quote\"inside" +Multiline ^ anchors (first token of each line): +first +Second +third +Dotall OFF (should fail): +0 +Dotall ON (should match): +1 +Word boundary, case-insensitive: +The +the +THE +Lookahead (word before number): +foo +baz +Greedy: +onetwo +Non-greedy: +one +two */ diff --git a/examples/extensions/pcre2/pcre2_findall.fun b/examples/extensions/pcre2/pcre2_findall.fun new file mode 100644 index 0000000..365920e --- /dev/null +++ b/examples/extensions/pcre2/pcre2_findall.fun @@ -0,0 +1,34 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-05-07 + */ + +// Minimal example showing only the pcre2_findall opcode +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +pattern = "[A-Za-z]+" +text = "One two THREE four" + +// 1 = I (ignore case) so we catch mixed case words uniformly +flags = 1 + +print("pcre2_findall example:") +matches = pcre2_findall(pattern, text, flags) +for m in matches + print(m["full"]) // prints matched word + +/* Expected output: +pcre2_findall example: +One +two +THREE +four +*/ diff --git a/examples/extensions/pcre2/pcre2_lib_findall.fun b/examples/extensions/pcre2/pcre2_lib_findall.fun new file mode 100644 index 0000000..3b7d397 --- /dev/null +++ b/examples/extensions/pcre2/pcre2_lib_findall.fun @@ -0,0 +1,38 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-05-07 + */ + +// Example: Using stdlib PCRE2 class to find all matches +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +#include + +rx = PCRE2() + +pattern = "[A-Za-z]+" +text = "One two THREE four" + +// Case-insensitive via class flag helper +flags = rx.i() + +print("pcre2_lib_findall example:") +matches = rx.find_all(pattern, text, flags) +for m in matches + print(m["full"]) // prints matched word + +/* Expected output: +pcre2_lib_findall examples: +One +two +THREE +four +*/ diff --git a/examples/extensions/pcre2/pcre2_lib_match.fun b/examples/extensions/pcre2/pcre2_lib_match.fun new file mode 100644 index 0000000..269846c --- /dev/null +++ b/examples/extensions/pcre2/pcre2_lib_match.fun @@ -0,0 +1,43 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-05-07 + */ + +// Example: Using stdlib PCRE2 class to perform a single match +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +#include + +rx = PCRE2() + +pattern = "(foo)-(\\d+)" +text = "bar foo-123 baz" + +// Default UTF flag is fine; show combining flags as well (UTF | I) +flags = bor(rx.u(), rx.i()) + +print("pcre2_lib_match example:") +m = rx.match(pattern, text, flags) +if (m != nil) + print(m["full"]) // foo-123 + print(m["start"]) // start index + print(m["end"]) // end index (exclusive) + print(len(m["groups"])) // 2 groups captured +else + print("no match") + +/* Expected output: +pcre2_lib_match examples: +foo-123 +4 +11 +2 +*/ diff --git a/examples/extensions/pcre2/pcre2_lib_test.fun b/examples/extensions/pcre2/pcre2_lib_test.fun new file mode 100644 index 0000000..edae0ad --- /dev/null +++ b/examples/extensions/pcre2/pcre2_lib_test.fun @@ -0,0 +1,36 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-05-07 + */ + +// Example: Using stdlib PCRE2 class to run a simple test +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +#include + +rx = PCRE2() + +pattern = "^hello$" +text = "Hello" // mismatches unless case-insensitive is set + +// Use case-insensitive flag from the class helper +flags = rx.i() + +print("pcre2_lib_test example:") +print(rx.test(pattern, text, flags)) // prints 1 on match, 0 otherwise + +/* Expected output: +pcre2_lib_test example: +foo-123 +4 +11 +2 +*/ diff --git a/examples/extensions/pcre2/pcre2_match.fun b/examples/extensions/pcre2/pcre2_match.fun new file mode 100644 index 0000000..64cf8ba --- /dev/null +++ b/examples/extensions/pcre2/pcre2_match.fun @@ -0,0 +1,39 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-05-07 + */ + +// Minimal example showing only the pcre2_match opcode +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +pattern = "(foo)-(\\d+)" +text = "bar foo-123 baz" + +// 8 = UTF flag is harmless here; kept for consistency +flags = 8 + +print("pcre2_match example:") +m = pcre2_match(pattern, text, flags) +if (m != nil) + print(m["full"]) // foo-123 + print(m["start"]) // start index + print(m["end"]) // end index (exclusive) + print(len(m["groups"])) // 2 groups captured: foo and 123 +else + print("no match") + +/* Expected output: +pcre2_match example: +foo-123 +4 +11 +2 +*/ diff --git a/examples/extensions/pcre2/pcre2_opcodes.fun b/examples/extensions/pcre2/pcre2_opcodes.fun deleted file mode 100755 index 1e6a94e..0000000 --- a/examples/extensions/pcre2/pcre2_opcodes.fun +++ /dev/null @@ -1,214 +0,0 @@ -#!/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"]) - -/* Expected output: --- PCRE2 builtins (no class) -- -test: -1 -first full: -Hello -span: -0 -.. -5 -groups count: -1 -all: -Hello -@ -0 -.. -5 -all: -123 -@ -6 -.. -9 -all: -world -@ -10 -.. -15 - --- More regex demos -- -Emails (findall): -one@example.com -Two@Example.COM -URLs: -IPv4: -8.8.8.8 -192.168.0.1 -Dates (with groups Y/M/D): -1999-12-31 -1999/12/31 -2025-11-25 -2025/11/25 -Hex colors: -#FF00FF -#1a2b3c -Quoted strings (with escapes): -"hi there" -"indented" -"quote\"inside" -Multiline ^ anchors (first token of each line): -first -Second -third -Dotall OFF (should fail): -0 -Dotall ON (should match): -1 -Word boundary, case-insensitive: -The -the -THE -Lookahead (word before number): -foo -baz -Greedy: -onetwo -Non-greedy: -one -two -*/ - diff --git a/examples/extensions/pcre2/pcre2_showcase.fun b/examples/extensions/pcre2/pcre2_showcase.fun deleted file mode 100755 index 2df2f6a..0000000 --- a/examples/extensions/pcre2/pcre2_showcase.fun +++ /dev/null @@ -1,32 +0,0 @@ -#!/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/examples/extensions/pcre2/pcre2_test.fun b/examples/extensions/pcre2/pcre2_test.fun new file mode 100644 index 0000000..55380a3 --- /dev/null +++ b/examples/extensions/pcre2/pcre2_test.fun @@ -0,0 +1,29 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-05-07 + */ + +// Minimal example showing only the pcre2_test opcode +// Requires building Fun with -DFUN_WITH_PCRE2=ON + +pattern = "^hello$" +text = "Hello" // mismatches unless case-insensitive is set + +// Flags: 1=I (ignore case), 2=M, 4=S, 8=U (UTF), 16=X +flags = 1 // make it case-insensitive so it matches + +print("pcre2_test example:") +print(pcre2_test(pattern, text, flags)) // prints 1 on match, 0 otherwise + +/* Expected output: +pcre2_test example: +1 +*/ diff --git a/examples/extensions/pcre2/pcsc2_example.fun b/examples/extensions/pcsc/pcsc2_example.fun similarity index 100% rename from examples/extensions/pcre2/pcsc2_example.fun rename to examples/extensions/pcsc/pcsc2_example.fun diff --git a/lib/regex/pcre2.fun b/lib/regex/pcre2.fun index 7133a34..335c2a2 100644 --- a/lib/regex/pcre2.fun +++ b/lib/regex/pcre2.fun @@ -15,14 +15,19 @@ // 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 diff --git a/src/extensions/pcre2.c b/src/extensions/pcre2.c index 21e92ba..4390606 100644 --- a/src/extensions/pcre2.c +++ b/src/extensions/pcre2.c @@ -9,10 +9,43 @@ /** * @file pcre2.c - * @brief PCRE2 configuration header and includes for regex-related opcodes. + * @brief PCRE2 helpers for Fun VM extension opcodes (conditional build). * - * Ensures PCRE2 code unit width is defined consistently prior to including - * when FUN_WITH_PCRE2 is enabled. + * This module centralizes the concrete PCRE2 implementation so VM opcodes in + * src/vm/pcre2/*.c only perform stack marshalling and delegate to these + * helpers. This mirrors the approach used by other extensions (e.g. SQLite, + * XML2) where the heavy lifting lives under src/extensions/ and the opcodes + * just call into small C helpers. + * + * Build-time feature flag: + * - The code in this file is compiled only when FUN_WITH_PCRE2 is enabled. + * When disabled, PCRE2-dependent opcodes are built with no-op fallbacks. + * + * PCRE2 width configuration: + * - PCRE2 requires defining PCRE2_CODE_UNIT_WIDTH before including to + * select 8/16/32-bit code units. We select 8-bit here. Because the Fun VM + * translates many opcode .c files into the same translation unit, it is + * important this macro is defined exactly once before the first + * inclusion. This file ensures that when FUN_WITH_PCRE2 is enabled. + * + * Flags mapping used by helpers/opcodes (bitmask in the VM): + * - 1 -> PCRE2_CASELESS ("i") + * - 2 -> PCRE2_MULTILINE ("m") + * - 4 -> PCRE2_DOTALL ("s") + * - 8 -> PCRE2_UTF ("u") + * - 16 -> PCRE2_EXTENDED ("x") + * + * Value and memory ownership: + * - The Value type and helper functions (make_map_empty, make_array_*, map_set, + * array_push, make_int, make_string, make_nil, string_substr, etc.) are + * provided by the Fun VM and are declared in the including translation unit. + * The arrays/maps returned from this module are owned by the caller (the + * VM opcode), consistent with other extension helpers. + * + * Thread-safety: + * - These helpers are not inherently thread-safe, but they do not maintain any + * internal state beyond stack-local variables. Coordinate usage externally if + * the embedding is multi-threaded. */ /* Ensure PCRE2 is configured consistently across the whole translation unit. @@ -25,4 +58,180 @@ #define PCRE2_CODE_UNIT_WIDTH 8 #endif #include -#endif +#include + +/** + * @brief Map Fun VM regex flags to PCRE2 compile options. + * + * The Fun VM passes a small integer bitmask controlling common regex + * behaviours. This function translates those bits into the corresponding + * PCRE2 compile options. + * + * Bit mapping: + * - 1 -> PCRE2_CASELESS (case-insensitive) + * - 2 -> PCRE2_MULTILINE (^ and $ match start/end of line) + * - 4 -> PCRE2_DOTALL (dot matches newlines) + * - 8 -> PCRE2_UTF (treat pattern/subject as UTF-8) + * - 16 -> PCRE2_EXTENDED (ignore unescaped whitespace and allow comments) + * + * @param flags Bitmask provided by the VM. + * @return uint32_t PCRE2 options suitable for pcre2_compile(). + */ +static uint32_t fun_pcre2_opts_from_flags(int flags) { + 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 */ + return opt; +} + +/** + * @brief Test whether a pattern matches a subject at least once. + * + * Compiles the given pattern with options derived from the flags bitmask and + * runs pcre2_match() once starting at offset 0. + * + * @param pattern NUL-terminated regex pattern string. + * @param subject NUL-terminated subject string. + * @param flags VM bitmask translated by fun_pcre2_opts_from_flags(). + * @return int 1 if pcre2_match() returns a non-negative value; 0 if there is + * no match or an error occurs (including compile error or OOM). + * + * @note This helper performs only a single match attempt at offset 0; it does + * not search for subsequent matches. Use fun_pcre2_findall() for that. + */ +static int fun_pcre2_test(const char *pattern, const char *subject, int flags) { + if (!pattern || !subject) return 0; + int errorcode = 0; PCRE2_SIZE erroff = 0; + uint32_t opt = fun_pcre2_opts_from_flags(flags); + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, opt, &errorcode, &erroff, NULL); + if (!re) return 0; + 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); + return rc >= 0 ? 1 : 0; +} + +/* Build a Value API is provided by the VM; declarations are in the including TU. */ +/** + * @brief Match a pattern once and return a structured result map. + * + * On success, returns a map with the following keys: + * - "full" -> string: the matched substring for group 0 + * - "start" -> int: start index (0-based) of the match in the subject + * - "end" -> int: end index (exclusive) + * - "groups" -> array: strings for each captured group (1..n), empty if none + * + * On no match, pattern compile failure, or memory allocation error, returns + * Nil. + * + * @param pattern NUL-terminated regex pattern string. + * @param subject NUL-terminated subject string. + * @param flags VM bitmask translated by fun_pcre2_opts_from_flags(). + * @return Value A VM map Value as described above, or Nil on failure. + * + * @see fun_pcre2_findall() + */ +static Value fun_pcre2_match(const char *pattern, const char *subject, int flags) { + if (!pattern || !subject) return make_nil(); + int errorcode = 0; PCRE2_SIZE erroff = 0; + uint32_t opt = fun_pcre2_opts_from_flags(flags); + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, opt, &errorcode, &erroff, NULL); + if (!re) return make_nil(); + 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); + return make_nil(); + } + PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata); + 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)); + 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); + return res; +} + +/** + * @brief Find all non-overlapping matches of a pattern in a subject. + * + * Scans the subject from left to right and appends, for each non-overlapping + * match, a map with the same shape as fun_pcre2_match() to the result array. + * If the engine reports an empty match (start == end), the scan advances by a + * single code unit to prevent infinite loops. + * + * On pattern compile failure or allocation error, returns an empty array. + * + * @param pattern NUL-terminated regex pattern string. + * @param subject NUL-terminated subject string. + * @param flags VM bitmask translated by fun_pcre2_opts_from_flags(). + * @return Value An array of match maps; may be empty when no matches are found + * or on error. + * + * @see fun_pcre2_match() + */ +static Value fun_pcre2_findall(const char *pattern, const char *subject, int flags) { + Value out = make_array_from_values(NULL, 0); + if (!pattern || !subject) return out; + int errorcode = 0; PCRE2_SIZE erroff = 0; + uint32_t opt = fun_pcre2_opts_from_flags(flags); + pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, opt, &errorcode, &erroff, NULL); + if (!re) return out; + pcre2_match_data *mdata = pcre2_match_data_create_from_pattern(re, NULL); + size_t subj_len = strlen(subject); + size_t start_off = 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]; + 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 safely (guard against empty match). */ + if (e0 == s0) { + if ((size_t)e0 < subj_len) start_off = e0 + 1; else break; + } else { + start_off = e0; + } + } + pcre2_match_data_free(mdata); + pcre2_code_free(re); + return out; +} +#endif /* FUN_WITH_PCRE2 */ diff --git a/src/vm/pcre2/findall.c b/src/vm/pcre2/findall.c index 0915312..7a5cc28 100644 --- a/src/vm/pcre2/findall.c +++ b/src/vm/pcre2/findall.c @@ -60,68 +60,7 @@ case OP_PCRE2_FINDALL: { 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); + Value out = fun_pcre2_findall(pattern, subject, flags); free(pattern); free(subject); push_value(vm, out); diff --git a/src/vm/pcre2/match.c b/src/vm/pcre2/match.c index f69037b..5785878 100644 --- a/src/vm/pcre2/match.c +++ b/src/vm/pcre2/match.c @@ -58,58 +58,7 @@ case OP_PCRE2_MATCH: { 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); + Value res = fun_pcre2_match(pattern, subject, flags); free(pattern); free(subject); push_value(vm, res); diff --git a/src/vm/pcre2/test.c b/src/vm/pcre2/test.c index c4dca4c..4235a96 100644 --- a/src/vm/pcre2/test.c +++ b/src/vm/pcre2/test.c @@ -53,32 +53,10 @@ case OP_PCRE2_TEST: { 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); + int rc = fun_pcre2_test(pattern, subject, flags); free(pattern); free(subject); - push_value(vm, make_int(rc >= 0 ? 1 : 0)); + push_value(vm, make_int(rc)); #else /* pop args and return 0 when PCRE2 disabled */ Value a = pop_value(vm);