From b92524280a4d493857e5dfed4ae813b8d5fac5dc Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 14:36:33 -0800 Subject: [PATCH 1/7] fix duplicate parse_json codegen, update parallel example and docs --- docs/stdlib/fetch.md | 21 ++++++++++++++++++++- examples/parallel.ts | 10 ++++------ src/codegen/stdlib/json.ts | 13 +++++++++++++ src/codegen/stdlib/response.ts | 13 +++++++++++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/docs/stdlib/fetch.md b/docs/stdlib/fetch.md index f3bc314e..8277f6f7 100644 --- a/docs/stdlib/fetch.md +++ b/docs/stdlib/fetch.md @@ -26,7 +26,7 @@ async function main(): any { | `text()` | `string` | Response body as a string | | `json()` | `T` | Parse response body as JSON with type parameter | -## Example +## Examples ```typescript interface Repo { @@ -42,6 +42,25 @@ async function main(): any { } ``` +### Parallel fetches with `Promise.all` + +```typescript +interface Repo { + stargazers_count: number; +} + +async function main(): Promise { + const results = await Promise.all([ + fetch("https://api.github.com/repos/vuejs/vue"), + fetch("https://api.github.com/repos/facebook/react"), + ]); + const vue = results[0].json(); + const react = results[1].json(); + console.log(`Vue: ${vue.stargazers_count} stars`); + console.log(`React: ${react.stargazers_count} stars`); +} +``` + ## Native Implementation | API | Maps to | diff --git a/examples/parallel.ts b/examples/parallel.ts index a6adf179..2f197fce 100644 --- a/examples/parallel.ts +++ b/examples/parallel.ts @@ -9,12 +9,10 @@ async function main(): Promise { fetch("https://api.github.com/repos/vuejs/vue"), fetch("https://api.github.com/repos/facebook/react"), ]); - - const vue = JSON.parse(results[0].text()); - const react = JSON.parse(results[1].text()); - - console.log("Vue: " + vue.stargazers_count + " stars"); - console.log("React: " + react.stargazers_count + " stars"); + const vue = results[0].json(); + const react = results[1].json(); + console.log(`Vue: ${vue.stargazers_count} stars`); + console.log(`React: ${react.stargazers_count} stars`); } main(); diff --git a/src/codegen/stdlib/json.ts b/src/codegen/stdlib/json.ts index 7ae96735..3f7c9b7e 100644 --- a/src/codegen/stdlib/json.ts +++ b/src/codegen/stdlib/json.ts @@ -187,6 +187,16 @@ export class JsonGenerator { return false; } + private hasParserInGlobalStrings(typeName: string): boolean { + const pattern = `@parse_json_${typeName}(i8* %json_str)`; + for (let i = 0; i < this.ctx.getGlobalStringsLength(); i++) { + if (this.ctx.getGlobalStringAt(i).includes(pattern)) { + return true; + } + } + return false; + } + private generateJsonStruct(typeName: string): void { if (this.hasGenerated(typeName)) { return; @@ -237,6 +247,9 @@ export class JsonGenerator { return; } this.markGenerated(parserKey); + if (this.hasParserInGlobalStrings(typeName)) { + return; + } const fieldCount = this.ctx.interfaceStructGenGetFieldCount(typeName); diff --git a/src/codegen/stdlib/response.ts b/src/codegen/stdlib/response.ts index 4c8f650b..d491334b 100644 --- a/src/codegen/stdlib/response.ts +++ b/src/codegen/stdlib/response.ts @@ -170,10 +170,23 @@ export class ResponseGenerator { } } + private hasParserInGlobalStrings(typeName: string): boolean { + const pattern = `@parse_json_${typeName}(i8* %json_str)`; + for (let i = 0; i < this.ctx.getGlobalStringsLength(); i++) { + if (this.ctx.getGlobalStringAt(i).includes(pattern)) { + return true; + } + } + return false; + } + /** * Generate a specialized JSON parser function for a struct type */ private generateJsonParser(typeName: string, interfaceDef: InterfaceDefInfo): void { + if (this.hasParserInGlobalStrings(typeName)) { + return; + } let parserIR = `define %${typeName}* @parse_json_${typeName}(i8* %json_str) {` + "\n"; parserIR += "entry:\n"; From 9acae284b946b8157eb8aa7a817aa08468e2b693 Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 14:46:08 -0800 Subject: [PATCH 2/7] fix JSON.parse() struct field access, add regression test --- src/codegen/infrastructure/type-inference.ts | 12 +++++++ .../json-parse-and-response-json-test.ts | 36 +++++++++++++++++++ tests/network.test.ts | 30 ++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 tests/fixtures/network/json-parse-and-response-json-test.ts diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index c890e89d..548a98d9 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -1749,6 +1749,18 @@ export class TypeInference { if (e.type === "method_call" && expr.method === "json" && expr.typeParameter) { return expr.typeParameter; } + if (e.type === "method_call" && expr.method === "parse" && expr.typeParameter) { + const objBase = expr.object as ExprBase; + if (objBase && objBase.type === "variable") { + const varNode = expr.object as { type: string; name: string }; + if (varNode.name === "JSON") { + const tp = expr.typeParameter; + if (tp !== "number[]" && tp !== "string" && tp !== "number" && tp !== "boolean") { + return tp; + } + } + } + } return null; } diff --git a/tests/fixtures/network/json-parse-and-response-json-test.ts b/tests/fixtures/network/json-parse-and-response-json-test.ts new file mode 100644 index 00000000..3c37aae1 --- /dev/null +++ b/tests/fixtures/network/json-parse-and-response-json-test.ts @@ -0,0 +1,36 @@ +// @test-skip +// Regression test: using JSON.parse() and response.json() with the same +// interface in the same file previously caused a duplicate parse_json_T +// function definition in the generated LLVM IR. + +interface Item { + id: number; + name: string; +} + +async function runTests(): Promise { + const fromParse = JSON.parse('{"id":1,"name":"parsed"}'); + if (fromParse.id !== 1) { + console.log("FAIL: JSON.parse id"); + process.exit(1); + } + if (fromParse.name !== "parsed") { + console.log("FAIL: JSON.parse name"); + process.exit(1); + } + + const response = await fetch("http://127.0.0.1:19882/item"); + const fromJson = response.json(); + if (fromJson.id !== 2) { + console.log("FAIL: response.json id"); + process.exit(1); + } + if (fromJson.name !== "fetched") { + console.log("FAIL: response.json name"); + process.exit(1); + } + + console.log("TEST_PASSED"); +} + +runTests(); diff --git a/tests/network.test.ts b/tests/network.test.ts index 8c3905a6..49779d39 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -125,6 +125,36 @@ describe("Network Tests", () => { } }); + it("should handle JSON.parse() and response.json() with the same type", async () => { + const server = http.createServer((req, res) => { + if (req.url === "/item") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"id":2,"name":"fetched"}'); + } else { + res.writeHead(404); + res.end("Not found"); + } + }); + + await new Promise((resolve) => { + server.listen(19882, "127.0.0.1", resolve); + }); + + try { + const testFile = "tests/fixtures/network/json-parse-and-response-json-test.ts"; + await execAsync(`node dist/chad-node.js build ${testFile}`); + const { stdout } = await execAsync( + ".build/tests/fixtures/network/json-parse-and-response-json-test", + ); + assert.ok( + stdout.includes("TEST_PASSED"), + "JSON.parse + response.json same type test should pass", + ); + } finally { + server.close(); + } + }); + it("should run Promise.race with resolved promises", async () => { const testFile = "tests/fixtures/network/promise-race-test.ts"; await execAsync(`node dist/chad-node.js build ${testFile}`); From f108e6c9d18dca73fe6b640b71b39b25a00d1aa7 Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 14:49:08 -0800 Subject: [PATCH 3/7] update parallel example with commented stringify note --- examples/parallel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/parallel.ts b/examples/parallel.ts index 2f197fce..4ab11e11 100644 --- a/examples/parallel.ts +++ b/examples/parallel.ts @@ -11,6 +11,7 @@ async function main(): Promise { ]); const vue = results[0].json(); const react = results[1].json(); + // console.log(JSON.stringify(results)); console.log(`Vue: ${vue.stargazers_count} stars`); console.log(`React: ${react.stargazers_count} stars`); } From 48193907e5856b6e13f8b948ec1611d8d42cf5b2 Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 14:50:37 -0800 Subject: [PATCH 4/7] add JSON.stringify(val, null, spaces) pretty-print support --- c_bridges/yyjson-bridge.c | 9 +++++++ src/codegen/runtime/runtime.ts | 1 + src/codegen/stdlib/json.ts | 44 +++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/c_bridges/yyjson-bridge.c b/c_bridges/yyjson-bridge.c index f7393046..ad408aa9 100644 --- a/c_bridges/yyjson-bridge.c +++ b/c_bridges/yyjson-bridge.c @@ -145,3 +145,12 @@ char *csyyjson_stringify(void *doc) { yyjson_mut_doc_free((yyjson_mut_doc *)doc); return result; } + +char *csyyjson_stringify_pretty(void *doc, int spaces) { + if (!doc) return NULL; + size_t len; + yyjson_write_flag flags = (spaces == 2) ? YYJSON_WRITE_PRETTY_TWO_SPACES : YYJSON_WRITE_PRETTY; + char *result = yyjson_mut_write((yyjson_mut_doc *)doc, flags, &len); + yyjson_mut_doc_free((yyjson_mut_doc *)doc); + return result; +} diff --git a/src/codegen/runtime/runtime.ts b/src/codegen/runtime/runtime.ts index 82b0c4a8..9c0e4897 100644 --- a/src/codegen/runtime/runtime.ts +++ b/src/codegen/runtime/runtime.ts @@ -262,6 +262,7 @@ export class RuntimeGenerator { ir += "declare void @csyyjson_obj_add_num(i8*, i8*, i8*, double)\n"; ir += "declare void @csyyjson_obj_add_bool(i8*, i8*, i8*, i32)\n"; ir += "declare i8* @csyyjson_stringify(i8*)\n"; + ir += "declare i8* @csyyjson_stringify_pretty(i8*, i32)\n"; ir += "declare i8* @csyyjson_create_arr()\n"; ir += "declare i8* @csyyjson_mut_arr_add_obj(i8*, i8*)\n"; ir += "\n"; diff --git a/src/codegen/stdlib/json.ts b/src/codegen/stdlib/json.ts index 3f7c9b7e..65bdf859 100644 --- a/src/codegen/stdlib/json.ts +++ b/src/codegen/stdlib/json.ts @@ -474,12 +474,34 @@ export class JsonGenerator { } } + private getSpaces(expr: MethodCallNode): number | null { + if (expr.args.length < 3) return null; + const spaceArg = expr.args[2] as { type: string; value?: number }; + if (spaceArg.type === "number" && typeof spaceArg.value === "number") { + return spaceArg.value; + } + return null; + } + + private emitStringify(jsonDoc: string, spaces: number | null): string { + if (spaces !== null) { + const spacesI32 = spaces === 2 ? "2" : "4"; + return this.ctx.emitCall( + "i8*", + "@csyyjson_stringify_pretty", + `i8* ${jsonDoc}, i32 ${spacesI32}`, + ); + } + return this.ctx.emitCall("i8*", "@csyyjson_stringify", `i8* ${jsonDoc}`); + } + generateStringify(expr: MethodCallNode, params: string[]): string { if (expr.args.length < 1) { return this.ctx.emitError("JSON.stringify() requires 1 argument", expr.loc); } const arg = expr.args[0]; + const spaces = this.getSpaces(expr); if (this.ctx.isStringExpression(arg)) { return this.stringifyString(arg, params); @@ -492,13 +514,13 @@ export class JsonGenerator { const varNode = arg as { type: string; name: string }; const elementType = this.ctx.symbolTable.getObjectArrayElementType(varNode.name); if (elementType) { - return this.stringifyObjectArray(arg, params, elementType); + return this.stringifyObjectArray(arg, params, elementType, spaces); } } const interfaceType = this.resolveInterfaceType(arg); if (interfaceType) { - return this.stringifyInterface(arg, params, interfaceType); + return this.stringifyInterface(arg, params, interfaceType, spaces); } return this.stringifyNumber(arg, params); @@ -531,7 +553,12 @@ export class JsonGenerator { return null; } - private stringifyInterface(arg: Expression, params: string[], interfaceType: string): string { + private stringifyInterface( + arg: Expression, + params: string[], + interfaceType: string, + spaces: number | null = null, + ): string { if (!this.ctx.interfaceStructGenHasInterface(interfaceType)) { return this.stringifyNumber(arg, params); } @@ -551,7 +578,7 @@ export class JsonGenerator { this.emitAddFieldsToJsonObj(typedPtr, structType, interfaceType, jsonDoc, jsonObj); - const result = this.ctx.emitCall("i8*", "@csyyjson_stringify", `i8* ${jsonDoc}`); + const result = this.emitStringify(jsonDoc, spaces); this.ctx.setVariableType(result, "i8*"); return result; @@ -612,7 +639,12 @@ export class JsonGenerator { } /** Stringify an ObjectArray (e.g. Post[]) as a JSON array of objects */ - private stringifyObjectArray(arg: Expression, params: string[], elementType: string): string { + private stringifyObjectArray( + arg: Expression, + params: string[], + elementType: string, + spaces: number | null = null, + ): string { if (!this.ctx.interfaceStructGenHasInterface(elementType)) { return this.stringifyNumber(arg, params); } @@ -678,7 +710,7 @@ export class JsonGenerator { this.ctx.emitLabel(loopEnd); - const result = this.ctx.emitCall("i8*", "@csyyjson_stringify", `i8* ${jsonDoc}`); + const result = this.emitStringify(jsonDoc, spaces); this.ctx.setVariableType(result, "i8*"); return result; From ad24d1185ea87d7b57028c71c16ca71d0da3a767 Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 15:08:03 -0800 Subject: [PATCH 5/7] fix JSON.stringify(obj) for response.json() variables allocateTypedJsonInterface now calls setRawInterfaceType so resolveInterfaceType in json.ts can find the interface name and dispatch to stringifyInterface instead of falling through to stringifyNumber (which used the struct pointer as a double) --- examples/parallel.ts | 3 ++- src/codegen/infrastructure/variable-allocator.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/parallel.ts b/examples/parallel.ts index 4ab11e11..12f950cd 100644 --- a/examples/parallel.ts +++ b/examples/parallel.ts @@ -2,6 +2,8 @@ interface Repo { stargazers_count: number; + updated_at: string; + archived: boolean; } async function main(): Promise { @@ -11,7 +13,6 @@ async function main(): Promise { ]); const vue = results[0].json(); const react = results[1].json(); - // console.log(JSON.stringify(results)); console.log(`Vue: ${vue.stargazers_count} stars`); console.log(`React: ${react.stargazers_count} stars`); } diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index 6922fc3b..8a5e9b4a 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -1448,6 +1448,7 @@ export class VariableAllocator { const allocaReg = this.ctx.nextAllocaReg(stmt.name); const structType = `%${interfaceName}*`; this.ctx.defineVariable(stmt.name, allocaReg, structType, SymbolKind.Object, "local"); + this.ctx.symbolTable.setRawInterfaceType(stmt.name, interfaceName); this.ctx.emit(`${allocaReg} = alloca ${structType}`); const structPtr = this.ctx.generateExpression(stmt.value!, params); From 5405d914e89dd51de720be7025870ac1a3d696f4 Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 15:12:19 -0800 Subject: [PATCH 6/7] rebuild libyyjson.a with csyyjson_stringify_pretty; add staleness check to build-vendor.sh --- scripts/build-vendor.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-vendor.sh b/scripts/build-vendor.sh index f890f786..04f28b20 100755 --- a/scripts/build-vendor.sh +++ b/scripts/build-vendor.sh @@ -62,7 +62,7 @@ else fi # --- yyjson --- -if [ ! -f "$VENDOR_DIR/yyjson/libyyjson.a" ]; then +if [ ! -f "$VENDOR_DIR/yyjson/libyyjson.a" ] || [ "$C_BRIDGES_DIR/yyjson-bridge.c" -nt "$VENDOR_DIR/yyjson/libyyjson.a" ]; then echo "==> Building yyjson..." cd "$VENDOR_DIR" if [ ! -d yyjson ]; then From 062348d8506df8c2e6ede2cc3a365f874c29cf8e Mon Sep 17 00:00:00 2001 From: cs01 Date: Thu, 26 Feb 2026 15:16:50 -0800 Subject: [PATCH 7/7] fix number|null spaces param causes ptr/double mismatch in self-hosted build --- src/codegen/stdlib/json.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/codegen/stdlib/json.ts b/src/codegen/stdlib/json.ts index 65bdf859..b76ae8e0 100644 --- a/src/codegen/stdlib/json.ts +++ b/src/codegen/stdlib/json.ts @@ -474,17 +474,17 @@ export class JsonGenerator { } } - private getSpaces(expr: MethodCallNode): number | null { - if (expr.args.length < 3) return null; + private getSpaces(expr: MethodCallNode): number { + if (expr.args.length < 3) return 0; const spaceArg = expr.args[2] as { type: string; value?: number }; if (spaceArg.type === "number" && typeof spaceArg.value === "number") { return spaceArg.value; } - return null; + return 0; } - private emitStringify(jsonDoc: string, spaces: number | null): string { - if (spaces !== null) { + private emitStringify(jsonDoc: string, spaces: number): string { + if (spaces > 0) { const spacesI32 = spaces === 2 ? "2" : "4"; return this.ctx.emitCall( "i8*", @@ -557,7 +557,7 @@ export class JsonGenerator { arg: Expression, params: string[], interfaceType: string, - spaces: number | null = null, + spaces: number = 0, ): string { if (!this.ctx.interfaceStructGenHasInterface(interfaceType)) { return this.stringifyNumber(arg, params); @@ -643,7 +643,7 @@ export class JsonGenerator { arg: Expression, params: string[], elementType: string, - spaces: number | null = null, + spaces: number = 0, ): string { if (!this.ctx.interfaceStructGenHasInterface(elementType)) { return this.stringifyNumber(arg, params);