diff --git a/c_bridges/regex-bridge.c b/c_bridges/regex-bridge.c index 5a6a2314..011d5de6 100644 --- a/c_bridges/regex-bridge.c +++ b/c_bridges/regex-bridge.c @@ -2,6 +2,9 @@ #include #include +extern void *GC_malloc(size_t); +extern void *GC_malloc_atomic(size_t); + void *cs_regex_alloc(void) { return malloc(sizeof(regex_t)); } @@ -32,3 +35,39 @@ void cs_regex_free(void *preg) { free(preg); } } + +char *cs_regex_exec_dyn(void *preg, const char *str, int max_groups) { + regex_t *regex = (regex_t *)preg; + int total = (int)regex->re_nsub + 1; + if (total > max_groups) total = max_groups; + + regmatch_t *pmatch = (regmatch_t *)calloc((size_t)total, sizeof(regmatch_t)); + if (!pmatch) return NULL; + + if (regexec(regex, str, (size_t)total, pmatch, 0) != 0) { + free(pmatch); + return NULL; + } + + char **strings = (char **)GC_malloc((size_t)total * sizeof(char *)); + for (int i = 0; i < total; i++) { + if (pmatch[i].rm_so >= 0) { + int slen = pmatch[i].rm_eo - pmatch[i].rm_so; + char *s = (char *)GC_malloc_atomic((size_t)(slen + 1)); + if (slen > 0) strncpy(s, str + pmatch[i].rm_so, (size_t)slen); + s[slen] = '\0'; + strings[i] = s; + } else { + char *s = (char *)GC_malloc_atomic(1); + s[0] = '\0'; + strings[i] = s; + } + } + free(pmatch); + + char *arr = (char *)GC_malloc(16); + *((char ***)arr) = strings; + *((int *)(arr + 8)) = total; + *((int *)(arr + 12)) = total; + return arr; +} diff --git a/chadscript.d.ts b/chadscript.d.ts index fb1cc8af..01b82ebc 100644 --- a/chadscript.d.ts +++ b/chadscript.d.ts @@ -225,14 +225,31 @@ interface HttpRequest { path: string; body: string; contentType: string; + headers: string; + bodyLen: number; } interface HttpResponse { status: number; body: string; + headers: string; +} + +interface WsEvent { + data: string; + event: string; + connId: string; } declare function httpServe(port: number, handler: (req: HttpRequest) => HttpResponse): void; +declare function httpServe( + port: number, + handler: (req: HttpRequest) => HttpResponse, + wsHandler: (event: WsEvent) => string, +): void; + +declare function wsBroadcast(message: string): void; +declare function wsSend(connId: string, message: string): void; // ============================================================================ // Async / Timers @@ -291,4 +308,13 @@ declare namespace ChadScript { function embedFile(path: string): string; function embedDir(path: string): void; function getEmbeddedFile(key: string): string; + function parseMultipart(req: HttpRequest): MultipartPart[]; +} + +interface MultipartPart { + name: string; + filename: string; + contentType: string; + data: string; + dataLen: number; } diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 48835055..6d25bca4 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -23,26 +23,42 @@ export default defineConfig({ }, nav: [ + { text: 'Why ChadScript?', link: '/why-chadscript' }, { text: 'Get Started', link: '/getting-started/installation' }, - { text: 'API', link: '/stdlib/' }, + { text: 'API Reference', link: '/stdlib/' }, { text: 'Benchmarks', link: '/benchmarks' }, { text: 'GitHub', link: 'https://github.com/cs01/ChadScript' } ], sidebar: [ { - text: 'Getting Started', + text: 'Why ChadScript?', + items: [ + { text: 'Why ChadScript?', link: '/why-chadscript' }, + { text: 'Benchmarks', link: '/benchmarks' }, + { text: 'FAQ', link: '/faq' }, + ] + }, + { + text: 'Get Started', items: [ - { text: 'About ChadScript', link: '/language/architecture' }, { text: 'Installation', link: '/getting-started/installation' }, - { text: 'Examples', link: '/getting-started/quickstart' }, + { text: 'Quickstart', link: '/getting-started/quickstart' }, { text: 'CLI Reference', link: '/getting-started/cli' }, + { text: 'Debugging', link: '/getting-started/debugging' }, + ] + }, + { + text: 'Language', + items: [ { text: 'Supported Features', link: '/language/features' }, - { text: 'Debugging', link: '/getting-started/debugging' } + { text: 'Classes & Interfaces', link: '/language/classes' }, + { text: 'Type Mappings', link: '/language/type-mappings' }, + { text: 'How It Works', link: '/language/architecture' }, ] }, { - text: 'Standard Library', + text: 'API Reference', items: [ { text: 'Overview', link: '/stdlib/' }, { text: 'Array Methods', link: '/stdlib/array' }, @@ -54,7 +70,7 @@ export default defineConfig({ { text: 'Date', link: '/stdlib/date' }, { text: 'fetch', link: '/stdlib/fetch' }, { text: 'fs', link: '/stdlib/fs' }, - { text: 'httpServe', link: '/stdlib/http-server' }, + { text: 'HTTP Server / Router', link: '/stdlib/http-server' }, { text: 'JSON', link: '/stdlib/json' }, { text: 'Map', link: '/stdlib/map' }, { text: 'Math', link: '/stdlib/math' }, @@ -71,12 +87,6 @@ export default defineConfig({ { text: 'tty', link: '/stdlib/tty' } ] }, - { - text: 'Performance', - items: [ - { text: 'Benchmarks', link: '/benchmarks' } - ] - }, ], socialLinks: [ diff --git a/docs/stdlib/http-server.md b/docs/stdlib/http-server.md index 44d10340..405c91c6 100644 --- a/docs/stdlib/http-server.md +++ b/docs/stdlib/http-server.md @@ -1,7 +1,113 @@ -# httpServe +# HTTP Server Built-in HTTP server with websocket support compiled to native code via libuv TCP + picohttpparser. +## Router (recommended) + +For most servers, use the `Router` class from `src/router.ts`. It provides an Express/Hono-style API with URL parameter extraction, method matching, and chainable response helpers. + +```typescript +import { Router, Context } from "./src/router"; + +const app = new Router(); + +app.get("/", (c: Context) => c.json('{"status":"ok"}')); + +app.get("/api/users/:id", (c: Context) => { + const id = c.req.param("id"); + return c.json('{"id":"' + id + '"}'); +}); + +app.post("/api/users", (c: Context) => { + c.status(201); + return c.text("Created"); +}); + +app.notFound((c: Context) => c.status(404).text("Not Found")); + +httpServe(3000, (req) => app.handle(req)); +``` + +### Router methods + +| Method | Description | +|--------|-------------| +| `app.get(pattern, handler)` | Register GET route | +| `app.post(pattern, handler)` | Register POST route | +| `app.put(pattern, handler)` | Register PUT route | +| `app.delete(pattern, handler)` | Register DELETE route | +| `app.all(pattern, handler)` | Match any HTTP method | +| `app.notFound(handler)` | Custom 404 handler | +| `app.handle(req)` | Dispatch an `HttpRequest` → `HttpResponse` | +| `app.compile()` | Pre-compile the combined regex (called automatically on first `handle`) | + +Route patterns support `:param` segments and `*` wildcards: + +```typescript +app.get("/users/:id", ...); // /users/42 → param("id") == "42" +app.get("/users/:name/posts/:pid", ...); // multiple params +app.all("/static/*", ...); // wildcard +``` + +### Context API + +The handler receives a `Context` (aliased as `c` by convention): + +```typescript +app.get("/example", (c: Context) => { + // Read request + const id = c.req.param("id"); // URL param + const auth = c.req.header("Authorization"); // request header + const body = c.req.body; // raw body + const method = c.req.method; // "GET", "POST", … + + // Build response (chainable) + c.status(201); + c.header("X-Custom", "value"); + return c.json('{"ok":true}'); +}); +``` + +**Response methods** — call one to return the `HttpResponse`: + +| Method | Content-Type | Notes | +|--------|--------------|-------| +| `c.text(body)` | `text/plain` | | +| `c.json(body)` | `application/json` | pass a JSON string | +| `c.html(body)` | `text/html` | | +| `c.redirect(url)` | — | 302, sets `Location` header | + +**Chainable setters** (return `Context`, call before a response method): + +| Method | Effect | +|--------|--------| +| `c.status(code)` | Set HTTP status code | +| `c.header(name, value)` | Add a response header | + +### HTTP utility functions + +`src/http-utils.ts` provides helpers for parsing common request data: + +```typescript +import { getHeader, parseQueryString, parseCookies } from "./src/http-utils"; + +// Parse a single request header by name (case-insensitive) +const auth = getHeader(req.headers, "Authorization"); // "Bearer abc" + +// Parse a query string into a Map +const qs = parseQueryString("page=2&limit=10"); +qs.get("page"); // "2" +qs.get("limit"); // "10" + +// Parse the Cookie header into a Map +const cookies = parseCookies(getHeader(req.headers, "Cookie")); +cookies.get("session"); // "abc123" +``` + +--- + +## `httpServe(port, handler)` + ## `httpServe(port, handler)` Start an HTTP server on the given port. The handler function receives an `HttpRequest` and returns an `HttpResponse`. diff --git a/docs/stdlib/regexp.md b/docs/stdlib/regexp.md index 4587c28c..e41a8065 100644 --- a/docs/stdlib/regexp.md +++ b/docs/stdlib/regexp.md @@ -44,7 +44,7 @@ const result = "hello world".match(/(\w+)/); // ["hello", "hello"] ``` -Returns `null` if no match. +Returns `null` if no match. `exec` works for both literal patterns (`/foo/`) and runtime-constructed patterns (`new RegExp(str)`) — the group count is read from the compiled regex at runtime. ## Example diff --git a/examples/http-server.ts b/examples/http-server.ts index cfdf4ffe..6da68995 100644 --- a/examples/http-server.ts +++ b/examples/http-server.ts @@ -1,106 +1,86 @@ -// ChadScript HTTP Server - Express-like routing with HttpRequest/HttpResponse import { ArgumentParser } from "../src/argparse.js"; +import { Router, Context } from "../src/router.js"; +import { getHeader, parseQueryString } from "../src/http-utils.js"; -const parser = new ArgumentParser("http-server", "HTTP server with Express-like routing"); +const parser = new ArgumentParser("http-server", "HTTP server with Router API"); parser.addOption("port", "p", "Port to listen on", "3000"); parser.parse(process.argv); const port = parseInt(parser.getOption("port")); -interface HttpRequest { - method: string; - path: string; - body: string; - contentType: string; -} - -interface HttpResponse { - status: number; - body: string; -} - -// --- Route Handlers --- - -function homeHandler(req: HttpRequest): HttpResponse { - return { status: 200, body: '{"name":"ChadScript HTTP Server","status":"running"}' }; -} - -function jsonHandler(req: HttpRequest): HttpResponse { - return { status: 200, body: '{"message":"hello","count":42}' }; -} - -function echoHandler(req: HttpRequest): HttpResponse { - return { status: 200, body: req.body }; -} - -function echoQueryHandler(req: HttpRequest): HttpResponse { - return { status: 200, body: req.path.substring(10, req.path.length) }; -} - -function statusHandler(req: HttpRequest): HttpResponse { - const code = req.path.substring(8, req.path.length); - return { status: 200, body: "Status " + code }; -} - -function contentTypeHandler(req: HttpRequest): HttpResponse { - return { status: 200, body: "Content-Type: " + req.contentType }; -} - -function errorHandler(req: HttpRequest): HttpResponse { - return { status: 500, body: "Internal Server Error" }; -} - -function createdHandler(req: HttpRequest): HttpResponse { - return { status: 201, body: "Resource Created" }; -} - -function notFoundHandler(req: HttpRequest): HttpResponse { - return { status: 404, body: "Not Found" }; -} - -// --- Router --- - -function handleRequest(req: HttpRequest): HttpResponse { - console.log(req.method + " " + req.path); - - // GET routes - if (req.method == "GET") { - if (req.path == "/") return homeHandler(req); - if (req.path == "/json") return jsonHandler(req); - if (req.path.startsWith("/echo?msg=")) return echoQueryHandler(req); - if (req.path.startsWith("/status/")) return statusHandler(req); - if (req.path == "/content-type") return contentTypeHandler(req); - if (req.path == "/error") return errorHandler(req); - if (req.path == "/created") return createdHandler(req); - } - - // POST routes - if (req.method == "POST") { - if (req.path == "/echo") return echoHandler(req); - } - - return notFoundHandler(req); -} - -// --- Start Server --- - -console.log("ChadScript HTTP Server"); +const app = new Router(); + +app.get("/", (c: Context) => { + return c.json('{"name":"ChadScript HTTP Server","status":"running"}'); +}); + +app.get("/json", (c: Context) => { + return c.json('{"message":"hello","count":42}'); +}); + +app.get("/api/users/:id", (c: Context) => { + const id = c.req.param("id"); + return c.json('{"id":"' + id + '"}'); +}); + +app.get("/api/users/:name/posts/:pid", (c: Context) => { + const name = c.req.param("name"); + const pid = c.req.param("pid"); + return c.json('{"user":"' + name + '","post":"' + pid + '"}'); +}); + +app.get("/status/:code", (c: Context) => { + const code = parseInt(c.req.param("code")); + c.status(code); + return c.text("Status " + c.req.param("code")); +}); + +app.get("/headers", (c: Context) => { + const auth = getHeader(c.req.headers, "Authorization"); + return c.text("Authorization: " + auth); +}); + +app.get("/redirect", (c: Context) => { + return c.redirect("/"); +}); + +app.post("/echo", (c: Context) => { + return c.text(c.req.body); +}); + +app.post("/api/users", (c: Context) => { + c.status(201); + return c.json('{"created":true}'); +}); + +app.notFound((c: Context) => { + c.status(404); + return c.json('{"error":"not found","path":"' + c.req.path + '"}'); +}); + +console.log("ChadScript HTTP Server (Router API)"); console.log(" listening on http://localhost:" + port); console.log(""); -console.log("Available routes:"); -console.log(" GET / - Server info (JSON)"); -console.log(" GET /json - JSON response"); -console.log(" GET /echo?msg=... - Echo query parameter"); -console.log(" GET /status/:code - Status code demo"); -console.log(" GET /content-type - Show request content type"); -console.log(" GET /error - 500 error response"); -console.log(" GET /created - 201 created response"); -console.log(" POST /echo - Echo request body"); +console.log("Routes:"); +console.log(" GET / - server info"); +console.log(" GET /json - JSON response"); +console.log(" GET /api/users/:id - user by ID"); +console.log(" GET /api/users/:name/posts/:pid - multi-param"); +console.log(" GET /status/:code - custom status code"); +console.log(" GET /headers - echo Authorization header"); +console.log(" GET /redirect - 302 → /"); +console.log(" POST /echo - echo body"); +console.log(" POST /api/users - 201 created"); console.log(""); -console.log("Try it out:"); -console.log(" curl http://localhost:" + port + "/"); -console.log(" curl http://localhost:" + port + "/json"); -console.log(" curl http://localhost:" + port + "/echo?msg=hello"); -console.log(" curl -X POST -d 'hello world' http://localhost:" + port + "/echo"); +console.log("Try it:"); +console.log(" curl http://localhost:" + port + "/api/users/42"); +console.log(" curl http://localhost:" + port + "/api/users/alice/posts/7"); +console.log(" curl -X POST -d 'hello' http://localhost:" + port + "/echo"); +console.log(" curl -H 'Authorization: Bearer token' http://localhost:" + port + "/headers"); console.log(""); + +function handleRequest(req: HttpRequest): HttpResponse { + return app.handle(req); +} + httpServe(port, handleRequest); diff --git a/src/codegen/expressions/calls.ts b/src/codegen/expressions/calls.ts index 0f854526..1f1ee2bf 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -50,6 +50,20 @@ export class CallExpressionGenerator { return this.generateSuperCall(expr, params); } + if (expr.name === "callHandler") { + const fnPtr = this.ctx.generateExpression(expr.args[0], params); + const typedFn = this.ctx.nextTemp(); + this.ctx.emit(`${typedFn} = bitcast i8* ${fnPtr} to double (i8*)*`); + const callArgsList: string[] = []; + for (let ai = 1; ai < expr.args.length; ai++) { + const argVal = this.ctx.generateExpression(expr.args[ai], params); + callArgsList.push(`i8* ${argVal}`); + } + const callResult = this.ctx.nextTemp(); + this.ctx.emit(`${callResult} = call double ${typedFn}(${callArgsList.join(", ")})`); + return fnPtr; + } + if (expr.name === "__gc_disable") { this.ctx.emitCallVoid("@GC_disable", ""); return "0.0"; diff --git a/src/codegen/expressions/method-calls.ts b/src/codegen/expressions/method-calls.ts index 5d056124..0276ecd9 100644 --- a/src/codegen/expressions/method-calls.ts +++ b/src/codegen/expressions/method-calls.ts @@ -1398,26 +1398,9 @@ export class MethodCallGenerator { if (expr.args.length !== 1) { return this.ctx.emitError(`exec() expects 1 argument, got ${expr.args.length}`, expr.loc); } - + const regexPtr = this.ctx.generateExpression(expr.object, params); const strPtr = this.ctx.generateExpression(expr.args[0], params); - - const regexObj = expr.object; - const regexBase = regexObj as { type: string; pattern?: string; flags?: string }; - - let numGroups = 0; - if (regexBase.type === "regex" && regexBase.pattern) { - const pattern = regexBase.pattern; - for (let gi = 0; gi < pattern.length; gi++) { - if (pattern[gi] === "(") { - numGroups = numGroups + 1; - } - } - } else { - numGroups = 9; - } - - const regexPtr = this.ctx.generateExpression(regexObj, params); - return this.ctx.regexGen.generateRegexMatch(regexPtr, strPtr, numGroups); + return this.ctx.regexGen.generateRegexExecDyn(regexPtr, strPtr); } private throwUnsupportedMethodError( diff --git a/src/codegen/expressions/method-calls/class-dispatch.ts b/src/codegen/expressions/method-calls/class-dispatch.ts index 7d8f4a2c..255aba28 100644 --- a/src/codegen/expressions/method-calls/class-dispatch.ts +++ b/src/codegen/expressions/method-calls/class-dispatch.ts @@ -672,6 +672,41 @@ export function handleClassMethods( className = resolvedType; } } + } else if (exprObjBase.type === "index_access") { + const indexAccess = expr.object as { type: string; object: Expression; index: Expression }; + const baseExpr = indexAccess.object; + const baseExprBase = baseExpr as ExprBase; + if (baseExprBase.type === "member_access") { + const memberAccess = baseExpr as MemberAccessNode; + const memberObjBase = memberAccess.object as ExprBase; + if (memberObjBase.type === "this") { + const curClassName = ctx.getCurrentClassName(); + if (curClassName) { + const fieldInfoResult = ctx.classGenGetFieldInfo(curClassName, memberAccess.property); + const fieldInfo = fieldInfoResult as { index: number; type: string; tsType: string }; + if (fieldInfoResult && fieldInfo.tsType) { + let tsType = fieldInfo.tsType; + if (tsType.indexOf(" | ") !== -1) { + tsType = tsType + .replace(/ \| undefined/g, "") + .replace(/ \| null/g, "") + .trim(); + } + if (tsType.endsWith("[]")) { + tsType = tsType.substring(0, tsType.length - 2); + } + const classesLenIA = ctx.getAstClassesLength(); + for (let ci = 0; ci < classesLenIA; ci++) { + if (ctx.getAstClassNameAt(ci) === tsType) { + instancePtr = ctx.generateExpression(expr.object, params); + className = tsType; + break; + } + } + } + } + } + } } else if (exprObjBase.type === "super") { const thisPtr = ctx.getThisPointer(); if (!thisPtr) { diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index cec83ff0..3b63a266 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -122,6 +122,7 @@ export interface IRegexGenerator { generateRegexTest(regexPtr: string, testStr: string): string; generateRegexMatch(regexPtr: string, testStr: string, numGroups: number): string; generateRegexCompileRuntime(patternPtr: string, cflags: number): string; + generateRegexExecDyn(regexPtr: string, testStr: string): string; } export interface IControlFlowGenerator { @@ -1823,6 +1824,7 @@ export class MockGeneratorContext implements IGeneratorContext { "%mock_regex_match", generateRegexCompileRuntime: (_patternPtr: string, _cflags: number): string => "%mock_regex_compile_runtime", + generateRegexExecDyn: (_regexPtr: string, _testStr: string): string => "%mock_regex_exec_dyn", }; controlFlowGen: IControlFlowGenerator = { generateLogicalOp: ( diff --git a/src/codegen/infrastructure/llvm-declarations.ts b/src/codegen/infrastructure/llvm-declarations.ts index 1634295a..d2f8ac48 100644 --- a/src/codegen/infrastructure/llvm-declarations.ts +++ b/src/codegen/infrastructure/llvm-declarations.ts @@ -76,6 +76,7 @@ export function getLLVMDeclarations(config?: DeclConfig): string { ir += "declare i64 @cs_pmatch_start(i8*, i32)\n"; ir += "declare i64 @cs_pmatch_end(i8*, i32)\n"; ir += "declare void @cs_regex_free(i8*)\n"; + ir += "declare i8* @cs_regex_exec_dyn(i8*, i8*, i32)\n"; ir += "\n"; // child_process bridge — %SpawnSyncResult = { stdout: i8*, stderr: i8*, status: double } diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 548a98d9..5265d742 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -359,7 +359,7 @@ export class TypeInference { return this.ctx.typeContext.booleanType; } - if (method === "match" || method === "exec") { + if (method === "match" || method === "exec" || method === "execDyn") { return this.ctx.typeContext.getArrayType("string"); } @@ -687,10 +687,10 @@ export class TypeInference { if (objBase.type === "this") { const className = this.ctx.getCurrentClassName(); if (className) { - const fieldType = this.ctx.classGenGetFieldType(className, prop); - if (fieldType) return this.ctx.typeContext.resolve(fieldType); const tsType = this.ctx.classGenGetFieldTsType(className, prop); if (tsType) return this.ctx.typeContext.resolve(stripNullable(tsType)); + const fieldType = this.ctx.classGenGetFieldType(className, prop); + if (fieldType) return this.ctx.typeContext.resolve(fieldType); } } @@ -2142,7 +2142,10 @@ export class TypeInference { return true; } } - if (methodExpr.method === "exec" && this.isRegexExpression(methodExpr.object)) { + if ( + (methodExpr.method === "exec" || methodExpr.method === "execDyn") && + this.isRegexExpression(methodExpr.object) + ) { return true; } if ( diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index 8a5e9b4a..be46d08b 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -2299,7 +2299,11 @@ export class VariableAllocator { const objectType = this.resolveMemberAccessObjectType(ma.object); if (!objectType) return null; - const fieldType = this.getInterfaceFieldTypeByName(objectType, ma.property); + const classFieldInfo = this.ctx.classGenGetFieldInfo(objectType, ma.property); + const classFieldTsType = classFieldInfo + ? (classFieldInfo as { index: number; type: string; tsType: string }).tsType + : null; + const fieldType = this.getInterfaceFieldTypeByName(objectType, ma.property) || classFieldTsType; if (!fieldType) return null; const arrayParsed = parseArrayTypeString(fieldType); diff --git a/src/codegen/types/objects/class.ts b/src/codegen/types/objects/class.ts index 99010e7a..a819bd16 100644 --- a/src/codegen/types/objects/class.ts +++ b/src/codegen/types/objects/class.ts @@ -1089,7 +1089,30 @@ export class ClassGenerator { if (ai < args.length) { const arg = args[ai]; const argTyped = arg as { type: string }; + if (argTyped.type === "arrow_function" && ai < paramTypes.length) { + const paramTypeStr = paramTypes[ai]; + if (paramTypeStr.startsWith("(")) { + const colonIdx = paramTypeStr.indexOf(": "); + if (colonIdx !== -1) { + const afterColon = paramTypeStr.substring(colonIdx + 2); + const commaIdx = afterColon.indexOf(","); + const parenIdx = afterColon.indexOf(")"); + const endIdx = + commaIdx === -1 + ? parenIdx + : parenIdx === -1 + ? commaIdx + : commaIdx < parenIdx + ? commaIdx + : parenIdx; + const firstParamType = + endIdx !== -1 ? afterColon.substring(0, endIdx).trim() : afterColon.trim(); + this.ctx.setExpectedCallbackParamType(firstParamType); + } + } + } const val = this.ctx.generateExpression(arg, params); + this.ctx.setExpectedCallbackParamType(null); let argType = "double"; if (ai < paramLLVMTypes.length) { @@ -1110,7 +1133,9 @@ export class ClassGenerator { if (argType === "double") { argParts.push(argType + " " + this.ctx.ensureDouble(val)); } else { - argParts.push(argType + " " + val); + const valRef = + val.startsWith("%") || val.startsWith("@") || val === "null" ? val : "@" + val; + argParts.push(argType + " " + valRef); } } else { let argType = "double"; diff --git a/src/codegen/types/objects/regex.ts b/src/codegen/types/objects/regex.ts index 5df0abf0..2e33246a 100644 --- a/src/codegen/types/objects/regex.ts +++ b/src/codegen/types/objects/regex.ts @@ -161,6 +161,17 @@ export class RegexGenerator { return result; } + generateRegexExecDyn(regexPtr: string, testStr: string): string { + this.ctx.setUsesRegex(true); + const result = this.ctx.emitCall( + "i8*", + "@cs_regex_exec_dyn", + `i8* ${regexPtr}, i8* ${testStr}, i32 64`, + ); + this.ctx.setVariableType(result, "i8*"); + return result; + } + // Clean up regex resources generateRegexFree(regexPtr: string): void { this.ctx.emitCallVoid("@cs_regex_free", `i8* ${regexPtr}`); diff --git a/src/http-utils.ts b/src/http-utils.ts new file mode 100644 index 00000000..7bdbb4f1 --- /dev/null +++ b/src/http-utils.ts @@ -0,0 +1,47 @@ +export function getHeader(headersRaw: string, name: string): string { + const lower = name.toLowerCase(); + const lines = headersRaw.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.length === 0) continue; + const colon = line.indexOf(":"); + if (colon < 1) continue; + const key = line.substring(0, colon).toLowerCase().trim(); + if (key === lower) { + return line.substring(colon + 1).trim(); + } + } + return ""; +} + +export function parseQueryString(qs: string): Map { + const result = new Map(); + if (qs.length === 0) return result; + const pairs = qs.split("&"); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i]; + const eq = pair.indexOf("="); + if (eq < 0) { + result.set(pair, ""); + } else { + result.set(pair.substring(0, eq), pair.substring(eq + 1)); + } + } + return result; +} + +export function parseCookies(cookieHeader: string): Map { + const result = new Map(); + if (cookieHeader.length === 0) return result; + const parts = cookieHeader.split(";"); + for (let i = 0; i < parts.length; i++) { + const part = parts[i].trim(); + const eq = part.indexOf("="); + if (eq < 0) { + result.set(part, ""); + } else { + result.set(part.substring(0, eq).trim(), part.substring(eq + 1).trim()); + } + } + return result; +} diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index cb5a8110..2a03f53b 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -2522,7 +2522,26 @@ function extractTypeString(typeNode: TreeSitterNode): string { } if (tn.type === "function_type") { - return "Function"; + const paramsNode = getChildByFieldName(typeNode, "parameters"); + const returnTypeNode = getChildByFieldName(typeNode, "return_type"); + const parts: string[] = []; + if (paramsNode) { + const pn = paramsNode as NodeBase; + for (let i = 0; i < pn.namedChildCount; i++) { + const param = getNamedChild(paramsNode, i); + if (!param) continue; + const p = param as NodeBase; + if (p.type === "required_parameter" || p.type === "optional_parameter") { + const patternNode = getChildByFieldName(param, "pattern"); + const typeAnnotationNode = getChildByFieldName(param, "type"); + const paramName = patternNode ? (patternNode as NodeBase).text : ""; + const paramType = typeAnnotationNode ? extractTypeString(typeAnnotationNode) : "any"; + parts.push(paramName + ": " + paramType); + } + } + } + const returnType = returnTypeNode ? extractTypeString(returnTypeNode) : "void"; + return "(" + parts.join(", ") + ") => " + returnType; } return tn.text; diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 00000000..9696ae4b --- /dev/null +++ b/src/router.ts @@ -0,0 +1,323 @@ +import { getHeader } from "./http-utils"; + +interface HttpRequest { + method: string; + path: string; + body: string; + contentType: string; + headers: string; + bodyLen: number; +} + +interface HttpResponse { + status: number; + body: string; + headers: string; +} + +class RouterRequest { + method: string; + path: string; + body: string; + contentType: string; + headers: string; + private _params: Map; + + constructor(req: HttpRequest, params: Map) { + this.method = req.method; + this.path = req.path; + this.body = req.body; + this.contentType = req.contentType; + this.headers = req.headers; + this._params = params; + } + + param(name: string): string { + const val = this._params.get(name); + if (val === undefined) return ""; + return val; + } + + header(name: string): string { + return getHeader(this.headers, name); + } +} + +export class Context { + req: RouterRequest; + private _status: number; + private _extraHeaders: string; + private _resultBody: string; + private _resultHeaders: string; + private _resultStatus: number; + + constructor(req: RouterRequest) { + this.req = req; + this._status = 200; + this._extraHeaders = ""; + this._resultBody = ""; + this._resultHeaders = ""; + this._resultStatus = 200; + } + + status(code: number): Context { + this._status = code; + return this; + } + + header(name: string, value: string): Context { + if (this._extraHeaders.length > 0) { + this._extraHeaders = this._extraHeaders + "\n" + name + ": " + value; + } else { + this._extraHeaders = name + ": " + value; + } + return this; + } + + text(body: string): HttpResponse { + let hdrs = "Content-Type: text/plain"; + if (this._extraHeaders.length > 0) { + hdrs = hdrs + "\n" + this._extraHeaders; + } + this._resultBody = body; + this._resultHeaders = hdrs; + this._resultStatus = this._status; + return { status: this._status, body: body, headers: hdrs }; + } + + json(data: string): HttpResponse { + let hdrs = "Content-Type: application/json"; + if (this._extraHeaders.length > 0) { + hdrs = hdrs + "\n" + this._extraHeaders; + } + this._resultBody = data; + this._resultHeaders = hdrs; + this._resultStatus = this._status; + return { status: this._status, body: data, headers: hdrs }; + } + + html(body: string): HttpResponse { + let hdrs = "Content-Type: text/html"; + if (this._extraHeaders.length > 0) { + hdrs = hdrs + "\n" + this._extraHeaders; + } + this._resultBody = body; + this._resultHeaders = hdrs; + this._resultStatus = this._status; + return { status: this._status, body: body, headers: hdrs }; + } + + redirect(url: string): HttpResponse { + let hdrs = "Location: " + url; + if (this._extraHeaders.length > 0) { + hdrs = hdrs + "\n" + this._extraHeaders; + } + this._resultBody = ""; + this._resultHeaders = hdrs; + this._resultStatus = 302; + return { status: 302, body: "", headers: hdrs }; + } + + getResult(): HttpResponse { + return { status: this._resultStatus, body: this._resultBody, headers: this._resultHeaders }; + } +} + +class RouteHandler { + private _fn: (c: Context) => HttpResponse; + + constructor(fn: (c: Context) => HttpResponse) { + this._fn = fn; + } + + dispatch(c: Context): HttpResponse { + callHandler(this._fn, c); + return c.getResult(); + } +} + +interface RouteEntry { + method: string; + pattern: string; + paramNames: string; + groupOffset: number; + groupCount: number; + handlerIndex: number; +} + +export class Router { + private routes: RouteEntry[]; + private handlers: RouteHandler[]; + private notFoundHandler: RouteHandler | null; + private compiled: boolean; + private compiledRegex: RegExp; + + constructor() { + this.routes = []; + this.handlers = []; + this.notFoundHandler = null; + this.compiled = false; + this.compiledRegex = new RegExp("."); + } + + private addRoute(method: string, pattern: string, handler: (c: Context) => HttpResponse): void { + this.compiled = false; + const paramNames = this.extractParamNames(pattern); + const entry: RouteEntry = { + method: method, + pattern: pattern, + paramNames: paramNames, + groupOffset: 0, + groupCount: 0, + handlerIndex: this.handlers.length, + }; + this.routes.push(entry); + this.handlers.push(new RouteHandler(handler)); + } + + get(pattern: string, handler: (c: Context) => HttpResponse): void { + this.addRoute("GET", pattern, handler); + } + + post(pattern: string, handler: (c: Context) => HttpResponse): void { + this.addRoute("POST", pattern, handler); + } + + put(pattern: string, handler: (c: Context) => HttpResponse): void { + this.addRoute("PUT", pattern, handler); + } + + delete(pattern: string, handler: (c: Context) => HttpResponse): void { + this.addRoute("DELETE", pattern, handler); + } + + all(pattern: string, handler: (c: Context) => HttpResponse): void { + this.addRoute("*", pattern, handler); + } + + notFound(handler: (c: Context) => HttpResponse): void { + this.notFoundHandler = new RouteHandler(handler); + } + + private extractParamNames(pattern: string): string { + let result = ""; + let i = 0; + while (i < pattern.length) { + if (pattern[i] === ":") { + let j = i + 1; + while (j < pattern.length && pattern[j] !== "/" && pattern[j] !== "?") { + j = j + 1; + } + const name = pattern.substring(i + 1, j); + if (result.length > 0) { + result = result + ","; + } + result = result + name; + i = j; + } else { + i = i + 1; + } + } + return result; + } + + private patternToRegex(pattern: string): string { + let result = ""; + let i = 0; + while (i < pattern.length) { + const ch = pattern[i]; + if (ch === ":") { + let j = i + 1; + while (j < pattern.length && pattern[j] !== "/" && pattern[j] !== "?") { + j = j + 1; + } + result = result + "([^/]+)"; + i = j; + } else if (ch === ".") { + result = result + "\\."; + i = i + 1; + } else if (ch === "*") { + result = result + "(.*)"; + i = i + 1; + } else { + result = result + ch; + i = i + 1; + } + } + return result; + } + + private countGroups(regexPart: string): number { + let count = 0; + for (let i = 0; i < regexPart.length; i++) { + if (regexPart[i] === "(") { + count = count + 1; + } + } + return count; + } + + compile(): void { + if (this.compiled) return; + let combined = ""; + let offset = 1; + for (let i = 0; i < this.routes.length; i++) { + const route = this.routes[i]; + const regexPart = this.patternToRegex(route.pattern); + const wrapped = "(" + regexPart + ")"; + const innerGroups = this.countGroups(regexPart); + + route.groupOffset = offset; + route.groupCount = innerGroups; + + if (combined.length > 0) { + combined = combined + "|"; + } + combined = combined + wrapped; + offset = offset + 1 + innerGroups; + } + this.compiledRegex = new RegExp(combined); + this.compiled = true; + } + + handle(rawReq: HttpRequest): HttpResponse { + this.compile(); + + const path = rawReq.path; + const method = rawReq.method; + const match = this.compiledRegex.exec(path); + + if (match !== null) { + for (let i = 0; i < this.routes.length; i++) { + const route = this.routes[i]; + const outerGroup = match[route.groupOffset]; + if (outerGroup !== "") { + if (route.method === "*" || route.method === method) { + const params = new Map(); + if (route.paramNames !== "") { + const names = route.paramNames.split(","); + for (let j = 0; j < names.length; j++) { + const groupIdx = route.groupOffset + 1 + j; + if (groupIdx < match.length) { + params.set(names[j], match[groupIdx]); + } + } + } + const rreq = new RouterRequest(rawReq, params); + const ctx = new Context(rreq); + return this.handlers[route.handlerIndex].dispatch(ctx); + } + } + } + } + + const emptyParams = new Map(); + const rreq = new RouterRequest(rawReq, emptyParams); + const ctx = new Context(rreq); + + if (this.notFoundHandler !== null) { + return this.notFoundHandler.dispatch(ctx); + } + return { status: 404, body: "Not Found", headers: "" }; + } +} diff --git a/tests/fixtures/network/router-params.ts b/tests/fixtures/network/router-params.ts new file mode 100644 index 00000000..ec2fa298 --- /dev/null +++ b/tests/fixtures/network/router-params.ts @@ -0,0 +1,116 @@ +// @test-description: Router param extraction and basic routing + +import { Router, Context } from "../../../src/router"; + +function testRouter(): void { + const app = new Router(); + + app.get("/hello", (c: Context) => { + return c.text("hello world"); + }); + + app.get("/api/rooms/:id", (c: Context) => { + const id = c.req.param("id"); + return c.json('{"id":"' + id + '"}'); + }); + + app.post("/api/rooms", (c: Context) => { + c.status(201); + return c.text("Created"); + }); + + app.get("/api/users/:name/posts/:pid", (c: Context) => { + const name = c.req.param("name"); + const pid = c.req.param("pid"); + return c.text(name + "/" + pid); + }); + + const r1 = app.handle({ + method: "GET", + path: "/hello", + body: "", + contentType: "", + headers: "", + bodyLen: 0, + }); + if (r1.status !== 200) { + console.log("FAIL: /hello status"); + process.exit(1); + } + if (r1.body !== "hello world") { + console.log("FAIL: /hello body: " + r1.body); + process.exit(1); + } + + const r2 = app.handle({ + method: "GET", + path: "/api/rooms/42", + body: "", + contentType: "", + headers: "", + bodyLen: 0, + }); + if (r2.status !== 200) { + console.log("FAIL: /api/rooms/:id status"); + process.exit(1); + } + if (r2.body !== '{"id":"42"}') { + console.log("FAIL: /api/rooms/:id body: " + r2.body); + process.exit(1); + } + + const r3 = app.handle({ + method: "POST", + path: "/api/rooms", + body: "", + contentType: "", + headers: "", + bodyLen: 0, + }); + if (r3.status !== 201) { + console.log("FAIL: POST /api/rooms status: " + r3.status); + process.exit(1); + } + + const r4 = app.handle({ + method: "GET", + path: "/api/users/alice/posts/99", + body: "", + contentType: "", + headers: "", + bodyLen: 0, + }); + if (r4.body !== "alice/99") { + console.log("FAIL: multi-param body: " + r4.body); + process.exit(1); + } + + const r5 = app.handle({ + method: "GET", + path: "/not-found", + body: "", + contentType: "", + headers: "", + bodyLen: 0, + }); + if (r5.status !== 404) { + console.log("FAIL: 404 status: " + r5.status); + process.exit(1); + } + + const r6 = app.handle({ + method: "DELETE", + path: "/api/rooms/5", + body: "", + contentType: "", + headers: "", + bodyLen: 0, + }); + if (r6.status !== 404) { + console.log("FAIL: wrong method should 404: " + r6.status); + process.exit(1); + } + + console.log("TEST_PASSED"); +} +testRouter(); diff --git a/tests/fixtures/regex/regex-exec-dynamic.ts b/tests/fixtures/regex/regex-exec-dynamic.ts new file mode 100644 index 00000000..ddbff8ff --- /dev/null +++ b/tests/fixtures/regex/regex-exec-dynamic.ts @@ -0,0 +1,31 @@ +// @test-description: regex exec on runtime-constructed pattern returns correct groups + +function testExecDynamic(): void { + const re = new RegExp("([a-z]+)/([0-9]+)"); + const m1 = re.exec("rooms/42"); + if (m1 === null) { + console.log("FAIL: expected match"); + process.exit(1); + } + if (m1[0] !== "rooms/42") { + console.log("FAIL: full match wrong"); + process.exit(1); + } + if (m1[1] !== "rooms") { + console.log("FAIL: group 1 wrong"); + process.exit(1); + } + if (m1[2] !== "42") { + console.log("FAIL: group 2 wrong"); + process.exit(1); + } + + const m2 = re.exec("nope"); + if (m2 !== null) { + console.log("FAIL: expected no match"); + process.exit(1); + } + + console.log("TEST_PASSED"); +} +testExecDynamic(); diff --git a/tsconfig.json b/tsconfig.json index d8662da6..1228bffc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,12 @@ "moduleResolution": "node" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests", "src/chad-native.ts"] + "exclude": [ + "node_modules", + "dist", + "tests", + "src/chad-native.ts", + "src/router.ts", + "src/http-utils.ts" + ] }