Compare commits

...

2 Commits

Author SHA1 Message Date
e4582ec85a float literals 2026-06-24 18:06:58 +02:00
213e3d4b37 floats 2026-06-24 17:20:57 +02:00
8 changed files with 208 additions and 93 deletions

View File

@@ -137,6 +137,23 @@ _builtin_set64:
mov [rdi], rsi mov [rdi], rsi
ret ret
section .text._builtin_cvtsi2sd
_builtin_cvtsi2sd:
cvtsi2sd xmm0, rdi
movq rax, xmm0
ret
section .text._builtin_cvttsd2si
_builtin_cvttsd2si:
cvttsd2si rax, xmm0
ret
section .text._builtin_f64_to_float
_builtin_f64_to_float:
cvtsd2ss xmm0, xmm0
movd eax, xmm0
ret
section .text._builtin_syscall section .text._builtin_syscall
_builtin_syscall: _builtin_syscall:
mov rax, rdi mov rax, rdi
@@ -592,6 +609,13 @@ _builtin_environ:
TokenType::IntLiteral => { TokenType::IntLiteral => {
emit!(&mut self.output, " mov rax, {}", token.lexeme); emit!(&mut self.output, " mov rax, {}", token.lexeme);
} }
TokenType::FloatLiteral => {
emit!(
&mut self.output,
" mov rax, __float64__({})",
token.lexeme
);
}
TokenType::CharLiteral => { TokenType::CharLiteral => {
emit!( emit!(
&mut self.output, &mut self.output,
@@ -628,7 +652,12 @@ _builtin_environ:
self.compile_expr(env, right)?; self.compile_expr(env, right)?;
match op.token_type { match op.token_type {
TokenType::Minus => { TokenType::Minus => {
emit!(&mut self.output, " neg rax"); if self.expr_types[&right.id] == "f64" {
emit!(&mut self.output, " mov rbx, 0x8000000000000000");
emit!(&mut self.output, " xor rax, rbx");
} else {
emit!(&mut self.output, " neg rax");
}
} }
TokenType::Bang => { TokenType::Bang => {
emit!(&mut self.output, " test rax, rax"); emit!(&mut self.output, " test rax, rax");
@@ -682,8 +711,11 @@ _builtin_environ:
emit!(&mut self.output, " push rax"); emit!(&mut self.output, " push rax");
} }
let arg_count = args.len(); let arg_types: Vec<String> = args
self.emit_call_setup(arg_count); .iter()
.map(|a| self.expr_types[&a.id].clone())
.collect();
self.emit_call_setup(&arg_types);
if let ExprKind::Variable(callee_name) = &callee.kind { if let ExprKind::Variable(callee_name) = &callee.kind {
if self if self
@@ -704,7 +736,7 @@ _builtin_environ:
emit!(&mut self.output, " call rax"); emit!(&mut self.output, " call rax");
} }
self.emit_call_cleanup(arg_count); self.emit_call_cleanup(args.len());
} }
ExprKind::ArrayLiteral(exprs) => { ExprKind::ArrayLiteral(exprs) => {
emit!(&mut self.output, " mov rdi, 24"); emit!(&mut self.output, " mov rdi, 24");
@@ -805,50 +837,65 @@ _builtin_environ:
emit!(&mut self.output, " push rax"); emit!(&mut self.output, " push rax");
} }
let arg_count = 1 + args.len(); let mut arg_types = vec![];
self.emit_call_setup(arg_count); arg_types.push(receiver_type.clone());
arg_types.extend(args.iter().map(|a| self.expr_types[&a.id].clone()));
self.emit_call_setup(&arg_types);
emit!(&mut self.output, " call {}", func_name); emit!(&mut self.output, " call {}", func_name);
self.emit_call_cleanup(arg_count); self.emit_call_cleanup(1 + args.len());
} }
} }
Ok(()) Ok(())
} }
fn emit_call_setup(&mut self, arg_count: usize) { fn emit_call_setup(&mut self, arg_types: &[String]) {
if arg_count <= 6 { let arg_count = arg_types.len();
for i in (0..arg_count).rev() {
emit!(&mut self.output, " pop {}", REGISTERS[i]); let to_register = arg_count.min(6);
} let mut fp_idx = 0;
} else { let mut int_idx = 0;
for (i, reg) in REGISTERS.iter().enumerate() { for (i, arg_type) in arg_types.iter().enumerate().take(to_register) {
let offset = 8 * (arg_count - 1 - i); let offset = 8 * (arg_count - 1 - i);
emit!( emit!(&mut self.output, " mov rax, QWORD [rsp + {}]", offset);
&mut self.output, if arg_type == "f64" {
" mov {}, QWORD [rsp + {}]", emit!(&mut self.output, " movq xmm{}, rax", fp_idx);
reg, fp_idx += 1;
offset } else {
); emit!(&mut self.output, " mov {}, rax", REGISTERS[int_idx]);
} int_idx += 1;
// TODO: since all zern values are 64bit large we currently cannot call
// external functions that expect a non-64bit value past the 6th argument
let num_stack = arg_count - 6;
for i in 0..num_stack {
let arg_idx = arg_count - 1 - i;
let offset = 8 * (arg_count - 1 - arg_idx);
emit!(
&mut self.output,
" mov rax, QWORD [rsp + {}]",
offset + 8 * i
);
emit!(&mut self.output, " push rax");
} }
} }
// TODO: since all zern values are 64bit large we currently cannot call
// external functions that expect a non-64bit value past the 6th argument
let num_stack = arg_count.saturating_sub(6);
for i in 0..num_stack {
let arg_idx = arg_count - 1 - i;
let offset = 8 * (arg_count - 1 - arg_idx);
emit!(
&mut self.output,
" mov rax, QWORD [rsp + {}]",
offset + 8 * i
);
emit!(&mut self.output, " push rax");
}
emit!(&mut self.output, " mov al, {}", fp_idx);
if num_stack == 0 {
emit!(&mut self.output, " add rsp, {}", 8 * to_register);
}
} }
fn emit_call_cleanup(&mut self, arg_count: usize) { fn emit_call_cleanup(&mut self, arg_count: usize) {
if arg_count > 6 { let num_stack = arg_count.saturating_sub(6);
emit!(&mut self.output, " add rsp, {}", 8 * (arg_count - 6)); if num_stack > 0 {
emit!(&mut self.output, " add rsp, {}", 8 * arg_count); emit!(
&mut self.output,
" add rsp, {}",
8 * (arg_count + num_stack)
);
} }
} }

