Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
add bytesResponse() primitive, make serveFile and c.bytes() real chad…
…script
  • Loading branch information
cs01 committed Mar 5, 2026
commit 2b47e2aac41a5370d78ce892a833cb7e9ca66001
6 changes: 5 additions & 1 deletion chadscript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,16 +338,19 @@ declare module "chadscript/http" {
export function wsBroadcast(message: string): void;
export function wsSend(connId: string, message: string): void;
export function parseMultipart(req: HttpRequest): MultipartPart[];
export function serveFile(path: string): HttpResponse;
export function bytesResponse(data: Uint8Array, status: number, headers: string): HttpResponse;
export function serveFile(path: string, contentType: string): HttpResponse;

export class RouterRequest {
method: string;
path: string;
body: string;
contentType: string;
headers: string;
bodyLen: number;
param(name: string): string;
header(name: string): string;
bodyBytes(): Uint8Array;
}

export class Context {
Expand All @@ -358,6 +361,7 @@ declare module "chadscript/http" {
json(data: string): HttpResponse;
html(body: string): HttpResponse;
redirect(url: string): HttpResponse;
bytes(data: Uint8Array, contentType: string): HttpResponse;
}

export class Router {
Expand Down
14 changes: 13 additions & 1 deletion lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ export function wsSend(connId: string, message: string): void {}
export function parseMultipart(req: HttpRequest): MultipartPart[] {
return [];
}
export function serveFile(path: string): HttpResponse {
export function bytesResponse(data: Uint8Array, status: number, headers: string): HttpResponse {
return { status: 0, body: "", headers: "", bodyLen: 0 };
}
export function serveFile(path: string, contentType: string): HttpResponse {
const data: Uint8Array = fs.readFileSync(path);
return bytesResponse(data, 200.0, "Content-Type: " + contentType);
}

export class RouterRequest {
method: string;
Expand Down Expand Up @@ -165,6 +169,14 @@ export class Context {
return { status: 302, body: "", headers: hdrs, bodyLen: 0 };
}

bytes(data: Uint8Array, contentType: string): HttpResponse {
let hdrs = "Content-Type: " + contentType;
if (this._extraHeaders.length > 0) {
hdrs = hdrs + "\n" + this._extraHeaders;
}
return bytesResponse(data, this._status, hdrs);
}

getResult(): HttpResponse {
return {
status: this._resultStatus,
Expand Down
69 changes: 67 additions & 2 deletions src/codegen/expressions/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export class CallExpressionGenerator {
return this.ctx.generateParseMultipart(expr, params);
}

if (expr.name === "serveFile") {
return this.ctx.embedGen.generateServeFile(expr, params);
if (expr.name === "bytesResponse") {
return this.generateBytesResponse(expr, params);
}

// Handle setTimeout() - libuv timer (one-shot)
Expand Down Expand Up @@ -1217,4 +1217,69 @@ export class CallExpressionGenerator {
}
return "i8*";
}

// bytesResponse(data: Uint8Array, status: number, headers: string): HttpResponse
// Extracts raw pointer + length from Uint8Array and constructs an HttpResponse struct.
private generateBytesResponse(expr: CallNode, params: string[]): string {
if (expr.args.length < 3) {
return this.ctx.emitError(
"bytesResponse() requires 3 arguments (data, status, headers)",
expr.loc,
);
}

const arrayPtr = this.ctx.generateExpression(expr.args[0], params);
const statusRaw = this.ctx.generateExpression(expr.args[1], params);
const headers = this.ctx.generateExpression(expr.args[2], params);

// Normalize status to double (may arrive as i64 from integer literals)
const statusType = this.ctx.getVariableType(statusRaw);
let status = statusRaw;
if (statusType === "i64" || statusType === "i32") {
status = this.ctx.nextTemp();
this.ctx.emit(`${status} = sitofp ${statusType} ${statusRaw} to double`);
}

// Load raw data pointer from Uint8Array field 0
const dataField = this.ctx.nextTemp();
this.ctx.emit(
`${dataField} = getelementptr inbounds %Uint8Array, %Uint8Array* ${arrayPtr}, i32 0, i32 0`,
);
const dataPtr = this.ctx.nextTemp();
this.ctx.emit(`${dataPtr} = load i8*, i8** ${dataField}`);

// Load length (i32) from Uint8Array field 1, convert to double
const lenField = this.ctx.nextTemp();
this.ctx.emit(
`${lenField} = getelementptr inbounds %Uint8Array, %Uint8Array* ${arrayPtr}, i32 0, i32 1`,
);
const lenI32 = this.ctx.nextTemp();
this.ctx.emit(`${lenI32} = load i32, i32* ${lenField}`);
const lenDbl = this.ctx.nextTemp();
this.ctx.emit(`${lenDbl} = sitofp i32 ${lenI32} to double`);

// Allocate HttpResponse struct: { double, i8*, i8*, double } = 32 bytes
const respType = "{ double, i8*, i8*, double }";
const structRaw = this.ctx.emitCall("i8*", "@GC_malloc", "i64 32");
const structPtr = this.ctx.emitBitcast(structRaw, "i8*", `${respType}*`);

const f0 = this.ctx.nextTemp();
this.ctx.emit(`${f0} = getelementptr ${respType}, ${respType}* ${structPtr}, i32 0, i32 0`);
this.ctx.emitStore("double", status, f0);

const f1 = this.ctx.nextTemp();
this.ctx.emit(`${f1} = getelementptr ${respType}, ${respType}* ${structPtr}, i32 0, i32 1`);
this.ctx.emitStore("i8*", dataPtr, f1);

const f2 = this.ctx.nextTemp();
this.ctx.emit(`${f2} = getelementptr ${respType}, ${respType}* ${structPtr}, i32 0, i32 2`);
this.ctx.emitStore("i8*", headers, f2);

const f3 = this.ctx.nextTemp();
this.ctx.emit(`${f3} = getelementptr ${respType}, ${respType}* ${structPtr}, i32 0, i32 3`);
this.ctx.emitStore("double", lenDbl, f3);

this.ctx.setVariableType(structRaw, "i8*");
return structRaw;
}
}
2 changes: 0 additions & 2 deletions src/codegen/infrastructure/generator-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ export interface IEmbedGenerator {
generateGetEmbeddedFile(expr: MethodCallNode, params: string[]): string;
generateGetEmbeddedFileAsUint8Array(expr: MethodCallNode, params: string[]): string;
generateServeEmbedded(expr: MethodCallNode, params: string[]): string;
generateServeFile(expr: CallNode, params: string[]): string;
generateLookupFunction(): string;
generateLengthLookupFunction(): string;
hasEmbeddedFiles(): boolean;
Expand Down Expand Up @@ -1978,7 +1977,6 @@ export class MockGeneratorContext implements IGeneratorContext {
"%mock_get_embedded_uint8array",
generateServeEmbedded: (_expr: MethodCallNode, _params: string[]): string =>
"%mock_serve_embedded",
generateServeFile: (_expr: CallNode, _params: string[]): string => "%mock_serve_file",
generateLookupFunction: (): string => "",
generateLengthLookupFunction: (): string => "",
hasEmbeddedFiles: (): boolean => false,
Expand Down
128 changes: 1 addition & 127 deletions src/codegen/stdlib/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Reads files as raw Buffers (not UTF-8) to preserve binary content like images. */
import * as fs from "fs";
import * as path from "path";
import { CallNode, MethodCallNode } from "../../ast/types.js";
import { MethodCallNode } from "../../ast/types.js";
import { IGeneratorContext } from "../infrastructure/generator-context.js";

interface ExprBase {
Expand Down Expand Up @@ -474,132 +474,6 @@ export class EmbedGenerator {
return result;
}

/**
* ChadScript.serveFile(path) — read a file from disk and return an HttpResponse.
* Uses fopen/fseek/fread to read binary-safe content, returns { 200, data, "", len }
* or { 404, "Not Found", "", 0 } if the file doesn't exist.
*/
generateServeFile(expr: CallNode, params: string[]): string {
if (expr.args.length < 1) {
return this.ctx.emitError("serveFile() requires 1 argument (path)", expr.loc);
}

const respType = "{ double, i8*, i8*, double }";
const structSize = "32";

// Pre-create global strings before branching
this.createGlobalStringDirect("");
const emptyStrId = this._lastStrId;
const emptyStrLen = this._lastLen;
this.createGlobalStringDirect("Not Found");
const nfBodyStrId = this._lastStrId;
const nfBodyLen = this._lastLen;

const pathPtr = this.ctx.generateExpression(expr.args[0], params);
const modeStr = this.ctx.createStringConstant("rb");
const filePtr = this.ctx.nextTemp();
this.ctx.emit(`${filePtr} = call i8* @fopen(i8* ${pathPtr}, i8* ${modeStr})`);
const isNull = this.ctx.nextTemp();
this.ctx.emit(`${isNull} = icmp eq i8* ${filePtr}, null`);

const foundLabel = this.ctx.nextLabel("servefile_found");
const notFoundLabel = this.ctx.nextLabel("servefile_notfound");
const joinLabel = this.ctx.nextLabel("servefile_join");
this.ctx.emit(`br i1 ${isNull}, label %${notFoundLabel}, label %${foundLabel}`);

// Found: read file, return { 200, data, "", size }
this.ctx.emit(`${foundLabel}:`);
const seekEnd = this.ctx.nextTemp();
this.ctx.emit(`${seekEnd} = call i32 @fseek(i8* ${filePtr}, i64 0, i32 2)`);
const fileSize = this.ctx.nextTemp();
this.ctx.emit(`${fileSize} = call i64 @ftell(i8* ${filePtr})`);
const seekStart = this.ctx.nextTemp();
this.ctx.emit(`${seekStart} = call i32 @fseek(i8* ${filePtr}, i64 0, i32 0)`);
const dataBuf = this.ctx.nextTemp();
this.ctx.emit(`${dataBuf} = call i8* @GC_malloc_atomic(i64 ${fileSize})`);
const bytesRead = this.ctx.nextTemp();
this.ctx.emit(
`${bytesRead} = call i64 @fread(i8* ${dataBuf}, i64 1, i64 ${fileSize}, i8* ${filePtr})`,
);
const closeRes = this.ctx.nextTemp();
this.ctx.emit(`${closeRes} = call i32 @fclose(i8* ${filePtr})`);

const foundStruct = this.ctx.nextTemp();
this.ctx.emit(`${foundStruct} = call i8* @GC_malloc(i64 ${structSize})`);
const foundTyped = this.ctx.nextTemp();
this.ctx.emit(`${foundTyped} = bitcast i8* ${foundStruct} to ${respType}*`);
const fStatusPtr = this.ctx.nextTemp();
this.ctx.emit(
`${fStatusPtr} = getelementptr ${respType}, ${respType}* ${foundTyped}, i32 0, i32 0`,
);
this.ctx.emit(`store double 200.0, double* ${fStatusPtr}`);
const fBodyPtr = this.ctx.nextTemp();
this.ctx.emit(
`${fBodyPtr} = getelementptr ${respType}, ${respType}* ${foundTyped}, i32 0, i32 1`,
);
this.ctx.emit(`store i8* ${dataBuf}, i8** ${fBodyPtr}`);
const emptyStr = this.ctx.nextTemp();
this.ctx.emit(
`${emptyStr} = getelementptr inbounds [${emptyStrLen} x i8], [${emptyStrLen} x i8]* ${emptyStrId}, i64 0, i64 0`,
);
const fHdrsPtr = this.ctx.nextTemp();
this.ctx.emit(
`${fHdrsPtr} = getelementptr ${respType}, ${respType}* ${foundTyped}, i32 0, i32 2`,
);
this.ctx.emit(`store i8* ${emptyStr}, i8** ${fHdrsPtr}`);
const fileSizeDbl = this.ctx.nextTemp();
this.ctx.emit(`${fileSizeDbl} = sitofp i64 ${fileSize} to double`);
const fLenPtr = this.ctx.nextTemp();
this.ctx.emit(
`${fLenPtr} = getelementptr ${respType}, ${respType}* ${foundTyped}, i32 0, i32 3`,
);
this.ctx.emit(`store double ${fileSizeDbl}, double* ${fLenPtr}`);
this.ctx.emit(`br label %${joinLabel}`);

// Not found: return { 404, "Not Found", "", 0 }
this.ctx.emit(`${notFoundLabel}:`);
const nfStruct = this.ctx.nextTemp();
this.ctx.emit(`${nfStruct} = call i8* @GC_malloc(i64 ${structSize})`);
const nfTyped = this.ctx.nextTemp();
this.ctx.emit(`${nfTyped} = bitcast i8* ${nfStruct} to ${respType}*`);
const nfStatusPtr = this.ctx.nextTemp();
this.ctx.emit(
`${nfStatusPtr} = getelementptr ${respType}, ${respType}* ${nfTyped}, i32 0, i32 0`,
);
this.ctx.emit(`store double 404.0, double* ${nfStatusPtr}`);
const nfBodyStr = this.ctx.nextTemp();
this.ctx.emit(
`${nfBodyStr} = getelementptr inbounds [${nfBodyLen} x i8], [${nfBodyLen} x i8]* ${nfBodyStrId}, i64 0, i64 0`,
);
const nfBodyPtr = this.ctx.nextTemp();
this.ctx.emit(
`${nfBodyPtr} = getelementptr ${respType}, ${respType}* ${nfTyped}, i32 0, i32 1`,
);
this.ctx.emit(`store i8* ${nfBodyStr}, i8** ${nfBodyPtr}`);
const emptyStr2 = this.ctx.nextTemp();
this.ctx.emit(
`${emptyStr2} = getelementptr inbounds [${emptyStrLen} x i8], [${emptyStrLen} x i8]* ${emptyStrId}, i64 0, i64 0`,
);
const nfHdrsPtr = this.ctx.nextTemp();
this.ctx.emit(
`${nfHdrsPtr} = getelementptr ${respType}, ${respType}* ${nfTyped}, i32 0, i32 2`,
);
this.ctx.emit(`store i8* ${emptyStr2}, i8** ${nfHdrsPtr}`);
const nfLenPtr = this.ctx.nextTemp();
this.ctx.emit(`${nfLenPtr} = getelementptr ${respType}, ${respType}* ${nfTyped}, i32 0, i32 3`);
this.ctx.emit(`store double 0.0, double* ${nfLenPtr}`);
this.ctx.emit(`br label %${joinLabel}`);

// Join
this.ctx.emit(`${joinLabel}:`);
const result = this.ctx.nextTemp();
this.ctx.emit(
`${result} = phi i8* [ ${foundStruct}, %${foundLabel} ], [ ${nfStruct}, %${notFoundLabel} ]`,
);
this.ctx.setVariableType(result, "i8*");
return result;
}

generateGetEmbeddedFile(expr: MethodCallNode, params: string[]): string {
if (expr.args.length < 1) {
return this.ctx.emitError(
Expand Down
Loading