use std::collections::BTreeSet;
use crate::compiler_error::{CompilerFailure, CompilerStage};
use crate::tree_height;
use crate::{
Ast, Binding, BindingKind, ClassMember, Expr, ExprId, ExprKind, Ident, ParamDecl,
PatternOrigin, Span, Stmt, StmtId, StmtKind,
};
pub fn lower(mut ast: Ast) -> Result<Ast, CompilerFailure> {
tree_height::check_syntax(&ast).map_err(|failure| failure.with_stage(CompilerStage::Infer))?;
let mut ctx = LowerCtx { next_tmp: 0 };
ctx.lower_arrows(&mut ast)?;
ctx.lower_function_params(&mut ast)?;
ctx.lower_class_member_params(&mut ast)?;
ctx.lower_for_of_patterns(&mut ast)?;
ctx.lower_for_pattern_inits(&mut ast)?;
ctx.lower_pattern_stmts(&mut ast)?;
Ok(ast)
}
const FRESH_MARKER: &str = "#pattern_";
const PATTERN_PARAM: &str = "p";
pub(crate) fn is_pattern_param(name: &str) -> bool {
name.is_empty()
|| name
.strip_prefix(FRESH_MARKER)
.and_then(|rest| rest.strip_prefix(PATTERN_PARAM))
.is_some_and(|rest| rest.starts_with('_'))
}
const PATTERN_VALUE: &str = "dst";
pub(crate) fn is_pattern_value(name: &str) -> bool {
name.strip_prefix(FRESH_MARKER)
.and_then(|rest| rest.strip_prefix(PATTERN_VALUE))
.is_some_and(|rest| rest.starts_with('_'))
}
struct LowerCtx {
next_tmp: u32,
}
#[derive(Clone, Copy)]
struct PatternRoot {
is_const: bool,
annotated: bool,
}
impl LowerCtx {
fn fresh(&mut self, prefix: &str, span: Span) -> Result<Ident, CompilerFailure> {
let n = self.next_tmp;
self.next_tmp = self
.next_tmp
.checked_add(1)
.ok_or_else(|| CompilerFailure::Limit {
stage: CompilerStage::Infer,
span: None,
message: "pattern temporary capacity exceeded".into(),
help: vec![],
})?;
Ok(Ident {
name: format!("{FRESH_MARKER}{prefix}_{n}"),
span,
})
}
fn lower_arrows(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
for id in ast
.expr_ids()
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
{
let has_pattern = match &ast
.try_expr(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
{
ExprKind::Arrow { params, .. } => params.iter().any(|p| p.pattern.is_some()),
_ => false,
};
if has_pattern {
self.lower_arrow(ast, id)?;
}
}
Ok(())
}
fn lower_arrow(&mut self, ast: &mut Ast, id: ExprId) -> Result<(), CompilerFailure> {
let kind = ast
.try_expr(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
.clone();
let ExprKind::Arrow {
mut params,
return_type,
type_predicate,
body,
} = kind
else {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
};
self.materialise_pattern_params(ast, &mut params)?;
ast.try_expr_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind = ExprKind::Arrow {
params,
return_type,
type_predicate,
body,
};
Ok(())
}
fn lower_function_params(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
for id in ast
.stmt_ids()
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
{
let has_pattern = match &ast
.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
{
StmtKind::Function { params, .. } => params.iter().any(|p| p.pattern.is_some()),
_ => false,
};
if has_pattern {
self.lower_function(ast, id)?;
}
}
Ok(())
}
fn lower_function(&mut self, ast: &mut Ast, id: StmtId) -> Result<(), CompilerFailure> {
let stmt_span = ast
.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.span;
let kind = std::mem::replace(
&mut ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::Block(Vec::new()), );
let StmtKind::Function {
name,
generics,
mut params,
return_type,
type_predicate,
body,
doc,
} = kind
else {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
};
self.materialise_pattern_params(ast, &mut params)?;
ast.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind = StmtKind::Function {
name,
generics,
params,
return_type,
type_predicate,
body,
doc,
};
let _ = stmt_span;
Ok(())
}
fn lower_class_member_params(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
for id in ast
.stmt_ids()
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
{
let StmtKind::ClassDecl { members, .. } = &mut ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
else {
continue;
};
let mut taken = std::mem::take(members);
let lowered = self.lower_member_params(ast, &mut taken);
let StmtKind::ClassDecl { members, .. } = &mut ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
else {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
};
*members = taken;
lowered?;
}
Ok(())
}
fn lower_member_params(
&mut self,
ast: &mut Ast,
members: &mut [ClassMember],
) -> Result<(), CompilerFailure> {
for member in members {
let params = match member {
ClassMember::Method { params, .. } | ClassMember::Constructor { params, .. } => {
params.as_mut_slice()
}
ClassMember::Accessor {
param: Some(param), ..
} => std::slice::from_mut(param.as_mut()),
ClassMember::Accessor { param: None, .. } | ClassMember::Field { .. } => continue,
};
self.materialise_pattern_params(ast, params)?;
}
Ok(())
}
fn lower_for_of_patterns(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
for id in ast
.stmt_ids()
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
{
if !matches!(
ast.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::ForOfPattern { .. }
) {
continue;
}
self.expand_for_of_pattern(ast, id)?;
}
Ok(())
}
fn expand_for_of_pattern(&mut self, ast: &mut Ast, id: StmtId) -> Result<(), CompilerFailure> {
let stmt_span = ast
.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.span;
let kind = std::mem::replace(
&mut ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::Block(Vec::new()),
);
let StmtKind::ForOfPattern {
binding_kind,
binding,
ty,
iter,
body,
} = kind
else {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
};
let pattern_span = binding.span();
let dst = self.fresh(PATTERN_VALUE, pattern_span)?;
let is_const = matches!(binding_kind, BindingKind::Const);
let root = PatternRoot {
is_const,
annotated: ty.is_some(),
};
let mut decompose = self.emit_decompose(
ast,
binding,
dst.clone(),
root,
None,
None,
)?;
let mut source_bindings = Vec::new();
for &id in &decompose {
match &ast
.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
{
StmtKind::Let { name, .. }
| StmtKind::Const { name, .. }
| StmtKind::ObjectRest { name, .. } => source_bindings.push(name.clone()),
_ => {}
}
}
ast.for_of_pattern_bindings.insert(id, source_bindings);
decompose.push(body);
let body = ast
.try_push_stmt(Stmt {
kind: StmtKind::Block(decompose),
span: ast
.try_stmt(body)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
ast.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind = StmtKind::ForOf {
binding_kind,
name: dst,
ty,
iter,
body,
};
let _ = stmt_span;
Ok(())
}
fn lower_for_pattern_inits(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
for id in ast
.stmt_ids()
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
{
let StmtKind::For {
init: Some(init), ..
} = ast
.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
else {
continue;
};
let is_let = match ast
.try_stmt(init)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
{
StmtKind::LetPattern { .. } => true,
StmtKind::ConstPattern { .. } => false,
_ => continue,
};
self.hoist_for_pattern_init(ast, id, init, is_let)?;
}
Ok(())
}
fn hoist_for_pattern_init(
&mut self,
ast: &mut Ast,
id: StmtId,
init: StmtId,
is_let: bool,
) -> Result<(), CompilerFailure> {
let mut hoisted = self.expand_pattern_stmt(ast, init)?;
let loop_stmt = ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
let span = loop_stmt.span;
let kind = std::mem::replace(&mut loop_stmt.kind, StmtKind::Block(Vec::new()));
let StmtKind::For {
condition,
update,
body,
..
} = kind
else {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
};
let lowered_loop = ast
.try_push_stmt(Stmt {
kind: StmtKind::For {
init: None,
condition,
update,
body,
},
span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
if is_let {
let bindings = let_names(ast, &hoisted)?;
if !bindings.is_empty() {
ast.for_pattern_init_bindings.insert(lowered_loop, bindings);
}
}
hoisted.push(lowered_loop);
ast.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind = StmtKind::Block(hoisted);
Ok(())
}
fn lower_pattern_stmts(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
let top = std::mem::take(&mut ast.top_level);
ast.top_level = self.expand_list(ast, top)?;
for id in ast
.stmt_ids()
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
{
let needs = matches!(
ast.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::Block(_)
);
if !needs {
continue;
}
let StmtKind::Block(taken) = std::mem::replace(
&mut ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::Block(Vec::new()),
) else {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
};
let expanded = self.expand_list(ast, taken)?;
ast.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind = StmtKind::Block(expanded);
}
Ok(())
}
fn expand_list(
&mut self,
ast: &mut Ast,
ids: Vec<StmtId>,
) -> Result<Vec<StmtId>, CompilerFailure> {
let mut out = Vec::with_capacity(ids.len());
for id in ids {
let is_pattern = matches!(
ast.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::LetPattern { .. } | StmtKind::ConstPattern { .. }
);
if !is_pattern {
out.push(id);
continue;
}
let expanded = self.expand_pattern_stmt(ast, id)?;
out.extend(expanded);
}
Ok(out)
}
fn expand_pattern_stmt(
&mut self,
ast: &mut Ast,
id: StmtId,
) -> Result<Vec<StmtId>, CompilerFailure> {
let span = ast
.try_stmt(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.span;
let kind = std::mem::replace(
&mut ast
.try_stmt_mut(id)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind,
StmtKind::Block(Vec::new()), );
let (is_const, binding, ty, value, doc) = match kind {
StmtKind::LetPattern {
binding,
ty,
value,
doc,
} => (false, binding, ty, value, doc),
StmtKind::ConstPattern {
binding,
ty,
value,
doc,
} => (true, binding, ty, value, doc),
_ => {
return Err(lowering_failure(
"unexpected node kind during pattern lowering",
));
}
};
let literal = match ty {
None => mark_literal_source(ast, &binding, value)?,
Some(_) => None,
};
let pattern_span = binding.span();
let dst = self.fresh(PATTERN_VALUE, pattern_span)?;
let annotated = ty.is_some();
let dst_stmt = ast
.try_push_stmt(Stmt {
kind: StmtKind::Const {
name: dst.clone(),
ty,
value,
doc: None,
},
span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
let mut out = vec![dst_stmt];
let root = PatternRoot {
is_const,
annotated,
};
out.extend(self.emit_decompose(ast, binding, dst, root, doc, literal.as_ref())?);
Ok(out)
}
fn materialise_pattern_params(
&mut self,
ast: &mut Ast,
params: &mut [ParamDecl],
) -> Result<(), CompilerFailure> {
for param in params.iter_mut() {
let Some(pattern) = param.pattern.take() else {
continue;
};
let fresh = self.fresh(PATTERN_PARAM, pattern.span())?;
param.name = fresh.clone();
let root = PatternRoot {
is_const: false,
annotated: param.ty.is_some(),
};
let literal = match (¶m.ty, param.default) {
(None, Some(default)) => mark_literal_source(ast, &pattern, default)?,
_ => None,
};
if let (Some(LiteralSource::Tuple(len)), Some(default)) = (literal, param.default)
&& let Some(pad_defaults) = defaults_past_end(&pattern, len)
{
ast.tuple_pattern_pad_defaults.insert(default, pad_defaults);
}
let statements =
self.emit_decompose(ast, pattern, fresh, root, None, None)?;
if ast
.parameter_bindings
.insert(param.name.span, statements)
.is_some()
{
return Err(lowering_failure(
"duplicate parameter binding span during pattern lowering",
));
}
}
Ok(())
}
fn emit_decompose(
&mut self,
ast: &mut Ast,
binding: Binding,
source: Ident,
root: PatternRoot,
mut doc: Option<crate::DocComment>,
literal: Option<&LiteralSource>,
) -> Result<Vec<StmtId>, CompilerFailure> {
let is_const = root.is_const;
let mut out = Vec::new();
match binding {
Binding::Object {
fields,
rest,
span: _,
} => {
let bound_names: Vec<Ident> = fields.iter().map(|f| f.source.clone()).collect();
for field in fields {
let absent = matches!(
literal,
Some(LiteralSource::Object(names)) if !names.contains(&field.source.name)
) && !OBJECT_PROTOTYPE_MEMBERS
.contains(&field.source.name.as_str());
let access = if absent && field.default.is_some() {
push_void_zero(ast, field.span)?
} else {
let recv =
push_expr(ast, ExprKind::Identifier(source.clone()), field.span)?;
push_expr(
ast,
ExprKind::FieldAccess {
receiver: recv,
name: field.source.clone(),
},
field.span,
)?
};
let value = self.apply_binding_default(
ast,
access,
field.default,
root.annotated,
field.span,
&mut out,
)?;
let decl = ast
.try_push_stmt(Stmt {
kind: make_decl(is_const, field.local, value, doc.take()),
span: field.span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
out.push(decl);
}
if let Some(rest_ident) = rest {
let src_expr = ast
.try_push_expr(Expr {
kind: ExprKind::Identifier(source.clone()),
span: rest_ident.span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
let decl = ast
.try_push_stmt(Stmt {
kind: StmtKind::ObjectRest {
is_const,
name: rest_ident.clone(),
source: src_expr,
exclude: bound_names,
ty: None,
doc: doc.take(),
},
span: rest_ident.span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
out.push(decl);
}
}
Binding::Array {
elems,
defaults,
rest,
span: pattern_span,
} => {
if elems.len() != defaults.len() {
return Err(lowering_failure(
"array pattern defaults do not match element positions",
));
}
let elems_len = elems.len();
for (i, (slot, default)) in elems.into_iter().zip(defaults).enumerate() {
let Some(local) = slot else {
if default.is_some() {
return Err(lowering_failure("array pattern hole has a default value"));
}
continue;
};
let past_tuple_end =
matches!(literal, Some(LiteralSource::Tuple(len)) if i >= *len);
let access = if default.is_some() && past_tuple_end {
push_void_zero(ast, local.span)?
} else {
let origin = PatternOrigin {
pattern_span,
slot_arity: elems_len,
defaulted: default.is_some(),
};
self.array_slot_read(ast, &source, i, origin, local.span)?
};
let value = self.apply_binding_default(
ast,
access,
default,
root.annotated,
local.span,
&mut out,
)?;
let decl = ast
.try_push_stmt(Stmt {
kind: make_decl(is_const, local.clone(), value, doc.take()),
span: local.span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
out.push(decl);
}
if let Some(rest_ident) = rest {
let rest_value = match literal {
Some(&LiteralSource::Tuple(len)) if len > elems_len => {
remaining_elements(ast, &source, elems_len..len, rest_ident.span)?
}
_ => array_rest(ast, &source, elems_len, rest_ident.span)?,
};
let decl = ast
.try_push_stmt(Stmt {
kind: make_decl(is_const, rest_ident.clone(), rest_value, doc.take()),
span: rest_ident.span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
out.push(decl);
}
}
}
Ok(out)
}
fn array_slot_read(
&mut self,
ast: &mut Ast,
source: &Ident,
index: usize,
origin: PatternOrigin,
span: Span,
) -> Result<ExprId, CompilerFailure> {
let receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
let index = push_expr(ast, ExprKind::Number(index as f64), span)?;
let access = push_expr(ast, ExprKind::IndexAccess { receiver, index }, span)?;
let defaulted = origin.defaulted;
ast.pattern_origins.insert(access, origin);
if !defaulted {
return Ok(access);
}
self.guard_defaulted_array_read(ast, receiver, index, access, span)
}
fn guard_defaulted_array_read(
&mut self,
ast: &mut Ast,
receiver: ExprId,
index: ExprId,
access: ExprId,
span: Span,
) -> Result<ExprId, CompilerFailure> {
let length = push_expr(
ast,
ExprKind::FieldAccess {
receiver,
name: Ident {
name: "length".to_string(),
span,
},
},
span,
)?;
let in_bounds = push_expr(
ast,
ExprKind::Binary {
op: crate::BinOp::Lt,
lhs: index,
rhs: length,
},
span,
)?;
let undefined = push_void_zero(ast, span)?;
push_expr(
ast,
ExprKind::Ternary {
cond: in_bounds,
then_: access,
else_: undefined,
},
span,
)
}
fn apply_binding_default(
&mut self,
ast: &mut Ast,
access: ExprId,
default: Option<ExprId>,
annotated: bool,
span: Span,
out: &mut Vec<StmtId>,
) -> Result<ExprId, CompilerFailure> {
let Some(default) = default else {
return Ok(access);
};
let saved = self.fresh("value", span)?;
let declaration = ast
.try_push_stmt(Stmt {
kind: make_decl(true, saved.clone(), access, None),
span,
})
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
out.push(declaration);
let saved_value = push_expr(ast, ExprKind::Identifier(saved.clone()), span)?;
let undefined = push_void_zero(ast, span)?;
let condition = push_expr(
ast,
ExprKind::Binary {
op: crate::BinOp::Eq,
lhs: saved_value,
rhs: undefined,
},
span,
)?;
let selected = push_expr(
ast,
ExprKind::Ternary {
cond: condition,
then_: default,
else_: saved_value,
},
span,
)?;
ast.binding_defaults.insert(
selected,
crate::ast::BindingDefault {
saved,
default,
annotated,
},
);
Ok(selected)
}
}
enum LiteralSource {
Tuple(usize),
Object(BTreeSet<String>),
}
fn mark_literal_source(
ast: &mut Ast,
binding: &Binding,
value: ExprId,
) -> Result<Option<LiteralSource>, CompilerFailure> {
if let Some(len) = tuple_source_len(ast, binding, value)? {
ast.tuple_pattern_sources.insert(value);
return Ok(Some(LiteralSource::Tuple(len)));
}
if let Binding::Object { fields, .. } = binding
&& let Some(names) = object_literal_names(ast, value)?
{
let defaulted: BTreeSet<String> = fields
.iter()
.filter(|field| field.default.is_some() && names.contains(&field.source.name))
.map(|field| field.source.name.clone())
.collect();
if !defaulted.is_empty() {
ast.object_pattern_sources.insert(value, defaulted);
}
return Ok(Some(LiteralSource::Object(names)));
}
mark_operand_tuple_sources(ast, binding, value)?;
Ok(None)
}
pub(crate) const OBJECT_PROTOTYPE_MEMBERS: &[&str] = &[
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
"__proto__",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
];
fn defaults_past_end(binding: &Binding, len: usize) -> Option<Vec<ExprId>> {
let Binding::Array {
elems, defaults, ..
} = binding
else {
return None;
};
let past: Vec<ExprId> = elems
.iter()
.zip(defaults)
.skip(len)
.map(|(slot, default)| slot.is_some().then_some(*default).flatten())
.collect::<Option<_>>()?;
(!past.is_empty()).then_some(past)
}
fn mark_operand_tuple_sources(
ast: &mut Ast,
binding: &Binding,
value: ExprId,
) -> Result<(), CompilerFailure> {
let operands = match &ast
.try_expr(value)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
{
ExprKind::Paren(inner) => vec![*inner],
ExprKind::Binary {
op: crate::BinOp::NullishCoalesce | crate::BinOp::Or,
rhs,
..
} => vec![*rhs],
ExprKind::Ternary { then_, else_, .. } => vec![*then_, *else_],
_ => return Ok(()),
};
for operand in operands {
if tuple_source_len(ast, binding, operand)?.is_some() {
ast.tuple_pattern_sources.insert(operand);
} else {
mark_operand_tuple_sources(ast, binding, operand)?;
}
}
Ok(())
}
fn object_literal_names(
ast: &Ast,
value: ExprId,
) -> Result<Option<BTreeSet<String>>, CompilerFailure> {
let ExprKind::ObjectLiteral { members } = &ast
.try_expr(value)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
else {
return Ok(None);
};
let mut names = BTreeSet::new();
for member in members {
let crate::ObjectLiteralMember::Field(field) = member else {
return Ok(None);
};
names.insert(field.name.name.clone());
}
Ok(Some(names))
}
fn tuple_source_len(
ast: &Ast,
binding: &Binding,
value: ExprId,
) -> Result<Option<usize>, CompilerFailure> {
let Binding::Array { elems, .. } = binding else {
return Ok(None);
};
if elems.is_empty() {
return Ok(None);
}
let ExprKind::ArrayLiteral { elements } = &ast
.try_expr(value)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
else {
return Ok(None);
};
let has_spread = elements
.iter()
.any(|element| matches!(element, crate::ArrayLiteralElement::Spread { .. }));
Ok((!has_spread).then_some(elements.len()))
}
fn array_rest(
ast: &mut Ast,
source: &Ident,
from: usize,
span: Span,
) -> Result<ExprId, CompilerFailure> {
let from_lit = push_expr(ast, ExprKind::Number(from as f64), span)?;
let slice_receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
let slice_callee = push_expr(
ast,
ExprKind::FieldAccess {
receiver: slice_receiver,
name: Ident {
name: "slice".to_string(),
span,
},
},
span,
)?;
let length_receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
let length_access = push_expr(
ast,
ExprKind::FieldAccess {
receiver: length_receiver,
name: Ident {
name: "length".to_string(),
span,
},
},
span,
)?;
let slice_call = push_expr(
ast,
ExprKind::Call {
callee: slice_callee,
type_args: None,
args: vec![from_lit, length_access],
},
span,
)?;
Ok(slice_call)
}
fn remaining_elements(
ast: &mut Ast,
source: &Ident,
indices: std::ops::Range<usize>,
span: Span,
) -> Result<ExprId, CompilerFailure> {
let count = indices.len();
let mut elements = Vec::with_capacity(count);
for i in indices {
let receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
let index = push_expr(ast, ExprKind::Number(i as f64), span)?;
let access = push_expr(ast, ExprKind::IndexAccess { receiver, index }, span)?;
elements.push(crate::ArrayLiteralElement::Value(access));
}
let remaining = push_expr(ast, ExprKind::ArrayLiteral { elements }, span)?;
ast.tuple_pattern_sources.insert(remaining);
let slice = push_expr(
ast,
ExprKind::FieldAccess {
receiver: remaining,
name: Ident {
name: "slice".to_string(),
span,
},
},
span,
)?;
let from = push_expr(ast, ExprKind::Number(0.0), span)?;
let to = push_expr(ast, ExprKind::Number(count as f64), span)?;
push_expr(
ast,
ExprKind::Call {
callee: slice,
type_args: None,
args: vec![from, to],
},
span,
)
}
fn push_expr(ast: &mut Ast, kind: ExprKind, span: Span) -> Result<ExprId, CompilerFailure> {
ast.try_push_expr(Expr { kind, span })
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))
}
fn push_void_zero(ast: &mut Ast, span: Span) -> Result<ExprId, CompilerFailure> {
let zero = push_expr(ast, ExprKind::Number(0.0), span)?;
push_expr(ast, ExprKind::Void { operand: zero }, span)
}
fn make_decl(
is_const: bool,
name: Ident,
value: ExprId,
doc: Option<crate::DocComment>,
) -> StmtKind {
if is_const {
StmtKind::Const {
name,
ty: None,
value,
doc,
}
} else {
StmtKind::Let {
name,
ty: None,
value,
doc,
}
}
}
fn let_names(ast: &Ast, stmts: &[StmtId]) -> Result<Vec<Ident>, CompilerFailure> {
let mut names = Vec::new();
for &stmt in stmts {
if let StmtKind::Let { name, .. } = &ast
.try_stmt(stmt)
.map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
.kind
{
names.push(name.clone());
}
}
Ok(names)
}
fn lowering_failure(message: &str) -> CompilerFailure {
CompilerFailure::Internal {
stage: CompilerStage::Infer,
span: None,
message: message.into(),
}
}
#[cfg(test)]
mod tests {
use super::lower;
use crate::{Asi, Binding, Stmt, StmtKind, Token, TokenKind, parse};
fn pipeline(source: &str) -> crate::Ast {
let mut asi = Asi::new(source, crate::FileId(0));
let mut tokens: Vec<Token> = Vec::new();
loop {
let t = asi.next_token();
let is_eof = matches!(t.kind, TokenKind::Eof);
tokens.push(t);
if is_eof {
break;
}
}
let _ = asi.into_diagnostics();
let (mut ast, diags) = parse(source, tokens, crate::FileId(0));
assert!(diags.is_empty(), "unexpected parse diags: {diags:?}");
ast = lower(ast).unwrap();
ast
}
fn no_patterns_left(ast: &crate::Ast) {
for i in 0..ast.stmts_len() {
let s = ast.try_stmt(crate::StmtId(i as u32)).unwrap();
assert!(
!matches!(
s.kind,
StmtKind::LetPattern { .. }
| StmtKind::ConstPattern { .. }
| StmtKind::ForOfPattern { .. }
),
"found unlowered pattern at stmt #{i}: {:?}",
s.kind,
);
if let StmtKind::Function { params, .. } = &s.kind {
for (pi, p) in params.iter().enumerate() {
assert!(
p.pattern.is_none(),
"function param #{pi} still has a pattern after lowering",
);
}
}
}
for i in 0..ast.exprs_len() {
let e = ast.try_expr(crate::ExprId(i as u32)).unwrap();
if let crate::ExprKind::Arrow { params, .. } = &e.kind {
for (pi, p) in params.iter().enumerate() {
assert!(
p.pattern.is_none(),
"arrow param #{pi} still has a pattern after lowering",
);
}
}
}
}
#[test]
fn lowers_simple_object_destructure() {
let ast = pipeline("const { a, b } = obj;");
no_patterns_left(&ast);
assert_eq!(ast.top_level.len(), 3);
let names: Vec<String> = ast
.top_level
.iter()
.map(|id| match &ast.try_stmt(*id).unwrap().kind {
StmtKind::Const { name, .. } => name.name.clone(),
other => panic!("expected Const, got {other:?}"),
})
.collect();
assert!(names[0].starts_with("#pattern_dst_"));
assert_eq!(names[1], "a");
assert_eq!(names[2], "b");
}
#[test]
fn lowers_renamed_object_destructure() {
let ast = pipeline("const { a: x, b: y } = obj;");
no_patterns_left(&ast);
let names: Vec<String> = ast
.top_level
.iter()
.map(|id| match &ast.try_stmt(*id).unwrap().kind {
StmtKind::Const { name, .. } => name.name.clone(),
other => panic!("expected Const, got {other:?}"),
})
.collect();
assert_eq!(names[1], "x");
assert_eq!(names[2], "y");
}
#[test]
fn lowers_array_destructure() {
let ast = pipeline("const [x, y] = arr;");
no_patterns_left(&ast);
assert_eq!(ast.top_level.len(), 3);
if let StmtKind::Const { value, .. } = &ast.try_stmt(ast.top_level[1]).unwrap().kind {
assert!(matches!(
ast.try_expr(*value).unwrap().kind,
crate::ExprKind::IndexAccess { .. }
));
}
}
#[test]
fn lowers_array_with_hole_and_rest() {
let ast = pipeline("const [, x, ...rest] = arr;");
no_patterns_left(&ast);
assert_eq!(ast.top_level.len(), 3);
if let StmtKind::Const { name, value, .. } = &ast.try_stmt(ast.top_level[1]).unwrap().kind {
assert_eq!(name.name, "x");
if let crate::ExprKind::IndexAccess { index, .. } = &ast.try_expr(*value).unwrap().kind
{
assert_eq!(
ast.try_expr(*index).unwrap().kind,
crate::ExprKind::Number(1.0)
);
} else {
panic!("expected IndexAccess");
}
}
if let StmtKind::Const { name, value, .. } = &ast.try_stmt(ast.top_level[2]).unwrap().kind {
assert_eq!(name.name, "rest");
match &ast.try_expr(*value).unwrap().kind {
crate::ExprKind::Call { callee, args, .. } => {
assert_eq!(args.len(), 2, "slice takes (start, end)");
assert_eq!(
ast.try_expr(args[0]).unwrap().kind,
crate::ExprKind::Number(2.0)
);
match &ast.try_expr(*callee).unwrap().kind {
crate::ExprKind::FieldAccess { name, .. } => {
assert_eq!(name.name, "slice");
}
other => panic!("expected slice FieldAccess, got {other:?}"),
}
}
other => panic!("expected Call, got {other:?}"),
}
}
}
#[test]
fn lowers_object_rest_to_object_rest() {
let ast = pipeline("const { a, ...rest } = obj;");
no_patterns_left(&ast);
assert_eq!(ast.top_level.len(), 3);
match &ast.try_stmt(ast.top_level[2]).unwrap().kind {
StmtKind::ObjectRest {
is_const,
name,
exclude,
..
} => {
assert!(*is_const);
assert_eq!(name.name, "rest");
assert_eq!(exclude.len(), 1);
assert_eq!(exclude[0].name, "a");
}
other => panic!("expected ObjectRest, got {other:?}"),
}
}
#[test]
fn lowers_function_param_pattern() {
let src = "function f({ a, b }: T): number { return a + b; }";
let ast = pipeline(src);
no_patterns_left(&ast);
let Stmt {
kind: StmtKind::Function { body, params, .. },
..
} = ast.try_stmt(ast.top_level[0]).unwrap().clone()
else {
panic!("expected Function");
};
assert_eq!(params.len(), 1);
assert!(params[0].pattern.is_none());
assert!(params[0].name.name.starts_with("#pattern_p_"));
let body_stmts = match &ast.try_stmt(body).unwrap().kind {
StmtKind::Block(s) => s.clone(),
_ => panic!("body is not a Block"),
};
assert_eq!(body_stmts.len(), 1);
let bindings = ast.parameter_bindings.get(¶ms[0].name.span).unwrap();
assert_eq!(bindings.len(), 2);
if let StmtKind::Let { name, .. } = &ast.try_stmt(bindings[0]).unwrap().kind {
assert_eq!(name.name, "a");
} else {
panic!("expected Let a");
}
if let StmtKind::Let { name, .. } = &ast.try_stmt(bindings[1]).unwrap().kind {
assert_eq!(name.name, "b");
} else {
panic!("expected Let b");
}
assert!(matches!(
ast.try_stmt(body_stmts[0]).unwrap().kind,
StmtKind::Return(_)
));
}
#[test]
fn parameter_object_rest_has_a_mutable_authored_binding() {
let ast = pipeline("function f({ head, ...tail }: T): number { return tail.value; }");
let StmtKind::Function { params, .. } = &ast.try_stmt(ast.top_level[0]).unwrap().kind
else {
panic!("expected function");
};
let bindings = ast.parameter_bindings.get(¶ms[0].name.span).unwrap();
assert_eq!(bindings.len(), 2);
let StmtKind::ObjectRest { is_const, name, .. } = &ast.try_stmt(bindings[1]).unwrap().kind
else {
panic!("expected object rest binding");
};
assert!(!*is_const);
assert_eq!(name.name, "tail");
}
#[test]
fn lowers_arrow_expr_body_with_pattern() {
let src = "const f = ({a, b}: T): number => a + b;";
let ast = pipeline(src);
no_patterns_left(&ast);
let mut found = false;
for i in 0..ast.exprs_len() {
let e = ast.try_expr(crate::ExprId(i as u32)).unwrap();
if let crate::ExprKind::Arrow { params, body, .. } = &e.kind {
assert!(matches!(body, crate::ArrowBody::Expr(_)));
assert_eq!(
ast.parameter_bindings
.get(¶ms[0].name.span)
.unwrap()
.len(),
2
);
found = true;
}
}
assert!(found, "no Arrow found in expr arena");
}
#[test]
fn lowers_for_of_array_pattern() {
let src = "function main(): void { for (const [a, b] of pairs) { use(a, b); } }";
let ast = pipeline(src);
no_patterns_left(&ast);
let Stmt {
kind: StmtKind::Function { body, .. },
..
} = ast.try_stmt(ast.top_level[0]).unwrap().clone()
else {
panic!("expected Function");
};
let body_stmts = match &ast.try_stmt(body).unwrap().kind {
StmtKind::Block(s) => s.clone(),
_ => panic!("function body is not a Block"),
};
assert_eq!(body_stmts.len(), 1, "main body has just the for-of");
let (loop_name, loop_body) = match &ast.try_stmt(body_stmts[0]).unwrap().kind {
StmtKind::ForOf { name, body, .. } => (name.name.clone(), *body),
other => panic!("expected ForOf, got {other:?}"),
};
assert!(
loop_name.starts_with("#pattern_dst_"),
"loop var was rewritten to a synth temp; got {loop_name}",
);
let loop_body_stmts = match &ast.try_stmt(loop_body).unwrap().kind {
StmtKind::Block(s) => s.clone(),
_ => panic!("for-of body is not a Block"),
};
assert_eq!(loop_body_stmts.len(), 3);
if let StmtKind::Const { name, value, .. } = &ast.try_stmt(loop_body_stmts[0]).unwrap().kind
{
assert_eq!(name.name, "a");
assert!(matches!(
ast.try_expr(*value).unwrap().kind,
crate::ExprKind::IndexAccess { .. }
));
assert!(ast.pattern_origins.contains_key(value));
} else {
panic!("expected Const a");
}
if let StmtKind::Const { name, .. } = &ast.try_stmt(loop_body_stmts[1]).unwrap().kind {
assert_eq!(name.name, "b");
} else {
panic!("expected Const b");
}
}
#[test]
fn lowers_let_pattern_preserves_mutability() {
let ast = pipeline("let { a } = obj;");
no_patterns_left(&ast);
assert!(matches!(
ast.try_stmt(ast.top_level[0]).unwrap().kind,
StmtKind::Const { .. }
));
assert!(matches!(
ast.try_stmt(ast.top_level[1]).unwrap().kind,
StmtKind::Let { .. }
));
}
#[test]
fn no_op_on_plain_program() {
let src = "const x = 1; function main(): void { }";
let before = {
let mut asi = Asi::new(src, crate::FileId(0));
let mut tokens: Vec<Token> = Vec::new();
loop {
let t = asi.next_token();
let is_eof = matches!(t.kind, TokenKind::Eof);
tokens.push(t);
if is_eof {
break;
}
}
let _ = asi.into_diagnostics();
let (ast, _) = parse(src, tokens, crate::FileId(0));
ast
};
let after = pipeline(src);
assert_eq!(format!("{before:?}"), format!("{after:?}"));
}
#[test]
fn lowers_pattern_in_nested_block() {
let src = "function main(): void { const { a } = obj; }";
let ast = pipeline(src);
no_patterns_left(&ast);
}
#[test]
fn idempotent() {
let src = "const { a, b } = obj;";
let mut ast1 = pipeline(src);
let snapshot = format!("{ast1:?}");
ast1 = lower(ast1).unwrap();
assert_eq!(snapshot, format!("{ast1:?}"));
}
#[test]
fn binding_span_preserved() {
let (ast, _) = parse(
"const { a } = obj;",
tokens_of("const { a } = obj;"),
crate::FileId(0),
);
match &ast.try_stmt(ast.top_level[0]).unwrap().kind {
crate::StmtKind::ConstPattern {
binding: Binding::Object { span, .. },
..
} => {
assert_eq!(*span, crate::Span::new(crate::FileId(0), 6, 11).unwrap());
}
other => panic!("expected ConstPattern Object, got {other:?}"),
}
}
#[test]
fn defaults_save_each_access_once_and_test_undefined() {
for source in [
"const { a = fallback() } = source();",
"const [a = fallback()] = source();",
] {
let ast = pipeline(source);
assert_eq!(ast.top_level.len(), 3);
let StmtKind::Const {
value: saved_access,
..
} = ast.try_stmt(ast.top_level[1]).unwrap().kind
else {
panic!("expected saved read")
};
assert!(matches!(
ast.try_expr(saved_access).unwrap().kind,
crate::ExprKind::FieldAccess { .. } | crate::ExprKind::Ternary { .. }
));
let StmtKind::Const { value, .. } = ast.try_stmt(ast.top_level[2]).unwrap().kind else {
panic!("expected binding")
};
let crate::ExprKind::Ternary { cond, then_, else_ } = ast.try_expr(value).unwrap().kind
else {
panic!("expected lazy default")
};
let crate::ExprKind::Binary {
op: crate::BinOp::Eq,
lhs,
rhs,
} = ast.try_expr(cond).unwrap().kind
else {
panic!("expected undefined comparison")
};
assert_eq!(lhs, else_);
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Void { .. }
));
assert!(matches!(
ast.try_expr(then_).unwrap().kind,
crate::ExprKind::Call { .. }
));
}
}
#[test]
fn array_defaults_keep_original_indices_and_origin_metadata() {
let ast = pipeline("const [, a = 1] = values;");
let StmtKind::Const { value, .. } = ast.try_stmt(ast.top_level[1]).unwrap().kind else {
panic!("expected saved index read")
};
let crate::ExprKind::Ternary {
cond,
then_: access,
else_,
} = ast.try_expr(value).unwrap().kind
else {
panic!("expected bounds guard")
};
assert!(matches!(
ast.try_expr(cond).unwrap().kind,
crate::ExprKind::Binary {
op: crate::BinOp::Lt,
..
}
));
assert!(matches!(
ast.try_expr(else_).unwrap().kind,
crate::ExprKind::Void { .. }
));
let crate::ExprKind::IndexAccess { index, .. } = ast.try_expr(access).unwrap().kind else {
panic!("expected guarded index")
};
assert!(matches!(
ast.try_expr(index).unwrap().kind,
crate::ExprKind::Number(1.0)
));
assert_eq!(ast.pattern_origins.get(&access).unwrap().slot_arity, 2);
}
#[test]
fn lowers_class_method_parameter_defaults() {
let ast = pipeline("class C { method({ x = 1 }: T): number { return x; } }");
let StmtKind::ClassDecl { ref members, .. } = ast.try_stmt(ast.top_level[0]).unwrap().kind
else {
panic!("expected class")
};
let crate::ClassMember::Method { params, body, .. } = &members[0] else {
panic!("expected method")
};
assert!(params[0].pattern.is_none());
let StmtKind::Block(ref statements) = ast.try_stmt(*body).unwrap().kind else {
panic!("expected block")
};
assert_eq!(statements.len(), 1);
assert_eq!(
ast.parameter_bindings
.get(¶ms[0].name.span)
.unwrap()
.len(),
2
);
}
fn tokens_of(source: &str) -> Vec<Token> {
let mut asi = Asi::new(source, crate::FileId(0));
let mut tokens = Vec::new();
loop {
let t = asi.next_token();
let is_eof = matches!(t.kind, TokenKind::Eof);
tokens.push(t);
if is_eof {
break;
}
}
let _ = asi.into_diagnostics();
tokens
}
}