feat: 搭建原生函数基础设施

This commit is contained in:
2026-06-07 01:07:45 +08:00
parent 5e78cb672f
commit 439b61e359
17 changed files with 271 additions and 15 deletions
Generated
+1
View File
@@ -783,6 +783,7 @@ dependencies = [
name = "oxide_kernel"
version = "0.1.0"
dependencies = [
"bumpalo",
"dashmap",
"hashbrown 0.14.5",
"oxide_compiler",
+17
View File
@@ -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,
})
}
}
+2 -1
View File
@@ -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
+33 -2
View File
@@ -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)),
}
}
+8 -1
View File
@@ -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);
+1
View File
@@ -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) {
+1
View File
@@ -13,3 +13,4 @@ oxide_types = { path = "../oxide_types" }
dashmap.workspace = true
hashbrown.workspace = true
rustc-hash.workspace = true
bumpalo.workspace = true
+24
View File
@@ -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)]
+28 -1
View File
@@ -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)]
+34 -10
View File
@@ -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"
);
}
+7
View File
@@ -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(())
}
+1
View File
@@ -0,0 +1 @@
pub mod global;
+2
View File
@@ -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;
+5
View File
@@ -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;
+61
View File
@@ -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());
}