From 64f4a6c3139b9e79e1a8222354dec1913504278f Mon Sep 17 00:00:00 2001 From: cs01 Date: Fri, 6 Mar 2026 10:27:53 -0800 Subject: [PATCH 1/4] add url and urlsearchparams classes with c bridge --- c_bridges/url-bridge.c | 254 ++++++++++++++++++ scripts/build-vendor.sh | 11 + src/codegen/expressions/access/member.ts | 8 + .../expressions/access/property-handlers.ts | 41 +++ src/codegen/expressions/literals.ts | 24 ++ src/codegen/expressions/method-calls.ts | 72 +++++ .../infrastructure/llvm-declarations.ts | 17 ++ src/codegen/infrastructure/symbol-table.ts | 72 +++++ src/codegen/infrastructure/type-inference.ts | 6 +- .../infrastructure/variable-allocator.ts | 29 ++ src/codegen/llvm-generator.ts | 25 ++ src/compiler.ts | 3 +- src/native-compiler-lib.ts | 3 + tests/fixtures/builtins/url-parse.ts | 40 +++ tests/fixtures/builtins/url-search-params.ts | 44 +++ 15 files changed, 647 insertions(+), 2 deletions(-) create mode 100644 c_bridges/url-bridge.c create mode 100644 tests/fixtures/builtins/url-parse.ts create mode 100644 tests/fixtures/builtins/url-search-params.ts diff --git a/c_bridges/url-bridge.c b/c_bridges/url-bridge.c new file mode 100644 index 00000000..0abd4b85 --- /dev/null +++ b/c_bridges/url-bridge.c @@ -0,0 +1,254 @@ +#include +#include +#include + +extern void* GC_malloc_atomic(size_t sz); +extern void* GC_malloc(size_t sz); + +static char* cs_strdup_gc(const char* s) { + if (!s) return NULL; + size_t len = strlen(s); + char* out = (char*)GC_malloc_atomic(len + 1); + memcpy(out, s, len + 1); + return out; +} + +static char* cs_strndup_gc(const char* s, size_t n) { + char* out = (char*)GC_malloc_atomic(n + 1); + memcpy(out, s, n); + out[n] = '\0'; + return out; +} + +const char* cs_url_parse_protocol(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* colon = strstr(href, "://"); + if (!colon) return cs_strdup_gc(""); + size_t len = colon - href + 1; + char* out = (char*)GC_malloc_atomic(len + 1); + memcpy(out, href, len); + out[len] = '\0'; + return out; +} + +const char* cs_url_parse_hostname(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* after = strstr(href, "://"); + if (!after) return cs_strdup_gc(""); + after += 3; + const char* end = after; + while (*end && *end != '/' && *end != '?' && *end != '#' && *end != ':') end++; + return cs_strndup_gc(after, end - after); +} + +const char* cs_url_parse_port(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* after = strstr(href, "://"); + if (!after) return cs_strdup_gc(""); + after += 3; + const char* colon = NULL; + const char* p = after; + while (*p && *p != '/' && *p != '?' && *p != '#') { + if (*p == ':') { colon = p; break; } + p++; + } + if (!colon) return cs_strdup_gc(""); + colon++; + const char* end = colon; + while (*end && *end != '/' && *end != '?' && *end != '#') end++; + return cs_strndup_gc(colon, end - colon); +} + +const char* cs_url_parse_host(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* after = strstr(href, "://"); + if (!after) return cs_strdup_gc(""); + after += 3; + const char* end = after; + while (*end && *end != '/' && *end != '?' && *end != '#') end++; + return cs_strndup_gc(after, end - after); +} + +const char* cs_url_parse_pathname(const char* href) { + if (!href) return cs_strdup_gc("/"); + const char* after = strstr(href, "://"); + if (!after) return cs_strdup_gc("/"); + after += 3; + while (*after && *after != '/' && *after != '?' && *after != '#') after++; + if (!*after || *after == '?' || *after == '#') return cs_strdup_gc("/"); + const char* end = after; + while (*end && *end != '?' && *end != '#') end++; + return cs_strndup_gc(after, end - after); +} + +const char* cs_url_parse_search(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* q = strchr(href, '?'); + if (!q) return cs_strdup_gc(""); + const char* end = q; + while (*end && *end != '#') end++; + return cs_strndup_gc(q, end - q); +} + +const char* cs_url_parse_hash(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* h = strchr(href, '#'); + if (!h) return cs_strdup_gc(""); + return cs_strdup_gc(h); +} + +const char* cs_url_parse_origin(const char* href) { + if (!href) return cs_strdup_gc(""); + const char* after = strstr(href, "://"); + if (!after) return cs_strdup_gc(""); + after += 3; + const char* end = after; + while (*end && *end != '/' && *end != '?' && *end != '#') end++; + size_t proto_len = (after - 3) - href; + size_t host_len = end - after; + size_t total = proto_len + 3 + host_len; + char* out = (char*)GC_malloc_atomic(total + 1); + memcpy(out, href, proto_len + 3 + host_len); + out[total] = '\0'; + return out; +} + +static const char* skip_qmark(const char* q) { + if (q && *q == '?') return q + 1; + return q ? q : ""; +} + +const char* cs_urlsearch_get(const char* query, const char* key) { + if (!query || !key) return NULL; + const char* q = skip_qmark(query); + size_t klen = strlen(key); + const char* p = q; + while (*p) { + const char* eq = strchr(p, '='); + if (!eq) break; + size_t nlen = eq - p; + if (nlen == klen && strncmp(p, key, klen) == 0) { + eq++; + const char* vend = strchr(eq, '&'); + if (!vend) vend = eq + strlen(eq); + return cs_strndup_gc(eq, vend - eq); + } + const char* amp = strchr(eq, '&'); + if (!amp) break; + p = amp + 1; + } + return NULL; +} + +int cs_urlsearch_has(const char* query, const char* key) { + if (!query || !key) return 0; + const char* q = skip_qmark(query); + size_t klen = strlen(key); + const char* p = q; + while (*p) { + const char* eq = strchr(p, '='); + if (!eq) { + if (strlen(p) == klen && strncmp(p, key, klen) == 0) return 1; + break; + } + size_t nlen = eq - p; + if (nlen == klen && strncmp(p, key, klen) == 0) return 1; + const char* amp = strchr(eq, '&'); + if (!amp) break; + p = amp + 1; + } + return 0; +} + +const char* cs_urlsearch_set(const char* query, const char* key, const char* value) { + if (!key || !value) return query ? cs_strdup_gc(query) : cs_strdup_gc(""); + const char* q = skip_qmark(query ? query : ""); + size_t klen = strlen(key); + size_t buf_size = strlen(q) + strlen(key) + strlen(value) + 64; + char* out = (char*)GC_malloc_atomic(buf_size); + out[0] = '\0'; + int found = 0; + const char* p = q; + int first = 1; + while (*p) { + const char* eq = strchr(p, '='); + if (!eq) break; + size_t nlen = eq - p; + const char* amp = strchr(eq, '&'); + const char* seg_end = amp ? amp : eq + strlen(eq); + if (!first) strcat(out, "&"); + first = 0; + if (nlen == klen && strncmp(p, key, klen) == 0) { + strncat(out, key, klen); + strcat(out, "="); + strcat(out, value); + found = 1; + } else { + strncat(out, p, seg_end - p); + } + if (!amp) break; + p = amp + 1; + } + if (!found) { + if (!first) strcat(out, "&"); + strcat(out, key); + strcat(out, "="); + strcat(out, value); + } + return out; +} + +const char* cs_urlsearch_append(const char* query, const char* key, const char* value) { + if (!key || !value) return query ? cs_strdup_gc(query) : cs_strdup_gc(""); + const char* q = skip_qmark(query ? query : ""); + size_t qlen = strlen(q); + size_t klen = strlen(key); + size_t vlen = strlen(value); + size_t total = qlen + klen + vlen + 4; + char* out = (char*)GC_malloc_atomic(total); + if (qlen > 0) { + memcpy(out, q, qlen); + out[qlen] = '&'; + memcpy(out + qlen + 1, key, klen); + out[qlen + 1 + klen] = '='; + memcpy(out + qlen + 1 + klen + 1, value, vlen); + out[qlen + 1 + klen + 1 + vlen] = '\0'; + } else { + memcpy(out, key, klen); + out[klen] = '='; + memcpy(out + klen + 1, value, vlen); + out[klen + 1 + vlen] = '\0'; + } + return out; +} + +const char* cs_urlsearch_delete(const char* query, const char* key) { + if (!query || !key) return cs_strdup_gc(""); + const char* q = skip_qmark(query); + size_t klen = strlen(key); + size_t buf_size = strlen(q) + 2; + char* out = (char*)GC_malloc_atomic(buf_size); + out[0] = '\0'; + const char* p = q; + int first = 1; + while (*p) { + const char* eq = strchr(p, '='); + if (!eq) break; + size_t nlen = eq - p; + const char* amp = strchr(eq, '&'); + const char* seg_end = amp ? amp : eq + strlen(eq); + if (!(nlen == klen && strncmp(p, key, klen) == 0)) { + if (!first) strcat(out, "&"); + first = 0; + strncat(out, p, seg_end - p); + } + if (!amp) break; + p = amp + 1; + } + return out; +} + +const char* cs_urlsearch_tostring(const char* query) { + if (!query) return cs_strdup_gc(""); + return cs_strdup_gc(skip_qmark(query)); +} diff --git a/scripts/build-vendor.sh b/scripts/build-vendor.sh index 150a1657..1372e6a1 100755 --- a/scripts/build-vendor.sh +++ b/scripts/build-vendor.sh @@ -221,6 +221,17 @@ else echo "==> base64-bridge already built, skipping" fi +# --- url-bridge --- +URL_BRIDGE_SRC="$C_BRIDGES_DIR/url-bridge.c" +URL_BRIDGE_OBJ="$C_BRIDGES_DIR/url-bridge.o" +if [ ! -f "$URL_BRIDGE_OBJ" ] || [ "$URL_BRIDGE_SRC" -nt "$URL_BRIDGE_OBJ" ]; then + echo "==> Building url-bridge..." + cc -c -O2 -fPIC "$URL_BRIDGE_SRC" -o "$URL_BRIDGE_OBJ" + echo " -> $URL_BRIDGE_OBJ" +else + echo "==> url-bridge already built, skipping" +fi + # --- child-process-spawn (async, requires libuv) --- CP_SPAWN_SRC="$C_BRIDGES_DIR/child-process-spawn.c" CP_SPAWN_OBJ="$C_BRIDGES_DIR/child-process-spawn.o" diff --git a/src/codegen/expressions/access/member.ts b/src/codegen/expressions/access/member.ts index fad56713..991b30e7 100644 --- a/src/codegen/expressions/access/member.ts +++ b/src/codegen/expressions/access/member.ts @@ -60,6 +60,7 @@ import { getArrayLengthFromPtr, getStringLength, handleSpawnSyncResultProperty, + handleUrlProperty, } from "./property-handlers.js"; import { parseInlineObjectTypeForAssertion, @@ -362,6 +363,9 @@ export class MemberAccessGenerator { result = this.handleSpawnSyncResultProperty(expr); if (result !== null) return result; + result = this.handleUrlProperty(expr); + if (result !== null) return result; + return null; } @@ -2420,6 +2424,10 @@ export class MemberAccessGenerator { return handleSpawnSyncResultProperty(this.ctx, expr); } + private handleUrlProperty(expr: MemberAccessNode): string | null { + return handleUrlProperty(this.ctx, expr); + } + private handleParameterPropertyAccess(expr: MemberAccessNode, params: string[]): string { const prop = expr.property; if (!prop) { diff --git a/src/codegen/expressions/access/property-handlers.ts b/src/codegen/expressions/access/property-handlers.ts index dcfb3583..97079a25 100644 --- a/src/codegen/expressions/access/property-handlers.ts +++ b/src/codegen/expressions/access/property-handlers.ts @@ -1,6 +1,47 @@ import { Expression, MemberAccessNode, VariableNode } from "../../../ast/types.js"; import type { MemberAccessGeneratorContext } from "./member.js"; +export function handleUrlProperty( + ctx: MemberAccessGeneratorContext, + expr: MemberAccessNode, +): string | null { + const exprObjBase = expr.object as ExprBase; + if (exprObjBase.type !== "variable") return null; + const varName = (expr.object as VariableNode).name; + if (!ctx.symbolTable.isUrl(varName)) return null; + const prop = expr.property; + const varAlloca = ctx.symbolTable.getAlloca(varName); + if (!varAlloca) return null; + const urlPtr = ctx.emitLoad("i8*", varAlloca); + if (prop === "href") { + ctx.setVariableType(urlPtr, "i8*"); + return urlPtr; + } + let urlFn = ""; + if (prop === "protocol") { + urlFn = "@cs_url_parse_protocol"; + } else if (prop === "hostname") { + urlFn = "@cs_url_parse_hostname"; + } else if (prop === "port") { + urlFn = "@cs_url_parse_port"; + } else if (prop === "host") { + urlFn = "@cs_url_parse_host"; + } else if (prop === "pathname") { + urlFn = "@cs_url_parse_pathname"; + } else if (prop === "search" || prop === "searchParams") { + urlFn = "@cs_url_parse_search"; + } else if (prop === "hash") { + urlFn = "@cs_url_parse_hash"; + } else if (prop === "origin") { + urlFn = "@cs_url_parse_origin"; + } else { + return null; + } + const result = ctx.emitCall("i8*", urlFn, `i8* ${urlPtr}`); + ctx.setVariableType(result, "i8*"); + return result; +} + interface ExprBase { type: string; } diff --git a/src/codegen/expressions/literals.ts b/src/codegen/expressions/literals.ts index f89a6ab9..78d963e2 100644 --- a/src/codegen/expressions/literals.ts +++ b/src/codegen/expressions/literals.ts @@ -201,6 +201,12 @@ export class LiteralExpressionGenerator { if (className === "Date") { return this.generateNewDate(args, params); } + if (className === "URL") { + return this.generateNewUrl(args, params); + } + if (className === "URLSearchParams") { + return this.generateNewUrlSearchParams(args, params); + } return this.ctx.classGenGenerateNewExpression(className, args, params); } @@ -293,6 +299,24 @@ export class LiteralExpressionGenerator { return structPtr; } + private generateNewUrl(args: Expression[], params: string[]): string { + if (args.length < 1) { + throw new Error("new URL() requires at least 1 argument"); + } + const hrefPtr = this.ctx.generateExpression(args[0], params); + this.ctx.setVariableType(hrefPtr, "i8*"); + return hrefPtr; + } + + private generateNewUrlSearchParams(args: Expression[], params: string[]): string { + if (args.length === 0) { + return this.ctx.stringGen.doCreateStringConstant(""); + } + const queryPtr = this.ctx.generateExpression(args[0], params); + this.ctx.setVariableType(queryPtr, "i8*"); + return queryPtr; + } + private generateNewDate(args: Expression[], params: string[]): string { const structRaw = this.ctx.nextTemp(); this.ctx.emit(`${structRaw} = call i8* @GC_malloc(i64 8)`); diff --git a/src/codegen/expressions/method-calls.ts b/src/codegen/expressions/method-calls.ts index 22732247..4b5b9007 100644 --- a/src/codegen/expressions/method-calls.ts +++ b/src/codegen/expressions/method-calls.ts @@ -1264,6 +1264,78 @@ export class MethodCallGenerator { } } + // Handle URLSearchParams methods + if ( + method === "get" || + method === "has" || + method === "set" || + method === "append" || + method === "delete" || + method === "toString" + ) { + const urlspVarName = this.getVariableName(expr.object); + if (urlspVarName && this.ctx.symbolTable.isUrlSearchParams(urlspVarName)) { + const urlspAlloca = this.ctx.symbolTable.getAlloca(urlspVarName); + if (urlspAlloca) { + const queryPtr = this.ctx.emitLoad("i8*", urlspAlloca); + if (method === "get") { + const keyPtr = this.ctx.generateExpression(expr.args[0], params); + const result = this.ctx.emitCall( + "i8*", + "@cs_urlsearch_get", + `i8* ${queryPtr}, i8* ${keyPtr}`, + ); + this.ctx.setVariableType(result, "i8*"); + return result; + } else if (method === "has") { + const keyPtr = this.ctx.generateExpression(expr.args[0], params); + const i32Result = this.ctx.emitCall( + "i32", + "@cs_urlsearch_has", + `i8* ${queryPtr}, i8* ${keyPtr}`, + ); + const dblResult = this.ctx.nextTemp(); + this.ctx.emit(`${dblResult} = sitofp i32 ${i32Result} to double`); + this.ctx.setVariableType(dblResult, "double"); + return dblResult; + } else if (method === "set") { + const keyPtr = this.ctx.generateExpression(expr.args[0], params); + const valPtr = this.ctx.generateExpression(expr.args[1], params); + const newQuery = this.ctx.emitCall( + "i8*", + "@cs_urlsearch_set", + `i8* ${queryPtr}, i8* ${keyPtr}, i8* ${valPtr}`, + ); + this.ctx.emitStore("i8*", newQuery, urlspAlloca); + return newQuery; + } else if (method === "append") { + const keyPtr = this.ctx.generateExpression(expr.args[0], params); + const valPtr = this.ctx.generateExpression(expr.args[1], params); + const newQuery = this.ctx.emitCall( + "i8*", + "@cs_urlsearch_append", + `i8* ${queryPtr}, i8* ${keyPtr}, i8* ${valPtr}`, + ); + this.ctx.emitStore("i8*", newQuery, urlspAlloca); + return newQuery; + } else if (method === "delete") { + const keyPtr = this.ctx.generateExpression(expr.args[0], params); + const newQuery = this.ctx.emitCall( + "i8*", + "@cs_urlsearch_delete", + `i8* ${queryPtr}, i8* ${keyPtr}`, + ); + this.ctx.emitStore("i8*", newQuery, urlspAlloca); + return newQuery; + } else if (method === "toString") { + const result = this.ctx.emitCall("i8*", "@cs_urlsearch_tostring", `i8* ${queryPtr}`); + this.ctx.setVariableType(result, "i8*"); + return result; + } + } + } + } + // Handle array methods (arrayGen uses context pattern - no sync needed! 🎯) // Skip to class dispatch if object is a class instance (e.g. Stack.push / Stack.pop) if (method === "push" && !this.isClassInstanceExpression(expr.object)) { diff --git a/src/codegen/infrastructure/llvm-declarations.ts b/src/codegen/infrastructure/llvm-declarations.ts index 02cde33e..79f69c5e 100644 --- a/src/codegen/infrastructure/llvm-declarations.ts +++ b/src/codegen/infrastructure/llvm-declarations.ts @@ -91,6 +91,23 @@ export function getLLVMDeclarations(config?: DeclConfig): string { ir += "declare i8* @cs_base64_decode(i8*)\n"; ir += "\n"; + // url bridge — URL parsing and URLSearchParams manipulation + ir += "declare i8* @cs_url_parse_protocol(i8*)\n"; + ir += "declare i8* @cs_url_parse_hostname(i8*)\n"; + ir += "declare i8* @cs_url_parse_port(i8*)\n"; + ir += "declare i8* @cs_url_parse_host(i8*)\n"; + ir += "declare i8* @cs_url_parse_pathname(i8*)\n"; + ir += "declare i8* @cs_url_parse_search(i8*)\n"; + ir += "declare i8* @cs_url_parse_hash(i8*)\n"; + ir += "declare i8* @cs_url_parse_origin(i8*)\n"; + ir += "declare i8* @cs_urlsearch_get(i8*, i8*)\n"; + ir += "declare i32 @cs_urlsearch_has(i8*, i8*)\n"; + ir += "declare i8* @cs_urlsearch_set(i8*, i8*, i8*)\n"; + ir += "declare i8* @cs_urlsearch_append(i8*, i8*, i8*)\n"; + ir += "declare i8* @cs_urlsearch_delete(i8*, i8*)\n"; + ir += "declare i8* @cs_urlsearch_tostring(i8*)\n"; + ir += "\n"; + ir += "declare i32 @printf(i8*, ...)\n"; ir += "declare i32 @fprintf(i8*, i8*, ...)\n"; const isMac = diff --git a/src/codegen/infrastructure/symbol-table.ts b/src/codegen/infrastructure/symbol-table.ts index ca36533b..9aa5b61e 100644 --- a/src/codegen/infrastructure/symbol-table.ts +++ b/src/codegen/infrastructure/symbol-table.ts @@ -35,6 +35,8 @@ export enum SymbolKind { Closure, Pointer, Uint8Array, + Url, + UrlSearchParams, } /** @@ -1147,6 +1149,76 @@ export class SymbolTable { return false; } + isUrl(name: string): boolean { + const symbol = this.symbols.get(name); + return !!(symbol && symbol.kind === SymbolKind.Url); + } + + isUrlSearchParams(name: string): boolean { + const symbol = this.symbols.get(name); + return !!(symbol && symbol.kind === SymbolKind.UrlSearchParams); + } + + defineUrl(name: string, allocaRegister: string, scope: string): void { + const symbol: Symbol = { + name, + kind: SymbolKind.Url, + llvmType: "i8*", + allocaRegister, + scope, + isPointerAlloca: false, + resolvedType: undefined, + objectMetadata: undefined, + classMetadata: undefined, + arrayMetadata: undefined, + objectArrayMetadata: undefined, + closureMetadata: undefined, + mapMetadata: undefined, + setMetadata: undefined, + interfaceType: undefined, + concreteClass: undefined, + }; + if (!this.symbols.has(name)) { + this.symbolKeys.push(name); + this.symbolKeysCount++; + } + this.symbols.set(name, symbol); + if (scope === "local" && this.scopeBoundariesCount > 0) { + this.scopeNames.push(name); + this.scopeNamesCount++; + } + } + + defineUrlSearchParams(name: string, allocaRegister: string, scope: string): void { + const symbol: Symbol = { + name, + kind: SymbolKind.UrlSearchParams, + llvmType: "i8*", + allocaRegister, + scope, + isPointerAlloca: false, + resolvedType: undefined, + objectMetadata: undefined, + classMetadata: undefined, + arrayMetadata: undefined, + objectArrayMetadata: undefined, + closureMetadata: undefined, + mapMetadata: undefined, + setMetadata: undefined, + interfaceType: undefined, + concreteClass: undefined, + }; + if (!this.symbols.has(name)) { + this.symbolKeys.push(name); + this.symbolKeysCount++; + } + this.symbols.set(name, symbol); + if (scope === "local" && this.scopeBoundariesCount > 0) { + this.scopeNames.push(name); + this.scopeNamesCount++; + } + } + // ============================================ // Metadata accessors // ============================================ diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 5099cb5a..155dcab3 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -1662,7 +1662,9 @@ export class TypeInference { resolved.base === "Promise" || resolved.base === "RegExp" || resolved.base === "Uint8Array" || - resolved.base === "Date" + resolved.base === "Date" || + resolved.base === "URL" || + resolved.base === "URLSearchParams" ) return false; if ( @@ -1688,6 +1690,8 @@ export class TypeInference { if (newExpr.className === "RegExp") return false; if (newExpr.className === "Uint8Array") return false; if (newExpr.className === "Date") return false; + if (newExpr.className === "URL") return false; + if (newExpr.className === "URLSearchParams") return false; return true; } return false; diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index 61f7fb77..ae05ad0a 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -707,6 +707,19 @@ export class VariableAllocator { const stmtValue = stmt.value!; + const stmtValueBase = stmtValue as { type: string }; + if (stmtValueBase.type === "new") { + const newNode = stmtValue as NewNode; + if (newNode.className === "URL") { + this.allocateUrl(stmt, params); + return; + } + if (newNode.className === "URLSearchParams") { + this.allocateUrlSearchParams(stmt, params); + return; + } + } + if (stmt.declaredType) { const strippedType = stripNullable(stmt.declaredType); if (strippedType === "string[]") { @@ -2054,6 +2067,22 @@ export class VariableAllocator { this.ctx.emit(`store i8* ${value}, i8** ${allocaReg}`); } + private allocateUrl(stmt: VariableDeclaration, params: string[]): void { + const allocaReg = this.ctx.nextAllocaReg(stmt.name); + this.ctx.symbolTable.defineUrl(stmt.name, allocaReg, "local"); + this.ctx.emit(`${allocaReg} = alloca i8*`); + const value = this.ctx.generateExpression(stmt.value!, params); + this.ctx.emit(`store i8* ${value}, i8** ${allocaReg}`); + } + + private allocateUrlSearchParams(stmt: VariableDeclaration, params: string[]): void { + const allocaReg = this.ctx.nextAllocaReg(stmt.name); + this.ctx.symbolTable.defineUrlSearchParams(stmt.name, allocaReg, "local"); + this.ctx.emit(`${allocaReg} = alloca i8*`); + const value = this.ctx.generateExpression(stmt.value!, params); + this.ctx.emit(`store i8* ${value}, i8** ${allocaReg}`); + } + private allocateString(stmt: VariableDeclaration, params: string[]): void { const allocaReg = this.ctx.nextAllocaReg(stmt.name); this.ctx.defineVariable(stmt.name, allocaReg, "i8*", SymbolKind.String, "local"); diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index 868dc1b5..e95be98b 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -1958,6 +1958,31 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { let kind: number = SymbolKind.Number; let defaultValue: string = "0.0"; + const stmtValueBase = stmt.value as { type: string }; + if (stmtValueBase.type === "new") { + const newNode = stmt.value as NewNode; + if (newNode.className === "URL") { + ir += `@${name} = global i8* null` + "\n"; + this.globalVariables.set(name, { + llvmType: "i8*", + kind: SymbolKind.Url, + initialized: false, + }); + this.symbolTable.defineUrl(name, `@${name}`, "global"); + continue; + } + if (newNode.className === "URLSearchParams") { + ir += `@${name} = global i8* null` + "\n"; + this.globalVariables.set(name, { + llvmType: "i8*", + kind: SymbolKind.UrlSearchParams, + initialized: false, + }); + this.symbolTable.defineUrlSearchParams(name, `@${name}`, "global"); + continue; + } + } + if (isString) { llvmType = "i8*"; kind = SymbolKind.String; diff --git a/src/compiler.ts b/src/compiler.ts index a0826eee..5af4ed60 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -405,6 +405,7 @@ export function compile( const osBridgeObj = `${bridgePath}/os-bridge.o`; const timeBridgeObj = `${bridgePath}/time-bridge.o`; const base64BridgeObj = `${bridgePath}/base64-bridge.o`; + const urlBridgeObj = `${bridgePath}/url-bridge.o`; const dotenvBridgeObj = fs.existsSync(`${bridgePath}/dotenv-bridge.o`) ? `${bridgePath}/dotenv-bridge.o` : ""; @@ -488,7 +489,7 @@ export function compile( const userObjs = extraLinkObjs.length > 0 ? " " + extraLinkObjs.join(" ") : ""; const userPaths = extraLinkPaths.map((p) => ` -L${p}`).join(""); const userLibs = extraLinkLibs.map((l) => ` -l${l}`).join(""); - const linkCmd = `${linker} ${objFile} ${lwsBridgeObj} ${regexBridgeObj} ${cpBridgeObj} ${osBridgeObj} ${timeBridgeObj} ${base64BridgeObj} ${dotenvBridgeObj} ${watchBridgeObj} ${cpSpawnObj}${extraObjs}${userObjs} -o ${outputFile}${noPie}${debugFlag}${stripFlag}${staticFlag}${crossTarget}${crossLinker}${sanitizeFlags} ${linkLibs}${userPaths}${userLibs}`; + const linkCmd = `${linker} ${objFile} ${lwsBridgeObj} ${regexBridgeObj} ${cpBridgeObj} ${osBridgeObj} ${timeBridgeObj} ${base64BridgeObj} ${urlBridgeObj} ${dotenvBridgeObj} ${watchBridgeObj} ${cpSpawnObj}${extraObjs}${userObjs} -o ${outputFile}${noPie}${debugFlag}${stripFlag}${staticFlag}${crossTarget}${crossLinker}${sanitizeFlags} ${linkLibs}${userPaths}${userLibs}`; logger.info(` ${linkCmd}`); const linkStdio = logger.getLevel() >= LogLevel.Verbose ? "inherit" : "pipe"; execSync(linkCmd, { stdio: linkStdio }); diff --git a/src/native-compiler-lib.ts b/src/native-compiler-lib.ts index f032e4bf..c54a4d93 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -506,6 +506,7 @@ export function compileNative(inputFile: string, outputFile: string): void { const osBridgeObj = effectiveBridgePath + "/os-bridge.o"; const timeBridgeObj = effectiveBridgePath + "/time-bridge.o"; const base64BridgeObj = effectiveBridgePath + "/base64-bridge.o"; + const urlBridgeObj = effectiveBridgePath + "/url-bridge.o"; const dotenvBridgePath = effectiveBridgePath + "/dotenv-bridge.o"; const dotenvBridgeObj = fs.existsSync(dotenvBridgePath) ? dotenvBridgePath : ""; const watchBridgeObj = effectiveBridgePath + "/watch-bridge.o"; @@ -584,6 +585,8 @@ export function compileNative(inputFile: string, outputFile: string): void { " " + base64BridgeObj + " " + + urlBridgeObj + + " " + dotenvBridgeObj + " " + watchBridgeObj + diff --git a/tests/fixtures/builtins/url-parse.ts b/tests/fixtures/builtins/url-parse.ts new file mode 100644 index 00000000..f19ed521 --- /dev/null +++ b/tests/fixtures/builtins/url-parse.ts @@ -0,0 +1,40 @@ +const u = new URL("https://example.com:8080/path/to/page?q=hello&page=2#section"); + +if (u.protocol !== "https:") { + console.log("FAIL: protocol got " + u.protocol); + process.exit(1); +} +if (u.hostname !== "example.com") { + console.log("FAIL: hostname got " + u.hostname); + process.exit(1); +} +if (u.port !== "8080") { + console.log("FAIL: port got " + u.port); + process.exit(1); +} +if (u.host !== "example.com:8080") { + console.log("FAIL: host got " + u.host); + process.exit(1); +} +if (u.pathname !== "/path/to/page") { + console.log("FAIL: pathname got " + u.pathname); + process.exit(1); +} +if (u.search !== "?q=hello&page=2") { + console.log("FAIL: search got " + u.search); + process.exit(1); +} +if (u.hash !== "#section") { + console.log("FAIL: hash got " + u.hash); + process.exit(1); +} +if (u.origin !== "https://example.com:8080") { + console.log("FAIL: origin got " + u.origin); + process.exit(1); +} +if (u.href !== "https://example.com:8080/path/to/page?q=hello&page=2#section") { + console.log("FAIL: href got " + u.href); + process.exit(1); +} + +console.log("TEST_PASSED"); diff --git a/tests/fixtures/builtins/url-search-params.ts b/tests/fixtures/builtins/url-search-params.ts new file mode 100644 index 00000000..d087a9f8 --- /dev/null +++ b/tests/fixtures/builtins/url-search-params.ts @@ -0,0 +1,44 @@ +const p = new URLSearchParams("q=hello&page=2"); + +if (p.get("q") !== "hello") { + console.log("FAIL: get q got " + p.get("q")); + process.exit(1); +} +if (p.get("page") !== "2") { + console.log("FAIL: get page got " + p.get("page")); + process.exit(1); +} +if (!p.has("q")) { + console.log("FAIL: has q returned false"); + process.exit(1); +} +if (p.has("missing")) { + console.log("FAIL: has missing returned true"); + process.exit(1); +} + +p.set("q", "world"); +if (p.get("q") !== "world") { + console.log("FAIL: after set q got " + p.get("q")); + process.exit(1); +} + +p.append("tag", "foo"); +if (p.get("tag") !== "foo") { + console.log("FAIL: after append tag got " + p.get("tag")); + process.exit(1); +} + +p.delete("page"); +if (p.has("page")) { + console.log("FAIL: page still present after delete"); + process.exit(1); +} + +const str = p.toString(); +if (str !== "q=world&tag=foo") { + console.log("FAIL: toString got " + str); + process.exit(1); +} + +console.log("TEST_PASSED"); From 885609476d5770e020266ae5de56d9e83a693a19 Mon Sep 17 00:00:00 2001 From: cs01 Date: Fri, 6 Mar 2026 15:09:15 -0800 Subject: [PATCH 2/4] fix: add url-bridge.o to ci install/release --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e59efcb..dd86dec1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: - name: Verify vendor libraries run: | fail=0 - for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/os-bridge.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o; do + for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/os-bridge.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o; do if [ ! -f "$lib" ]; then echo "MISSING: $lib" fail=1 @@ -132,7 +132,7 @@ jobs: cp c_bridges/child-process-bridge.o release/lib/ cp c_bridges/child-process-spawn.o release/lib/ cp c_bridges/os-bridge.o release/lib/ - cp c_bridges/time-bridge.o c_bridges/base64-bridge.o release/lib/ + cp c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o release/lib/ cp c_bridges/dotenv-bridge.o release/lib/ cp c_bridges/watch-bridge.o release/lib/ tar -czf chadscript-linux-x64.tar.gz -C release chad lib @@ -186,7 +186,7 @@ jobs: - name: Verify vendor libraries run: | fail=0 - for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/os-bridge.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o; do + for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/os-bridge.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o; do if [ ! -f "$lib" ]; then echo "MISSING: $lib" fail=1 @@ -257,7 +257,7 @@ jobs: cp c_bridges/child-process-bridge.o release/lib/ cp c_bridges/child-process-spawn.o release/lib/ cp c_bridges/os-bridge.o release/lib/ - cp c_bridges/time-bridge.o c_bridges/base64-bridge.o release/lib/ + cp c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o release/lib/ cp c_bridges/dotenv-bridge.o release/lib/ cp c_bridges/watch-bridge.o release/lib/ tar -czf chadscript-macos-arm64.tar.gz -C release chad lib From a4db4497997f218b5cb60a7641e8c92bc4927197 Mon Sep 17 00:00:00 2001 From: cs01 Date: Fri, 6 Mar 2026 15:15:54 -0800 Subject: [PATCH 3/4] fix: add url-bridge.o to cross-compile target sdk packaging --- scripts/build-target-sdk.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-target-sdk.sh b/scripts/build-target-sdk.sh index f8c808c0..86133883 100755 --- a/scripts/build-target-sdk.sh +++ b/scripts/build-target-sdk.sh @@ -69,7 +69,7 @@ fi # Copy C bridge object files echo " Copying bridge objects..." -for bridge in child-process-bridge.o os-bridge.o time-bridge.o base64-bridge.o regex-bridge.o dotenv-bridge.o watch-bridge.o lws-bridge.o multipart-bridge.o child-process-spawn.o; do +for bridge in child-process-bridge.o os-bridge.o time-bridge.o base64-bridge.o url-bridge.o regex-bridge.o dotenv-bridge.o watch-bridge.o lws-bridge.o multipart-bridge.o child-process-spawn.o; do if [ -f "$C_BRIDGES_DIR/$bridge" ]; then cp "$C_BRIDGES_DIR/$bridge" "$SDK_DIR/bridges/" fi From fd484b3898266540ccfdf7aed4f75dcf809395b4 Mon Sep 17 00:00:00 2001 From: cs01 Date: Fri, 6 Mar 2026 15:20:21 -0800 Subject: [PATCH 4/4] fix: pass --target-cpu=x86-64 to stage1 compilation --- tests/self-hosting.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/self-hosting.test.ts b/tests/self-hosting.test.ts index e4b03da7..0720a034 100644 --- a/tests/self-hosting.test.ts +++ b/tests/self-hosting.test.ts @@ -203,10 +203,13 @@ describe("Self-Hosting", { timeout: 600000 }, () => { assert.ok(fsSync.existsSync(STAGE0), "Stage 0 binary must exist"); if (fsSync.existsSync(STAGE1)) fsSync.unlinkSync(STAGE1); - await execWithRetry(`${STAGE0} build -v src/chad-native.ts -o ${STAGE1}`, { - timeout: 180000, - env: NATIVE_ENV, - }); + await execWithRetry( + `${STAGE0} build -v src/chad-native.ts -o ${STAGE1} --target-cpu=x86-64`, + { + timeout: 180000, + env: NATIVE_ENV, + }, + ); assert.ok(fsSync.existsSync(STAGE1), `Stage 1 binary should exist at ${STAGE1}`); const stats = fsSync.statSync(STAGE1);