Skip to content

Commit a2a5bc6

Browse files
committed
Allow reading (g)megabuf
1 parent 29badc2 commit a2a5bc6

12 files changed

+151
-37
lines changed

compiler-rs/src/builtin_functions.rs

+54-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
use parity_wasm::elements::{BlockType, FuncBody, Instruction, Instructions, Local, ValueType};
1+
use parity_wasm::elements::{
2+
BlockType, FuncBody, FunctionType, Instruction, Instructions, Local, ValueType,
3+
};
24

5+
use crate::constants::{BUFFER_SIZE, EPSILON};
6+
use crate::utils::f64_const;
37
use crate::EelFunctionType;
48

59
#[derive(PartialEq, Eq, Hash)]
610
pub enum BuiltinFunction {
711
Div,
12+
GetBufferIndex,
813
}
914

1015
impl BuiltinFunction {
1116
pub fn get_type(&self) -> EelFunctionType {
1217
match self {
13-
Self::Div => (2, 1),
18+
Self::Div => {
19+
FunctionType::new(vec![ValueType::F64, ValueType::F64], vec![ValueType::F64])
20+
}
21+
Self::GetBufferIndex => FunctionType::new(vec![ValueType::F64], vec![ValueType::I32]),
1422
}
1523
}
1624

@@ -32,6 +40,50 @@ impl BuiltinFunction {
3240
Instruction::End,
3341
]),
3442
),
43+
// Takes a float buffer index and converts it to an int. Values out of range
44+
// are returned as `-1`.
45+
//
46+
// NOTE: There's actually a subtle bug that exists in Milkdrop's Eel
47+
// implementation, which we reproduce here.
48+
//
49+
// Wasm's `trunc()` rounds towards zero. This means that for index `-1` we
50+
// will return zero, since: `roundTowardZero(-1 + EPSILON) == 0`
51+
//
52+
// A subsequent check handles negative indexes, so negative indexes > than
53+
// `-1` are not affected.
54+
Self::GetBufferIndex => FuncBody::new(
55+
vec![Local::new(1, ValueType::F64), Local::new(1, ValueType::I32)],
56+
Instructions::new(vec![
57+
Instruction::F64Const(f64_const(EPSILON)),
58+
Instruction::GetLocal(0),
59+
Instruction::F64Add,
60+
// STACK: [$i + EPSILON]
61+
Instruction::TeeLocal(1), // $with_near
62+
Instruction::I32TruncSF64,
63+
// TODO We could probably make this a tee and get rid of the next get if we swap the final condition
64+
Instruction::SetLocal(2),
65+
// STACK: []
66+
Instruction::I32Const(-1),
67+
Instruction::GetLocal(2),
68+
// STACK: [-1, $truncated]
69+
Instruction::I32Const(8),
70+
Instruction::I32Mul,
71+
// STACK: [-1, $truncated * 8]
72+
Instruction::GetLocal(2), // $truncated
73+
Instruction::I32Const(0),
74+
// STACK: [-1, $truncated * 8, $truncated, 0]
75+
Instruction::I32LtS,
76+
// STACK: [-1, $truncated * 8, <is index less than 0>]
77+
Instruction::GetLocal(2), // $truncated
78+
Instruction::I32Const(BUFFER_SIZE as i32 - 1),
79+
Instruction::I32GtS,
80+
// STACK: [-1, $truncated * 8, <is index less than 0>, <is index more than MAX>]
81+
Instruction::I32Or,
82+
// STACK: [-1, $truncated * 8, <is index out of range>]
83+
Instruction::Select,
84+
Instruction::End,
85+
]),
86+
),
3587
}
3688
}
3789
}

compiler-rs/src/constants.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
pub static EPSILON: f64 = 0.00001;
2+
3+
pub static WASM_PAGE_SIZE: u32 = 65536;
4+
5+
static BYTES_PER_F64: u32 = 8;
6+
static BUFFER_COUNT: u32 = 2;
7+
8+
// The number of items allowed in each buffer (megabuf/gmegabuf).
9+
// https://github.com/WACUP/vis_milk2/blob/de9625a89e724afe23ed273b96b8e48496095b6c/ns-eel2/ns-eel.h#L145
10+
pub static BUFFER_SIZE: u32 = 65536 * 128;
11+
12+
pub static WASM_MEMORY_SIZE: u32 = (BUFFER_SIZE * BYTES_PER_F64 * BUFFER_COUNT) / WASM_PAGE_SIZE;

