Refactor Purity enum and NativeFunction struct

Move `Purity` enum definition from `optimizer.rs` to `types.rs` and
create a `NativeFunction` struct to hold the function and its purity.
Update `Value::Function` to store `Rc<NativeFunction>` and
`Value::make_function`
helper to simplify creation.

This change allows tracking the purity of native functions, which is
useful
for optimization and static analysis.
This commit is contained in:
Michael Schimmel
2026-02-22 10:50:37 +01:00
parent b54a449369
commit a726b79d8a
9 changed files with 111 additions and 83 deletions
+35 -34
View File
@@ -1,18 +1,11 @@
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode}; use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::nodes::Node; use crate::ast::nodes::Node;
use crate::ast::types::{StaticType, Value}; use crate::ast::types::{Purity, StaticType, Value};
use crate::ast::vm::Closure; use crate::ast::vm::Closure;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::rc::Rc; use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Purity {
Impure, // Has side effects (e.g. Set, Print)
SideEffectFree, // No side effects, but not deterministic (e.g. now(), random())
Pure, // No side effects and deterministic (e.g. sin(x), 1 + 2)
}
/// The Optimizer performs Phase 2 (Cracking), Phase 2.5/2.6 (Aggressive Collapsing & DCE) /// The Optimizer performs Phase 2 (Cracking), Phase 2.5/2.6 (Aggressive Collapsing & DCE)
/// and Phase 4 (Global Inlining). /// and Phase 4 (Global Inlining).
pub struct Optimizer { pub struct Optimizer {
@@ -117,21 +110,21 @@ impl Optimizer {
let mut info = UsageInfo::default(); let mut info = UsageInfo::default();
self.collect_usage(node, &mut info); self.collect_usage(node, &mut info);
if let Some(idx) = global_idx { if let Some(idx) = global_idx
if info.used_globals.contains(&idx) { && info.used_globals.contains(&idx)
{
return true; return true;
} }
} if let Some(slot) = local_slot
if let Some(slot) = local_slot { && info.used_locals.contains(&slot)
if info.used_locals.contains(&slot) { {
return true; return true;
} }
} if let Some(id) = identity
if let Some(id) = identity { && info.used_identities.contains(id)
if info.used_identities.contains(id) { {
return true; return true;
} }
}
false false
} }
@@ -172,15 +165,15 @@ impl Optimizer {
} }
Address::Global(idx) => { Address::Global(idx) => {
if !sub.assigned_globals.contains(&idx) { if !sub.assigned_globals.contains(&idx) {
if let Some(val) = sub.globals.get(&idx) { if let Some(val) = sub.globals.get(&idx)
if self.is_inlinable_value(val, Some(idx)) { && self.is_inlinable_value(val, Some(idx))
{
return Node { return Node {
identity: node.identity, identity: node.identity,
ty: val.static_type(), ty: val.static_type(),
kind: BoundKind::Constant(val.clone()), kind: BoundKind::Constant(val.clone()),
}; };
} }
}
if let Some(globals_rc) = &self.globals { if let Some(globals_rc) = &self.globals {
let globals = globals_rc.borrow(); let globals = globals_rc.borrow();
if let Some(val) = globals.get(idx as usize) if let Some(val) = globals.get(idx as usize)
@@ -306,7 +299,12 @@ impl Optimizer {
&& positional_count.is_some() && positional_count.is_some()
{ {
// STRICT RECURSION CHECK: Don't inline if recursive (idx or identity) // STRICT RECURSION CHECK: Don't inline if recursive (idx or identity)
if !self.is_recursive(body, Some(*idx), None, Some(&lambda_node.identity)) { if !self.is_recursive(
body,
Some(*idx),
None,
Some(&lambda_node.identity),
) {
let mut inner_sub = SubstitutionMap::new(); let mut inner_sub = SubstitutionMap::new();
path.inlining_stack.insert(*idx); path.inlining_stack.insert(*idx);
path.inlining_depth += 1; path.inlining_depth += 1;
@@ -681,11 +679,11 @@ impl Optimizer {
value, value,
} => { } => {
let value = Box::new(self.visit_node(*value, sub, path)); let value = Box::new(self.visit_node(*value, sub, path));
if let BoundKind::Constant(val) = &value.kind { if let BoundKind::Constant(val) = &value.kind
if self.is_inlinable_value(val, Some(global_index)) { && self.is_inlinable_value(val, Some(global_index))
{
sub.add_global(global_index, val.clone()); sub.add_global(global_index, val.clone());
} }
}
let p = self.purity_of(&value); let p = self.purity_of(&value);
if p > Purity::Impure if p > Purity::Impure
&& let Some(purity_rc) = &self.global_purity && let Some(purity_rc) = &self.global_purity
@@ -712,12 +710,7 @@ impl Optimizer {
BoundKind::Record { fields } => { BoundKind::Record { fields } => {
let fields = fields let fields = fields
.into_iter() .into_iter()
.map(|(k, v)| { .map(|(k, v)| (self.visit_node(k, sub, path), self.visit_node(v, sub, path)))
(
self.visit_node(k, sub, path),
self.visit_node(v, sub, path),
)
})
.collect(); .collect();
(BoundKind::Record { fields }, node.ty) (BoundKind::Record { fields }, node.ty)
} }
@@ -846,7 +839,7 @@ impl Optimizer {
Purity::Impure Purity::Impure
} }
} }
BoundKind::Constant(Value::Function(_)) => Purity::Pure, BoundKind::Constant(Value::Function(f)) => f.purity,
BoundKind::Constant(Value::Object(obj)) => { BoundKind::Constant(Value::Object(obj)) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() { if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
self.purity_of(&closure.function_node) self.purity_of(&closure.function_node)
@@ -894,7 +887,7 @@ impl Optimizer {
_ => return None, _ => return None,
}; };
let result = match func_val { let result = match func_val {
Value::Function(f) => f(arg_values), Value::Function(f) => (f.func)(arg_values),
_ => return None, _ => return None,
}; };
Some(Node { Some(Node {
@@ -1354,7 +1347,15 @@ mod tests {
// Der Dump sollte 'Call' auf 'f' (oder Slot-Get) noch enthalten, // Der Dump sollte 'Call' auf 'f' (oder Slot-Get) noch enthalten,
// statt 5 Ebenen tief entfaltet zu sein. // statt 5 Ebenen tief entfaltet zu sein.
assert!(dump.contains("Call"), "Recursive call should remain as a call. Dump: \n{}", dump); assert!(
assert!(!dump.contains("- Capturer:"), "Should not be over-optimized. Dump: \n{}", dump); dump.contains("Call"),
"Recursive call should remain as a call. Dump: \n{}",
dump
);
assert!(
!dump.contains("- Capturer:"),
"Should not be over-optimized. Dump: \n{}",
dump
);
} }
} }
+3 -3
View File
@@ -2,7 +2,6 @@ use crate::ast::compiler::binder::Binder;
use crate::ast::compiler::{TypeChecker, TypedNode}; use crate::ast::compiler::{TypeChecker, TypedNode};
use crate::ast::nodes::{Node, Symbol, UntypedKind}; use crate::ast::nodes::{Node, Symbol, UntypedKind};
use crate::ast::parser::Parser; use crate::ast::parser::Parser;
use crate::ast::types::{Object, StaticType, Value};
use crate::ast::vm::{TracingObserver, VM}; use crate::ast::vm::{TracingObserver, VM};
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
@@ -12,11 +11,12 @@ use crate::ast::compiler::bound_nodes::{Address, BoundNode};
use crate::ast::compiler::dumper::Dumper; use crate::ast::compiler::dumper::Dumper;
use crate::ast::compiler::lambda_collector::LambdaCollector; use crate::ast::compiler::lambda_collector::LambdaCollector;
use crate::ast::compiler::macros::{MacroEvaluator, MacroExpander, MacroRegistry}; use crate::ast::compiler::macros::{MacroEvaluator, MacroExpander, MacroRegistry};
use crate::ast::compiler::optimizer::{Optimizer, Purity}; use crate::ast::compiler::optimizer::Optimizer;
use crate::ast::compiler::specializer::{FunctionRegistry, MonoCache, Specializer}; use crate::ast::compiler::specializer::{FunctionRegistry, MonoCache, Specializer};
use crate::ast::compiler::tco::{ExecNode, TCO}; use crate::ast::compiler::tco::{ExecNode, TCO};
use crate::ast::rtl; use crate::ast::rtl;
use crate::ast::rtl::intrinsics; use crate::ast::rtl::intrinsics;
use crate::ast::types::{Object, Purity, StaticType, Value};
pub struct Environment { pub struct Environment {
pub global_names: Rc<RefCell<HashMap<Symbol, u32>>>, pub global_names: Rc<RefCell<HashMap<Symbol, u32>>>,
@@ -130,7 +130,7 @@ impl Environment {
names.insert(Symbol::from(name), idx); names.insert(Symbol::from(name), idx);
types.insert(idx, ty); types.insert(idx, ty);
purity.insert(idx, purity_level); purity.insert(idx, purity_level);
values.push(Value::Function(Rc::new(func))); values.push(Value::make_function(purity_level, func));
} }
pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) { pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) {
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::ast::compiler::optimizer::Purity;
use crate::ast::environment::Environment; use crate::ast::environment::Environment;
use crate::ast::types::{Signature, StaticType, Value}; use crate::ast::types::{Purity, Signature, StaticType, Value};
use std::rc::Rc; use std::rc::Rc;
pub fn register(env: &Environment) { pub fn register(env: &Environment) {
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::ast::compiler::optimizer::Purity;
use crate::ast::environment::Environment; use crate::ast::environment::Environment;
use crate::ast::types::{Signature, StaticType, Value}; use crate::ast::types::{Purity, Signature, StaticType, Value};
use chrono::{NaiveDate, NaiveDateTime}; use chrono::{NaiveDate, NaiveDateTime};
pub fn register(env: &Environment) { pub fn register(env: &Environment) {
+19 -20
View File
@@ -1,5 +1,4 @@
use crate::ast::types::{StaticType, Value}; use crate::ast::types::{Purity, StaticType, Value};
use std::rc::Rc;
/// Looks up a specialized intrinsic function for the given operator and argument types. /// Looks up a specialized intrinsic function for the given operator and argument types.
/// Returns (Executable Value, Return Type) if a fast-path exists. /// Returns (Executable Value, Return Type) if a fast-path exists.
@@ -7,29 +6,29 @@ pub fn lookup(name: &str, args: &[StaticType]) -> Option<(Value, StaticType)> {
match (name, args) { match (name, args) {
// --- Integer Arithmetic --- // --- Integer Arithmetic ---
("+", [StaticType::Int, StaticType::Int]) => Some(( ("+", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Int(a + b) Value::Int(a + b)
} else { } else {
Value::Int(0) // Should not happen if type checker works Value::Int(0) // Should not happen if type checker works
} }
})), }),
StaticType::Int, StaticType::Int,
)), )),
("-", [StaticType::Int, StaticType::Int]) => Some(( ("-", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Int(a - b) Value::Int(a - b)
} else { } else {
Value::Int(0) Value::Int(0)
} }
})), }),
StaticType::Int, StaticType::Int,
)), )),
("-", [StaticType::Int, StaticType::Int, StaticType::Int]) => Some(( ("-", [StaticType::Int, StaticType::Int, StaticType::Int]) => Some((
// Variadic optimization for 3 args (common in some Lisp dialects, though - usually is binary/unary) // Variadic optimization for 3 args (common in some Lisp dialects, though - usually is binary/unary)
// MyC's core.rs supports variadic subtraction. // MyC's core.rs supports variadic subtraction.
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
let a = match args[0] { let a = match args[0] {
Value::Int(i) => i, Value::Int(i) => i,
_ => 0, _ => 0,
@@ -43,69 +42,69 @@ pub fn lookup(name: &str, args: &[StaticType]) -> Option<(Value, StaticType)> {
_ => 0, _ => 0,
}; };
Value::Int(a - b - c) Value::Int(a - b - c)
})), }),
StaticType::Int, StaticType::Int,
)), )),
("*", [StaticType::Int, StaticType::Int]) => Some(( ("*", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Int(a * b) Value::Int(a * b)
} else { } else {
Value::Int(0) Value::Int(0)
} }
})), }),
StaticType::Int, StaticType::Int,
)), )),
// --- Integer Comparison --- // --- Integer Comparison ---
("<=", [StaticType::Int, StaticType::Int]) => Some(( ("<=", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Bool(a <= b) Value::Bool(a <= b)
} else { } else {
Value::Bool(false) Value::Bool(false)
} }
})), }),
StaticType::Bool, StaticType::Bool,
)), )),
("<", [StaticType::Int, StaticType::Int]) => Some(( ("<", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Bool(a < b) Value::Bool(a < b)
} else { } else {
Value::Bool(false) Value::Bool(false)
} }
})), }),
StaticType::Bool, StaticType::Bool,
)), )),
(">", [StaticType::Int, StaticType::Int]) => Some(( (">", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Bool(a > b) Value::Bool(a > b)
} else { } else {
Value::Bool(false) Value::Bool(false)
} }
})), }),
StaticType::Bool, StaticType::Bool,
)), )),
(">=", [StaticType::Int, StaticType::Int]) => Some(( (">=", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Bool(a >= b) Value::Bool(a >= b)
} else { } else {
Value::Bool(false) Value::Bool(false)
} }
})), }),
StaticType::Bool, StaticType::Bool,
)), )),
("=", [StaticType::Int, StaticType::Int]) => Some(( ("=", [StaticType::Int, StaticType::Int]) => Some((
Value::Function(Rc::new(|args| { Value::make_function(Purity::Pure, |args| {
if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) { if let (Value::Int(a), Value::Int(b)) = (&args[0], &args[1]) {
Value::Bool(a == b) Value::Bool(a == b)
} else { } else {
Value::Bool(false) Value::Bool(false)
} }
})), }),
StaticType::Bool, StaticType::Bool,
)), )),
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::ast::compiler::optimizer::Purity;
use crate::ast::environment::Environment; use crate::ast::environment::Environment;
use crate::ast::types::{Signature, StaticType, Value}; use crate::ast::types::{Purity, Signature, StaticType, Value};
use std::f64::consts; use std::f64::consts;
pub fn register(env: &Environment) { pub fn register(env: &Environment) {
+7 -6
View File
@@ -1,4 +1,4 @@
use crate::ast::types::{Keyword, Signature, StaticType, Value}; use crate::ast::types::{Keyword, Purity, Signature, StaticType, Value};
use std::any::TypeId; use std::any::TypeId;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
@@ -68,7 +68,7 @@ impl TypeRegistry {
} }
} }
}; };
Value::Function(Rc::new(closure)) Value::make_function(Purity::Impure, closure)
} }
} }
@@ -94,7 +94,8 @@ impl RecordBuilder {
F: Fn(Vec<Value>) -> Value + 'static, F: Fn(Vec<Value>) -> Value + 'static,
{ {
let key = Keyword::intern(name); let key = Keyword::intern(name);
self.fields.push((key, Value::Function(Rc::new(func)))); self.fields
.push((key, Value::make_function(Purity::Impure, func)));
self self
} }
@@ -217,7 +218,7 @@ mod tests {
let greet_fn = &r.values[idx]; let greet_fn = &r.values[idx];
if let Value::Function(f) = greet_fn { if let Value::Function(f) = greet_fn {
let res = f(vec![]); let res = (f.func)(vec![]);
if let Value::Text(s) = res { if let Value::Text(s) = res {
assert_eq!(&*s, "Hello Alice"); assert_eq!(&*s, "Hello Alice");
} else { } else {
@@ -249,7 +250,7 @@ mod tests {
}); });
if let Value::Function(f) = factory_val { if let Value::Function(f) = factory_val {
let instance = f(vec![Value::Text("Bob".into()), Value::Int(40)]); let instance = (f.func)(vec![Value::Text("Bob".into()), Value::Int(40)]);
if let Value::Record(r) = instance { if let Value::Record(r) = instance {
let idx = r let idx = r
.keys .keys
@@ -259,7 +260,7 @@ mod tests {
let greet_fn = &r.values[idx]; let greet_fn = &r.values[idx];
if let Value::Function(gf) = greet_fn { if let Value::Function(gf) = greet_fn {
let res = gf(vec![]); let res = (gf.func)(vec![]);
if let Value::Text(s) = res { if let Value::Text(s) = res {
assert_eq!(&*s, "Hello Bob"); assert_eq!(&*s, "Hello Bob");
} else { } else {
+32 -2
View File
@@ -76,6 +76,29 @@ pub struct RecordData {
pub values: ValueList, pub values: ValueList,
} }
/// Purity level of an expression or function.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Purity {
/// Has side effects (e.g. Set, Print)
Impure,
/// No side effects, but not deterministic (e.g. now(), random())
SideEffectFree,
/// No side effects and deterministic (e.g. sin(x), 1 + 2)
Pure,
}
/// A native host function with metadata.
pub struct NativeFunction {
pub func: Rc<dyn Fn(Vec<Value>) -> Value>,
pub purity: Purity,
}
impl fmt::Debug for NativeFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<native fn, {:?}>", self.purity)
}
}
/// Core data value in Myc Script (similar to TDataValue) /// Core data value in Myc Script (similar to TDataValue)
#[derive(Clone)] #[derive(Clone)]
pub enum Value { pub enum Value {
@@ -88,7 +111,7 @@ pub enum Value {
Keyword(Keyword), Keyword(Keyword),
Tuple(ValueList), Tuple(ValueList),
Record(Rc<RecordData>), Record(Rc<RecordData>),
Function(Rc<dyn Fn(Vec<Value>) -> Value>), Function(Rc<NativeFunction>),
Object(Rc<dyn Object>), // For compiled Closures and other opaque types Object(Rc<dyn Object>), // For compiled Closures and other opaque types
Cell(Rc<RefCell<Value>>), // Boxed value for captures Cell(Rc<RefCell<Value>>), // Boxed value for captures
TailCallRequest(Box<(Rc<dyn Object>, Vec<Value>)>), // Internal: For TCO (Boxed to keep Value small) TailCallRequest(Box<(Rc<dyn Object>, Vec<Value>)>), // Internal: For TCO (Boxed to keep Value small)
@@ -304,6 +327,13 @@ impl Value {
})) }))
} }
pub fn make_function(purity: Purity, func: impl Fn(Vec<Value>) -> Value + 'static) -> Self {
Value::Function(Rc::new(NativeFunction {
func: Rc::new(func),
purity,
}))
}
pub fn static_type(&self) -> StaticType { pub fn static_type(&self) -> StaticType {
match self { match self {
Value::Void => StaticType::Void, Value::Void => StaticType::Void,
@@ -391,7 +421,7 @@ impl fmt::Display for Value {
} }
write!(f, "}}") write!(f, "}}")
} }
Value::Function(_) => write!(f, "<native fn>"), Value::Function(f_meta) => write!(f, "<native fn, {:?}>", f_meta.purity),
Value::Object(o) => write!(f, "<{}>", o.type_name()), Value::Object(o) => write!(f, "<{}>", o.type_name()),
Value::Cell(c) => write!(f, "{}", c.borrow()), Value::Cell(c) => write!(f, "{}", c.borrow()),
Value::TailCallRequest(_) => write!(f, "<tail call request>"), Value::TailCallRequest(_) => write!(f, "<tail call request>"),
+2 -2
View File
@@ -194,14 +194,14 @@ macro_rules! dispatch_eval {
if $node.ty.is_tail { if $node.ty.is_tail {
match func_val { match func_val {
Value::Object(obj) => return Ok(Value::TailCallRequest(Box::new((obj, arg_vals)))), Value::Object(obj) => return Ok(Value::TailCallRequest(Box::new((obj, arg_vals)))),
Value::Function(f) => return Ok(f(arg_vals)), Value::Function(f) => return Ok((f.func)(arg_vals)),
_ => return Err(format!("Tail call target is not a function: {}", func_val)), _ => return Err(format!("Tail call target is not a function: {}", func_val)),
} }
} }
loop { loop {
match func_val { match func_val {
Value::Function(f) => break Ok(f(arg_vals)), Value::Function(f) => break Ok((f.func)(arg_vals)),
Value::Object(obj) => { Value::Object(obj) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() { if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
let old_stack_top = $self.stack.len(); let old_stack_top = $self.stack.len();