fix: make all example programs work correctly

- Add string concatenation support to + operator in typechecker
- Register ADT constructors in both type environment and interpreter
- Bind handlers as values so they can be referenced in run...with
- Fix effect checking to use subset instead of exact match
- Add built-in effects (Console, Fail, State) to run block contexts
- Suppress dead code warnings in diagnostics, modules, parser

Update all example programs with:
- Expected output documented in comments
- Proper run...with statements to execute code

Add new example programs:
- behavioral.lux: pure, idempotent, deterministic, commutative functions
- pipelines.lux: pipe operator demonstrations
- statemachine.lux: ADT-based state machines
- tailcall.lux: tail call optimization examples
- traits.lux: type classes and pattern matching

Add documentation:
- docs/IMPLEMENTATION_PLAN.md: feature roadmap and status
- docs/PERFORMANCE_AND_TRADEOFFS.md: performance analysis

Add benchmarks for performance testing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 09:05:06 -05:00
parent 20bf75a5f8
commit 15a820a467
25 changed files with 1210 additions and 28 deletions

View File

@@ -1,5 +1,7 @@
//! Elm-style diagnostic messages for beautiful error reporting
#![allow(dead_code)]
use crate::ast::Span;
/// ANSI color codes for terminal output

View File

@@ -881,7 +881,21 @@ impl Interpreter {
Ok(value)
}
Declaration::Effect(_) | Declaration::Type(_) | Declaration::Trait(_) | Declaration::Impl(_) => {
Declaration::Type(type_decl) => {
// Register ADT constructors if this is an enum type
if let crate::ast::TypeDef::Enum(variants) = &type_decl.definition {
for variant in variants {
let constructor = Value::Constructor {
name: variant.name.name.clone(),
fields: Vec::new(),
};
self.global_env.define(&variant.name.name, constructor);
}
}
Ok(Value::Unit)
}
Declaration::Effect(_) | Declaration::Trait(_) | Declaration::Impl(_) => {
// These are compile-time only
Ok(Value::Unit)
}

View File

@@ -2,6 +2,8 @@
//!
//! Handles loading, parsing, and resolving module imports.
#![allow(dead_code)]
use crate::ast::{Declaration, ImportDecl, Program, Visibility};
use crate::parser::Parser;
use std::collections::{HashMap, HashSet};

View File

@@ -1,5 +1,7 @@
//! Parser for the Lux language
#![allow(dead_code)]
use crate::ast::*;
use crate::diagnostics::{Diagnostic, Severity};
use crate::lexer::{LexError, Lexer, Token, TokenKind};

View File

@@ -245,13 +245,47 @@ impl TypeChecker {
}
Declaration::Type(type_decl) => {
let type_def = self.type_def(type_decl);
self.env.types.insert(type_decl.name.name.clone(), type_def);
self.env.types.insert(type_decl.name.name.clone(), type_def.clone());
// Register ADT constructors as values in the type environment
if let ast::TypeDef::Enum(variants) = &type_decl.definition {
let type_name = Type::Named(type_decl.name.name.clone());
for variant in variants {
let constructor_type = match &variant.fields {
VariantFields::Unit => {
// Unit variant is just the type itself
type_name.clone()
}
VariantFields::Tuple(field_types) => {
// Tuple variant is a function from fields to the type
let param_types: Vec<Type> = field_types
.iter()
.map(|t| self.resolve_type(t))
.collect();
Type::function(param_types, type_name.clone())
}
VariantFields::Record(fields) => {
// Record variant is a function from record to the type
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|f| (f.name.name.clone(), self.resolve_type(&f.typ)))
.collect();
Type::function(vec![Type::Record(field_types)], type_name.clone())
}
};
self.env.bind(&variant.name.name, TypeScheme::mono(constructor_type));
}
}
}
Declaration::Handler(handler) => {
let handler_def = self.handler_def(handler);
self.env
.handlers
.insert(handler.name.name.clone(), handler_def);
// Also bind the handler as a value so it can be referenced in run...with expressions
// Handler type is the effect it handles (as an opaque type for now)
let handler_type = Type::Named(format!("Handler<{}>", handler.effect.name));
self.env.bind(&handler.name.name, TypeScheme::mono(handler_type));
}
Declaration::Let(let_decl) => {
// Will be typed in second pass
@@ -625,7 +659,30 @@ impl TypeChecker {
let right_type = self.infer_expr(right);
match op {
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
BinaryOp::Add => {
// Add supports both numeric types and string concatenation
if let Err(e) = unify(&left_type, &right_type) {
self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e),
span,
});
}
match &left_type {
Type::Int | Type::Float | Type::String | Type::Var(_) => left_type,
_ => {
self.errors.push(TypeError {
message: format!(
"Operator '{}' requires numeric or string operands, got {}",
op, left_type
),
span,
});
Type::Error
}
}
}
BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
// Arithmetic: both operands must be same numeric type
if let Err(e) = unify(&left_type, &right_type) {
self.errors.push(TypeError {
@@ -741,7 +798,13 @@ impl TypeChecker {
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
let result_type = Type::var();
let expected_fn = Type::function(arg_types.clone(), result_type.clone());
// Include current effects in the expected function type
// This allows calling functions that require effects when those effects are available
let expected_fn = Type::function_with_effects(
arg_types.clone(),
result_type.clone(),
self.current_effects.clone(),
);
match unify(&func_type, &expected_fn) {
Ok(subst) => result_type.apply(&subst),
@@ -1302,8 +1365,12 @@ impl TypeChecker {
let handled_effects: EffectSet =
EffectSet::from_iter(handlers.iter().map(|(e, _)| e.name.clone()));
// Extend current effects with handled ones
let combined = self.current_effects.union(&handled_effects);
// Built-in effects are always available in run blocks (they have runtime implementations)
let builtin_effects: EffectSet =
EffectSet::from_iter(["Console", "Fail", "State"].iter().map(|s| s.to_string()));
// Extend current effects with handled ones and built-in effects
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);
let old_effects = std::mem::replace(&mut self.current_effects, combined);
// Type check the expression

View File

@@ -1148,8 +1148,10 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
));
}
// For now, effects must match exactly
if e1 != e2 {
// Function's required effects (e1) must be a subset of available effects (e2)
// A pure function (empty effects) can be called anywhere
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
if !e1.is_subset(&e2) {
return Err(format!(
"Effect mismatch: expected {{{}}}, got {{{}}}",
e1, e2