oxc-project / oxc-project/backlog
AST: Separate Function Expression Name and Function Scopes
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 7
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Parent: oxc-project/backlog#210
Summary
Tighten how functions own their name, parameter, and body scopes.
Function currently has one scope_id covering the function expression's local name, type parameters, parameters, return type, and body. Named function expressions require a separate outer name scope so a parameter or body binding can shadow the local function name without creating two
same-named symbols in one scope.
This issue follows the class/function-name split, which moves names onto declaration and expression wrappers but still needs to define where the corresponding scopes live.
Motivation
A named function expression creates a local name binding that is separate from its parameters and body bindings:
const fn = function local(local) {
return local;
};
The bindings are conceptually nested:
Program scope
fn
Function-expression name scope
local // `function local`
Function scope
local // parameter; shadows the function-expression name
The same applies to body bindings:
const fn = function copy(dst) {
const copy = Boolean(dst);
return copy;
};
The expression-local function copy binding remains available to the function, but the body binding may shadow it. They must not be recorded as two distinct symbols with the same name in one scope.
The current semantic traversal enters Function::scope_id, binds a function-expression name there, then visits parameters and the body in that same scope. This can produce contradictory Scoping state:
- two symbols claim the same scope and name;
- the scope's binding map can contain only one of them;
- the other symbol still points at that scope but is absent from its binding map;
- consumers observe different symbol sets depending on which representation they inspect.
This surfaced as a mangler idempotency bug for code shaped like:
BufferList.prototype.copy = function copy(dst, dstStart) {
if (typeof dstStart !== "number") {
dstStart = 0;
}
const copy = Boolean(dst);
return dst || Buffer.alloc(0);
};
When the function-expression name and body copy binding are treated as collisions in one scope, the first mangle pass changes the binding set seen by the second pass and generated slots can drift. The mangler can defensively handle malformed scope data, but the AST and semantic shape should not make that inconsistency representable.
Ordinary function declarations differ: their required name binds in the enclosing declaration scope, then the function enters its parameter/body scope. Anonymous function expressions do not need a function-expression-name scope at all.
Current AST Shape
The checked-in AST stores the optional name and the only function scope on the same node:
pub struct Function<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: FunctionType,
pub id: Option<BindingIdentifier<'a>>,
pub generator: bool,
pub r#async: bool,
#[ts]
pub declare: bool,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
#[ts]
pub this_param: Option<Box<'a, TSThisParameter<'a>>>,
pub params: Box<'a, FormalParameters<'a>>,
#[ts]
pub return_type: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub body: Option<Box<'a, FunctionBody<'a>>>,
pub scope_id: Cell<Option<ScopeId>>,
pub pure: bool,
pub pife: bool,
}
Parameters and the body are separate AST nodes, but neither owns the function scope:
pub struct FormalParameters<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: FormalParameterKind,
pub items: Vec<'a, FormalParameter<'a>>,
pub rest: Option<Box<'a, FormalParameterRest<'a>>>,
}
pub struct FunctionBody<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub directives: Vec<'a, Directive<'a>>,
pub statements: Vec<'a, Statement<'a>>,
}
After the class/function-name proposal, the relevant owner shapes become:
pub struct FunctionDeclaration<'a> {
pub id: BindingIdentifier<'a>,
pub function: Function<'a>,
}
pub struct ExportDefaultFunctionDeclaration<'a> {
pub id: Option<BindingIdentifier<'a>>,
pub function: Function<'a>,
}
pub struct FunctionExpression<'a> {
pub id: Option<BindingIdentifier<'a>>,
pub function: Function<'a>,
}
That fixes name optionality but does not itself encode the extra scope required only by a named
function expression. The remaining design must determine which AST nodes own:
- the optional function-expression-name scope;
- the function scope used by type parameters, parameters, return types, and body bindings;
- scope IDs used by transforms, traversal, semantic analysis, and the mangler.
Proposed AST Shape
TODO
Syntax Mapping
TODO
Guaranteed Invariants
TODO
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the Current AST Shape and trace Function scope IDs through semantic traversal, transforms, and the mangler. Define the proposed AST ownership for the function-expression-name scope and function scope, complete the syntax mapping and guaranteed invariants, then verify that all named, anonymous, and declared function cases preserve consistent scope and symbol state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, rust
- Domain
- compilers
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100