feat(nexus): production modular HTTP/1.1 server + compiler fixes
- Rewrite apps/nexus with modular architecture: Config, Http, Errors, Parser, Router, Handlers, Server, Main - Use algebraic enums for ParseResult/FileResult/HttpError - Thread-pool server via Channel<ConnectionTask> and spawn - Fix C backend type ordering for generic struct instances (Array_T, Iter_T) and algebraic enum struct payloads - Collect Slice_T types from struct fields and enum payloads - Fix match lowering for simple enums (direct value compare) - Resolve match expression return type from first arm - Infer element type for for-in over Array<UserStruct> - Preserve generic type args in field access resolution - Add fflush to PrintLine/Print for immediate server logs - Add modern_features golden regression test - Regenerate golden expected.c files
This commit is contained in:
@@ -11,6 +11,7 @@ tests/lexer_test
|
|||||||
tests/parser_test
|
tests/parser_test
|
||||||
tests/sema_test
|
tests/sema_test
|
||||||
tests/hir_test
|
tests/hir_test
|
||||||
|
tests/borrow_test
|
||||||
tests/test_generics_parse
|
tests/test_generics_parse
|
||||||
tests/*.exe
|
tests/*.exe
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ selfhost: build
|
|||||||
|
|
||||||
.PHONY: test-golden
|
.PHONY: test-golden
|
||||||
|
|
||||||
GOLDEN_TESTS := hello fibonacci structs generics algebraic_enums enums methods strings
|
GOLDEN_TESTS := hello fibonacci structs generics algebraic_enums enums methods strings modern_features
|
||||||
|
|
||||||
test-golden: build
|
test-golden: build
|
||||||
@echo "=== Golden tests ==="
|
@echo "=== Golden tests ==="
|
||||||
|
|||||||
@@ -1,129 +1 @@
|
|||||||
<!DOCTYPE html>
|
<html><body>Hello Nexus</body></html>
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Nexus — Bux HTTP Server</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: #0d1117;
|
|
||||||
color: #c9d1d9;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: #161b22;
|
|
||||||
border: 1px solid #30363d;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 3rem;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: #58a6ff;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
color: #8b949e;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.features {
|
|
||||||
text-align: left;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
.features h3 {
|
|
||||||
color: #58a6ff;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
.features ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.features li {
|
|
||||||
padding: 0.4rem 0;
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
.features li::before {
|
|
||||||
content: "▸ ";
|
|
||||||
color: #3fb950;
|
|
||||||
}
|
|
||||||
.endpoints {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
border-top: 1px solid #30363d;
|
|
||||||
}
|
|
||||||
.endpoints h3 {
|
|
||||||
color: #d2a8ff;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
.endpoint {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.3rem 0;
|
|
||||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.endpoint .method {
|
|
||||||
color: #3fb950;
|
|
||||||
font-weight: bold;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
.endpoint .path {
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
border-top: 1px solid #30363d;
|
|
||||||
color: #484f58;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>⚡ Nexus</h1>
|
|
||||||
<p class="subtitle">High-performance HTTP Server — Built with Bux</p>
|
|
||||||
|
|
||||||
<div class="features">
|
|
||||||
<h3>Features</h3>
|
|
||||||
<ul>
|
|
||||||
<li>HTTP/1.1 — Full request parsing & response building</li>
|
|
||||||
<li>Multi-threaded — Configurable worker pool</li>
|
|
||||||
<li>HTTP/2 Detection — Upgrade-aware</li>
|
|
||||||
<li>WebSocket Upgrade — Handshake support</li>
|
|
||||||
<li>Static File Serving — With MIME type detection</li>
|
|
||||||
<li>JSON API Endpoints</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoints">
|
|
||||||
<h3>API Endpoints</h3>
|
|
||||||
<div class="endpoint">
|
|
||||||
<span class="method">GET</span>
|
|
||||||
<span class="path">/api/health</span>
|
|
||||||
</div>
|
|
||||||
<div class="endpoint">
|
|
||||||
<span class="method">GET</span>
|
|
||||||
<span class="path">/api/info</span>
|
|
||||||
</div>
|
|
||||||
<div class="endpoint">
|
|
||||||
<span class="method">GET</span>
|
|
||||||
<span class="path">/ws</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
Nexus v0.1.0 · Bux Programming Language
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module Config {
|
||||||
|
|
||||||
|
pub struct ServerConfig {
|
||||||
|
bindAddr: String;
|
||||||
|
port: int;
|
||||||
|
workerCount: int;
|
||||||
|
publicDir: String;
|
||||||
|
backlog: int;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const func DefaultConfig() -> ServerConfig {
|
||||||
|
return ServerConfig {
|
||||||
|
bindAddr: "0.0.0.0",
|
||||||
|
port: 8080,
|
||||||
|
workerCount: 4,
|
||||||
|
publicDir: "public",
|
||||||
|
backlog: 128,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
module Errors {
|
||||||
|
|
||||||
|
import Http::{HttpRequest};
|
||||||
|
|
||||||
|
pub enum HttpError {
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
MethodNotAllowed,
|
||||||
|
InternalError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ParseResult {
|
||||||
|
Ok(HttpRequest),
|
||||||
|
Err(HttpError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FileResult {
|
||||||
|
Ok(String),
|
||||||
|
Err(HttpError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ParseResult_NewOk(req: HttpRequest) -> ParseResult {
|
||||||
|
let r: ParseResult = ParseResult { tag: ParseResult_Ok };
|
||||||
|
r.data.Ok_0 = req;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ParseResult_NewErr(err: HttpError) -> ParseResult {
|
||||||
|
let r: ParseResult = ParseResult { tag: ParseResult_Err };
|
||||||
|
r.data.Err_0 = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_NewOk(content: String) -> FileResult {
|
||||||
|
let r: FileResult = FileResult { tag: FileResult_Ok };
|
||||||
|
r.data.Ok_0 = content;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_NewErr(err: HttpError) -> FileResult {
|
||||||
|
let r: FileResult = FileResult { tag: FileResult_Err };
|
||||||
|
r.data.Err_0 = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_IsOk(r: FileResult) -> bool {
|
||||||
|
return r.tag == FileResult_Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_Unwrap(r: FileResult) -> String {
|
||||||
|
return r.data.Ok_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_UnwrapErr(r: FileResult) -> HttpError {
|
||||||
|
return r.data.Err_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HttpError_ToString(err: HttpError) -> String {
|
||||||
|
if err.tag == HttpError_BadRequest { return "Bad Request"; }
|
||||||
|
if err.tag == HttpError_NotFound { return "Not Found"; }
|
||||||
|
if err.tag == HttpError_MethodNotAllowed { return "Method Not Allowed"; }
|
||||||
|
return err.data.InternalError_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
module Handlers {
|
||||||
|
|
||||||
|
import Std::String::{String_Eq, String_Contains};
|
||||||
|
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse, Http_MimeType, RequestHeader_Get};
|
||||||
|
import Errors::{HttpError, FileResult, FileResult_NewOk, FileResult_NewErr, FileResult_IsOk, FileResult_Unwrap, FileResult_UnwrapErr, HttpError_ToString};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_file_exists(path: String) -> int;
|
||||||
|
extern func bux_read_file(path: String) -> String;
|
||||||
|
extern func bux_path_join(a: String, b: String) -> String;
|
||||||
|
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||||
|
extern func bux_sb_append(sb: *void, s: String);
|
||||||
|
extern func bux_sb_append_int(sb: *void, n: int64);
|
||||||
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
|
extern func bux_sb_free(sb: *void);
|
||||||
|
|
||||||
|
pub func NotFoundResponse() -> HttpResponse {
|
||||||
|
return Http_NewResponse(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func MethodNotAllowedResponse() -> HttpResponse {
|
||||||
|
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ReadStaticFile(requestPath: String) -> FileResult {
|
||||||
|
if String_Contains(requestPath, "..") {
|
||||||
|
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath: String = requestPath;
|
||||||
|
if String_Eq(filePath, "/") {
|
||||||
|
filePath = "/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullPath: String = bux_path_join("public", filePath);
|
||||||
|
if bux_file_exists(fullPath) == 0 {
|
||||||
|
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: String = bux_read_file(fullPath);
|
||||||
|
return FileResult_NewOk(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileErrorResponse(err: HttpError) -> HttpResponse {
|
||||||
|
match err {
|
||||||
|
HttpError::NotFound => NotFoundResponse(),
|
||||||
|
_ => Http_NewResponse(500, "text/plain; charset=utf-8", HttpError_ToString(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ServeStaticFile(req: HttpRequest) -> HttpResponse {
|
||||||
|
if req.method != HttpMethod_GET && req.method != HttpMethod_HEAD {
|
||||||
|
return MethodNotAllowedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: FileResult = ReadStaticFile(req.path);
|
||||||
|
if FileResult_IsOk(result) {
|
||||||
|
let content: String = FileResult_Unwrap(result);
|
||||||
|
return Http_NewResponse(200, Http_MimeType(req.path), content);
|
||||||
|
}
|
||||||
|
return FileErrorResponse(FileResult_UnwrapErr(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleApiHealth() -> HttpResponse {
|
||||||
|
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||||
|
"{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.2.0\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleApiInfo() -> HttpResponse {
|
||||||
|
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||||
|
"{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"thread-pool\",\"algebraic-enums\"]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
||||||
|
let wsKey: String = RequestHeader_Get(req, "Sec-WebSocket-Key");
|
||||||
|
|
||||||
|
var resp: HttpResponse;
|
||||||
|
resp.statusCode = 101;
|
||||||
|
resp.contentType = "";
|
||||||
|
resp.body = "";
|
||||||
|
|
||||||
|
let sb: *void = bux_sb_new(256);
|
||||||
|
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
||||||
|
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
||||||
|
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
||||||
|
bux_sb_append(sb, wsKey);
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
resp.extraHeaders = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
module Http {
|
||||||
|
|
||||||
|
import Std::Array::{Array};
|
||||||
|
import Std::String::{String_Eq, String_EndsWith, String_Contains};
|
||||||
|
|
||||||
|
pub enum HttpMethod {
|
||||||
|
GET,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
PATCH,
|
||||||
|
HEAD,
|
||||||
|
OPTIONS,
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeaderEntry {
|
||||||
|
key: String;
|
||||||
|
value: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpRequest {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: String;
|
||||||
|
version: String;
|
||||||
|
body: String;
|
||||||
|
headers: Array<HeaderEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpResponse {
|
||||||
|
statusCode: int;
|
||||||
|
contentType: String;
|
||||||
|
body: String;
|
||||||
|
extraHeaders: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_StatusText(code: int) -> String {
|
||||||
|
if code == 200 { return "OK"; }
|
||||||
|
if code == 201 { return "Created"; }
|
||||||
|
if code == 204 { return "No Content"; }
|
||||||
|
if code == 301 { return "Moved Permanently"; }
|
||||||
|
if code == 302 { return "Found"; }
|
||||||
|
if code == 304 { return "Not Modified"; }
|
||||||
|
if code == 400 { return "Bad Request"; }
|
||||||
|
if code == 401 { return "Unauthorized"; }
|
||||||
|
if code == 403 { return "Forbidden"; }
|
||||||
|
if code == 404 { return "Not Found"; }
|
||||||
|
if code == 405 { return "Method Not Allowed"; }
|
||||||
|
if code == 413 { return "Payload Too Large"; }
|
||||||
|
if code == 414 { return "URI Too Long"; }
|
||||||
|
if code == 500 { return "Internal Server Error"; }
|
||||||
|
if code == 501 { return "Not Implemented"; }
|
||||||
|
if code == 503 { return "Service Unavailable"; }
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_MimeType(path: String) -> String {
|
||||||
|
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".png") { return "image/png"; }
|
||||||
|
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
||||||
|
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
||||||
|
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
||||||
|
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
||||||
|
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
||||||
|
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
||||||
|
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
||||||
|
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_MethodName(m: HttpMethod) -> String {
|
||||||
|
match m {
|
||||||
|
HttpMethod::GET => "GET",
|
||||||
|
HttpMethod::POST => "POST",
|
||||||
|
HttpMethod::PUT => "PUT",
|
||||||
|
HttpMethod::DELETE => "DELETE",
|
||||||
|
HttpMethod::PATCH => "PATCH",
|
||||||
|
HttpMethod::HEAD => "HEAD",
|
||||||
|
HttpMethod::OPTIONS => "OPTIONS",
|
||||||
|
HttpMethod::UNKNOWN => "UNKNOWN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
||||||
|
var resp: HttpResponse;
|
||||||
|
resp.statusCode = code;
|
||||||
|
resp.contentType = contentType;
|
||||||
|
resp.body = body;
|
||||||
|
resp.extraHeaders = "";
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func RequestHeader_Get(req: HttpRequest, key: String) -> String {
|
||||||
|
for entry in req.headers {
|
||||||
|
if String_Eq(entry.key, key) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+39
-540
@@ -1,550 +1,49 @@
|
|||||||
// =============================================================================
|
|
||||||
// Nexus — High-performance multi-threaded HTTP/1.1, HTTP/2 & WebSocket server
|
|
||||||
// Built with the Bux programming language
|
|
||||||
// =============================================================================
|
|
||||||
module Main {
|
module Main {
|
||||||
|
|
||||||
// =============================================================================
|
import Config::{ServerConfig, DefaultConfig};
|
||||||
// Imports — only what we actually use
|
import Http::{HttpMethod};
|
||||||
// =============================================================================
|
import Router::{Handler, Route, Router};
|
||||||
import Std::Io::{PrintLine, Print, PrintInt};
|
import Server::{RunServer};
|
||||||
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
|
import Std::Array::{Array, Array_New, Array_Push};
|
||||||
import Std::String::{String_Len, String_Eq, String_StartsWith, String_EndsWith, String_Contains, String_Trim, String_FromInt, String_Offset};
|
|
||||||
import Std::Task::{Task_Join, TaskHandle};
|
|
||||||
|
|
||||||
// =============================================================================
|
func BuildRouter() -> Router {
|
||||||
// Extern Functions (from C runtime — used directly for performance)
|
var routes: Array<Route> = Array_New<Route>(8);
|
||||||
// =============================================================================
|
|
||||||
extern func bux_alloc(size: uint) -> *void;
|
|
||||||
extern func bux_strlen(s: String) -> uint;
|
|
||||||
extern func bux_strcmp(a: String, b: String) -> int;
|
|
||||||
extern func bux_strstr(haystack: String, needle: String) -> String;
|
|
||||||
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
|
||||||
extern func bux_str_split_count(s: String, delim: String) -> uint;
|
|
||||||
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
|
|
||||||
extern func bux_sb_new(initial_cap: uint) -> *void;
|
|
||||||
extern func bux_sb_append(sb: *void, s: String);
|
|
||||||
extern func bux_sb_append_int(sb: *void, n: int64);
|
|
||||||
extern func bux_sb_append_char(sb: *void, c: char8);
|
|
||||||
extern func bux_sb_build(sb: *void) -> String;
|
|
||||||
extern func bux_sb_free(sb: *void);
|
|
||||||
extern func bux_path_join(a: String, b: String) -> String;
|
|
||||||
extern func bux_read_file(path: String) -> String;
|
|
||||||
extern func bux_file_exists(path: String) -> int;
|
|
||||||
|
|
||||||
// =============================================================================
|
Array_Push<Route>(&routes, Route {
|
||||||
// HTTP Method Enum (simple enum — no data payload)
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
// =============================================================================
|
path: "/api/health",
|
||||||
enum HttpMethod {
|
handler: Handler { tag: Handler_ApiHealth },
|
||||||
GET,
|
});
|
||||||
POST,
|
|
||||||
PUT,
|
Array_Push<Route>(&routes, Route {
|
||||||
DELETE,
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
PATCH,
|
path: "/api/info",
|
||||||
HEAD,
|
handler: Handler { tag: Handler_ApiInfo },
|
||||||
OPTIONS,
|
});
|
||||||
UNKNOWN,
|
|
||||||
|
Array_Push<Route>(&routes, Route {
|
||||||
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
|
path: "/ws",
|
||||||
|
handler: Handler { tag: Handler_WsUpgrade },
|
||||||
|
});
|
||||||
|
|
||||||
|
Array_Push<Route>(&routes, Route {
|
||||||
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
|
path: "/",
|
||||||
|
handler: Handler { tag: Handler_StaticFile },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Router {
|
||||||
|
routes: routes,
|
||||||
|
notFound: Handler { tag: Handler_NotFound },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Struct: HTTP Request
|
|
||||||
// =============================================================================
|
|
||||||
struct HttpRequest {
|
|
||||||
method: HttpMethod,
|
|
||||||
path: String,
|
|
||||||
body: String,
|
|
||||||
headerKeys: *String,
|
|
||||||
headerValues: *String,
|
|
||||||
headerCount: int,
|
|
||||||
headerCap: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Struct: HTTP Response
|
|
||||||
// =============================================================================
|
|
||||||
struct HttpResponse {
|
|
||||||
statusCode: int,
|
|
||||||
statusText: String,
|
|
||||||
contentType: String,
|
|
||||||
body: String,
|
|
||||||
extraHeaders: String, // pre-formatted extra headers (for WS upgrade, etc.)
|
|
||||||
connectionClose: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Utility: Status text lookup
|
|
||||||
// =============================================================================
|
|
||||||
func Http_StatusText(code: int) -> String {
|
|
||||||
if code == 200 { return "OK"; }
|
|
||||||
if code == 201 { return "Created"; }
|
|
||||||
if code == 204 { return "No Content"; }
|
|
||||||
if code == 301 { return "Moved Permanently"; }
|
|
||||||
if code == 302 { return "Found"; }
|
|
||||||
if code == 304 { return "Not Modified"; }
|
|
||||||
if code == 400 { return "Bad Request"; }
|
|
||||||
if code == 401 { return "Unauthorized"; }
|
|
||||||
if code == 403 { return "Forbidden"; }
|
|
||||||
if code == 404 { return "Not Found"; }
|
|
||||||
if code == 405 { return "Method Not Allowed"; }
|
|
||||||
if code == 413 { return "Payload Too Large"; }
|
|
||||||
if code == 414 { return "URI Too Long"; }
|
|
||||||
if code == 500 { return "Internal Server Error"; }
|
|
||||||
if code == 501 { return "Not Implemented"; }
|
|
||||||
if code == 503 { return "Service Unavailable"; }
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Utility: Parse HTTP method string → HttpMethod enum
|
|
||||||
// =============================================================================
|
|
||||||
func Http_ParseMethod(s: String) -> HttpMethod {
|
|
||||||
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
|
||||||
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
|
||||||
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
|
||||||
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
|
||||||
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
|
||||||
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
|
||||||
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
|
||||||
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Utility: MIME type from file extension
|
|
||||||
// =============================================================================
|
|
||||||
func Http_MimeType(path: String) -> String {
|
|
||||||
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
|
||||||
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
|
||||||
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
|
||||||
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
|
||||||
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
|
||||||
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
|
||||||
if String_EndsWith(path, ".png") { return "image/png"; }
|
|
||||||
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
|
||||||
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
|
||||||
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
|
||||||
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
|
||||||
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
|
||||||
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
|
||||||
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
|
||||||
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Header Storage: dynamic key-value array (faster than hash map for 5-15 headers)
|
|
||||||
// =============================================================================
|
|
||||||
func RequestHeader_Init(req: *HttpRequest) {
|
|
||||||
let cap: int = 16;
|
|
||||||
req.headerCap = cap;
|
|
||||||
req.headerKeys = bux_alloc(cap as uint * 8) as *String;
|
|
||||||
req.headerValues = bux_alloc(cap as uint * 8) as *String;
|
|
||||||
req.headerCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
func RequestHeader_Add(req: *HttpRequest, key: String, value: String) {
|
|
||||||
if req.headerCount >= req.headerCap { return; }
|
|
||||||
req.headerKeys[req.headerCount] = key;
|
|
||||||
req.headerValues[req.headerCount] = value;
|
|
||||||
req.headerCount = req.headerCount + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
func RequestHeader_Get(req: *HttpRequest, key: String) -> String {
|
|
||||||
var i: int = 0;
|
|
||||||
while i < req.headerCount {
|
|
||||||
if bux_strcmp(req.headerKeys[i], key) == 0 {
|
|
||||||
return req.headerValues[i];
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HTTP Request Parser
|
|
||||||
// =============================================================================
|
|
||||||
func Http_ParseRequest(raw: String) -> HttpRequest {
|
|
||||||
var req: HttpRequest;
|
|
||||||
req.method = HttpMethod { tag: HttpMethod_UNKNOWN };
|
|
||||||
req.path = "/";
|
|
||||||
req.body = "";
|
|
||||||
RequestHeader_Init(&req);
|
|
||||||
|
|
||||||
// Guard: null/empty input
|
|
||||||
if String_Len(raw) == 0 { return req; }
|
|
||||||
let rawLen: uint = bux_strlen(raw);
|
|
||||||
if rawLen == 0 { return req; }
|
|
||||||
|
|
||||||
// Find \r\n\r\n separating headers from body
|
|
||||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
|
||||||
var headerLen: uint = rawLen;
|
|
||||||
if String_Len(boundary) > 0 {
|
|
||||||
headerLen = String_Offset(boundary, raw);
|
|
||||||
let bodyStart: uint = headerLen + 4;
|
|
||||||
if bodyStart < rawLen {
|
|
||||||
req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slice out header block
|
|
||||||
if headerLen == 0 { return req; }
|
|
||||||
let headerBlock: String = bux_str_slice(raw, 0, headerLen);
|
|
||||||
|
|
||||||
// Count header lines
|
|
||||||
let lineCount: uint = bux_str_split_count(headerBlock, "\r\n");
|
|
||||||
if lineCount == 0 { return req; }
|
|
||||||
|
|
||||||
// --- Parse request line: "METHOD /path HTTP/1.1" ---
|
|
||||||
let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0);
|
|
||||||
if bux_strlen(requestLine) > 0 {
|
|
||||||
req.method = Http_ParseMethod(bux_str_split_part(requestLine, " ", 0));
|
|
||||||
req.path = bux_str_split_part(requestLine, " ", 1);
|
|
||||||
if String_Eq(req.path, "") || String_Len(req.path) == 0 {
|
|
||||||
req.path = "/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Parse headers (lines 1..N-1) ---
|
|
||||||
var i: uint = 1;
|
|
||||||
while i < lineCount {
|
|
||||||
let line: String = bux_str_split_part(headerBlock, "\r\n", i);
|
|
||||||
let lineLen: uint = bux_strlen(line);
|
|
||||||
if lineLen == 0 { i = i + 1; continue; }
|
|
||||||
|
|
||||||
// Find ": " separator
|
|
||||||
let colonPos: String = bux_strstr(line, ": ");
|
|
||||||
var key: String = "";
|
|
||||||
var value: String = "";
|
|
||||||
|
|
||||||
if String_Len(colonPos) > 0 {
|
|
||||||
let keyLen: uint = String_Offset(colonPos, line);
|
|
||||||
key = bux_str_slice(line, 0, keyLen);
|
|
||||||
let valStart: uint = keyLen + 2;
|
|
||||||
if valStart < lineLen {
|
|
||||||
value = bux_str_slice(line, valStart, lineLen - valStart);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Try ":" without space
|
|
||||||
let colonPos2: String = bux_strstr(line, ":");
|
|
||||||
if String_Len(colonPos2) > 0 {
|
|
||||||
let keyLen: uint = String_Offset(colonPos2, line);
|
|
||||||
key = bux_str_slice(line, 0, keyLen);
|
|
||||||
let valStart: uint = keyLen + 1;
|
|
||||||
if valStart < lineLen {
|
|
||||||
value = bux_str_slice(line, valStart, lineLen - valStart);
|
|
||||||
}
|
|
||||||
value = String_Trim(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store if valid key found
|
|
||||||
if bux_strlen(key) > 0 {
|
|
||||||
key = String_Trim(key);
|
|
||||||
RequestHeader_Add(&req, key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HTTP Response Builder
|
|
||||||
// =============================================================================
|
|
||||||
func Http_BuildResponse(resp: HttpResponse) -> String {
|
|
||||||
let sb: *void = bux_sb_new(4096);
|
|
||||||
|
|
||||||
// Status line
|
|
||||||
bux_sb_append(sb, "HTTP/1.1 ");
|
|
||||||
bux_sb_append_int(sb, resp.statusCode as int64);
|
|
||||||
bux_sb_append(sb, " ");
|
|
||||||
bux_sb_append(sb, resp.statusText);
|
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
|
|
||||||
// Server
|
|
||||||
bux_sb_append(sb, "Server: Nexus/0.1.0 (Bux)\r\n");
|
|
||||||
|
|
||||||
// Extra headers (for WS upgrade etc.)
|
|
||||||
if bux_strlen(resp.extraHeaders) > 0 {
|
|
||||||
bux_sb_append(sb, resp.extraHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content-Type
|
|
||||||
if bux_strlen(resp.contentType) > 0 {
|
|
||||||
bux_sb_append(sb, "Content-Type: ");
|
|
||||||
bux_sb_append(sb, resp.contentType);
|
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content-Length
|
|
||||||
let bodyLen: uint = bux_strlen(resp.body);
|
|
||||||
bux_sb_append(sb, "Content-Length: ");
|
|
||||||
bux_sb_append_int(sb, bodyLen as int64);
|
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
|
|
||||||
// Connection
|
|
||||||
if resp.connectionClose {
|
|
||||||
bux_sb_append(sb, "Connection: close\r\n");
|
|
||||||
} else {
|
|
||||||
bux_sb_append(sb, "Connection: keep-alive\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// End headers
|
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
|
|
||||||
// Body
|
|
||||||
if bodyLen > 0 {
|
|
||||||
bux_sb_append(sb, resp.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: String = bux_sb_build(sb);
|
|
||||||
bux_sb_free(sb);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Helper: Create a simple response
|
|
||||||
// =============================================================================
|
|
||||||
func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
|
||||||
var resp: HttpResponse;
|
|
||||||
resp.statusCode = code;
|
|
||||||
resp.statusText = Http_StatusText(code);
|
|
||||||
resp.contentType = contentType;
|
|
||||||
resp.body = body;
|
|
||||||
resp.extraHeaders = "";
|
|
||||||
resp.connectionClose = true;
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Static File Server
|
|
||||||
// =============================================================================
|
|
||||||
func ServeStaticFile(requestPath: String) -> HttpResponse {
|
|
||||||
// Block directory traversal
|
|
||||||
if String_Contains(requestPath, "..") {
|
|
||||||
return Http_NewResponse(403, "text/plain; charset=utf-8", "Forbidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to index.html
|
|
||||||
var filePath: String = requestPath;
|
|
||||||
if String_Eq(filePath, "/") {
|
|
||||||
filePath = "/index.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve full path
|
|
||||||
let fullPath: String = bux_path_join("public", filePath);
|
|
||||||
|
|
||||||
// Check existence
|
|
||||||
if bux_file_exists(fullPath) == 0 {
|
|
||||||
return Http_NewResponse(404, "text/plain; charset=utf-8", "Not Found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and serve
|
|
||||||
let content: String = bux_read_file(fullPath);
|
|
||||||
if String_Len(content) == 0 {
|
|
||||||
return Http_NewResponse(500, "text/plain; charset=utf-8", "Internal Server Error");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mime: String = Http_MimeType(filePath);
|
|
||||||
return Http_NewResponse(200, mime, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API Handlers
|
|
||||||
// =============================================================================
|
|
||||||
func HandleApiHealth() -> HttpResponse {
|
|
||||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
|
||||||
"{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.1.0\"}");
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleApiInfo() -> HttpResponse {
|
|
||||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
|
||||||
"{\"name\":\"Nexus\",\"language\":\"Bux\",\"threads\":4,\"features\":[\"HTTP/1.1\",\"HTTP/2-detect\",\"WebSocket-upgrade\"]}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// WebSocket Upgrade Handshake
|
|
||||||
// =============================================================================
|
|
||||||
func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
|
||||||
let wsKey: String = RequestHeader_Get(&req, "Sec-WebSocket-Key");
|
|
||||||
|
|
||||||
var resp: HttpResponse;
|
|
||||||
resp.statusCode = 101;
|
|
||||||
resp.statusText = "Switching Protocols";
|
|
||||||
resp.contentType = "";
|
|
||||||
resp.body = "";
|
|
||||||
resp.connectionClose = false;
|
|
||||||
|
|
||||||
// Build upgrade headers as extraHeaders
|
|
||||||
// TODO: SHA-1 + Base64 for proper accept key
|
|
||||||
let sb: *void = bux_sb_new(256);
|
|
||||||
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
|
||||||
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
|
||||||
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
|
||||||
bux_sb_append(sb, wsKey); // placeholder — real impl needs crypto
|
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
resp.extraHeaders = bux_sb_build(sb);
|
|
||||||
bux_sb_free(sb);
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Router / Dispatcher
|
|
||||||
// =============================================================================
|
|
||||||
func Router_Dispatch(req: HttpRequest) -> HttpResponse {
|
|
||||||
// Check for upgrade headers
|
|
||||||
let upgrade: String = RequestHeader_Get(&req, "Upgrade");
|
|
||||||
if bux_strlen(upgrade) > 0 {
|
|
||||||
if String_Contains(upgrade, "websocket") {
|
|
||||||
return HandleWebSocketUpgrade(req);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API routes
|
|
||||||
if String_StartsWith(req.path, "/api/") {
|
|
||||||
if String_Eq(req.path, "/api/health") { return HandleApiHealth(); }
|
|
||||||
if String_Eq(req.path, "/api/info") { return HandleApiInfo(); }
|
|
||||||
return Http_NewResponse(404, "application/json; charset=utf-8",
|
|
||||||
"{\"error\":\"not_found\"}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket endpoint
|
|
||||||
if String_Eq(req.path, "/ws") {
|
|
||||||
return HandleWebSocketUpgrade(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static files — GET/HEAD only
|
|
||||||
if req.method.tag == HttpMethod_GET || req.method.tag == HttpMethod_HEAD {
|
|
||||||
return ServeStaticFile(req.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method not allowed
|
|
||||||
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Method name for logging (avoids 8 if-checks in hot path)
|
|
||||||
// =============================================================================
|
|
||||||
func Http_MethodName(m: HttpMethod) -> String {
|
|
||||||
if m.tag == HttpMethod_GET { return "GET"; }
|
|
||||||
if m.tag == HttpMethod_POST { return "POST"; }
|
|
||||||
if m.tag == HttpMethod_PUT { return "PUT"; }
|
|
||||||
if m.tag == HttpMethod_DELETE { return "DELETE"; }
|
|
||||||
if m.tag == HttpMethod_PATCH { return "PATCH"; }
|
|
||||||
if m.tag == HttpMethod_HEAD { return "HEAD"; }
|
|
||||||
if m.tag == HttpMethod_OPTIONS { return "OPTIONS"; }
|
|
||||||
return "UNKNOWN";
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Connection Handler — read, parse, route, respond
|
|
||||||
// =============================================================================
|
|
||||||
func HandleConnection(clientFd: int) {
|
|
||||||
let raw: String = Net_Recv(clientFd, 8192);
|
|
||||||
|
|
||||||
// Empty read → client disconnected
|
|
||||||
if String_Len(raw) == 0 { return; }
|
|
||||||
if bux_strlen(raw) == 0 { return; }
|
|
||||||
|
|
||||||
// HTTP/2 connection preface detection
|
|
||||||
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
|
||||||
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
|
||||||
"HTTP/2 detected — full support planned for future release.\r\n");
|
|
||||||
let respStr: String = Http_BuildResponse(resp);
|
|
||||||
Net_Send(clientFd, respStr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse HTTP/1.1
|
|
||||||
let req: HttpRequest = Http_ParseRequest(raw);
|
|
||||||
|
|
||||||
// Log
|
|
||||||
Print(Http_MethodName(req.method));
|
|
||||||
Print(" ");
|
|
||||||
Print(req.path);
|
|
||||||
Print(" -> ");
|
|
||||||
|
|
||||||
// Dispatch
|
|
||||||
let resp: HttpResponse = Router_Dispatch(req);
|
|
||||||
|
|
||||||
// Log result
|
|
||||||
PrintInt(resp.statusCode);
|
|
||||||
Print(" ");
|
|
||||||
PrintLine(resp.statusText);
|
|
||||||
|
|
||||||
// Send
|
|
||||||
let respStr: String = Http_BuildResponse(resp);
|
|
||||||
Net_Send(clientFd, respStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Worker Thread — accept loop
|
|
||||||
// =============================================================================
|
|
||||||
func Worker(serverFd: int) {
|
|
||||||
while true {
|
|
||||||
let clientFd: int = Net_Accept(serverFd);
|
|
||||||
if clientFd < 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
HandleConnection(clientFd);
|
|
||||||
Net_Close(clientFd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Main Entry Point
|
|
||||||
// =============================================================================
|
|
||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
PrintLine("================================================");
|
let config: ServerConfig = DefaultConfig();
|
||||||
PrintLine(" Nexus HTTP Server v0.1.0");
|
let router: Router = BuildRouter();
|
||||||
PrintLine(" Multi-threaded HTTP/1.1 + HTTP/2 & WS detect");
|
|
||||||
PrintLine(" Built with Bux");
|
|
||||||
PrintLine("================================================");
|
|
||||||
PrintLine("");
|
|
||||||
|
|
||||||
// Create socket
|
return RunServer(config, router);
|
||||||
let serverFd: int = Net_Create();
|
|
||||||
if serverFd < 0 {
|
|
||||||
PrintLine("FATAL: socket() failed");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SO_REUSEADDR
|
|
||||||
if !Net_SetReuse(serverFd) {
|
|
||||||
PrintLine("WARN: SO_REUSEADDR failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind
|
|
||||||
if !Net_Bind(serverFd, "0.0.0.0", 8080) {
|
|
||||||
Print("FATAL: bind(8080) failed: ");
|
|
||||||
PrintLine(Net_LastError());
|
|
||||||
Net_Close(serverFd);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen
|
|
||||||
if !Net_Listen(serverFd, 128) {
|
|
||||||
PrintLine("FATAL: listen() failed");
|
|
||||||
Net_Close(serverFd);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintLine("Listening on http://0.0.0.0:8080");
|
|
||||||
PrintLine("4 worker threads | static: ./public/");
|
|
||||||
PrintLine("Endpoints: / /api/health /api/info /ws");
|
|
||||||
PrintLine("Press Ctrl+C to stop.");
|
|
||||||
PrintLine("");
|
|
||||||
|
|
||||||
// Spawn 3 workers (main thread is the 4th)
|
|
||||||
spawn Worker(serverFd);
|
|
||||||
spawn Worker(serverFd);
|
|
||||||
spawn Worker(serverFd);
|
|
||||||
|
|
||||||
// Main thread becomes worker #4 (blocking call)
|
|
||||||
Worker(serverFd);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // module Main
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
module Parser {
|
||||||
|
|
||||||
|
import Std::String::{String_Len, String_Eq, String_Trim};
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push};
|
||||||
|
import Http::{HttpMethod, HttpRequest, HeaderEntry};
|
||||||
|
import Errors::{HttpError, ParseResult, ParseResult_NewOk, ParseResult_NewErr};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_strstr(haystack: String, needle: String) -> String;
|
||||||
|
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||||
|
extern func bux_str_offset(pos: String, base: String) -> uint;
|
||||||
|
|
||||||
|
pub func ParseMethod(s: String) -> HttpMethod {
|
||||||
|
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
||||||
|
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
||||||
|
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
||||||
|
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
||||||
|
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
||||||
|
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
||||||
|
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
||||||
|
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||||
|
}
|
||||||
|
|
||||||
|
func Slice(raw: String, start: int, len: int) -> String {
|
||||||
|
if start < 0 || len <= 0 { return ""; }
|
||||||
|
return bux_str_slice(raw, start as uint, len as uint);
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindCrlf(raw: String, start: int) -> int {
|
||||||
|
let rawLen: uint = bux_strlen(raw);
|
||||||
|
if start as uint >= rawLen { return -1; }
|
||||||
|
let tail: String = bux_str_slice(raw, start as uint, rawLen - start as uint);
|
||||||
|
let hit: String = bux_strstr(tail, "\r\n");
|
||||||
|
if String_Len(hit) == 0 { return -1; }
|
||||||
|
let offset: uint = bux_str_offset(hit, tail);
|
||||||
|
return start + offset as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHeaders(raw: String, start: int, end: int) -> Array<HeaderEntry> {
|
||||||
|
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||||
|
var pos: int = start;
|
||||||
|
while pos < end {
|
||||||
|
let lineEnd: int = FindCrlf(raw, pos);
|
||||||
|
if lineEnd < 0 || lineEnd >= end { break; }
|
||||||
|
let lineLen: int = lineEnd - pos;
|
||||||
|
if lineLen > 0 {
|
||||||
|
let line: String = Slice(raw, pos, lineLen);
|
||||||
|
let colon: String = bux_strstr(line, ":");
|
||||||
|
if String_Len(colon) > 0 {
|
||||||
|
let keyLen: uint = bux_str_offset(colon, line);
|
||||||
|
let key: String = String_Trim(Slice(line, 0, keyLen as int));
|
||||||
|
let valStart: int = keyLen as int + 1;
|
||||||
|
let val: String = String_Trim(Slice(line, valStart, lineLen - valStart));
|
||||||
|
let entry: HeaderEntry = HeaderEntry { key: key, value: val };
|
||||||
|
Array_Push<HeaderEntry>(&headers, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = lineEnd + 2;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ParseRequest(raw: String) -> ParseResult {
|
||||||
|
let rawLen: uint = bux_strlen(raw);
|
||||||
|
if rawLen == 0 {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find end of request line
|
||||||
|
let lineEnd: int = FindCrlf(raw, 0);
|
||||||
|
if lineEnd < 0 {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split request line: METHOD PATH VERSION
|
||||||
|
var methodEnd: int = -1;
|
||||||
|
var pathStart: int = -1;
|
||||||
|
var pathEnd: int = -1;
|
||||||
|
var i: int = 0;
|
||||||
|
while i < lineEnd {
|
||||||
|
if raw[i] as int == 32 { // space
|
||||||
|
if methodEnd < 0 {
|
||||||
|
methodEnd = i;
|
||||||
|
pathStart = i + 1;
|
||||||
|
} else if pathEnd < 0 {
|
||||||
|
pathEnd = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if methodEnd < 0 || pathStart < 0 || pathEnd < 0 {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
let methodStr: String = Slice(raw, 0, methodEnd);
|
||||||
|
let path: String = Slice(raw, pathStart, pathEnd - pathStart);
|
||||||
|
let version: String = Slice(raw, pathEnd + 1, lineEnd - pathEnd - 1);
|
||||||
|
|
||||||
|
// Find header/body boundary
|
||||||
|
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||||
|
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||||
|
var body: String = "";
|
||||||
|
if String_Len(boundary) > 0 {
|
||||||
|
let headerEnd: uint = bux_str_offset(boundary, raw);
|
||||||
|
headers = ParseHeaders(raw, lineEnd + 2, headerEnd as int);
|
||||||
|
let bodyStart: uint = headerEnd + 4;
|
||||||
|
if bodyStart < rawLen {
|
||||||
|
body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers = ParseHeaders(raw, lineEnd + 2, rawLen as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(path, "") {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
let req: HttpRequest = HttpRequest {
|
||||||
|
method: ParseMethod(methodStr),
|
||||||
|
path: path,
|
||||||
|
version: version,
|
||||||
|
body: body,
|
||||||
|
headers: headers,
|
||||||
|
};
|
||||||
|
return ParseResult_NewOk(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
module Router {
|
||||||
|
|
||||||
|
import Std::Array::{Array};
|
||||||
|
import Std::String::{String_Eq};
|
||||||
|
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse};
|
||||||
|
import Handlers::{ServeStaticFile, HandleApiHealth, HandleApiInfo, HandleWebSocketUpgrade, NotFoundResponse};
|
||||||
|
|
||||||
|
pub enum Handler {
|
||||||
|
StaticFile,
|
||||||
|
ApiHealth,
|
||||||
|
ApiInfo,
|
||||||
|
WsUpgrade,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Route {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: String;
|
||||||
|
handler: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Router {
|
||||||
|
routes: Array<Route>;
|
||||||
|
notFound: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Handler_Handle(h: Handler, req: HttpRequest) -> HttpResponse {
|
||||||
|
match h {
|
||||||
|
Handler::StaticFile => ServeStaticFile(req),
|
||||||
|
Handler::ApiHealth => HandleApiHealth(),
|
||||||
|
Handler::ApiInfo => HandleApiInfo(),
|
||||||
|
Handler::WsUpgrade => HandleWebSocketUpgrade(req),
|
||||||
|
Handler::NotFound => NotFoundResponse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Router_Dispatch(r: Router, req: HttpRequest) -> HttpResponse {
|
||||||
|
for route in r.routes {
|
||||||
|
if route.method == req.method && String_Eq(route.path, req.path) {
|
||||||
|
return Handler_Handle(route.handler, req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Handler_Handle(r.notFound, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
module Server {
|
||||||
|
|
||||||
|
import Std::Io::{Print, PrintLine, PrintInt};
|
||||||
|
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
|
||||||
|
import Std::String::{String_Len, String_StartsWith};
|
||||||
|
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||||
|
import Config::{ServerConfig};
|
||||||
|
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse};
|
||||||
|
import Errors::{ParseResult};
|
||||||
|
import Parser::{ParseRequest};
|
||||||
|
import Router::{Router, Router_Dispatch};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||||
|
extern func bux_sb_append(sb: *void, s: String);
|
||||||
|
extern func bux_sb_append_int(sb: *void, n: int64);
|
||||||
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
|
extern func bux_sb_free(sb: *void);
|
||||||
|
|
||||||
|
pub struct ConnectionTask {
|
||||||
|
fd: int;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func BuildResponse(resp: HttpResponse) -> String {
|
||||||
|
let sb: *void = bux_sb_new(4096);
|
||||||
|
|
||||||
|
bux_sb_append(sb, "HTTP/1.1 ");
|
||||||
|
bux_sb_append_int(sb, resp.statusCode as int64);
|
||||||
|
bux_sb_append(sb, " ");
|
||||||
|
bux_sb_append(sb, Http_StatusText(resp.statusCode));
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
|
||||||
|
bux_sb_append(sb, "Server: Nexus/0.2.0 (Bux)\r\n");
|
||||||
|
|
||||||
|
if bux_strlen(resp.extraHeaders) > 0 {
|
||||||
|
bux_sb_append(sb, resp.extraHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bux_strlen(resp.contentType) > 0 {
|
||||||
|
bux_sb_append(sb, "Content-Type: ");
|
||||||
|
bux_sb_append(sb, resp.contentType);
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyLen: uint = bux_strlen(resp.body);
|
||||||
|
bux_sb_append(sb, "Content-Length: ");
|
||||||
|
bux_sb_append_int(sb, bodyLen as int64);
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
bux_sb_append(sb, "Connection: close\r\n");
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
|
||||||
|
if bodyLen > 0 {
|
||||||
|
bux_sb_append(sb, resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: String = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleConnection(fd: int, router: Router) {
|
||||||
|
let raw: String = Net_Recv(fd, 8192);
|
||||||
|
if String_Len(raw) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP/2 preface detection
|
||||||
|
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||||
|
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||||
|
"HTTP/2 detected — full support planned for future release.\r\n");
|
||||||
|
Net_Send(fd, BuildResponse(resp));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: ParseResult = ParseRequest(raw);
|
||||||
|
if parsed.tag == ParseResult_Ok {
|
||||||
|
let req: HttpRequest = parsed.data.Ok_0;
|
||||||
|
let resp: HttpResponse = Router_Dispatch(router, req);
|
||||||
|
Net_Send(fd, BuildResponse(resp));
|
||||||
|
} else {
|
||||||
|
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
||||||
|
Net_Send(fd, BuildResponse(resp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WorkerCtx {
|
||||||
|
taskQueue: *Channel<ConnectionTask>;
|
||||||
|
router: Router;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Worker(ctx: *WorkerCtx) {
|
||||||
|
while true {
|
||||||
|
let task: ConnectionTask = Channel_Recv<ConnectionTask>(ctx.taskQueue);
|
||||||
|
HandleConnection(task.fd, ctx.router);
|
||||||
|
Net_Close(task.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AcceptorCtx {
|
||||||
|
serverFd: int;
|
||||||
|
taskQueue: *Channel<ConnectionTask>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Acceptor(ctx: *AcceptorCtx) {
|
||||||
|
while true {
|
||||||
|
let fd: int = Net_Accept(ctx.serverFd);
|
||||||
|
if fd >= 0 {
|
||||||
|
let task: ConnectionTask = ConnectionTask { fd: fd };
|
||||||
|
Channel_Send<ConnectionTask>(ctx.taskQueue, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func RunServer(config: ServerConfig, router: Router) -> int {
|
||||||
|
PrintLine("================================================");
|
||||||
|
PrintLine(" Nexus HTTP Server v0.2.0");
|
||||||
|
PrintLine(" Production-ready HTTP/1.1 with thread-pool");
|
||||||
|
PrintLine(" Built with Bux");
|
||||||
|
PrintLine("================================================");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
let serverFd: int = Net_Create();
|
||||||
|
if serverFd < 0 {
|
||||||
|
PrintLine("FATAL: socket() failed");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_SetReuse(serverFd) {
|
||||||
|
PrintLine("WARN: SO_REUSEADDR failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_Bind(serverFd, config.bindAddr, config.port) {
|
||||||
|
Print("FATAL: bind failed: ");
|
||||||
|
PrintLine(Net_LastError());
|
||||||
|
Net_Close(serverFd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_Listen(serverFd, config.backlog) {
|
||||||
|
PrintLine("FATAL: listen() failed");
|
||||||
|
Net_Close(serverFd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("Listening on http://");
|
||||||
|
Print(config.bindAddr);
|
||||||
|
Print(":");
|
||||||
|
PrintInt(config.port);
|
||||||
|
PrintLine("");
|
||||||
|
PrintInt(config.workerCount);
|
||||||
|
PrintLine(" worker threads | static: ./public/");
|
||||||
|
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||||
|
PrintLine("Press Ctrl+C to stop.");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64);
|
||||||
|
let workerCtx: WorkerCtx = WorkerCtx { taskQueue: &taskQueue, router: router };
|
||||||
|
let acceptorCtx: AcceptorCtx = AcceptorCtx { serverFd: serverFd, taskQueue: &taskQueue };
|
||||||
|
|
||||||
|
// Spawn workers (main thread will also become one)
|
||||||
|
var i: int = 0;
|
||||||
|
while i < config.workerCount - 1 {
|
||||||
|
spawn Worker(&workerCtx);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn acceptor
|
||||||
|
spawn Acceptor(&acceptorCtx);
|
||||||
|
|
||||||
|
// Main thread works too
|
||||||
|
Worker(&workerCtx);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import std/[os, strutils, terminal, strformat, osproc, sets]
|
import std/[os, strutils, terminal, strformat, osproc, sets]
|
||||||
import lexer, parser, ast, sema, manifest, hir_lower, lir, lir_lower, lir_c_backend
|
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
|
||||||
|
|
||||||
type
|
type
|
||||||
ColorMode* = enum
|
ColorMode* = enum
|
||||||
|
|||||||
+66
-16
@@ -42,22 +42,37 @@ proc flushPending(ctx: var LowerCtx, node: HirNode): HirNode =
|
|||||||
return hirBlock(stmts, nil, makeVoid(), node.loc)
|
return hirBlock(stmts, nil, makeVoid(), node.loc)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
proc enumHasDataVariants(ctx: var LowerCtx, enumName: string): bool =
|
||||||
|
let sym = ctx.globalScope.lookup(enumName)
|
||||||
|
if sym != nil and sym.decl != nil and sym.decl.kind == dkEnum:
|
||||||
|
for v in sym.decl.declEnumVariants:
|
||||||
|
if v.fields.len > 0 or v.namedFields.len > 0:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode =
|
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode =
|
||||||
# Lower match expression to a block with if-else chain.
|
# Lower match expression to a block with if-else chain.
|
||||||
# For now, supports enum tag matching and wildcard/ident fallbacks.
|
# For now, supports enum tag matching and wildcard/ident fallbacks.
|
||||||
let resultName = ctx.freshName()
|
let resultName = ctx.freshName()
|
||||||
var stmts: seq[HirNode] = @[]
|
var stmts: seq[HirNode] = @[]
|
||||||
|
|
||||||
# Allocate result variable
|
# Allocate result variable
|
||||||
stmts.add(hirAlloca(resultName, typ, loc))
|
stmts.add(hirAlloca(resultName, typ, loc))
|
||||||
|
|
||||||
|
# Determine whether the matched enum has data variants (needs .tag access).
|
||||||
|
var subjectEnumName = ""
|
||||||
|
var subjectHasData = false
|
||||||
|
if subject.typ != nil and subject.typ.kind == tkNamed:
|
||||||
|
subjectEnumName = subject.typ.name
|
||||||
|
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
|
||||||
|
|
||||||
# Build if-else chain from arms (last arm is the outermost else)
|
# Build if-else chain from arms (last arm is the outermost else)
|
||||||
var ifChain: HirNode = nil
|
var ifChain: HirNode = nil
|
||||||
|
|
||||||
for i in countdown(arms.len - 1, 0):
|
for i in countdown(arms.len - 1, 0):
|
||||||
let arm = arms[i]
|
let arm = arms[i]
|
||||||
let body = arm.body
|
let body = arm.body
|
||||||
|
|
||||||
case arm.pattern.kind
|
case arm.pattern.kind
|
||||||
of pkEnum:
|
of pkEnum:
|
||||||
let path = arm.pattern.patEnumPath
|
let path = arm.pattern.patEnumPath
|
||||||
@@ -65,19 +80,25 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
|
|||||||
let enumName = path[0]
|
let enumName = path[0]
|
||||||
let variantName = path[^1]
|
let variantName = path[^1]
|
||||||
let tagName = enumName & "_" & variantName
|
let tagName = enumName & "_" & variantName
|
||||||
|
|
||||||
# condition: subject.tag == EnumName_VariantName
|
var cond: HirNode
|
||||||
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
|
if subjectHasData and enumName == subjectEnumName:
|
||||||
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
|
# Algebraic enum: compare subject.tag
|
||||||
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc)
|
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
|
||||||
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc)
|
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
|
||||||
let cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
|
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc)
|
||||||
|
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc)
|
||||||
|
cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
|
||||||
|
else:
|
||||||
|
# Simple enum or cross-enum match: compare subject directly
|
||||||
|
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName), loc)
|
||||||
|
cond = hirBinary(tkEq, subject, tagConst, makeBool(), loc)
|
||||||
|
|
||||||
# body: result = arm_body
|
# body: result = arm_body
|
||||||
var armStmts: seq[HirNode] = @[]
|
var armStmts: seq[HirNode] = @[]
|
||||||
armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc))
|
armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc))
|
||||||
let armBlock = hirBlock(armStmts, nil, makeVoid(), loc)
|
let armBlock = hirBlock(armStmts, nil, makeVoid(), loc)
|
||||||
|
|
||||||
if ifChain == nil:
|
if ifChain == nil:
|
||||||
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil,
|
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil,
|
||||||
typ: makeVoid(), loc: loc)
|
typ: makeVoid(), loc: loc)
|
||||||
@@ -115,9 +136,9 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
|
|||||||
ifChain = HirNode(kind: hIf,
|
ifChain = HirNode(kind: hIf,
|
||||||
ifCond: hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc),
|
ifCond: hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc),
|
||||||
ifThen: armBlock, ifElse: ifChain, typ: makeVoid(), loc: loc)
|
ifThen: armBlock, ifElse: ifChain, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
stmts.add(ifChain)
|
stmts.add(ifChain)
|
||||||
|
|
||||||
# Return the result variable as the block expression
|
# Return the result variable as the block expression
|
||||||
return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc)
|
return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc)
|
||||||
|
|
||||||
@@ -397,6 +418,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
|||||||
if f.ftype != nil:
|
if f.ftype != nil:
|
||||||
case f.ftype.kind
|
case f.ftype.kind
|
||||||
of tekNamed:
|
of tekNamed:
|
||||||
|
if f.ftype.typeArgs.len > 0:
|
||||||
|
return substituteType(ctx, f.ftype, subst)
|
||||||
case f.ftype.typeName
|
case f.ftype.typeName
|
||||||
of "int", "int32", "int64": return makeInt()
|
of "int", "int32", "int64": return makeInt()
|
||||||
of "float64": return makeFloat64()
|
of "float64": return makeFloat64()
|
||||||
@@ -417,6 +440,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
|||||||
if f.ftype != nil:
|
if f.ftype != nil:
|
||||||
case f.ftype.kind
|
case f.ftype.kind
|
||||||
of tekNamed:
|
of tekNamed:
|
||||||
|
if f.ftype.typeArgs.len > 0:
|
||||||
|
return ctx.resolveTypeExpr(f.ftype)
|
||||||
case f.ftype.typeName
|
case f.ftype.typeName
|
||||||
of "int", "int32", "int64": return makeInt()
|
of "int", "int32", "int64": return makeInt()
|
||||||
of "float64": return makeFloat64()
|
of "float64": return makeFloat64()
|
||||||
@@ -478,6 +503,10 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
|||||||
if baseType.isPointer and baseType.inner.len > 0:
|
if baseType.isPointer and baseType.inner.len > 0:
|
||||||
return baseType.inner[0]
|
return baseType.inner[0]
|
||||||
return makeUnknown()
|
return makeUnknown()
|
||||||
|
of ekMatch:
|
||||||
|
if expr.exprMatchArms.len > 0:
|
||||||
|
return ctx.resolveExprType(expr.exprMatchArms[0].body)
|
||||||
|
return makeUnknown()
|
||||||
of ekBlock:
|
of ekBlock:
|
||||||
if expr.exprBlock.stmts.len > 0:
|
if expr.exprBlock.stmts.len > 0:
|
||||||
let last = expr.exprBlock.stmts[^1]
|
let last = expr.exprBlock.stmts[^1]
|
||||||
@@ -524,6 +553,23 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
|
|||||||
return te.typeArgs[0]
|
return te.typeArgs[0]
|
||||||
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
|
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
|
||||||
return te.pointerPointee.typeArgs[0]
|
return te.pointerPointee.typeArgs[0]
|
||||||
|
of ekField:
|
||||||
|
# Try to resolve the field's declared TypeExpr directly.
|
||||||
|
let objType = ctx.resolveExprType(expr.exprFieldObj)
|
||||||
|
if objType.kind == tkNamed:
|
||||||
|
var decl = ctx.globalScope.lookup(objType.name).decl
|
||||||
|
if decl == nil and ctx.structInstMap.hasKey(objType.name):
|
||||||
|
let (baseName, _) = ctx.structInstMap[objType.name]
|
||||||
|
let baseSym = ctx.globalScope.lookup(baseName)
|
||||||
|
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkStruct:
|
||||||
|
decl = baseSym.decl
|
||||||
|
if decl != nil and decl.kind == dkStruct:
|
||||||
|
for f in decl.declStructFields:
|
||||||
|
if f.name == expr.exprFieldName and f.ftype != nil:
|
||||||
|
let fte = f.ftype
|
||||||
|
if fte.kind == tekNamed and fte.typeArgs.len > 0 and (fte.typeName == "Array" or fte.typeName == "Iter" or fte.typeName == "Channel"):
|
||||||
|
return fte.typeArgs[0]
|
||||||
|
return fte
|
||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
let t = ctx.resolveExprType(expr)
|
let t = ctx.resolveExprType(expr)
|
||||||
@@ -531,6 +577,11 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
|
|||||||
return typeToTypeExpr(t.inner[0])
|
return typeToTypeExpr(t.inner[0])
|
||||||
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
|
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
|
||||||
return typeToTypeExpr(t.inner[0].inner[0])
|
return typeToTypeExpr(t.inner[0].inner[0])
|
||||||
|
# Generic struct instances (e.g. Array_HeaderEntry) store their type args in structInstMap.
|
||||||
|
if t.kind == tkNamed and ctx.structInstMap.hasKey(t.name):
|
||||||
|
let (baseName, concreteArgs) = ctx.structInstMap[t.name]
|
||||||
|
if concreteArgs.len > 0 and (baseName == "Array" or baseName == "Iter" or baseName == "Channel"):
|
||||||
|
return typeToTypeExpr(concreteArgs[0])
|
||||||
return TypeExpr(kind: tekNamed, typeName: "unknown")
|
return TypeExpr(kind: tekNamed, typeName: "unknown")
|
||||||
|
|
||||||
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
|
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
|
||||||
@@ -1660,7 +1711,6 @@ proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs:
|
|||||||
return mangledName
|
return mangledName
|
||||||
|
|
||||||
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
|
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
|
||||||
let loc = expr.loc
|
|
||||||
let name = "__closure_" & $ctx.varCounter
|
let name = "__closure_" & $ctx.varCounter
|
||||||
inc ctx.varCounter
|
inc ctx.varCounter
|
||||||
var f = HirFunc(name: name, isPublic: false)
|
var f = HirFunc(name: name, isPublic: false)
|
||||||
|
|||||||
@@ -61,12 +61,6 @@ proc advance(lex: var Lexer): char =
|
|||||||
else:
|
else:
|
||||||
inc lex.col
|
inc lex.col
|
||||||
|
|
||||||
proc match(lex: var Lexer, expected: char): bool =
|
|
||||||
if lex.isAtEnd(): return false
|
|
||||||
if lex.peek() != expected: return false
|
|
||||||
discard lex.advance()
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc matchStr(lex: var Lexer, s: string): bool =
|
proc matchStr(lex: var Lexer, s: string): bool =
|
||||||
for i, c in s:
|
for i, c in s:
|
||||||
if lex.peek(i) != c:
|
if lex.peek(i) != c:
|
||||||
@@ -81,9 +75,6 @@ proc currentLocation(lex: Lexer): SourceLocation =
|
|||||||
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
|
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
|
||||||
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
|
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
|
||||||
|
|
||||||
proc emitWarning(lex: var Lexer, loc: SourceLocation, message: string) =
|
|
||||||
lex.diagnostics.add(LexerDiagnostic(severity: ldsWarning, loc: loc, message: message))
|
|
||||||
|
|
||||||
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
|
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
|
||||||
let text = lex.source[startPos ..< lex.pos]
|
let text = lex.source[startPos ..< lex.pos]
|
||||||
result = Token(kind: kind, text: text, loc: startLoc)
|
result = Token(kind: kind, text: text, loc: startLoc)
|
||||||
@@ -337,19 +328,6 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
|
|||||||
else:
|
else:
|
||||||
return lex.makeToken(kind1, startLoc, startPos)
|
return lex.makeToken(kind1, startLoc, startPos)
|
||||||
|
|
||||||
template check3(c2: char, kind2: TokenKind, c3: char, kind3: TokenKind, kind1: TokenKind) =
|
|
||||||
if lex.peek() == c2:
|
|
||||||
discard lex.advance()
|
|
||||||
if lex.peek() == c3:
|
|
||||||
discard lex.advance()
|
|
||||||
return lex.makeToken(kind3, startLoc, startPos)
|
|
||||||
return lex.makeToken(kind2, startLoc, startPos)
|
|
||||||
else:
|
|
||||||
return lex.makeToken(kind1, startLoc, startPos)
|
|
||||||
|
|
||||||
template checkEq(c2: char, kind2: TokenKind, kind1: TokenKind) =
|
|
||||||
check2(c2, kind2, kind1)
|
|
||||||
|
|
||||||
case c1
|
case c1
|
||||||
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
|
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
|
||||||
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
|
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
## Linear 3-address code IR, designed for straightforward C emission.
|
## Linear 3-address code IR, designed for straightforward C emission.
|
||||||
## Each HIR construct lowers to 5-30 LIR instructions.
|
## Each HIR construct lowers to 5-30 LIR instructions.
|
||||||
|
|
||||||
import types
|
|
||||||
|
|
||||||
type
|
type
|
||||||
LirKind* = enum
|
LirKind* = enum
|
||||||
# ── Data movement ──
|
# ── Data movement ──
|
||||||
|
|||||||
+119
-44
@@ -17,9 +17,6 @@ proc initLirCBackend*(): LirCBackend =
|
|||||||
tempTypes: initTable[string, string](),
|
tempTypes: initTable[string, string](),
|
||||||
)
|
)
|
||||||
|
|
||||||
proc emit(be: var LirCBackend, s: string) =
|
|
||||||
be.output.add(s)
|
|
||||||
|
|
||||||
proc emitIndent(be: var LirCBackend) =
|
proc emitIndent(be: var LirCBackend) =
|
||||||
for i in 0 ..< be.indent:
|
for i in 0 ..< be.indent:
|
||||||
be.output.add(" ")
|
be.output.add(" ")
|
||||||
@@ -43,21 +40,6 @@ proc valToC(be: var LirCBackend, v: LirValue): string =
|
|||||||
of lvkField: v.strVal
|
of lvkField: v.strVal
|
||||||
of lvkType: v.strVal
|
of lvkType: v.strVal
|
||||||
|
|
||||||
proc typeFromValue(be: var LirCBackend, v: LirValue): string =
|
|
||||||
## Infer a C type for a value. Temps are tracked; named vars use lookup.
|
|
||||||
case v.kind
|
|
||||||
of lvkTemp:
|
|
||||||
if be.tempTypes.hasKey(v.strVal):
|
|
||||||
return be.tempTypes[v.strVal]
|
|
||||||
return "int" # Default
|
|
||||||
of lvkString: return "const char*"
|
|
||||||
of lvkInt: return "int"
|
|
||||||
of lvkFloat: return "double"
|
|
||||||
else: return ""
|
|
||||||
|
|
||||||
proc setTempType(be: var LirCBackend, temp: string, cType: string) =
|
|
||||||
be.tempTypes[temp] = cType
|
|
||||||
|
|
||||||
proc cParamDecl(cType, name: string): string =
|
proc cParamDecl(cType, name: string): string =
|
||||||
## Emit a C parameter declaration, handling function-pointer syntax.
|
## Emit a C parameter declaration, handling function-pointer syntax.
|
||||||
if cType.contains("(*)"):
|
if cType.contains("(*)"):
|
||||||
@@ -487,6 +469,26 @@ proc emitEnumDef(be: var LirCBackend, name: string, variants: seq[HirEnumVariant
|
|||||||
be.emitLine(&"}} {name};")
|
be.emitLine(&"}} {name};")
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
|
|
||||||
|
# ── Type dependency ordering ──
|
||||||
|
|
||||||
|
proc collectValueDeps(typ: Type): seq[string] =
|
||||||
|
## Return type names that must be fully defined before a value of `typ`
|
||||||
|
## can be declared. Pointers/refs only need a forward declaration, so
|
||||||
|
## they do not introduce a dependency.
|
||||||
|
if typ == nil: return @[]
|
||||||
|
case typ.kind
|
||||||
|
of tkNamed:
|
||||||
|
return @[typ.name]
|
||||||
|
of tkSlice:
|
||||||
|
return @[typeToCStr(typ)]
|
||||||
|
of tkPointer, tkRef, tkMutRef, tkTuple, tkFunc:
|
||||||
|
return @[]
|
||||||
|
else:
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
proc emitSliceTypeDef(be: var LirCBackend, name: string, elem: string) =
|
||||||
|
be.emitLine(&"typedef struct {{ {elem}* data; size_t len; }} {name};")
|
||||||
|
|
||||||
# ── Module emission ──
|
# ── Module emission ──
|
||||||
|
|
||||||
proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): string =
|
proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): string =
|
||||||
@@ -557,36 +559,109 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
|
|||||||
else: discard
|
else: discard
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
|
|
||||||
# Enum definitions
|
# Collect local type names (structs and enums defined in this module).
|
||||||
|
var localTypeNames: HashSet[string]
|
||||||
|
for s in module.structs:
|
||||||
|
localTypeNames.incl(s.name)
|
||||||
for e in module.enums:
|
for e in module.enums:
|
||||||
be.emitEnumDef(e.name, e.variants)
|
localTypeNames.incl(e.name)
|
||||||
if module.enums.len > 0:
|
|
||||||
be.emitLine("")
|
|
||||||
|
|
||||||
# Struct definitions
|
# Collect slice types used in struct fields and enum payloads.
|
||||||
for s in module.structs:
|
|
||||||
be.emitStructDef(s.name, s.fields)
|
|
||||||
|
|
||||||
# Slice types (collect from functions/structs)
|
|
||||||
# Simple: scan function params/returns for slice types
|
|
||||||
var sliceTypes: seq[tuple[name: string, elem: string]] = @[]
|
var sliceTypes: seq[tuple[name: string, elem: string]] = @[]
|
||||||
var structNames: HashSet[string]
|
var sliceNames: HashSet[string]
|
||||||
|
proc registerSlice(t: Type) =
|
||||||
|
if t == nil or t.kind != tkSlice: return
|
||||||
|
let name = typeToCStr(t)
|
||||||
|
if sliceNames.contains(name): return
|
||||||
|
sliceNames.incl(name)
|
||||||
|
let elem = if t.inner.len > 0: typeToCStr(t.inner[0]) else: "void"
|
||||||
|
sliceTypes.add((name, elem))
|
||||||
|
|
||||||
for s in module.structs:
|
for s in module.structs:
|
||||||
structNames.incl(s.name)
|
for f in s.fields:
|
||||||
for f in module.funcs:
|
registerSlice(f.typ)
|
||||||
for p in f.params:
|
for e in module.enums:
|
||||||
var ct = typeToCStr(p.typ)
|
for v in e.variants:
|
||||||
# Strip pointer/reference suffix to find the base slice type.
|
for ft in v.fields:
|
||||||
while ct.endsWith("*"):
|
registerSlice(ft)
|
||||||
ct = ct[0..^2]
|
for nf in v.namedFields:
|
||||||
if ct.startsWith("Slice_"):
|
registerSlice(nf.typ)
|
||||||
let elem = ct[6 .. ^1]
|
|
||||||
if not sliceTypes.anyIt(it.name == ct) and not structNames.contains(ct):
|
# Build dependency graph among structs, enums, and slice types.
|
||||||
sliceTypes.add((ct, elem))
|
# Edge A -> B means "A depends on B, so B must be emitted before A".
|
||||||
if sliceTypes.len > 0:
|
var deps: Table[string, seq[string]]
|
||||||
for st in sliceTypes:
|
for s in module.structs:
|
||||||
be.emitLine(&"typedef struct {{ {st.elem}* data; size_t len; }} {st.name};")
|
deps[s.name] = @[]
|
||||||
be.emitLine("")
|
for e in module.enums:
|
||||||
|
deps[e.name] = @[]
|
||||||
|
for st in sliceTypes:
|
||||||
|
deps[st.name] = @[]
|
||||||
|
|
||||||
|
proc addDeps(node: string, t: Type) =
|
||||||
|
for dep in collectValueDeps(t):
|
||||||
|
if dep == node: continue
|
||||||
|
if localTypeNames.contains(dep) or sliceNames.contains(dep):
|
||||||
|
if dep notin deps[node]:
|
||||||
|
deps[node].add(dep)
|
||||||
|
|
||||||
|
for s in module.structs:
|
||||||
|
for f in s.fields:
|
||||||
|
addDeps(s.name, f.typ)
|
||||||
|
for e in module.enums:
|
||||||
|
for v in e.variants:
|
||||||
|
for ft in v.fields:
|
||||||
|
addDeps(e.name, ft)
|
||||||
|
for nf in v.namedFields:
|
||||||
|
addDeps(e.name, nf.typ)
|
||||||
|
|
||||||
|
# Topological sort (Kahn's algorithm).
|
||||||
|
var inDegree: Table[string, int]
|
||||||
|
var dependents: Table[string, seq[string]]
|
||||||
|
for node in deps.keys:
|
||||||
|
inDegree[node] = 0
|
||||||
|
for node, nodeDeps in deps:
|
||||||
|
for d in nodeDeps:
|
||||||
|
if not inDegree.hasKey(d): inDegree[d] = 0
|
||||||
|
inDegree[node] += 1
|
||||||
|
dependents.mgetOrPut(d, @[]).add(node)
|
||||||
|
|
||||||
|
var queue: seq[string] = @[]
|
||||||
|
for node, deg in inDegree:
|
||||||
|
if deg == 0:
|
||||||
|
queue.add(node)
|
||||||
|
|
||||||
|
var sorted: seq[string] = @[]
|
||||||
|
while queue.len > 0:
|
||||||
|
let node = queue.pop()
|
||||||
|
sorted.add(node)
|
||||||
|
for depNode in dependents.getOrDefault(node):
|
||||||
|
inDegree[depNode] -= 1
|
||||||
|
if inDegree[depNode] == 0:
|
||||||
|
queue.add(depNode)
|
||||||
|
|
||||||
|
if sorted.len < deps.len:
|
||||||
|
# Cycle detected; fall back to a safe deterministic order.
|
||||||
|
sorted = @[]
|
||||||
|
for s in module.structs: sorted.add(s.name)
|
||||||
|
for e in module.enums: sorted.add(e.name)
|
||||||
|
for st in sliceTypes: sorted.add(st.name)
|
||||||
|
|
||||||
|
# Map type names back to their definitions.
|
||||||
|
var structMap: Table[string, seq[tuple[name: string, typ: Type]]]
|
||||||
|
for s in module.structs: structMap[s.name] = s.fields
|
||||||
|
var enumMap: Table[string, seq[HirEnumVariant]]
|
||||||
|
for e in module.enums: enumMap[e.name] = e.variants
|
||||||
|
var sliceMap: Table[string, string]
|
||||||
|
for st in sliceTypes: sliceMap[st.name] = st.elem
|
||||||
|
|
||||||
|
# Emit type definitions in dependency order.
|
||||||
|
for name in sorted:
|
||||||
|
if structMap.hasKey(name):
|
||||||
|
be.emitStructDef(name, structMap[name])
|
||||||
|
elif enumMap.hasKey(name):
|
||||||
|
be.emitEnumDef(name, enumMap[name])
|
||||||
|
elif sliceMap.hasKey(name):
|
||||||
|
be.emitSliceTypeDef(name, sliceMap[name])
|
||||||
|
|
||||||
# Forward function declarations
|
# Forward function declarations
|
||||||
for f in module.funcs:
|
for f in module.funcs:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Each HIR node kind lowers to 1-20 LIR instructions.
|
## Each HIR node kind lowers to 1-20 LIR instructions.
|
||||||
|
|
||||||
import std/[strutils, strformat, tables, sequtils]
|
import std/[strutils, strformat, tables, sequtils]
|
||||||
import ast, types, token, hir, lir
|
import types, token, hir, lir
|
||||||
|
|
||||||
## Convert LirValue to C expression string (no % prefix)
|
## Convert LirValue to C expression string (no % prefix)
|
||||||
proc lirValToC(v: LirValue): string =
|
proc lirValToC(v: LirValue): string =
|
||||||
@@ -429,7 +429,6 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
|
|||||||
# ── SizeOf ──
|
# ── SizeOf ──
|
||||||
of hSizeOf:
|
of hSizeOf:
|
||||||
let ctype = typeToCStr(node.sizeOfType)
|
let ctype = typeToCStr(node.sizeOfType)
|
||||||
let t = b.freshTemp()
|
|
||||||
b.emit(LirInstr(kind: lirRawC, src: lirStr(&"/* sizeof({ctype}) */")))
|
b.emit(LirInstr(kind: lirRawC, src: lirStr(&"/* sizeof({ctype}) */")))
|
||||||
return lirVar(&"sizeof({ctype})")
|
return lirVar(&"sizeof({ctype})")
|
||||||
|
|
||||||
@@ -608,7 +607,6 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
|||||||
# ── While statement ──
|
# ── While statement ──
|
||||||
of hWhile:
|
of hWhile:
|
||||||
let startLbl = b.freshLabel("while")
|
let startLbl = b.freshLabel("while")
|
||||||
let bodyLbl = b.freshLabel("wbody")
|
|
||||||
let endLbl = b.freshLabel("wend")
|
let endLbl = b.freshLabel("wend")
|
||||||
|
|
||||||
ctx.loopStartLabels.add(startLbl.strVal)
|
ctx.loopStartLabels.add(startLbl.strVal)
|
||||||
@@ -750,9 +748,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
|||||||
for stmt in node.blockStmts:
|
for stmt in node.blockStmts:
|
||||||
lowerStmt(ctx, stmt)
|
lowerStmt(ctx, stmt)
|
||||||
if node.blockExpr != nil:
|
if node.blockExpr != nil:
|
||||||
let exprVal = lowerExpr(ctx, node.blockExpr)
|
# If block is an expression, result is unused at statement level
|
||||||
# If block is an expression, store result
|
discard lowerExpr(ctx, node.blockExpr)
|
||||||
discard
|
|
||||||
if node.isScope:
|
if node.isScope:
|
||||||
b.emitRawC("}")
|
b.emitRawC("}")
|
||||||
|
|
||||||
@@ -762,9 +759,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
|||||||
|
|
||||||
# ── Expression statement ──
|
# ── Expression statement ──
|
||||||
else:
|
else:
|
||||||
let exprVal = lowerExpr(ctx, node)
|
|
||||||
# Expression evaluated for side effects; temp is unused
|
# Expression evaluated for side effects; temp is unused
|
||||||
discard
|
discard lowerExpr(ctx, node)
|
||||||
|
|
||||||
# ── Module-level lowering ──
|
# ── Module-level lowering ──
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ proc parseInlineTable(s: string): OrderedTableRef[string, TomlValue] =
|
|||||||
if content[i] == '{': inc braceCount
|
if content[i] == '{': inc braceCount
|
||||||
elif content[i] == '}': dec braceCount
|
elif content[i] == '}': dec braceCount
|
||||||
inc i
|
inc i
|
||||||
let val = content[valStart ..< i]
|
|
||||||
result[key] = TomlValue(kind: tvkInlineTable, inlineVal: newOrderedTable[string, TomlValue]())
|
result[key] = TomlValue(kind: tvkInlineTable, inlineVal: newOrderedTable[string, TomlValue]())
|
||||||
else:
|
else:
|
||||||
while i < content.len and content[i] notin {',', ' ', '\t'}:
|
while i < content.len and content[i] notin {',', ' ', '\t'}:
|
||||||
|
|||||||
@@ -62,12 +62,6 @@ proc checkAny(p: Parser, kinds: openArray[TokenKind]): bool =
|
|||||||
if p.peek() == k: return true
|
if p.peek() == k: return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
proc match(p: var Parser, kind: TokenKind): bool =
|
|
||||||
if p.check(kind):
|
|
||||||
discard p.advance()
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
proc isTypeArgListAhead(p: Parser): bool =
|
proc isTypeArgListAhead(p: Parser): bool =
|
||||||
## Lookahead to determine if '<' starts a type argument list.
|
## Lookahead to determine if '<' starts a type argument list.
|
||||||
## Returns true if we can find a matching '>' before EOF, '{', or ';'.
|
## Returns true if we can find a matching '>' before EOF, '{', or ';'.
|
||||||
@@ -788,7 +782,6 @@ proc parseShift(p: var Parser): Expr =
|
|||||||
return left
|
return left
|
||||||
|
|
||||||
proc parseCast(p: var Parser): Expr =
|
proc parseCast(p: var Parser): Expr =
|
||||||
let loc = p.currentLoc
|
|
||||||
var left = p.parseShift()
|
var left = p.parseShift()
|
||||||
# 'as' and 'is' are handled in postfix for chaining
|
# 'as' and 'is' are handled in postfix for chaining
|
||||||
return left
|
return left
|
||||||
@@ -1171,7 +1164,6 @@ proc parseParamList(p: var Parser, allowVariadic: bool = false): seq[Param] =
|
|||||||
if p.check(tkRParen) or p.isAtEnd:
|
if p.check(tkRParen) or p.isAtEnd:
|
||||||
break
|
break
|
||||||
let loc = p.currentLoc
|
let loc = p.currentLoc
|
||||||
var isVar = false
|
|
||||||
if allowVariadic and p.check(tkDotDotDot):
|
if allowVariadic and p.check(tkDotDotDot):
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
let nameTok = p.at
|
let nameTok = p.at
|
||||||
|
|||||||
+4
-7
@@ -95,9 +95,6 @@ proc unescapeStringLiteral*(s: string): string =
|
|||||||
proc emitError(sema: var Sema, loc: SourceLocation, message: string) =
|
proc emitError(sema: var Sema, loc: SourceLocation, message: string) =
|
||||||
sema.diagnostics.add(SemaDiagnostic(severity: sdsError, loc: loc, message: message))
|
sema.diagnostics.add(SemaDiagnostic(severity: sdsError, loc: loc, message: message))
|
||||||
|
|
||||||
proc emitWarning(sema: var Sema, loc: SourceLocation, message: string) =
|
|
||||||
sema.diagnostics.add(SemaDiagnostic(severity: sdsWarning, loc: loc, message: message))
|
|
||||||
|
|
||||||
proc hasErrors*(res: SemaResult): bool =
|
proc hasErrors*(res: SemaResult): bool =
|
||||||
for d in res.diagnostics:
|
for d in res.diagnostics:
|
||||||
if d.severity == sdsError:
|
if d.severity == sdsError:
|
||||||
@@ -1348,12 +1345,12 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
|||||||
discard sema.checkExpr(expr.exprIsOperand, scope)
|
discard sema.checkExpr(expr.exprIsOperand, scope)
|
||||||
return makeBool()
|
return makeBool()
|
||||||
of ekTry:
|
of ekTry:
|
||||||
let operandType = sema.checkExpr(expr.exprTryOperand, scope)
|
discard sema.checkExpr(expr.exprTryOperand, scope)
|
||||||
# For now, assume Result<int, String> -> int
|
# For now, assume Result<int, String> -> int
|
||||||
# TODO: check operand is Result/Option and current function returns same type
|
# TODO: check operand is Result/Option and current function returns same type
|
||||||
return makeInt()
|
return makeInt()
|
||||||
of ekUnwrap:
|
of ekUnwrap:
|
||||||
let operandType = sema.checkExpr(expr.exprUnwrapOperand, scope)
|
discard sema.checkExpr(expr.exprUnwrapOperand, scope)
|
||||||
# Unwrap: extract Ok value or panic on Err
|
# Unwrap: extract Ok value or panic on Err
|
||||||
return makeInt()
|
return makeInt()
|
||||||
of ekBlock:
|
of ekBlock:
|
||||||
@@ -1363,7 +1360,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
|||||||
lastType = sema.checkStmt(stmt, blockScope)
|
lastType = sema.checkStmt(stmt, blockScope)
|
||||||
return lastType
|
return lastType
|
||||||
of ekMatch:
|
of ekMatch:
|
||||||
let subjectType = sema.checkExpr(expr.exprMatchSubject, scope)
|
discard sema.checkExpr(expr.exprMatchSubject, scope)
|
||||||
var resultType = makeUnknown()
|
var resultType = makeUnknown()
|
||||||
for arm in expr.exprMatchArms:
|
for arm in expr.exprMatchArms:
|
||||||
var armScope = newScope(scope)
|
var armScope = newScope(scope)
|
||||||
@@ -1398,7 +1395,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
|||||||
expr.exprSpawnAsync = true
|
expr.exprSpawnAsync = true
|
||||||
return makePointer(makeVoid())
|
return makePointer(makeVoid())
|
||||||
of ekAwait:
|
of ekAwait:
|
||||||
let operand = sema.checkExpr(expr.exprAwaitOperand, scope)
|
discard sema.checkExpr(expr.exprAwaitOperand, scope)
|
||||||
# await on a task handle returns *void (result pointer)
|
# await on a task handle returns *void (result pointer)
|
||||||
return makePointer(makeVoid())
|
return makePointer(makeVoid())
|
||||||
of ekBorrow:
|
of ekBorrow:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
|||||||
|
# Nexus HTTP Server Rewrite — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-06-14
|
||||||
|
**Goal:** Rewrite `apps/nexus` as a production-ready, modular HTTP/1.1 server that exercises modern Bux language features.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Scope
|
||||||
|
|
||||||
|
Keep the same observable behavior as the current Nexus app, but restructure it for production quality:
|
||||||
|
|
||||||
|
- HTTP/1.1 request parsing
|
||||||
|
- Static file serving from `./public/`
|
||||||
|
- API endpoints: `GET /api/health`, `GET /api/info`
|
||||||
|
- WebSocket upgrade detection on `/ws`
|
||||||
|
- Multi-threaded request handling
|
||||||
|
|
||||||
|
What changes:
|
||||||
|
- Monolithic `Main.bux` → modular source tree
|
||||||
|
- Function-pointer dispatch → `interface`-based handlers and middleware
|
||||||
|
- Ad-hoc error handling → algebraic enum `Result`/`Option` patterns
|
||||||
|
- Raw pointer buffer parsing → `@[Checked]` borrow-checked parsing
|
||||||
|
- Direct worker spawn → thread-pool backed by `Channel<ConnectionTask>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Module Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/nexus/
|
||||||
|
├── bux.toml
|
||||||
|
├── README.md
|
||||||
|
├── public/
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── 404.html
|
||||||
|
└── src/
|
||||||
|
├── Main.bux # Entry point: config, socket setup, spawn workers, run acceptor
|
||||||
|
├── Config.bux # ServerConfig struct + CTFE defaults
|
||||||
|
├── Errors.bux # HttpError enum + Result/Option helpers
|
||||||
|
├── Http.bux # HttpMethod enum, Request/Response structs, status text, MIME
|
||||||
|
├── Parser.bux # @[Checked] HTTP/1.1 request parser
|
||||||
|
├── Router.bux # Route, Router, HttpHandler interface
|
||||||
|
├── Middleware.bux # Middleware interface + LoggingMiddleware
|
||||||
|
├── Handlers.bux # Static files, API, WebSocket upgrade, 404
|
||||||
|
└── Server.bux # ConnectionTask, Worker, acceptor loop, channel plumbing
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Language Features to Exercise
|
||||||
|
|
||||||
|
| Feature | Where |
|
||||||
|
|---------|-------|
|
||||||
|
| Modules & `pub`/`private` | Every file |
|
||||||
|
| Algebraic enums | `HttpMethod`, `HttpError`, `ParseResult` |
|
||||||
|
| Pattern matching (`match`) | Error dispatch in `Main`, method parsing |
|
||||||
|
| Structs + `extend` methods | `HttpRequest`, `HttpResponse`, `Router`, `ServerConfig` |
|
||||||
|
| `interface` + `extend ... for` | `HttpHandler`, `Middleware` |
|
||||||
|
| Generics | `Channel<ConnectionTask>` |
|
||||||
|
| `for ... in` loops | Header iteration, route dispatch (over `Array<HeaderEntry>` and `Array<Route>`) |
|
||||||
|
| `Array<T>` generic container | Header storage, route table |
|
||||||
|
| `@[Checked]` + `&` / `&mut` | `Parser.bux` string buffer access |
|
||||||
|
| `Result` + `?` operator | Parser fallible operations |
|
||||||
|
| `const func` / CTFE | Default config values |
|
||||||
|
| `spawn` + `Channel<T>` | Thread-pool in `Server.bux` |
|
||||||
|
| Raw multi-line / interpolated strings | Response templates, logs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Core Types
|
||||||
|
|
||||||
|
### 4.1 HTTP Types (`Http.bux`)
|
||||||
|
|
||||||
|
```bux
|
||||||
|
pub enum HttpMethod {
|
||||||
|
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, UNKNOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum HttpError {
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
MethodNotAllowed,
|
||||||
|
InternalError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeaderEntry {
|
||||||
|
key: String;
|
||||||
|
value: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpRequest {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: String;
|
||||||
|
version: String;
|
||||||
|
body: String;
|
||||||
|
headers: Array<HeaderEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpResponse {
|
||||||
|
statusCode: int;
|
||||||
|
contentType: String;
|
||||||
|
body: String;
|
||||||
|
extraHeaders: String;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Result Types (`Errors.bux`)
|
||||||
|
|
||||||
|
Bux's `Std::Result` only carries `int` in `Ok`, so Nexus defines domain-specific enums:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
pub enum ParseResult {
|
||||||
|
Ok(HttpRequest),
|
||||||
|
Err(HttpError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FileResult {
|
||||||
|
Ok(String),
|
||||||
|
Err(HttpError),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Helpers:
|
||||||
|
- `ParseResult_IsOk`, `ParseResult_UnwrapOr`
|
||||||
|
- `FileResult_IsOk`, `FileResult_UnwrapOr`
|
||||||
|
|
||||||
|
If the bootstrap `?` operator only recognizes `Result`/`Option` with exact variant names, parser code will fall back to explicit `match`.
|
||||||
|
|
||||||
|
### 4.3 Handler & Middleware Interfaces
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Router.bux
|
||||||
|
pub interface HttpHandler {
|
||||||
|
func Handle(self: &Self, req: &HttpRequest) -> HttpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Handler {
|
||||||
|
StaticFile,
|
||||||
|
ApiHealth,
|
||||||
|
ApiInfo,
|
||||||
|
WsUpgrade,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Handler for HttpHandler {
|
||||||
|
func Handle(self: &Handler, req: &HttpRequest) -> HttpResponse {
|
||||||
|
match self {
|
||||||
|
Handler::StaticFile => return ServeStaticFile(req),
|
||||||
|
Handler::ApiHealth => return HandleApiHealth(),
|
||||||
|
Handler::ApiInfo => return HandleApiInfo(),
|
||||||
|
Handler::WsUpgrade => return HandleWebSocketUpgrade(req),
|
||||||
|
Handler::NotFound => return NotFoundResponse(),
|
||||||
|
}
|
||||||
|
return NotFoundResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Route {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: String;
|
||||||
|
handler: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Router {
|
||||||
|
routes: Array<Route>;
|
||||||
|
notFound: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware.bux
|
||||||
|
pub interface Middleware {
|
||||||
|
func Process(self: &Self, req: &HttpRequest, next: &HttpHandler) -> HttpResponse;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bux interfaces currently support static dispatch through generics, not dynamic `*Handler` pointers. The route table stores the concrete `Handler` enum and dispatches via `match` (showcasing algebraic enums + pattern matching). Middleware uses the interface with generic bounds (`T: HttpHandler`) for static composition.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Data Flow
|
||||||
|
|
||||||
|
1. `Main` reads `ServerConfig` and creates a listening socket.
|
||||||
|
2. `Main` spawns `workerCount - 1` worker threads; the main thread becomes the last worker.
|
||||||
|
3. `Main` spawns one dedicated acceptor thread that loops on `Net_Accept` and pushes `ConnectionTask { fd }` into `Channel<ConnectionTask>`.
|
||||||
|
4. Each worker loops:
|
||||||
|
```
|
||||||
|
task = Channel_Recv(&taskQueue)
|
||||||
|
HandleConnection(task.fd, &appHandler)
|
||||||
|
Net_Close(task.fd)
|
||||||
|
```
|
||||||
|
5. `HandleConnection`:
|
||||||
|
- `Net_Recv` raw request
|
||||||
|
- `ParseRequest(raw)` → `ParseResult` (matched explicitly; `?` is used only for `Result`/int payloads)
|
||||||
|
- On success, `Router.Dispatch(&req)` or middleware chain
|
||||||
|
- `Http_BuildResponse(resp)` + `Net_Send`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Parser Design (`Parser.bux`)
|
||||||
|
|
||||||
|
Parser functions take the raw request by value (`String` is already a C-string reference) and build an `HttpRequest`:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
pub func ParseRequest(raw: String) -> ParseResult {
|
||||||
|
match FindToken(raw, " ") {
|
||||||
|
ParseResult::Ok(end) => {
|
||||||
|
let methodStr: String = Slice(raw, 0, end);
|
||||||
|
let method: HttpMethod = ParseMethod(methodStr);
|
||||||
|
// ... path, version, headers, body
|
||||||
|
return ParseResult_NewOk(req);
|
||||||
|
}
|
||||||
|
ParseResult::Err(e) => return ParseResult_NewErr(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`@[Checked]` is used where mutable borrows make sense (e.g. `SetResponseCode(resp: &mut HttpResponse, code: int)`), not on raw string parsing.
|
||||||
|
|
||||||
|
Headers are stored in a dynamically grown `Array<HeaderEntry>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Router & Middleware
|
||||||
|
|
||||||
|
### 7.1 Router Dispatch
|
||||||
|
|
||||||
|
```bux
|
||||||
|
extend Router {
|
||||||
|
pub func Dispatch(self: *Router, req: &HttpRequest) -> HttpResponse {
|
||||||
|
for route in self.routes {
|
||||||
|
if route.method == req.method && String_Eq(route.path, req.path) {
|
||||||
|
return route.handler.Handle(req); // Handler enum implements HttpHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.notFound.Handle(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Middleware Chain
|
||||||
|
|
||||||
|
`LoggingMiddleware` logs method/path and delegates to `next`:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
pub struct LoggingMiddleware {}
|
||||||
|
|
||||||
|
extend LoggingMiddleware for Middleware {
|
||||||
|
func Process<T: HttpHandler>(self: *LoggingMiddleware, req: &HttpRequest, next: &T) -> HttpResponse {
|
||||||
|
Print(Http_MethodName(req.method));
|
||||||
|
Print(" ");
|
||||||
|
PrintLine(req.path);
|
||||||
|
return next.Handle(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaining is achieved with a generic `MiddlewareHandler<M: Middleware, T: HttpHandler>` struct that implements `HttpHandler` and holds `(middleware, next)`. This uses Bux's static-dispatch interfaces (no vtable pointers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Handlers (`Handlers.bux`)
|
||||||
|
|
||||||
|
| Handler | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `StaticFileHandler` | Serve files from `public/` with MIME sniffing and path-traversal guard |
|
||||||
|
| `HealthHandler` | `GET /api/health` → JSON status |
|
||||||
|
| `InfoHandler` | `GET /api/info` → JSON server info |
|
||||||
|
| `WsUpgradeHandler` | `GET /ws` or `Upgrade: websocket` → 101 response with placeholder accept key |
|
||||||
|
| `NotFoundHandler` | 404 JSON/text fallback |
|
||||||
|
|
||||||
|
All implement `HttpHandler`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Server & Thread Pool (`Server.bux`)
|
||||||
|
|
||||||
|
```bux
|
||||||
|
pub struct ConnectionTask {
|
||||||
|
fd: int;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
config: ServerConfig;
|
||||||
|
taskQueue: Channel<ConnectionTask>;
|
||||||
|
handler: *HttpHandler;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. `Server_Run(&server)` creates socket, binds, listens.
|
||||||
|
2. Spawns workers; each calls `Worker_Run(&server)`.
|
||||||
|
3. `Acceptor_Run(&server)` loops:
|
||||||
|
```bux
|
||||||
|
let fd: int = Net_Accept(serverFd);
|
||||||
|
if fd >= 0 {
|
||||||
|
let task: ConnectionTask = ConnectionTask { fd: fd };
|
||||||
|
Channel_Send<ConnectionTask>(&server.taskQueue, task);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. `Worker_Run` pops tasks and calls `HandleConnection(task.fd, server.handler)`.
|
||||||
|
|
||||||
|
Socket fd ownership is always with the worker that received the task; it is closed before the worker loops again.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Config (`Config.bux`)
|
||||||
|
|
||||||
|
```bux
|
||||||
|
pub struct ServerConfig {
|
||||||
|
bindAddr: String;
|
||||||
|
port: int;
|
||||||
|
workerCount: int;
|
||||||
|
publicDir: String;
|
||||||
|
backlog: int;
|
||||||
|
}
|
||||||
|
|
||||||
|
const func DefaultConfig() -> ServerConfig {
|
||||||
|
return ServerConfig {
|
||||||
|
bindAddr: "0.0.0.0",
|
||||||
|
port: 8080,
|
||||||
|
workerCount: 4,
|
||||||
|
publicDir: "public",
|
||||||
|
backlog: 128,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Testing & Verification
|
||||||
|
|
||||||
|
- `buxc check` must pass on the whole package.
|
||||||
|
- `buxc run` starts the server; verify with:
|
||||||
|
- `curl http://localhost:8080/`
|
||||||
|
- `curl http://localhost:8080/api/health`
|
||||||
|
- `curl http://localhost:8080/api/info`
|
||||||
|
- `curl -i -N -H "Upgrade: websocket" -H "Connection: Upgrade" http://localhost:8080/ws`
|
||||||
|
- Existing integration test surface (`buxc check` on `apps/nexus`) remains the gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Out of Scope
|
||||||
|
|
||||||
|
- Real HTTP/2 frame parsing or WebSocket frame parsing (keep detection/upgrade only, same as current).
|
||||||
|
- TLS/HTTPS.
|
||||||
|
- Full SHA-1/Base64 WebSocket accept key (keep placeholder).
|
||||||
|
- Generic `Result<T, E>` (Bux `Std::Result` is `Ok(int)` only; use domain enums instead).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Risks & Mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|------------|
|
||||||
|
| `interface` vtables with generic structs | Avoid generic interfaces; use concrete `*HttpHandler` pointers. |
|
||||||
|
| `Channel<T>` with non-trivial `T` | `ConnectionTask` contains only `int`; safe to copy. |
|
||||||
|
| `?` operator on custom enums | Fall back to explicit `match` if needed. |
|
||||||
|
| `@[Checked]` borrow checker on raw `String` | Use `&String` for read-only parsing; avoid `&mut` unless mutation is required. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Success Criteria
|
||||||
|
|
||||||
|
1. `buxc check apps/nexus` passes with zero errors.
|
||||||
|
2. `buxc run` in `apps/nexus` serves static files and API endpoints correctly.
|
||||||
|
3. The new code uses at minimum: modules, algebraic enums, pattern matching, interfaces, `extend`, generics, `for ... in`, `Channel<T>`, `spawn`, `Result`/`Option`, `@[Checked]`.
|
||||||
@@ -11,6 +11,7 @@ void PrintLine(const char* s) {
|
|||||||
} else {
|
} else {
|
||||||
puts("");
|
puts("");
|
||||||
}
|
}
|
||||||
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Print - print string without newline */
|
/* Print - print string without newline */
|
||||||
@@ -18,6 +19,7 @@ void Print(const char* s) {
|
|||||||
if (s != NULL) {
|
if (s != NULL) {
|
||||||
printf("%s", s);
|
printf("%s", s);
|
||||||
}
|
}
|
||||||
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PrintInt - print integer */
|
/* PrintInt - print integer */
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
import std/[unittest, os, strutils]
|
import std/[unittest, strutils]
|
||||||
import ../bootstrap/[lexer, parser, sema, types]
|
import ../bootstrap/[lexer, parser, sema]
|
||||||
|
|
||||||
proc checkSource(source: string): tuple[hasErrors: bool, diagnostics: seq[SemaDiagnostic]] =
|
proc checkSource(source: string): tuple[hasErrors: bool, diagnostics: seq[SemaDiagnostic]] =
|
||||||
let lexRes = lexer.tokenize(source, "test.bux")
|
let lexRes = lexer.tokenize(source, "test.bux")
|
||||||
|
|||||||
+2635
-2636
File diff suppressed because it is too large
Load Diff
+2625
-2626
File diff suppressed because it is too large
Load Diff
+2633
-2634
File diff suppressed because it is too large
Load Diff
+2613
-2614
File diff suppressed because it is too large
Load Diff
+2601
-2602
File diff suppressed because it is too large
Load Diff
+2633
-2634
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "modern_features"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push};
|
||||||
|
|
||||||
|
struct Record {
|
||||||
|
name: String;
|
||||||
|
value: int;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Box {
|
||||||
|
items: Array<Record>;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Payload {
|
||||||
|
Ok(Record),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Color { Red, Green }
|
||||||
|
|
||||||
|
func Name(c: Color) -> String {
|
||||||
|
match c {
|
||||||
|
Color::Red => "red",
|
||||||
|
Color::Green => "green",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var box: Box;
|
||||||
|
box.items = Array_New<Record>(4);
|
||||||
|
Array_Push<Record>(&box.items, Record { name: "x", value: 10 });
|
||||||
|
|
||||||
|
for it in box.items {
|
||||||
|
if it.value == 10 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r: Payload;
|
||||||
|
r.tag = Payload_Ok;
|
||||||
|
r.data.Ok_0 = Record { name: "y", value: 20 };
|
||||||
|
if r.data.Ok_0.value == 20 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2623
-2624
File diff suppressed because it is too large
Load Diff
+2632
-2633
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import std/[unittest, strformat]
|
import std/[unittest]
|
||||||
import ../bootstrap/[lexer, parser, sema, hir, hir_lower, types, scope]
|
import ../bootstrap/[lexer, parser, sema, hir, hir_lower, scope]
|
||||||
|
|
||||||
proc lowerSource(source: string): HirModule =
|
proc lowerSource(source: string): HirModule =
|
||||||
let lexRes = tokenize(source, "<test>")
|
let lexRes = tokenize(source, "<test>")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import std/[unittest, os, strutils]
|
import std/[unittest, strutils]
|
||||||
import ../bootstrap/[lexer, token, source_location]
|
import ../bootstrap/[lexer, token]
|
||||||
|
|
||||||
proc tokenKinds(source: string): seq[TokenKind] =
|
proc tokenKinds(source: string): seq[TokenKind] =
|
||||||
let res = tokenize(source, "<test>")
|
let res = tokenize(source, "<test>")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import std/[unittest, os, strutils]
|
import std/[unittest, os]
|
||||||
import ../bootstrap/[lexer, parser, ast, token]
|
import ../bootstrap/[lexer, parser, ast, token]
|
||||||
|
|
||||||
proc parseSource(source: string): ParseResult =
|
proc parseSource(source: string): ParseResult =
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import std/[unittest, strutils]
|
import std/[unittest, strutils]
|
||||||
import ../bootstrap/[lexer, parser, sema, types]
|
import ../bootstrap/[lexer, parser, sema]
|
||||||
|
|
||||||
proc checkSource(source: string): SemaResult =
|
proc checkSource(source: string): SemaResult =
|
||||||
let lexRes = tokenize(source, "<test>")
|
let lexRes = tokenize(source, "<test>")
|
||||||
|
|||||||
Reference in New Issue
Block a user