Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .build_meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"year": 26,
"month": 2,
"build": 5
"month": 1,
"build": 56
}
8 changes: 0 additions & 8 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,3 @@
## 2026-01-24 - [Avoid Async Box Allocation for Simple Expressions]
**Learning:** `evaluate_expression` was wrapping every call in `Box::pin` for async recursion, even for simple arithmetic operations like `1 + 2`. This caused significant overhead in tight loops.
**Action:** Implemented `try_evaluate_expression_sync` to recursively evaluate simple expressions (Literals, Variables, Binary/Unary ops) synchronously, bypassing `Box::pin` allocation. This yielded a ~30% performance improvement in arithmetic-heavy loops.

## 2026-02-05 - [Batch Interpreter Timeout Checks]
**Learning:** Checking `Instant::elapsed()` on every instruction creates significant overhead (15-20%) in tight loops due to syscalls/hardware clock reads.
**Action:** Implemented a batched check using a simple instruction counter (`op_count & 1023 == 0`), only checking the system clock every 1024 operations. This maintains safety (timeouts are still enforced, just with slightly coarser granularity) while significantly reducing per-instruction overhead.

## 2026-02-18 - [Unified and Optimized Value Equality]
**Learning:** Three different equality implementations existed (`Value::eq`, `Interpreter::is_equal`, `values_equal`), leading to inconsistent behavior (e.g., `[1] == [1]` was false in WFL code but true in Rust `PartialEq`). Additionally, `Value::eq` unconditionally allocated a `HashSet` for cycle detection, penalizing simple primitive comparisons.
**Action:** Optimized `Value::eq` with a fast path for primitives (avoiding allocation) and updated all call sites to use it. This unified equality logic, fixed correctness bugs for containers, and improved performance for primitives.
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "wfl"
version = "26.2.5"
version = "26.1.56"
edition = "2024"
description = "WFL (WebFirst Language) is a programming language designed to be readable and intuitive using natural language constructs."
license = "Apache-2.0"
Expand All @@ -11,7 +11,7 @@ default-run = "wfl"
name = "WFL"
identifier = "com.logbie.wfl"
icon = ["icons/wfl.png"]
version = "26.2.5"
version = "26.1.56"
copyright = "© 2025 Logbie LLC"
category = "Developer Tool"
short_description = "WebFirst Language Compiler and Runtime"
Expand Down Expand Up @@ -50,7 +50,7 @@ sqlx = { version = "0.8.1", features = ["runtime-tokio-rustls", "sqlite", "mysql
serde_json = "1.0.114"
warp = "0.3.7"
uuid = { version = "1.6.1", features = ["v4"] }
bytes = "1.11.1"
bytes = "1.5.0"
codespan-reporting = "0.11.1"
simplelog = "0.12.1"
chrono = "0.4.31"
Expand Down
2 changes: 1 addition & 1 deletion src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ impl Analyzer {
let server_symbol = Symbol {
name: server_name.clone(),
kind: SymbolKind::Variable { mutable: false },
symbol_type: Some(Type::Text), // Server is represented as text
symbol_type: Some(Type::Custom("Server".to_string())), // Server is represented as a custom Server type
line: *line,
column: *column,
};
Expand Down
20 changes: 19 additions & 1 deletion src/interpreter/assertion_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,25 @@ impl Interpreter {

/// Helper function to check if two values are equal
fn values_equal(a: &Value, b: &Value) -> bool {
a == b
match (a, b) {
(Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Null, Value::Null) => true,
(Value::Nothing, Value::Nothing) => true,
(Value::List(a), Value::List(b)) => {
let a_ref = a.borrow();
let b_ref = b.borrow();
if a_ref.len() != b_ref.len() {
return false;
}
a_ref
.iter()
.zip(b_ref.iter())
.all(|(x, y)| values_equal(x, y))
}
_ => false,
}
}

/// Helper function to check if a value is truthy
Expand Down
21 changes: 8 additions & 13 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::parser::ast::{
};
use crate::pattern::CompiledPattern;
use crate::stdlib;
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::{self, Write};
use std::net::IpAddr;
Expand Down Expand Up @@ -341,7 +341,6 @@ pub struct Interpreter {
current_count: RefCell<Option<f64>>,
in_count_loop: RefCell<bool>,
in_main_loop: RefCell<bool>, // Track if we're in a main loop (disables timeout)
op_count: Cell<usize>, // Instruction counter for optimized timeout checks
started: Instant,
max_duration: Duration,
call_stack: RefCell<Vec<CallFrame>>,
Expand Down Expand Up @@ -1177,7 +1176,6 @@ impl Interpreter {
current_count: RefCell::new(None),
in_count_loop: RefCell::new(false),
in_main_loop: RefCell::new(false),
op_count: Cell::new(0),
started: Instant::now(),
max_duration: Duration::from_secs(config.timeout_seconds),
call_stack: RefCell::new(Vec::new()),
Expand Down Expand Up @@ -1431,15 +1429,6 @@ impl Interpreter {
return Ok(());
}

// Optimization: Only check system time every 1024 operations
// This avoids expensive syscalls/hardware clock reads in tight loops
let count = self.op_count.get();
self.op_count.set(count.wrapping_add(1));

if count & 1023 != 0 {
return Ok(());
}

if self.started.elapsed() > self.max_duration {
if *self.in_count_loop.borrow() {
*self.in_count_loop.borrow_mut() = false;
Expand Down Expand Up @@ -7001,7 +6990,13 @@ impl Interpreter {
}

fn is_equal(&self, left: &Value, right: &Value) -> bool {
left == right
match (left, right) {
(Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Null, Value::Null) => true,
(a, b) => a == b,
}
}

// Helper method to create container instance with inheritance
Expand Down
169 changes: 18 additions & 151 deletions src/interpreter/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::error::RuntimeError;
use crate::parser::ast::Statement;
use crate::pattern::CompiledPattern;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fmt;
use std::rc::{Rc, Weak};

Expand Down Expand Up @@ -370,158 +370,25 @@ impl fmt::Display for Value {

impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
// Fast path for simple types that don't need cycle detection
match (self, other) {
(Value::Number(a), Value::Number(b)) => return (a - b).abs() < f64::EPSILON,
(Value::Text(a), Value::Text(b)) => return a == b,
(Value::Bool(a), Value::Bool(b)) => return a == b,
(Value::Null, Value::Null) => return true,
(Value::Nothing, Value::Nothing) => return true,
(Value::Date(a), Value::Date(b)) => return a == b,
(Value::Time(a), Value::Time(b)) => return a == b,
(Value::DateTime(a), Value::DateTime(b)) => return a == b,
(Value::Pattern(a), Value::Pattern(b)) => return Rc::ptr_eq(a, b),
// For types that might contain cycles or require deeper inspection, use the full visited check
_ => {}
}

let mut visited = HashSet::new();
eq_with_visited(self, other, &mut visited)
}
}

fn eq_with_visited(
lhs: &Value,
rhs: &Value,
visited: &mut HashSet<(*const (), *const ())>,
) -> bool {
match (lhs, rhs) {
(Value::Number(a), Value::Number(b)) => a == b,
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Date(a), Value::Date(b)) => a == b,
(Value::Time(a), Value::Time(b)) => a == b,
(Value::DateTime(a), Value::DateTime(b)) => a == b,
(Value::Null, Value::Null) => true,
(Value::Nothing, Value::Nothing) => true,

(Value::List(a), Value::List(b)) => {
if Rc::ptr_eq(a, b) {
return true;
}

let ptr_a = Rc::as_ptr(a) as *const ();
let ptr_b = Rc::as_ptr(b) as *const ();
let pair = (ptr_a, ptr_b);

if visited.contains(&pair) {
return true; // Cycle detected, assume equal for now
}
visited.insert(pair);

// Use try_borrow to avoid panics if already borrowed mutably
match (a.try_borrow(), b.try_borrow()) {
(Ok(a_ref), Ok(b_ref)) => {
if a_ref.len() != b_ref.len() {
return false;
}
a_ref
.iter()
.zip(b_ref.iter())
.all(|(x, y)| eq_with_visited(x, y, visited))
}
_ => false, // Cannot compare if mutably borrowed elsewhere
}
}

(Value::Object(a), Value::Object(b)) => {
if Rc::ptr_eq(a, b) {
return true;
}

let ptr_a = Rc::as_ptr(a) as *const ();
let ptr_b = Rc::as_ptr(b) as *const ();
let pair = (ptr_a, ptr_b);

if visited.contains(&pair) {
return true;
}
visited.insert(pair);

match (a.try_borrow(), b.try_borrow()) {
(Ok(a_ref), Ok(b_ref)) => {
if a_ref.len() != b_ref.len() {
return false;
}
a_ref.iter().all(|(k, v)| {
b_ref
.get(k)
.is_some_and(|bv| eq_with_visited(v, bv, visited))
})
}
_ => false,
}
}

(Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
(Value::NativeFunction(name_a, func_a), Value::NativeFunction(name_b, func_b)) => {
name_a == name_b && std::ptr::fn_addr_eq(*func_a, *func_b)
}
(Value::Future(a), Value::Future(b)) => Rc::ptr_eq(a, b),
(Value::Pattern(a), Value::Pattern(b)) => Rc::ptr_eq(a, b),

(Value::ContainerDefinition(a), Value::ContainerDefinition(b)) => a.name == b.name,
(Value::ContainerInstance(a), Value::ContainerInstance(b)) => {
if Rc::ptr_eq(a, b) {
return true;
}

let ptr_a = Rc::as_ptr(a) as *const ();
let ptr_b = Rc::as_ptr(b) as *const ();
let pair = (ptr_a, ptr_b);

if visited.contains(&pair) {
return true;
}
visited.insert(pair);

match (a.try_borrow(), b.try_borrow()) {
(Ok(a_ref), Ok(b_ref)) => {
if a_ref.container_type != b_ref.container_type {
return false;
}

// Compare parent hierarchy
let parents_match = match (&a_ref.parent, &b_ref.parent) {
(Some(p1), Some(p2)) => {
let v1 = Value::ContainerInstance(Rc::clone(p1));
let v2 = Value::ContainerInstance(Rc::clone(p2));
eq_with_visited(&v1, &v2, visited)
}
(None, None) => true,
_ => false,
};

if !parents_match {
return false;
}

if a_ref.properties.len() != b_ref.properties.len() {
return false;
}
a_ref.properties.iter().all(|(k, v)| {
b_ref
.properties
.get(k)
.is_some_and(|bv| eq_with_visited(v, bv, visited))
})
}
_ => false,
(Value::Number(a), Value::Number(b)) => a == b,
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Date(a), Value::Date(b)) => a == b,
(Value::Time(a), Value::Time(b)) => a == b,
(Value::DateTime(a), Value::DateTime(b)) => a == b,
(Value::Null, Value::Null) => true,
(Value::Nothing, Value::Nothing) => true,
(Value::ContainerDefinition(a), Value::ContainerDefinition(b)) => a.name == b.name,
(Value::ContainerInstance(a), Value::ContainerInstance(b)) => {
let a = a.borrow();
let b = b.borrow();
a.container_type == b.container_type
Comment on lines +384 to +386

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Container instance equality now only checks container_type, ignoring properties and parent hierarchy. This could cause instances with different data to be considered equal, breaking correctness.

Suggested change
let a = a.borrow();
let b = b.borrow();
a.container_type == b.container_type
std::rc::Rc::ptr_eq(a, b)

Copilot uses AI. Check for mistakes.
}
(Value::ContainerMethod(a), Value::ContainerMethod(b)) => a.name == b.name,
(Value::ContainerEvent(a), Value::ContainerEvent(b)) => a.name == b.name,
(Value::InterfaceDefinition(a), Value::InterfaceDefinition(b)) => a.name == b.name,
_ => false,
}
(Value::ContainerMethod(a), Value::ContainerMethod(b)) => a.name == b.name,
(Value::ContainerEvent(a), Value::ContainerEvent(b)) => a.name == b.name,
(Value::InterfaceDefinition(a), Value::InterfaceDefinition(b)) => a.name == b.name,
_ => false,
}
}
17 changes: 14 additions & 3 deletions src/stdlib/core.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use super::helpers::check_arg_count;
use crate::interpreter::environment::Environment;
use crate::interpreter::error::RuntimeError;
use crate::interpreter::value::Value;
Expand All @@ -16,14 +15,26 @@ pub fn native_print(args: Vec<Value>) -> Result<Value, RuntimeError> {
}

pub fn native_typeof(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("typeof", &args, 1)?;
if args.len() != 1 {
return Err(RuntimeError::new(
format!("typeof expects 1 argument, got {}", args.len()),
0,
0,
));
}

let type_name = args[0].type_name();
Ok(Value::Text(Rc::from(type_name)))
}

pub fn native_isnothing(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("isnothing", &args, 1)?;
if args.len() != 1 {
return Err(RuntimeError::new(
format!("isnothing expects 1 argument, got {}", args.len()),
0,
0,
));
}

match &args[0] {
Value::Null => Ok(Value::Bool(true)),
Expand Down
Loading