changes
This commit is contained in:
parent
06b97d3132
commit
0d81019cb1
9 changed files with 399 additions and 210 deletions
2
blank.html
Normal file
2
blank.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- blank html file + miniserve because of cors -->
|
12
src/data.rs
12
src/data.rs
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashMap;
|
// TODO: TypedStatement and TypedExpression?
|
||||||
|
|
||||||
use crate::lexer::{Token, Symbol};
|
use crate::lexer::{Token, Symbol};
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ pub enum BinaryOp {
|
||||||
LogicOr,
|
LogicOr,
|
||||||
// TODO
|
// TODO
|
||||||
// Set,
|
// Set,
|
||||||
Comma,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -50,7 +49,8 @@ pub struct Func {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub params: Vec<(String, Type)>,
|
pub params: Vec<(String, Type)>,
|
||||||
pub ret: Type,
|
pub ret: Type,
|
||||||
pub block: Block
|
pub block: Block,
|
||||||
|
pub public: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -109,7 +109,7 @@ impl BinaryOp {
|
||||||
Self::BitOr => (6, 7),
|
Self::BitOr => (6, 7),
|
||||||
Self::LogicAnd => (4, 5),
|
Self::LogicAnd => (4, 5),
|
||||||
Self::LogicOr => (2, 3),
|
Self::LogicOr => (2, 3),
|
||||||
Self::Comma => (0, 1),
|
// Self::Comma => (0, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,10 +169,10 @@ impl Type {
|
||||||
pub fn string(&self) -> String {
|
pub fn string(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Integer => "i32".into(),
|
Self::Integer => "i32".into(),
|
||||||
Self::Float => "f32".into(),
|
Self::Float => "f64".into(),
|
||||||
Self::Boolean => "i32".into(),
|
Self::Boolean => "i32".into(),
|
||||||
Self::Tuple(v) => {
|
Self::Tuple(v) => {
|
||||||
let s: Vec<_> = v.into_iter().map(Type::string).collect();
|
let s: Vec<_> = v.iter().map(Type::string).collect();
|
||||||
format!("({})", s.join(", "))
|
format!("({})", s.join(", "))
|
||||||
},
|
},
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub enum Error {
|
||||||
SyntaxError(String),
|
SyntaxError(String),
|
||||||
TypeError(String),
|
TypeError(String),
|
||||||
ReferenceError(String),
|
ReferenceError(String),
|
||||||
|
IoError(std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
@ -14,3 +15,9 @@ impl Error {
|
||||||
Error::TypeError(what.to_string())
|
Error::TypeError(what.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Error::IoError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
438
src/generator.rs
438
src/generator.rs
|
@ -1,11 +1,13 @@
|
||||||
/*
|
/*
|
||||||
optimizations
|
optimizations
|
||||||
|
|
||||||
- use i32.eqz when comparing to zero
|
- [ ] use i32.eqz when comparing to zero
|
||||||
- write negative numberss directly instead of as positive + sign flip
|
- [x] write negative numbers directly instead of as positive + sign flip
|
||||||
|
- [ ] replace `local.set` + `local.get` with `local.tee`
|
||||||
|
- [ ] don't create local variables at all if they're only used once
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::data::{self, BinaryOp, Block, Expr, Func, Literal, Pattern, PrefixOp, Statement, Type};
|
use crate::data::{BinaryOp, Expr, Func, Literal, Pattern, PrefixOp, Statement, Type};
|
||||||
use crate::parser::Context;
|
use crate::parser::Context;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
|
@ -13,57 +15,86 @@ pub struct Generator {
|
||||||
output: Box<dyn std::io::Write>,
|
output: Box<dyn std::io::Write>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Allocator {
|
||||||
|
count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// const PRELUDE: &str = r#"
|
||||||
|
// ;; begin prelude
|
||||||
|
// (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
|
||||||
|
// (export "memory" (memory 0))
|
||||||
|
// ;; (func $print (param $offset i32) (param $length i32)
|
||||||
|
// (func $print (param $string i32)
|
||||||
|
// ;; (i32.store (i32.const 0) (local.get $offset)) ;; iov.iov_base (pointer to string)
|
||||||
|
// ;; (i32.store (i32.const 4) (local.get $length)) ;; iov.iov_len (length of string)
|
||||||
|
// ;; (call $fd_write
|
||||||
|
// ;; (i32.const 1) ;; fd: stdout = 1
|
||||||
|
// ;; (i32.const 0) ;; data: pointer to memory - this is the first memory we create (index 0)
|
||||||
|
// ;; (i32.const 1) ;; data_len: there's 1 string
|
||||||
|
// ;; (i32.const 0) ;; nwritten: i don't care about this, write it wherever
|
||||||
|
// ;; )
|
||||||
|
// ;; drop
|
||||||
|
// )
|
||||||
|
// ;; end prelude
|
||||||
|
// "#;
|
||||||
|
|
||||||
impl Generator {
|
impl Generator {
|
||||||
pub fn new(output: Box<dyn std::io::Write>) -> Generator {
|
pub fn new(output: Box<dyn std::io::Write>) -> Generator {
|
||||||
Generator { output }
|
Generator { output }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_module(&mut self, stmts: &[Statement]) -> Result<(), Error> {
|
pub fn write_module(&mut self, stmts: &[Statement]) -> Result<(), Error> {
|
||||||
|
writeln!(self.output, "(module")?;
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
write!(self.output, "(module");
|
// ctx.funcs.insert("print".to_string(), (vec![("message".to_string(), Type::String)], Type::empty()));
|
||||||
|
|
||||||
|
// writeln!(self.output, "{}", PRELUDE)?;
|
||||||
|
|
||||||
|
let mut gctx = GenContext::new();
|
||||||
|
let mut alloc = Allocator::new();
|
||||||
|
get_locals(&Expr::Block(crate::data::Block(stmts.to_vec())), &mut alloc, &mut ctx, &mut gctx);
|
||||||
|
// get_strings(&Expr::Block(crate::data::Block(stmts.to_vec())), &mut gctx);
|
||||||
|
// if !gctx.strings.is_empty() {
|
||||||
|
// writeln!(self.output, "(memory 1)")?;
|
||||||
|
// for (s, offset) in &mut gctx.strings {
|
||||||
|
// writeln!(self.output, "(data $strings (i32.const {}) \"{}\")", offset, s.replace("\n", "\\n").replace("\"", "\\\""))?;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
for stmt in stmts {
|
for stmt in stmts {
|
||||||
match stmt {
|
match stmt {
|
||||||
Statement::Func(Func { name, ret, params, .. }) => {
|
Statement::Func(func) => self.write_func(&gctx, &ctx, func)?,
|
||||||
ctx.funcs.insert(name.clone(), (params.clone(), ret.clone()));
|
|
||||||
}
|
|
||||||
Statement::Let(..) | Statement::TailExpr(..) | Statement::Expr (..) => {
|
Statement::Let(..) | Statement::TailExpr(..) | Statement::Expr (..) => {
|
||||||
return Err(Error::syn("incorrect top level statement"))
|
return Err(Error::syn("incorrect top level statement"))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for stmt in stmts {
|
writeln!(self.output, ")")?;
|
||||||
match stmt {
|
|
||||||
Statement::Func(func) => self.write_func(&ctx, func)?,
|
|
||||||
_ => {
|
|
||||||
return Err(Error::syn("incorrect top level statement"))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
write!(self.output, ")");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_func(&mut self, ctx: &Context, func: &Func) -> Result<(), Error> {
|
fn write_func(&mut self, parent_gctx: &GenContext, ctx: &Context, func: &Func) -> Result<(), Error> {
|
||||||
write!(self.output, " (func ${}", func.name);
|
write!(self.output, "(func ${}", func.name)?;
|
||||||
if func.name == "main" {
|
if func.name == "main" {
|
||||||
write!(self.output, " (export \"_start\")");
|
write!(self.output, " (export \"_start\")")?;
|
||||||
|
} else if func.public {
|
||||||
|
write!(self.output, " (export \"{}\")", func.name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let expr = Expr::Block(func.block.clone());
|
let expr = Expr::Block(func.block.clone());
|
||||||
let mut ctx = ctx.clone();
|
let mut ctx = ctx.clone();
|
||||||
let mut exprs = Vec::new();
|
|
||||||
|
|
||||||
for (name, ty) in &func.params {
|
for (name, ty) in &func.params {
|
||||||
ctx.locals.insert(name.clone(), ty.clone());
|
ctx.locals.insert(name.clone(), ty.clone());
|
||||||
write!(self.output, " (param ${} {})", name, ty.string());
|
write!(self.output, " (param ${} {})", name, ty.string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !func.ret.is_empty() {
|
if !func.ret.is_empty() {
|
||||||
write!(self.output, " (result {})", func.ret.string());
|
write!(self.output, " (result {})", func.ret.string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(self.output);
|
writeln!(self.output)?;
|
||||||
writeln!(self.output, "(local $match i32)");
|
writeln!(self.output, "(local $match i32)")?;
|
||||||
|
|
||||||
let inferred = func.block.infer(&ctx)?;
|
let inferred = func.block.infer(&ctx)?;
|
||||||
if func.ret != inferred {
|
if func.ret != inferred {
|
||||||
|
@ -73,170 +104,263 @@ impl Generator {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
get_locals(&expr, &mut ctx, &mut exprs);
|
let mut alloc = Allocator::new();
|
||||||
|
let mut gctx = GenContext::new();
|
||||||
|
get_locals(&expr, &mut alloc, &mut ctx, &mut gctx);
|
||||||
|
let exprs = gctx.exprs;
|
||||||
|
|
||||||
for (name, _) in &exprs {
|
for (name, expr) in &exprs {
|
||||||
let ty = match ctx.locals.get(name).unwrap() {
|
let ty = match expr.infer(&ctx).unwrap() {
|
||||||
Type::Integer => "i32",
|
Type::Integer => "i32",
|
||||||
Type::Float => "f64",
|
Type::Float => "f64",
|
||||||
|
// Type::String => "i32",
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
};
|
};
|
||||||
println!("(local ${name} {ty})");
|
writeln!(self.output, "(local ${name} {ty})")?;
|
||||||
}
|
}
|
||||||
for (name, expr) in &exprs {
|
for (name, expr) in &exprs {
|
||||||
gen_expr(expr, &ctx);
|
self.gen_expr(expr, parent_gctx, &ctx)?;
|
||||||
println!("local.set ${name}");
|
writeln!(self.output, "local.set ${name}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
gen_expr(&expr, &ctx);
|
self.gen_expr(&expr, parent_gctx, &ctx)?;
|
||||||
|
|
||||||
write!(self.output, ")");
|
writeln!(self.output, ")")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gen_expr(&mut self, expr: &Expr, parent_gctx: &GenContext, ctx: &Context) -> Result<(), Error> {
|
||||||
|
match expr {
|
||||||
|
Expr::Literal(lit) => match lit {
|
||||||
|
Literal::Integer(int) => writeln!(self.output, "i32.const {int}")?,
|
||||||
|
Literal::Float(f) => writeln!(self.output, "f64.const {f}")?,
|
||||||
|
Literal::Boolean(b) => writeln!(self.output, "i32.const {}", if *b { 1 } else { 0 })?,
|
||||||
|
// Literal::String(s) => writeln!(self.output, "i32.const {}", parent_gctx.strings.iter().find_map(|(s2, off)| (s == s2).then_some(off)).unwrap())?,
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
Expr::Variable(name) => {
|
||||||
|
writeln!(self.output, "local.get ${name}")?;
|
||||||
|
}
|
||||||
|
Expr::Binary(op, a, b) => {
|
||||||
|
self.gen_expr(a, parent_gctx, ctx)?;
|
||||||
|
self.gen_expr(b, parent_gctx, ctx)?;
|
||||||
|
|
||||||
|
let ty = match expr.infer(ctx).unwrap() {
|
||||||
|
Type::Integer => "i32",
|
||||||
|
Type::Float => "f64",
|
||||||
|
Type::Boolean => "i32",
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
match op {
|
||||||
|
BinaryOp::Add => writeln!(self.output, "{ty}.add")?,
|
||||||
|
BinaryOp::Mul => writeln!(self.output, "{ty}.mul")?,
|
||||||
|
BinaryOp::Sub => writeln!(self.output, "{ty}.sub")?,
|
||||||
|
BinaryOp::Div => writeln!(self.output, "{ty}.div_u")?, // do i _u or _s?
|
||||||
|
BinaryOp::Mod => writeln!(self.output, "{ty}.rem_u")?,
|
||||||
|
BinaryOp::Eq => writeln!(self.output, "{ty}.eq")?,
|
||||||
|
BinaryOp::Neq => writeln!(self.output, "{ty}.neq")?,
|
||||||
|
BinaryOp::Less => writeln!(self.output, "{ty}.lt_u")?,
|
||||||
|
BinaryOp::Greater => writeln!(self.output, "{ty}.gt_u")?,
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Unary(op, e) => {
|
||||||
|
if let Expr::Literal(lit) = e.as_ref() {
|
||||||
|
match lit {
|
||||||
|
Literal::Integer(int) => writeln!(self.output, "i32.const {}", -int)?,
|
||||||
|
Literal::Float(f) => writeln!(self.output, "f64.const {}", -f)?,
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gen_expr(e, parent_gctx, ctx)?;
|
||||||
|
match op {
|
||||||
|
PrefixOp::Minus => {
|
||||||
|
// this is so inefficent, but i don't care
|
||||||
|
writeln!(self.output, "i32.const -1")?;
|
||||||
|
writeln!(self.output, "i32.mul")?;
|
||||||
|
}
|
||||||
|
PrefixOp::LogicNot => {
|
||||||
|
writeln!(self.output, "i32.eqz")?;
|
||||||
|
}
|
||||||
|
PrefixOp::BitNot => {
|
||||||
|
// TODO: do i flip the sign bit?
|
||||||
|
writeln!(self.output, "i32.const {}", i32::MAX)?;
|
||||||
|
writeln!(self.output, "i32.xor")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: awful code until i fix patching up parser and lexer
|
||||||
|
Expr::Match(cond, arms) => {
|
||||||
|
self.gen_expr(cond, parent_gctx, ctx)?;
|
||||||
|
writeln!(self.output, "local.set $match")?;
|
||||||
|
|
||||||
|
for (idx, (pat, expr)) in arms.iter().enumerate() {
|
||||||
|
match pat {
|
||||||
|
Pattern::Literal(lit) => match lit {
|
||||||
|
Literal::Integer(int) => writeln!(self.output, "i32.const {}", int)?,
|
||||||
|
Literal::Boolean(b) => writeln!(self.output, "i32.const {}", if *b { 1 } else { 0 })?,
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(self.output, "local.get $match")?;
|
||||||
|
writeln!(self.output, "i32.eq")?;
|
||||||
|
writeln!(self.output, "(if (result i32) (then")?;
|
||||||
|
self.gen_expr(expr, parent_gctx, ctx)?;
|
||||||
|
|
||||||
|
if idx == arms.len() - 1 {
|
||||||
|
// TODO: verify its actually unreachable earlier on
|
||||||
|
writeln!(self.output, ") (else unreachable")?;
|
||||||
|
} else {
|
||||||
|
writeln!(self.output, ") (else")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(self.output, "{}", ")".repeat(arms.len() * 2))?;
|
||||||
|
}
|
||||||
|
Expr::Block(b) => {
|
||||||
|
let mut ctx = ctx.clone();
|
||||||
|
for (_i, stmt) in b.0.iter().enumerate() {
|
||||||
|
match stmt {
|
||||||
|
Statement::TailExpr(expr) => {
|
||||||
|
self.gen_expr(expr, parent_gctx, &ctx)?;
|
||||||
|
}
|
||||||
|
Statement::Expr(expr) => {
|
||||||
|
self.gen_expr(expr, parent_gctx, &ctx)?;
|
||||||
|
if !expr.infer(&ctx).unwrap().is_empty() {
|
||||||
|
writeln!(self.output, "drop")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Statement::Let(name, expr) => {
|
||||||
|
let ty = expr.infer(&ctx).unwrap();
|
||||||
|
ctx.locals.insert(name.clone(), ty);
|
||||||
|
// exprs.push((name.clone(), expr.clone()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Call(func, args) => {
|
||||||
|
expr.infer(ctx)?;
|
||||||
|
for expr in args {
|
||||||
|
self.gen_expr(expr, parent_gctx, ctx)?;
|
||||||
|
}
|
||||||
|
writeln!(self.output, "call ${func}")?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(statements: &[Statement]) -> Result<(), Error> {
|
// FIXME: not sure what i was thinking
|
||||||
let mut gen = Generator::new(Box::new(std::io::stdout()));
|
impl Allocator {
|
||||||
gen.write_module(statements)
|
fn new() -> Allocator {
|
||||||
|
Allocator {
|
||||||
|
count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alloc_ident(&mut self, name: &str) -> String {
|
||||||
|
// self.count += 1;
|
||||||
|
// format!("{name}_{}", self.count)
|
||||||
|
name.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_expr(expr: &Expr, ctx: &Context) {
|
#[derive(Debug)]
|
||||||
match expr {
|
struct GenContext {
|
||||||
Expr::Literal(lit) => match lit {
|
exprs: Vec<(String, Expr)>,
|
||||||
Literal::Integer(int) => println!("i32.const {int}"),
|
strings: Vec<(String, usize)>,
|
||||||
Literal::Float(f) => println!("f64.const {f}"),
|
|
||||||
Literal::Boolean(b) => println!("i32.const {}", if *b { 1 } else { 0 }),
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
Expr::Variable(name) => {
|
|
||||||
println!("local.get ${name}");
|
|
||||||
}
|
|
||||||
Expr::Binary(op, a, b) => {
|
|
||||||
gen_expr(&a, ctx);
|
|
||||||
gen_expr(&b, ctx);
|
|
||||||
|
|
||||||
let ty = match expr.infer(&ctx).unwrap() {
|
|
||||||
Type::Integer => "i32",
|
|
||||||
Type::Float => "f64",
|
|
||||||
Type::Boolean => "i32",
|
|
||||||
_ => todo!(),
|
|
||||||
};
|
|
||||||
match op {
|
|
||||||
BinaryOp::Add => println!("{ty}.add"),
|
|
||||||
BinaryOp::Mul => println!("{ty}.mul"),
|
|
||||||
BinaryOp::Sub => println!("{ty}.sub"),
|
|
||||||
BinaryOp::Div => println!("{ty}.div_u"), // do i _u or _s?
|
|
||||||
BinaryOp::Mod => println!("{ty}.rem_u"),
|
|
||||||
BinaryOp::Eq => println!("{ty}.eq"),
|
|
||||||
BinaryOp::Neq => println!("{ty}.neq"),
|
|
||||||
BinaryOp::Less => println!("{ty}.lt_u"),
|
|
||||||
BinaryOp::Greater => println!("{ty}.gt_u"),
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Expr::Unary(op, e) => {
|
|
||||||
if let Expr::Literal(lit) = e.as_ref() {
|
|
||||||
match lit {
|
|
||||||
Literal::Integer(int) => println!("i32.const {}", -int),
|
|
||||||
Literal::Float(f) => println!("f64.const {}", -f),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gen_expr(e, ctx);
|
|
||||||
match op {
|
|
||||||
PrefixOp::Minus => {
|
|
||||||
// this is so inefficent, but i don't care
|
|
||||||
println!("i32.const -1");
|
|
||||||
println!("i32.mul");
|
|
||||||
}
|
|
||||||
PrefixOp::LogicNot => {
|
|
||||||
println!("i32.eqz");
|
|
||||||
}
|
|
||||||
PrefixOp::BitNot => {
|
|
||||||
// TODO: do i flip the sign bit?
|
|
||||||
println!("i32.const {}", i32::MAX);
|
|
||||||
println!("i32.xor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// FIXME: awful code until i fix patching up parser and lexer
|
|
||||||
Expr::Match(cond, arms) => {
|
|
||||||
gen_expr(cond, ctx);
|
|
||||||
println!("local.set $match");
|
|
||||||
|
|
||||||
for (idx, (pat, expr)) in arms.iter().enumerate() {
|
|
||||||
match pat {
|
|
||||||
Pattern::Literal(lit) => match lit {
|
|
||||||
Literal::Integer(int) => println!("i32.const {}", int),
|
|
||||||
Literal::Boolean(b) => println!("i32.const {}", if *b { 1 } else { 0 }),
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("local.get $match");
|
|
||||||
println!("i32.eq");
|
|
||||||
println!("(if (result i32) (then");
|
|
||||||
gen_expr(expr, ctx);
|
|
||||||
|
|
||||||
if idx == arms.len() - 1 {
|
|
||||||
// TODO: verify its actually unreachable earlier on
|
|
||||||
println!(") (else unreachable");
|
|
||||||
} else {
|
|
||||||
println!(") (else");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("{}", ")".repeat(arms.len() * 2));
|
|
||||||
}
|
|
||||||
Expr::Block(b) => {
|
|
||||||
for (i, stmt) in b.0.iter().enumerate() {
|
|
||||||
match stmt {
|
|
||||||
Statement::TailExpr(expr) => {
|
|
||||||
gen_expr(expr, &ctx);
|
|
||||||
}
|
|
||||||
Statement::Expr(expr) => {
|
|
||||||
gen_expr(expr, &ctx);
|
|
||||||
println!("drop");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Expr::Call(func, args) => {
|
|
||||||
for expr in args {
|
|
||||||
gen_expr(expr, ctx);
|
|
||||||
}
|
|
||||||
println!("call ${func}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: block scoped variables
|
impl GenContext {
|
||||||
fn get_locals(expr: &Expr, ctx: &mut Context, exprs: &mut Vec<(String, Expr)>) {
|
pub fn new() -> GenContext {
|
||||||
|
GenContext {
|
||||||
|
exprs: vec![],
|
||||||
|
strings: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_locals(expr: &Expr, alloc: &mut Allocator, ctx: &mut Context, gctx: &mut GenContext) {
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Block(b) => {
|
Expr::Block(b) => {
|
||||||
for stmt in &b.0 {
|
for stmt in &b.0 {
|
||||||
match stmt {
|
match stmt {
|
||||||
Statement::Let(name, expr) => {
|
Statement::Let(name, expr) => {
|
||||||
|
// FIXME: block scoped variables (name collisions)
|
||||||
|
get_locals(expr, alloc, ctx, gctx);
|
||||||
|
|
||||||
let ty = expr.infer(ctx).unwrap();
|
let ty = expr.infer(ctx).unwrap();
|
||||||
ctx.locals.insert(name.clone(), ty);
|
ctx.locals.insert(name.clone(), ty);
|
||||||
exprs.push((name.clone(), expr.clone()));
|
gctx.exprs.push((alloc.alloc_ident(name), expr.clone()));
|
||||||
}
|
}
|
||||||
Statement::TailExpr(expr) => get_locals(&expr, ctx, exprs),
|
Statement::TailExpr(expr) => get_locals(expr, alloc, ctx, gctx),
|
||||||
Statement::Expr(expr) => get_locals(&expr, ctx, exprs),
|
Statement::Expr(expr) => get_locals(expr, alloc, ctx, gctx),
|
||||||
Statement::Func(Func { name, ret, .. }) => {
|
Statement::Func(Func { name, ret, params, .. }) => {
|
||||||
ctx.locals.insert(name.clone(), todo!());
|
ctx.funcs.insert(name.clone(), (params.clone(), ret.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Unary(_, expr) => get_locals(&expr, ctx, exprs),
|
Expr::Unary(_, expr) => get_locals(expr, alloc, ctx, gctx),
|
||||||
Expr::Binary(_, a, b) => {
|
Expr::Binary(_, a, b) => {
|
||||||
get_locals(&a, ctx, exprs);
|
get_locals(a, alloc, ctx, gctx);
|
||||||
get_locals(&b, ctx, exprs);
|
get_locals(b, alloc, ctx, gctx);
|
||||||
}
|
}
|
||||||
_ => (),
|
Expr::Literal(Literal::String(s)) => {
|
||||||
|
let offset = gctx.strings.last().map(|(s, off)| off + s.len()).unwrap_or(8);
|
||||||
|
gctx.strings.push((s.clone(), offset));
|
||||||
|
}
|
||||||
|
Expr::Match(pat, arms) => {
|
||||||
|
get_locals(pat, alloc, ctx, gctx);
|
||||||
|
for (_, arm) in arms {
|
||||||
|
get_locals(arm, alloc, ctx, gctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Call(_, exprs) => {
|
||||||
|
for expr in exprs {
|
||||||
|
get_locals(expr, alloc, ctx, gctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Variable(..) | Expr::Literal(..) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fn get_strings(expr: &Expr, gctx: &mut GenContext) {
|
||||||
|
// match expr {
|
||||||
|
// Expr::Block(b) => {
|
||||||
|
// for stmt in &b.0 {
|
||||||
|
// match stmt {
|
||||||
|
// Statement::Let(_, expr) => get_strings(expr, gctx),
|
||||||
|
// Statement::TailExpr(expr) => get_strings(expr, gctx),
|
||||||
|
// Statement::Expr(expr) => get_strings(expr, gctx),
|
||||||
|
// Statement::Func(Func { block, .. }) => get_strings(&Expr::Block(block.clone()), gctx),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Expr::Unary(_, expr) => get_strings(expr, gctx),
|
||||||
|
// Expr::Binary(_, a, b) => {
|
||||||
|
// get_strings(a, gctx);
|
||||||
|
// get_strings(b, gctx);
|
||||||
|
// }
|
||||||
|
// Expr::Literal(Literal::String(s)) => {
|
||||||
|
// let offset = gctx.strings.last().map(|(s, off)| off + s.len()).unwrap_or(8);
|
||||||
|
// gctx.strings.push((s.clone(), offset));
|
||||||
|
// }
|
||||||
|
// Expr::Match(pat, arms) => {
|
||||||
|
// get_strings(pat, gctx);
|
||||||
|
// for (_, arm) in arms {
|
||||||
|
// get_strings(arm, gctx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Expr::Call(_, exprs) => {
|
||||||
|
// for expr in exprs {
|
||||||
|
// get_strings(expr, gctx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Expr::Variable(..) | Expr::Literal(..) => (),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -24,6 +24,7 @@ pub enum Token {
|
||||||
True, False,
|
True, False,
|
||||||
If, Else, Match,
|
If, Else, Match,
|
||||||
While, Loop, For, Break, Continue,
|
While, Loop, For, Break, Continue,
|
||||||
|
Pub,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
@ -89,6 +90,7 @@ impl Lexer {
|
||||||
"for" => Token::For,
|
"for" => Token::For,
|
||||||
"break" => Token::Break,
|
"break" => Token::Break,
|
||||||
"continue" => Token::Continue,
|
"continue" => Token::Continue,
|
||||||
|
"pub" => Token::Pub,
|
||||||
ident => Token::Ident(ident.to_string()),
|
ident => Token::Ident(ident.to_string()),
|
||||||
},
|
},
|
||||||
ch if ch.is_whitespace() => {
|
ch if ch.is_whitespace() => {
|
||||||
|
@ -103,7 +105,7 @@ impl Lexer {
|
||||||
fn lex_number(&mut self) -> Result<Token, Error> {
|
fn lex_number(&mut self) -> Result<Token, Error> {
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
let radix = match (self.input[self.pos], self.input.get(self.pos + 1)) {
|
let radix = match (self.input[self.pos], self.input.get(self.pos + 1)) {
|
||||||
('0', Some(ch)) if ch.is_digit(10) => 10,
|
('0', Some(ch)) if ch.is_ascii_digit() => 10,
|
||||||
('0', Some(ch)) => {
|
('0', Some(ch)) => {
|
||||||
self.pos += 2;
|
self.pos += 2;
|
||||||
match ch {
|
match ch {
|
||||||
|
|
61
src/main.rs
61
src/main.rs
|
@ -4,7 +4,7 @@ a second time when generating (so the types are known), there should be
|
||||||
a better way
|
a better way
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#![allow(unused)]
|
#![allow(dead_code, clippy::single_match, clippy::only_used_in_recursion)]
|
||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -14,19 +14,7 @@ mod parser;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
use parser::Context;
|
use generator::Generator;
|
||||||
|
|
||||||
use crate::data::Statement;
|
|
||||||
|
|
||||||
pub struct Foo {
|
|
||||||
a: u8,
|
|
||||||
b: Bar,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Bar {
|
|
||||||
a: u8,
|
|
||||||
b: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// let mut lexer = lexer::Lexer::new("
|
// let mut lexer = lexer::Lexer::new("
|
||||||
|
@ -54,21 +42,44 @@ fn main() {
|
||||||
// ".into());
|
// ".into());
|
||||||
let mut lexer = lexer::Lexer::new("
|
let mut lexer = lexer::Lexer::new("
|
||||||
fn main() -> i32 {
|
fn main() -> i32 {
|
||||||
if is_three(add_four(-1)) {
|
let n = -1;
|
||||||
|
if is_three(add_four(n)) {
|
||||||
20
|
20
|
||||||
} else {
|
} else {
|
||||||
30
|
30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_four(n: i32) -> i32 {
|
pub fn add_four(n: i32) -> i32 {
|
||||||
n + 4
|
n + 4
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_three(n: i32) -> bool {
|
fn is_three(n: i32) -> bool {
|
||||||
n == 3
|
n == 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_four_float(n: f64) -> f64 {
|
||||||
|
n + 4.0
|
||||||
|
}
|
||||||
".into());
|
".into());
|
||||||
|
// let mut lexer = lexer::Lexer::new("
|
||||||
|
// fn main() -> i32 {
|
||||||
|
// let x = {
|
||||||
|
// let y = 20;
|
||||||
|
// y + 10
|
||||||
|
// };
|
||||||
|
// x
|
||||||
|
// }
|
||||||
|
// ".into());
|
||||||
|
// let mut lexer = lexer::Lexer::new(r#"
|
||||||
|
// fn main() {
|
||||||
|
// let x = "example string!\n";
|
||||||
|
// let y = "another str.\n";
|
||||||
|
// print(x);
|
||||||
|
// print(y);
|
||||||
|
// print("Hello, world!");
|
||||||
|
// }
|
||||||
|
// "#.into());
|
||||||
|
|
||||||
let mut tokens = vec![];
|
let mut tokens = vec![];
|
||||||
loop {
|
loop {
|
||||||
|
@ -89,17 +100,7 @@ fn main() {
|
||||||
loop {
|
loop {
|
||||||
match parser.next() {
|
match parser.next() {
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
Ok(Some(tree)) => {
|
Ok(Some(tree)) => statements.push(tree),
|
||||||
// dbg!(&tree);
|
|
||||||
// match &tree {
|
|
||||||
// Statement::Expr(expr) => match expr.infer(&Context::new()) {
|
|
||||||
// Ok(ty) => eprintln!("type: {:?}", ty),
|
|
||||||
// Err(err) => eprintln!("err: {:?}", err),
|
|
||||||
// },
|
|
||||||
// _ => todo!(),
|
|
||||||
// };
|
|
||||||
statements.push(tree);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("error: {:?}", error);
|
eprintln!("error: {:?}", error);
|
||||||
return;
|
return;
|
||||||
|
@ -107,7 +108,11 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = generator::generate(&statements) {
|
// dbg!(&statements);
|
||||||
|
|
||||||
|
let mut gen = Generator::new(Box::new(std::io::stdout()));
|
||||||
|
|
||||||
|
if let Err(err) = gen.write_module(&statements) {
|
||||||
panic!("{:?}", err);
|
panic!("{:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl Parser {
|
||||||
|
|
||||||
fn parse_statement(&mut self) -> Result<Option<Statement>, Error> {
|
fn parse_statement(&mut self) -> Result<Option<Statement>, Error> {
|
||||||
let Some(tok) = self.peek_tok() else {
|
let Some(tok) = self.peek_tok() else {
|
||||||
return Err(Error::SyntaxError(format!("unexpected eof")));
|
return Err(Error::syn("unexpected eof"));
|
||||||
};
|
};
|
||||||
let stmt = match tok {
|
let stmt = match tok {
|
||||||
Token::Let => {
|
Token::Let => {
|
||||||
|
@ -58,7 +58,14 @@ impl Parser {
|
||||||
self.eat(Token::Symbol(Symbol::Semicolon))?;
|
self.eat(Token::Symbol(Symbol::Semicolon))?;
|
||||||
Statement::Let(name, expr)
|
Statement::Let(name, expr)
|
||||||
}
|
}
|
||||||
Token::Fn => {
|
Token::Pub | Token::Fn => {
|
||||||
|
// TODO: public things that aren't functions
|
||||||
|
let public = if tok == &Token::Pub {
|
||||||
|
self.eat(Token::Pub)?;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
self.eat(Token::Fn)?;
|
self.eat(Token::Fn)?;
|
||||||
let name = self.parse_ident()?;
|
let name = self.parse_ident()?;
|
||||||
self.eat(Token::OpenParan)?;
|
self.eat(Token::OpenParan)?;
|
||||||
|
@ -86,7 +93,7 @@ impl Parser {
|
||||||
self.eat(Token::OpenBrace)?;
|
self.eat(Token::OpenBrace)?;
|
||||||
let block = self.parse_block()?;
|
let block = self.parse_block()?;
|
||||||
self.eat(Token::CloseBrace)?;
|
self.eat(Token::CloseBrace)?;
|
||||||
Statement::Func(Func { name, params, ret, block })
|
Statement::Func(Func { name, params, ret, block, public })
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let expr = self.parse_expr(0)?;
|
let expr = self.parse_expr(0)?;
|
||||||
|
@ -103,29 +110,29 @@ impl Parser {
|
||||||
|
|
||||||
fn parse_type(&mut self) -> Result<Type, Error> {
|
fn parse_type(&mut self) -> Result<Type, Error> {
|
||||||
let Some(tok) = self.next_tok() else {
|
let Some(tok) = self.next_tok() else {
|
||||||
return Err(Error::SyntaxError(format!("unexpected eof")));
|
return Err(Error::syn("unexpected eof"));
|
||||||
};
|
};
|
||||||
match tok {
|
let ty = match tok {
|
||||||
Token::Ident(ident) => match ident.as_str() {
|
Token::Ident(ident) => match ident.as_str() {
|
||||||
"i32" => Ok(Type::Integer),
|
"i32" => Type::Integer,
|
||||||
"bool" => Ok(Type::Boolean),
|
"f64" => Type::Float,
|
||||||
|
"bool" => Type::Boolean,
|
||||||
_ => return Err(Error::TypeError(format!("unknown type {ident}"))),
|
_ => return Err(Error::TypeError(format!("unknown type {ident}"))),
|
||||||
},
|
},
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
};
|
||||||
|
Ok(ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ident(&mut self) -> Result<String, Error> {
|
fn parse_ident(&mut self) -> Result<String, Error> {
|
||||||
match self.next_tok() {
|
match self.next_tok() {
|
||||||
Some(Token::Ident(ident)) => Ok(ident.to_string()),
|
Some(Token::Ident(ident)) => Ok(ident.to_string()),
|
||||||
Some(tk) => {
|
Some(tk) => {
|
||||||
return Err(Error::SyntaxError(format!(
|
Err(Error::SyntaxError(format!(
|
||||||
"expected identifier, got {tk:?}"
|
"expected identifier, got {tk:?}"
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
None => {
|
None => Err(Error::syn("expected identifier, got eof")),
|
||||||
return Err(Error::SyntaxError(format!("expected identifier, got eof")))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +149,7 @@ impl Parser {
|
||||||
statements.push(stmt);
|
statements.push(stmt);
|
||||||
match self.peek_tok() {
|
match self.peek_tok() {
|
||||||
Some(Token::CloseBrace) => break,
|
Some(Token::CloseBrace) => break,
|
||||||
tok => return Err(Error::SyntaxError(format!("unexpected token {tok:?}"))),
|
Some(tok) => return Err(Error::SyntaxError(format!("unexpected token {tok:?}"))),
|
||||||
None => return Err(Error::syn("unexpected eof")),
|
None => return Err(Error::syn("unexpected eof")),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -153,7 +160,6 @@ impl Parser {
|
||||||
// Some(_) => return Err(Error::SyntaxError(format!("unexpected token {tok:?}"))),
|
// Some(_) => return Err(Error::SyntaxError(format!("unexpected token {tok:?}"))),
|
||||||
// tok => return Err(Error::SyntaxError(format!("unexpected token {tok:?}"))),
|
// tok => return Err(Error::SyntaxError(format!("unexpected token {tok:?}"))),
|
||||||
_ => (),
|
_ => (),
|
||||||
None => return Err(Error::syn("unexpected eof")),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(Block(statements))
|
Ok(Block(statements))
|
||||||
|
@ -172,7 +178,7 @@ impl Parser {
|
||||||
Token::Ident(ident) => {
|
Token::Ident(ident) => {
|
||||||
let ident = ident.clone();
|
let ident = ident.clone();
|
||||||
if self.peek_tok().is_some_and(|t| *t == Token::OpenParan) {
|
if self.peek_tok().is_some_and(|t| *t == Token::OpenParan) {
|
||||||
self.eat(Token::OpenParan);
|
self.eat(Token::OpenParan)?;
|
||||||
let mut params = vec![];
|
let mut params = vec![];
|
||||||
while !self.peek_tok().is_some_and(|tok| tok == &Token::CloseParan) {
|
while !self.peek_tok().is_some_and(|tok| tok == &Token::CloseParan) {
|
||||||
params.push(self.parse_expr(0)?);
|
params.push(self.parse_expr(0)?);
|
||||||
|
@ -182,7 +188,7 @@ impl Parser {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.eat(Token::CloseParan);
|
self.eat(Token::CloseParan)?;
|
||||||
Expr::Call(ident, params)
|
Expr::Call(ident, params)
|
||||||
} else {
|
} else {
|
||||||
Expr::Variable(ident)
|
Expr::Variable(ident)
|
||||||
|
@ -190,6 +196,7 @@ impl Parser {
|
||||||
}
|
}
|
||||||
Token::False => Expr::Literal(Literal::Boolean(false)),
|
Token::False => Expr::Literal(Literal::Boolean(false)),
|
||||||
Token::True => Expr::Literal(Literal::Boolean(true)),
|
Token::True => Expr::Literal(Literal::Boolean(true)),
|
||||||
|
Token::String(s) => Expr::Literal(Literal::String(s.to_string())),
|
||||||
Token::Char(ch) => Expr::Literal(Literal::Char(*ch)),
|
Token::Char(ch) => Expr::Literal(Literal::Char(*ch)),
|
||||||
Token::If => {
|
Token::If => {
|
||||||
let cond = self.parse_expr(0)?;
|
let cond = self.parse_expr(0)?;
|
||||||
|
@ -222,7 +229,7 @@ impl Parser {
|
||||||
Expr::Match(Box::new(cond), map)
|
Expr::Match(Box::new(cond), map)
|
||||||
}
|
}
|
||||||
Token::Symbol(_) => {
|
Token::Symbol(_) => {
|
||||||
let Some(op) = PrefixOp::from_token(&tok) else {
|
let Some(op) = PrefixOp::from_token(tok) else {
|
||||||
return Err(Error::SyntaxError(format!("unexpected token {tok:?}")));
|
return Err(Error::SyntaxError(format!("unexpected token {tok:?}")));
|
||||||
};
|
};
|
||||||
let expr = self.parse_expr(1)?;
|
let expr = self.parse_expr(1)?;
|
||||||
|
|
BIN
test.wasm
Normal file
BIN
test.wasm
Normal file
Binary file not shown.
44
test.wat
44
test.wat
|
@ -1 +1,43 @@
|
||||||
(module (func $name (export "_start") (result i32) i32.const 10))
|
(module
|
||||||
|
(func $main (export "_start") (result i32)
|
||||||
|
(local $match i32)
|
||||||
|
(local $n i32)
|
||||||
|
i32.const -1
|
||||||
|
local.set $n
|
||||||
|
local.get $n
|
||||||
|
call $add_four
|
||||||
|
call $is_three
|
||||||
|
local.set $match
|
||||||
|
i32.const 1
|
||||||
|
local.get $match
|
||||||
|
i32.eq
|
||||||
|
(if (result i32) (then
|
||||||
|
i32.const 20
|
||||||
|
) (else
|
||||||
|
i32.const 0
|
||||||
|
local.get $match
|
||||||
|
i32.eq
|
||||||
|
(if (result i32) (then
|
||||||
|
i32.const 30
|
||||||
|
) (else unreachable
|
||||||
|
))))
|
||||||
|
)
|
||||||
|
(func $add_four (export "add_four") (param $n i32) (result i32)
|
||||||
|
(local $match i32)
|
||||||
|
local.get $n
|
||||||
|
i32.const 4
|
||||||
|
i32.add
|
||||||
|
)
|
||||||
|
(func $is_three (param $n i32) (result i32)
|
||||||
|
(local $match i32)
|
||||||
|
local.get $n
|
||||||
|
i32.const 3
|
||||||
|
i32.eq
|
||||||
|
)
|
||||||
|
(func $add_four_float (param $n f64) (result f64)
|
||||||
|
(local $match i32)
|
||||||
|
local.get $n
|
||||||
|
f64.const 4
|
||||||
|
f64.add
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue