diff --git a/c_bridges/regex-bridge.c b/c_bridges/regex-bridge.c index 5a6a2314..b6889e94 100644 --- a/c_bridges/regex-bridge.c +++ b/c_bridges/regex-bridge.c @@ -2,6 +2,9 @@ #include #include +extern void *GC_malloc(size_t); +extern void *GC_malloc_atomic(size_t); + void *cs_regex_alloc(void) { return malloc(sizeof(regex_t)); } @@ -32,3 +35,35 @@ void cs_regex_free(void *preg) { free(preg); } } + +char *cs_regex_exec_dyn(void *preg, const char *str, int max_groups) { + regmatch_t *pmatch = (regmatch_t *)calloc((size_t)max_groups, sizeof(regmatch_t)); + if (!pmatch) return NULL; + + if (regexec((regex_t *)preg, str, (size_t)max_groups, pmatch, 0) != 0) { + free(pmatch); + return NULL; + } + + int count = 0; + for (int i = 0; i < max_groups; i++) { + if (pmatch[i].rm_so < 0) break; + count++; + } + + char **strings = (char **)GC_malloc((size_t)count * sizeof(char *)); + for (int i = 0; i < count; i++) { + int slen = pmatch[i].rm_eo - pmatch[i].rm_so; + char *s = (char *)GC_malloc_atomic((size_t)(slen + 1)); + if (slen > 0) strncpy(s, str + pmatch[i].rm_so, (size_t)slen); + s[slen] = '\0'; + strings[i] = s; + } + free(pmatch); + + char *arr = (char *)GC_malloc(16); + *((char ***)arr) = strings; + *((int *)(arr + 8)) = count; + *((int *)(arr + 12)) = count; + return arr; +} diff --git a/src/codegen/expressions/method-calls.ts b/src/codegen/expressions/method-calls.ts index 5d056124..8fb92a87 100644 --- a/src/codegen/expressions/method-calls.ts +++ b/src/codegen/expressions/method-calls.ts @@ -864,6 +864,13 @@ export class MethodCallGenerator { } } + if (method === "execDyn") { + const isRegex = this.ctx.isRegexExpression(expr.object); + if (isRegex) { + return this.handleRegexExecDyn(expr, params); + } + } + if (method === "isFile" || method === "isDirectory") { let statI8Ptr: string | null = null; @@ -1420,6 +1427,15 @@ export class MethodCallGenerator { return this.ctx.regexGen.generateRegexMatch(regexPtr, strPtr, numGroups); } + private handleRegexExecDyn(expr: MethodCallNode, params: string[]): string { + if (expr.args.length !== 1) { + return this.ctx.emitError(`execDyn() expects 1 argument, got ${expr.args.length}`, expr.loc); + } + const regexPtr = this.ctx.generateExpression(expr.object, params); + const strPtr = this.ctx.generateExpression(expr.args[0], params); + return this.ctx.regexGen.generateRegexExecDyn(regexPtr, strPtr); + } + private throwUnsupportedMethodError( method: string, _objectType?: string, diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index cec83ff0..3b63a266 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -122,6 +122,7 @@ export interface IRegexGenerator { generateRegexTest(regexPtr: string, testStr: string): string; generateRegexMatch(regexPtr: string, testStr: string, numGroups: number): string; generateRegexCompileRuntime(patternPtr: string, cflags: number): string; + generateRegexExecDyn(regexPtr: string, testStr: string): string; } export interface IControlFlowGenerator { @@ -1823,6 +1824,7 @@ export class MockGeneratorContext implements IGeneratorContext { "%mock_regex_match", generateRegexCompileRuntime: (_patternPtr: string, _cflags: number): string => "%mock_regex_compile_runtime", + generateRegexExecDyn: (_regexPtr: string, _testStr: string): string => "%mock_regex_exec_dyn", }; controlFlowGen: IControlFlowGenerator = { generateLogicalOp: ( diff --git a/src/codegen/infrastructure/llvm-declarations.ts b/src/codegen/infrastructure/llvm-declarations.ts index 1634295a..d2f8ac48 100644 --- a/src/codegen/infrastructure/llvm-declarations.ts +++ b/src/codegen/infrastructure/llvm-declarations.ts @@ -76,6 +76,7 @@ export function getLLVMDeclarations(config?: DeclConfig): string { ir += "declare i64 @cs_pmatch_start(i8*, i32)\n"; ir += "declare i64 @cs_pmatch_end(i8*, i32)\n"; ir += "declare void @cs_regex_free(i8*)\n"; + ir += "declare i8* @cs_regex_exec_dyn(i8*, i8*, i32)\n"; ir += "\n"; // child_process bridge — %SpawnSyncResult = { stdout: i8*, stderr: i8*, status: double } diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 548a98d9..de0dde9b 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -359,7 +359,7 @@ export class TypeInference { return this.ctx.typeContext.booleanType; } - if (method === "match" || method === "exec") { + if (method === "match" || method === "exec" || method === "execDyn") { return this.ctx.typeContext.getArrayType("string"); } @@ -2142,7 +2142,10 @@ export class TypeInference { return true; } } - if (methodExpr.method === "exec" && this.isRegexExpression(methodExpr.object)) { + if ( + (methodExpr.method === "exec" || methodExpr.method === "execDyn") && + this.isRegexExpression(methodExpr.object) + ) { return true; } if ( diff --git a/src/codegen/types/objects/regex.ts b/src/codegen/types/objects/regex.ts index 5df0abf0..2e33246a 100644 --- a/src/codegen/types/objects/regex.ts +++ b/src/codegen/types/objects/regex.ts @@ -161,6 +161,17 @@ export class RegexGenerator { return result; } + generateRegexExecDyn(regexPtr: string, testStr: string): string { + this.ctx.setUsesRegex(true); + const result = this.ctx.emitCall( + "i8*", + "@cs_regex_exec_dyn", + `i8* ${regexPtr}, i8* ${testStr}, i32 64`, + ); + this.ctx.setVariableType(result, "i8*"); + return result; + } + // Clean up regex resources generateRegexFree(regexPtr: string): void { this.ctx.emitCallVoid("@cs_regex_free", `i8* ${regexPtr}`); diff --git a/tests/fixtures/regex/regex-exec-dyn.ts b/tests/fixtures/regex/regex-exec-dyn.ts new file mode 100644 index 00000000..84ee413e --- /dev/null +++ b/tests/fixtures/regex/regex-exec-dyn.ts @@ -0,0 +1,31 @@ +// @test-description: dynamic regex exec returns string array + +function testExecDyn(): void { + const re = new RegExp("([a-z]+)/([0-9]+)"); + const m1 = re.execDyn("rooms/42"); + if (m1 === null) { + console.log("FAIL: expected match"); + process.exit(1); + } + if (m1[0] !== "rooms/42") { + console.log("FAIL: full match wrong"); + process.exit(1); + } + if (m1[1] !== "rooms") { + console.log("FAIL: group 1 wrong"); + process.exit(1); + } + if (m1[2] !== "42") { + console.log("FAIL: group 2 wrong"); + process.exit(1); + } + + const m2 = re.execDyn("nope"); + if (m2 !== null) { + console.log("FAIL: expected no match"); + process.exit(1); + } + + console.log("TEST_PASSED"); +} +testExecDyn();