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/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..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 { @@ -9,12 +11,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/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 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/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); 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 7ae96735..b76ae8e0 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); @@ -461,12 +474,34 @@ export class JsonGenerator { } } + 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 0; + } + + private emitStringify(jsonDoc: string, spaces: number): string { + if (spaces > 0) { + 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); @@ -479,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); @@ -518,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 = 0, + ): string { if (!this.ctx.interfaceStructGenHasInterface(interfaceType)) { return this.stringifyNumber(arg, params); } @@ -538,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; @@ -599,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 = 0, + ): string { if (!this.ctx.interfaceStructGenHasInterface(elementType)) { return this.stringifyNumber(arg, params); } @@ -665,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; 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"; 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}`);