oxc-project / oxc-project/backlog

Re-design `ScopeFlags`

Open
#16 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
No language data
Stars
7
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Personally I find the design of ScopeFlags confusing. It's also a bit bloated - we can pack more information into less bits.

For example, presently we use separate bits for Constructor and GetAccessor, but the two are mutually exclusive - a function can't be both a constructor and a getter. So we can compress this info.

New design

I propose redesigning it as an 8-bit bitfield with the following parts:

  • Bits 0-2: "Full" function type i.e. function or method (not arrow)
  • Bits 3-4: "Special" block i.e. ClassStaticBlock / TsModuleBlock
  • Bit 5: Strict mode
  • Bit 6: Arrow function
  • Bit 7: Statement block
Why?
Encode more info

This allows us to distinguish between many different states which we can't presently without searching up the scopes stack. e.g.:

  • At program top level (ScopeFlags::Top).
  • In a nested statement block at program top level (ScopeFlags::Block).
  • At top level within a function (ScopeFlags::Function).
  • In a nested statement block within a function (ScopeFlags::Function | ScopeFlags::Block).
  • In an arrow function within a class constructor (ScopeFlags::Constructor | ScopeFlags::Arrow).
Reduce type size

Reducing ScopeFlags from 2 bytes to 1 byte is not such a big wow in itself.

But getting it down to 8 bits would also allow packing it into ScopeId. ScopeId can be a u32, but 24 bits should suffice for the actual ID (16 million scopes is probably enough for any JS file). So the top 8 bits could contain the scope flags. Then we can have scope type info embedded inline, reducing memory accesses.

Implementation

Implementation something like this:

const FUNCTION_TYPE_MASK:  u8 = 0b00000111;
const BLOCK_TYPE_MASK:     u8 = 0b00011000;
const BLOCK_TYPE_SHIFT:    u8 = 3;
const STRICT_MODE:         u8 = 0b00100000;
const ARROW:               u8 = 0b01000000;
const BLOCK:               u8 = 0b10000000;

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScopeFlags(u8);

#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u8)]
pub enum FunctionType {
  None = 0,
  Function = 1,
  ObjectMethod = 2,
  ClassMethod = 3,
  GetAccessor = 4,
  SetAccessor = 5,
  Constructor = 6,
  // 7 is unused
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u8)]
pub enum SpecialBlockType {
  None = 0,
  ClassStaticBlock = 1 << BLOCK_TYPE_SHIFT,
  TsModuleBlock = 2 << BLOCK_TYPE_SHIFT,
  // 3 is unused
}

#[allow(non_upper_case_globals)]
impl ScopeFlags {
  pub const Top: Self = Self(FunctionType::None as u8);
  pub const Function: Self = Self(FunctionType::Function as u8);
  pub const Constructor: Self = Self(FunctionType::Constructor as u8);
  pub const GetAccessor: Self = Self(FunctionType::GetAccessor as u8);
  pub const SetAccessor: Self = Self(FunctionType::SetAccessor as u8);

  pub const ClassStaticBlock: Self = Self(SpecialBlockType::ClassStaticBlock as u8);
  pub const TsModuleBlock: Self = Self(SpecialBlockType::TsModuleBlock as u8);

  pub const StrictMode: Self = Self(STRICT_MODE);
  pub const Arrow: Self = Self(ARROW);
  pub const Block: Self = Self(BLOCK);
}

impl ScopeFlags {
  pub const fn at_top(self) -> bool {
    self.function_type() == FunctionType::None
  }

  pub const fn function_type(self) -> FunctionType {
    unsafe { std::mem::transmute(self.0 & FUNCTION_TYPE_MASK) }
  }

  pub const fn in_full_function(&self) -> bool {
    self.function_type() != FunctionType::None
  }

  pub const fn special_block_type(self) -> SpecialBlockType {
    unsafe { std::mem::transmute(self.0 & BLOCK_TYPE_MASK) }
  }

  pub const fn in_special_block(self) -> bool {
    self.special_block_type() != SpecialBlockType::None
  }

  pub const fn is_strict_mode(self) -> bool {
    (self.0 & STRICT_MODE) != 0
  }

  pub const fn in_arrow(self) -> bool {
    (self.0 & ARROW) != 0
  }

  pub const fn in_block(self) -> bool {
    (self.0 & BLOCK) != 0
  }

  pub const fn with_function_type(self, fn_type: FunctionType) -> Self {
    Self((self.0 & !FUNCTION_TYPE_MASK) | fn_type as u8)
  }

  pub const fn with_special_block_type(self, block_type: SpecialBlockType) -> Self {
    Self((self.0 & !BLOCK_TYPE_MASK) | block_type as u8)
  }

  pub const fn with_strict_mode(self, is_strict: bool) -> Self {
    Self((self.0 & !STRICT_MODE) | (is_strict as u8 * STRICT_MODE))
  }

  pub const fn with_arrow(self, is_arrow: bool) -> Self {
    Self((self.0 & !ARROW) | (is_arrow as u8 * ARROW))
  }

  pub const fn with_block(self, is_block: bool) -> Self {
    Self((self.0 & !BLOCK) | (is_block as u8 * BLOCK))
  }
}

Any thoughts?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Locate the existing ScopeFlags and ScopeId definitions and their uses; start by comparing the current representations with the proposed 8-bit layout. Then verify that function, block, strict-mode, arrow, and ScopeId semantics remain correct; the issue names no specific files or tests, so identify those entry points first.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.