From c34021f040f9d4329962fcd42d49bcf96bd266cb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 23:16:51 +0000 Subject: [PATCH] feat: add HTTP fetch support as a language primitive Add `fetch` keyword for making HTTP requests directly from Bonk code. Supports GET/POST/PUT/DELETE via a libcurl-based C runtime. Syntax: fetch "url" -- GET request fetch "POST" "url" "body" -- method + URL + body result = fetch "url"; -- capture response as string https://claude.ai/code/session_01M76SM5XsfmERRhkueq1d4W --- CLAUDE.md | 25 +++++++++++++++++++++-- Makefile | 11 ++++++++--- examples/http.bonk | 7 +++++++ runtime/http.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++ src/ast.rs | 13 ++++++++++-- src/compiler.rs | 48 ++++++++++++++++++++++++++++++++++++++++----- src/lexer.rs | 1 + src/parser.rs | 45 ++++++++++++++++++++++++++++++++++++++++++ src/tokens.rs | 1 + 9 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 examples/http.bonk create mode 100644 runtime/http.c diff --git a/CLAUDE.md b/CLAUDE.md index 07fa826..def4824 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,7 @@ Pipeline: **source → lexer → parser → compiler → x86-64 NASM assembly** | `src/parser.rs` | Recursive descent parser — tokens to AST (`Vec`) | | `src/ast.rs` | AST types: `Statement`, `Expression`, `BinaryOperator` | | `src/compiler.rs` | Code generator — walks AST, emits x86-64 NASM lines | +| `runtime/http.c` | C runtime — HTTP fetch via libcurl, linked into final binary | ## Language Syntax @@ -53,11 +54,31 @@ run foo(a, b) # function with parameters end ``` +### HTTP Fetch + +``` +run main() + result = fetch "http://example.com"; # GET request (1 arg = URL) + print result; + + resp = fetch "POST" "http://api.com" "data"; # method + URL + body + print resp; + + fetch "DELETE" "http://api.com/item"; # fire-and-forget (no body) +end +``` + +- 1 arg: `fetch URL` — implicit GET +- 2 args: `fetch METHOD URL` — custom method, no body +- 3 args: `fetch METHOD URL BODY` — custom method with request body +- Returns the response body as a string +- Works as both expression (`x = fetch ...;`) and statement (`fetch ...;`) + Operators: `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `>` ## Platform - Target: macOS x86-64 (`nasm -f macho64`, symbols prefixed with `_`) -- Links against libc for `_printf` +- Links against libc for `_printf` and libcurl for HTTP fetch - On Apple Silicon: uses `gcc -arch x86_64` to cross-link; runs via Rosetta -- Requires: Rust, NASM, GCC +- Requires: Rust, NASM, GCC, libcurl diff --git a/Makefile b/Makefile index f3151fa..727782b 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,10 @@ FILE ?= examples/basic.bonk BUILD_DIR = build ASM = $(BUILD_DIR)/output.asm OBJ = $(BUILD_DIR)/output.o +RT_OBJ = $(BUILD_DIR)/http.o BIN = $(BUILD_DIR)/prog -.PHONY: cargo-build compile assemble link run clean +.PHONY: cargo-build compile assemble runtime link run clean cargo-build: cargo build @@ -16,8 +17,12 @@ compile: cargo-build assemble: compile nasm -f macho64 $(ASM) -o $(OBJ) -link: assemble - gcc -arch x86_64 $(OBJ) -o $(BIN) +runtime: + @mkdir -p $(BUILD_DIR) + gcc -arch x86_64 -c runtime/http.c -o $(RT_OBJ) + +link: assemble runtime + gcc -arch x86_64 $(OBJ) $(RT_OBJ) -lcurl -o $(BIN) run: link ./$(BIN) diff --git a/examples/http.bonk b/examples/http.bonk new file mode 100644 index 0000000..a92ed19 --- /dev/null +++ b/examples/http.bonk @@ -0,0 +1,7 @@ +run main() + result = fetch "http://httpbin.org/get"; + print result; + + resp = fetch "POST" "http://httpbin.org/post" "hello=world"; + print resp; +end diff --git a/runtime/http.c b/runtime/http.c new file mode 100644 index 0000000..288d92c --- /dev/null +++ b/runtime/http.c @@ -0,0 +1,49 @@ +#include +#include +#include + +struct response_buffer { + char *data; + size_t size; +}; + +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { + size_t realsize = size * nmemb; + struct response_buffer *buf = (struct response_buffer *)userp; + char *ptr = realloc(buf->data, buf->size + realsize + 1); + if (!ptr) return 0; + buf->data = ptr; + memcpy(&(buf->data[buf->size]), contents, realsize); + buf->size += realsize; + buf->data[buf->size] = 0; + return realsize; +} + +char *bonk_http_fetch(const char *method, const char *url, const char *body) { + CURL *curl = curl_easy_init(); + if (!curl) { + char *err = malloc(1); + err[0] = 0; + return err; + } + + struct response_buffer buf; + buf.data = malloc(1); + buf.data[0] = 0; + buf.size = 0; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + if (body && *body) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); + } + + curl_easy_perform(curl); + curl_easy_cleanup(curl); + + return buf.data; +} diff --git a/src/ast.rs b/src/ast.rs index 5ad20f4..64542f9 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -26,7 +26,11 @@ pub enum Statement { condition: Expression, body: Vec, }, - + Fetch { + method: Expression, + url: Expression, + body: Option, + }, } #[derive(Debug, Clone)] @@ -43,7 +47,12 @@ pub enum Expression { FunctionCall{ name: String, args: Vec - } + }, + Fetch { + method: Box, + url: Box, + body: Option>, + }, } #[derive(Debug, Clone)] diff --git a/src/compiler.rs b/src/compiler.rs index 9f954ea..1a11cdb 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::ast::{Statement, Expression}; use std::slice::Iter; @@ -13,6 +13,7 @@ pub struct Compiler { var_offset: i32, label_count: i32, epilogue_label: String, + string_vars: HashSet, } @@ -27,6 +28,7 @@ impl Compiler { var_offset: 8, label_count: 0, epilogue_label: String::new(), + string_vars: HashSet::new(), } } pub fn compile(&mut self, ast: Vec) -> Vec { @@ -78,6 +80,7 @@ impl Compiler { let mut data = Vec::new(); // Allows the use of printf if gcc is used to link data.push("extern _printf".into()); + data.push("extern _bonk_http_fetch".into()); data.push("section .rodata".into()); // String required for printf to print ints data.push("fmt: db \"%ld\", 10, 0".to_string()); @@ -102,6 +105,7 @@ impl Compiler { let saved_offset_map = std::mem::take(&mut self.offset_map); let saved_var_offset = self.var_offset; let saved_epilogue_label = std::mem::take(&mut self.epilogue_label); + let saved_string_vars = std::mem::take(&mut self.string_vars); self.var_offset = 8; self.epilogue_label = self.new_label("epilogue"); @@ -136,6 +140,7 @@ impl Compiler { self.offset_map = saved_offset_map; self.var_offset = saved_var_offset; self.epilogue_label = saved_epilogue_label; + self.string_vars = saved_string_vars; } else { self.assem.push("Error: No function found\n".to_string()); } @@ -195,6 +200,9 @@ impl Compiler { self.compile_expression(expr); self.assem.push(format!(" jmp {}", self.epilogue_label)); } + Statement::Fetch { method, url, body } => { + self.compile_fetch(method, url, body.as_ref()); + } _ => {} } } @@ -292,6 +300,9 @@ impl Compiler { self.assem.push(" mov rax, 0".into()); self.assem.push(format!(" call _{}", name)); } + Expression::Fetch { method, url, body } => { + self.compile_fetch(method, url, body.as_deref()); + } } } @@ -304,6 +315,12 @@ impl Compiler { self.var_offset += 8; off }; + // Track whether this variable holds a string value + if matches!(value, Expression::StringLiteral(_) | Expression::Fetch { .. }) { + self.string_vars.insert(name.clone()); + } else { + self.string_vars.remove(name); + } self.compile_expression(value); self.assem.push(format!(" mov [rbp - {}], rax", offset)); } @@ -321,10 +338,10 @@ impl Compiler { } fn compile_print(&mut self, value: &crate::ast::Expression) { - let fmt_label = if matches!(value, Expression::StringLiteral(_)) { - "fmt_str" - } else { - "fmt" + let fmt_label = match value { + Expression::StringLiteral(_) | Expression::Fetch { .. } => "fmt_str", + Expression::Variable(name) if self.string_vars.contains(name) => "fmt_str", + _ => "fmt", }; self.compile_expression(value); self.assem.push(" mov rsi, rax".into()); @@ -333,6 +350,27 @@ impl Compiler { self.assem.push(" call _printf".into()); } + fn compile_fetch(&mut self, method: &Expression, url: &Expression, body: Option<&Expression>) { + // Evaluate args left-to-right, push onto stack + self.compile_expression(method); + self.assem.push(" push rax".into()); + self.compile_expression(url); + self.assem.push(" push rax".into()); + if let Some(body_expr) = body { + self.compile_expression(body_expr); + self.assem.push(" push rax".into()); + } else { + self.assem.push(" push 0".into()); // NULL body + } + // Pop into argument registers: rdi=method, rsi=url, rdx=body + self.assem.push(" pop rdx".into()); + self.assem.push(" pop rsi".into()); + self.assem.push(" pop rdi".into()); + self.assem.push(" mov rax, 0".into()); + self.assem.push(" call _bonk_http_fetch".into()); + // Result (response string pointer) is in rax + } + fn compile_while(&mut self, condition: &Expression, body: &[Statement]) { let start_label = self.new_label("while_start"); let end_label = self.new_label("while_end"); diff --git a/src/lexer.rs b/src/lexer.rs index 6c7e204..7baec93 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -64,6 +64,7 @@ impl Lexer { "do" => Token::Do, "print" => Token::Print, "send" => Token::Send, + "fetch" => Token::Fetch, "if" => Token::If, "then" => Token::Then, "else" => Token::Else, diff --git a/src/parser.rs b/src/parser.rs index e510936..42ec201 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -49,6 +49,7 @@ fn parse_statement(iter: &mut Peekable>) -> Result parse_while(iter), + Some(Token::Fetch) => parse_fetch_statement(iter), Some(Token::Identifier(_)) => parse_assignment(iter), _ => Err(format!("Cannot parse found {:?}", iter.peek())), }?; @@ -251,6 +252,15 @@ fn parse_atomics(iter: &mut Peekable>) -> Result iter.next(); Ok(Expression::Variable(name.clone())) } + Some(Token::Fetch) => { + iter.next(); // consume Fetch token + let (method, url, body) = parse_fetch_args(iter)?; + Ok(Expression::Fetch { + method: Box::new(method), + url: Box::new(url), + body: body.map(Box::new), + }) + } Some(Token::FunctionCall(_)) => { let name = match iter.next() { Some(Token::FunctionCall(n)) => n.clone(), @@ -273,6 +283,41 @@ fn parse_atomics(iter: &mut Peekable>) -> Result } } +fn is_fetch_arg_start(token: &Token) -> bool { + matches!(token, Token::StringLiteral(_) | Token::Identifier(_) | Token::Number(_) | Token::FunctionCall(_)) +} + +fn parse_fetch_args(iter: &mut Peekable>) -> Result<(Expression, Expression, Option), String> { + // Parse first arg (required) + let first = parse_atomics(iter)?; + + // Check for second arg + if let Some(tok) = iter.peek() { + if is_fetch_arg_start(tok) { + let second = parse_atomics(iter)?; + + // Check for third arg + if let Some(tok) = iter.peek() { + if is_fetch_arg_start(tok) { + let third = parse_atomics(iter)?; + // 3 args: method, url, body + return Ok((first, second, Some(third))); + } + } + // 2 args: method, url + return Ok((first, second, None)); + } + } + // 1 arg: url only, default to GET + Ok((Expression::StringLiteral("GET".to_string()), first, None)) +} + +fn parse_fetch_statement(iter: &mut Peekable>) -> Result { + iter.next(); // consume Fetch token + let (method, url, body) = parse_fetch_args(iter)?; + Ok(Statement::Fetch { method, url, body }) +} + fn parse_while(iter: &mut Peekable>) -> Result { iter.next(); // Consuming while let condition = parse_expression(iter)?; diff --git a/src/tokens.rs b/src/tokens.rs index efb9f11..f2b089a 100644 --- a/src/tokens.rs +++ b/src/tokens.rs @@ -12,6 +12,7 @@ pub enum Token { Else, Print, Send, + Fetch, Plus, Eq, NotEq,