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
+45 -44
View File
@@ -1,18 +1,11 @@
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::nodes::Node;
use crate::ast::types::{StaticType, Value};
use crate::ast::types::{Purity, StaticType, Value};
use crate::ast::vm::Closure;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
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)
/// and Phase 4 (Global Inlining).
pub struct Optimizer {
@@ -117,20 +110,20 @@ impl Optimizer {
let mut info = UsageInfo::default();
self.collect_usage(node, &mut info);
if let Some(idx) = global_idx {
if info.used_globals.contains(&idx) {
return true;
}
if let Some(idx) = global_idx
&& info.used_globals.contains(&idx)
{
return true;
}
if let Some(slot) = local_slot {
if info.used_locals.contains(&slot) {
return true;
}
if let Some(slot) = local_slot
&& info.used_locals.contains(&slot)
{
return true;
}
if let Some(id) = identity {
if info.used_identities.contains(id) {
return true;
}
if let Some(id) = identity
&& info.used_identities.contains(id)
{
return true;
}
false
}
@@ -172,14 +165,14 @@ impl Optimizer {
}
Address::Global(idx) => {
if !sub.assigned_globals.contains(&idx) {
if let Some(val) = sub.globals.get(&idx) {
if self.is_inlinable_value(val, Some(idx)) {
return Node {
identity: node.identity,
ty: val.static_type(),
kind: BoundKind::Constant(val.clone()),
};
}
if let Some(val) = sub.globals.get(&idx)
&& self.is_inlinable_value(val, Some(idx))
{
return Node {
identity: node.identity,
ty: val.static_type(),
kind: BoundKind::Constant(val.clone()),
};
}
if let Some(globals_rc) = &self.globals {
let globals = globals_rc.borrow();
@@ -306,7 +299,12 @@ impl Optimizer {
&& positional_count.is_some()
{
// 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();
path.inlining_stack.insert(*idx);
path.inlining_depth += 1;
@@ -681,10 +679,10 @@ impl Optimizer {
value,
} => {
let value = Box::new(self.visit_node(*value, sub, path));
if let BoundKind::Constant(val) = &value.kind {
if self.is_inlinable_value(val, Some(global_index)) {
sub.add_global(global_index, val.clone());
}
if let BoundKind::Constant(val) = &value.kind
&& self.is_inlinable_value(val, Some(global_index))
{
sub.add_global(global_index, val.clone());
}
let p = self.purity_of(&value);
if p > Purity::Impure
@@ -712,12 +710,7 @@ impl Optimizer {
BoundKind::Record { fields } => {
let fields = fields
.into_iter()
.map(|(k, v)| {
(
self.visit_node(k, sub, path),
self.visit_node(v, sub, path),
)
})
.map(|(k, v)| (self.visit_node(k, sub, path), self.visit_node(v, sub, path)))
.collect();
(BoundKind::Record { fields }, node.ty)
}
@@ -846,7 +839,7 @@ impl Optimizer {
Purity::Impure
}
}
BoundKind::Constant(Value::Function(_)) => Purity::Pure,
BoundKind::Constant(Value::Function(f)) => f.purity,
BoundKind::Constant(Value::Object(obj)) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
self.purity_of(&closure.function_node)
@@ -894,7 +887,7 @@ impl Optimizer {
_ => return None,
};
let result = match func_val {
Value::Function(f) => f(arg_values),
Value::Function(f) => (f.func)(arg_values),
_ => return None,
};
Some(Node {
@@ -1351,10 +1344,18 @@ mod tests {
// Hier sollte der Optimizer 'f' nicht unendlich oft in sich selbst inlinen.
let source = "(fn [] (do (def f (fn [x] (f x))) (f 1)))";
let dump = get_optimized_dump(source, true);
// Der Dump sollte 'Call' auf 'f' (oder Slot-Get) noch enthalten,
// statt 5 Ebenen tief entfaltet zu sein.
assert!(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);
assert!(
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::nodes::{Node, Symbol, UntypedKind};
use crate::ast::parser::Parser;
use crate::ast::types::{Object, StaticType, Value};
use crate::ast::vm::{TracingObserver, VM};
use std::cell::RefCell;
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::lambda_collector::LambdaCollector;
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::tco::{ExecNode, TCO};
use crate::ast::rtl;
use crate::ast::rtl::intrinsics;
use crate::ast::types::{Object, Purity, StaticType, Value};
pub struct Environment {
pub global_names: Rc<RefCell<HashMap<Symbol, u32>>>,
@@ -130,7 +130,7 @@ impl Environment {
names.insert(Symbol::from(name), idx);
types.insert(idx, ty);
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) {
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::ast::compiler::optimizer::Purity;
use crate::ast::environment::Environment;
use crate::ast::types::{Signature, StaticType, Value};
use crate::ast::types::{Purity, Signature, StaticType, Value};
use std::rc::Rc;
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::types::{Signature, StaticType, Value};
use crate::ast::types::{Purity, Signature, StaticType, Value};
use chrono::{NaiveDate, NaiveDateTime};
pub fn register(env: &Environment) {
+19 -20
View File
@@ -1,5 +1,4 @@
use crate::ast::types::{StaticType, Value};
use std::rc::Rc;
use crate::ast::types::{Purity, StaticType, Value};
/// Looks up a specialized intrinsic function for the given operator and argument types.
/// 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) {
// --- Integer Arithmetic ---
("+", [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]) {
Value::Int(a + b)
} else {
Value::Int(0) // Should not happen if type checker works
}
})),
}),
StaticType::Int,
)),
("-", [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]) {
Value::Int(a - b)
} else {
Value::Int(0)
}
})),
}),
StaticType::Int,
)),
("-", [StaticType::Int, StaticType::Int, StaticType::Int]) => Some((
// Variadic optimization for 3 args (common in some Lisp dialects, though - usually is binary/unary)
// MyC's core.rs supports variadic subtraction.
Value::Function(Rc::new(|args| {
Value::make_function(Purity::Pure, |args| {
let a = match args[0] {
Value::Int(i) => i,
_ => 0,
@@ -43,69 +42,69 @@ pub fn lookup(name: &str, args: &[StaticType]) -> Option<(Value, StaticType)> {
_ => 0,
};
Value::Int(a - b - c)
})),
}),
StaticType::Int,
)),
("*", [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]) {
Value::Int(a * b)
} else {
Value::Int(0)
}
})),
}),
StaticType::Int,
)),
// --- Integer Comparison ---
("<=", [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]) {
Value::Bool(a <= b)
} else {
Value::Bool(false)
}
})),
}),
StaticType::Bool,
)),
("<", [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]) {
Value::Bool(a < b)
} else {
Value::Bool(false)
}
})),
}),
StaticType::Bool,
)),
(">", [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]) {
Value::Bool(a > b)
} else {
Value::Bool(false)
}
})),
}),
StaticType::Bool,
)),
(">=", [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]) {
Value::Bool(a >= b)
} else {
Value::Bool(false)
}
})),
}),
StaticType::Bool,
)),
("=", [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]) {
Value::Bool(a == b)
} else {
Value::Bool(false)
}
})),
}),
StaticType::Bool,
)),
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::ast::compiler::optimizer::Purity;
use crate::ast::environment::Environment;
use crate::ast::types::{Signature, StaticType, Value};
use crate::ast::types::{Purity, Signature, StaticType, Value};
use std::f64::consts;
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::collections::HashMap;
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,
{
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
}
@@ -217,7 +218,7 @@ mod tests {
let greet_fn = &r.values[idx];
if let Value::Function(f) = greet_fn {
let res = f(vec![]);
let res = (f.func)(vec![]);
if let Value::Text(s) = res {
assert_eq!(&*s, "Hello Alice");
} else {
@@ -249,7 +250,7 @@ mod tests {
});
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 {
let idx = r
.keys
@@ -259,7 +260,7 @@ mod tests {
let greet_fn = &r.values[idx];
if let Value::Function(gf) = greet_fn {
let res = gf(vec![]);
let res = (gf.func)(vec![]);
if let Value::Text(s) = res {
assert_eq!(&*s, "Hello Bob");
} else {
+32 -2
View File
@@ -76,6 +76,29 @@ pub struct RecordData {
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)
#[derive(Clone)]
pub enum Value {
@@ -88,7 +111,7 @@ pub enum Value {
Keyword(Keyword),
Tuple(ValueList),
Record(Rc<RecordData>),
Function(Rc<dyn Fn(Vec<Value>) -> Value>),
Function(Rc<NativeFunction>),
Object(Rc<dyn Object>), // For compiled Closures and other opaque types
Cell(Rc<RefCell<Value>>), // Boxed value for captures
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 {
match self {
Value::Void => StaticType::Void,
@@ -391,7 +421,7 @@ impl fmt::Display for Value {
}
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::Cell(c) => write!(f, "{}", c.borrow()),
Value::TailCallRequest(_) => write!(f, "<tail call request>"),
+2 -2
View File
@@ -194,14 +194,14 @@ macro_rules! dispatch_eval {
if $node.ty.is_tail {
match func_val {
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)),
}
}
loop {
match func_val {
Value::Function(f) => break Ok(f(arg_vals)),
Value::Function(f) => break Ok((f.func)(arg_vals)),
Value::Object(obj) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
let old_stack_top = $self.stack.len();