diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd86dec1..02f9bacb 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/url-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/uri-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 c_bridges/url-bridge.o release/lib/ + cp c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/uri-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/url-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/uri-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 c_bridges/url-bridge.o release/lib/ + cp c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/uri-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 diff --git a/c_bridges/base64-bridge.c b/c_bridges/base64-bridge.c index f6fe2ab8..04db3441 100644 --- a/c_bridges/base64-bridge.c +++ b/c_bridges/base64-bridge.c @@ -1,5 +1,4 @@ -// base64-bridge.c — base64 decode for Buffer.from(str, 'base64') support. -// Returns a heap-allocated %Uint8Array struct (GC-managed). +// base64-bridge.c — base64 encode/decode for Buffer.from, btoa, atob support. #include #include @@ -9,6 +8,67 @@ extern void* GC_malloc(size_t sz); typedef struct { char* data; int len; int cap; } CsUint8Array; +static const char b64_enc[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +char* cs_btoa(const char* input) { + if (!input) { + char* out = (char*)GC_malloc_atomic(1); + out[0] = '\0'; + return out; + } + size_t in_len = strlen(input); + size_t out_len = ((in_len + 2) / 3) * 4 + 1; + char* out = (char*)GC_malloc_atomic(out_len); + size_t i = 0, j = 0; + while (i < in_len) { + unsigned char a = (unsigned char)input[i++]; + int has_b = (i < in_len); + unsigned char b = has_b ? (unsigned char)input[i++] : 0; + int has_c = (i < in_len); + unsigned char c = has_c ? (unsigned char)input[i++] : 0; + unsigned int triple = (a << 16) | (b << 8) | c; + out[j++] = b64_enc[(triple >> 18) & 0x3F]; + out[j++] = b64_enc[(triple >> 12) & 0x3F]; + out[j++] = has_b ? b64_enc[(triple >> 6) & 0x3F] : '='; + out[j++] = has_c ? b64_enc[triple & 0x3F] : '='; + } + out[j] = '\0'; + return out; +} + +char* cs_atob(const char* input) { + if (!input) { + char* out = (char*)GC_malloc_atomic(1); + out[0] = '\0'; + return out; + } + size_t in_len = strlen(input); + size_t max_out = (in_len / 4) * 3 + 4; + char* out = (char*)GC_malloc_atomic(max_out); + size_t out_pos = 0; + int buf = 0, bits = 0; + for (size_t i = 0; i < in_len; i++) { + unsigned char ch = (unsigned char)input[i]; + signed char v; + if (ch >= 'A' && ch <= 'Z') v = ch - 'A'; + else if (ch >= 'a' && ch <= 'z') v = ch - 'a' + 26; + else if (ch >= '0' && ch <= '9') v = ch - '0' + 52; + else if (ch == '+') v = 62; + else if (ch == '/') v = 63; + else if (ch == '=') break; + else continue; + buf = (buf << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[out_pos++] = (char)(buf >> bits); + buf &= (1 << bits) - 1; + } + } + out[out_pos] = '\0'; + return out; +} + static const signed char b64_dec[256] = { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, diff --git a/c_bridges/uri-bridge.c b/c_bridges/uri-bridge.c new file mode 100644 index 00000000..832f43be --- /dev/null +++ b/c_bridges/uri-bridge.c @@ -0,0 +1,66 @@ +#include +#include + +extern void* GC_malloc_atomic(size_t sz); + +static const char hex_chars[17] = "0123456789ABCDEF"; + +static int is_unreserved(unsigned char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~'; +} + +char* cs_encode_uri_component(const char* input) { + if (!input) { + char* out = (char*)GC_malloc_atomic(1); + out[0] = '\0'; + return out; + } + size_t in_len = strlen(input); + char* out = (char*)GC_malloc_atomic(in_len * 3 + 1); + size_t j = 0; + for (size_t i = 0; i < in_len; i++) { + unsigned char c = (unsigned char)input[i]; + if (is_unreserved(c)) { + out[j++] = (char)c; + } else { + out[j++] = '%'; + out[j++] = hex_chars[(c >> 4) & 0xF]; + out[j++] = hex_chars[c & 0xF]; + } + } + out[j] = '\0'; + return out; +} + +static int hex_val(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +char* cs_decode_uri_component(const char* input) { + if (!input) { + char* out = (char*)GC_malloc_atomic(1); + out[0] = '\0'; + return out; + } + size_t in_len = strlen(input); + char* out = (char*)GC_malloc_atomic(in_len + 1); + size_t j = 0; + for (size_t i = 0; i < in_len; i++) { + if (input[i] == '%' && i + 2 < in_len) { + int hi = hex_val(input[i + 1]); + int lo = hex_val(input[i + 2]); + if (hi >= 0 && lo >= 0) { + out[j++] = (char)((hi << 4) | lo); + i += 2; + continue; + } + } + out[j++] = input[i]; + } + out[j] = '\0'; + return out; +} diff --git a/docs/stdlib/crypto.md b/docs/stdlib/crypto.md index 48b5b173..3472f851 100644 --- a/docs/stdlib/crypto.md +++ b/docs/stdlib/crypto.md @@ -37,6 +37,17 @@ const sig = crypto.hmacSha256("secret", "payload"); // "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" ``` +## `crypto.pbkdf2(password, salt, iterations, keylen)` + +Derive a key using PBKDF2-HMAC-SHA256. Returns a hex string of `keylen` bytes (2×keylen characters). Use for password hashing and key derivation. + +```typescript +const hash = crypto.pbkdf2("password", "salt", 1, 20); +// "0c60c80f961f0e71f3a9b524af6012062fe037a6" + +const key = crypto.pbkdf2(userPassword, randomSalt, 100000, 32); +``` + ## `crypto.randomBytes(n)` Generate `n` random bytes, returned as a hex string (2n characters). @@ -77,5 +88,6 @@ console.log(uuid); | `crypto.sha512()` | OpenSSL EVP API (`EVP_sha512`) | | `crypto.md5()` | OpenSSL EVP API (`EVP_md5`) | | `crypto.hmacSha256()` | OpenSSL `HMAC()` with `EVP_sha256` | +| `crypto.pbkdf2()` | OpenSSL `PKCS5_PBKDF2_HMAC()` with `EVP_sha256` | | `crypto.randomBytes()` | `RAND_bytes()` | | `crypto.randomUUID()` | `RAND_bytes(16)` + version/variant bits + `snprintf` | diff --git a/docs/stdlib/encoding.md b/docs/stdlib/encoding.md new file mode 100644 index 00000000..42e8a10b --- /dev/null +++ b/docs/stdlib/encoding.md @@ -0,0 +1,63 @@ +# Encoding + +Global functions for base64 and percent-encoding (URL encoding). Available without any import. + +## `btoa(str)` + +Encode a string to base64. + +```typescript +const encoded = btoa("hello world"); +// "aGVsbG8gd29ybGQ=" + +const auth = "Basic " + btoa(username + ":" + password); +``` + +## `atob(str)` + +Decode a base64 string. + +```typescript +const decoded = atob("aGVsbG8gd29ybGQ="); +// "hello world" +``` + +## `encodeURIComponent(str)` + +Percent-encode a string per RFC 3986. Unreserved characters (`A-Z a-z 0-9 - _ . ~`) are left as-is; everything else is encoded as `%XX`. + +```typescript +const q = encodeURIComponent("hello world & foo=bar"); +// "hello%20world%20%26%20foo%3Dbar" + +const url = "https://api.example.com/search?q=" + encodeURIComponent(userInput); +``` + +## `decodeURIComponent(str)` + +Decode a percent-encoded string. + +```typescript +const s = decodeURIComponent("hello%20world%20%26%20foo%3Dbar"); +// "hello world & foo=bar" +``` + +## Example + +```typescript +const token = btoa("user:pass"); +console.log(token); // "dXNlcjpwYXNz" +console.log(atob(token)); // "user:pass" + +const params = "name=" + encodeURIComponent("John Doe") + "&city=" + encodeURIComponent("New York"); +// "name=John%20Doe&city=New%20York" +``` + +## Native Implementation + +| API | Maps to | +|-----|---------| +| `btoa()` | `cs_btoa()` in `c_bridges/base64-bridge.c` | +| `atob()` | `cs_atob()` in `c_bridges/base64-bridge.c` | +| `encodeURIComponent()` | `cs_encode_uri_component()` in `c_bridges/uri-bridge.c` | +| `decodeURIComponent()` | `cs_decode_uri_component()` in `c_bridges/uri-bridge.c` | diff --git a/docs/stdlib/index.md b/docs/stdlib/index.md index 15267658..6616cd4c 100644 --- a/docs/stdlib/index.md +++ b/docs/stdlib/index.md @@ -11,7 +11,11 @@ Most APIs are available everywhere with no import: ```typescript console.log("hello"); const data = fs.readFileSync("file.txt"); -const hash = crypto.createHash("sha256"); +const hash = crypto.sha256("hello"); +const encoded = btoa("hello"); +const url = "https://api.example.com/q=" + encodeURIComponent(query); +const u = new URL("https://example.com:8080/path?q=hello"); +const p = new URLSearchParams("q=hello&page=2"); ``` ## Modules diff --git a/docs/stdlib/url.md b/docs/stdlib/url.md new file mode 100644 index 00000000..f43dac66 --- /dev/null +++ b/docs/stdlib/url.md @@ -0,0 +1,72 @@ +# URL + +Built-in `URL` and `URLSearchParams` classes for parsing and manipulating URLs. Available without any import. + +## `URL` + +Parse a URL string into its components. + +```typescript +const u = new URL("https://example.com:8080/path/to/page?q=hello&page=2#section"); + +u.protocol // "https:" +u.hostname // "example.com" +u.port // "8080" +u.host // "example.com:8080" +u.pathname // "/path/to/page" +u.search // "?q=hello&page=2" +u.hash // "#section" +u.origin // "https://example.com:8080" +u.href // "https://example.com:8080/path/to/page?q=hello&page=2#section" +``` + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `protocol` | `string` | Scheme with colon — `"https:"` | +| `hostname` | `string` | Host without port — `"example.com"` | +| `port` | `string` | Port number as string, or `""` if absent | +| `host` | `string` | `hostname` + `:` + `port` if port present | +| `pathname` | `string` | Path component — `"/path/to/page"` | +| `search` | `string` | Query string including `?`, or `""` | +| `hash` | `string` | Fragment including `#`, or `""` | +| `origin` | `string` | `protocol + "//" + host` | +| `href` | `string` | Full URL string | + +## `URLSearchParams` + +Parse and manipulate query strings. + +```typescript +const p = new URLSearchParams("q=hello&page=2"); + +p.get("q") // "hello" +p.get("page") // "2" +p.has("q") // true +p.has("foo") // false + +p.set("q", "world") +p.append("tag", "foo") +p.delete("page") + +p.toString() // "q=world&tag=foo" +``` + +### Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `get(key)` | `string` | Value for key, or `""` if absent | +| `has(key)` | `boolean` | Whether key exists | +| `set(key, value)` | `void` | Set or replace value for key | +| `append(key, value)` | `void` | Add key/value (allows duplicates) | +| `delete(key)` | `void` | Remove all entries for key | +| `toString()` | `string` | Serialize back to query string (no leading `?`) | + +## Native Implementation + +| API | Maps to | +|-----|---------| +| `URL` properties | `cs_url_parse_*()` in `c_bridges/url-bridge.c` | +| `URLSearchParams` methods | `cs_urlsearch_*()` in `c_bridges/url-bridge.c` | diff --git a/scripts/build-target-sdk.sh b/scripts/build-target-sdk.sh index 86133883..b0fdb52e 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 url-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 uri-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 diff --git a/scripts/build-vendor.sh b/scripts/build-vendor.sh index 1372e6a1..20c303a5 100755 --- a/scripts/build-vendor.sh +++ b/scripts/build-vendor.sh @@ -232,6 +232,16 @@ else echo "==> url-bridge already built, skipping" fi +# --- uri-bridge --- +URI_BRIDGE_SRC="$C_BRIDGES_DIR/uri-bridge.c" +URI_BRIDGE_OBJ="$C_BRIDGES_DIR/uri-bridge.o" +if [ ! -f "$URI_BRIDGE_OBJ" ] || [ "$URI_BRIDGE_SRC" -nt "$URI_BRIDGE_OBJ" ]; then + echo "==> Building uri-bridge..." + cc -c -O2 -fPIC "$URI_BRIDGE_SRC" -o "$URI_BRIDGE_OBJ" + echo " -> $URI_BRIDGE_OBJ" +else + echo "==> uri-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/calls.ts b/src/codegen/expressions/calls.ts index ad073514..9444de72 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -198,6 +198,46 @@ export class CallExpressionGenerator { return this.generateIsNaN(expr, params); } + if (expr.name === "btoa") { + if (expr.args.length !== 1) { + return this.ctx.emitError("btoa() requires exactly 1 argument", expr.loc); + } + const arg = this.ctx.generateExpression(expr.args[0], params); + const result = this.ctx.emitCall("i8*", "@cs_btoa", `i8* ${arg}`); + this.ctx.setVariableType(result, "i8*"); + return result; + } + + if (expr.name === "atob") { + if (expr.args.length !== 1) { + return this.ctx.emitError("atob() requires exactly 1 argument", expr.loc); + } + const arg = this.ctx.generateExpression(expr.args[0], params); + const result = this.ctx.emitCall("i8*", "@cs_atob", `i8* ${arg}`); + this.ctx.setVariableType(result, "i8*"); + return result; + } + + if (expr.name === "encodeURIComponent") { + if (expr.args.length !== 1) { + return this.ctx.emitError("encodeURIComponent() requires exactly 1 argument", expr.loc); + } + const arg = this.ctx.generateExpression(expr.args[0], params); + const result = this.ctx.emitCall("i8*", "@cs_encode_uri_component", `i8* ${arg}`); + this.ctx.setVariableType(result, "i8*"); + return result; + } + + if (expr.name === "decodeURIComponent") { + if (expr.args.length !== 1) { + return this.ctx.emitError("decodeURIComponent() requires exactly 1 argument", expr.loc); + } + const arg = this.ctx.generateExpression(expr.args[0], params); + const result = this.ctx.emitCall("i8*", "@cs_decode_uri_component", `i8* ${arg}`); + this.ctx.setVariableType(result, "i8*"); + return result; + } + // Handle C built-in functions with proper signatures if (expr.name === "malloc") { return this.generateMalloc(expr, params); diff --git a/src/codegen/expressions/method-calls.ts b/src/codegen/expressions/method-calls.ts index 4b5b9007..bca37b49 100644 --- a/src/codegen/expressions/method-calls.ts +++ b/src/codegen/expressions/method-calls.ts @@ -842,6 +842,8 @@ export class MethodCallGenerator { return this.ctx.cryptoGen.generateRandomUUID(expr, params); } else if (method === "hmacSha256") { return this.ctx.cryptoGen.generateHmacSha256(expr, params); + } else if (method === "pbkdf2") { + return this.ctx.cryptoGen.generatePbkdf2(expr, params); } } diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index 8f379535..c10ce7f6 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -196,6 +196,7 @@ export interface ICryptoGenerator { generateRandomBytes(expr: MethodCallNode, params: string[]): string; generateRandomUUID(expr: MethodCallNode, params: string[]): string; generateHmacSha256(expr: MethodCallNode, params: string[]): string; + generatePbkdf2(expr: MethodCallNode, params: string[]): string; } export interface ISqliteGenerator { @@ -1917,6 +1918,7 @@ export class MockGeneratorContext implements IGeneratorContext { "%mock_crypto_random_uuid", generateHmacSha256: (_expr: MethodCallNode, _params: string[]): string => "%mock_crypto_hmac_sha256", + generatePbkdf2: (_expr: MethodCallNode, _params: string[]): string => "%mock_crypto_pbkdf2", }; sqliteGen: ISqliteGenerator = { canHandle: (_expr: MethodCallNode): boolean => false, diff --git a/src/codegen/infrastructure/llvm-declarations.ts b/src/codegen/infrastructure/llvm-declarations.ts index 79f69c5e..3f05b279 100644 --- a/src/codegen/infrastructure/llvm-declarations.ts +++ b/src/codegen/infrastructure/llvm-declarations.ts @@ -87,8 +87,15 @@ export function getLLVMDeclarations(config?: DeclConfig): string { ir += "declare void @cs_spawn(i8*, i8**, i32, void (i8*)*, void (i8*)*, void (double)*)\n"; ir += "\n"; - // base64 bridge — Buffer.from(str, 'base64') → %Uint8Array* + // base64 bridge — Buffer.from(str, 'base64') → %Uint8Array*; btoa/atob → i8* ir += "declare i8* @cs_base64_decode(i8*)\n"; + ir += "declare i8* @cs_btoa(i8*)\n"; + ir += "declare i8* @cs_atob(i8*)\n"; + ir += "\n"; + + // uri bridge — encodeURIComponent / decodeURIComponent + ir += "declare i8* @cs_encode_uri_component(i8*)\n"; + ir += "declare i8* @cs_decode_uri_component(i8*)\n"; ir += "\n"; // url bridge — URL parsing and URLSearchParams manipulation @@ -242,11 +249,13 @@ export function getLLVMDeclarations(config?: DeclConfig): string { ir += "declare i32 @EVP_DigestInit_ex(i8*, i8*, i8*)\n"; ir += "declare i32 @EVP_DigestUpdate(i8*, i8*, i64)\n"; ir += "declare i32 @EVP_DigestFinal_ex(i8*, i8*, i32*)\n"; + ir += "declare i8* @EVP_sha1()\n"; ir += "declare i8* @EVP_sha256()\n"; ir += "declare i8* @EVP_md5()\n"; ir += "declare i8* @EVP_sha512()\n"; ir += "declare i32 @RAND_bytes(i8*, i32)\n"; ir += "declare i8* @HMAC(i8*, i8*, i32, i8*, i64, i8*, i32*)\n"; + ir += "declare i32 @PKCS5_PBKDF2_HMAC(i8*, i32, i8*, i32, i32, i8*, i32, i8*)\n"; ir += "\n"; } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index e95be98b..8ea30c47 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -1898,6 +1898,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { let isClassInstance = false; let isUint8Array = false; let isBoolean = false; + let isNumber = false; if (resolved) { const base = resolved.base; const depth = resolved.arrayDepth; @@ -1910,6 +1911,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { isRegex = base === "RegExp"; isObject = base === "object" && depth === 0; isBoolean = base === "boolean" && depth === 0; + isNumber = base === "number" && depth === 0; isUint8Array = base === "Uint8Array" && depth === 0; isClassInstance = !isRegex && @@ -2146,6 +2148,10 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { llvmType = "double"; kind = SymbolKind.Boolean; defaultValue = "0.0"; + } else if (isNumber) { + llvmType = "double"; + kind = SymbolKind.Number; + defaultValue = "0.0"; } else if (isJSONParse) { const interfaceName = this.typeInference.getJSONParseInterface( stmt.value as MethodCallNode, @@ -3520,7 +3526,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { private getGenericMethodReturnError(expr: MethodCallNode, varName: string): string | null { const objBase = expr.object as { type: string }; if (objBase.type !== "variable") return null; - const objName = (expr.object as { type: string; name: string }).name; + const objName = (expr.object as VariableNode).name; const className = this.symbolTable.getConcreteClass(objName); if (!className || !this.ast || !this.ast.classes) return null; for (let i = 0; i < this.ast.classes.length; i++) { diff --git a/src/codegen/stdlib/crypto.ts b/src/codegen/stdlib/crypto.ts index b943de00..92ae6d75 100644 --- a/src/codegen/stdlib/crypto.ts +++ b/src/codegen/stdlib/crypto.ts @@ -14,7 +14,15 @@ export class CryptoGenerator { if (exprObjBase.type !== "variable") return false; const varNode = expr.object as { type: string; name: string }; if (varNode.name !== "crypto") return false; - const supported = ["sha256", "md5", "sha512", "randomBytes", "randomUUID", "hmacSha256"]; + const supported = [ + "sha256", + "md5", + "sha512", + "randomBytes", + "randomUUID", + "hmacSha256", + "pbkdf2", + ]; return supported.indexOf(expr.method) !== -1; } @@ -212,6 +220,57 @@ export class CryptoGenerator { return result; } + generatePbkdf2(expr: MethodCallNode, params: string[]): string { + if (expr.args.length < 4) { + return this.ctx.emitError( + "crypto.pbkdf2() requires 4 arguments (password, salt, iterations, keylen)", + expr.loc, + ); + } + + const passwordPtr = this.ctx.generateExpression(expr.args[0], params); + const saltPtr = this.ctx.generateExpression(expr.args[1], params); + const iterDouble = this.ctx.generateExpression(expr.args[2], params); + const keylenDouble = this.ctx.generateExpression(expr.args[3], params); + + const passLen = this.ctx.nextTemp(); + this.ctx.emit(`${passLen} = call i64 @strlen(i8* ${passwordPtr})`); + const passLen32 = this.ctx.nextTemp(); + this.ctx.emit(`${passLen32} = trunc i64 ${passLen} to i32`); + + const saltLen = this.ctx.nextTemp(); + this.ctx.emit(`${saltLen} = call i64 @strlen(i8* ${saltPtr})`); + const saltLen32 = this.ctx.nextTemp(); + this.ctx.emit(`${saltLen32} = trunc i64 ${saltLen} to i32`); + + const iterD = this.ctx.ensureDouble(iterDouble); + const iterI32 = this.ctx.nextTemp(); + this.ctx.emit(`${iterI32} = fptosi double ${iterD} to i32`); + + const keylenD = this.ctx.ensureDouble(keylenDouble); + const keylenI32 = this.ctx.nextTemp(); + this.ctx.emit(`${keylenI32} = fptosi double ${keylenD} to i32`); + const keylenI64 = this.ctx.nextTemp(); + this.ctx.emit(`${keylenI64} = sext i32 ${keylenI32} to i64`); + + const outBuf = this.ctx.nextTemp(); + this.ctx.emit(`${outBuf} = call i8* @GC_malloc_atomic(i64 ${keylenI64})`); + + const evpMd = this.ctx.nextTemp(); + this.ctx.emit(`${evpMd} = call i8* @EVP_sha1()`); + + const pbkdfResult = this.ctx.nextTemp(); + this.ctx.emit( + `${pbkdfResult} = call i32 @PKCS5_PBKDF2_HMAC(i8* ${passwordPtr}, i32 ${passLen32}, i8* ${saltPtr}, i32 ${saltLen32}, i32 ${iterI32}, i8* ${evpMd}, i32 ${keylenI32}, i8* ${outBuf})`, + ); + + const result = this.ctx.nextTemp(); + this.ctx.emit(`${result} = call i8* @__bytes_to_hex(i8* ${outBuf}, i32 ${keylenI32})`); + this.ctx.setVariableType(result, "i8*"); + + return result; + } + generateBytesToHexHelper(): string { let ir = ""; ir += diff --git a/src/compiler.ts b/src/compiler.ts index 5af4ed60..7a646ee6 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -406,6 +406,7 @@ export function compile( const timeBridgeObj = `${bridgePath}/time-bridge.o`; const base64BridgeObj = `${bridgePath}/base64-bridge.o`; const urlBridgeObj = `${bridgePath}/url-bridge.o`; + const uriBridgeObj = `${bridgePath}/uri-bridge.o`; const dotenvBridgeObj = fs.existsSync(`${bridgePath}/dotenv-bridge.o`) ? `${bridgePath}/dotenv-bridge.o` : ""; @@ -489,7 +490,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} ${urlBridgeObj} ${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} ${uriBridgeObj} ${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 c54a4d93..2ac98feb 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -507,6 +507,7 @@ export function compileNative(inputFile: string, outputFile: string): void { const timeBridgeObj = effectiveBridgePath + "/time-bridge.o"; const base64BridgeObj = effectiveBridgePath + "/base64-bridge.o"; const urlBridgeObj = effectiveBridgePath + "/url-bridge.o"; + const uriBridgeObj = effectiveBridgePath + "/uri-bridge.o"; const dotenvBridgePath = effectiveBridgePath + "/dotenv-bridge.o"; const dotenvBridgeObj = fs.existsSync(dotenvBridgePath) ? dotenvBridgePath : ""; const watchBridgeObj = effectiveBridgePath + "/watch-bridge.o"; @@ -587,6 +588,8 @@ export function compileNative(inputFile: string, outputFile: string): void { " " + urlBridgeObj + " " + + uriBridgeObj + + " " + dotenvBridgeObj + " " + watchBridgeObj + diff --git a/tests/fixtures/builtins/btoa-atob.ts b/tests/fixtures/builtins/btoa-atob.ts new file mode 100644 index 00000000..ebb9ef25 --- /dev/null +++ b/tests/fixtures/builtins/btoa-atob.ts @@ -0,0 +1,23 @@ +function testBtoaAtob(): void { + const encoded = btoa("Hello, World!"); + const expected = "SGVsbG8sIFdvcmxkIQ=="; + if (encoded !== expected) { + console.log("FAILED btoa: got " + encoded); + process.exit(1); + } + + const decoded = atob(encoded); + if (decoded !== "Hello, World!") { + console.log("FAILED atob: got " + decoded); + process.exit(1); + } + + const roundtrip = atob(btoa("ChadScript")); + if (roundtrip !== "ChadScript") { + console.log("FAILED roundtrip: got " + roundtrip); + process.exit(1); + } + + console.log("TEST_PASSED"); +} +testBtoaAtob(); diff --git a/tests/fixtures/builtins/encode-uri-component.ts b/tests/fixtures/builtins/encode-uri-component.ts new file mode 100644 index 00000000..532fc7f5 --- /dev/null +++ b/tests/fixtures/builtins/encode-uri-component.ts @@ -0,0 +1,23 @@ +function testEncodeUri(): void { + const encoded = encodeURIComponent("hello world & foo=bar"); + const expected = "hello%20world%20%26%20foo%3Dbar"; + if (encoded !== expected) { + console.log("FAILED encode: got " + encoded); + process.exit(1); + } + + const decoded = decodeURIComponent(encoded); + if (decoded !== "hello world & foo=bar") { + console.log("FAILED decode: got " + decoded); + process.exit(1); + } + + const unreserved = encodeURIComponent("abc-_.~123"); + if (unreserved !== "abc-_.~123") { + console.log("FAILED unreserved: got " + unreserved); + process.exit(1); + } + + console.log("TEST_PASSED"); +} +testEncodeUri(); diff --git a/tests/fixtures/crypto/pbkdf2.ts b/tests/fixtures/crypto/pbkdf2.ts new file mode 100644 index 00000000..840c908a --- /dev/null +++ b/tests/fixtures/crypto/pbkdf2.ts @@ -0,0 +1,10 @@ +function testPbkdf2(): void { + const result = crypto.pbkdf2("password", "salt", 1, 20); + const expected = "0c60c80f961f0e71f3a9b524af6012062fe037a6"; + if (result !== expected) { + console.log("FAILED: got " + result); + process.exit(1); + } + console.log("TEST_PASSED"); +} +testPbkdf2(); diff --git a/tests/self-hosting.test.ts b/tests/self-hosting.test.ts index 0720a034..2fbd1aa2 100644 --- a/tests/self-hosting.test.ts +++ b/tests/self-hosting.test.ts @@ -20,6 +20,7 @@ const FIXTURE_OUT_DIR = "/tmp/self-hosting-fixtures"; const isMac = process.platform === "darwin"; const brewPrefix = process.arch === "arm64" ? "/opt/homebrew" : "/usr/local"; +const targetCpuFlag = process.arch === "x64" ? "--target-cpu=x86-64" : ""; const NATIVE_ENV: NodeJS.ProcessEnv = { PATH: process.env.PATH, @@ -167,9 +168,12 @@ describe("Self-Hosting", { timeout: 600000 }, () => { it("Node.js → Stage 0: compile chad-native.ts", async () => { if (fsSync.existsSync(STAGE0)) fsSync.unlinkSync(STAGE0); - await execAsync(`node dist/chad-node.js build src/chad-native.ts -o ${STAGE0}`, { - timeout: 180000, - }); + await execAsync( + `node dist/chad-node.js build src/chad-native.ts -o ${STAGE0} ${targetCpuFlag}`.trim(), + { + timeout: 180000, + }, + ); assert.ok(fsSync.existsSync(STAGE0), `Stage 0 binary should exist at ${STAGE0}`); const stats = fsSync.statSync(STAGE0); @@ -204,7 +208,7 @@ describe("Self-Hosting", { timeout: 600000 }, () => { if (fsSync.existsSync(STAGE1)) fsSync.unlinkSync(STAGE1); await execWithRetry( - `${STAGE0} build -v src/chad-native.ts -o ${STAGE1} --target-cpu=x86-64`, + `${STAGE0} build -v src/chad-native.ts -o ${STAGE1} ${targetCpuFlag}`.trim(), { timeout: 180000, env: NATIVE_ENV,