Refactor: Implement block scoping and statement separation

This commit introduces fundamental changes to Myc's language structure
to enhance type safety and scoping clarity.

Key changes include:
- Introducing local scopes for `do` blocks.
- Separating statements (`def`, `assign`) from expressions, where
  statements now have no return value and are only allowed within
  blocks.
- A block now consists of multiple statements followed by a single final
  expression.
- Stricter validation rules are enforced, such as disallowing
  redefinition of local symbols in the same scope while allowing
  shadowing of outer symbols.
- Compiler errors are generated for statements used in expression
  contexts.

The implementation involved modifications to the AST, parser, binder
(scope-stack), and various compiler passes, along with VM runtime
optimizations.
This commit is contained in:
Michael Schimmel
2026-03-09 16:38:44 +01:00
parent 8339ee413e
commit beb693a068
26 changed files with 1057 additions and 948 deletions
+46 -43
View File
@@ -1,5 +1,5 @@
use crate::ast::compiler::bound_nodes::{
Address, AnalyzedNode, BoundKind, GlobalAnalyzedRegistry, GlobalIdx, UpvalueIdx,
Address, AnalyzedNode, BoundKind, GlobalAnalyzedRegistry, GlobalIdx, UpvalueIdx, UpvalueSource,
};
use crate::ast::nodes::Node;
use crate::ast::types::{Purity, Value};
@@ -65,8 +65,6 @@ impl Optimizer {
}
current = next;
}
// Unwrap the final Rc if we are at the end, or return a clone of the inner node.
// Since AnalyzedNode is small now (header + Rcs), cloning is cheap.
(*current).clone()
}
@@ -85,7 +83,6 @@ impl Optimizer {
if inliner.prepare_beta_reduction(params, arg_nodes, body, &mut inner_sub).is_some() {
let res = self.visit_node(Rc::new(body.clone()), &mut inner_sub, path);
// Sync back state to parent substitution map
sub.next_slot = inner_sub.next_slot;
sub.used.extend(inner_sub.used.iter().cloned());
sub.assigned.extend(inner_sub.assigned.iter().cloned());
@@ -111,19 +108,16 @@ impl Optimizer {
BoundKind::Get { addr, name } => {
let addr = *addr;
if !sub.assigned.contains(&addr) {
// 1. Try inlining from current value substitution map (locals/globals/upvalues)
if let Some(val) = sub.get_value(&addr)
&& inliner.is_inlinable_value(val, addr)
{
return Rc::new(folder.make_constant_node(val.clone(), node));
}
// 2. Try inlining from AST substitution map (pure expressions)
if let Some(inlined_node) = sub.ast_substitutions.get(&addr) {
return inlined_node.clone();
}
// 3. Fallback for Globals: check the actual VM environment
if let Address::Global(idx) = addr
&& let Some(globals_rc) = &self.globals
{
@@ -151,7 +145,6 @@ impl Optimizer {
BoundKind::GetField { rec, field } => {
let rec_opt = self.visit_node(rec.clone(), sub, path);
// Constant folding for Field Access
if let BoundKind::Constant(Value::Record(layout, values)) = &rec_opt.kind
&& let Some(idx) = layout.index_of(*field)
{
@@ -197,12 +190,13 @@ impl Optimizer {
addr,
kind,
value,
identity,
captured_by,
} => {
let value_opt = self.visit_node(value.clone(), sub, path);
if let Address::Local(slot) = addr
&& !captured_by.is_empty()
&& !captured_by.borrow().is_empty()
{
sub.captured_slots.insert(*slot);
}
@@ -232,6 +226,7 @@ impl Optimizer {
addr: sub.map_address(*addr),
kind: *kind,
value: value_opt,
identity: identity.clone(),
captured_by: captured_by.clone(),
},
node.ty.clone(),
@@ -243,7 +238,6 @@ impl Optimizer {
let args_opt = self.visit_node(args.clone(), sub, path);
if self.enabled {
// Optimized Field Access Transformation
if let BoundKind::FieldAccessor(k) = &callee_opt.kind
&& let BoundKind::Tuple { elements } = &args_opt.kind
&& elements.len() == 1
@@ -267,10 +261,9 @@ impl Optimizer {
params,
body,
upvalues,
positional_count,
..
} = &callee_opt.kind
&& upvalues.is_empty()
&& positional_count.is_some()
&& path.inlining_depth < 5
&& !callee_opt.ty.is_recursive
&& path.enter_lambda(&callee_opt.identity)
@@ -299,10 +292,9 @@ impl Optimizer {
params,
body,
upvalues,
positional_count,
..
} = &lambda_node.kind
&& upvalues.is_empty()
&& positional_count.is_some()
&& !lambda_node.ty.is_recursive
{
path.inlining_stack.insert(*idx);
@@ -453,31 +445,29 @@ impl Optimizer {
node.ty.clone(),
)
}
BoundKind::Block { exprs } => {
BoundKind::Block { statements, result } => {
let mut info = UsageInfo::default();
if !exprs.is_empty() {
for e in exprs {
info.collect(e);
}
for s in statements {
info.collect(s);
}
info.collect(result);
sub.assigned.extend(info.assigned.iter().cloned());
let mut new_exprs = Vec::with_capacity(exprs.len());
let last_idx = exprs.len().saturating_sub(1);
let mut new_statements = Vec::with_capacity(statements.len());
let mut block_changed = false;
for (i, e) in exprs.iter().enumerate() {
let is_last = i == last_idx;
if self.enabled && !is_last {
let removable = match &e.kind {
BoundKind::Define { addr, value, .. } => {
for s in statements {
if self.enabled {
let removable = match &s.kind {
BoundKind::Define { addr, value, captured_by, .. } => {
!info.is_used(addr)
&& (if let Address::Local(slot) = addr {
!sub.captured_slots.contains(slot)
} else {
true
})
&& captured_by.borrow().is_empty()
&& (value.ty.purity >= Purity::SideEffectFree
|| matches!(value.kind, BoundKind::Lambda { .. }))
}
@@ -490,7 +480,7 @@ impl Optimizer {
})
&& value.ty.purity >= Purity::SideEffectFree
}
_ => e.ty.purity >= Purity::SideEffectFree,
_ => s.ty.purity >= Purity::SideEffectFree,
};
if removable {
@@ -499,15 +489,20 @@ impl Optimizer {
}
}
let opt = self.visit_node(e.clone(), sub, path);
if self.enabled && matches!(opt.kind, BoundKind::Nop) && !is_last {
let opt = self.visit_node(s.clone(), sub, path);
if self.enabled && matches!(opt.kind, BoundKind::Nop) {
block_changed = true;
continue;
}
if !Rc::ptr_eq(&opt, e) {
if !Rc::ptr_eq(&opt, s) {
block_changed = true;
}
new_exprs.push(opt);
new_statements.push(opt);
}
let new_result = self.visit_node(result.clone(), sub, path);
if !Rc::ptr_eq(&new_result, result) {
block_changed = true;
}
if !block_changed {
@@ -515,14 +510,12 @@ impl Optimizer {
}
if self.enabled {
if new_exprs.is_empty() {
return Rc::new(folder.make_nop_node(node));
} else if new_exprs.len() == 1 {
return new_exprs.pop().unwrap();
if new_statements.is_empty() {
return new_result;
}
}
(BoundKind::Block { exprs: new_exprs }, node.ty.clone())
(BoundKind::Block { statements: new_statements, result: new_result }, node.ty.clone())
}
BoundKind::Lambda {
@@ -540,16 +533,21 @@ impl Optimizer {
next_inner_subs.assigned = info.assigned;
let mut upvalues_changed = false;
for (old_idx, capture_addr) in upvalues.iter().enumerate() {
for (old_idx, source) in upvalues.iter().enumerate() {
let capture_addr = match source {
UpvalueSource::Local(s) => Address::Local(*s),
UpvalueSource::Upvalue(i) => Address::Upvalue(*i),
};
let mut inlined_val = None;
if !sub.assigned.contains(capture_addr)
&& let Some(val) = sub.get_value(capture_addr)
if !sub.assigned.contains(&capture_addr)
&& let Some(val) = sub.get_value(&capture_addr)
{
inlined_val = Some(val.clone());
}
if let Address::Local(slot) = capture_addr {
sub.captured_slots.insert(*slot);
sub.captured_slots.insert(slot);
}
if let Some(val) = inlined_val {
@@ -559,11 +557,16 @@ impl Optimizer {
upvalues_changed = true;
} else {
mapping.push(Some(new_upvalues.len() as u32));
let mapped = sub.map_address(*capture_addr);
if mapped != *capture_addr {
let mapped_addr = sub.map_address(capture_addr);
let mapped_source = match mapped_addr {
Address::Local(s) => UpvalueSource::Local(s),
Address::Upvalue(i) => UpvalueSource::Upvalue(i),
Address::Global(_) => panic!("Cannot map upvalue to global")
};
if mapped_source != *source {
upvalues_changed = true;
}
new_upvalues.push(mapped);
new_upvalues.push(mapped_source);
}
}