diff --git a/CMakeLists.txt b/CMakeLists.txt index 6947a82..2af9cbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.19.0 LANGUAGES C) +project(fun VERSION 0.20.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/regex_demo.fun b/examples/regex_demo.fun new file mode 100644 index 0000000..8ea3637 --- /dev/null +++ b/examples/regex_demo.fun @@ -0,0 +1,38 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-04 + */ + +// Simple demo of Regex stdlib class. + +include + +r = Regex() + +text = "abc-123-xyz" + +// full match (should be 0) +m1 = r.match(text, "[a-z]+-[0-9]+-[a-z]+$") +print(m1) + +// search +s1 = r.search(text, "[0-9]+") +print(s1) + +// replace digits with # +out = r.replace(text, "[0-9]", "#") +print(out) + +/* Expected output: +1 +{"match": 123, "start": 4, "end": 7, "groups": []} +abc-###-xyz +*/ diff --git a/examples/regex_procedural.fun b/examples/regex_procedural.fun new file mode 100644 index 0000000..7036ceb --- /dev/null +++ b/examples/regex_procedural.fun @@ -0,0 +1,35 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-04 + */ + +// Procedural regex demo using built-in functions. +// Shows internal power without the Regex class wrapper. + +text = "abc-123-xyz" + +// Full string match +m1 = regex_match(text, "[a-z]+-[0-9]+-[a-z]+$") +print(m1) + +// First search with groups (prints a map) +s1 = regex_search(text, "([0-9])([0-9])([0-9])") +print(s1) + +// Global replace: replace digits with '#' +out = regex_replace(text, "[0-9]", "#") +print(out) + +/* Expected output: +1 +{"match": 123, "start": 4, "end": 7, "groups": [1, 2, 3]} +abc-###-xyz +*/ diff --git a/lib/regex.fun b/lib/regex.fun new file mode 100644 index 0000000..fbe0213 --- /dev/null +++ b/lib/regex.fun @@ -0,0 +1,33 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-04 + */ + +// Regex stdlib abstraction over VM regex opcodes. +// Provides simple methods and sensible defaults. + +/* +Methods: +- match(text, pattern) -> 1/0 (full match) +- search(text, pattern) -> map {"match": str, "start": int, "end": int, "groups": array} +- replace(text, pattern, repl) -> string (global) +*/ + +class Regex() + // Full match: returns 1 if the whole string matches the pattern + fun match(this, text, pattern) + return regex_match(to_string(text), to_string(pattern)) + + // First match with groups as array + fun search(this, text, pattern) + return regex_search(to_string(text), to_string(pattern)) + + // Global replace; replacement is literal (no backrefs) + fun replace(this, text, pattern, repl) + return regex_replace(to_string(text), to_string(pattern), to_string(repl)) diff --git a/src/bytecode.c b/src/bytecode.c index 901c67f..6fa5091 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -98,6 +98,9 @@ static const char *opcode_name(OpCode op) { case OP_JOIN: return "JOIN"; case OP_SUBSTR: return "SUBSTR"; case OP_FIND: return "FIND"; + case OP_REGEX_MATCH: return "REGEX_MATCH"; + case OP_REGEX_SEARCH: return "REGEX_SEARCH"; + case OP_REGEX_REPLACE: return "REGEX_REPLACE"; case OP_CONTAINS: return "CONTAINS"; case OP_INDEX_OF: return "INDEX_OF"; case OP_CLEAR: return "CLEAR"; diff --git a/src/bytecode.h b/src/bytecode.h index 082cdb5..63a6059 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -84,6 +84,11 @@ typedef enum { OP_SUBSTR, // pops len, start, string; pushes string OP_FIND, // pops needle, haystack; pushes int index or -1 + // regex ops (POSIX) + OP_REGEX_MATCH, // pops pattern, string; pushes 1/0 for full match + OP_REGEX_SEARCH, // pops pattern, string; pushes map {"match":str, "start":int, "end":int, "groups":array} + OP_REGEX_REPLACE, // pops repl, pattern, string; pushes string with global replacements + // array utils OP_CONTAINS, // pops value, array; pushes 1/0 OP_INDEX_OF, // pops value, array; pushes index or -1 diff --git a/src/parser.c b/src/parser.c index 0f65f01..cb8e537 100644 --- a/src/parser.c +++ b/src/parser.c @@ -800,6 +800,39 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* regex ops */ + if (strcmp(name, "regex_match") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_match expects text"); free(name); return 0; } + if (*pos < len && src[*pos] == ',') { (*pos)++; skip_spaces(src, len, pos); } else { parser_fail(*pos, "regex_match expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_match expects pattern"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after regex_match args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_REGEX_MATCH, 0); + free(name); + return 1; + } + if (strcmp(name, "regex_search") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_search expects text"); free(name); return 0; } + if (*pos < len && src[*pos] == ',') { (*pos)++; skip_spaces(src, len, pos); } else { parser_fail(*pos, "regex_search expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_search expects pattern"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after regex_search args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_REGEX_SEARCH, 0); + free(name); + return 1; + } + if (strcmp(name, "regex_replace") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_replace expects text"); free(name); return 0; } + if (*pos < len && src[*pos] == ',') { (*pos)++; skip_spaces(src, len, pos); } else { parser_fail(*pos, "regex_replace expects 3 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_replace expects pattern"); free(name); return 0; } + if (*pos < len && src[*pos] == ',') { (*pos)++; skip_spaces(src, len, pos); } else { parser_fail(*pos, "regex_replace expects 3 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "regex_replace expects replacement"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after regex_replace args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_REGEX_REPLACE, 0); + free(name); + return 1; + } /* array utils */ if (strcmp(name, "contains") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index 9e06dbe..11495a8 100644 --- a/src/vm.c +++ b/src/vm.c @@ -338,6 +338,9 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/pcsc/transmit.c" #include "vm/strings/find.c" + #include "vm/strings/regex_match.c" + #include "vm/strings/regex_search.c" + #include "vm/strings/regex_replace.c" #include "vm/strings/split.c" #include "vm/strings/substr.c" diff --git a/src/vm.h b/src/vm.h index 7028060..246f158 100644 --- a/src/vm.h +++ b/src/vm.h @@ -29,6 +29,7 @@ static const char *opcode_names[] = { "LEN","PUSH","APOP","SET","INSERT","REMOVE","SLICE", "TO_NUMBER","TO_STRING","CAST","TYPEOF", "SPLIT","JOIN","SUBSTR","FIND", + "REGEX_MATCH","REGEX_SEARCH","REGEX_REPLACE", "CONTAINS","INDEX_OF","CLEAR", "ENUMERATE","ZIP", "MIN","MAX","CLAMP","ABS","POW","RANDOM_SEED","RANDOM_INT", diff --git a/src/vm/strings/regex_match.c b/src/vm/strings/regex_match.c new file mode 100644 index 0000000..43ceacb --- /dev/null +++ b/src/vm/strings/regex_match.c @@ -0,0 +1,57 @@ +/** + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-04 + */ + +/* Regex full-match opcode using POSIX regex */ +#ifdef __unix__ +#include +#endif + +case OP_REGEX_MATCH: { + Value pattern = pop_value(vm); + Value str = pop_value(vm); + if (str.type != VAL_STRING || pattern.type != VAL_STRING) { + fprintf(stderr, "Runtime type error: REGEX_MATCH expects (string, string)\n"); + exit(1); + } +#ifndef __unix__ + /* Not supported on non-UNIX: return 0 gracefully */ + free_value(pattern); + int truth = 0; + free_value(str); + push_value(vm, make_int(truth)); + break; +#else + regex_t rx; + int rc = regcomp(&rx, pattern.s ? pattern.s : "", REG_EXTENDED); + if (rc != 0) { + /* invalid regex -> false */ + free_value(pattern); + free_value(str); + push_value(vm, make_int(0)); + break; + } + regmatch_t m; + int ok = regexec(&rx, str.s ? str.s : "", 1, &m, 0) == 0; + int truth = 0; + if (ok) { + /* full match means the match spans whole string */ + if (m.rm_so == 0 && str.s) { + size_t slen = strlen(str.s); + truth = (m.rm_eo == (regoff_t)slen) ? 1 : 0; + } + } + regfree(&rx); + free_value(pattern); + free_value(str); + push_value(vm, make_int(truth)); + break; +#endif +} diff --git a/src/vm/strings/regex_replace.c b/src/vm/strings/regex_replace.c new file mode 100644 index 0000000..3d29c95 --- /dev/null +++ b/src/vm/strings/regex_replace.c @@ -0,0 +1,116 @@ +/** + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-04 + */ + +/* Regex global replace opcode using POSIX regex */ +#ifdef __unix__ +#include +#include +#include +#endif + +case OP_REGEX_REPLACE: { + Value repl = pop_value(vm); + Value pattern = pop_value(vm); + Value str = pop_value(vm); + if (str.type != VAL_STRING || pattern.type != VAL_STRING || repl.type != VAL_STRING) { + fprintf(stderr, "Runtime type error: REGEX_REPLACE expects (string, string, string)\n"); + exit(1); + } +#ifndef __unix__ + /* Not supported: return original string */ + Value out = make_string(str.s ? str.s : ""); + free_value(repl); + free_value(pattern); + free_value(str); + push_value(vm, out); + break; +#else + regex_t rx; + int rc = regcomp(&rx, pattern.s ? pattern.s : "", REG_EXTENDED); + if (rc != 0) { + /* invalid regex -> return original */ + Value out = make_string(str.s ? str.s : ""); + free_value(repl); + free_value(pattern); + free_value(str); + push_value(vm, out); + break; + } + + const char *s = str.s ? str.s : ""; + const char *r = repl.s ? repl.s : ""; + + size_t out_cap = strlen(s) + 1; + char *outbuf = (char*)malloc(out_cap); + size_t out_len = 0; + size_t pos = 0; + + enum { MAX_CAP = 16 }; + regmatch_t caps[MAX_CAP]; + + while (1) { + if (regexec(&rx, s + pos, MAX_CAP, caps, 0) != 0) { + /* no more matches: append the rest */ + size_t rest = strlen(s + pos); + if (out_len + rest + 1 > out_cap) { + out_cap = out_len + rest + 1; + outbuf = (char*)realloc(outbuf, out_cap); + } + memcpy(outbuf + out_len, s + pos, rest + 1); + out_len += rest; + break; + } + int mstart = (int)caps[0].rm_so; + int mend = (int)caps[0].rm_eo; + if (mstart < 0 || mend < mstart) { + /* Shouldn't happen, avoid infinite loop */ + break; + } + /* append prefix */ + size_t pre_len = (size_t)mstart; + if (out_len + pre_len + 1 > out_cap) { + out_cap = (out_len + pre_len + 1) * 2; + outbuf = (char*)realloc(outbuf, out_cap); + } + memcpy(outbuf + out_len, s + pos, pre_len); + out_len += pre_len; + + /* append replacement (no backref expansion for simplicity) */ + size_t rlen = strlen(r); + if (out_len + rlen + 1 > out_cap) { + out_cap = (out_len + rlen + 1) * 2; + outbuf = (char*)realloc(outbuf, out_cap); + } + memcpy(outbuf + out_len, r, rlen); + out_len += rlen; + + /* advance */ + pos += (size_t)mend; + if (mend == 0) { /* prevent zero-length match infinite loop */ + if (pos < strlen(s)) { + if (out_len + 1 > out_cap) { out_cap = out_len + 2; outbuf = (char*)realloc(outbuf, out_cap);} + outbuf[out_len++] = s[pos++]; + } else { + break; + } + } + } + + Value out = make_string(outbuf ? outbuf : ""); + if (outbuf) free(outbuf); + regfree(&rx); + free_value(repl); + free_value(pattern); + free_value(str); + push_value(vm, out); + break; +#endif +} diff --git a/src/vm/strings/regex_search.c b/src/vm/strings/regex_search.c new file mode 100644 index 0000000..1baf602 --- /dev/null +++ b/src/vm/strings/regex_search.c @@ -0,0 +1,113 @@ +/** + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-04 + */ + +/* Regex search (first match) opcode using POSIX regex */ +#ifdef __unix__ +#include +#include +#include +#endif + +case OP_REGEX_SEARCH: { + Value pattern = pop_value(vm); + Value str = pop_value(vm); + if (str.type != VAL_STRING || pattern.type != VAL_STRING) { + fprintf(stderr, "Runtime type error: REGEX_SEARCH expects (string, string)\n"); + exit(1); + } +#ifndef __unix__ + /* Return default empty result on unsupported platforms */ + Value m = make_map_empty(); + (void)map_set(&m, "match", make_string("")); + (void)map_set(&m, "start", make_int(-1)); + (void)map_set(&m, "end", make_int(-1)); + Value emptyArr = make_array_from_values(NULL, 0); + (void)map_set(&m, "groups", emptyArr); + free_value(pattern); + free_value(str); + push_value(vm, m); + break; +#else + regex_t rx; + int rc = regcomp(&rx, pattern.s ? pattern.s : "", REG_EXTENDED); + if (rc != 0) { + /* invalid regex -> empty result */ + Value m = make_map_empty(); + (void)map_set(&m, "match", make_string("")); + (void)map_set(&m, "start", make_int(-1)); + (void)map_set(&m, "end", make_int(-1)); + Value emptyArr = make_array_from_values(NULL, 0); + (void)map_set(&m, "groups", emptyArr); + free_value(pattern); + free_value(str); + push_value(vm, m); + break; + } + /* capture up to, say, 10 groups (including whole match) */ + enum { MAX_CAP = 16 }; + regmatch_t caps[MAX_CAP]; + int ok = regexec(&rx, str.s ? str.s : "", MAX_CAP, caps, 0) == 0; + Value outMap = make_map_empty(); + if (!ok) { + (void)map_set(&outMap, "match", make_string("")); + (void)map_set(&outMap, "start", make_int(-1)); + (void)map_set(&outMap, "end", make_int(-1)); + Value emptyArr = make_array_from_values(NULL, 0); + (void)map_set(&outMap, "groups", emptyArr); + } else { + int s = (int)caps[0].rm_so; + int e = (int)caps[0].rm_eo; + char *matchStr = NULL; + if (str.s && s >= 0 && e >= s) { + int len = e - s; + matchStr = (char*)malloc((size_t)len + 1); + if (matchStr) { memcpy(matchStr, str.s + s, (size_t)len); matchStr[len] = '\0'; } + } + (void)map_set(&outMap, "match", make_string(matchStr ? matchStr : "")); + if (matchStr) free(matchStr); + (void)map_set(&outMap, "start", make_int(s)); + (void)map_set(&outMap, "end", make_int(e)); + /* groups 1..n */ + Value groupsArr = make_array_from_values(NULL, 0); + /* Count groups available */ + int groupCount = 0; + for (int i = 1; i < MAX_CAP; ++i) { + if (caps[i].rm_so == -1 || caps[i].rm_eo == -1) break; + groupCount++; + } + if (groupCount > 0) { + Value *vals = (Value*)calloc((size_t)groupCount, sizeof(Value)); + int vi = 0; + for (int i = 1; i <= groupCount; ++i) { + int gs = (int)caps[i].rm_so; + int ge = (int)caps[i].rm_eo; + char *gstr = NULL; + if (str.s && gs >= 0 && ge >= gs) { + int gl = ge - gs; + gstr = (char*)malloc((size_t)gl + 1); + if (gstr) { memcpy(gstr, str.s + gs, (size_t)gl); gstr[gl] = '\0'; } + } + vals[vi++] = make_string(gstr ? gstr : ""); + if (gstr) free(gstr); + } + groupsArr = make_array_from_values(vals, groupCount); + for (int i = 0; i < groupCount; ++i) free_value(vals[i]); + free(vals); + } + (void)map_set(&outMap, "groups", groupsArr); + } + regfree(&rx); + free_value(pattern); + free_value(str); + push_value(vm, outMap); + break; +#endif +}