compiler-rs/src/emitter.rs

+69-16
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1-
use std::collections::{HashMap, HashSet};
1+
use std::{
2+
collections::{HashMap, HashSet},
3+
mem,
4+
};
25

36
use crate::{
47
ast::{
58
Assignment, BinaryExpression, BinaryOperator, EelFunction, Expression, ExpressionBlock,
69
FunctionCall, UnaryExpression, UnaryOperator,
710
},
811
builtin_functions::BuiltinFunction,
12+
constants::{BUFFER_SIZE, EPSILON, WASM_MEMORY_SIZE},
913
error::CompilerError,
1014
index_store::IndexStore,
1115
shim::Shim,
1216
span::Span,
17+
utils::f64_const,
1318
EelFunctionType,
1419
};
1520
use parity_wasm::elements::{
1621
BlockType, CodeSection, ExportEntry, ExportSection, External, Func, FuncBody, FunctionSection,
1722
FunctionType, GlobalEntry, GlobalSection, GlobalType, ImportEntry, ImportSection, InitExpr,
18-
Instruction, Instructions, Internal, Module, Section, Serialize, Type, TypeSection, ValueType,
23+
Instruction, Instructions, Internal, Local, MemorySection, MemoryType, Module, Section,
24+
Serialize, Type, TypeSection, ValueType,
1925
};
2026

2127
type EmitterResult<T> = Result<T, CompilerError>;
2228

