mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 搭建原生函数基础设施
This commit is contained in:
@@ -57,6 +57,7 @@ pub(crate) struct CompileCtx {
|
||||
pub(crate) switch_stack: Vec<Label>,
|
||||
pub(crate) label_counter: u32,
|
||||
pub(crate) projected_pc: usize,
|
||||
pub(crate) builtin_reg_map: Vec<(String, u8)>,
|
||||
}
|
||||
|
||||
impl CompileCtx {
|
||||
@@ -72,6 +73,7 @@ impl CompileCtx {
|
||||
switch_stack: Vec::new(),
|
||||
label_counter: 0,
|
||||
projected_pc: 0,
|
||||
builtin_reg_map: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +166,19 @@ impl CompileCtx {
|
||||
pub(crate) fn current_switch(&self) -> Option<&Label> {
|
||||
self.switch_stack.last()
|
||||
}
|
||||
|
||||
pub(crate) fn pre_register_builtins(&mut self) {
|
||||
let builtins = [
|
||||
"NaN", "undefined", "Object", "Array", "String", "Number", "Boolean",
|
||||
"Function", "Error", "Math", "JSON", "Promise", "parseInt", "parseFloat",
|
||||
"isNaN", "isFinite",
|
||||
];
|
||||
for name in &builtins {
|
||||
let reg = self.alloc_reg();
|
||||
self.symbols.pre_register_global(name, reg);
|
||||
self.builtin_reg_map.push((name.to_string(), reg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
@@ -173,6 +188,7 @@ impl Compiler {
|
||||
|
||||
pub fn compile(&self, program: &oxide_parser::Program) -> Result<CompiledModule, String> {
|
||||
let mut ctx = CompileCtx::new();
|
||||
ctx.pre_register_builtins();
|
||||
|
||||
for stmt in &program.body {
|
||||
self.count_statement(stmt, &mut ctx);
|
||||
@@ -205,6 +221,7 @@ impl Compiler {
|
||||
bytecode: ctx.bytecode,
|
||||
constants: ctx.constants,
|
||||
n_registers: ctx.max_regs,
|
||||
builtin_reg_map: ctx.builtin_reg_map,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,13 +199,14 @@ impl Compiler {
|
||||
}
|
||||
Expression::CallExpression(call) => {
|
||||
self.count_expression(&call.callee, ctx);
|
||||
ctx.alloc_reg();
|
||||
for arg in &call.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
self.count_expression(expr, ctx);
|
||||
}
|
||||
}
|
||||
ctx.alloc_reg();
|
||||
ctx.projected_pc += 1; // CALL
|
||||
ctx.projected_pc += 2; // CALL + extension word (arg_count)
|
||||
}
|
||||
Expression::AssignmentExpression(assign) => {
|
||||
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left
|
||||
|
||||
@@ -668,10 +668,10 @@ impl Compiler {
|
||||
ctx.emit(opcode::encode(op, var_reg, rhs, 0));
|
||||
Ok(var_reg)
|
||||
} else {
|
||||
return Err(format!(
|
||||
Err(format!(
|
||||
"compound assignment operator {:?} not supported",
|
||||
assign.operator
|
||||
));
|
||||
))
|
||||
}
|
||||
} else {
|
||||
let val_reg = self.emit_expression(&assign.right, ctx)?;
|
||||
@@ -738,6 +738,37 @@ impl Compiler {
|
||||
Ok(r)
|
||||
}
|
||||
Expression::ParenthesizedExpression(p) => self.emit_expression(&p.expression, ctx),
|
||||
Expression::CallExpression(call) => {
|
||||
let callee_reg = self.emit_expression(&call.callee, ctx)?;
|
||||
let this_idx = ctx.add_constant(Constant::Undefined);
|
||||
let this_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(
|
||||
OpCode::LOAD_CONST,
|
||||
this_reg,
|
||||
(this_idx & 0xFF) as u8,
|
||||
((this_idx >> 8) & 0xFF) as u8,
|
||||
));
|
||||
let mut arg_regs = Vec::new();
|
||||
for arg in &call.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
arg_regs.push(self.emit_expression(expr, ctx)?);
|
||||
}
|
||||
}
|
||||
let first_arg_reg = if arg_regs.is_empty() {
|
||||
0u8
|
||||
} else {
|
||||
arg_regs[0]
|
||||
};
|
||||
let result_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(
|
||||
OpCode::CALL,
|
||||
callee_reg,
|
||||
this_reg,
|
||||
first_arg_reg,
|
||||
));
|
||||
ctx.emit(arg_regs.len() as u32);
|
||||
Ok(result_reg)
|
||||
}
|
||||
_ => Err(format!("unsupported expression type: {:?}", expr)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +125,15 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) {
|
||||
std::mem::discriminant(&un.operator).hash(h);
|
||||
hash_expression(&un.argument, h);
|
||||
}
|
||||
Expression::CallExpression(_) => {
|
||||
Expression::CallExpression(call) => {
|
||||
2u8.hash(h);
|
||||
(call.arguments.len() as u32).hash(h);
|
||||
hash_expression(&call.callee, h);
|
||||
for arg in &call.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
hash_expression(expr, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression::LogicalExpression(log) => {
|
||||
3u8.hash(h);
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct CompiledModule {
|
||||
pub bytecode: Vec<opcode::Instr>,
|
||||
pub constants: Vec<Constant>,
|
||||
pub n_registers: u8,
|
||||
pub builtin_reg_map: Vec<(String, u8)>,
|
||||
}
|
||||
|
||||
impl fmt::Display for CompiledModule {
|
||||
|
||||
@@ -73,6 +73,13 @@ impl SymbolTable {
|
||||
reg_for_new
|
||||
}
|
||||
|
||||
pub fn pre_register_global(&mut self, name: &str, reg: u8) {
|
||||
self.scopes[0].entry(name.to_string()).or_insert(Binding {
|
||||
reg,
|
||||
initialized: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init_var(&mut self, name: &str) {
|
||||
let current = self.scopes.last_mut().unwrap();
|
||||
if let Some(b) = current.get_mut(name) {
|
||||
|
||||
@@ -13,3 +13,4 @@ oxide_types = { path = "../oxide_types" }
|
||||
dashmap.workspace = true
|
||||
hashbrown.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
bumpalo.workspace = true
|
||||
|
||||
@@ -151,6 +151,30 @@ impl BuiltinWorld {
|
||||
symbol_constructor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_method(
|
||||
proto: &mut JsObject,
|
||||
shape_forge: &ShapeForge,
|
||||
string_forge: &StringForge,
|
||||
method_name: &str,
|
||||
native_fn_ptr: *const (),
|
||||
arg_count: u8,
|
||||
) -> Result<(), String> {
|
||||
let si = string_forge.intern(method_name).0;
|
||||
let mut wrapper = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
|
||||
wrapper.set_function(true);
|
||||
wrapper.set_native_fn(Some(native_fn_ptr));
|
||||
wrapper.set_native_arg_count(arg_count);
|
||||
let wrapper_val = JsValue::from_js_object(Box::into_raw(wrapper));
|
||||
let new_offset = proto.prop_count();
|
||||
let new_shape = shape_forge.make_shape(proto.shape_id(), si);
|
||||
proto.set_shape_id(new_shape);
|
||||
proto.set_prop_count(new_offset + 1);
|
||||
let bump = bumpalo::Bump::new();
|
||||
proto.set_prop_expand(new_offset, wrapper_val, &bump);
|
||||
proto.bump_generation();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_types::mem::P;
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use crate::builtin::BuiltinWorld;
|
||||
use crate::code_forge::CodeForge;
|
||||
use crate::prop_forge::PropForge;
|
||||
use crate::shape_forge::ShapeForge;
|
||||
use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
|
||||
use crate::string_forge::StringForge;
|
||||
|
||||
pub struct KernelConfig {
|
||||
@@ -65,6 +69,7 @@ pub struct OxideKernel {
|
||||
pub code_forge: Arc<CodeForge>,
|
||||
pub prop_forge: Arc<PropForge>,
|
||||
pub builtin_world: Arc<BuiltinWorld>,
|
||||
pub global_object: P<JsObject>,
|
||||
}
|
||||
|
||||
impl OxideKernel {
|
||||
@@ -75,6 +80,23 @@ impl OxideKernel {
|
||||
let code_forge = Arc::new(CodeForge::new());
|
||||
let prop_forge = Arc::new(PropForge::new());
|
||||
|
||||
let mut global_obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
|
||||
let si_nan = string_forge.intern("NaN").0;
|
||||
let si_undef = string_forge.intern("undefined").0;
|
||||
|
||||
let nan_shape = shape_forge.make_shape(EMPTY_SHAPE_ID, si_nan);
|
||||
global_obj.set_shape_id(nan_shape);
|
||||
global_obj.set_prop_count(1);
|
||||
global_obj.set_inline_prop(0, JsValue::float(f64::NAN));
|
||||
|
||||
let undef_shape = shape_forge.make_shape(nan_shape, si_undef);
|
||||
global_obj.set_shape_id(undef_shape);
|
||||
global_obj.set_prop_count(2);
|
||||
global_obj.set_inline_prop(1, JsValue::undefined());
|
||||
|
||||
let global_object = P::new(global_obj);
|
||||
|
||||
Self {
|
||||
config,
|
||||
string_forge,
|
||||
@@ -82,6 +104,7 @@ impl OxideKernel {
|
||||
code_forge,
|
||||
prop_forge,
|
||||
builtin_world,
|
||||
global_object,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +131,10 @@ impl OxideKernel {
|
||||
pub fn config(&self) -> &KernelConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn global_object(&self) -> &P<JsObject> {
|
||||
&self.global_object
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2,37 +2,40 @@ use crate::value::JsValue;
|
||||
|
||||
pub type ShapeId = u32;
|
||||
|
||||
/// Layout:
|
||||
/// Layout (72B):
|
||||
/// header: u32 bits
|
||||
/// [0:23] shape_id
|
||||
/// [24:28] prop_count (5 bits, 0-31)
|
||||
/// [29] is_array
|
||||
/// [30] is_extensible
|
||||
/// [31] is_function
|
||||
/// native_arg_count: u8 (1 byte + 3 pad)
|
||||
/// inline[0..=3]: 4 x JsValue (32 bytes)
|
||||
/// overflow: *mut JsValue (8 bytes, null if ≤4 properties)
|
||||
/// overflow: *mut JsValue (8 bytes, null if <=4 properties)
|
||||
/// proto: JsValue (8 bytes)
|
||||
/// generation: u32 (4 bytes)
|
||||
/// _pad: u32 (4 bytes, 64B total = one cache line)
|
||||
/// generation: u32 (4 bytes + 4 pad)
|
||||
/// native_fn: u64 (8 bytes, 0 = None sentinel)
|
||||
#[repr(C)]
|
||||
pub struct JsObject {
|
||||
header: u32,
|
||||
native_arg_count: u8,
|
||||
inline: [JsValue; 4],
|
||||
overflow: *mut JsValue,
|
||||
proto: JsValue,
|
||||
generation: u32,
|
||||
_pad: u32,
|
||||
native_fn: u64,
|
||||
}
|
||||
|
||||
impl JsObject {
|
||||
pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self {
|
||||
Self {
|
||||
header: (shape_id & 0x00FF_FFFF) | (1 << 30),
|
||||
native_arg_count: 0,
|
||||
inline: [JsValue::undefined(); 4],
|
||||
overflow: std::ptr::null_mut(),
|
||||
proto,
|
||||
generation: 1,
|
||||
_pad: 0,
|
||||
native_fn: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +47,12 @@ impl JsObject {
|
||||
) -> Self {
|
||||
let mut obj = Self {
|
||||
header: (shape_id & 0x00FF_FFFF) | (1 << 30) | (1 << 29),
|
||||
native_arg_count: 0,
|
||||
inline: [JsValue::undefined(); 4],
|
||||
overflow: std::ptr::null_mut(),
|
||||
proto,
|
||||
generation: 1,
|
||||
_pad: 0,
|
||||
native_fn: 0,
|
||||
};
|
||||
let count = (n_elements as u8).min(31);
|
||||
obj.set_prop_count(count);
|
||||
@@ -189,6 +193,26 @@ impl JsObject {
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn native_fn(&self) -> Option<*const ()> {
|
||||
if self.native_fn == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.native_fn as *const ())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_native_fn(&mut self, ptr: Option<*const ()>) {
|
||||
self.native_fn = ptr.map_or(0, |p| p as u64);
|
||||
}
|
||||
|
||||
pub fn native_arg_count(&self) -> u8 {
|
||||
self.native_arg_count
|
||||
}
|
||||
|
||||
pub fn set_native_arg_count(&mut self, n: u8) {
|
||||
self.native_arg_count = n;
|
||||
}
|
||||
|
||||
pub fn alloc_overflow(&mut self, _bump: &bumpalo::Bump, new_count: usize) -> *mut JsValue {
|
||||
use std::alloc::Layout;
|
||||
debug_assert!(new_count > 4);
|
||||
@@ -213,11 +237,11 @@ mod tests {
|
||||
use crate::shape::EMPTY_SHAPE_ID;
|
||||
|
||||
#[test]
|
||||
fn object_size_64_bytes() {
|
||||
fn object_size_72_bytes() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<JsObject>(),
|
||||
64,
|
||||
"JsObject layout mismatch — expected 64B (one cache line)"
|
||||
72,
|
||||
"JsObject layout mismatch - expected 72B"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_kernel::kernel::OxideKernel;
|
||||
|
||||
pub fn register_global_properties(_kernel: &Arc<OxideKernel>, _bump: &bumpalo::Bump) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod global;
|
||||
@@ -1,6 +1,8 @@
|
||||
#![doc = "OxideJS - Register-based VM with epoch arena memory"]
|
||||
|
||||
pub mod coercion;
|
||||
pub mod native;
|
||||
pub mod builtins;
|
||||
pub mod vm;
|
||||
pub mod vm_pool;
|
||||
pub use oxide_types::value::JsValue;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
pub type NativeResult = Result<JsValue, JsValue>;
|
||||
|
||||
pub type NativeFn = fn(&mut crate::vm::Vm, args: &[u8]) -> NativeResult;
|
||||
@@ -7,6 +7,7 @@ use oxide_compiler::module::CompiledModule;
|
||||
use oxide_compiler::opcode::{self, OpCode};
|
||||
|
||||
use crate::coercion;
|
||||
use crate::native::NativeFn;
|
||||
use oxide_kernel::kernel::{KernelConfig, OxideKernel};
|
||||
use oxide_kernel::prop_forge::PropTemplate;
|
||||
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
|
||||
@@ -172,6 +173,27 @@ impl Vm {
|
||||
self.regs = [JsValue::undefined(); 256];
|
||||
self.frames.clear();
|
||||
|
||||
for (name, reg) in &module.builtin_reg_map {
|
||||
let si = self.kernel.string_forge().intern(name.as_str()).0;
|
||||
let global = self.kernel.global_object();
|
||||
let mut shape_id = global.shape_id();
|
||||
let mut found: Option<u8> = None;
|
||||
while shape_id != EMPTY_SHAPE_ID {
|
||||
if let Some(shape) = self.kernel.shape_forge().get_shape(shape_id) {
|
||||
if shape.property_name == si {
|
||||
found = Some(shape.property_offset);
|
||||
break;
|
||||
}
|
||||
shape_id = shape.parent.unwrap_or(EMPTY_SHAPE_ID);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(offset) = found {
|
||||
self.regs[*reg as usize] = global.get_prop(offset);
|
||||
}
|
||||
}
|
||||
|
||||
self.dispatch()
|
||||
}
|
||||
|
||||
@@ -353,6 +375,45 @@ impl Vm {
|
||||
}
|
||||
|
||||
OpCode::CALL => {
|
||||
let callee_reg = rd;
|
||||
let this_reg = a as u8;
|
||||
let first_arg_reg = b as u8;
|
||||
|
||||
let callee = self.regs[callee_reg];
|
||||
|
||||
if callee.is_object() {
|
||||
let obj_ptr = callee.as_js_object_ptr();
|
||||
if !obj_ptr.is_null() {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
if obj.is_function() && obj.native_fn().is_some() {
|
||||
let ext = self.bytecode[self.pc];
|
||||
self.pc += 1;
|
||||
let arg_count = (ext & 0xFF) as usize;
|
||||
|
||||
let mut args_buf = [0u8; 257];
|
||||
args_buf[0] = this_reg;
|
||||
for i in 0..arg_count.min(256) {
|
||||
args_buf[i + 1] =
|
||||
first_arg_reg.wrapping_add(i as u8);
|
||||
}
|
||||
let args_slice = &args_buf[..arg_count + 1];
|
||||
|
||||
let func: NativeFn =
|
||||
unsafe { std::mem::transmute(obj.native_fn().unwrap()) };
|
||||
match func(self, args_slice) {
|
||||
Ok(val) => self.regs[0] = val,
|
||||
Err(err_val) => {
|
||||
return Err(format!(
|
||||
"Native error: {:?}",
|
||||
err_val
|
||||
));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let offset = opcode::offset16(instr) as usize;
|
||||
self.frames.push(CallFrame {
|
||||
return_addr: self.pc,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<oxide_types::value::JsValue, String> {
|
||||
let allocator = Allocator::default();
|
||||
let program =
|
||||
oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||
let module = Compiler::new()
|
||||
.compile(&program)
|
||||
.map_err(|e| format!("Compile error: {}", e))?;
|
||||
let mut vm = Vm::new();
|
||||
vm.run(&module)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nan_returns_nan() {
|
||||
let result = eval("NaN").unwrap();
|
||||
assert!(result.is_double());
|
||||
assert!(result.as_double().is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_undefined_returns_undefined() {
|
||||
let result = eval("undefined").unwrap();
|
||||
assert!(result.is_undefined());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeof_nan_is_number() {
|
||||
let result = eval("typeof NaN").unwrap();
|
||||
assert!(result.is_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeof_undefined_is_undefined() {
|
||||
let result = eval("typeof undefined").unwrap();
|
||||
assert!(result.is_string());
|
||||
}
|
||||
Reference in New Issue
Block a user