From 2a2c6d2760de92fbe93ddce369abbfdaa3be188c Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Sat, 14 Feb 2026 12:14:06 -0500 Subject: [PATCH] feat: add Http effect to C backend Implement HTTP client using POSIX sockets: - Http.get(url) - GET request - Http.post(url, body) - POST request - Http.put(url, body) - PUT request - Http.delete(url) - DELETE request Features: - Self-contained implementation (no libcurl dependency) - URL parsing for host, port, and path - HTTP/1.1 protocol with Connection: close - Response body extraction All Http operations use evidence passing for handler customization. Co-Authored-By: Claude Opus 4.5 --- docs/C_BACKEND.md | 8 +- src/codegen/c_backend.rs | 195 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 200 insertions(+), 3 deletions(-) diff --git a/docs/C_BACKEND.md b/docs/C_BACKEND.md index fc05e08..5767780 100644 --- a/docs/C_BACKEND.md +++ b/docs/C_BACKEND.md @@ -301,11 +301,15 @@ Implemented C versions of: All effects use evidence passing for handler customization. -### Phase 3: Http Effect +### Phase 3: Http Effect ✅ COMPLETE -Implement HTTP client: +HTTP client using POSIX sockets: - `Http.get(url)` - GET request - `Http.post(url, body)` - POST request +- `Http.put(url, body)` - PUT request +- `Http.delete(url)` - DELETE request + +Self-contained implementation (no external dependencies like libcurl). ### Phase 5: JavaScript Backend diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index e7c72d7..0ae3807 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -467,6 +467,15 @@ impl CBackend { self.writeln(" void* env;"); self.writeln("} LuxFileHandler;"); self.writeln(""); + self.writeln("// Handler struct for Http effect"); + self.writeln("typedef struct {"); + self.writeln(" LuxString (*get)(void* env, LuxString url);"); + self.writeln(" LuxString (*post)(void* env, LuxString url, LuxString body);"); + self.writeln(" LuxString (*put)(void* env, LuxString url, LuxString body);"); + self.writeln(" LuxString (*delete_req)(void* env, LuxString url);"); + self.writeln(" void* env;"); + self.writeln("} LuxHttpHandler;"); + self.writeln(""); self.writeln("// Evidence struct - passed to effectful functions"); self.writeln("// Contains pointers to current handlers for each effect type"); self.writeln("typedef struct {"); @@ -476,6 +485,7 @@ impl CBackend { self.writeln(" LuxRandomHandler* random;"); self.writeln(" LuxTimeHandler* time;"); self.writeln(" LuxFileHandler* file;"); + self.writeln(" LuxHttpHandler* http;"); self.writeln("} LuxEvidence;"); self.writeln(""); self.writeln("// Default Console handler using built-in implementations"); @@ -672,6 +682,142 @@ impl CBackend { self.writeln(" .env = NULL"); self.writeln("};"); self.writeln(""); + self.writeln("// === Http Effect Built-in Implementations ==="); + self.writeln(""); + self.writeln("#include "); + self.writeln("#include "); + self.writeln("#include "); + self.writeln("#include "); + self.writeln(""); + self.writeln("// Parse URL into host, port, and path"); + self.writeln("static int lux_http_parse_url(const char* url, char* host, int* port, char* path) {"); + self.writeln(" *port = 80;"); + self.writeln(" path[0] = '/'; path[1] = '\\0';"); + self.writeln(" const char* start = url;"); + self.writeln(" if (strncmp(url, \"http://\", 7) == 0) start = url + 7;"); + self.writeln(" else if (strncmp(url, \"https://\", 8) == 0) { start = url + 8; *port = 443; }"); + self.writeln(" const char* slash = strchr(start, '/');"); + self.writeln(" const char* colon = strchr(start, ':');"); + self.writeln(" if (colon && (!slash || colon < slash)) {"); + self.writeln(" strncpy(host, start, colon - start);"); + self.writeln(" host[colon - start] = '\\0';"); + self.writeln(" *port = atoi(colon + 1);"); + self.writeln(" } else if (slash) {"); + self.writeln(" strncpy(host, start, slash - start);"); + self.writeln(" host[slash - start] = '\\0';"); + self.writeln(" } else {"); + self.writeln(" strcpy(host, start);"); + self.writeln(" }"); + self.writeln(" if (slash) strcpy(path, slash);"); + self.writeln(" return 0;"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString lux_http_request(const char* method, LuxString url, LuxString body) {"); + self.writeln(" char host[256], path[1024];"); + self.writeln(" int port;"); + self.writeln(" lux_http_parse_url(url, host, &port, path);"); + self.writeln(""); + self.writeln(" struct hostent* server = gethostbyname(host);"); + self.writeln(" if (!server) return strdup(\"Error: Could not resolve host\");"); + self.writeln(""); + self.writeln(" int sockfd = socket(AF_INET, SOCK_STREAM, 0);"); + self.writeln(" if (sockfd < 0) return strdup(\"Error: Could not create socket\");"); + self.writeln(""); + self.writeln(" struct sockaddr_in serv_addr;"); + self.writeln(" memset(&serv_addr, 0, sizeof(serv_addr));"); + self.writeln(" serv_addr.sin_family = AF_INET;"); + self.writeln(" memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length);"); + self.writeln(" serv_addr.sin_port = htons(port);"); + self.writeln(""); + self.writeln(" if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {"); + self.writeln(" close(sockfd);"); + self.writeln(" return strdup(\"Error: Connection failed\");"); + self.writeln(" }"); + self.writeln(""); + self.writeln(" char request[4096];"); + self.writeln(" if (body && strlen(body) > 0) {"); + self.writeln(" snprintf(request, sizeof(request),"); + self.writeln(" \"%s %s HTTP/1.1\\r\\n\""); + self.writeln(" \"Host: %s\\r\\n\""); + self.writeln(" \"Content-Type: application/x-www-form-urlencoded\\r\\n\""); + self.writeln(" \"Content-Length: %zu\\r\\n\""); + self.writeln(" \"Connection: close\\r\\n\\r\\n%s\","); + self.writeln(" method, path, host, strlen(body), body);"); + self.writeln(" } else {"); + self.writeln(" snprintf(request, sizeof(request),"); + self.writeln(" \"%s %s HTTP/1.1\\r\\n\""); + self.writeln(" \"Host: %s\\r\\n\""); + self.writeln(" \"Connection: close\\r\\n\\r\\n\","); + self.writeln(" method, path, host);"); + self.writeln(" }"); + self.writeln(""); + self.writeln(" send(sockfd, request, strlen(request), 0);"); + self.writeln(""); + self.writeln(" char* response = malloc(65536);"); + self.writeln(" size_t total = 0;"); + self.writeln(" ssize_t n;"); + self.writeln(" while ((n = recv(sockfd, response + total, 65536 - total - 1, 0)) > 0) {"); + self.writeln(" total += n;"); + self.writeln(" if (total >= 65535) break;"); + self.writeln(" }"); + self.writeln(" response[total] = '\\0';"); + self.writeln(" close(sockfd);"); + self.writeln(""); + self.writeln(" // Find body (after \\r\\n\\r\\n)"); + self.writeln(" char* body_start = strstr(response, \"\\r\\n\\r\\n\");"); + self.writeln(" if (body_start) {"); + self.writeln(" body_start += 4;"); + self.writeln(" char* result = strdup(body_start);"); + self.writeln(" free(response);"); + self.writeln(" return result;"); + self.writeln(" }"); + self.writeln(" return response;"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString lux_http_get(LuxString url) {"); + self.writeln(" return lux_http_request(\"GET\", url, NULL);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString lux_http_post(LuxString url, LuxString body) {"); + self.writeln(" return lux_http_request(\"POST\", url, body);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString lux_http_put(LuxString url, LuxString body) {"); + self.writeln(" return lux_http_request(\"PUT\", url, body);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString lux_http_delete(LuxString url) {"); + self.writeln(" return lux_http_request(\"DELETE\", url, NULL);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString default_http_get(void* env, LuxString url) {"); + self.writeln(" (void)env;"); + self.writeln(" return lux_http_get(url);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString default_http_post(void* env, LuxString url, LuxString body) {"); + self.writeln(" (void)env;"); + self.writeln(" return lux_http_post(url, body);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString default_http_put(void* env, LuxString url, LuxString body) {"); + self.writeln(" (void)env;"); + self.writeln(" return lux_http_put(url, body);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxString default_http_delete(void* env, LuxString url) {"); + self.writeln(" (void)env;"); + self.writeln(" return lux_http_delete(url);"); + self.writeln("}"); + self.writeln(""); + self.writeln("static LuxHttpHandler default_http_handler = {"); + self.writeln(" .get = default_http_get,"); + self.writeln(" .post = default_http_post,"); + self.writeln(" .put = default_http_put,"); + self.writeln(" .delete_req = default_http_delete,"); + self.writeln(" .env = NULL"); + self.writeln("};"); + self.writeln(""); self.writeln("// Default evidence with built-in handlers"); self.writeln("static LuxEvidence default_evidence = {"); self.writeln(" .console = &default_console_handler,"); @@ -679,7 +825,8 @@ impl CBackend { self.writeln(" .reader = NULL,"); self.writeln(" .random = &default_random_handler,"); self.writeln(" .time = &default_time_handler,"); - self.writeln(" .file = &default_file_handler"); + self.writeln(" .file = &default_file_handler,"); + self.writeln(" .http = &default_http_handler"); self.writeln("};"); self.writeln(""); self.writeln("// === List Types ==="); @@ -1409,6 +1556,47 @@ impl CBackend { } } + // Built-in Http effect + if effect.name == "Http" { + match operation.name.as_str() { + "get" => { + let url = self.emit_expr(&args[0])?; + if self.has_evidence { + return Ok(format!("ev->http->get(ev->http->env, {})", url)); + } else { + return Ok(format!("lux_http_get({})", url)); + } + } + "post" => { + let url = self.emit_expr(&args[0])?; + let body = self.emit_expr(&args[1])?; + if self.has_evidence { + return Ok(format!("ev->http->post(ev->http->env, {}, {})", url, body)); + } else { + return Ok(format!("lux_http_post({}, {})", url, body)); + } + } + "put" => { + let url = self.emit_expr(&args[0])?; + let body = self.emit_expr(&args[1])?; + if self.has_evidence { + return Ok(format!("ev->http->put(ev->http->env, {}, {})", url, body)); + } else { + return Ok(format!("lux_http_put({}, {})", url, body)); + } + } + "delete" => { + let url = self.emit_expr(&args[0])?; + if self.has_evidence { + return Ok(format!("ev->http->delete_req(ev->http->env, {})", url)); + } else { + return Ok(format!("lux_http_delete({})", url)); + } + } + _ => {} + } + } + // For other effects, emit generic evidence-passing call let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); if self.has_evidence { @@ -1915,6 +2103,11 @@ impl CBackend { "exists" | "isDir" => Some("LuxBool".to_string()), _ => None, } + } else if effect.name == "Http" { + match operation.name.as_str() { + "get" | "post" | "put" | "delete" => Some("LuxString".to_string()), + _ => None, + } } else if effect.name == "Console" { match operation.name.as_str() { "print" => Some("void".to_string()),