Browse Source

`Break` Opcode and `ByteCompiler` changes (#2523)

<!---
Thank you for contributing to Boa! Please fill out the template below, and remove or add any
information as you feel necessary.
--->

Hi all! 😄

This Pull Request addresses #2424. There are also a few changes made to the `ByteCompiler`, the majority of which are involving `JumpControlInfo`.

It changes the following:

- Adds `Break` Opcode
- Shifts `compile_stmt` into the `statement` module. 
- Moves `JumpControlInfo` to it's own module.
pull/2521/head
Kevin 2 years ago
parent
commit
989edd42c7
  1. 389
      boa_engine/src/bytecompiler/jump_control.rs
  2. 259
      boa_engine/src/bytecompiler/mod.rs
  3. 39
      boa_engine/src/bytecompiler/statement/block.rs
  4. 70
      boa_engine/src/bytecompiler/statement/break.rs
  5. 35
      boa_engine/src/bytecompiler/statement/continue.rs
  6. 25
      boa_engine/src/bytecompiler/statement/if.rs
  7. 69
      boa_engine/src/bytecompiler/statement/labelled.rs
  8. 21
      boa_engine/src/bytecompiler/statement/loop.rs
  9. 257
      boa_engine/src/bytecompiler/statement/mod.rs
  10. 54
      boa_engine/src/bytecompiler/statement/switch.rs
  11. 4
      boa_engine/src/bytecompiler/statement/try.rs
  12. 103
      boa_engine/src/tests.rs
  13. 3
      boa_engine/src/vm/code_block.rs
  14. 16
      boa_engine/src/vm/flowgraph/mod.rs
  15. 44
      boa_engine/src/vm/opcode/jump/break.rs
  16. 3
      boa_engine/src/vm/opcode/jump/mod.rs
  17. 3
      boa_engine/src/vm/opcode/mod.rs

389
boa_engine/src/bytecompiler/jump_control.rs

@ -0,0 +1,389 @@
//! `JumpControlInfo` tracks relevant jump information used during compilation.
//!
//! Primarily, jump control tracks information related to the compilation of [iteration
//! statements][iteration spec], [switch statements][switch spec], [try statements][try spec],
//! and [labelled statements][labelled spec].
//!
//! [iteration spec]: https://tc39.es/ecma262/#sec-iteration-statements
//! [switch spec]: https://tc39.es/ecma262/#sec-switch-statement
//! [try spec]: https://tc39.es/ecma262/#sec-try-statement
//! [labelled spec]: https://tc39.es/ecma262/#sec-labelled-statements
use crate::{
bytecompiler::{ByteCompiler, Label},
vm::Opcode,
};
use bitflags::bitflags;
use boa_interner::Sym;
use std::mem::size_of;
/// Boa's `ByteCompiler` jump information tracking struct.
#[derive(Debug, Clone)]
pub(crate) struct JumpControlInfo {
label: Option<Sym>,
start_address: u32,
decl_envs: u32,
flags: JumpControlInfoFlags,
breaks: Vec<Label>,
try_continues: Vec<Label>,
finally_start: Option<Label>,
}
bitflags! {
/// A bitflag that contains the type flags and relevant booleans for `JumpControlInfo`.
pub(crate) struct JumpControlInfoFlags: u8 {
const LOOP = 0b0000_0001;
const SWITCH = 0b0000_0010;
const TRY_BLOCK = 0b0000_0100;
const LABELLED_BLOCK = 0b0000_1000;
const IN_CATCH = 0b0001_0000;
const HAS_FINALLY = 0b0010_0000;
const FOR_OF_IN_LOOP = 0b0100_0000;
}
}
impl Default for JumpControlInfoFlags {
fn default() -> Self {
JumpControlInfoFlags::empty()
}
}
impl Default for JumpControlInfo {
fn default() -> Self {
Self {
label: None,
start_address: u32::MAX,
decl_envs: 0,
flags: JumpControlInfoFlags::default(),
breaks: Vec::new(),
try_continues: Vec::new(),
finally_start: None,
}
}
}
// ---- `JumpControlInfo` Creation Methods ---- //
impl JumpControlInfo {
pub(crate) const fn with_label(mut self, label: Option<Sym>) -> Self {
self.label = label;
self
}
pub(crate) const fn with_start_address(mut self, address: u32) -> Self {
self.start_address = address;
self
}
pub(crate) fn with_loop_flag(mut self, value: bool) -> Self {
self.flags.set(JumpControlInfoFlags::LOOP, value);
self
}
pub(crate) fn with_switch_flag(mut self, value: bool) -> Self {
self.flags.set(JumpControlInfoFlags::SWITCH, value);
self
}
pub(crate) fn with_try_block_flag(mut self, value: bool) -> Self {
self.flags.set(JumpControlInfoFlags::TRY_BLOCK, value);
self
}
pub(crate) fn with_labelled_block_flag(mut self, value: bool) -> Self {
self.flags.set(JumpControlInfoFlags::LABELLED_BLOCK, value);
self
}
pub(crate) fn with_has_finally(mut self, value: bool) -> Self {
self.flags.set(JumpControlInfoFlags::HAS_FINALLY, value);
self
}
pub(crate) fn with_for_of_in_loop(mut self, value: bool) -> Self {
self.flags.set(JumpControlInfoFlags::FOR_OF_IN_LOOP, value);
self
}
}
// ---- `JumpControlInfo` const fn methods ---- //
impl JumpControlInfo {
pub(crate) const fn label(&self) -> Option<Sym> {
self.label
}
pub(crate) const fn start_address(&self) -> u32 {
self.start_address
}
pub(crate) const fn is_loop(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::LOOP)
}
pub(crate) const fn is_switch(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::SWITCH)
}
pub(crate) const fn is_try_block(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::TRY_BLOCK)
}
pub(crate) const fn is_labelled_block(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::LABELLED_BLOCK)
}
pub(crate) const fn in_catch(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::IN_CATCH)
}
pub(crate) const fn has_finally(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::HAS_FINALLY)
}
pub(crate) const fn finally_start(&self) -> Option<Label> {
self.finally_start
}
pub(crate) const fn for_of_in_loop(&self) -> bool {
self.flags.contains(JumpControlInfoFlags::FOR_OF_IN_LOOP)
}
pub(crate) const fn decl_envs(&self) -> u32 {
self.decl_envs
}
}
impl JumpControlInfo {
/// Sets the `label` field of `JumpControlInfo`.
pub(crate) fn set_label(&mut self, label: Option<Sym>) {
assert!(self.label.is_none());
self.label = label;
}
/// Sets the `start_address` field of `JumpControlInfo`.
pub(crate) fn set_start_address(&mut self, start_address: u32) {
self.start_address = start_address;
}
/// Sets the `in_catch` field of `JumpControlInfo`.
pub(crate) fn set_in_catch(&mut self, value: bool) {
self.flags.set(JumpControlInfoFlags::IN_CATCH, value);
}
/// Sets the `finally_start` field of `JumpControlInfo`.
pub(crate) fn set_finally_start(&mut self, label: Label) {
self.finally_start = Some(label);
}
/// Increments the `decl_env` field of `JumpControlInfo`.
pub(crate) fn inc_decl_envs(&mut self) {
self.decl_envs += 1;
}
/// Decrements the `decl_env` field of `JumpControlInfo`.
pub(crate) fn dec_decl_envs(&mut self) {
self.decl_envs -= 1;
}
/// Pushes a `Label` onto the `break` vector of `JumpControlInfo`.
pub(crate) fn push_break_label(&mut self, break_label: Label) {
self.breaks.push(break_label);
}
/// Pushes a `Label` onto the `try_continues` vector of `JumpControlInfo`.
pub(crate) fn push_try_continue_label(&mut self, try_continue_label: Label) {
self.try_continues.push(try_continue_label);
}
}
// `JumpControlInfo` related methods that are implemented on `ByteCompiler`.
impl ByteCompiler<'_, '_> {
/// Pushes a generic `JumpControlInfo` onto `ByteCompiler`
///
/// Default `JumpControlInfoKind` is `JumpControlInfoKind::Loop`
pub(crate) fn push_empty_loop_jump_control(&mut self) {
self.jump_info
.push(JumpControlInfo::default().with_loop_flag(true));
}
pub(crate) fn current_jump_control_mut(&mut self) -> Option<&mut JumpControlInfo> {
self.jump_info.last_mut()
}
pub(crate) fn set_jump_control_finally_start(&mut self, start: Label) {
if !self.jump_info.is_empty() {
let info = self
.jump_info
.last_mut()
.expect("must have try control label");
assert!(info.is_try_block());
info.set_finally_start(start);
}
}
pub(crate) fn set_jump_control_catch_start(&mut self, value: bool) {
if !self.jump_info.is_empty() {
let info = self
.jump_info
.last_mut()
.expect("must have try control label");
assert!(info.is_try_block());
info.set_in_catch(value);
}
}
/// Emits the `PushDeclarativeEnvironment` and updates the current jump info to track environments.
pub(crate) fn emit_and_track_decl_env(&mut self) -> (Label, Label) {
let pushed_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment);
if !self.jump_info.is_empty() {
let current_jump_info = self
.jump_info
.last_mut()
.expect("Jump info must exist as the vector is not empty");
current_jump_info.inc_decl_envs();
}
pushed_env
}
/// Emits the `PopEnvironment` Opcode and updates the current jump that the env is removed.
pub(crate) fn emit_and_track_pop_env(&mut self) {
self.emit_opcode(Opcode::PopEnvironment);
if !self.jump_info.is_empty() {
let current_info = self.jump_info.last_mut().expect("JumpInfo must exist");
current_info.dec_decl_envs();
}
}
pub(crate) fn push_loop_control_info(&mut self, label: Option<Sym>, start_address: u32) {
let new_info = JumpControlInfo::default()
.with_loop_flag(true)
.with_label(label)
.with_start_address(start_address);
self.jump_info.push(new_info);
}
pub(crate) fn push_loop_control_info_for_of_in_loop(
&mut self,
label: Option<Sym>,
start_address: u32,
) {
let new_info = JumpControlInfo::default()
.with_loop_flag(true)
.with_label(label)
.with_start_address(start_address)
.with_for_of_in_loop(true);
self.jump_info.push(new_info);
}
pub(crate) fn pop_loop_control_info(&mut self) {
let loop_info = self.jump_info.pop().expect("no jump information found");
assert!(loop_info.is_loop());
for label in loop_info.breaks {
self.patch_jump(label);
}
for label in loop_info.try_continues {
self.patch_jump_with_target(label, loop_info.start_address);
}
}
pub(crate) fn push_switch_control_info(&mut self, label: Option<Sym>, start_address: u32) {
let new_info = JumpControlInfo::default()
.with_switch_flag(true)
.with_label(label)
.with_start_address(start_address);
self.jump_info.push(new_info);
}
pub(crate) fn pop_switch_control_info(&mut self) {
let info = self.jump_info.pop().expect("no jump information found");
assert!(info.is_switch());
for label in info.breaks {
self.patch_jump(label);
}
}
pub(crate) fn push_try_control_info(&mut self, has_finally: bool) {
if !self.jump_info.is_empty() {
let start_address = self
.jump_info
.last()
.expect("no jump information found")
.start_address();
let new_info = JumpControlInfo::default()
.with_try_block_flag(true)
.with_start_address(start_address)
.with_has_finally(has_finally);
self.jump_info.push(new_info);
}
}
pub(crate) fn pop_try_control_info(&mut self, finally_start_address: Option<u32>) {
if !self.jump_info.is_empty() {
let mut info = self.jump_info.pop().expect("no jump information found");
assert!(info.is_try_block());
let mut breaks = Vec::with_capacity(info.breaks.len());
if let Some(finally_start_address) = finally_start_address {
for label in info.try_continues {
if label.index < finally_start_address {
self.patch_jump_with_target(label, finally_start_address);
} else {
self.patch_jump_with_target(label, info.start_address);
}
}
for label in info.breaks {
if label.index < finally_start_address {
self.patch_jump_with_target(label, finally_start_address);
let Label { mut index } = label;
index -= size_of::<Opcode>() as u32;
index -= size_of::<u32>() as u32;
breaks.push(Label { index });
} else {
breaks.push(label);
}
}
if let Some(jump_info) = self.jump_info.last_mut() {
jump_info.breaks.append(&mut breaks);
}
} else if let Some(jump_info) = self.jump_info.last_mut() {
jump_info.breaks.append(&mut info.breaks);
jump_info.try_continues.append(&mut info.try_continues);
}
}
}
pub(crate) fn push_labelled_block_control_info(&mut self, label: Sym, start_address: u32) {
let new_info = JumpControlInfo::default()
.with_labelled_block_flag(true)
.with_label(Some(label))
.with_start_address(start_address);
self.jump_info.push(new_info);
}
pub(crate) fn pop_labelled_block_control_info(&mut self) {
let info = self.jump_info.pop().expect("no jump information found");
assert!(info.is_labelled_block());
self.emit_opcode(Opcode::PopEnvironment);
for label in info.breaks {
self.patch_jump(label);
}
for label in info.try_continues {
self.patch_jump_with_target(label, info.start_address);
}
}
}