23-
static EPSILON: f64 = 0.00001;
24-
2529
pub fn emit(
2630
eel_functions: Vec<(String, EelFunction, String)>,
2731
globals_map: HashMap<String, HashSet<String>>,
@@ -37,6 +41,7 @@ struct Emitter {
3741
builtin_functions: IndexStore<BuiltinFunction>,
3842
function_types: IndexStore<EelFunctionType>,
3943
builtin_offset: Option<u32>,
44+
locals: Vec<ValueType>,
4045
}
4146

4247
impl Emitter {
@@ -48,6 +53,7 @@ impl Emitter {
4853
function_types: Default::default(),
4954
builtin_functions: IndexStore::new(),
5055
builtin_offset: None,
56+
locals: Default::default(),
5157
}
5258
}
5359
fn emit(
@@ -90,6 +96,11 @@ impl Emitter {
9096
sections.push(Section::Import(import_section));
9197
}
9298
sections.push(Section::Function(self.emit_function_section(funcs)));
99+
100+
sections.push(Section::Memory(MemorySection::with_entries(vec![
101+
MemoryType::new(WASM_MEMORY_SIZE, Some(WASM_MEMORY_SIZE)),
102+
])));
103+
93104
if let Some(global_section) = self.emit_global_section() {
94105
sections.push(Section::Global(global_section));
95106
}
@@ -113,11 +124,12 @@ impl Emitter {
113124
let function_types = self
114125
.function_types
115126
.keys()
116-
.iter()
117-
.map(|(args, returns)| {
127+
.into_iter()
128+
.map(|function_type| {
118129
Type::Function(FunctionType::new(
119-
vec![ValueType::F64; *args],
120-
vec![ValueType::F64; *returns],
130+
// TODO: This is clone with more steps. What's going on
131+
function_type.params().to_vec(),
132+
function_type.results().to_vec(),
121133
))
122134
})
123135
.collect();
@@ -178,15 +190,25 @@ impl Emitter {
178190
let mut function_bodies = Vec::new();
179191
let mut function_definitions = Vec::new();
180192
for (i, (name, program, pool_name)) in eel_functions.into_iter().enumerate() {
193+
// Note: We assume self.locals has been rest during the previous run.
181194
self.current_pool = pool_name;
182195
exports.push(ExportEntry::new(
183196
name,
184197
Internal::Function(i as u32 + offset),
185198
));
186-
let locals = Vec::new();
187-
function_bodies.push(FuncBody::new(locals, self.emit_program(program)?));
188199

189-
let function_type = self.function_types.get((0, 0));
200+
let instructions = self.emit_program(program)?;
201+
202+
let local_types = mem::replace(&mut self.locals, Vec::new());
203+
204+
let locals = local_types
205+
.into_iter()
206+
.map(|type_| Local::new(1, type_))
207+
.collect();
208+
209+
function_bodies.push(FuncBody::new(locals, instructions));
210+
211+
let function_type = self.function_types.get(FunctionType::new(vec![], vec![]));
190212

191213
function_definitions.push(Func::new(function_type))
192214
}
@@ -334,6 +356,10 @@ impl Emitter {
334356
self.emit_expression(alternate, instructions)?;
335357
instructions.push(Instruction::End);
336358
}
359+
"megabuf" => self.emit_memory_access(&mut function_call, 0, instructions)?,
360+
"gmegabuf" => {
361+
self.emit_memory_access(&mut function_call, BUFFER_SIZE * 8, instructions)?
362+
}
337363
shim_name if Shim::from_str(shim_name).is_some() => {
338364
let shim = Shim::from_str(shim_name).unwrap();
339365
assert_arity(&function_call, shim.arity())?;
@@ -353,6 +379,33 @@ impl Emitter {
353379
Ok(())
354380
}
355381

382+
fn emit_memory_access(
383+
&mut self,
384+
function_call: &mut FunctionCall,
385+
memory_offset: u32,
386+
instructions: &mut Vec<Instruction>,
387+
) -> EmitterResult<()> {
388+
assert_arity(&function_call, 1)?;
389+
let index = self.resolve_local(ValueType::I32);
390+
self.emit_expression(function_call.arguments.pop().unwrap(), instructions)?;
391+
instructions.push(Instruction::Call(
392+
self.resolve_builtin_function(BuiltinFunction::GetBufferIndex),
393+
));
394+
//
395+
instructions.push(Instruction::TeeLocal(index));
396+
instructions.push(Instruction::I32Const(-1));
397+
instructions.push(Instruction::I32Ne);
398+
// STACK: [in range]
399+
instructions.push(Instruction::If(BlockType::Value(ValueType::F64)));
400+
instructions.push(Instruction::GetLocal(index));
401+
instructions.push(Instruction::F64Load(3, memory_offset));
402+
instructions.push(Instruction::Else);
403+
instructions.push(Instruction::F64Const(f64_const(0.0)));
404+
instructions.push(Instruction::End);
405+
406+
Ok(())
407+
}
408+
356409
fn resolve_variable(&mut self, name: String) -> u32 {
357410
let pool = if variable_is_register(&name) {
358411
None
@@ -363,6 +416,11 @@ impl Emitter {
363416
self.globals.get((pool, name))
364417
}
365418

419+
fn resolve_local(&mut self, type_: ValueType) -> u32 {
420+
self.locals.push(type_);
421+
return self.locals.len() as u32 - 1;
422+
}
423+
366424
fn resolve_builtin_function(&mut self, builtin: BuiltinFunction) -> u32 {
367425
self.function_types.ensure(builtin.get_type());
368426
let offset = self
@@ -378,11 +436,6 @@ fn emit_is_not_zeroish(instructions: &mut Vec<Instruction>) {
378436
instructions.push(Instruction::F64Gt);
379437
}
380438

381-
// TODO: There's got to be a better way.
382-
fn f64_const(value: f64) -> u64 {
383-
u64::from_le_bytes(value.to_le_bytes())
384-
}
385-
386439
fn variable_is_register(name: &str) -> bool {
387440
let chars: Vec<_> = name.chars().collect();
388441
// We avoided pulling in the regex crate! (But at what cost?)

compiler-rs/src/index_store.rs

-15
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,3 @@ impl<T: Eq + Hash> IndexStore<T> {
3636
self.map.keys().collect()
3737
}
3838
}
39-
40-
#[cfg(test)]
41-
mod tests {
42-
use super::*;
43-
use crate::EelFunctionType;
44-
#[test]
45-
fn tuple() {
46-
let mut function_types: IndexStore<EelFunctionType> = IndexStore::new();
47-
let one_arg_one_return = function_types.get((1, 1));
48-
let no_arg_one_return = function_types.get((0, 1));
49-
50-
assert_eq!(one_arg_one_return, 0);
51-
assert_eq!(no_arg_one_return, 1);
52-
}
53-
}

compiler-rs/src/lib.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod ast;
22
mod builtin_functions;
3+
mod constants;
34
mod emitter;
45
mod error;
56
mod file_chars;
@@ -9,6 +10,7 @@ mod parser;
910
mod shim;
1011
mod span;
1112
mod tokens;
13+
mod utils;
1214

1315
use std::collections::{HashMap, HashSet};
1416

@@ -17,11 +19,12 @@ use emitter::emit;
1719
use error::CompilerError;
1820
// Only exported for tests
1921
pub use lexer::Lexer;
22+
use parity_wasm::elements::FunctionType;
2023
pub use parser::parse;
2124
pub use tokens::Token;
2225
pub use tokens::TokenKind;
2326

24-
pub type EelFunctionType = (usize, usize);
27+
pub type EelFunctionType = FunctionType;
2528

2629
use wasm_bindgen::prelude::*;
2730

compiler-rs/src/shim.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::EelFunctionType;
2+
use parity_wasm::elements::{FunctionType, ValueType};
23

34
// TODO: We could use https://docs.rs/strum_macros/0.20.1/strum_macros/index.html
45
#[derive(PartialEq, Eq, Hash)]
@@ -8,7 +9,9 @@ pub enum Shim {
89

910
impl Shim {
1011
pub fn get_type(&self) -> EelFunctionType {
11-
(self.arity(), 1)
12+
match self {
13+
Shim::Sin => FunctionType::new(vec![ValueType::F64], vec![ValueType::F64]),
14+
}
1215
}
1316
pub fn arity(&self) -> usize {
1417
match self {

compiler-rs/src/utils.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// TODO: There's got to be a better way.
2+
pub fn f64_const(value: f64) -> u64 {
3+
u64::from_le_bytes(value.to_le_bytes())
4+
}

compiler-rs/tests/compatibility_test.rs

-2
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,6 @@ fn compatibility_tests() {
430430
"Less than or equal (false)",
431431
"Greater than or equal (true)",
432432
"Greater than or equal (false)",
433-
"Megabuf access",
434433
"Max index megabuf",
435434
"Max index + 1 megabuf",
436435
"Max index gmegabuf",
@@ -456,7 +455,6 @@ fn compatibility_tests() {
456455
"Assign return value",
457456
"EPSILON buffer indexes",
458457
"+EPSILON & rounding -#s toward 0",
459-
"Negative buffer index read as 0",
460458
"Negative buffer index",
461459
"Negative buffer index gmegabuf",
462460
"Negative buf index execs right hand side",

compiler-rs/tests/fixtures/wat/div.snapshot

+1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ g = 100 / 0
2424
else
2525
f64.const 0x0p+0 (;=0;)
2626
end)
27+
(memory (;0;) 2048 2048)
2728
(global (;0;) (mut f64) (f64.const 0x0p+0 (;=0;)))
2829
(export "test" (func 1)))

compiler-rs/tests/fixtures/wat/one_plus_one.snapshot

+1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
f64.const 0x1p+0 (;=1;)
1010
f64.add
1111
drop)
12+
(memory (;0;) 2048 2048)
1213
(export "test" (func 1)))

compiler-rs/tests/fixtures/wat/reg.snapshot

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ reg00=10
99
global.set 0
1010
global.get 0
1111
drop)
12+
(memory (;0;) 2048 2048)
1213
(global (;0;) (mut f64) (f64.const 0x0p+0 (;=0;)))
1314
(export "test" (func 1)))

compiler-rs/tests/fixtures/wat/sin.snapshot

+1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ sin(100)
88
f64.const 0x1.9p+6 (;=100;)
99
call 0
1010
drop)
11+
(memory (;0;) 2048 2048)
1112
(export "test" (func 1)))

0 commit comments

Comments
 (0)