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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions c_bridges/lws-bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
12 changes: 11 additions & 1 deletion chadscript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

// ============================================================================
Expand Down Expand Up @@ -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;
}
14 changes: 11 additions & 3 deletions docs/stdlib/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions examples/hackernews/app.ts
Original file line number Diff line number Diff line change
@@ -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 + '<div class="post"><span class="rank">' + rank + '.</span>';
html = html + '<form method="POST" action="/upvote/' + id + '" style="display:inline">';
html = html + '<button type="submit" class="upvote" title="upvote"></button></form>';
html = html + '<span class="title"><a href="' + url + '">' + title + '</a></span></div>';
html = html + '<div class="meta">' + points + ' points</div>';
}
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 = '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=/"></head><body>Redirecting...</body></html>';
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);
26 changes: 26 additions & 0 deletions examples/hackernews/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hacker News</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="header">
<a href="/" class="logo">
<span class="logo-box">Y</span>
<b>Hacker News</b>
</a>
<span class="tagline">Served by a ChadScript native binary</span>
</div>
<div id="content">
<!-- posts injected server-side -->
{{POSTS}}
</div>
<div id="footer">
<p>Powered by ChadScript &mdash; compiled from TypeScript to a native ELF binary via LLVM.</p>
<p>HTML, CSS, and data are all embedded in a single binary. Zero runtime dependencies.</p>
</div>
</body>
</html>
124 changes: 124 additions & 0 deletions examples/hackernews/public/style.css
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions examples/parallel.ts
Original file line number Diff line number Diff line change
@@ -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();
9 changes: 9 additions & 0 deletions examples/query.ts
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 2 additions & 6 deletions src/codegen/expressions/access/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand Down
Loading
Loading