Reorganize code and clean up Makefile

This commit is contained in:
2025-08-09 08:47:47 +02:00
parent e200f52f40
commit 890a5c48e4
22 changed files with 331 additions and 179 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
*.o *.o
*.exe *.exe
*.out
+72 -11
View File
@@ -351,6 +351,7 @@ typedef enum {
NODE_INCLUDE, NODE_INCLUDE,
NODE_SELECT, NODE_SELECT,
NODE_NESTED, NODE_NESTED,
NODE_OPER_LEN,
NODE_OPER_POS, NODE_OPER_POS,
NODE_OPER_NEG, NODE_OPER_NEG,
NODE_OPER_ASS, NODE_OPER_ASS,
@@ -459,6 +460,7 @@ typedef enum {
TOKEN_KWORD_TRUE, TOKEN_KWORD_TRUE,
TOKEN_KWORD_FALSE, TOKEN_KWORD_FALSE,
TOKEN_KWORD_INCLUDE, TOKEN_KWORD_INCLUDE,
TOKEN_KWORD_LEN,
TOKEN_VALUE_FLOAT, TOKEN_VALUE_FLOAT,
TOKEN_VALUE_INT, TOKEN_VALUE_INT,
TOKEN_VALUE_STR, TOKEN_VALUE_STR,
@@ -550,6 +552,7 @@ String tok2str(Token token, char *buf, int max)
case TOKEN_KWORD_TRUE: return S("true"); case TOKEN_KWORD_TRUE: return S("true");
case TOKEN_KWORD_FALSE: return S("false"); case TOKEN_KWORD_FALSE: return S("false");
case TOKEN_KWORD_INCLUDE: return S("include"); case TOKEN_KWORD_INCLUDE: return S("include");
case TOKEN_KWORD_LEN: return S("len");
case TOKEN_VALUE_FLOAT: case TOKEN_VALUE_FLOAT:
{ {
@@ -676,18 +679,19 @@ Token next_token(Parser *p)
p->s.cur - start p->s.cur - start
}; };
if (streq(kword, S("if"))) return (Token) { .type=TOKEN_KWORD_IF }; if (streq(kword, S("if"))) return (Token) { .type=TOKEN_KWORD_IF };
if (streq(kword, S("else"))) return (Token) { .type=TOKEN_KWORD_ELSE }; if (streq(kword, S("else"))) return (Token) { .type=TOKEN_KWORD_ELSE };
if (streq(kword, S("while"))) return (Token) { .type=TOKEN_KWORD_WHILE }; if (streq(kword, S("while"))) return (Token) { .type=TOKEN_KWORD_WHILE };
if (streq(kword, S("for"))) return (Token) { .type=TOKEN_KWORD_FOR }; if (streq(kword, S("for"))) return (Token) { .type=TOKEN_KWORD_FOR };
if (streq(kword, S("in"))) return (Token) { .type=TOKEN_KWORD_IN }; if (streq(kword, S("in"))) return (Token) { .type=TOKEN_KWORD_IN };
if (streq(kword, S("fun"))) return (Token) { .type=TOKEN_KWORD_FUN }; if (streq(kword, S("fun"))) return (Token) { .type=TOKEN_KWORD_FUN };
if (streq(kword, S("let"))) return (Token) { .type=TOKEN_KWORD_LET }; if (streq(kword, S("let"))) return (Token) { .type=TOKEN_KWORD_LET };
if (streq(kword, S("print"))) return (Token) { .type=TOKEN_KWORD_PRINT }; if (streq(kword, S("print"))) return (Token) { .type=TOKEN_KWORD_PRINT };
if (streq(kword, S("none"))) return (Token) { .type=TOKEN_KWORD_NONE }; if (streq(kword, S("none"))) return (Token) { .type=TOKEN_KWORD_NONE };
if (streq(kword, S("true"))) return (Token) { .type=TOKEN_KWORD_TRUE }; if (streq(kword, S("true"))) return (Token) { .type=TOKEN_KWORD_TRUE };
if (streq(kword, S("false"))) return (Token) { .type=TOKEN_KWORD_FALSE }; if (streq(kword, S("false"))) return (Token) { .type=TOKEN_KWORD_FALSE };
if (streq(kword, S("include"))) return (Token) { .type=TOKEN_KWORD_INCLUDE }; if (streq(kword, S("include"))) return (Token) { .type=TOKEN_KWORD_INCLUDE };
if (streq(kword, S("len"))) return (Token) { .type=TOKEN_KWORD_LEN };
return (Token) { .type=TOKEN_IDENT, .sval=kword }; return (Token) { .type=TOKEN_IDENT, .sval=kword };
} }
@@ -1249,6 +1253,23 @@ Node *parse_atom(Parser *p)
} }
break; break;
case TOKEN_KWORD_LEN:
{
Node *child = parse_atom(p);
if (child == NULL)
return NULL;
Node *parent = alloc_node(p);
if (parent == NULL)
return NULL;
parent->type = NODE_OPER_LEN;
parent->left = child;
ret = parent;
}
break;
case TOKEN_IDENT: case TOKEN_IDENT:
{ {
Node *node = alloc_node(p); Node *node = alloc_node(p);
@@ -2046,6 +2067,12 @@ void print_node(Node *node)
} }
break; break;
case NODE_OPER_LEN:
printf("len(");
print_node(node->left);
printf(")");
break;
case NODE_OPER_POS: case NODE_OPER_POS:
printf("("); printf("(");
printf("+"); printf("+");
@@ -2420,6 +2447,7 @@ enum {
OPCODE_FOR = 0x2E, OPCODE_FOR = 0x2E,
OPCODE_PUSHT = 0x2F, OPCODE_PUSHT = 0x2F,
OPCODE_PUSHFL = 0x30, OPCODE_PUSHFL = 0x30,
OPCODE_LEN = 0x31,
}; };
typedef struct { typedef struct {
@@ -2757,6 +2785,7 @@ bool is_expr(Node *node)
case NODE_SELECT: case NODE_SELECT:
case NODE_NESTED: case NODE_NESTED:
case NODE_FUNC_CALL: case NODE_FUNC_CALL:
case NODE_OPER_LEN:
case NODE_OPER_POS: case NODE_OPER_POS:
case NODE_OPER_NEG: case NODE_OPER_NEG:
case NODE_OPER_ASS: case NODE_OPER_ASS:
@@ -3002,6 +3031,20 @@ void assemble_expr(Assembler *a, Node *node, int num_results)
} }
break; break;
case NODE_OPER_LEN:
assemble_expr(a, node->left, 1);
append_u8(&a->out, OPCODE_LEN);
if (num_results == 0)
append_u8(&a->out, OPCODE_POP);
else if (num_results != -1 && num_results != 1) {
append_u8(&a->out, OPCODE_GROUP);
append_u8(&a->out, OPCODE_GTRUNC);
append_u32(&a->out, num_results-1);
append_u8(&a->out, OPCODE_GCOALESCE);
}
break;
case NODE_OPER_POS: case NODE_OPER_POS:
assemble_expr(a, node->left, num_results); assemble_expr(a, node->left, num_results);
break; break;
@@ -5574,6 +5617,24 @@ int step(WL_State *state)
} }
break; break;
case OPCODE_LEN:
{
Value set = state->eval_stack[state->eval_depth-1];
Type type = type_of(set);
if (type != TYPE_ARRAY && type != TYPE_MAP) {
assert(0); // TODO
}
Value len = make_int(state->a, value_length(set));
if (len == VALUE_ERROR) {
assert(0); // TODO
}
state->eval_stack[state->eval_depth++] = len;
}
break;
default: default:
eval_report(state, "Invalid opcode (offset %d)", state->off-1); eval_report(state, "Invalid opcode (offset %d)", state->off-1);
return -1; return -1;
View File
View File
View File
View File
View File
View File
+21 -4
View File
@@ -1,6 +1,23 @@
ifeq ($(OS),Windows_NT)
EXT = .exe
LFLAGS = -lws2_32
else
EXT = .out
LFLAGS =
endif
all: sqlite3.o CFLAGS = -Wall -Wextra -O0 -g3 -I3p
gcc main.c variadic.c sqlite3utils.c chttp.c WL.c sqlite3.o -o main -Wall -Wextra -g3 -O0 -lws2_32
sqlite3.o: HFILES = $(shell find src 3p -name "*.h")
gcc -c sqlite3.c -o sqlite3.o CFILES = $(filter-out 3p/sqlite3.c, $(shell find src 3p -name "*.c"))
all: cozisnews$(EXT)
cozisnews$(EXT): $(CFILES) $(HFILES) sqlite3.o
gcc -o $@ $(CFILES) sqlite3.o $(CFLAGS) $(LFLAGS)
sqlite3.o: 3p/sqlite3.c
gcc -o $@ -c $<
clean:
rm *.o *.out *.exe
-114
View File
@@ -1,114 +0,0 @@
#ifndef WL_AMALGAMATION
#include "includes.h"
#include "file.h"
#endif
int file_open(String path, File *handle, int *size)
{
char zt[1<<10];
if (path.len >= COUNT(zt))
return -1;
memcpy(zt, path.ptr, path.len);
zt[path.len] = '\0';
#ifdef _WIN32
*handle = CreateFileA(
zt,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (*handle == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND ||
error == ERROR_ACCESS_DENIED)
return 1;
return -1;
}
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(*handle, &fileSize)) {
CloseHandle(*handle);
return -1;
}
if (fileSize.QuadPart > INT_MAX) {
CloseHandle(*handle);
return -1;
}
*size = (int) fileSize.QuadPart;
#else
*handle = open(zt, O_RDONLY);
if (*handle < 0) {
if (errno == ENOENT)
return 1;
return -1;
}
struct stat info;
if (fstat(*handle, &info) < 0) {
close(*handle);
return -1;
}
if (S_ISDIR(info.st_mode)) {
close(*handle);
return 1;
}
if (info.st_size > INT_MAX) {
close(*handle);
return -1;
}
*size = (int) info.st_size;
#endif
return 0;
}
void file_close(File file)
{
#ifdef _WIN32
CloseHandle(file);
#else
close(file);
#endif
}
int file_read(File file, char *dst, int max)
{
#ifdef _WIN32
DWORD num;
BOOL ok = ReadFile(file, dst, max, &num, NULL);
if (!ok)
return -1;
return (int) num;
#else
return read(file, dst, max);
#endif
}
int file_read_all(String path, String *dst)
{
int len;
File handle;
if (file_open(path, &handle, &len) < 0)
return -1;
char *ptr = malloc(len+1);
if (ptr == NULL) {
file_close(handle);
return -1;
}
for (int copied = 0; copied < len; ) {
int ret = file_read(handle, ptr + copied, len - copied);
if (ret <= 0) {
free(ptr);
file_close(handle);
return -1;
}
copied += ret;
}
*dst = (String) { ptr, len };
file_close(handle);
return 0;
}
-20
View File
@@ -1,20 +0,0 @@
#ifndef WL_FILE_INCLUDED
#define WL_FILE_INCLUDED
#ifndef WL_AMALGAMATION
#include "includes.h"
#include "basic.h"
#endif
#ifdef _WIN32
typedef HANDLE File;
#else
typedef int File;
#endif
int file_open(String path, File *handle, int *size);
void file_close(File file);
int file_read(File file, char *dst, int max);
int file_read_all(String path, String *dst);
#endif // WL_FILE_INCLUDED
BIN
View File
Binary file not shown.
+1
View File
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS Users (
CREATE TABLE IF NOT EXISTS Posts ( CREATE TABLE IF NOT EXISTS Posts (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
author INTEGER NOT NULL, author INTEGER NOT NULL,
title TEXT NOT NULL,
is_link BOOLEAN NOT NULL, is_link BOOLEAN NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP,
View File
+20 -9
View File
@@ -1,10 +1,9 @@
include "pages/page.wl" include "pages/page.wl"
let posts = [ let posts = $query("SELECT id, title, is_link, content, 0 as num_comments, CURRENT_TIMESTAMP as date FROM Posts")
{ title: "Show HN: Kitten TTS - 25MB CPU-Only, Open-Source TTS Model", date: "3 hours ago", num_comments: 127, link: "https://github.com/KittenML/KittenTTS" },
{ title: "Open models by OpenAI", date: "15 hours ago", num_comments: 651, link: "https://openai.com/open-models/" }, if posts == none:
{ title: "Anthropic rejects the main developer of the library they use", date: "40 minutes ago", num_comments: 437, link: "https://grell.dev/blog/ai_rejection" } posts = []
]
let style = let style =
<style> <style>
@@ -35,15 +34,27 @@ let style =
let main = let main =
<main> <main>
\for post in posts:
\if len(posts) == 0:
<div>There are no posts yet!</div>
\for post in posts: {
let link
if post.is_link != 0:
link = post.content
else
link = ["/post?id=", post.id]
<div class="item"> <div class="item">
<div> <div>
<a href=post.link>\post.title</a> <a href=link>\post.title</a>
</div> </div>
<div> <div>
<span>\post.date</span> | <span><a href="/thread">\post.num_comments comments</a></span> <span>\post.date</span> | <span><a href=link>\post.num_comments comments</a></span>
</div> </div>
</div> </div>
}
</main> </main>
page("Index", $login_user_id, style, main) page("Index", $login_user_id, style, main)
+16
View File
@@ -0,0 +1,16 @@
include "pages/page.wl"
let posts = $query("SELECT title, content FROM Posts WHERE id=?", $post_id)
let post = posts[0]
let style =
<style>
</style>
let main =
<main>
<h3>\post.title</h3>
<p>\post.content</p>
</main>
page(post.title, $login_user_id, style, main)
+82 -5
View File
@@ -2,15 +2,92 @@ include "pages/page.wl"
let style = let style =
<style> <style>
form {
max-width: 400px;
margin: 30px auto;
}
form input,
form textarea {
border: 0;
outline: 0;
width: 100%;
border-radius: 3px;
padding: 8px;
margin-bottom: 15px;
background: #E8D4A9;
border: 1px solid #D4C298;
}
form input:focus,
form textarea:focus {
border-color: #5780C9;
background: #fff;
}
form textarea {
height: 120px;
resize: vertical;
}
form input[type=submit] {
cursor: pointer;
background: #5780C9;
color: #F7E6C0;
}
form input[type=submit]:hover {
background: #1D2B42;
}
.checkbox-row {
margin-bottom: 15px;
}
.checkbox-row input[type="checkbox"] {
width: auto;
margin-right: 8px;
}
.checkbox-row label {
color: #1D2B42;
font-size: 14px;
cursor: pointer;
}
</style> </style>
let main = let main =
<main> <main>
<form action="api/post" method="POST"> <form action="/api/post" method="POST">
<input type="text" name="title" /> <input type="text" id="title" name="title" placeholder="Title" required />
<textarea name="content"></textarea>
<input type="submit" value="Publish" /> <div class="checkbox-row">
<input type="checkbox" id="is_link" name="is_link" onchange="togglePostType()" />
<label>This is a link post</label>
</div>
<input type="url" id="url" name="link" placeholder="URL" style="display: none;" />
<textarea id="content" name="content" placeholder="Write your post here..."></textarea>
<input type="submit" value="Submit Post" />
</form> </form>
<script>
function togglePostType() {
const checkbox = document.getElementById('is_link');
const urlInput = document.getElementById('url');
const contentTextarea = document.getElementById('content');
if (checkbox.checked) {
urlInput.style.display = 'block';
contentTextarea.style.display = 'none';
} else {
urlInput.style.display = 'none';
contentTextarea.style.display = 'block';
}
}
</script>
</main> </main>
page("Write Post", $login_user_id, style, main) page("Write Post", $login_user_id, style, main)
+116 -15
View File
@@ -161,7 +161,7 @@ static int query_routine(WL_State *state)
int evaluate_template(HTTP_ResponseBuilder builder, int evaluate_template(HTTP_ResponseBuilder builder,
WL_Program program, WL_Arena arena, WL_Program program, WL_Arena arena,
char *err, int errmax, int user_id) char *err, int errmax, int user_id, int post_id)
{ {
//WL_dump_program(program); //WL_dump_program(program);
@@ -186,10 +186,18 @@ int evaluate_template(HTTP_ResponseBuilder builder,
case WL_VAR: case WL_VAR:
if (WL_streq(result.str, "login_user_id", -1)) { if (WL_streq(result.str, "login_user_id", -1)) {
if (user_id < 0) if (user_id < 0)
WL_pushnone(state); WL_pushnone(state);
else else
WL_pushint(state, user_id); WL_pushint(state, user_id);
} else if (WL_streq(result.str, "post_id", -1)) {
if (post_id < 0)
WL_pushnone(state);
else
WL_pushint(state, post_id);
} }
break; break;
@@ -209,7 +217,7 @@ int evaluate_template(HTTP_ResponseBuilder builder,
return 0; return 0;
} }
void evaluate_template_2(HTTP_ResponseBuilder builder, WL_Arena arena, char *file, int user_id) void evaluate_template_2(HTTP_ResponseBuilder builder, WL_Arena arena, char *file, int user_id, int post_id)
{ {
http_response_builder_status(builder, 200); http_response_builder_status(builder, 200);
http_response_builder_header(builder, HTTP_STR("Content-Type: text/html")); http_response_builder_header(builder, HTTP_STR("Content-Type: text/html"));
@@ -284,7 +292,7 @@ void evaluate_template_2(HTTP_ResponseBuilder builder, WL_Arena arena, char *fil
WL_Program program = result.program; WL_Program program = result.program;
char err[1<<9]; char err[1<<9];
if (evaluate_template(builder, program, arena, err, (int) sizeof(err), user_id) < 0) { if (evaluate_template(builder, program, arena, err, (int) sizeof(err), user_id, post_id) < 0) {
http_response_builder_undo(builder); http_response_builder_undo(builder);
http_response_builder_status(builder, 500); http_response_builder_status(builder, 500);
http_response_builder_body(builder, (HTTP_String) { err, (int) strlen(err) }); http_response_builder_body(builder, (HTTP_String) { err, (int) strlen(err) });
@@ -531,6 +539,12 @@ bool valid_post_content(HTTP_String str)
return true; return true;
} }
bool valid_link(HTTP_String str)
{
(void) str;
return true;
}
int main(void) int main(void)
{ {
http_global_init(); http_global_init();
@@ -546,7 +560,7 @@ int main(void)
char *schema_data; char *schema_data;
long schema_size; long schema_size;
ret = load_file("schema.sql", &schema_data, &schema_size); ret = load_file("misc/schema.sql", &schema_data, &schema_size);
if (ret < 0) { if (ret < 0) {
printf("Couldn't load schema\n"); printf("Couldn't load schema\n");
return -1; return -1;
@@ -738,28 +752,75 @@ int main(void)
} else if (http_streq(path, HTTP_STR("/api/post"))) { } else if (http_streq(path, HTTP_STR("/api/post"))) {
if (req->method != HTTP_METHOD_POST) { if (req->method != HTTP_METHOD_POST) {
assert(0); // TODO http_response_builder_status(builder, 405);
http_response_builder_done(builder);
continue;
} }
if (user_id == -1) { if (user_id == -1) {
assert(0); // TODO http_response_builder_status(builder, 303);
http_response_builder_header(builder, HTTP_STR("Location: /index"));
http_response_builder_done(builder);
continue;
} }
HTTP_String title = http_getparam(req->body, HTTP_STR("title")); HTTP_String title = http_getparam(req->body, HTTP_STR("title"));
HTTP_String link = http_getparam(req->body, HTTP_STR("link"));
HTTP_String content = http_getparam(req->body, HTTP_STR("content")); HTTP_String content = http_getparam(req->body, HTTP_STR("content"));
title = http_trim(title); title = http_trim(title);
link = http_trim(link);
content = http_trim(content); content = http_trim(content);
if (!valid_post_title(title) || !valid_post_content(content)) { if (!valid_post_title(title) || !valid_link(link) || !valid_post_content(content)) {
assert(0); // TODO assert(0); // TODO
} }
// TODO if (content.len == 0 && link.len == 0) {
assert(0); // TODO
}
bool is_link = false;
if (link.len > 0) {
is_link = true;
content = link;
}
sqlite3_stmt *stmt;
int ret = sqlite3utils_prepare(db, &stmt, "INSERT INTO Posts(author, title, is_link, content) VALUES (?, ?, ?, ?)", user_id, title, is_link, content);
if (ret != SQLITE_OK) {
assert(0); // TODO
}
ret = sqlite3_step(stmt);
if (ret != SQLITE_DONE) {
assert(0); // TODO
}
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK) {
assert(0); // TODO
}
int64_t tmp = sqlite3_last_insert_rowid(db);
if (tmp < 0 || tmp > INT_MAX) {
assert(0); // TODO
}
int post_id = (int) tmp;
char location[1<<9];
ret = snprintf(location, sizeof(location), "Location: /post?id=%d", post_id);
if (ret < 0 || ret >= (int) sizeof(location)) {
assert(0); // TODO
}
http_response_builder_status(builder, 303);
http_response_builder_header(builder, (HTTP_String) { location, ret });
http_response_builder_done(builder);
} else if (http_streq(path, HTTP_STR("/index"))) { } else if (http_streq(path, HTTP_STR("/index"))) {
evaluate_template_2(builder, arena, "pages/index.wl", user_id); evaluate_template_2(builder, arena, "pages/index.wl", user_id, -1);
} else if (http_streq(path, HTTP_STR("/write"))) { } else if (http_streq(path, HTTP_STR("/write"))) {
@@ -771,7 +832,7 @@ int main(void)
continue; continue;
} }
evaluate_template_2(builder, arena, "pages/write.wl", user_id); evaluate_template_2(builder, arena, "pages/write.wl", user_id, -1);
} else if (http_streq(path, HTTP_STR("/login"))) { } else if (http_streq(path, HTTP_STR("/login"))) {
@@ -783,7 +844,7 @@ int main(void)
continue; continue;
} }
evaluate_template_2(builder, arena, "pages/login.wl", user_id); evaluate_template_2(builder, arena, "pages/login.wl", user_id, -1);
} else if (http_streq(path, HTTP_STR("/signup"))) { } else if (http_streq(path, HTTP_STR("/signup"))) {
@@ -795,15 +856,55 @@ int main(void)
continue; continue;
} }
evaluate_template_2(builder, arena, "pages/signup.wl", user_id); evaluate_template_2(builder, arena, "pages/signup.wl", user_id, -1);
} else if (http_streq(path, HTTP_STR("/thread"))) { } else if (http_streq(path, HTTP_STR("/post"))) {
evaluate_template_2(builder, arena, "pages/thread.wl", user_id); HTTP_String idstr = http_getparam(req->url.query, HTTP_STR("id"));
char buf[32];
if (idstr.len == 0 || idstr.len >= (int) sizeof(buf)) {
evaluate_template_2(builder, arena, "pages/notfound.wl", user_id, -1);
continue;
}
memcpy(buf, idstr.ptr, idstr.len);
buf[idstr.len] = '\0';
int post_id = atoi(buf);
if (post_id == 0) {
evaluate_template_2(builder, arena, "pages/notfound.wl", user_id, -1);
continue;
}
sqlite3_stmt *stmt;
int ret = sqlite3utils_prepare(db, &stmt, "SELECT COUNT(*) FROM Posts WHERE id=?", post_id);
if (ret != SQLITE_OK) {
assert(0); // TODO
}
ret = sqlite3_step(stmt);
if (ret != SQLITE_ROW) {
assert(0); // TODO
}
int64_t num = sqlite3_column_int64(stmt, 0);
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK) {
assert(0); // TODO
}
if (num < 0) {
assert(0); // TODO
}
if (num == 0)
evaluate_template_2(builder, arena, "pages/notfound.wl", user_id, -1);
else
evaluate_template_2(builder, arena, "pages/post.wl", user_id, post_id);
} else { } else {
evaluate_template_2(builder, arena, "pages/notfound.wl", user_id); evaluate_template_2(builder, arena, "pages/notfound.wl", user_id, -1);
} }
} }
View File
View File