259
boa_engine/src/bytecompiler/mod.rs

@ -4,6 +4,7 @@ mod class;
mod declaration;
mod expression;
mod function;
mod jump_control;
mod statement;
use crate::{
@ -29,9 +30,9 @@ use boa_ast::{
use boa_gc::{Gc, GcCell};
use boa_interner::{Interner, Sym};
use rustc_hash::FxHashMap;
use std::mem::size_of;
pub(crate) use function::FunctionCompiler;
pub(crate) use jump_control::JumpControlInfo;
/// Describes how a node has been defined in the source code.
#[derive(Debug, Clone, Copy, PartialEq)]
@ -169,31 +170,10 @@ enum Literal {
#[must_use]
#[derive(Debug, Clone, Copy)]
struct Label {
pub(crate) struct Label {
index: u32,
}
#[derive(Debug, Clone)]
struct JumpControlInfo {
label: Option<Sym>,
start_address: u32,
kind: JumpControlInfoKind,
breaks: Vec<Label>,
try_continues: Vec<Label>,
in_catch: bool,
has_finally: bool,
finally_start: Option<Label>,
for_of_in_loop: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum JumpControlInfoKind {
Loop,
Switch,
Try,
LabelledBlock,
}
#[derive(Debug, Clone, Copy)]
enum Access<'a> {
Variable { name: Identifier },
@ -237,7 +217,7 @@ impl<'b, 'icu> ByteCompiler<'b, 'icu> {
/// Represents a placeholder address that will be patched later.
const DUMMY_ADDRESS: u32 = u32::MAX;
/// Creates a new [`ByteCompiler`].
/// Creates a new `ByteCompiler`.
#[inline]
pub fn new(
name: Sym,
@ -481,182 +461,6 @@ impl<'b, 'icu> ByteCompiler<'b, 'icu> {
self.patch_jump_with_target(label, target);
}
fn push_loop_control_info(&mut self, label: Option<Sym>, start_address: u32) {
self.jump_info.push(JumpControlInfo {
label,
start_address,
kind: JumpControlInfoKind::Loop,
breaks: Vec::new(),
try_continues: Vec::new(),
in_catch: false,
has_finally: false,
finally_start: None,
for_of_in_loop: false,
});
}
fn push_loop_control_info_for_of_in_loop(&mut self, label: Option<Sym>, start_address: u32) {
self.jump_info.push(JumpControlInfo {
label,
start_address,
kind: JumpControlInfoKind::Loop,
breaks: Vec::new(),
try_continues: Vec::new(),
in_catch: false,
has_finally: false,
finally_start: None,
for_of_in_loop: true,
});
}
fn pop_loop_control_info(&mut self) {
let loop_info = self.jump_info.pop().expect("no jump information found");
assert!(loop_info.kind == JumpControlInfoKind::Loop);
for label in loop_info.breaks {
self.patch_jump(label);
}
for label in loop_info.try_continues {
self.patch_jump_with_target(label, loop_info.start_address);
}
}
fn push_switch_control_info(&mut self, label: Option<Sym>, start_address: u32) {
self.jump_info.push(JumpControlInfo {
label,
start_address,
kind: JumpControlInfoKind::Switch,
breaks: Vec::new(),
try_continues: Vec::new(),
in_catch: false,
has_finally: false,
finally_start: None,
for_of_in_loop: false,
});
}
fn pop_switch_control_info(&mut self) {
let info = self.jump_info.pop().expect("no jump information found");
assert!(info.kind == JumpControlInfoKind::Switch);
for label in info.breaks {
self.patch_jump(label);
}
}
fn push_try_control_info(&mut self, has_finally: bool) {
if !self.jump_info.is_empty() {
let start_address = self
.jump_info
.last()
.expect("no jump information found")
.start_address;
self.jump_info.push(JumpControlInfo {
label: None,
start_address,
kind: JumpControlInfoKind::Try,
breaks: Vec::new(),
try_continues: Vec::new(),
in_catch: false,
has_finally,
finally_start: None,
for_of_in_loop: false,
});
}
}
fn push_try_control_info_catch_start(&mut self) {
if !self.jump_info.is_empty() {
let mut info = self
.jump_info
.last_mut()
.expect("must have try control label");
assert!(info.kind == JumpControlInfoKind::Try);
info.in_catch = true;
}
}
fn push_try_control_info_finally_start(&mut self, start: Label) {
if !self.jump_info.is_empty() {
let mut info = self
.jump_info
.last_mut()
.expect("must have try control label");
assert!(info.kind == JumpControlInfoKind::Try);
info.finally_start = Some(start);
}
}
fn pop_try_control_info(&mut self, finally_start_address: Option<u32>) {
if !self.jump_info.is_empty() {
let mut info = self.jump_info.pop().expect("no jump information found");
assert!(info.kind == JumpControlInfoKind::Try);
let mut breaks = Vec::with_capacity(info.breaks.len());
if let Some(finally_start_address) = finally_start_address {
for label in info.try_continues {
if label.index < finally_start_address {
self.patch_jump_with_target(label, finally_start_address);
} else {
self.patch_jump_with_target(label, info.start_address);
}
}
for label in info.breaks {
if label.index < finally_start_address {
self.patch_jump_with_target(label, finally_start_address);
let Label { mut index } = label;
index -= size_of::<Opcode>() as u32;
index -= size_of::<u32>() as u32;
breaks.push(Label { index });
} else {
breaks.push(label);
}
}
if let Some(jump_info) = self.jump_info.last_mut() {
jump_info.breaks.append(&mut breaks);
}
} else if let Some(jump_info) = self.jump_info.last_mut() {
jump_info.breaks.append(&mut info.breaks);
jump_info.try_continues.append(&mut info.try_continues);
}
}
}
fn push_labelled_block_control_info(&mut self, label: Sym, start_address: u32) {
self.jump_info.push(JumpControlInfo {
label: Some(label),
start_address,
kind: JumpControlInfoKind::LabelledBlock,
breaks: Vec::new(),
try_continues: Vec::new(),
in_catch: false,
has_finally: false,
finally_start: None,
for_of_in_loop: false,
});
}
fn pop_labelled_block_control_info(&mut self) {
let info = self.jump_info.pop().expect("no jump information found");
assert!(info.kind == JumpControlInfoKind::LabelledBlock);
for label in info.breaks {
self.patch_jump(label);
}
for label in info.try_continues {
self.patch_jump_with_target(label, info.start_address);
}
}
fn access_get(&mut self, access: Access<'_>, use_expr: bool) -> JsResult<()> {
match access {
Access::Variable { name } => {
@ -1174,61 +978,6 @@ impl<'b, 'icu> ByteCompiler<'b, 'icu> {
}
}
/// Compiles a [`Statement`]
pub fn compile_stmt(
&mut self,
node: &Statement,
use_expr: bool,
configurable_globals: bool,
) -> JsResult<()> {
match node {
Statement::Var(var) => self.compile_var_decl(var)?,
Statement::If(node) => self.compile_if(node, configurable_globals)?,
Statement::ForLoop(for_loop) => {
self.compile_for_loop(for_loop, None, configurable_globals)?;
}
Statement::ForInLoop(for_in_loop) => {
self.compile_for_in_loop(for_in_loop, None, configurable_globals)?;
}
Statement::ForOfLoop(for_of_loop) => {
self.compile_for_of_loop(for_of_loop, None, configurable_globals)?;
}
Statement::WhileLoop(while_loop) => {
self.compile_while_loop(while_loop, None, configurable_globals)?;
}
Statement::DoWhileLoop(do_while_loop) => {
self.compile_do_while_loop(do_while_loop, None, configurable_globals)?;
}
Statement::Block(block) => {
self.compile_block(block, None, use_expr, configurable_globals)?;
}
Statement::Labelled(labelled) => {
self.compile_labelled(labelled, use_expr, configurable_globals)?;
}
Statement::Continue(node) => self.compile_continue(*node)?,
Statement::Break(node) => self.compile_break(*node)?,
Statement::Throw(throw) => {
self.compile_expr(throw.target(), true)?;
self.emit(Opcode::Throw, &[]);
}
Statement::Switch(switch) => {
self.compile_switch(switch, configurable_globals)?;
}
Statement::Return(ret) => {
if let Some(expr) = ret.target() {
self.compile_expr(expr, true)?;
} else {
self.emit(Opcode::PushUndefined, &[]);
}
self.emit(Opcode::Return, &[]);
}
Statement::Try(t) => self.compile_try(t, use_expr, configurable_globals)?,
Statement::Empty => {}
Statement::Expression(expr) => self.compile_expr(expr, use_expr)?,
}
Ok(())
}
/// Compile a function AST Node into bytecode.
fn function(
&mut self,

39
boa_engine/src/bytecompiler/statement/block.rs

@ -0,0 +1,39 @@
use crate::{bytecompiler::ByteCompiler, JsResult};
use boa_ast::statement::Block;
use boa_interner::Sym;
impl ByteCompiler<'_, '_> {
/// Compile a [`Block`] `boa_ast` node
pub(crate) fn compile_block(
&mut self,
block: &Block,
label: Option<Sym>,
use_expr: bool,
configurable_globals: bool,
) -> JsResult<()> {
if let Some(label) = label {
let next = self.next_opcode_location();
self.push_labelled_block_control_info(label, next);
}
self.context.push_compile_time_environment(false);
let push_env = self.emit_and_track_decl_env();
self.create_decls(block.statement_list(), configurable_globals);
self.compile_statement_list(block.statement_list(), use_expr, configurable_globals)?;
let (num_bindings, compile_environment) = self.context.pop_compile_time_environment();
let index_compile_environment = self.push_compile_environment(compile_environment);
self.patch_jump_with_target(push_env.0, num_bindings as u32);
self.patch_jump_with_target(push_env.1, index_compile_environment as u32);
if label.is_some() {
self.pop_labelled_block_control_info();
} else {
self.emit_and_track_pop_env();
}
Ok(())
}
}

70
boa_engine/src/bytecompiler/statement/break.rs

@ -0,0 +1,70 @@
use boa_ast::statement::Break;
use crate::{bytecompiler::ByteCompiler, vm::Opcode, JsNativeError, JsResult};
impl ByteCompiler<'_, '_> {
/// Compile a [`Break`] `boa_ast` node
pub(crate) fn compile_break(&mut self, node: Break) -> JsResult<()> {
let next = self.next_opcode_location();
if let Some(info) = self.jump_info.last().filter(|info| info.is_try_block()) {
let in_finally = if let Some(finally_start) = info.finally_start() {
next >= finally_start.index
} else {
false
};
let in_catch_no_finally = !info.has_finally() && info.in_catch();
if in_finally {
self.emit_opcode(Opcode::PopIfThrown);
}
if in_finally || in_catch_no_finally {
self.emit_opcode(Opcode::CatchEnd2);
} else {
self.emit_opcode(Opcode::TryEnd);
}
self.emit(Opcode::FinallySetJump, &[u32::MAX]);
}
let (break_label, envs_to_pop) = self.emit_opcode_with_two_operands(Opcode::Break);
if let Some(label_name) = node.label() {
let mut found = false;
let mut total_envs: u32 = 0;
for info in self.jump_info.iter_mut().rev() {
total_envs += info.decl_envs();
if info.label() == Some(label_name) {
info.push_break_label(break_label);
found = true;
break;
}
}
// TODO: promote to an early error.
if !found {
return Err(JsNativeError::syntax()
.with_message(format!(
"Cannot use the undeclared label '{}'",
self.interner().resolve_expect(label_name)
))
.into());
}
self.patch_jump_with_target(envs_to_pop, total_envs);
} else {
let envs = self
.jump_info
.last()
// TODO: promote to an early error.
.ok_or_else(|| {
JsNativeError::syntax()
.with_message("unlabeled break must be inside loop or switch")
})?
.decl_envs();
self.patch_jump_with_target(envs_to_pop, envs);
self.jump_info
.last_mut()
.expect("cannot throw error as last access would have thrown")
.push_break_label(break_label);
}
Ok(())
}
}

35
boa_engine/src/bytecompiler/statement/continue.rs

@ -1,26 +1,18 @@
use boa_ast::statement::Continue;
use crate::{
bytecompiler::{ByteCompiler, JumpControlInfoKind},
vm::Opcode,
JsNativeError, JsResult,
};
use crate::{bytecompiler::ByteCompiler, vm::Opcode, JsNativeError, JsResult};
impl ByteCompiler<'_, '_> {
pub(crate) fn compile_continue(&mut self, node: Continue) -> JsResult<()> {
let next = self.next_opcode_location();
if let Some(info) = self
.jump_info
.last()
.filter(|info| info.kind == JumpControlInfoKind::Try)
{
let start_address = info.start_address;
let in_finally = if let Some(finally_start) = info.finally_start {
if let Some(info) = self.jump_info.last().filter(|info| info.is_try_block()) {
let start_address = info.start_address();
let in_finally = if let Some(finally_start) = info.finally_start() {
next > finally_start.index
} else {
false
};
let in_catch_no_finally = !info.has_finally && info.in_catch;
let in_catch_no_finally = !info.has_finally() && info.in_catch();
if in_finally {
self.emit_opcode(Opcode::PopIfThrown);
@ -37,25 +29,20 @@ impl ByteCompiler<'_, '_> {
self.jump_info
.last_mut()
.expect("no jump information found")
.try_continues
.push(label);
.push_try_continue_label(label);
} else {
let mut items = self
.jump_info
.iter()
.rev()
.filter(|info| info.kind == JumpControlInfoKind::Loop);
let mut items = self.jump_info.iter().rev().filter(|info| info.is_loop());
let address = if let Some(label_name) = node.label() {
let mut num_loops = 0;
let mut emit_for_of_in_exit = 0;
let mut address_info = None;
for info in items {
if info.label == node.label() {
if info.label() == node.label() {
address_info = Some(info);
break;
}
num_loops += 1;
if info.for_of_in_loop {
if info.for_of_in_loop() {
emit_for_of_in_exit += 1;
}
}
@ -67,7 +54,7 @@ impl ByteCompiler<'_, '_> {
self.context.interner().resolve_expect(label_name)
))
})?
.start_address;
.start_address();
for _ in 0..emit_for_of_in_exit {
self.emit_opcode(Opcode::Pop);
self.emit_opcode(Opcode::Pop);
@ -84,7 +71,7 @@ impl ByteCompiler<'_, '_> {
.ok_or_else(|| {
JsNativeError::syntax().with_message("continue must be inside loop")
})?
.start_address
.start_address()
};
self.emit_opcode(Opcode::LoopEnd);
self.emit_opcode(Opcode::LoopStart);

25
boa_engine/src/bytecompiler/statement/if.rs

@ -0,0 +1,25 @@
use crate::{bytecompiler::ByteCompiler, JsResult};
use boa_ast::statement::If;
impl ByteCompiler<'_, '_> {
pub(crate) fn compile_if(&mut self, node: &If, configurable_globals: bool) -> JsResult<()> {
self.compile_expr(node.cond(), true)?;
let jelse = self.jump_if_false();
self.compile_stmt(node.body(), false, configurable_globals)?;
match node.else_node() {
None => {
self.patch_jump(jelse);
}
Some(else_body) => {
let exit = self.jump();
self.patch_jump(jelse);
self.compile_stmt(else_body, false, configurable_globals)?;
self.patch_jump(exit);
}
}
Ok(())
}
}

69
boa_engine/src/bytecompiler/statement/labelled.rs

@ -0,0 +1,69 @@
use boa_ast::{
statement::{Labelled, LabelledItem},
Statement,
};
use crate::{
bytecompiler::{ByteCompiler, NodeKind},
JsResult,
};
impl ByteCompiler<'_, '_> {
/// Compile a [`Labelled`] `boa_ast` node
pub(crate) fn compile_labelled(
&mut self,
labelled: &Labelled,
use_expr: bool,
configurable_globals: bool,
) -> JsResult<()> {
match labelled.item() {
LabelledItem::Statement(stmt) => match stmt {
Statement::ForLoop(for_loop) => {
self.compile_for_loop(for_loop, Some(labelled.label()), configurable_globals)?;
}
Statement::ForInLoop(for_in_loop) => {
self.compile_for_in_loop(
for_in_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::ForOfLoop(for_of_loop) => {
self.compile_for_of_loop(
for_of_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::WhileLoop(while_loop) => {
self.compile_while_loop(
while_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::DoWhileLoop(do_while_loop) => {
self.compile_do_while_loop(
do_while_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::Block(block) => {
self.compile_block(
block,
Some(labelled.label()),
use_expr,
configurable_globals,
)?;
}
stmt => self.compile_stmt(stmt, use_expr, configurable_globals)?,
},
LabelledItem::Function(f) => {
self.function(f.into(), NodeKind::Declaration, false)?;
}
}
Ok(())
}
}

21
boa_engine/src/bytecompiler/statement/loop.rs

@ -22,7 +22,8 @@ impl ByteCompiler<'_, '_> {
configurable_globals: bool,
) -> JsResult<()> {
self.context.push_compile_time_environment(false);
let push_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment);
let push_env = self.emit_and_track_decl_env();
self.push_empty_loop_jump_control();
if let Some(init) = for_loop.init() {
match init {
@ -42,7 +43,12 @@ impl ByteCompiler<'_, '_> {
let initial_jump = self.jump();
let start_address = self.next_opcode_location();
self.push_loop_control_info(label, start_address);
self.current_jump_control_mut()
.expect("jump_control must exist as it was just pushed")
.set_label(label);
self.current_jump_control_mut()
.expect("jump_control must exist as it was just pushed")
.set_start_address(start_address);
self.emit_opcode(Opcode::LoopContinue);
if let Some(final_expr) = for_loop.final_expr() {
@ -62,15 +68,16 @@ impl ByteCompiler<'_, '_> {
self.emit(Opcode::Jump, &[start_address]);
self.patch_jump(exit);
self.pop_loop_control_info();
self.emit_opcode(Opcode::LoopEnd);
let (num_bindings, compile_environment) = self.context.pop_compile_time_environment();
let index_compile_environment = self.push_compile_environment(compile_environment);
self.patch_jump_with_target(push_env.0, num_bindings as u32);
self.patch_jump_with_target(push_env.1, index_compile_environment as u32);
self.emit_opcode(Opcode::PopEnvironment);
self.patch_jump(exit);
self.pop_loop_control_info();
self.emit_opcode(Opcode::LoopEnd);
self.emit_and_track_pop_env();
Ok(())
}

257
boa_engine/src/bytecompiler/statement/mod.rs

@ -1,228 +1,69 @@
use boa_ast::{
statement::{Block, Break, If, Labelled, LabelledItem, Switch},
Statement,
};
use boa_interner::Sym;
use crate::{bytecompiler::ByteCompiler, vm::Opcode, JsResult};
use crate::{vm::Opcode, JsNativeError, JsResult};
use super::{ByteCompiler, JumpControlInfoKind, NodeKind};
use boa_ast::Statement;
mod block;
mod r#break;
mod r#continue;
mod r#if;
mod labelled;
mod r#loop;
mod switch;
mod r#try;
impl ByteCompiler<'_, '_> {
pub(crate) fn compile_if(&mut self, node: &If, configurable_globals: bool) -> JsResult<()> {
self.compile_expr(node.cond(), true)?;
let jelse = self.jump_if_false();
self.compile_stmt(node.body(), false, configurable_globals)?;
match node.else_node() {
None => {
self.patch_jump(jelse);
}
Some(else_body) => {
let exit = self.jump();
self.patch_jump(jelse);
self.compile_stmt(else_body, false, configurable_globals)?;
self.patch_jump(exit);
}
}
Ok(())
}
pub(crate) fn compile_labelled(
/// Compiles a [`Statement`] `boa_ast` node.
pub fn compile_stmt(
&mut self,
labelled: &Labelled,
node: &Statement,
use_expr: bool,
configurable_globals: bool,
) -> JsResult<()> {
match labelled.item() {
LabelledItem::Statement(stmt) => match stmt {
Statement::ForLoop(for_loop) => {
self.compile_for_loop(for_loop, Some(labelled.label()), configurable_globals)?;
}
Statement::ForInLoop(for_in_loop) => {
self.compile_for_in_loop(
for_in_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::ForOfLoop(for_of_loop) => {
self.compile_for_of_loop(
for_of_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::WhileLoop(while_loop) => {
self.compile_while_loop(
while_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::DoWhileLoop(do_while_loop) => {
self.compile_do_while_loop(
do_while_loop,
Some(labelled.label()),
configurable_globals,
)?;
}
Statement::Block(block) => {
self.compile_block(
block,
Some(labelled.label()),
use_expr,
configurable_globals,
)?;
}
stmt => self.compile_stmt(stmt, use_expr, configurable_globals)?,
},
LabelledItem::Function(f) => {
self.function(f.into(), NodeKind::Declaration, false)?;
match node {
Statement::Var(var) => self.compile_var_decl(var)?,
Statement::If(node) => self.compile_if(node, configurable_globals)?,
Statement::ForLoop(for_loop) => {
self.compile_for_loop(for_loop, None, configurable_globals)?;
}
}
Ok(())
}
pub(crate) fn compile_break(&mut self, node: Break) -> JsResult<()> {
let next = self.next_opcode_location();
if let Some(info) = self
.jump_info
.last()
.filter(|info| info.kind == JumpControlInfoKind::Try)
{
let in_finally = if let Some(finally_start) = info.finally_start {
next >= finally_start.index
} else {
false
};
let in_catch_no_finally = !info.has_finally && info.in_catch;
if in_finally {
self.emit_opcode(Opcode::PopIfThrown);
Statement::ForInLoop(for_in_loop) => {
self.compile_for_in_loop(for_in_loop, None, configurable_globals)?;
}
if in_finally || in_catch_no_finally {
self.emit_opcode(Opcode::CatchEnd2);
} else {
self.emit_opcode(Opcode::TryEnd);
Statement::ForOfLoop(for_of_loop) => {
self.compile_for_of_loop(for_of_loop, None, configurable_globals)?;
}
self.emit(Opcode::FinallySetJump, &[u32::MAX]);
}
let label = self.jump();
if let Some(label_name) = node.label() {
let mut found = false;
for info in self.jump_info.iter_mut().rev() {
if info.label == Some(label_name) {
info.breaks.push(label);
found = true;
break;
}
Statement::WhileLoop(while_loop) => {
self.compile_while_loop(while_loop, None, configurable_globals)?;
}
// TODO: promote to an early error.
if !found {
return Err(JsNativeError::syntax()
.with_message(format!(
"Cannot use the undeclared label '{}'",
self.interner().resolve_expect(label_name)
))
.into());
Statement::DoWhileLoop(do_while_loop) => {
self.compile_do_while_loop(do_while_loop, None, configurable_globals)?;
}
} else {
self.jump_info
.last_mut()
// TODO: promote to an early error.
.ok_or_else(|| {
JsNativeError::syntax()
.with_message("unlabeled break must be inside loop or switch")
})?
.breaks
.push(label);
}
Ok(())
}
pub(crate) fn compile_switch(
&mut self,
switch: &Switch,
configurable_globals: bool,
) -> JsResult<()> {
self.context.push_compile_time_environment(false);
let push_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment);
for case in switch.cases() {
self.create_decls(case.body(), configurable_globals);
}
self.emit_opcode(Opcode::LoopStart);
let start_address = self.next_opcode_location();
self.push_switch_control_info(None, start_address);
self.compile_expr(switch.val(), true)?;
let mut labels = Vec::with_capacity(switch.cases().len());
for case in switch.cases() {
self.compile_expr(case.condition(), true)?;
labels.push(self.emit_opcode_with_operand(Opcode::Case));
}
let exit = self.emit_opcode_with_operand(Opcode::Default);
for (label, case) in labels.into_iter().zip(switch.cases()) {
self.patch_jump(label);
self.compile_statement_list(case.body(), false, configurable_globals)?;
}
self.patch_jump(exit);
if let Some(body) = switch.default() {
self.create_decls(body, configurable_globals);
self.compile_statement_list(body, false, configurable_globals)?;
}
self.pop_switch_control_info();
self.emit_opcode(Opcode::LoopEnd);
let (num_bindings, compile_environment) = self.context.pop_compile_time_environment();
let index_compile_environment = self.push_compile_environment(compile_environment);
self.patch_jump_with_target(push_env.0, num_bindings as u32);
self.patch_jump_with_target(push_env.1, index_compile_environment as u32);
self.emit_opcode(Opcode::PopEnvironment);
Ok(())
}
pub(crate) fn compile_block(
&mut self,
block: &Block,
label: Option<Sym>,
use_expr: bool,
configurable_globals: bool,
) -> JsResult<()> {
if let Some(label) = label {
let next = self.next_opcode_location();
self.push_labelled_block_control_info(label, next);
}
self.context.push_compile_time_environment(false);
let push_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment);
self.create_decls(block.statement_list(), configurable_globals);
self.compile_statement_list(block.statement_list(), use_expr, configurable_globals)?;
let (num_bindings, compile_environment) = self.context.pop_compile_time_environment();
let index_compile_environment = self.push_compile_environment(compile_environment);
self.patch_jump_with_target(push_env.0, num_bindings as u32);
self.patch_jump_with_target(push_env.1, index_compile_environment as u32);
if label.is_some() {
self.pop_labelled_block_control_info();
Statement::Block(block) => {
self.compile_block(block, None, use_expr, configurable_globals)?;
}
Statement::Labelled(labelled) => {
self.compile_labelled(labelled, use_expr, configurable_globals)?;
}
Statement::Continue(node) => self.compile_continue(*node)?,
Statement::Break(node) => self.compile_break(*node)?,
Statement::Throw(throw) => {
self.compile_expr(throw.target(), true)?;
self.emit(Opcode::Throw, &[]);
}
Statement::Switch(switch) => {
self.compile_switch(switch, configurable_globals)?;
}
Statement::Return(ret) => {
if let Some(expr) = ret.target() {
self.compile_expr(expr, true)?;
} else {
self.emit(Opcode::PushUndefined, &[]);
}
self.emit(Opcode::Return, &[]);
}
Statement::Try(t) => self.compile_try(t, use_expr, configurable_globals)?,
Statement::Empty => {}
Statement::Expression(expr) => self.compile_expr(expr, use_expr)?,
}
self.emit_opcode(Opcode::PopEnvironment);
Ok(())
}
}

54
boa_engine/src/bytecompiler/statement/switch.rs

@ -0,0 +1,54 @@
use boa_ast::statement::Switch;
use crate::{bytecompiler::ByteCompiler, vm::Opcode, JsResult};
impl ByteCompiler<'_, '_> {
/// Compile a [`Switch`] `boa_ast` node
pub(crate) fn compile_switch(
&mut self,
switch: &Switch,
configurable_globals: bool,
) -> JsResult<()> {
self.context.push_compile_time_environment(false);
let push_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment);
for case in switch.cases() {
self.create_decls(case.body(), configurable_globals);
}
self.emit_opcode(Opcode::LoopStart);
let start_address = self.next_opcode_location();
self.push_switch_control_info(None, start_address);
self.compile_expr(switch.val(), true)?;
let mut labels = Vec::with_capacity(switch.cases().len());
for case in switch.cases() {
self.compile_expr(case.condition(), true)?;
labels.push(self.emit_opcode_with_operand(Opcode::Case));
}
let exit = self.emit_opcode_with_operand(Opcode::Default);
for (label, case) in labels.into_iter().zip(switch.cases()) {
self.patch_jump(label);
self.compile_statement_list(case.body(), false, configurable_globals)?;
}
self.patch_jump(exit);
if let Some(body) = switch.default() {
self.create_decls(body, configurable_globals);
self.compile_statement_list(body, false, configurable_globals)?;
}
self.pop_switch_control_info();
self.emit_opcode(Opcode::LoopEnd);
let (num_bindings, compile_environment) = self.context.pop_compile_time_environment();
let index_compile_environment = self.push_compile_environment(compile_environment);
self.patch_jump_with_target(push_env.0, num_bindings as u32);
self.patch_jump_with_target(push_env.1, index_compile_environment as u32);
self.emit_opcode(Opcode::PopEnvironment);
Ok(())
}
}

4
boa_engine/src/bytecompiler/statement/try.rs

@ -33,7 +33,7 @@ impl ByteCompiler<'_, '_> {
self.patch_jump(Label { index: try_start });
if let Some(catch) = t.catch() {
self.push_try_control_info_catch_start();
self.set_jump_control_catch_start(true);
let catch_start = if t.finally().is_some() {
Some(self.emit_opcode_with_operand(Opcode::CatchStart))
} else {
@ -83,7 +83,7 @@ impl ByteCompiler<'_, '_> {
if let Some(finally) = t.finally() {
self.emit_opcode(Opcode::FinallyStart);
let finally_start_address = self.next_opcode_location();
self.push_try_control_info_finally_start(Label {
self.set_jump_control_finally_start(Label {
index: finally_start_address,
});
self.patch_jump_with_target(

103
boa_engine/src/tests.rs

@ -2095,6 +2095,109 @@ fn bigger_switch_example() {
}
}
#[test]
fn break_environment_gauntlet() {
// test that break handles popping environments correctly.
let scenario = r#"
let a = 0;
while(true) {
break;
}
while(true) break
do {break} while(true);
while (a < 3) {
let a = 1;
if (a == 1) {
break;
}
let b = 2;
}
{
b = 0;
do {
b = 2
if (b == 2) {
break;
}
b++
} while( i < 3);
let c = 1;
}
{
for (let r = 0; r< 3; r++) {
if (r == 2) {
break;
}
}
}
basic: for (let a = 0; a < 2; a++) {
break;
}
{
let result = true;
{
let x = 2;
L: {
let x = 3;
result &&= (x === 3);
break L;
result &&= (false);
}
result &&= (x === 2);
}
result;
}
{
var str = "";
far_outer: {
outer: for (let i = 0; i < 5; i++) {
inner: for (let b = 5; b < 10; b++) {
if (b === 7) {
break far_outer;
}
str = str + b;
}
str = str + i;
}
}
str
}
{
for (let r = 0; r < 2; r++) {
str = str + r
}
}
{
result = "";
lab_block: {
try {
result = "try_block";
break lab_block;
result = "did not break"
} catch (err) {}
}
str = str + result
str
}
"#;
assert_eq!(&exec(scenario), "\"5601try_block\"");
}
#[test]
fn while_loop_late_break() {
// Ordering with statement before the break.

3
boa_engine/src/vm/code_block.rs

@ -241,7 +241,8 @@ impl CodeBlock {
Opcode::TryStart
| Opcode::PushDeclarativeEnvironment
| Opcode::PushFunctionEnvironment
| Opcode::CopyDataProperties => {
| Opcode::CopyDataProperties
| Opcode::Break => {
let operand1 = self.read::<u32>(*pc);
*pc += size_of::<u32>();
let operand2 = self.read::<u32>(*pc);

16
boa_engine/src/vm/flowgraph/mod.rs

@ -129,6 +129,22 @@ impl CodeBlock {
EdgeStyle::Line,
);
}
Opcode::Break => {
let jump_operand = self.read::<u32>(pc);
pc += size_of::<u32>();
let envs_operand = self.read::<u32>(pc);
pc += size_of::<u32>();
let label = format!("{opcode_str} {jump_operand}, pop {envs_operand}");
graph.add_node(previous_pc, NodeShape::None, label.into(), Color::Red);
graph.add_edge(
previous_pc,
jump_operand as usize,
Some("BREAK".into()),
Color::Red,
EdgeStyle::Line,
);
}
Opcode::LogicalAnd | Opcode::LogicalOr | Opcode::Coalesce => {
let exit = self.read::<u32>(pc);
pc += size_of::<u32>();

44
boa_engine/src/vm/opcode/jump/break.rs

@ -0,0 +1,44 @@
use crate::{
vm::{opcode::Operation, ShouldExit},
Context, JsResult,
};
/// `Break` implements the Opcode Operation for `Opcode::Break`
///
/// Operation:
/// - Pop required environments and jump to address.
pub(crate) struct Break;
impl Operation for Break {
const NAME: &'static str = "Break";
const INSTRUCTION: &'static str = "INST - Break";
fn execute(context: &mut Context<'_>) -> JsResult<ShouldExit> {
let address = context.vm.read::<u32>();
let pop_envs = context.vm.read::<u32>();
for _ in 0..pop_envs {
context.realm.environments.pop();
let loop_envs = *context
.vm
.frame()
.loop_env_stack
.last()
.expect("loop env stack must exist");
if loop_envs == 0 {
context
.vm
.frame_mut()
.loop_env_stack
.pop()
.expect("loop env stack must exist");
}
context.vm.frame_mut().loop_env_stack_dec();
context.vm.frame_mut().try_env_stack_dec();
}
context.vm.frame_mut().pc = address as usize;
Ok(ShouldExit::False)
}
}

3
boa_engine/src/vm/opcode/jump/mod.rs

@ -3,6 +3,9 @@ use crate::{
Context, JsResult,
};
pub(crate) mod r#break;
pub(crate) use r#break::*;
/// `Jump` implements the Opcode Operation for `Opcode::Jump`
///
/// Operation:

3
boa_engine/src/vm/opcode/mod.rs

@ -1435,6 +1435,9 @@ generate_impl! {
/// Stack: promise **=>**
Await,
/// Jumps to a target location and pops the environments involved.
Break,
/// Push the current new target to the stack.
///
/// Operands:

Loading…
Cancel
Save