Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions c_bridges/yyjson-bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
21 changes: 20 additions & 1 deletion docs/stdlib/fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function main(): any {
| `text()` | `string` | Response body as a string |
| `json<T>()` | `T` | Parse response body as JSON with type parameter |

## Example
## Examples

```typescript
interface Repo {
Expand All @@ -42,6 +42,25 @@ async function main(): any {
}
```

### Parallel fetches with `Promise.all`

```typescript
interface Repo {
stargazers_count: number;
}

async function main(): Promise<void> {
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<Repo>();
const react = results[1].json<Repo>();
console.log(`Vue: ${vue.stargazers_count} stars`);
console.log(`React: ${react.stargazers_count} stars`);
}
```

## Native Implementation

| API | Maps to |
Expand Down
12 changes: 6 additions & 6 deletions examples/parallel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

interface Repo {
stargazers_count: number;
updated_at: string;
archived: boolean;
}

async function main(): Promise<void> {
const results = await Promise.all([
fetch("https://api.github.com/repos/vuejs/vue"),
fetch("https://api.github.com/repos/facebook/react"),
]);

const vue = JSON.parse<Repo>(results[0].text());
const react = JSON.parse<Repo>(results[1].text());

console.log("Vue: " + vue.stargazers_count + " stars");
console.log("React: " + react.stargazers_count + " stars");
const vue = results[0].json<Repo>();
const react = results[1].json<Repo>();
console.log(`Vue: ${vue.stargazers_count} stars`);
console.log(`React: ${react.stargazers_count} stars`);
}

main();
2 changes: 1 addition & 1 deletion scripts/build-vendor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/codegen/infrastructure/type-inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/codegen/infrastructure/variable-allocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/codegen/runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
57 changes: 51 additions & 6 deletions src/codegen/stdlib/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -237,6 +247,9 @@ export class JsonGenerator {
return;
}
this.markGenerated(parserKey);
if (this.hasParserInGlobalStrings(typeName)) {
return;
}

const fieldCount = this.ctx.interfaceStructGenGetFieldCount(typeName);

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/codegen/stdlib/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
36 changes: 36 additions & 0 deletions tests/fixtures/network/json-parse-and-response-json-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @test-skip
// Regression test: using JSON.parse<T>() and response.json<T>() 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<void> {
const fromParse = JSON.parse<Item>('{"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<Item>();
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();
30 changes: 30 additions & 0 deletions tests/network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,36 @@ describe("Network Tests", () => {
}
});

it("should handle JSON.parse<T>() and response.json<T>() 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<void>((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}`);
Expand Down
Loading