View File

@@ -675,6 +675,7 @@ impl Parser {
fn primary(&mut self) -> Result<Expr, ZernError> { fn primary(&mut self) -> Result<Expr, ZernError> {
if self.match_token(&[ if self.match_token(&[
TokenType::IntLiteral, TokenType::IntLiteral,
TokenType::FloatLiteral,
TokenType::CharLiteral, TokenType::CharLiteral,
TokenType::StringLiteral, TokenType::StringLiteral,
TokenType::True, TokenType::True,

View File

@@ -1,28 +1,28 @@
const STDIN = 0 const STDIN = 0
const STDOUT = 1 const STDOUT = 1
const SIGABRT = 6 const SIGABRT = 6
const AT_FDCWD = -100 const AT_FDCWD = -100
const S_IFDIR = 0o040000 const S_IFDIR = 0o040000
const S_IFMT = 0o170000 const S_IFMT = 0o170000
const PROT_READ = 1 const PROT_READ = 1
const PROT_WRITE = 2 const PROT_WRITE = 2
const MAP_PRIVATE = 2 const MAP_PRIVATE = 2
const MAP_ANONYMOUS = 32 const MAP_ANONYMOUS = 32
const O_RDONLY = 0 const O_RDONLY = 0
const O_WRONLY = 1 const O_WRONLY = 1
const O_CREAT = 64 const O_CREAT = 64
const O_TRUNC = 512 const O_TRUNC = 512
const SEEK_SET = 0 const SEEK_SET = 0
const SEEK_END = 2 const SEEK_END = 2
const F_OK = 0 const F_OK = 0
const SYS_read = 0 const SYS_read = 0
const SYS_write = 1 const SYS_write = 1

View File

@@ -309,18 +309,6 @@ func io.read_line[] : str
b->destroy() b->destroy()
return s return s
func io.file_exists[path: str] : bool
return _builtin_syscall(SYS_faccessat, AT_FDCWD, path, F_OK, 0) == 0
func io.is_a_directory[path: str] : bool
st := _stackalloc(256) // it has 21 mixed-size fields so ptr for now
rc := _builtin_syscall(SYS_newfstatat, AT_FDCWD, path, st, 0)
if rc != 0
return false
return (mem.read32(st + 24) & S_IFMT) == S_IFDIR
func io.read_text_file[path: str] : str, bool func io.read_text_file[path: str] : str, bool
fd := _builtin_syscall(SYS_openat, AT_FDCWD, path, O_RDONLY, 0) fd := _builtin_syscall(SYS_openat, AT_FDCWD, path, O_RDONLY, 0)
if fd < 0 if fd < 0
@@ -929,6 +917,12 @@ func Array._partition[arr: Array, low: i64, high: i64] : i64
arr->set(high, temp) arr->set(high, temp)
return i + 1 return i + 1
func Array.contains_str[arr: Array, s: str] : bool
for i in 0..arr->size
if (arr->nth(i) as str)->equal(s)
return true
return false
func Array.count[arr: Array, item: any] : i64 func Array.count[arr: Array, item: any] : i64
count := 0 count := 0
for i in 0..arr->size for i in 0..arr->size
@@ -1108,6 +1102,18 @@ func os.run_shell_command[command: str] : i64, bool
else else
return -(st & 0x7f), true return -(st & 0x7f), true
func os.file_exists[path: str] : bool
return _builtin_syscall(SYS_faccessat, AT_FDCWD, path, F_OK, 0) == 0
func os.is_a_directory[path: str] : bool
st := _stackalloc(256) // it has 21 mixed-size fields so ptr for now
rc := _builtin_syscall(SYS_newfstatat, AT_FDCWD, path, st, 0)
if rc != 0
return false
return (mem.read32(st + 24) & S_IFMT) == S_IFDIR
func os.list_directory[path: str] : Array, bool func os.list_directory[path: str] : Array, bool
fd := _builtin_syscall(SYS_openat, AT_FDCWD, path, O_RDONLY, 0) fd := _builtin_syscall(SYS_openat, AT_FDCWD, path, O_RDONLY, 0)
if fd < 0 if fd < 0

View File

@@ -51,6 +51,12 @@ impl SymbolTable {
"_builtin_set64".into(), "_builtin_set64".into(),
FnType::new("void", vec!["ptr", "i64"]), FnType::new("void", vec!["ptr", "i64"]),
), ),
("_builtin_cvtsi2sd".into(), FnType::new("f64", vec!["i64"])),
("_builtin_cvttsd2si".into(), FnType::new("i64", vec!["f64"])),
(
"_builtin_f64_to_float".into(),
FnType::new("i64", vec!["f64"]),
),
("_builtin_syscall".into(), FnType::new_variadic("i64")), ("_builtin_syscall".into(), FnType::new_variadic("i64")),
("_builtin_environ".into(), FnType::new("ptr", vec![])), ("_builtin_environ".into(), FnType::new("ptr", vec![])),
("_var_arg".into(), FnType::new("any", vec!["i64"])), ("_var_arg".into(), FnType::new("any", vec!["i64"])),

View File

@@ -39,6 +39,7 @@ pub enum TokenType {
StringLiteral, StringLiteral,
CharLiteral, CharLiteral,
IntLiteral, IntLiteral,
FloatLiteral,
True, True,
False, False,
@@ -338,6 +339,8 @@ impl Tokenizer {
} }
fn scan_number(&mut self) -> Result<(), ZernError> { fn scan_number(&mut self) -> Result<(), ZernError> {
let mut is_float = false;
if self.match_char('x') { if self.match_char('x') {
while self.peek().is_ascii_hexdigit() { while self.peek().is_ascii_hexdigit() {
self.advance(); self.advance();
@@ -350,9 +353,23 @@ impl Tokenizer {
while self.peek().is_ascii_digit() { while self.peek().is_ascii_digit() {
self.advance(); self.advance();
} }
if self.current + 1 < self.source.len()
&& self.peek() == '.'
&& self.source[self.current + 1] != '.'
{
is_float = true;
self.advance();
while self.peek().is_ascii_digit() {
self.advance();
}
}
} }
self.add_token(TokenType::IntLiteral) if is_float {
self.add_token(TokenType::FloatLiteral)
} else {
self.add_token(TokenType::IntLiteral)
}
} }
fn scan_identifier(&mut self) -> Result<(), ZernError> { fn scan_identifier(&mut self) -> Result<(), ZernError> {

View File

@@ -33,7 +33,7 @@ macro_rules! expect_types {
}; };
} }
static BUILTIN_TYPES: [&str; 7] = ["void", "u8", "i64", "str", "bool", "ptr", "any"]; static BUILTIN_TYPES: [&str; 8] = ["void", "u8", "i64", "f64", "str", "bool", "ptr", "any"];
pub struct Env { pub struct Env {
scopes: Vec<HashMap<String, Type>>, scopes: Vec<HashMap<String, Type>>,
@@ -300,6 +300,9 @@ impl<'a> TypeChecker<'a> {
"unrecognized type: ".to_owned() + &return_type "unrecognized type: ".to_owned() + &return_type
); );
} }
if return_type == "f64" {
return error!(&return_types[0].loc, "returning f64 not implemented yet");
}
self.current_function_return_type = return_type.clone(); self.current_function_return_type = return_type.clone();
@@ -314,6 +317,12 @@ impl<'a> TypeChecker<'a> {
"unrecognized type: ".to_owned() + &param.var_type.lexeme "unrecognized type: ".to_owned() + &param.var_type.lexeme
); );
} }
if param.var_type.lexeme == "f64" {
return error!(
&param.var_name.loc,
"f64 params not implemented yet"
);
}
env.define_var( env.define_var(
param.var_name.lexeme.clone(), param.var_name.lexeme.clone(),
@@ -420,6 +429,7 @@ impl<'a> TypeChecker<'a> {
ExprKind::Grouping(expr) => self.typecheck_expr(env, expr), ExprKind::Grouping(expr) => self.typecheck_expr(env, expr),
ExprKind::Literal(token) => match token.token_type { ExprKind::Literal(token) => match token.token_type {
TokenType::IntLiteral => Ok("i64".into()), TokenType::IntLiteral => Ok("i64".into()),
TokenType::FloatLiteral => Ok("f64".into()),
TokenType::CharLiteral => Ok("u8".into()), TokenType::CharLiteral => Ok("u8".into()),
TokenType::StringLiteral => Ok("str".into()), TokenType::StringLiteral => Ok("str".into()),
TokenType::True => Ok("bool".into()), TokenType::True => Ok("bool".into()),
@@ -430,8 +440,8 @@ impl<'a> TypeChecker<'a> {
let right_type = self.typecheck_expr(env, right)?; let right_type = self.typecheck_expr(env, right)?;
match op.token_type { match op.token_type {
TokenType::Minus => { TokenType::Minus => {
expect_type!(right_type, "i64", op.loc); expect_types!(right_type, ["f64", "i64"], op.loc);
Ok("i64".into()) Ok(right_type)
} }
TokenType::Bang => { TokenType::Bang => {
expect_types!(right_type, ["bool", "i64", "ptr", "u8"], op.loc); expect_types!(right_type, ["bool", "i64", "ptr", "u8"], op.loc);
@@ -549,6 +559,12 @@ impl<'a> TypeChecker<'a> {
Ok(field.field_type.clone()) Ok(field.field_type.clone())
} }
ExprKind::Cast { expr, type_name } => { ExprKind::Cast { expr, type_name } => {
if type_name.lexeme == "f64" {
return error!(
&type_name.loc,
"use _builtin_cvtsi2sd and _builtin_cvttsd2si to cast to f64"
);
}
self.typecheck_expr(env, expr)?; self.typecheck_expr(env, expr)?;
if !self.is_valid_type_name(&type_name.lexeme) { if !self.is_valid_type_name(&type_name.lexeme) {
return error!( return error!(

68
test.zr
View File

@@ -1,32 +1,52 @@
func run_test[x: str] : void struct TestRunner
build_blacklist := ["examples/euler", "examples/raylib.zr", "examples/chip8.zr", "examples/sqlite_todo.zr"] build_blacklist: Array
run_blacklist := ["guess_number.zr", "tcp_server.zr", "udp_server.zr"] run_blacklist: Array
custom_build_flags: HashMap
custom_run_args: HashMap
for i in 0..build_blacklist->size func TestRunner.run_directory[tr: TestRunner, dir: str] : void
if x->equal(build_blacklist->nth(i)) ~files, ok := os.list_directory(dir)
io.printf("Skipping %s...\n", x) if !ok
return panic("failed to open test directory")
for i in 0..files->size
joined := dir->concat("/")->concat(files->nth(i))
if os.is_a_directory(joined)
tr->run_directory(joined)
else
tr->run_test(joined)
func TestRunner.run_test[tr: TestRunner, x: str] : void
name := os.basename(x)
if tr->build_blacklist->contains_str(name)
io.printf("Skipping %s...\n", x)
return
io.printf("Building %s...\n", x) io.printf("Building %s...\n", x)
build_cmd := "./target/release/zern "->concat(x)
~custom_build_flags, ok := tr->custom_build_flags->get(name)
if ok
build_cmd = build_cmd->concat(" ")->concat(custom_build_flags)
build_start_time := os.time() build_start_time := os.time()
~status, ok := os.run_shell_command("./target/release/zern "->concat(x)) ~status, ok := os.run_shell_command(build_cmd)
if !ok || status != 0 if !ok || status != 0
os.exit(1) os.exit(1)
build_end_time := os.time() build_end_time := os.time()
io.printf("%dms\n", build_end_time - build_start_time) io.printf("%dms\n", build_end_time - build_start_time)
for i in 0..run_blacklist->size if tr->run_blacklist->contains_str(name)
if x->contains(run_blacklist->nth(i)) io.printf("Skipping %s...\n", x)
io.printf("Skipping %s...\n", x) return
return
io.printf("Running %s...\n", x) io.printf("Running %s...\n", x)
run_cmd := "./out" run_cmd := "./out"
if x->equal("examples/curl.zr") ~custom_run_args, ok := tr->custom_run_args->get(name)
run_cmd = run_cmd->concat(" http://example.com") if ok
run_cmd = run_cmd->concat(" ")->concat(custom_run_args)
run_start_time := os.time() run_start_time := os.time()
~status, ok := os.run_shell_command(run_cmd) ~status, ok := os.run_shell_command(run_cmd)
@@ -37,18 +57,20 @@ func run_test[x: str] : void
io.printf("Running %s took %dms\n", x, run_end_time - run_start_time) io.printf("Running %s took %dms\n", x, run_end_time - run_start_time)
func run_directory[dir: str] : void
~files, ok := os.list_directory(dir)
if !ok
panic("failed to open test directory")
for i in 0..files->size
run_test(dir->concat(files->nth(i)))
func main[] : i64 func main[] : i64
~status, ok := os.run_shell_command("cargo build --release") ~status, ok := os.run_shell_command("cargo build --release")
if !ok || status != 0 if !ok || status != 0
os.exit(1) os.exit(1)
run_directory("examples/") tr := new TestRunner
run_directory("examples/euler/") tr->build_blacklist = []
tr->run_blacklist = ["raylib.zr", "sqlite_todo.zr", "guess_number.zr", "udp_server.zr", "chip8.zr", "tcp_server.zr"]
tr->custom_build_flags = HashMap.new()
tr->custom_build_flags->insert("raylib.zr", "-m -C \"-lraylib\"")
tr->custom_build_flags->insert("chip8.zr", "-m -C \"-lraylib\"")
tr->custom_build_flags->insert("sqlite_todo.zr", "-m -C \"-lsqlite3\"")
tr->custom_run_args = HashMap.new()
tr->custom_run_args->insert("curl.zr", "http://example.com")
tr->run_directory("examples")
io.println("DONE") io.println("DONE")