diff --git a/c_bridges/lws-bridge.c b/c_bridges/lws-bridge.c index 5466d17f..c7310943 100644 --- a/c_bridges/lws-bridge.c +++ b/c_bridges/lws-bridge.c @@ -153,8 +153,23 @@ do_response: { g_http_handler(&req, &resp); const char *resp_ct = "text/plain"; - if (resp.body && resp.body[0] == '<') resp_ct = "text/html"; - else if (resp.body && (resp.body[0] == '{' || resp.body[0] == '[')) resp_ct = "application/json"; + const char *dot = strrchr(req.path, '.'); + if (dot) { + if (!strcmp(dot, ".css")) resp_ct = "text/css"; + else if (!strcmp(dot, ".js")) resp_ct = "text/javascript"; + else if (!strcmp(dot, ".json")) resp_ct = "application/json"; + else if (!strcmp(dot, ".html") || !strcmp(dot, ".htm")) resp_ct = "text/html"; + else if (!strcmp(dot, ".svg")) resp_ct = "image/svg+xml"; + else if (!strcmp(dot, ".png")) resp_ct = "image/png"; + else if (!strcmp(dot, ".jpg") || !strcmp(dot, ".jpeg")) resp_ct = "image/jpeg"; + else if (!strcmp(dot, ".woff2")) resp_ct = "font/woff2"; + else if (!strcmp(dot, ".wasm")) resp_ct = "application/wasm"; + else if (resp.body && resp.body[0] == '<') resp_ct = "text/html"; + else if (resp.body && (resp.body[0] == '{' || resp.body[0] == '[')) resp_ct = "application/json"; + } else { + if (resp.body && resp.body[0] == '<') resp_ct = "text/html"; + else if (resp.body && (resp.body[0] == '{' || resp.body[0] == '[')) resp_ct = "application/json"; + } size_t body_len = resp.body_len > 0 ? (size_t)resp.body_len : (resp.body ? strlen(resp.body) : 0); diff --git a/chadscript.d.ts b/chadscript.d.ts index 5a40a77b..e98e980a 100644 --- a/chadscript.d.ts +++ b/chadscript.d.ts @@ -68,7 +68,7 @@ declare namespace fs { function existsSync(filename: string): boolean; function unlinkSync(filename: string): number; function readdirSync(path: string): string[]; - function statSync(path: string): { size: number; isFile: boolean; isDirectory: boolean }; + function statSync(path: string): { size: number; isFile(): boolean; isDirectory(): boolean }; } // ============================================================================ @@ -260,3 +260,13 @@ declare namespace assert { declare function test(name: string, fn: () => void): void; declare function describe(name: string, fn: () => void): void; + +// ============================================================================ +// Compile-Time File Embedding +// ============================================================================ + +declare namespace ChadScript { + function embedFile(path: string): string; + function embedDir(path: string): void; + function getEmbeddedFile(key: string): string; +} diff --git a/docs/stdlib/sqlite.md b/docs/stdlib/sqlite.md index c9c03bf5..3aecffc4 100644 --- a/docs/stdlib/sqlite.md +++ b/docs/stdlib/sqlite.md @@ -22,20 +22,28 @@ sqlite.exec(db, "INSERT INTO users VALUES (1, 'Alice')"); ## `sqlite.get(db, sql)` -Execute a query and return the first column of the first row as a string. +Execute a query and return the first row as a string. Multi-column results are pipe-separated. ```typescript const name = sqlite.get(db, "SELECT name FROM users WHERE id = 1"); // "Alice" + +const row = sqlite.get(db, "SELECT id, name FROM users WHERE id = 1"); +// "1|Alice" ``` ## `sqlite.all(db, sql)` -Execute a query and return the first column of all rows as a string array. +Execute a query and return all rows as a string array. Multi-column results are pipe-separated. ```typescript const names = sqlite.all(db, "SELECT name FROM users ORDER BY id"); // ["Alice", "Bob"] + +const rows = sqlite.all(db, "SELECT id, name FROM users ORDER BY id"); +// ["1|Alice", "2|Bob"] +const parts = rows[0].split('|'); +// parts[0] = "1", parts[1] = "Alice" ``` ## `sqlite.close(db)` @@ -64,7 +72,7 @@ sqlite.close(db); ``` ::: tip -`get` and `all` return only the first column. Select the specific column you need in your SQL query. +Multi-column queries return pipe-separated values. Use `.split('|')` to access individual columns. ::: ## Native Implementation diff --git a/examples/hackernews/app.ts b/examples/hackernews/app.ts new file mode 100644 index 00000000..fceedceb --- /dev/null +++ b/examples/hackernews/app.ts @@ -0,0 +1,81 @@ +interface HttpRequest { + method: string; + path: string; + body: string; + contentType: string; +} + +interface HttpResponse { + status: number; + body: string; +} + +ChadScript.embedDir('./public'); + +const db = sqlite.open(":memory:"); +sqlite.exec(db, "CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, url TEXT, points INTEGER)"); + +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Show HN: ChadScript - TypeScript to native compiler via LLVM', 'https://github.com/cs01/ChadScript', 342)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Why we moved from Node.js to native binaries', 'https://example.com/native', 287)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('LLVM IR is surprisingly readable', 'https://llvm.org/docs/LangRef.html', 256)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('SQLite is the only database you need', 'https://sqlite.org', 234)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Single-binary deployments changed everything', 'https://example.com/single-binary', 198)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('The Boehm GC: garbage collection for C programs', 'https://hboehm.info/gc/', 176)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Zero-cost TypeScript: no runtime overhead', 'https://example.com/zero-cost', 165)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('libwebsockets: lightweight C WebSocket library', 'https://libwebsockets.org', 154)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Self-hosting compilers: the ultimate test', 'https://example.com/self-hosting', 143)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Compile-time file embedding in native languages', 'https://example.com/embed', 132)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Why I stopped using Docker for simple services', 'https://example.com/no-docker', 121)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('libuv: the event loop behind Node.js', 'https://libuv.org', 110)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Tree-sitter for building parsers', 'https://tree-sitter.github.io', 98)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Ask HN: What is your deploy strategy for side projects?', 'https://news.ycombinator.com', 87)"); +sqlite.exec(db, "INSERT INTO posts (title, url, points) VALUES ('Building a compiler is easier than you think', 'https://example.com/compiler-easy', 76)"); + +function renderPosts(): string { + const rows = sqlite.all(db, "SELECT id, title, url, points FROM posts ORDER BY points DESC"); + let html = ''; + for (let i = 0; i < rows.length; i++) { + const parts = rows[i].split('|'); + const id = parts[0]; + const title = parts[1]; + const url = parts[2]; + const points = parts[3]; + const rank = i + 1; + html = html + '
' + rank + '.'; + html = html + '
'; + html = html + '
'; + html = html + '' + title + '
'; + html = html + '
' + points + ' points
'; + } + return html; +} + +function handleRequest(req: HttpRequest): HttpResponse { + console.log(req.method + " " + req.path); + + if (req.method === "GET" && req.path === "/") { + const template = ChadScript.getEmbeddedFile('index.html'); + const posts = renderPosts(); + const body = template.replace('{{POSTS}}', posts); + return { status: 200, body: body }; + } + + if (req.method === "GET" && req.path === "/style.css") { + const css = ChadScript.getEmbeddedFile('style.css'); + return { status: 200, body: css }; + } + + if (req.method === "POST" && req.path.startsWith("/upvote/")) { + const idStr = req.path.substring(8, req.path.length); + sqlite.exec(db, "UPDATE posts SET points = points + 1 WHERE id = " + idStr); + const redirectHtml = 'Redirecting...'; + return { status: 200, body: redirectHtml }; + } + + return { status: 404, body: "Not Found" }; +} + +console.log("Hacker News clone starting on http://localhost:3000"); +console.log("All HTML/CSS embedded in the binary at compile time"); +console.log("SQLite database running in-memory"); +httpServe(3000, handleRequest); diff --git a/examples/hackernews/public/index.html b/examples/hackernews/public/index.html new file mode 100644 index 00000000..24c11097 --- /dev/null +++ b/examples/hackernews/public/index.html @@ -0,0 +1,26 @@ + + + + + + Hacker News + + + + +
+ + {{POSTS}} +
+ + + diff --git a/examples/hackernews/public/style.css b/examples/hackernews/public/style.css new file mode 100644 index 00000000..3e9870c7 --- /dev/null +++ b/examples/hackernews/public/style.css @@ -0,0 +1,124 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Verdana, Geneva, sans-serif; + font-size: 10pt; + background: #f6f6ef; + color: #828282; +} + +#header { + background: #ff6600; + padding: 4px 8px; + display: flex; + align-items: center; + gap: 8px; +} + +#header .logo { + display: flex; + align-items: center; + gap: 4px; + text-decoration: none; + color: #000; +} + +#header .logo-box { + display: inline-block; + width: 18px; + height: 18px; + background: #fff; + color: #ff6600; + text-align: center; + font-weight: bold; + font-size: 13px; + line-height: 18px; + border: 1px solid #fff; +} + +#header b { + font-size: 10pt; +} + +#header .tagline { + color: #000; + font-size: 8pt; + margin-left: auto; +} + +#content { + padding: 10px 8px; +} + +.post { + display: flex; + align-items: baseline; + padding: 3px 0; +} + +.rank { + color: #828282; + min-width: 28px; + text-align: right; + margin-right: 6px; +} + +.upvote { + display: inline-block; + width: 0; + height: 0; + border: none; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 10px solid #828282; + background: none; + padding: 0; + cursor: pointer; + margin-right: 4px; + vertical-align: middle; + -webkit-appearance: none; + appearance: none; +} + +.title a { + color: #000; + text-decoration: none; + font-size: 10pt; +} + +.title a:visited { + color: #828282; +} + +.meta { + font-size: 7pt; + color: #828282; + padding-left: 38px; + padding-bottom: 5px; +} + +.meta a { + color: #828282; + text-decoration: none; +} + +.meta a:hover { + text-decoration: underline; +} + +#footer { + border-top: 2px solid #ff6600; + padding: 16px 8px; + text-align: center; + font-size: 8pt; + color: #828282; + margin-top: 20px; +} + +#footer p { + margin: 4px 0; +} diff --git a/examples/parallel.ts b/examples/parallel.ts new file mode 100644 index 00000000..3020cae4 --- /dev/null +++ b/examples/parallel.ts @@ -0,0 +1,9 @@ +async function main() { + const a = fetch("https://api.example.com/users"); + const b = fetch("https://api.example.com/posts"); + const [users, posts] = await Promise.all([a, b]); + console.log("users: " + users.status); + console.log("posts: " + posts.status); +} + +main(); diff --git a/examples/query.ts b/examples/query.ts new file mode 100644 index 00000000..aa92180c --- /dev/null +++ b/examples/query.ts @@ -0,0 +1,9 @@ +const db = sqlite.open("app.db"); +sqlite.exec(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"); +sqlite.exec(db, "INSERT INTO users (name) VALUES ('Alice')"); +const rows = sqlite.all(db, "SELECT * FROM users"); +console.log(rows.length + " rows"); +for (let i = 0; i < rows.length; i++) { + console.log(rows[i]); +} +sqlite.close(db); diff --git a/src/codegen/expressions/access/member.ts b/src/codegen/expressions/access/member.ts index e7c1d675..dbb51ed6 100644 --- a/src/codegen/expressions/access/member.ts +++ b/src/codegen/expressions/access/member.ts @@ -2545,7 +2545,7 @@ export class MemberAccessGenerator { } private handleStatProperty(expr: MemberAccessNode): string | null { - if (expr.property !== 'size' && expr.property !== 'isFile' && expr.property !== 'isDirectory') return null; + if (expr.property !== 'size') return null; const exprObjBase = expr.object as ExprBase; if (exprObjBase.type !== 'variable') return null; const varName = (expr.object as VariableNode).name; @@ -2559,12 +2559,8 @@ export class MemberAccessGenerator { this.ctx.emit(`${raw} = load i8*, i8** ${varPtr}`); const statPtr = this.ctx.nextTemp(); this.ctx.emit(`${statPtr} = bitcast i8* ${raw} to double*`); - let fieldIdx = 0; - if (expr.property === 'size') fieldIdx = 0; - else if (expr.property === 'isFile') fieldIdx = 1; - else if (expr.property === 'isDirectory') fieldIdx = 2; const fieldPtr = this.ctx.nextTemp(); - this.ctx.emit(`${fieldPtr} = getelementptr inbounds double, double* ${statPtr}, i64 ${fieldIdx}`); + this.ctx.emit(`${fieldPtr} = getelementptr inbounds double, double* ${statPtr}, i64 0`); const result = this.ctx.nextTemp(); this.ctx.emit(`${result} = load double, double* ${fieldPtr}`); this.ctx.setVariableType(result, 'double'); diff --git a/src/codegen/expressions/method-calls.ts b/src/codegen/expressions/method-calls.ts index 850d04b5..3c420c35 100644 --- a/src/codegen/expressions/method-calls.ts +++ b/src/codegen/expressions/method-calls.ts @@ -32,7 +32,7 @@ import { SourceLocation, } from '../../ast/types.js'; import type { SymbolTable } from '../infrastructure/symbol-table.js'; -import type { IStringGenerator, IFsGenerator, IPathGenerator, IJsonGenerator, IMathGenerator, IDateGenerator, ICryptoGenerator, ISqliteGenerator, IResponseGenerator, IRegexGenerator, IArrowFunctionGenerator, IStringMapGenerator, IMapGenerator, ISetGenerator, IStringSetGenerator, IPointerMapGenerator, IArrayGenerator } from '../infrastructure/generator-context.js'; +import type { IStringGenerator, IFsGenerator, IPathGenerator, IJsonGenerator, IMathGenerator, IDateGenerator, ICryptoGenerator, ISqliteGenerator, IResponseGenerator, IRegexGenerator, IArrowFunctionGenerator, IStringMapGenerator, IMapGenerator, ISetGenerator, IStringSetGenerator, IPointerMapGenerator, IArrayGenerator, IEmbedGenerator } from '../infrastructure/generator-context.js'; import { parseMapTypeString, parseSetTypeString } from '../infrastructure/type-system.js'; import { generateConsoleCallInline } from './method-calls/console.js'; import { handleAssertStrictEqual, handleAssertNotStrictEqual, handleAssertOk, handleAssertDeepEqual, handleAssertFail } from './method-calls/assert.js'; @@ -115,6 +115,7 @@ export interface MethodCallGeneratorContext { readonly stringSetGen: IStringSetGenerator; readonly pointerMapGen: IPointerMapGenerator; readonly arrayGen: IArrayGenerator; + readonly embedGen: IEmbedGenerator; readonly typeResolver?: { getThisFieldMapKeyType(expr: Expression): string | null; getThisFieldSetValueType(expr: Expression): string | null }; ensureDouble(value: string): string; ensureI64(value: string): string; @@ -266,6 +267,18 @@ export class MethodCallGenerator { return handlePromiseStaticMethods(this.ctx, expr, params); } + // Handle ChadScript.embedFile/embedDir/getEmbeddedFile + if (this.isVariableWithName(expr.object, 'ChadScript')) { + if (method === 'embedFile') { + return this.ctx.embedGen.generateEmbedFile(expr, params); + } else if (method === 'embedDir') { + return this.ctx.embedGen.generateEmbedDir(expr, params); + } else if (method === 'getEmbeddedFile') { + return this.ctx.embedGen.generateGetEmbeddedFile(expr, params); + } + return this.ctx.emitError(`ChadScript.${method}() is not a supported method`, expr.loc); + } + // Handle Array.from() - returns the argument as-is since our iterators already produce arrays if (this.isVariableWithName(expr.object, 'Array') && method === 'from') { if (expr.args.length === 0) { @@ -513,6 +526,41 @@ export class MethodCallGenerator { } } + if (method === 'isFile' || method === 'isDirectory') { + let statI8Ptr: string | null = null; + + if (objBase2.type === 'variable') { + const varName = (expr.object as VariableNode).name; + const varType = this.ctx.getVariableType(varName); + if (varType === '%StatResult*') { + const varPtr = this.ctx.symbolTable.getAlloca(varName); + if (varPtr) { + const raw = this.nextTemp(); + this.emit(`${raw} = load i8*, i8** ${varPtr}`); + statI8Ptr = raw; + } + } + } else { + const objVal = this.ctx.generateExpression(expr.object, params); + const objType = this.ctx.getVariableType(objVal); + if (objType === '%StatResult*') { + statI8Ptr = objVal; + } + } + + if (statI8Ptr) { + const statPtr = this.nextTemp(); + this.emit(`${statPtr} = bitcast i8* ${statI8Ptr} to double*`); + const fieldIdx = method === 'isFile' ? 1 : 2; + const fieldPtr = this.nextTemp(); + this.emit(`${fieldPtr} = getelementptr inbounds double, double* ${statPtr}, i64 ${fieldIdx}`); + const result = this.nextTemp(); + this.emit(`${result} = load double, double* ${fieldPtr}`); + this.ctx.setVariableType(result, 'double'); + return result; + } + } + // Handle Response methods (from fetch()) if (method === 'text' || method === 'json') { const isLikelyResponse = this.isLikelyResponseExpression(expr); diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index 722f9e45..0d81f8ba 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -149,6 +149,14 @@ export interface ISqliteGenerator { generateClose(expr: MethodCallNode, params: string[]): string; } +export interface IEmbedGenerator { + generateEmbedFile(expr: MethodCallNode, params: string[]): string; + generateEmbedDir(expr: MethodCallNode, params: string[]): string; + generateGetEmbeddedFile(expr: MethodCallNode, params: string[]): string; + generateLookupFunction(): string; + hasEmbeddedFiles(): boolean; +} + export interface IArrowFunctionGenerator { generateArrowFunction( expr: Expression, @@ -763,6 +771,8 @@ export interface IGeneratorContext { readonly arrowFunctionGen: IArrowFunctionGenerator; + readonly embedGen: IEmbedGenerator; + ensureDouble(value: string): string; ensureI64(value: string): string; } @@ -1459,6 +1469,13 @@ export class MockGeneratorContext implements IGeneratorContext { generatePointerMapGet: (_mapPtr: string, _keyValue: string, _valueType: string): string => '%mock_pointer_map_get', generatePointerMapClear: (_mapPtr: string): string => '%mock_pointer_map_clear', }; + embedGen: IEmbedGenerator = { + generateEmbedFile: (_expr: MethodCallNode, _params: string[]): string => '%mock_embed_file', + generateEmbedDir: (_expr: MethodCallNode, _params: string[]): string => '%mock_embed_dir', + generateGetEmbeddedFile: (_expr: MethodCallNode, _params: string[]): string => '%mock_get_embedded', + generateLookupFunction: (): string => '', + hasEmbeddedFiles: (): boolean => false, + }; arrayGen: IArrayGenerator = { generateArrayLiteral: (_expr: ArrayNode, _params: string[]): string => '%mock_array_literal', generateArrayPush: (_expr: MethodCallNode, _params: string[]): string => '%mock_array_push', diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 67f32063..f87275ed 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -263,7 +263,8 @@ export class TypeInference { } if (method === 'startsWith' || method === 'endsWith' || method === 'test' || - method === 'has' || method === 'delete' || method === 'every' || method === 'some') { + method === 'has' || method === 'delete' || method === 'every' || method === 'some' || + method === 'isFile' || method === 'isDirectory') { return this.ctx.typeContext.booleanType; } @@ -306,6 +307,10 @@ export class TypeInference { if (varName === 'Promise') return this.ctx.typeContext.resolve('Promise'); + if (varName === 'ChadScript') { + if (method === 'embedFile' || method === 'getEmbeddedFile') return this.ctx.typeContext.stringType; + } + if (varName === 'Array' && method === 'from') return this.ctx.typeContext.getArrayType('number'); if (this.ctx.symbolTable.isClass(varName)) { diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index f07df57d..36463563 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -30,6 +30,7 @@ import { FilesystemGenerator } from './stdlib/fs.js'; import { ResponseGenerator } from './stdlib/response.js'; import { CryptoGenerator } from './stdlib/crypto.js'; import { SqliteGenerator } from './stdlib/sqlite.js'; +import { EmbedGenerator } from './stdlib/embed.js'; import { RuntimeGenerator } from './runtime/runtime.js'; import { HttpServerGenerator } from './stdlib/http-server.js'; import { LibuvGenerator } from './stdlib/libuv.js'; @@ -110,6 +111,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { public arrowFunctionGen!: IArrowFunctionGenerator; public cryptoGen: CryptoGenerator; public sqliteGen: SqliteGenerator; + public embedGen: EmbedGenerator; private runtimeGen: RuntimeGenerator; private httpServerGen: HttpServerGenerator; private libuvGen: LibuvGenerator; @@ -1018,6 +1020,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { this.responseGen = new ResponseGenerator(this); this.cryptoGen = new CryptoGenerator(this); this.sqliteGen = new SqliteGenerator(this); + this.embedGen = new EmbedGenerator(this, this.filename); this.runtimeGen = new RuntimeGenerator(); this.httpServerGen = new HttpServerGenerator(); this.libuvGen = new LibuvGenerator(); @@ -1766,6 +1769,10 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { irParts.push('\n'); } + if (this.embedGen.hasEmbeddedFiles()) { + irParts.push(this.embedGen.generateLookupFunction()); + } + const needsLibuv = this.usesTimers || this.usesPromises || this.usesCurl || this.usesUvHrtime; const needsPromise = this.usesPromises || this.usesCurl; @@ -1846,6 +1853,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { } if (this.usesSqlite) { + finalParts.push(this.sqliteGen.generateSqliteRowToStringHelper()); finalParts.push(this.sqliteGen.generateSqliteGetHelper()); finalParts.push(this.sqliteGen.generateSqliteAllHelper()); finalParts.push(this.sqliteGen.generateSqliteBindParamsHelper()); diff --git a/src/codegen/stdlib/embed.ts b/src/codegen/stdlib/embed.ts new file mode 100644 index 00000000..5a7630dc --- /dev/null +++ b/src/codegen/stdlib/embed.ts @@ -0,0 +1,226 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { MethodCallNode } from '../../ast/types.js'; +import { IGeneratorContext } from '../infrastructure/generator-context.js'; + +interface ExprBase { type: string; } + +interface StringLiteralNode { + type: 'string'; + value: string; +} + +export class EmbedGenerator { + private embeddedKeys: string[] = []; + private embeddedStrIds: string[] = []; + private embeddedStrLens: number[] = []; + private entryDir: string; + private _lastStrId: string = ''; + private _lastLen: number = 0; + + constructor(private ctx: IGeneratorContext, filename: string) { + this.entryDir = filename ? path.dirname(path.resolve(filename)) : process.cwd(); + } + + hasEmbeddedFiles(): boolean { + return this.embeddedKeys.length > 0; + } + + private escapeForLLVM(value: string): string { + let escaped = ''; + for (let i = 0; i < value.length; i++) { + const ch = value[i]; + const code = value.charCodeAt(i); + if (ch === '\\') { + escaped += '\\5C'; + } else if (ch === '\n') { + escaped += '\\0A'; + } else if (ch === '\r') { + escaped += '\\0D'; + } else if (ch === '\t') { + escaped += '\\09'; + } else if (ch === '"') { + escaped += '\\22'; + } else if (code < 32 || code > 126) { + if (code < 128) { + escaped += '\\' + this.byteToHex(code); + } else if (code < 0x800) { + escaped += '\\' + this.byteToHex(0xC0 | (code >> 6)); + escaped += '\\' + this.byteToHex(0x80 | (code & 0x3F)); + } else { + escaped += '\\' + this.byteToHex(0xE0 | (code >> 12)); + escaped += '\\' + this.byteToHex(0x80 | ((code >> 6) & 0x3F)); + escaped += '\\' + this.byteToHex(0x80 | (code & 0x3F)); + } + } else { + escaped += ch; + } + } + return escaped; + } + + private countUtf8Bytes(value: string): number { + let count = 0; + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); + if (code < 128) { + count += 1; + } else if (code < 0x800) { + count += 2; + } else { + count += 3; + } + } + return count; + } + + private byteToHex(b: number): string { + const hexChars = '0123456789ABCDEF'; + const hi = hexChars.charAt((b >> 4) & 0xF); + const lo = hexChars.charAt(b & 0xF); + return hi + lo; + } + + private createGlobalStringDirect(value: string): void { + const strId = this.ctx.nextString(); + const escaped = this.escapeForLLVM(value); + const len = this.countUtf8Bytes(value) + 1; + this.ctx.pushGlobalString(strId + ' = private unnamed_addr constant [' + len + ' x i8] c"' + escaped + '\\00", align 1'); + this._lastStrId = strId; + this._lastLen = len; + } + + generateEmbedFile(expr: MethodCallNode, _params: string[]): string { + if (expr.args.length < 1) { + return this.ctx.emitError('ChadScript.embedFile() requires 1 argument (file path)', expr.loc); + } + + const argBase = expr.args[0] as ExprBase; + if (argBase.type !== 'string') { + return this.ctx.emitError('ChadScript.embedFile() argument must be a string literal', expr.loc); + } + + const relPath = (expr.args[0] as StringLiteralNode).value; + const absPath = path.resolve(this.entryDir, relPath); + + if (!fs.existsSync(absPath)) { + return this.ctx.emitError('ChadScript.embedFile(): file not found: ' + absPath, expr.loc); + } + + const content = fs.readFileSync(absPath, 'utf-8'); + this.createGlobalStringDirect(content); + const strId = this._lastStrId; + const len = this._lastLen; + + const ptrReg = this.ctx.nextTemp(); + this.ctx.emit(ptrReg + ' = getelementptr inbounds [' + len + ' x i8], [' + len + ' x i8]* ' + strId + ', i64 0, i64 0'); + this.ctx.setVariableType(ptrReg, 'i8*'); + + const key = path.basename(relPath); + this.embeddedKeys.push(key); + this.embeddedStrIds.push(strId); + this.embeddedStrLens.push(len); + + return ptrReg; + } + + generateEmbedDir(expr: MethodCallNode, _params: string[]): string { + if (expr.args.length < 1) { + return this.ctx.emitError('ChadScript.embedDir() requires 1 argument (directory path)', expr.loc); + } + + const argBase = expr.args[0] as ExprBase; + if (argBase.type !== 'string') { + return this.ctx.emitError('ChadScript.embedDir() argument must be a string literal', expr.loc); + } + + const relPath = (expr.args[0] as StringLiteralNode).value; + const absPath = path.resolve(this.entryDir, relPath); + + if (!fs.existsSync(absPath)) { + return this.ctx.emitError('ChadScript.embedDir(): directory not found: ' + absPath, expr.loc); + } + + this.walkDir(absPath, absPath); + + return 'null'; + } + + private walkDir(dirPath: string, baseDir: string): void { + const entries = fs.readdirSync(dirPath); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const fullPath = path.join(dirPath, entry); + if (fs.statSync(fullPath).isDirectory()) { + this.walkDir(fullPath, baseDir); + } else { + const content = fs.readFileSync(fullPath, 'utf-8'); + this.createGlobalStringDirect(content); + const key = fullPath.substring(baseDir.length + 1); + this.embeddedKeys.push(key); + this.embeddedStrIds.push(this._lastStrId); + this.embeddedStrLens.push(this._lastLen); + } + } + } + + generateGetEmbeddedFile(expr: MethodCallNode, params: string[]): string { + if (expr.args.length < 1) { + return this.ctx.emitError('ChadScript.getEmbeddedFile() requires 1 argument (file key)', expr.loc); + } + + const keyPtr = this.ctx.generateExpression(expr.args[0], params); + + const result = this.ctx.nextTemp(); + this.ctx.emit(result + ' = call i8* @__cs_get_embedded_file(i8* ' + keyPtr + ')'); + this.ctx.setVariableType(result, 'i8*'); + + return result; + } + + generateLookupFunction(): string { + if (this.embeddedKeys.length === 0) { + return ''; + } + + const keyStrIds: string[] = []; + const keyLens: number[] = []; + for (let i = 0; i < this.embeddedKeys.length; i++) { + this.createGlobalStringDirect(this.embeddedKeys[i]); + keyStrIds.push(this._lastStrId); + keyLens.push(this._lastLen); + } + this.createGlobalStringDirect(''); + const emptyStrId = this._lastStrId; + const emptyLen = this._lastLen; + + let ir = ''; + ir += 'define i8* @__cs_get_embedded_file(i8* %key) {\n'; + ir += 'entry:\n'; + + for (let i = 0; i < this.embeddedKeys.length; i++) { + const contentStrId = this.embeddedStrIds[i]; + const contentStrLen = this.embeddedStrLens[i]; + + ir += ' %key_ptr_' + i + ' = getelementptr inbounds [' + keyLens[i] + ' x i8], [' + keyLens[i] + ' x i8]* ' + keyStrIds[i] + ', i64 0, i64 0\n'; + ir += ' %cmp_' + i + ' = call i32 @strcmp(i8* %key, i8* %key_ptr_' + i + ')\n'; + ir += ' %is_' + i + ' = icmp eq i32 %cmp_' + i + ', 0\n'; + const foundLabel = 'found' + i; + const nextLabel = i < this.embeddedKeys.length - 1 ? 'check' + (i + 1) : 'notfound'; + ir += ' br i1 %is_' + i + ', label %' + foundLabel + ', label %' + nextLabel + '\n'; + ir += foundLabel + ':\n'; + ir += ' %content_ptr_' + i + ' = getelementptr inbounds [' + contentStrLen + ' x i8], [' + contentStrLen + ' x i8]* ' + contentStrId + ', i64 0, i64 0\n'; + ir += ' ret i8* %content_ptr_' + i + '\n'; + if (i < this.embeddedKeys.length - 1) { + ir += 'check' + (i + 1) + ':\n'; + } + } + + ir += 'notfound:\n'; + ir += ' %empty_ptr = getelementptr inbounds [' + emptyLen + ' x i8], [' + emptyLen + ' x i8]* ' + emptyStrId + ', i64 0, i64 0\n'; + ir += ' ret i8* %empty_ptr\n'; + ir += '}\n\n'; + + return ir; + } +} diff --git a/src/codegen/stdlib/embedded-dts.ts b/src/codegen/stdlib/embedded-dts.ts new file mode 100644 index 00000000..af765610 --- /dev/null +++ b/src/codegen/stdlib/embedded-dts.ts @@ -0,0 +1,3 @@ +export function getDtsContent(): string { + return '/**\n * ChadScript Built-in Global Types\n *\n * Type definitions for ChadScript\'s built-in runtime APIs. These globals\n * are available without imports in all ChadScript programs and are compiled\n * directly to native code via LLVM.\n *\n * Generate this file in any project with: chad init\n *\n * Note: Standard JavaScript types (String, Number, Array, etc.) are provided\n * by TypeScript\'s ES2020 lib. This file only defines ChadScript-specific APIs.\n */\n\n// ============================================================================\n// Console\n// ============================================================================\n\ndeclare namespace console {\n function log(...args: any[]): void;\n function error(...args: any[]): void;\n function warn(...args: any[]): void;\n function debug(...args: any[]): void;\n}\n\n// ============================================================================\n// Process\n// ============================================================================\n\ndeclare namespace process {\n const argv: string[];\n const argv0: string;\n const platform: string;\n const arch: string;\n const pid: number;\n const ppid: number;\n const execPath: string;\n const version: string;\n\n const env: { [key: string]: string };\n\n function exit(code?: number): never;\n function cwd(): string;\n function chdir(path: string): void;\n function uptime(): number;\n function kill(pid: number, signal?: number): void;\n function abort(): never;\n function getuid(): number;\n function getgid(): number;\n function geteuid(): number;\n function getegid(): number;\n\n namespace stdout {\n function write(str: string): void;\n }\n namespace stderr {\n function write(str: string): void;\n }\n}\n\n// ============================================================================\n// Filesystem\n// ============================================================================\n\ndeclare namespace fs {\n function readFileSync(filename: string): string;\n function writeFileSync(filename: string, data: string): number;\n function appendFileSync(filename: string, data: string): void;\n function existsSync(filename: string): boolean;\n function unlinkSync(filename: string): number;\n function readdirSync(path: string): string[];\n function statSync(path: string): { size: number; isFile(): boolean; isDirectory(): boolean };\n}\n\n// ============================================================================\n// Path\n// ============================================================================\n\ndeclare namespace path {\n function join(a: string, b: string): string;\n function resolve(p: string): string;\n function dirname(p: string): string;\n function basename(p: string): string;\n}\n\n// ============================================================================\n// Math\n// ============================================================================\n\ndeclare namespace Math {\n const PI: number;\n const E: number;\n\n function sqrt(x: number): number;\n function pow(base: number, exp: number): number;\n function floor(x: number): number;\n function ceil(x: number): number;\n function round(x: number): number;\n function abs(x: number): number;\n function max(a: number, b: number): number;\n function min(a: number, b: number): number;\n function random(): number;\n function log(x: number): number;\n function log2(x: number): number;\n function log10(x: number): number;\n function sin(x: number): number;\n function cos(x: number): number;\n function tan(x: number): number;\n function trunc(x: number): number;\n function sign(x: number): number;\n}\n\n// ============================================================================\n// Date\n// ============================================================================\n\ndeclare namespace Date {\n function now(): number;\n}\n\n// ============================================================================\n// JSON\n// ============================================================================\n\ndeclare namespace JSON {\n function parse(str: string): T;\n function stringify(value: any): string;\n}\n\n// ============================================================================\n// Crypto\n// ============================================================================\n\ndeclare namespace crypto {\n function sha256(input: string): string;\n function sha512(input: string): string;\n function md5(input: string): string;\n function randomBytes(n: number): string;\n}\n\n// ============================================================================\n// SQLite\n// ============================================================================\n\ndeclare namespace sqlite {\n function open(path: string): any;\n function exec(db: any, sql: string): void;\n function get(db: any, sql: string): string;\n function all(db: any, sql: string): string[];\n function close(db: any): void;\n}\n\n// ============================================================================\n// Child Process\n// ============================================================================\n\ndeclare namespace child_process {\n function execSync(command: string): string;\n}\n\n// ============================================================================\n// TTY\n// ============================================================================\n\ndeclare namespace tty {\n function isatty(fd: number): boolean;\n}\n\n// ============================================================================\n// Number\n// ============================================================================\n\ndeclare namespace Number {\n function isFinite(x: number): boolean;\n function isNaN(x: number): boolean;\n function isInteger(x: number): boolean;\n}\n\n// ============================================================================\n// Object\n// ============================================================================\n\ndeclare namespace Object {\n function keys(obj: any): string[];\n function values(obj: any): string[];\n function entries(obj: any): string[];\n}\n\n// ============================================================================\n// HTTP & Networking\n// ============================================================================\n\ninterface Response {\n text(): string;\n json(): T;\n status: number;\n ok: boolean;\n}\n\ndeclare function fetch(url: string): Promise;\n\ninterface HttpRequest {\n method: string;\n path: string;\n body: string;\n contentType: string;\n}\n\ninterface HttpResponse {\n status: number;\n body: string;\n}\n\ndeclare function httpServe(port: number, handler: (req: HttpRequest) => HttpResponse): void;\n\n// ============================================================================\n// Async / Timers\n// ============================================================================\n\ndeclare function setTimeout(callback: () => void, delay: number): number;\ndeclare function setInterval(callback: () => void, interval: number): number;\ndeclare function clearTimeout(id: number): void;\ndeclare function clearInterval(id: number): void;\ndeclare function runEventLoop(): void;\n\n// ============================================================================\n// Global Functions\n// ============================================================================\n\ndeclare function parseInt(str: string, radix?: number): number;\ndeclare function parseFloat(str: string): number;\ndeclare function isNaN(value: any): boolean;\ndeclare function execSync(command: string): string;\n\n// ============================================================================\n// Low-Level System Calls\n// ============================================================================\n\ndeclare function malloc(size: number): number;\ndeclare function free(ptr: number): void;\ndeclare function socket(domain: number, type: number, protocol: number): number;\ndeclare function bind(socket: number, addr: number, addrlen: number): number;\ndeclare function listen(socket: number, backlog: number): number;\ndeclare function accept(socket: number, addr: number, addrlen: number): number;\ndeclare function htons(hostshort: number): number;\ndeclare function close(fd: number): number;\ndeclare function read(fd: number, buf: number, count: number): number;\ndeclare function write(fd: number, buf: number, count: number): number;\n\n// ============================================================================\n// Test Runner\n// ============================================================================\n\ndeclare namespace assert {\n function strictEqual(actual: any, expected: any): void;\n function notStrictEqual(actual: any, expected: any): void;\n function deepEqual(actual: any, expected: any): void;\n function ok(value: any): void;\n function fail(message?: string): void;\n}\n\ndeclare function test(name: string, fn: () => void): void;\ndeclare function describe(name: string, fn: () => void): void;\n\n// ============================================================================\n// Compile-Time File Embedding\n// ============================================================================\n\ndeclare namespace ChadScript {\n function embedFile(path: string): string;\n function embedDir(path: string): void;\n function getEmbeddedFile(key: string): string;\n}\n'; +} diff --git a/src/codegen/stdlib/path.ts b/src/codegen/stdlib/path.ts index d8c3ecc8..18c0440a 100644 --- a/src/codegen/stdlib/path.ts +++ b/src/codegen/stdlib/path.ts @@ -36,19 +36,25 @@ export class PathGenerator { return this.ctx.emitError('path.resolve() requires at least 1 argument', expr.loc); } - const pathPtr = this.ctx.generateExpression(expr.args[0], params); + let pathPtr = this.ctx.generateExpression(expr.args[0], params); + + if (expr.args.length > 1) { + const slash = this.ctx.stringGen.doCreateStringConstant('/'); + for (let i = 1; i < expr.args.length; i++) { + const part = this.ctx.generateExpression(expr.args[i], params); + const withSlash = this.ctx.stringGen.doGenerateStringConcatDirect(pathPtr, slash); + pathPtr = this.ctx.stringGen.doGenerateStringConcatDirect(withSlash, part); + } + } - // Allocate buffer for resolved path (PATH_MAX = 4096) const bufferSize = this.ctx.nextTemp(); this.ctx.emit(`${bufferSize} = add i64 0, 4096`); const buffer = this.ctx.nextTemp(); this.ctx.emit(`${buffer} = call i8* @GC_malloc_atomic(i64 ${bufferSize})`); - // Call realpath: realpath(path, buffer) const resolvedPtr = this.ctx.nextTemp(); this.ctx.emit(`${resolvedPtr} = call i8* @realpath(i8* ${pathPtr}, i8* ${buffer})`); - // If realpath returns NULL, return the original path const isNull = this.ctx.nextTemp(); this.ctx.emit(`${isNull} = icmp eq i8* ${resolvedPtr}, null`); @@ -58,15 +64,12 @@ export class PathGenerator { this.ctx.emit(`br i1 ${isNull}, label %${failLabel}, label %${successLabel}`); - // Success: return resolved path this.ctx.emit(`${successLabel}:`); this.ctx.emit(`br label %${endLabel}`); - // Failure: GC will handle cleanup, return original path this.ctx.emit(`${failLabel}:`); this.ctx.emit(`br label %${endLabel}`); - // End: phi node this.ctx.emit(`${endLabel}:`); const result = this.ctx.nextTemp(); this.ctx.emit(`${result} = phi i8* [ ${resolvedPtr}, %${successLabel} ], [ ${pathPtr}, %${failLabel} ]`); @@ -145,11 +148,8 @@ export class PathGenerator { for (let i = 1; i < expr.args.length; i++) { const part = this.ctx.generateExpression(expr.args[i], params); - const withSlash = this.ctx.nextTemp(); - this.ctx.emit(`${withSlash} = call i8* @__string_concat(i8* ${result}, i8* ${slash})`); - const joined = this.ctx.nextTemp(); - this.ctx.emit(`${joined} = call i8* @__string_concat(i8* ${withSlash}, i8* ${part})`); - result = joined; + const withSlash = this.ctx.stringGen.doGenerateStringConcatDirect(result, slash); + result = this.ctx.stringGen.doGenerateStringConcatDirect(withSlash, part); } this.ctx.setVariableType(result, 'i8*'); diff --git a/src/codegen/stdlib/sqlite.ts b/src/codegen/stdlib/sqlite.ts index 377da449..1860b08f 100644 --- a/src/codegen/stdlib/sqlite.ts +++ b/src/codegen/stdlib/sqlite.ts @@ -165,6 +165,92 @@ export class SqliteGenerator { return arr; } + generateSqliteRowToStringHelper(): string { + let ir = ''; + ir += 'define i8* @__sqlite_row_to_string(i8* %stmt, i32 %col_count) {\n'; + ir += 'entry:\n'; + ir += ' %is_single = icmp eq i32 %col_count, 1\n'; + ir += ' br i1 %is_single, label %single_col, label %multi_col_start\n'; + ir += '\n'; + ir += 'single_col:\n'; + ir += ' %sc_text = call i8* @sqlite3_column_text(i8* %stmt, i32 0)\n'; + ir += ' %sc_copy = call i8* @strdup(i8* %sc_text)\n'; + ir += ' ret i8* %sc_copy\n'; + ir += '\n'; + ir += 'multi_col_start:\n'; + ir += ' %total_len_init = sext i32 0 to i64\n'; + ir += ' br label %len_loop\n'; + ir += '\n'; + ir += 'len_loop:\n'; + ir += ' %li = phi i32 [ 0, %multi_col_start ], [ %li_next, %len_add ]\n'; + ir += ' %total_len = phi i64 [ 0, %multi_col_start ], [ %total_len_next, %len_add ]\n'; + ir += ' %li_done = icmp sge i32 %li, %col_count\n'; + ir += ' br i1 %li_done, label %alloc, label %len_body\n'; + ir += '\n'; + ir += 'len_body:\n'; + ir += ' %lt = call i8* @sqlite3_column_text(i8* %stmt, i32 %li)\n'; + ir += ' %lt_nn = icmp ne i8* %lt, null\n'; + ir += ' br i1 %lt_nn, label %len_has_text, label %len_null_text\n'; + ir += '\n'; + ir += 'len_has_text:\n'; + ir += ' %lt_len = call i64 @strlen(i8* %lt)\n'; + ir += ' br label %len_add\n'; + ir += '\n'; + ir += 'len_null_text:\n'; + ir += ' br label %len_add\n'; + ir += '\n'; + ir += 'len_add:\n'; + ir += ' %col_len = phi i64 [ %lt_len, %len_has_text ], [ 0, %len_null_text ]\n'; + ir += ' %total_len_with_col = add i64 %total_len, %col_len\n'; + ir += ' %is_not_last = icmp slt i32 %li, %col_count\n'; + ir += ' %pipe_extra = select i1 %is_not_last, i64 1, i64 0\n'; + ir += ' %total_len_next = add i64 %total_len_with_col, %pipe_extra\n'; + ir += ' %li_next = add i32 %li, 1\n'; + ir += ' br label %len_loop\n'; + ir += '\n'; + ir += 'alloc:\n'; + ir += ' %buf_size = add i64 %total_len, 1\n'; + ir += ' %buf = call i8* @GC_malloc_atomic(i64 %buf_size)\n'; + ir += ' store i8 0, i8* %buf\n'; + ir += ' br label %cat_loop\n'; + ir += '\n'; + ir += 'cat_loop:\n'; + ir += ' %ci = phi i32 [ 0, %alloc ], [ %ci_next, %cat_continue ]\n'; + ir += ' %ci_done = icmp sge i32 %ci, %col_count\n'; + ir += ' br i1 %ci_done, label %cat_done, label %cat_body\n'; + ir += '\n'; + ir += 'cat_body:\n'; + ir += ' %need_pipe = icmp sgt i32 %ci, 0\n'; + ir += ' br i1 %need_pipe, label %add_pipe, label %add_col\n'; + ir += '\n'; + ir += 'add_pipe:\n'; + ir += ' %cur_len_p = call i64 @strlen(i8* %buf)\n'; + ir += ' %pipe_ptr = getelementptr inbounds i8, i8* %buf, i64 %cur_len_p\n'; + ir += ' store i8 124, i8* %pipe_ptr\n'; + ir += ' %after_pipe = add i64 %cur_len_p, 1\n'; + ir += ' %null_ptr_p = getelementptr inbounds i8, i8* %buf, i64 %after_pipe\n'; + ir += ' store i8 0, i8* %null_ptr_p\n'; + ir += ' br label %add_col\n'; + ir += '\n'; + ir += 'add_col:\n'; + ir += ' %ct = call i8* @sqlite3_column_text(i8* %stmt, i32 %ci)\n'; + ir += ' %ct_nn = icmp ne i8* %ct, null\n'; + ir += ' br i1 %ct_nn, label %cat_has_text, label %cat_continue\n'; + ir += '\n'; + ir += 'cat_has_text:\n'; + ir += ' call i8* @strcat(i8* %buf, i8* %ct)\n'; + ir += ' br label %cat_continue\n'; + ir += '\n'; + ir += 'cat_continue:\n'; + ir += ' %ci_next = add i32 %ci, 1\n'; + ir += ' br label %cat_loop\n'; + ir += '\n'; + ir += 'cat_done:\n'; + ir += ' ret i8* %buf\n'; + ir += '}\n\n'; + return ir; + } + generateSqliteGetHelper(): string { let ir = ''; ir += 'define i8* @__sqlite_get(i8* %db, i8* %sql) {\n'; @@ -180,8 +266,8 @@ export class SqliteGenerator { ir += ' br i1 %is_row, label %has_row, label %no_row\n'; ir += '\n'; ir += 'has_row:\n'; - ir += ' %col_text = call i8* @sqlite3_column_text(i8* %stmt, i32 0)\n'; - ir += ' %result = call i8* @strdup(i8* %col_text)\n'; + ir += ' %col_count = call i32 @sqlite3_column_count(i8* %stmt)\n'; + ir += ' %result = call i8* @__sqlite_row_to_string(i8* %stmt, i32 %col_count)\n'; ir += ' call i32 @sqlite3_finalize(i8* %stmt)\n'; ir += ' ret i8* %result\n'; ir += '\n'; @@ -215,8 +301,8 @@ export class SqliteGenerator { ir += ' br i1 %is_row, label %body, label %done\n'; ir += '\n'; ir += 'body:\n'; - ir += ' %col_text = call i8* @sqlite3_column_text(i8* %stmt, i32 0)\n'; - ir += ' %text_copy = call i8* @strdup(i8* %col_text)\n'; + ir += ' %col_count = call i32 @sqlite3_column_count(i8* %stmt)\n'; + ir += ' %text_copy = call i8* @__sqlite_row_to_string(i8* %stmt, i32 %col_count)\n'; ir += ' %need_grow = icmp eq i32 %len, %cap\n'; ir += ' br i1 %need_grow, label %grow, label %store\n'; ir += '\n'; @@ -317,8 +403,8 @@ export class SqliteGenerator { ir += ' br i1 %is_row, label %has_row, label %no_row\n'; ir += '\n'; ir += 'has_row:\n'; - ir += ' %col_text = call i8* @sqlite3_column_text(i8* %stmt, i32 0)\n'; - ir += ' %result = call i8* @strdup(i8* %col_text)\n'; + ir += ' %col_count_gp = call i32 @sqlite3_column_count(i8* %stmt)\n'; + ir += ' %result = call i8* @__sqlite_row_to_string(i8* %stmt, i32 %col_count_gp)\n'; ir += ' call i32 @sqlite3_finalize(i8* %stmt)\n'; ir += ' ret i8* %result\n'; ir += '\n'; @@ -353,8 +439,8 @@ export class SqliteGenerator { ir += ' br i1 %is_row, label %body, label %done\n'; ir += '\n'; ir += 'body:\n'; - ir += ' %col_text = call i8* @sqlite3_column_text(i8* %stmt, i32 0)\n'; - ir += ' %text_copy = call i8* @strdup(i8* %col_text)\n'; + ir += ' %col_count_ap = call i32 @sqlite3_column_count(i8* %stmt)\n'; + ir += ' %text_copy = call i8* @__sqlite_row_to_string(i8* %stmt, i32 %col_count_ap)\n'; ir += ' %need_grow = icmp eq i32 %len, %cap\n'; ir += ' br i1 %need_grow, label %grow, label %store\n'; ir += '\n'; diff --git a/tests/examples.test.ts b/tests/examples.test.ts index e478280e..cb8628cb 100644 --- a/tests/examples.test.ts +++ b/tests/examples.test.ts @@ -22,8 +22,12 @@ const EXAMPLES: ExampleTest[] = [ { file: 'hello.ts', mode: 'run' }, { file: 'timers.ts', mode: 'run', timeout: 15000 }, { file: 'cli-parser-demo.ts', mode: 'run' }, + { file: 'query.ts', mode: 'run' }, { file: 'http-server.ts', mode: 'compile-only' }, { file: 'word-count.ts', mode: 'compile-only' }, + { file: 'parallel.ts', mode: 'compile-only' }, + { file: 'hackernews/app.ts', mode: 'compile-only' }, + { file: 'websocket-server.ts', mode: 'compile-only' }, ]; describe('Examples Integration', { concurrency: 1 }, () => { @@ -31,8 +35,10 @@ describe('Examples Integration', { concurrency: 1 }, () => { const sourcePath = path.join(EXAMPLES_DIR, example.file); const ext = path.extname(example.file); const baseName = path.basename(example.file, ext); - const exeFile = path.join(BUILD_DIR, baseName); - const llFile = path.join(BUILD_DIR, `${baseName}.ll`); + const subdir = path.dirname(example.file); + const outputDir = subdir !== '.' ? path.join(BUILD_DIR, subdir) : BUILD_DIR; + const exeFile = path.join(outputDir, baseName); + const llFile = path.join(outputDir, `${baseName}.ll`); if (example.mode === 'run') { it(`${example.file}: compiles and runs without crash`, async () => { @@ -62,8 +68,8 @@ describe('Examples Integration', { concurrency: 1 }, () => { } const stdout = result.stdout || ''; - assert.ok(stdout.includes('TEST_PASSED'), - `expected TEST_PASSED in stdout, got: ${stdout.slice(0, 500)}`); + assert.ok(stdout.length > 0, + `expected non-empty stdout, got empty output`); assert.strictEqual(exitCode, 0, `expected exit code 0, got ${exitCode}`); } finally { try { diff --git a/tests/fixtures/builtins/embed-dir-data/index.html b/tests/fixtures/builtins/embed-dir-data/index.html new file mode 100644 index 00000000..d237837e --- /dev/null +++ b/tests/fixtures/builtins/embed-dir-data/index.html @@ -0,0 +1 @@ +

Embedded Index

\ No newline at end of file diff --git a/tests/fixtures/builtins/embed-dir-data/style.css b/tests/fixtures/builtins/embed-dir-data/style.css new file mode 100644 index 00000000..307d2465 --- /dev/null +++ b/tests/fixtures/builtins/embed-dir-data/style.css @@ -0,0 +1 @@ +body { color: red; } \ No newline at end of file diff --git a/tests/fixtures/builtins/embed-dir.ts b/tests/fixtures/builtins/embed-dir.ts new file mode 100644 index 00000000..12111ff6 --- /dev/null +++ b/tests/fixtures/builtins/embed-dir.ts @@ -0,0 +1,22 @@ +ChadScript.embedDir('./embed-dir-data'); + +const html = ChadScript.getEmbeddedFile('index.html'); +const css = ChadScript.getEmbeddedFile('style.css'); +const missing = ChadScript.getEmbeddedFile('nonexistent.txt'); + +if (html.indexOf('

Embedded Index

') === -1) { + console.log('FAIL: index.html content not found'); + process.exit(1); +} + +if (css.indexOf('color: red') === -1) { + console.log('FAIL: style.css content not found'); + process.exit(1); +} + +if (missing !== '') { + console.log('FAIL: nonexistent file should return empty string'); + process.exit(1); +} + +console.log('TEST_PASSED'); diff --git a/tests/fixtures/builtins/embed-file.ts b/tests/fixtures/builtins/embed-file.ts new file mode 100644 index 00000000..26a77010 --- /dev/null +++ b/tests/fixtures/builtins/embed-file.ts @@ -0,0 +1,19 @@ +const content = ChadScript.embedFile('./embed-test-data.txt'); + +const lines = content.split('\n'); +if (lines.length < 3) { + console.log('FAIL: expected at least 3 lines'); + process.exit(1); +} + +if (lines[0] !== 'Hello from embedded file!') { + console.log('FAIL: first line mismatch'); + process.exit(1); +} + +if (lines[2] !== 'Line 3 of the test data.') { + console.log('FAIL: third line mismatch'); + process.exit(1); +} + +console.log('TEST_PASSED'); diff --git a/tests/fixtures/builtins/embed-test-data.txt b/tests/fixtures/builtins/embed-test-data.txt new file mode 100644 index 00000000..ff0807d2 --- /dev/null +++ b/tests/fixtures/builtins/embed-test-data.txt @@ -0,0 +1,3 @@ +Hello from embedded file! +This content is baked into the binary at compile time. +Line 3 of the test data. \ No newline at end of file diff --git a/tests/fixtures/builtins/fs-stat.ts b/tests/fixtures/builtins/fs-stat.ts index fafe3025..c68cc580 100644 --- a/tests/fixtures/builtins/fs-stat.ts +++ b/tests/fixtures/builtins/fs-stat.ts @@ -4,21 +4,21 @@ function testStatSync(): void { console.log("FAIL: file size should be > 0"); process.exit(1); } - if (!stats.isFile) { + if (!stats.isFile()) { console.log("FAIL: should be a file"); process.exit(1); } - if (stats.isDirectory) { + if (stats.isDirectory()) { console.log("FAIL: should not be a directory"); process.exit(1); } const dirStats = fs.statSync("tests/fixtures/builtins"); - if (!dirStats.isDirectory) { + if (!dirStats.isDirectory()) { console.log("FAIL: should be a directory"); process.exit(1); } - if (dirStats.isFile) { + if (dirStats.isFile()) { console.log("FAIL: directory should not be a file"); process.exit(1); } diff --git a/tests/fixtures/builtins/sqlite-test.ts b/tests/fixtures/builtins/sqlite-test.ts index a8663b58..b07234bf 100644 --- a/tests/fixtures/builtins/sqlite-test.ts +++ b/tests/fixtures/builtins/sqlite-test.ts @@ -55,6 +55,42 @@ function testSqlite(): void { process.exit(1); } + const row = sqlite.get(db, "SELECT id, name FROM users WHERE id = 1"); + if (row !== "1|Alice") { + console.log("FAIL: multi-col get expected '1|Alice', got:"); + console.log(row); + process.exit(1); + } + + const rows = sqlite.all(db, "SELECT id, name FROM users ORDER BY id"); + if (rows.length !== 4) { + console.log("FAIL: multi-col all expected 4 rows, got:"); + console.log(rows.length); + process.exit(1); + } + if (rows[0] !== "1|Alice") { + console.log("FAIL: multi-col row 0 expected '1|Alice', got:"); + console.log(rows[0]); + process.exit(1); + } + if (rows[1] !== "2|Bob") { + console.log("FAIL: multi-col row 1 expected '2|Bob', got:"); + console.log(rows[1]); + process.exit(1); + } + + const parts = rows[0].split('|'); + if (parts[0] !== "1") { + console.log("FAIL: split id expected '1', got:"); + console.log(parts[0]); + process.exit(1); + } + if (parts[1] !== "Alice") { + console.log("FAIL: split name expected 'Alice', got:"); + console.log(parts[1]); + process.exit(1); + } + sqlite.close(db); console.log("TEST_PASSED"); } diff --git a/tests/test-fixtures.ts b/tests/test-fixtures.ts index 53d744b8..87b26ee6 100644 --- a/tests/test-fixtures.ts +++ b/tests/test-fixtures.ts @@ -842,4 +842,16 @@ export const testCases: TestCase[] = [ description: 'ArgumentParser --key=value option syntax', args: ['--target-cpu=x86-64', 'build', 'hello.ts'] }, + { + name: 'embed-file', + fixture: 'tests/fixtures/builtins/embed-file.ts', + expectTestPassed: true, + description: 'ChadScript.embedFile() embeds file content at compile time' + }, + { + name: 'embed-dir', + fixture: 'tests/fixtures/builtins/embed-dir.ts', + expectTestPassed: true, + description: 'ChadScript.embedDir() + getEmbeddedFile() for directory embedding' + }, ];