Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Pipeline: **source → lexer → parser → compiler → x86-64 NASM assembly**
| `src/parser.rs` | Recursive descent parser — tokens to AST (`Vec<Statement>`) |
| `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

Expand Down Expand Up @@ -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
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions examples/http.bonk
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions runtime/http.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>

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;
}
13 changes: 11 additions & 2 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ pub enum Statement {
condition: Expression,
body: Vec<Statement>,
},

Fetch {
method: Expression,
url: Expression,
body: Option<Expression>,
},
}

#[derive(Debug, Clone)]
Expand All @@ -43,7 +47,12 @@ pub enum Expression {
FunctionCall{
name: String,
args: Vec<Expression>
}
},
Fetch {
method: Box<Expression>,
url: Box<Expression>,
body: Option<Box<Expression>>,
},
}

#[derive(Debug, Clone)]
Expand Down
48 changes: 43 additions & 5 deletions src/compiler.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

use crate::ast::{Statement, Expression};
use std::slice::Iter;
Expand All @@ -13,6 +13,7 @@ pub struct Compiler {
var_offset: i32,
label_count: i32,
epilogue_label: String,
string_vars: HashSet<String>,
}


Expand All @@ -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<Statement>) -> Vec<String> {
Expand Down Expand Up @@ -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());
Expand All @@ -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");

Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}
_ => {}
}
}
Expand Down Expand Up @@ -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());
}
}
}

Expand All @@ -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));
}
Expand All @@ -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());
Expand All @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fn parse_statement(iter: &mut Peekable<Iter<Token>>) -> Result<Statement, String
Ok(Statement::Send(expr))
}
Some(Token::While) => parse_while(iter),
Some(Token::Fetch) => parse_fetch_statement(iter),
Some(Token::Identifier(_)) => parse_assignment(iter),
_ => Err(format!("Cannot parse found {:?}", iter.peek())),
}?;
Expand Down Expand Up @@ -251,6 +252,15 @@ fn parse_atomics(iter: &mut Peekable<Iter<Token>>) -> Result<Expression, String>
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(),
Expand All @@ -273,6 +283,41 @@ fn parse_atomics(iter: &mut Peekable<Iter<Token>>) -> Result<Expression, String>
}
}

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<Iter<Token>>) -> Result<(Expression, Expression, Option<Expression>), 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<Iter<Token>>) -> Result<Statement, String> {
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<Iter<Token>>) -> Result<Statement, String> {
iter.next(); // Consuming while
let condition = parse_expression(iter)?;
Expand Down
1 change: 1 addition & 0 deletions src/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub enum Token {
Else,
Print,
Send,
Fetch,
Plus,
Eq,
NotEq,
Expand Down