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/sema_test
|
||||
tests/hir_test
|
||||
tests/borrow_test
|
||||
tests/test_generics_parse
|
||||
tests/*.exe
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ selfhost: build
|
||||
|
||||
.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
|
||||
@echo "=== Golden tests ==="
|
||||
|
||||
@@ -1,129 +1 @@
|
||||
<!DOCTYPE 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>
|
||||
<html><body>Hello Nexus</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 {
|
||||
|
||||
// =============================================================================
|
||||
// Imports — only what we actually use
|
||||
// =============================================================================
|
||||
import Std::Io::{PrintLine, Print, 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_Eq, String_StartsWith, String_EndsWith, String_Contains, String_Trim, String_FromInt, String_Offset};
|
||||
import Std::Task::{Task_Join, TaskHandle};
|
||||
import Config::{ServerConfig, DefaultConfig};
|
||||
import Http::{HttpMethod};
|
||||
import Router::{Handler, Route, Router};
|
||||
import Server::{RunServer};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
|
||||
// =============================================================================
|
||||
// Extern Functions (from C runtime — used directly for performance)
|
||||
// =============================================================================
|
||||
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;
|
||||
func BuildRouter() -> Router {
|
||||
var routes: Array<Route> = Array_New<Route>(8);
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Method Enum (simple enum — no data payload)
|
||||
// =============================================================================
|
||||
enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
UNKNOWN,
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/health",
|
||||
handler: Handler { tag: Handler_ApiHealth },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/info",
|
||||
handler: Handler { tag: Handler_ApiInfo },
|
||||
});
|
||||
|
||||
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 {
|
||||
PrintLine("================================================");
|
||||
PrintLine(" Nexus HTTP Server v0.1.0");
|
||||
PrintLine(" Multi-threaded HTTP/1.1 + HTTP/2 & WS detect");
|
||||
PrintLine(" Built with Bux");
|
||||
PrintLine("================================================");
|
||||
PrintLine("");
|
||||
let config: ServerConfig = DefaultConfig();
|
||||
let router: Router = BuildRouter();
|
||||
|
||||
// Create socket
|
||||
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;
|
||||
return RunServer(config, router);
|
||||
}
|
||||
|
||||
} // 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 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
|
||||
ColorMode* = enum
|
||||
|
||||
+57
-7
@@ -42,6 +42,14 @@ proc flushPending(ctx: var LowerCtx, node: HirNode): HirNode =
|
||||
return hirBlock(stmts, nil, makeVoid(), node.loc)
|
||||
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 =
|
||||
# Lower match expression to a block with if-else chain.
|
||||
# For now, supports enum tag matching and wildcard/ident fallbacks.
|
||||
@@ -51,6 +59,13 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
|
||||
# Allocate result variable
|
||||
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)
|
||||
var ifChain: HirNode = nil
|
||||
|
||||
@@ -66,12 +81,18 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
|
||||
let variantName = path[^1]
|
||||
let tagName = enumName & "_" & variantName
|
||||
|
||||
# condition: subject.tag == EnumName_VariantName
|
||||
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
|
||||
typ: makePointer(makeNamed(enumName & "_Tag")), loc: 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)
|
||||
let cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
|
||||
var cond: HirNode
|
||||
if subjectHasData and enumName == subjectEnumName:
|
||||
# Algebraic enum: compare subject.tag
|
||||
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
|
||||
typ: makePointer(makeNamed(enumName & "_Tag")), loc: 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
|
||||
var armStmts: seq[HirNode] = @[]
|
||||
@@ -397,6 +418,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if f.ftype != nil:
|
||||
case f.ftype.kind
|
||||
of tekNamed:
|
||||
if f.ftype.typeArgs.len > 0:
|
||||
return substituteType(ctx, f.ftype, subst)
|
||||
case f.ftype.typeName
|
||||
of "int", "int32", "int64": return makeInt()
|
||||
of "float64": return makeFloat64()
|
||||
@@ -417,6 +440,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if f.ftype != nil:
|
||||
case f.ftype.kind
|
||||
of tekNamed:
|
||||
if f.ftype.typeArgs.len > 0:
|
||||
return ctx.resolveTypeExpr(f.ftype)
|
||||
case f.ftype.typeName
|
||||
of "int", "int32", "int64": return makeInt()
|
||||
of "float64": return makeFloat64()
|
||||
@@ -478,6 +503,10 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
|
||||
if baseType.isPointer and baseType.inner.len > 0:
|
||||
return baseType.inner[0]
|
||||
return makeUnknown()
|
||||
of ekMatch:
|
||||
if expr.exprMatchArms.len > 0:
|
||||
return ctx.resolveExprType(expr.exprMatchArms[0].body)
|
||||
return makeUnknown()
|
||||
of ekBlock:
|
||||
if expr.exprBlock.stmts.len > 0:
|
||||
let last = expr.exprBlock.stmts[^1]
|
||||
@@ -524,6 +553,23 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
|
||||
return te.typeArgs[0]
|
||||
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 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:
|
||||
discard
|
||||
let t = ctx.resolveExprType(expr)
|
||||
@@ -531,6 +577,11 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
|
||||
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:
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
|
||||
let loc = expr.loc
|
||||
let name = "__closure_" & $ctx.varCounter
|
||||
inc ctx.varCounter
|
||||
var f = HirFunc(name: name, isPublic: false)
|
||||
|
||||
@@ -61,12 +61,6 @@ proc advance(lex: var Lexer): char =
|
||||
else:
|
||||
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 =
|
||||
for i, c in s:
|
||||
if lex.peek(i) != c:
|
||||
@@ -81,9 +75,6 @@ proc currentLocation(lex: Lexer): SourceLocation =
|
||||
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
|
||||
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 =
|
||||
let text = lex.source[startPos ..< lex.pos]
|
||||
result = Token(kind: kind, text: text, loc: startLoc)
|
||||
@@ -337,19 +328,6 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
|
||||
else:
|
||||
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
|
||||
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
|
||||
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
## Linear 3-address code IR, designed for straightforward C emission.
|
||||
## Each HIR construct lowers to 5-30 LIR instructions.
|
||||
|
||||
import types
|
||||
|
||||
type
|
||||
LirKind* = enum
|
||||
# ── Data movement ──
|
||||
|
||||
+119
-44
@@ -17,9 +17,6 @@ proc initLirCBackend*(): LirCBackend =
|
||||
tempTypes: initTable[string, string](),
|
||||
)
|
||||
|
||||
proc emit(be: var LirCBackend, s: string) =
|
||||
be.output.add(s)
|
||||
|
||||
proc emitIndent(be: var LirCBackend) =
|
||||
for i in 0 ..< be.indent:
|
||||
be.output.add(" ")
|
||||
@@ -43,21 +40,6 @@ proc valToC(be: var LirCBackend, v: LirValue): string =
|
||||
of lvkField: 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 =
|
||||
## Emit a C parameter declaration, handling function-pointer syntax.
|
||||
if cType.contains("(*)"):
|
||||
@@ -487,6 +469,26 @@ proc emitEnumDef(be: var LirCBackend, name: string, variants: seq[HirEnumVariant
|
||||
be.emitLine(&"}} {name};")
|
||||
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 ──
|
||||
|
||||
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
|
||||
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:
|
||||
be.emitEnumDef(e.name, e.variants)
|
||||
if module.enums.len > 0:
|
||||
be.emitLine("")
|
||||
localTypeNames.incl(e.name)
|
||||
|
||||
# Struct definitions
|
||||
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
|
||||
# Collect slice types used in struct fields and enum payloads.
|
||||
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:
|
||||
structNames.incl(s.name)
|
||||
for f in module.funcs:
|
||||
for p in f.params:
|
||||
var ct = typeToCStr(p.typ)
|
||||
# Strip pointer/reference suffix to find the base slice type.
|
||||
while ct.endsWith("*"):
|
||||
ct = ct[0..^2]
|
||||
if ct.startsWith("Slice_"):
|
||||
let elem = ct[6 .. ^1]
|
||||
if not sliceTypes.anyIt(it.name == ct) and not structNames.contains(ct):
|
||||
sliceTypes.add((ct, elem))
|
||||
if sliceTypes.len > 0:
|
||||
for st in sliceTypes:
|
||||
be.emitLine(&"typedef struct {{ {st.elem}* data; size_t len; }} {st.name};")
|
||||
be.emitLine("")
|
||||
for f in s.fields:
|
||||
registerSlice(f.typ)
|
||||
for e in module.enums:
|
||||
for v in e.variants:
|
||||
for ft in v.fields:
|
||||
registerSlice(ft)
|
||||
for nf in v.namedFields:
|
||||
registerSlice(nf.typ)
|
||||
|
||||
# Build dependency graph among structs, enums, and slice types.
|
||||
# Edge A -> B means "A depends on B, so B must be emitted before A".
|
||||
var deps: Table[string, seq[string]]
|
||||
for s in module.structs:
|
||||
deps[s.name] = @[]
|
||||
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
|
||||
for f in module.funcs:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Each HIR node kind lowers to 1-20 LIR instructions.
|
||||
|
||||
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)
|
||||
proc lirValToC(v: LirValue): string =
|
||||
@@ -429,7 +429,6 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
|
||||
# ── SizeOf ──
|
||||
of hSizeOf:
|
||||
let ctype = typeToCStr(node.sizeOfType)
|
||||
let t = b.freshTemp()
|
||||
b.emit(LirInstr(kind: lirRawC, src: lirStr(&"/* sizeof({ctype}) */")))
|
||||
return lirVar(&"sizeof({ctype})")
|
||||
|
||||
@@ -608,7 +607,6 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
# ── While statement ──
|
||||
of hWhile:
|
||||
let startLbl = b.freshLabel("while")
|
||||
let bodyLbl = b.freshLabel("wbody")
|
||||
let endLbl = b.freshLabel("wend")
|
||||
|
||||
ctx.loopStartLabels.add(startLbl.strVal)
|
||||
@@ -750,9 +748,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
for stmt in node.blockStmts:
|
||||
lowerStmt(ctx, stmt)
|
||||
if node.blockExpr != nil:
|
||||
let exprVal = lowerExpr(ctx, node.blockExpr)
|
||||
# If block is an expression, store result
|
||||
discard
|
||||
# If block is an expression, result is unused at statement level
|
||||
discard lowerExpr(ctx, node.blockExpr)
|
||||
if node.isScope:
|
||||
b.emitRawC("}")
|
||||
|
||||
@@ -762,9 +759,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
|
||||
|
||||
# ── Expression statement ──
|
||||
else:
|
||||
let exprVal = lowerExpr(ctx, node)
|
||||
# Expression evaluated for side effects; temp is unused
|
||||
discard
|
||||
discard lowerExpr(ctx, node)
|
||||
|
||||
# ── Module-level lowering ──
|
||||
|
||||
|
||||
@@ -90,7 +90,6 @@ proc parseInlineTable(s: string): OrderedTableRef[string, TomlValue] =
|
||||
if content[i] == '{': inc braceCount
|
||||
elif content[i] == '}': dec braceCount
|
||||
inc i
|
||||
let val = content[valStart ..< i]
|
||||
result[key] = TomlValue(kind: tvkInlineTable, inlineVal: newOrderedTable[string, TomlValue]())
|
||||
else:
|
||||
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
|
||||
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 =
|
||||
## Lookahead to determine if '<' starts a type argument list.
|
||||
## Returns true if we can find a matching '>' before EOF, '{', or ';'.
|
||||
@@ -788,7 +782,6 @@ proc parseShift(p: var Parser): Expr =
|
||||
return left
|
||||
|
||||
proc parseCast(p: var Parser): Expr =
|
||||
let loc = p.currentLoc
|
||||
var left = p.parseShift()
|
||||
# 'as' and 'is' are handled in postfix for chaining
|
||||
return left
|
||||
@@ -1171,7 +1164,6 @@ proc parseParamList(p: var Parser, allowVariadic: bool = false): seq[Param] =
|
||||
if p.check(tkRParen) or p.isAtEnd:
|
||||
break
|
||||
let loc = p.currentLoc
|
||||
var isVar = false
|
||||
if allowVariadic and p.check(tkDotDotDot):
|
||||
discard p.advance()
|
||||
let nameTok = p.at
|
||||
|
||||
+4
-7
@@ -95,9 +95,6 @@ proc unescapeStringLiteral*(s: string): string =
|
||||
proc emitError(sema: var Sema, loc: SourceLocation, message: string) =
|
||||
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 =
|
||||
for d in res.diagnostics:
|
||||
if d.severity == sdsError:
|
||||
@@ -1348,12 +1345,12 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
discard sema.checkExpr(expr.exprIsOperand, scope)
|
||||
return makeBool()
|
||||
of ekTry:
|
||||
let operandType = sema.checkExpr(expr.exprTryOperand, scope)
|
||||
discard sema.checkExpr(expr.exprTryOperand, scope)
|
||||
# For now, assume Result<int, String> -> int
|
||||
# TODO: check operand is Result/Option and current function returns same type
|
||||
return makeInt()
|
||||
of ekUnwrap:
|
||||
let operandType = sema.checkExpr(expr.exprUnwrapOperand, scope)
|
||||
discard sema.checkExpr(expr.exprUnwrapOperand, scope)
|
||||
# Unwrap: extract Ok value or panic on Err
|
||||
return makeInt()
|
||||
of ekBlock:
|
||||
@@ -1363,7 +1360,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
lastType = sema.checkStmt(stmt, blockScope)
|
||||
return lastType
|
||||
of ekMatch:
|
||||
let subjectType = sema.checkExpr(expr.exprMatchSubject, scope)
|
||||
discard sema.checkExpr(expr.exprMatchSubject, scope)
|
||||
var resultType = makeUnknown()
|
||||
for arm in expr.exprMatchArms:
|
||||
var armScope = newScope(scope)
|
||||
@@ -1398,7 +1395,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
expr.exprSpawnAsync = true
|
||||
return makePointer(makeVoid())
|
||||
of ekAwait:
|
||||
let operand = sema.checkExpr(expr.exprAwaitOperand, scope)
|
||||
discard sema.checkExpr(expr.exprAwaitOperand, scope)
|
||||
# await on a task handle returns *void (result pointer)
|
||||
return makePointer(makeVoid())
|
||||
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 {
|
||||
puts("");
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/* Print - print string without newline */
|
||||
@@ -18,6 +19,7 @@ void Print(const char* s) {
|
||||
if (s != NULL) {
|
||||
printf("%s", s);
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/* PrintInt - print integer */
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
import std/[unittest, os, strutils]
|
||||
import ../bootstrap/[lexer, parser, sema, types]
|
||||
import std/[unittest, strutils]
|
||||
import ../bootstrap/[lexer, parser, sema]
|
||||
|
||||
proc checkSource(source: string): tuple[hasErrors: bool, diagnostics: seq[SemaDiagnostic]] =
|
||||
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 ../bootstrap/[lexer, parser, sema, hir, hir_lower, types, scope]
|
||||
import std/[unittest]
|
||||
import ../bootstrap/[lexer, parser, sema, hir, hir_lower, scope]
|
||||
|
||||
proc lowerSource(source: string): HirModule =
|
||||
let lexRes = tokenize(source, "<test>")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import std/[unittest, os, strutils]
|
||||
import ../bootstrap/[lexer, token, source_location]
|
||||
import std/[unittest, strutils]
|
||||
import ../bootstrap/[lexer, token]
|
||||
|
||||
proc tokenKinds(source: string): seq[TokenKind] =
|
||||
let res = tokenize(source, "<test>")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import std/[unittest, os, strutils]
|
||||
import std/[unittest, os]
|
||||
import ../bootstrap/[lexer, parser, ast, token]
|
||||
|
||||
proc parseSource(source: string): ParseResult =
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import std/[unittest, strutils]
|
||||
import ../bootstrap/[lexer, parser, sema, types]
|
||||
import ../bootstrap/[lexer, parser, sema]
|
||||
|
||||
proc checkSource(source: string): SemaResult =
|
||||
let lexRes = tokenize(source, "<test>")
|
||||
|
||||
Reference in New Issue
Block a user