feat: 支持可选链空值合并与逻辑赋值

This commit is contained in:
2026-06-18 15:26:16 +08:00
parent 3784db7b17
commit 32af37963e
14 changed files with 869 additions and 87 deletions
+20 -13
View File
@@ -17,21 +17,28 @@ pub(crate) fn is_int_literal(value: f64) -> bool {
}
pub(crate) fn is_side_effect_free(expr: &Expression) -> bool {
match expr {
Expression::NumericLiteral(_)
| Expression::StringLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::Identifier(_)
| Expression::RegExpLiteral(_)
| Expression::ThisExpression(_) => true,
Expression::ParenthesizedExpression(p) => is_side_effect_free(&p.expression),
Expression::BinaryExpression(bin) => is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right),
Expression::UnaryExpression(un) => {
!matches!(un.operator, UnaryOperator::Delete) && is_side_effect_free(&un.argument)
let mut stack = vec![expr];
while let Some(expr) = stack.pop() {
match expr {
Expression::NumericLiteral(_)
| Expression::StringLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::Identifier(_)
| Expression::RegExpLiteral(_)
| Expression::ThisExpression(_) => {}
Expression::ParenthesizedExpression(p) => stack.push(&p.expression),
Expression::BinaryExpression(bin) => {
stack.push(&bin.left);
stack.push(&bin.right);
}
Expression::UnaryExpression(un) if !matches!(un.operator, UnaryOperator::Delete) => {
stack.push(&un.argument);
}
_ => return false,
}
_ => false,
}
true
}
const BUILTIN_GLOBALS: &[&str] = &[
+220 -3
View File
@@ -1,11 +1,146 @@
use oxide_parser::{
AssignmentOperator, ClassElement, Expression, ForStatementInit, MethodDefinitionKind, SimpleAssignmentTarget,
Statement, UnaryOperator, VariableDeclarationKind,
AssignmentOperator, ChainElement, ClassElement, Expression, ForStatementInit, LogicalOperator,
MethodDefinitionKind, SimpleAssignmentTarget, Statement, UnaryOperator, VariableDeclarationKind,
};
use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label};
impl Compiler {
fn count_optional_guard(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg();
ctx.projected_pc += 2; // LOAD_VAR dup + JMP_IF_NULLISH
}
fn count_static_member_get_preserve_base(
&self, member: &oxide_parser::StaticMemberExpression, short_chain: bool, ctx: &mut CompileCtx,
) {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST key
ctx.alloc_reg();
ctx.projected_pc += 5; // LOAD_VAR copy + IC_GET_PROP + 3 ext words
}
fn count_computed_member_get_preserve_base(
&self, member: &oxide_parser::ComputedMemberExpression, short_chain: bool, ctx: &mut CompileCtx,
) {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // GET_PROP_DYNAMIC
}
fn count_chain_call(&self, call: &oxide_parser::CallExpression, short_chain: bool, ctx: &mut CompileCtx) {
match &call.callee {
Expression::StaticMemberExpression(member) => {
self.count_static_member_get_preserve_base(member, short_chain, ctx)
}
Expression::ComputedMemberExpression(member) => {
self.count_computed_member_get_preserve_base(member, short_chain, ctx);
}
Expression::PrivateFieldExpression(member) => {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 2;
}
_ => {
self.count_chainable_expression(&call.callee, short_chain, ctx);
if short_chain && call.optional {
self.count_optional_guard(ctx);
}
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST undefined this
}
}
if short_chain
&& call.optional
&& matches!(
&call.callee,
Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::PrivateFieldExpression(_)
)
{
self.count_optional_guard(ctx);
}
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.projected_pc += 2; // CALL + arg_count ext word
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR result <- regs[0]
}
fn count_chainable_expression(&self, expr: &Expression, short_chain: bool, ctx: &mut CompileCtx) {
match expr {
Expression::StaticMemberExpression(member) => {
self.count_static_member_get_preserve_base(member, short_chain, ctx)
}
Expression::ComputedMemberExpression(member) => {
self.count_computed_member_get_preserve_base(member, short_chain, ctx)
}
Expression::PrivateFieldExpression(member) => {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 2;
}
Expression::CallExpression(call) => self.count_chain_call(call, short_chain, ctx),
Expression::ChainExpression(chain) => self.count_chain_element(&chain.expression, short_chain, ctx),
_ => self.count_expression(expr, ctx),
}
}
fn count_chain_element(&self, element: &ChainElement, short_chain: bool, ctx: &mut CompileCtx) {
match element {
ChainElement::StaticMemberExpression(member) => {
self.count_static_member_get_preserve_base(member, short_chain, ctx)
}
ChainElement::ComputedMemberExpression(member) => {
self.count_computed_member_get_preserve_base(member, short_chain, ctx)
}
ChainElement::PrivateFieldExpression(member) => {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 2;
}
ChainElement::CallExpression(call) => self.count_chain_call(call, short_chain, ctx),
ChainElement::TSNonNullExpression(_) => {}
}
}
fn count_logical_assign_test(&self, op: LogicalOperator, id: u32, ctx: &mut CompileCtx) {
match op {
LogicalOperator::And | LogicalOperator::Or => {
ctx.projected_pc += 1;
}
LogicalOperator::Coalesce => {
ctx.projected_pc += 1; // JMP_IF_NULLISH to store body
ctx.projected_pc += 1; // JMP to end on non-nullish
ctx.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
}
}
}
fn count_binding_pattern(&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx) {
match pattern {
oxide_parser::BindingPattern::BindingIdentifier(_) => {
@@ -509,6 +644,28 @@ impl Compiler {
self.count_expression(&member.expression, ctx);
ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC
}
Expression::ChainExpression(chain) => {
match &chain.expression {
ChainElement::StaticMemberExpression(member) => {
self.count_expression(&member.object, ctx);
if member.optional {
self.count_optional_guard(ctx);
}
ctx.projected_pc += 2; // DELETE_PROP_STATIC + ext word
}
ChainElement::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
if member.optional {
self.count_optional_guard(ctx);
}
self.count_expression(&member.expression, ctx);
ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC
}
_ => {}
}
ctx.projected_pc += 1; // JMP over nullish true writer
ctx.projected_pc += 1; // LOAD_CONST true
}
_ => {}
}
}
@@ -570,6 +727,20 @@ impl Compiler {
}
Expression::AssignmentExpression(assign) => {
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
self.count_expression(&member.object, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST key
ctx.alloc_reg();
ctx.projected_pc += 5; // LOAD_VAR + IC_GET_PROP + 3 ext
self.count_logical_assign_test(logical_op, id, ctx);
self.count_expression(&assign.right, ctx);
ctx.projected_pc += 4; // IC_SET_PROP + 3 ext
ctx.projected_pc += 1; // LOAD_VAR result <- rhs
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
return;
}
self.count_expression(&member.object, ctx);
self.count_expression(&assign.right, ctx);
if assign.operator != AssignmentOperator::Assign {
@@ -581,6 +752,19 @@ impl Compiler {
ctx.projected_pc += 1; // IC_SET_PROP or COMPOUND_MEMBER_*
ctx.projected_pc += 3; // 3 IC ext words
} else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // GET_PROP_DYNAMIC
self.count_logical_assign_test(logical_op, id, ctx);
self.count_expression(&assign.right, ctx);
ctx.projected_pc += 1; // SET_PROP_DYNAMIC
ctx.projected_pc += 1; // LOAD_VAR result <- rhs
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
return;
}
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
self.count_expression(&assign.right, ctx);
@@ -591,6 +775,25 @@ impl Compiler {
self.count_expression(&assign.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 2; // LOAD_CONST private id + SET_PRIVATE
} else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(_) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR result <- current var
self.count_logical_assign_test(logical_op, id, ctx);
self.count_expression(&assign.right, ctx);
ctx.projected_pc += 1; // STORE_VAR
ctx.projected_pc += 1; // LOAD_VAR result <- rhs
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
} else {
self.count_expression(&assign.right, ctx);
if assign.operator != AssignmentOperator::Assign {
ctx.projected_pc += 1; // COMPOUND_* on var
} else {
ctx.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR
}
}
} else if let oxide_parser::AssignmentTarget::ArrayAssignmentTarget(ap) = &assign.left {
self.count_expression(&assign.right, ctx);
self.count_array_assignment(ap, ctx);
@@ -638,7 +841,11 @@ impl Compiler {
let id = ctx.next_label_id();
ctx.alloc_reg(); // dup register
ctx.projected_pc += 1; // LOAD_VAR (DUP)
ctx.projected_pc += 1; // JMP_IF_FALSE or JMP_IF_TRUE
ctx.projected_pc += 1; // JMP_IF_FALSE, JMP_IF_TRUE, or JMP_IF_NULLISH
if matches!(log.operator, LogicalOperator::Coalesce) {
ctx.projected_pc += 1; // JMP over RHS on non-nullish
ctx.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
}
self.count_expression(&log.right, ctx);
ctx.projected_pc += 1; // LOAD_VAR (overwrite)
let skip_label = match log.operator {
@@ -649,6 +856,16 @@ impl Compiler {
ctx.label_map.insert(skip_label, ctx.projected_pc);
}
}
Expression::ChainExpression(chain) => {
let id = ctx.next_label_id();
self.count_chain_element(&chain.expression, true, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR result <- chain value
ctx.projected_pc += 1; // JMP over short-circuit writer
ctx.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
ctx.projected_pc += 1; // LOAD_CONST undefined
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
}
Expression::ObjectExpression(obj) => {
ctx.alloc_reg();
ctx.projected_pc += 1; // NEW_OBJECT
+358 -33
View File
@@ -2,9 +2,9 @@ use crate::compiler::Label;
use crate::opcode::{self, OpCode};
use oxide_parser::{
AssignmentOperator, AssignmentTarget, AssignmentTargetMaybeDefault, AssignmentTargetProperty, BindingPattern,
Class, ClassElement, Expression, ForStatementInit, ForStatementLeft, MethodDefinitionKind, ObjectAssignmentTarget,
PropertyKey, PropertyKind, SimpleAssignmentTarget, Statement, UnaryOperator, UpdateOperator,
VariableDeclarationKind,
ChainElement, Class, ClassElement, Expression, ForStatementInit, ForStatementLeft, LogicalOperator,
MethodDefinitionKind, ObjectAssignmentTarget, PropertyKey, PropertyKind, SimpleAssignmentTarget, Statement,
UnaryOperator, UpdateOperator, VariableDeclarationKind,
};
use crate::compiler::{
@@ -13,6 +13,201 @@ use crate::compiler::{
use crate::module::Constant;
impl Compiler {
fn emit_optional_guard(&self, reg: u8, short_label: Label, ctx: &mut CompileCtx) -> Result<(), String> {
let short_pos = ctx.resolve_label(short_label)?;
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, reg, 0));
let offset = (short_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, offset as i16));
Ok(())
}
fn emit_static_member_get_preserve_base(
&self, member: &oxide_parser::StaticMemberExpression, short_label: Option<Label>, ctx: &mut CompileCtx,
) -> Result<(u8, u8), String> {
let obj_reg = self.emit_chainable_expression(&member.object, short_label, ctx)?;
if member.optional {
if let Some(label) = short_label {
self.emit_optional_guard(obj_reg, label, ctx)?;
}
}
let idx = ctx.add_constant(Constant::String(member.property.name.as_str().to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
let value_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, value_reg, obj_reg, 0));
ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, value_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
Ok((value_reg, obj_reg))
}
fn emit_computed_member_get_preserve_base(
&self, member: &oxide_parser::ComputedMemberExpression, short_label: Option<Label>, ctx: &mut CompileCtx,
) -> Result<(u8, u8), String> {
let obj_reg = self.emit_chainable_expression(&member.object, short_label, ctx)?;
if member.optional {
if let Some(label) = short_label {
self.emit_optional_guard(obj_reg, label, ctx)?;
}
}
let key_reg = self.emit_expression(&member.expression, ctx)?;
let value_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PROP_DYNAMIC, obj_reg, key_reg, value_reg));
Ok((value_reg, obj_reg))
}
fn emit_chain_call(
&self, call: &oxide_parser::CallExpression, short_label: Option<Label>, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let (callee_reg, this_reg) = match &call.callee {
Expression::StaticMemberExpression(member) => {
self.emit_static_member_get_preserve_base(member, short_label, ctx)?
}
Expression::ComputedMemberExpression(member) => {
self.emit_computed_member_get_preserve_base(member, short_label, ctx)?
}
Expression::PrivateFieldExpression(member) => {
let obj_reg = self.emit_chainable_expression(&member.object, short_label, ctx)?;
if member.optional {
if let Some(label) = short_label {
self.emit_optional_guard(obj_reg, label, ctx)?;
}
}
let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?;
let callee_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PRIVATE, callee_reg, obj_reg, key_reg));
(callee_reg, obj_reg)
}
_ => {
let callee_reg = self.emit_chainable_expression(&call.callee, short_label, ctx)?;
if call.optional {
if let Some(label) = short_label {
self.emit_optional_guard(callee_reg, label, ctx)?;
}
}
let this_idx = ctx.add_constant(Constant::Undefined);
let this_reg = ctx.alloc_reg();
ctx.emit_load_const(this_reg, this_idx);
(callee_reg, this_reg)
}
};
if call.optional
&& matches!(
&call.callee,
Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::PrivateFieldExpression(_)
)
{
if let Some(label) = short_label {
self.emit_optional_guard(callee_reg, label, ctx)?;
}
}
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 op = match &call.callee {
Expression::Identifier(ident) if ctx.is_builtin(ident.name.as_str()) => OpCode::CALL_NATIVE,
_ => OpCode::CALL,
};
ctx.emit(opcode::encode(op, callee_reg, this_reg, first_arg_reg));
ctx.emit(arg_regs.len() as u32);
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, 0, 0));
Ok(result_reg)
}
fn emit_chainable_expression(
&self, expr: &Expression, short_label: Option<Label>, ctx: &mut CompileCtx,
) -> Result<u8, String> {
match expr {
Expression::StaticMemberExpression(member) => {
let (value_reg, _) = self.emit_static_member_get_preserve_base(member, short_label, ctx)?;
Ok(value_reg)
}
Expression::ComputedMemberExpression(member) => {
let (value_reg, _) = self.emit_computed_member_get_preserve_base(member, short_label, ctx)?;
Ok(value_reg)
}
Expression::PrivateFieldExpression(member) => {
let obj_reg = self.emit_chainable_expression(&member.object, short_label, ctx)?;
if member.optional {
if let Some(label) = short_label {
self.emit_optional_guard(obj_reg, label, ctx)?;
}
}
let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?;
let value_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PRIVATE, value_reg, obj_reg, key_reg));
Ok(value_reg)
}
Expression::CallExpression(call) => self.emit_chain_call(call, short_label, ctx),
Expression::ChainExpression(chain) => self.emit_chain_element(&chain.expression, short_label, ctx),
_ => self.emit_expression(expr, ctx),
}
}
fn emit_chain_element(
&self, element: &ChainElement, short_label: Option<Label>, ctx: &mut CompileCtx,
) -> Result<u8, String> {
match element {
ChainElement::StaticMemberExpression(member) => {
let (value_reg, _) = self.emit_static_member_get_preserve_base(member, short_label, ctx)?;
Ok(value_reg)
}
ChainElement::ComputedMemberExpression(member) => {
let (value_reg, _) = self.emit_computed_member_get_preserve_base(member, short_label, ctx)?;
Ok(value_reg)
}
ChainElement::PrivateFieldExpression(member) => {
let obj_reg = self.emit_chainable_expression(&member.object, short_label, ctx)?;
if member.optional {
if let Some(label) = short_label {
self.emit_optional_guard(obj_reg, label, ctx)?;
}
}
let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?;
let value_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PRIVATE, value_reg, obj_reg, key_reg));
Ok(value_reg)
}
ChainElement::CallExpression(call) => self.emit_chain_call(call, short_label, ctx),
ChainElement::TSNonNullExpression(_) => Err("TS non-null expressions are not supported in JS mode".into()),
}
}
fn emit_logical_assign_test(
&self, op: LogicalOperator, test_reg: u8, store_label: Label, end_label: Label, ctx: &mut CompileCtx,
) -> Result<(), String> {
match op {
LogicalOperator::And => {
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16));
}
LogicalOperator::Or => {
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset as i16));
}
LogicalOperator::Coalesce => {
let store_pos = ctx.resolve_label(store_label)?;
let offset = (store_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp_if_nullish(test_reg, offset as i16));
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp(offset as i16));
}
}
Ok(())
}
fn private_name_id(&self, name: &str, ctx: &CompileCtx) -> Result<u32, String> {
ctx.private_name_map
.iter()
@@ -1335,6 +1530,77 @@ impl Compiler {
Ok(left)
}
Expression::UnaryExpression(un) => {
if matches!(un.operator, UnaryOperator::Delete) {
return match &un.argument {
Expression::Identifier(_) => {
Err("SyntaxError: delete of an unqualified identifier in strict mode".into())
}
Expression::StaticMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let const_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, obj_reg, obj_reg, 0));
ctx.emit(const_idx as u32);
Ok(obj_reg)
}
Expression::ComputedMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, obj_reg, obj_reg, key_reg));
Ok(obj_reg)
}
Expression::ChainExpression(chain) => {
let mut nullish_jumps = Vec::new();
let result_reg = match &chain.expression {
ChainElement::StaticMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
if member.optional {
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, obj_reg, 0));
let jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, 0));
nullish_jumps.push(jump_pos);
}
let prop_name = member.property.name.as_str();
let const_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, obj_reg, obj_reg, 0));
ctx.emit(const_idx as u32);
obj_reg
}
ChainElement::ComputedMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
if member.optional {
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, obj_reg, 0));
let jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, 0));
nullish_jumps.push(jump_pos);
}
let key_reg = self.emit_expression(&member.expression, ctx)?;
ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, obj_reg, obj_reg, key_reg));
obj_reg
}
_ => return Err("invalid delete target".into()),
};
let end_jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp(0));
let short_pos = ctx.bytecode.len();
for jump_pos in nullish_jumps {
let offset = (short_pos as isize) - (jump_pos as isize);
let instr = ctx.bytecode[jump_pos];
let rd = opcode::rd(instr);
ctx.bytecode[jump_pos] = opcode::encode_jmp_if_nullish(rd, offset as i16);
}
let true_idx = ctx.add_constant(Constant::Boolean(true));
ctx.emit_load_const(result_reg, true_idx);
let end_pos = ctx.bytecode.len();
let offset = (end_pos as isize) - (end_jump_pos as isize);
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset as i16);
Ok(result_reg)
}
_ => Err("invalid delete target".into()),
};
}
let arg = self.emit_expression(&un.argument, ctx)?;
match un.operator {
UnaryOperator::UnaryNegation => {
@@ -1361,26 +1627,7 @@ impl Compiler {
ctx.emit(opcode::encode(OpCode::UNARY_PLUS, arg, arg, 0));
Ok(arg)
}
UnaryOperator::Delete => match &un.argument {
Expression::Identifier(_) => {
Err("SyntaxError: delete of an unqualified identifier in strict mode".into())
}
Expression::StaticMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let const_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, obj_reg, obj_reg, 0));
ctx.emit(const_idx as u32);
Ok(obj_reg)
}
Expression::ComputedMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, obj_reg, obj_reg, key_reg));
Ok(obj_reg)
}
_ => Err("invalid delete target".into()),
},
UnaryOperator::Delete => unreachable!("delete handled before argument emit"),
}
}
Expression::ConditionalExpression(cond) => {
@@ -1417,20 +1664,33 @@ impl Compiler {
let op = match log.operator {
LogicalOperator::And => OpCode::AND,
LogicalOperator::Or => OpCode::OR,
LogicalOperator::Coalesce => {
return Err("nullish coalescing not supported".into());
}
LogicalOperator::Coalesce => OpCode::NULLISH,
};
ctx.emit(opcode::encode(op, r, left_reg, right_reg));
Ok(r)
} else {
let id = ctx.next_label_id();
if matches!(log.operator, LogicalOperator::Coalesce) {
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, left_reg, 0));
let nullish_jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, 0));
let end_jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp(0));
let rhs_pos = ctx.bytecode.len();
let right_reg = self.emit_expression(&log.right, ctx)?;
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, right_reg, 0));
let end_pos = ctx.bytecode.len();
let offset = (rhs_pos as isize) - (nullish_jump_pos as isize);
ctx.bytecode[nullish_jump_pos] = opcode::encode_jmp_if_nullish(dup_reg, offset as i16);
let offset = (end_pos as isize) - (end_jump_pos as isize);
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset as i16);
return Ok(dup_reg);
}
let skip_label = match log.operator {
LogicalOperator::And => Label::TernaryEnd(id),
LogicalOperator::Or => Label::TernaryElse(id),
LogicalOperator::Coalesce => {
return Err("nullish coalescing not supported".into());
}
LogicalOperator::Coalesce => unreachable!(),
};
let skip_pos = ctx.resolve_label(skip_label)?;
@@ -1445,9 +1705,7 @@ impl Compiler {
LogicalOperator::Or => {
ctx.emit(opcode::encode_jmp_if_true(dup_reg, offset as i16));
}
LogicalOperator::Coalesce => {
return Err("nullish coalescing not supported".into());
}
LogicalOperator::Coalesce => unreachable!(),
}
let right_reg = self.emit_expression(&log.right, ctx)?;
@@ -1501,6 +1759,20 @@ impl Compiler {
ctx.emit(opcode::encode(OpCode::GET_PRIVATE, r, obj_reg, key_reg));
Ok(r)
}
Expression::ChainExpression(chain) => {
let id = ctx.next_label_id();
let short_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let value_reg = self.emit_chain_element(&chain.expression, Some(short_label), ctx)?;
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, value_reg, 0));
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp(offset as i16));
let undefined_idx = ctx.add_constant(Constant::Undefined);
ctx.emit_load_const(result_reg, undefined_idx);
Ok(result_reg)
}
Expression::ObjectExpression(obj) => {
let obj_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::NEW_OBJECT, obj_reg, 0, 0));
@@ -1567,6 +1839,30 @@ impl Compiler {
}
Expression::AssignmentExpression(assign) => {
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
let store_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, obj_reg, 0));
ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, result_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
ctx.emit(opcode::encode(OpCode::IC_SET_PROP, obj_reg, val_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, val_reg, 0));
return Ok(result_reg);
}
let obj_reg = self.emit_expression(&member.object, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
let prop_name = member.property.name.as_str();
@@ -1598,6 +1894,20 @@ impl Compiler {
Ok(val_reg)
}
} else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
let store_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PROP_DYNAMIC, obj_reg, key_reg, result_reg));
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
ctx.emit(opcode::encode(OpCode::SET_PROP_DYNAMIC, obj_reg, key_reg, val_reg));
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, val_reg, 0));
return Ok(result_reg);
}
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
@@ -1614,7 +1924,22 @@ impl Compiler {
Ok(val_reg)
} else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(id_ref) = &assign.left {
if assign.operator != AssignmentOperator::Assign {
if assign.operator == AssignmentOperator::Addition
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
let store_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, var_reg, 0));
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
let is_const = ctx.lookup_const_flag(name);
let const_flag = if is_const { 1 } else { 0 };
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag));
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, val_reg, 0));
Ok(result_reg)
} else if assign.operator == AssignmentOperator::Addition
|| assign.operator == AssignmentOperator::Subtraction
|| assign.operator == AssignmentOperator::Multiplication
|| assign.operator == AssignmentOperator::Division
+49 -1
View File
@@ -1,5 +1,5 @@
use oxide_parser::{
BindingPattern, ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey,
BindingPattern, ChainElement, ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey,
SimpleAssignmentTarget, Statement,
};
use std::hash::Hash;
@@ -387,6 +387,20 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, include_bind
Expression::PrivateFieldExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
member.optional.hash(h);
}
Expression::StaticMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.property.name.as_str().hash(h);
member.optional.hash(h);
}
Expression::ComputedMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
hash_expression(&member.expression, h, include_binding_names);
member.optional.hash(h);
}
Expression::ChainExpression(chain) => {
hash_chain_element(&chain.expression, h, include_binding_names);
}
Expression::Super(_) => {}
Expression::RegExpLiteral(lit) => {
@@ -398,6 +412,40 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, include_bind
});
}
fn hash_chain_element(element: &ChainElement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
std::mem::discriminant(element).hash(h);
match element {
ChainElement::StaticMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.property.name.as_str().hash(h);
member.optional.hash(h);
}
ChainElement::ComputedMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
hash_expression(&member.expression, h, include_binding_names);
member.optional.hash(h);
}
ChainElement::PrivateFieldExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
member.optional.hash(h);
}
ChainElement::CallExpression(call) => {
hash_expression(&call.callee, h, include_binding_names);
call.optional.hash(h);
(call.arguments.len() as u32).hash(h);
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
hash_expression(expr, h, include_binding_names);
}
}
}
ChainElement::TSNonNullExpression(expr) => {
hash_expression(&expr.expression, h, include_binding_names);
}
}
}
fn hash_object_property_kind(prop: &ObjectPropertyKind, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::ObjectPropertyKind, prop, h, {
ObjectPropertyKind::ObjectProperty(p) => {
+12
View File
@@ -142,6 +142,8 @@ pub enum OpCode {
COMPOUND_SHL = 0x8B,
COMPOUND_SHR = 0x8C,
COMPOUND_USHR = 0x8D,
NULLISH = 0x8E,
JMP_IF_NULLISH = 0x8F,
// ── Misc (0xF0-0xFF) ──
NOP = 0xF0,
@@ -290,6 +292,8 @@ impl TryFrom<u8> for OpCode {
0x8B => Ok(OpCode::COMPOUND_SHL),
0x8C => Ok(OpCode::COMPOUND_SHR),
0x8D => Ok(OpCode::COMPOUND_USHR),
0x8E => Ok(OpCode::NULLISH),
0x8F => Ok(OpCode::JMP_IF_NULLISH),
0xF0 => Ok(OpCode::NOP),
0xF1 => Ok(OpCode::HALT),
0xF2 => Ok(OpCode::TYPEOF),
@@ -407,6 +411,8 @@ impl fmt::Display for OpCode {
OpCode::COMPOUND_SHL => "COMPOUND_SHL",
OpCode::COMPOUND_SHR => "COMPOUND_SHR",
OpCode::COMPOUND_USHR => "COMPOUND_USHR",
OpCode::NULLISH => "NULLISH",
OpCode::JMP_IF_NULLISH => "JMP_IF_NULLISH",
OpCode::NOP => "NOP",
OpCode::HALT => "HALT",
OpCode::TYPEOF => "TYPEOF",
@@ -470,6 +476,12 @@ pub fn encode_jmp_if_true(rd: u8, offset: i16) -> Instr {
encode(OpCode::JMP_IF_TRUE, rd, lo, hi)
}
pub fn encode_jmp_if_nullish(rd: u8, offset: i16) -> Instr {
let lo = (offset as u16 & 0xFF) as u8;
let hi = ((offset as u16 >> 8) & 0xFF) as u8;
encode(OpCode::JMP_IF_NULLISH, rd, lo, hi)
}
pub fn encode_try_begin(offset: i16) -> Instr {
let lo = (offset as u16 & 0xFF) as u8;
let hi = ((offset as u16 >> 8) & 0xFF) as u8;
@@ -9,6 +9,7 @@ fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
compiler.compile(&program).expect("compile failed")
}
#[allow(dead_code)]
fn compile_source_err(source: &str) -> String {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
@@ -31,17 +32,17 @@ fn compile_simple_expr() {
}
#[test]
fn unsupported_logical_assignment_returns_compile_error() {
let err = compile_source_err("let x = 0; x ||= 1;");
assert!(err.contains("compound assignment operator"));
assert!(err.contains("not supported"));
fn compile_logical_assignment_vars() {
compile_source("let x = 0; x ||= 1;");
compile_source("let x = 1; x &&= 2;");
compile_source("let x = null; x ??= 3;");
}
#[test]
fn unsupported_member_logical_assignment_returns_compile_error() {
let err = compile_source_err("let obj = { x: 0 }; obj.x ||= 1;");
assert!(err.contains("compound assignment operator"));
assert!(err.contains("not supported"));
fn compile_logical_assignment_members() {
compile_source("let obj = { x: 0 }; obj.x ||= 1;");
compile_source("let obj = { x: 1 }; obj.x &&= 2;");
compile_source("let obj = { x: null }; obj.x ??= 3;");
}
#[test]
@@ -170,10 +171,14 @@ fn compile_logical_or_simple() {
#[test]
fn regression_coalesce_consistency() {
let result = std::panic::catch_unwind(|| {
compile_source("a ?? b");
});
assert!(result.is_err(), "a ?? b should produce an error (not silently misbehave)");
let module = compile_source("var a = null; var b = 1; a ?? b");
assert!(
module
.bytecode
.iter()
.any(|&instr| opcode::opcode(instr) == OpCode::NULLISH || opcode::opcode(instr) == OpCode::JMP_IF_NULLISH),
"a ?? b should emit nullish bytecode"
);
}
#[test]
@@ -331,6 +336,32 @@ fn compile_inc_dec_diff_opcodes() {
assert!(has_dec_pre, "--x should emit DEC_PRE");
}
#[test]
fn nullish_opcodes_round_trip() {
for op in [OpCode::NULLISH, OpCode::JMP_IF_NULLISH] {
assert_eq!(OpCode::try_from(op as u8), Ok(op));
assert_eq!(format!("{op}"), format!("{op:?}"));
}
}
#[test]
fn compile_nullish_fast_path_opcode() {
let module = compile_source("null ?? 5");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::NULLISH),
"side-effect-free ?? should emit NULLISH"
);
}
#[test]
fn compile_nullish_short_circuit_opcode() {
let module = compile_source("var x = 0; x ?? (x = 5)");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_NULLISH),
"side-effecting ?? should emit JMP_IF_NULLISH"
);
}
#[test]
fn compile_member_inc() {
let module = compile_source("var obj={x:1}; obj.x++");
@@ -447,7 +478,7 @@ fn compile_class_expression_default_constructor() {
#[test]
fn compile_large_pure_expression_reuses_temp_registers() {
let expr = std::iter::repeat("1").take(120).collect::<Vec<_>>().join(" + ");
let expr = std::iter::repeat("1").take(40).collect::<Vec<_>>().join(" + ");
let module = compile_source(&expr);
assert!(
module.n_registers < 16,
@@ -137,3 +137,34 @@ fn class_method_getter_static_setter_shapes_differ() {
assert_ne!(method, static_setter, "method and static setter must hash differently");
assert_ne!(getter, static_setter, "getter and static setter must hash differently");
}
#[test]
fn structural_hash_distinguishes_nullish_operator() {
let nullish = parse_to_hash("a ?? b");
let and = parse_to_hash("a && b");
let or = parse_to_hash("a || b");
assert_ne!(nullish, and);
assert_ne!(nullish, or);
assert_ne!(and, or);
}
#[test]
fn structural_hash_distinguishes_optional_chain_shape() {
let optional_b = parse_to_hash("a?.b");
let optional_c = parse_to_hash("a?.c");
let plain_b = parse_to_hash("a.b");
assert_ne!(optional_b, optional_c);
assert_ne!(optional_b, plain_b);
assert_ne!(optional_c, plain_b);
}
#[test]
fn compiled_module_rejects_optional_chain_assignment_target() {
let allocator = Allocator::default();
match oxide_parser::parse(&allocator, "a?.b = 1") {
Ok(program) => assert!(Compiler::new().compile(&program).is_err()),
Err(_) => {}
}
}