diff --git a/CHANGELOG.md b/CHANGELOG.md index a17704a..873b77d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [release] — 2026-03-26 +- Bumped compiler crate version to `0.1.1` in `compiler/v1/Cargo.toml`. +- Fixed `match` on struct instances via struct field patterns. +- Fixed enum variant matching for both qualified (`Enum::Variant`) and unqualified (`Variant`) patterns. +- Improved grouped import diagnostics so missing items highlight the specific missing name. +- Verified nested module resolution where `module/main.rey` imports other local files. +- Fixed numeric semantics by distinguishing `int` and `float` values at runtime: + - `10 / 3` performs integer division. + - `10.0 / 3.0` and `10 / 3.0` perform float division. +- Allowed external mutation of `pub` struct fields (`obj.field = ...`, `obj.field += ...`) and emit a clear error for nested field assignment (`obj.inner.field = ...`) for now. +- Added `rey-compiler/` v5 bootstrap skeleton (API surface only). +- Added `compiler/README.md` to document the v1 Rust interpreter vs bootstrap target. + ## [release] — 2026-03-23 - Prepared `v0.1.0` release candidate assets and docs. - Audited syntax documentation against current parser/runtime behavior. diff --git a/compiler/README.md b/compiler/README.md new file mode 100644 index 0000000..626bf25 --- /dev/null +++ b/compiler/README.md @@ -0,0 +1,17 @@ +# Compiler Layout + +This repo currently contains two compiler tracks: + +## `compiler/v1/` +The shipping Rust implementation for Rey v0. + +It is a reference interpreter + typechecker + import resolver used for day-to-day +language development and releases. + +## `rey-compiler/` +The long-term bootstrap target: a compiler written in Rey ("Rey-in-Rey"). + +At the moment this is API-only (types + public function signatures) so we can +stabilize the architecture and start iterating once the v1 Rust runtime is +stable enough to host it. + diff --git a/compiler/v1/Cargo.toml b/compiler/v1/Cargo.toml index 9e06cad..bdd9a0c 100644 --- a/compiler/v1/Cargo.toml +++ b/compiler/v1/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rey-v0" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "Rey v0 reference interpreter" diff --git a/compiler/v1/src/ast/literal.rs b/compiler/v1/src/ast/literal.rs index 5debd1a..ae889df 100644 --- a/compiler/v1/src/ast/literal.rs +++ b/compiler/v1/src/ast/literal.rs @@ -2,7 +2,8 @@ pub enum Literal { String(String), Char(char), - Number(f64), + Int(i64), + Float(f64), Bool(bool), Null, } diff --git a/compiler/v1/src/ast/stmt.rs b/compiler/v1/src/ast/stmt.rs index 2206ef5..247cc80 100644 --- a/compiler/v1/src/ast/stmt.rs +++ b/compiler/v1/src/ast/stmt.rs @@ -35,9 +35,15 @@ pub enum FunctionVisibility { #[derive(Debug, Clone, PartialEq)] pub enum ImportKind { - FileSymbols { module: String, symbols: Vec }, + FileSymbols { module: String, symbols: Vec }, ModuleNamespace { module: String }, - ModuleItems { module: String, items: Vec }, + ModuleItems { module: String, items: Vec }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ImportName { + pub name: String, + pub span: Span, } #[derive(Debug, Clone, PartialEq)] @@ -104,6 +110,10 @@ pub struct MatchArm { #[derive(Debug, Clone, PartialEq)] pub enum Pattern { EnumVariant(String, String), // enum_name, variant_name + Struct { + struct_name: String, + fields: Vec<(String, Pattern)>, // field_name -> field_pattern + }, Literal(Literal), Variable(String), Wildcard, diff --git a/compiler/v1/src/imports.rs b/compiler/v1/src/imports.rs index 6fa544d..9b85ccd 100644 --- a/compiler/v1/src/imports.rs +++ b/compiler/v1/src/imports.rs @@ -156,26 +156,26 @@ impl ResolverState { } for symbol in symbols { - if !injectedNames.insert(symbol.clone()) { + if !injectedNames.insert(symbol.name.clone()) { return Err(CompileError { title: "import".to_string(), file: ownerFile.to_path_buf(), source: ownerSource.to_string(), - span, - message: format!("Duplicate import: '{}'", symbol), + span: symbol.span, + message: format!("Duplicate import: '{}'", symbol.name), }); } - match imported.localFunctionVisibility.get(&symbol) { + match imported.localFunctionVisibility.get(&symbol.name) { Some(FunctionVisibility::ExportPub) => {} Some(FunctionVisibility::Pub) => { return Err(CompileError { title: "import".to_string(), file: ownerFile.to_path_buf(), source: ownerSource.to_string(), - span, + span: symbol.span, message: format!( "Function '{}' exists in '{}' but is 'pub', not 'export pub'", - symbol, + symbol.name, importFile.display() ), }); @@ -185,10 +185,10 @@ impl ResolverState { title: "import".to_string(), file: ownerFile.to_path_buf(), source: ownerSource.to_string(), - span, + span: symbol.span, message: format!( "Function '{}' exists in '{}' but is private", - symbol, + symbol.name, importFile.display() ), }); @@ -198,10 +198,10 @@ impl ResolverState { title: "import".to_string(), file: ownerFile.to_path_buf(), source: ownerSource.to_string(), - span, + span: symbol.span, message: format!( "Function '{}' not found in file '{}'", - symbol, + symbol.name, importFile.display() ), }); @@ -293,26 +293,26 @@ impl ResolverState { } ImportKind::ModuleItems { module, items } => { for item in items { - if !injectedNames.insert(item.clone()) { + if !injectedNames.insert(item.name.clone()) { return Err(CompileError { title: "import".to_string(), file: ownerFile.to_path_buf(), source: ownerSource.to_string(), - span, - message: format!("Duplicate import: '{}'", item), + span: item.span, + message: format!("Duplicate import: '{}'", item.name), }); } let importFile = self.findModuleItemFile( currentDir, &module, - &item, + &item.name, ownerFile, ownerSource, - span, + item.span, )?; if self.stack.contains(&importFile) { - return Err(self.circularError(ownerFile, ownerSource, span, &importFile)); + return Err(self.circularError(ownerFile, ownerSource, item.span, &importFile)); } let imported = self.resolveFile(&importFile)?; if !includedFiles.contains(&importFile) { @@ -325,11 +325,11 @@ impl ResolverState { if visibility == FunctionVisibility::ExportPub { namespaceEntries.push(( name.clone(), - Expr::Variable { name, span }, + Expr::Variable { name, span: item.span }, )); } } - resolved.push(self.namespaceStmt(&item, namespaceEntries, span)); + resolved.push(self.namespaceStmt(&item.name, namespaceEntries, item.span)); } Ok(()) } diff --git a/compiler/v1/src/interpreter/executor.rs b/compiler/v1/src/interpreter/executor.rs index d20b0fb..e136f78 100644 --- a/compiler/v1/src/interpreter/executor.rs +++ b/compiler/v1/src/interpreter/executor.rs @@ -119,16 +119,28 @@ impl Executor { let end_val = self.evaluate_expr(end, env)?; let start_num = match start_val { - Value::Number(n) => n as i64, + Value::Int(n) => n, + Value::Float(n) => { + if n.fract() != 0.0 { + return Err("Range start must be an integer".to_string()); + } + n as i64 + } _ => return Err("Range start must be a number".to_string()), }; let end_num = match end_val { - Value::Number(n) => n as i64, + Value::Int(n) => n, + Value::Float(n) => { + if n.fract() != 0.0 { + return Err("Range end must be an integer".to_string()); + } + n as i64 + } _ => return Err("Range end must be a number".to_string()), }; for i in start_num..end_num { - env.define(variable.clone(), Value::Number(i as f64)); + env.define(variable.clone(), Value::Int(i)); match self.execute_block_with_control_flow(body, env)? { ControlFlow::Break => break, @@ -171,26 +183,11 @@ impl Executor { } Stmt::Match { expr, arms } => { let value = self.evaluate_expr(expr, env)?; - use crate::ast::stmt::Pattern; for arm in arms { - let matched = match (&arm.pattern, &value) { - (Pattern::Wildcard, _) => true, - (Pattern::Variable(_), _) => true, - (Pattern::Literal(lit), val) => { - let pattern_val = Value::from(lit.clone()); - pattern_val == *val - } - (Pattern::EnumVariant(enum_name, variant), Value::EnumVariant { enum_name: en, variant: v }) => { - enum_name == en && variant == v - } - _ => false, - }; - - if matched { - // Bind variable if it's a variable pattern - if let Pattern::Variable(name) = &arm.pattern { - env.define(name.clone(), value.clone()); + if let Some(bindings) = self.patternMatch(&arm.pattern, &value, env) { + for (name, val) in bindings { + env.define(name, val); } // Execute the arm body @@ -201,11 +198,6 @@ impl Executor { } } - // Return the value of the last statement if it's a return - if let Some(Stmt::Return(expr)) = arm.body.last() { - return Ok(ControlFlow::return_value(self.evaluate_expr(expr, env)?)); - } - return Ok(ControlFlow::normal(Value::Null)); } } @@ -216,6 +208,60 @@ impl Executor { } } + fn patternMatch( + &self, + pattern: &crate::ast::stmt::Pattern, + value: &Value, + env: &Environment, + ) -> Option> { + use crate::ast::stmt::Pattern; + match (pattern, value) { + (Pattern::Wildcard, _) => Some(Vec::new()), + (Pattern::Literal(lit), val) => { + let pattern_val = Value::from(lit.clone()); + if pattern_val == *val { + Some(Vec::new()) + } else { + None + } + } + (Pattern::EnumVariant(enum_name, variant), Value::EnumVariant { enum_name: en, variant: v }) => { + if enum_name == en && variant == v { + Some(Vec::new()) + } else { + None + } + } + (Pattern::Struct { struct_name, fields }, Value::StructInstance { struct_name: sn, fields: vals }) => { + if struct_name != sn { + return None; + } + let mut bindings = Vec::new(); + for (field_name, field_pat) in fields { + let vals_ref = vals.borrow(); + let field_val = vals_ref.get(field_name)?; + let mut b = self.patternMatch(field_pat, field_val, env)?; + bindings.append(&mut b); + } + Some(bindings) + } + (Pattern::Variable(name), val) => { + // Disambiguate enum-variant constants from variable-binding patterns. + // If the identifier resolves to an enum variant value, treat it as a constant pattern. + if let (Some(Value::EnumVariant { enum_name: enp, variant: vp }), Value::EnumVariant { enum_name: envv, variant: vv }) = + (env.get(name), val) + { + if enp == envv && vp == vv { + return Some(Vec::new()); + } + return None; + } + Some(vec![(name.clone(), val.clone())]) + } + _ => None, + } + } + pub fn evaluate_expr(&self, expr: &Expr, env: &mut Environment) -> Result { match expr { Expr::Literal { value, .. } => Ok(Value::from(value.clone())), @@ -254,7 +300,18 @@ impl Executor { let target_val = self.evaluate_expr(target, env)?; let index_val = self.evaluate_expr(index, env)?; match (target_val, index_val) { - (Value::Array(arr), Value::Number(n)) => { + (Value::Array(arr), Value::Int(n)) => { + let idx = n as isize; + if idx < 0 { + return Err("Array index must be non-negative".to_string()); + } + let idx = idx as usize; + arr.borrow() + .get(idx) + .cloned() + .ok_or_else(|| "Array index out of bounds".to_string()) + } + (Value::Array(arr), Value::Float(n)) => { if n.fract() != 0.0 { return Err("Array index must be an integer".to_string()); } @@ -442,9 +499,12 @@ impl Executor { (Value::String(_), "String") => true, (Value::Char(_), "char") => true, (Value::Bool(_), "bool") => true, - (Value::Number(n), "int") => n.fract() == 0.0, - (Value::Number(_), "float") => true, - (Value::Number(_), "double") => true, + (Value::Int(_), "int") => true, + (Value::Float(_), "int") => false, + (Value::Int(_), "float") => true, + (Value::Float(_), "float") => true, + (Value::Int(_), "double") => true, + (Value::Float(_), "double") => true, (Value::Array(_), t) => t.starts_with('[') && t.ends_with(']'), (Value::Dict(_), t) => t.starts_with('{') && t.ends_with('}'), (Value::Tuple(_), "Tuple") => true, @@ -471,6 +531,12 @@ impl Executor { Ok(val) } Expr::Set { object, name, value, .. } => { + if matches!(object.as_ref(), Expr::Get { .. }) { + return Err(format!( + "error[E010]: nested field assignment is not supported (got '.{} = ...')", + name + )); + } let obj_val = self.evaluate_expr(object, env)?; let val = self.evaluate_expr(value, env)?; match obj_val { @@ -479,6 +545,21 @@ impl Executor { Ok(val) } Value::StructInstance { struct_name, fields } => { + let def = env + .get_struct(&struct_name) + .ok_or_else(|| format!("Undefined struct '{}'", struct_name))?; + let field = def.fields.iter().find(|f| f.name == *name).ok_or_else(|| { + format!( + "error[E004]: unknown field '{}' on struct '{}'", + name, struct_name + ) + })?; + if !field.is_pub { + return Err(format!( + "error[E011]: cannot mutate private field '{}' on struct '{}'", + name, struct_name + )); + } if fields.borrow().contains_key(name) { fields.borrow_mut().insert(name.clone(), val.clone()); Ok(val) @@ -497,7 +578,20 @@ impl Executor { let index_val = self.evaluate_expr(index, env)?; let val = self.evaluate_expr(value, env)?; match (target_val, index_val) { - (Value::Array(arr), Value::Number(n)) => { + (Value::Array(arr), Value::Int(n)) => { + let idx = n as isize; + if idx < 0 { + return Err("Array index must be non-negative".to_string()); + } + let idx = idx as usize; + let mut arr_mut = arr.borrow_mut(); + if idx >= arr_mut.len() { + return Err("Array index out of bounds".to_string()); + } + arr_mut[idx] = val.clone(); + Ok(val) + } + (Value::Array(arr), Value::Float(n)) => { if n.fract() != 0.0 { return Err("Array index must be an integer".to_string()); } @@ -526,20 +620,29 @@ impl Executor { .cloned() .ok_or_else(|| format!("Undefined variable '{}'", name))?; - let delta = match op { - TokenKind::PlusPlus => 1.0, - TokenKind::MinusMinus => -1.0, - _ => return Err("Invalid update operator".to_string()), - }; - - let current_num = match current { - Value::Number(n) => n, - _ => return Err("Can only apply ++/-- to numbers".to_string()), - }; - - let new_num = current_num + delta; - env.assign(name, Value::Number(new_num))?; - Ok(Value::Number(if *prefix { new_num } else { current_num })) + match current { + Value::Int(n) => { + let delta = match op { + TokenKind::PlusPlus => 1, + TokenKind::MinusMinus => -1, + _ => return Err("Invalid update operator".to_string()), + }; + let new_n = n + delta; + env.assign(name, Value::Int(new_n))?; + Ok(if *prefix { Value::Int(new_n) } else { Value::Int(n) }) + } + Value::Float(n) => { + let delta = match op { + TokenKind::PlusPlus => 1.0, + TokenKind::MinusMinus => -1.0, + _ => return Err("Invalid update operator".to_string()), + }; + let new_n = n + delta; + env.assign(name, Value::Float(new_n))?; + Ok(if *prefix { Value::Float(new_n) } else { Value::Float(n) }) + } + _ => Err("Can only apply ++/-- to numbers".to_string()), + } } Expr::Call { callee, args, .. } => { let mut evaluated_args = Vec::new(); @@ -748,7 +851,7 @@ impl Executor { return Err(format!("toInt() expects 0 arguments, got {}", args.len())); } match s.parse::() { - Ok(n) => Ok(Value::Number(n.trunc())), + Ok(n) => Ok(Value::Int(n.trunc() as i64)), Err(_) => Err(format!("Cannot convert string '{}' to int", s)), } } @@ -757,21 +860,33 @@ impl Executor { return Err(format!("toFloat() expects 0 arguments, got {}", args.len())); } match s.parse::() { - Ok(n) => Ok(Value::Number(n)), + Ok(n) => Ok(Value::Float(n)), Err(_) => Err(format!("Cannot convert string '{}' to float", s)), } } - (Value::Number(n), "toInt") => { + (Value::Int(n), "toInt") => { if !args.is_empty() { return Err(format!("toInt() expects 0 arguments, got {}", args.len())); } - Ok(Value::Number(n.trunc())) + Ok(Value::Int(n)) } - (Value::Number(n), "toFloat") => { + (Value::Float(n), "toInt") => { + if !args.is_empty() { + return Err(format!("toInt() expects 0 arguments, got {}", args.len())); + } + Ok(Value::Int(n.trunc() as i64)) + } + (Value::Int(n), "toFloat") => { if !args.is_empty() { return Err(format!("toFloat() expects 0 arguments, got {}", args.len())); } - Ok(Value::Number(n)) + Ok(Value::Float(n as f64)) + } + (Value::Float(n), "toFloat") => { + if !args.is_empty() { + return Err(format!("toFloat() expects 0 arguments, got {}", args.len())); + } + Ok(Value::Float(n)) } (Value::Array(arr), "length") => { if !args.is_empty() { @@ -781,7 +896,7 @@ impl Executor { args.len() )); } - Ok(Value::Number(arr.borrow().len() as f64)) + Ok(Value::Int(arr.borrow().len() as i64)) } (Value::Array(arr), "push") => { if args.len() != 1 { @@ -802,7 +917,7 @@ impl Executor { args.len() )); } - Ok(Value::Number(s.chars().count() as f64)) + Ok(Value::Int(s.chars().count() as i64)) } (Value::String(s), "upper") => { if !args.is_empty() { @@ -865,7 +980,8 @@ impl Executor { match value { Value::Bool(false) => false, Value::Null => false, - Value::Number(n) => *n != 0.0, + Value::Int(n) => *n != 0, + Value::Float(n) => *n != 0.0, _ => true, } } @@ -874,40 +990,106 @@ impl Executor { use TokenKind::*; match (left, op, right) { - (Value::Number(l), Plus, Value::Number(r)) => Ok(Value::Number(l + r)), - (Value::Number(l), Minus, Value::Number(r)) => Ok(Value::Number(l - r)), - (Value::Number(l), Star, Value::Number(r)) => Ok(Value::Number(l * r)), - (Value::Number(l), Slash, Value::Number(r)) => { + (Value::Int(l), Plus, Value::Int(r)) => Ok(Value::Int(l + r)), + (Value::Int(l), Minus, Value::Int(r)) => Ok(Value::Int(l - r)), + (Value::Int(l), Star, Value::Int(r)) => Ok(Value::Int(l * r)), + (Value::Int(l), Slash, Value::Int(r)) => { + if r == 0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Int(l / r)) + } + } + (Value::Int(l), Percent, Value::Int(r)) => { + if r == 0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Int(l % r)) + } + } + + (Value::Float(l), Plus, Value::Float(r)) => Ok(Value::Float(l + r)), + (Value::Float(l), Minus, Value::Float(r)) => Ok(Value::Float(l - r)), + (Value::Float(l), Star, Value::Float(r)) => Ok(Value::Float(l * r)), + (Value::Float(l), Slash, Value::Float(r)) => { if r == 0.0 { Err("Division by zero".to_string()) } else { - // Integer division if both operands are whole numbers - let l_is_int = l.fract() == 0.0; - let r_is_int = r.fract() == 0.0; - if l_is_int && r_is_int { - Ok(Value::Number(((l / r) as i64) as f64)) - } else { - Ok(Value::Number(l / r)) - } + Ok(Value::Float(l / r)) + } + } + (Value::Float(l), Percent, Value::Float(r)) => { + if r == 0.0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Float(l % r)) + } + } + + (Value::Int(l), Plus, Value::Float(r)) => Ok(Value::Float((l as f64) + r)), + (Value::Float(l), Plus, Value::Int(r)) => Ok(Value::Float(l + (r as f64))), + (Value::Int(l), Minus, Value::Float(r)) => Ok(Value::Float((l as f64) - r)), + (Value::Float(l), Minus, Value::Int(r)) => Ok(Value::Float(l - (r as f64))), + (Value::Int(l), Star, Value::Float(r)) => Ok(Value::Float((l as f64) * r)), + (Value::Float(l), Star, Value::Int(r)) => Ok(Value::Float(l * (r as f64))), + (Value::Int(l), Slash, Value::Float(r)) => { + if r == 0.0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Float((l as f64) / r)) + } + } + (Value::Float(l), Slash, Value::Int(r)) => { + if r == 0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Float(l / (r as f64))) } } - (Value::Number(l), Percent, Value::Number(r)) => { + (Value::Int(l), Percent, Value::Float(r)) => { if r == 0.0 { Err("Division by zero".to_string()) } else { - Ok(Value::Number(l % r)) + Ok(Value::Float((l as f64) % r)) + } + } + (Value::Float(l), Percent, Value::Int(r)) => { + if r == 0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Float(l % (r as f64))) } } (Value::Null, EqualEqual, Value::Null) => Ok(Value::Bool(true)), (Value::Null, NotEqual, Value::Null) => Ok(Value::Bool(false)), (Value::Null, EqualEqual, _) | (_, EqualEqual, Value::Null) => Ok(Value::Bool(false)), (Value::Null, NotEqual, _) | (_, NotEqual, Value::Null) => Ok(Value::Bool(true)), - (Value::Number(l), EqualEqual, Value::Number(r)) => Ok(Value::Bool(l == r)), - (Value::Number(l), NotEqual, Value::Number(r)) => Ok(Value::Bool(l != r)), - (Value::Number(l), Less, Value::Number(r)) => Ok(Value::Bool(l < r)), - (Value::Number(l), LessEqual, Value::Number(r)) => Ok(Value::Bool(l <= r)), - (Value::Number(l), Greater, Value::Number(r)) => Ok(Value::Bool(l > r)), - (Value::Number(l), GreaterEqual, Value::Number(r)) => Ok(Value::Bool(l >= r)), + (Value::Int(l), EqualEqual, Value::Int(r)) => Ok(Value::Bool(l == r)), + (Value::Int(l), NotEqual, Value::Int(r)) => Ok(Value::Bool(l != r)), + (Value::Int(l), Less, Value::Int(r)) => Ok(Value::Bool(l < r)), + (Value::Int(l), LessEqual, Value::Int(r)) => Ok(Value::Bool(l <= r)), + (Value::Int(l), Greater, Value::Int(r)) => Ok(Value::Bool(l > r)), + (Value::Int(l), GreaterEqual, Value::Int(r)) => Ok(Value::Bool(l >= r)), + + (Value::Float(l), EqualEqual, Value::Float(r)) => Ok(Value::Bool(l == r)), + (Value::Float(l), NotEqual, Value::Float(r)) => Ok(Value::Bool(l != r)), + (Value::Float(l), Less, Value::Float(r)) => Ok(Value::Bool(l < r)), + (Value::Float(l), LessEqual, Value::Float(r)) => Ok(Value::Bool(l <= r)), + (Value::Float(l), Greater, Value::Float(r)) => Ok(Value::Bool(l > r)), + (Value::Float(l), GreaterEqual, Value::Float(r)) => Ok(Value::Bool(l >= r)), + + (Value::Int(l), EqualEqual, Value::Float(r)) => Ok(Value::Bool((l as f64) == r)), + (Value::Float(l), EqualEqual, Value::Int(r)) => Ok(Value::Bool(l == (r as f64))), + (Value::Int(l), NotEqual, Value::Float(r)) => Ok(Value::Bool((l as f64) != r)), + (Value::Float(l), NotEqual, Value::Int(r)) => Ok(Value::Bool(l != (r as f64))), + (Value::Int(l), Less, Value::Float(r)) => Ok(Value::Bool((l as f64) < r)), + (Value::Float(l), Less, Value::Int(r)) => Ok(Value::Bool(l < (r as f64))), + (Value::Int(l), LessEqual, Value::Float(r)) => Ok(Value::Bool((l as f64) <= r)), + (Value::Float(l), LessEqual, Value::Int(r)) => Ok(Value::Bool(l <= (r as f64))), + (Value::Int(l), Greater, Value::Float(r)) => Ok(Value::Bool((l as f64) > r)), + (Value::Float(l), Greater, Value::Int(r)) => Ok(Value::Bool(l > (r as f64))), + (Value::Int(l), GreaterEqual, Value::Float(r)) => Ok(Value::Bool((l as f64) >= r)), + (Value::Float(l), GreaterEqual, Value::Int(r)) => Ok(Value::Bool(l >= (r as f64))), (Value::String(l), Plus, r) => { Ok(Value::String(l + &super::std::StdLib::formatValue(&r))) @@ -931,7 +1113,8 @@ impl Executor { use TokenKind::*; match (op, right) { - (Minus, Value::Number(n)) => Ok(Value::Number(-n)), + (Minus, Value::Int(n)) => Ok(Value::Int(-n)), + (Minus, Value::Float(n)) => Ok(Value::Float(-n)), (Not, Value::Bool(b)) => Ok(Value::Bool(!b)), _ => Err("Invalid unary operation".to_string()), } diff --git a/compiler/v1/src/interpreter/std.rs b/compiler/v1/src/interpreter/std.rs index f98496d..5547162 100644 --- a/compiler/v1/src/interpreter/std.rs +++ b/compiler/v1/src/interpreter/std.rs @@ -94,7 +94,8 @@ impl StdLib { return Some(Err(format!("abs expects 1 argument, got {}", args.len()))); } match &args[0] { - Value::Number(n) => Some(Ok(Value::Number(n.abs()))), + Value::Int(n) => Some(Ok(Value::Int(n.abs()))), + Value::Float(n) => Some(Ok(Value::Float(n.abs()))), _ => Some(Err("abs expects a number".to_string())), } } @@ -103,9 +104,10 @@ impl StdLib { return Some(Err(format!("max expects 2 arguments, got {}", args.len()))); } match (&args[0], &args[1]) { - (Value::Number(a), Value::Number(b)) => { - Some(Ok(Value::Number(if a > b { *a } else { *b }))) - } + (Value::Int(a), Value::Int(b)) => Some(Ok(Value::Int(if a > b { *a } else { *b }))), + (Value::Float(a), Value::Float(b)) => Some(Ok(Value::Float(if a > b { *a } else { *b }))), + (Value::Int(a), Value::Float(b)) => Some(Ok(Value::Float(if (*a as f64) > *b { *a as f64 } else { *b }))), + (Value::Float(a), Value::Int(b)) => Some(Ok(Value::Float(if *a > (*b as f64) { *a } else { *b as f64 }))), _ => Some(Err("max expects two numbers".to_string())), } } @@ -114,9 +116,10 @@ impl StdLib { return Some(Err(format!("min expects 2 arguments, got {}", args.len()))); } match (&args[0], &args[1]) { - (Value::Number(a), Value::Number(b)) => { - Some(Ok(Value::Number(if a < b { *a } else { *b }))) - } + (Value::Int(a), Value::Int(b)) => Some(Ok(Value::Int(if a < b { *a } else { *b }))), + (Value::Float(a), Value::Float(b)) => Some(Ok(Value::Float(if a < b { *a } else { *b }))), + (Value::Int(a), Value::Float(b)) => Some(Ok(Value::Float(if (*a as f64) < *b { *a as f64 } else { *b }))), + (Value::Float(a), Value::Int(b)) => Some(Ok(Value::Float(if *a < (*b as f64) { *a } else { *b as f64 }))), _ => Some(Err("min expects two numbers".to_string())), } } @@ -134,16 +137,16 @@ impl StdLib { // Simple pseudo-random using time and a bit of math to avoid contiguous values let rand_val = ((time.wrapping_mul(1103515245).wrapping_add(12345)) % 10000) as f64 / 10000.0; - Some(Ok(Value::Number(rand_val))) + Some(Ok(Value::Float(rand_val))) } "len" => { if args.len() != 1 { return Some(Err(format!("len expects 1 argument, got {}", args.len()))); } match &args[0] { - Value::String(s) => Some(Ok(Value::Number(s.chars().count() as f64))), - Value::Array(arr) => Some(Ok(Value::Number(arr.borrow().len() as f64))), - Value::Dict(d) => Some(Ok(Value::Number(d.borrow().len() as f64))), + Value::String(s) => Some(Ok(Value::Int(s.chars().count() as i64))), + Value::Array(arr) => Some(Ok(Value::Int(arr.borrow().len() as i64))), + Value::Dict(d) => Some(Ok(Value::Int(d.borrow().len() as i64))), _ => Some(Err("len expects a string, array, or dictionary".to_string())), } } @@ -207,7 +210,8 @@ impl StdLib { match value { Value::String(s) => s.clone(), Value::Char(c) => c.to_string(), - Value::Number(n) => { + Value::Int(n) => format!("{}", n), + Value::Float(n) => { if n.fract() == 0.0 { format!("{}", *n as i64) } else { diff --git a/compiler/v1/src/interpreter/value.rs b/compiler/v1/src/interpreter/value.rs index d95e380..490c1a0 100644 --- a/compiler/v1/src/interpreter/value.rs +++ b/compiler/v1/src/interpreter/value.rs @@ -18,7 +18,8 @@ pub struct StructDef { pub enum Value { String(String), Char(char), - Number(f64), + Int(i64), + Float(f64), Bool(bool), EnumVariant { enum_name: String, variant: String }, Function(Function), @@ -37,7 +38,8 @@ impl std::fmt::Display for Value { match self { Value::String(s) => write!(f, "{}", s), Value::Char(c) => write!(f, "{}", c), - Value::Number(n) => { + Value::Int(n) => write!(f, "{}", n), + Value::Float(n) => { if n.fract() == 0.0 { write!(f, "{}", *n as i64) } else { @@ -83,7 +85,9 @@ impl PartialEq for Value { match (self, other) { (Value::String(a), Value::String(b)) => a == b, (Value::Char(a), Value::Char(b)) => a == b, - (Value::Number(a), Value::Number(b)) => a == b, + (Value::Int(a), Value::Int(b)) => a == b, + (Value::Float(a), Value::Float(b)) => a == b, + (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => (*a as f64) == *b, (Value::Bool(a), Value::Bool(b)) => a == b, (Value::EnumVariant { enum_name: en1, variant: v1 }, Value::EnumVariant { enum_name: en2, variant: v2 }) => { en1 == en2 && v1 == v2 @@ -124,7 +128,8 @@ impl From for Value { match lit { Literal::String(s) => Value::String(s), Literal::Char(c) => Value::Char(c), - Literal::Number(n) => Value::Number(n), + Literal::Int(n) => Value::Int(n), + Literal::Float(n) => Value::Float(n), Literal::Bool(b) => Value::Bool(b), Literal::Null => Value::Null, } diff --git a/compiler/v1/src/lexer/lexer.rs b/compiler/v1/src/lexer/lexer.rs index c4a1f54..ab6f454 100644 --- a/compiler/v1/src/lexer/lexer.rs +++ b/compiler/v1/src/lexer/lexer.rs @@ -231,9 +231,8 @@ impl<'a> Lexer<'a> { } } - let value: f64 = number.parse().unwrap(); Ok(Token { - kind: TokenKind::NumberLiteral(value), + kind: TokenKind::NumberLiteral(number), span: Span::new(start, self.cursor.position()), }) } diff --git a/compiler/v1/src/lexer/token.rs b/compiler/v1/src/lexer/token.rs index 46b2a8e..b783a6a 100644 --- a/compiler/v1/src/lexer/token.rs +++ b/compiler/v1/src/lexer/token.rs @@ -72,7 +72,7 @@ pub enum TokenKind { Identifier(String), StringLiteral(String), CharLiteral(char), - NumberLiteral(f64), + NumberLiteral(String), //operators Equal, diff --git a/compiler/v1/src/main.rs b/compiler/v1/src/main.rs index 3cdd7d8..7bd1320 100644 --- a/compiler/v1/src/main.rs +++ b/compiler/v1/src/main.rs @@ -94,3 +94,61 @@ fn main() { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn runRey(rel: &str) -> Result<(), String> { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(rel); + let program = resolveEntry(&path).map_err(|e| format!("{}: {}", e.title, e.message))?; + let mut interpreter = Interpreter::new(); + interpreter + .interpret(&program.statements) + .map_err(|e| e.to_string()) + } + + #[test] + fn matchStructPatterns() { + runRey("src/tests/match_struct.rey").unwrap(); + } + + #[test] + fn matchEnumVariantsQualifiedAndUnqualified() { + runRey("src/tests/match_enum.rey").unwrap(); + } + + #[test] + fn importGroupedMissingSymbolPointsAtMissingName() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/imports/errors/group_missing_symbol.rey"); + let err = resolveEntry(&path).unwrap_err(); + let got = err + .source + .get(err.span.start..err.span.end) + .unwrap_or("") + .to_string(); + assert_eq!(got, "nope"); + } + + #[test] + fn importModuleMainCanImportLocalFile() { + runRey("../../tests/imports/success/nested_resolution.rey").unwrap(); + } + + #[test] + fn integerDivisionSemantics() { + runRey("src/tests/integer_division.rey").unwrap(); + } + + #[test] + fn structFieldMutationPubWorks() { + runRey("src/tests/struct_field_mutation.rey").unwrap(); + } + + #[test] + fn structFieldMutationNestedErrorsClearly() { + let err = runRey("src/tests/struct_field_mutation_nested_error.rey").unwrap_err(); + assert!(err.contains("nested field assignment"), "got: {}", err); + } +} diff --git a/compiler/v1/src/parser/parser.rs b/compiler/v1/src/parser/parser.rs index 9943a9a..f22ec2f 100644 --- a/compiler/v1/src/parser/parser.rs +++ b/compiler/v1/src/parser/parser.rs @@ -308,6 +308,7 @@ impl Parser { fn parseImportStatement(&mut self) -> Result { let import_span = self.previous().span; + use crate::ast::stmt::ImportName; let module = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), _ => return Err(self.error("Expected module or file name after 'import'.")), @@ -318,6 +319,7 @@ impl Parser { let symbols = if self.matchToken(&TokenKind::LeftBrace) { let mut values = Vec::new(); loop { + let span = self.peek().span; let name = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), _ => { @@ -327,7 +329,7 @@ impl Parser { } }; self.advance(); - values.push(name); + values.push(ImportName { name, span }); if !self.matchToken(&TokenKind::Comma) { break; } @@ -338,24 +340,26 @@ impl Parser { )?; values } else { + let span = self.peek().span; let symbol = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), _ => return Err(self.error("Expected symbol name after file import '.'.")), }; self.advance(); - vec![symbol] + vec![ImportName { name: symbol, span }] }; ImportKind::FileSymbols { module, symbols } } else if self.matchToken(&TokenKind::ColonColon) { let items = if self.matchToken(&TokenKind::LeftBrace) { let mut values = Vec::new(); loop { + let span = self.peek().span; let name = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), _ => return Err(self.error("Expected identifier in grouped module import list.")), }; self.advance(); - values.push(name); + values.push(ImportName { name, span }); if !self.matchToken(&TokenKind::Comma) { break; } @@ -366,12 +370,13 @@ impl Parser { )?; values } else { + let span = self.peek().span; let item = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), _ => return Err(self.error("Expected file name after '::' in module import.")), }; self.advance(); - vec![item] + vec![ImportName { name: item, span }] }; ImportKind::ModuleItems { module, items } } else { @@ -514,6 +519,38 @@ impl Parser { let name = name.clone(); self.advance(); + // Struct pattern: StructName { field: , ... } + if name + .chars() + .next() + .map(|c| c.is_uppercase()) + .unwrap_or(false) + && self.check(&TokenKind::LeftBrace) + { + self.advance(); // consume '{' + let mut fields = Vec::new(); + if !self.check(&TokenKind::RightBrace) { + loop { + let field_name = match &self.peek().kind { + TokenKind::Identifier(f) => f.clone(), + _ => return Err(self.error("Expected field name in struct pattern.")), + }; + self.advance(); + self.consume(&TokenKind::Colon, "Expected ':' after field name in struct pattern.")?; + let field_pat = self.parsePattern()?; + fields.push((field_name, field_pat)); + if !self.matchToken(&TokenKind::Comma) { + break; + } + } + } + self.consume(&TokenKind::RightBrace, "Expected '}' after struct pattern fields.")?; + return Ok(Pattern::Struct { + struct_name: name, + fields, + }); + } + // Check for Enum::Variant pattern if self.matchToken(&TokenKind::ColonColon) { let variant = match &self.peek().kind { @@ -529,9 +566,14 @@ impl Parser { } } TokenKind::NumberLiteral(n) => { - let val = *n; + let val = n.clone(); self.advance(); - Ok(Pattern::Literal(crate::ast::Literal::Number(val))) + let lit = if val.contains('.') { + crate::ast::Literal::Float(val.parse().unwrap()) + } else { + crate::ast::Literal::Int(val.parse().unwrap()) + }; + Ok(Pattern::Literal(lit)) } TokenKind::StringLiteral(s) => { let val = s.clone(); @@ -725,7 +767,7 @@ impl Parser { let expr = self.parseUnary()?; Ok(Expr::Binary { left: Box::new(Expr::Literal { - value: Literal::Number(0.0), + value: Literal::Int(0), span, }), op: TokenKind::Minus, @@ -787,11 +829,12 @@ impl Parser { if self.matchToken(&TokenKind::Dot) { let member_name = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), - TokenKind::NumberLiteral(n) => { + TokenKind::NumberLiteral(raw) => { + let n: f64 = raw.parse().unwrap_or(0.0); if n.fract() != 0.0 { return Err(self.error("Tuple index after '.' must be an integer.")); } - (*n as i64).to_string() + (n as i64).to_string() } _ => return Err(self.error("Expected identifier after '.'.")), }; @@ -959,10 +1002,18 @@ impl Parser { TokenKind::NumberLiteral(value) => { let span = self.peek().span; self.advance(); - Ok(Expr::Literal { - value: Literal::Number(value), - span, - }) + let is_float = value.contains('.'); + if is_float { + Ok(Expr::Literal { + value: Literal::Float(value.parse().unwrap()), + span, + }) + } else { + Ok(Expr::Literal { + value: Literal::Int(value.parse().unwrap()), + span, + }) + } } TokenKind::True => { let span = self.peek().span; diff --git a/compiler/v1/src/tests/integer_division.rey b/compiler/v1/src/tests/integer_division.rey new file mode 100644 index 0000000..591af8c --- /dev/null +++ b/compiler/v1/src/tests/integer_division.rey @@ -0,0 +1,16 @@ +func main(): Void { + var a = 10 / 3; + if (a != 3) { + var fail = 1 / 0; + } + + var b = 10.0 / 3.0; + if (b <= 3) { + var fail = 1 / 0; + } + + var c = 10 / 3.0; + if (c <= 3) { + var fail = 1 / 0; + } +} diff --git a/compiler/v1/src/tests/match_enum.rey b/compiler/v1/src/tests/match_enum.rey new file mode 100644 index 0000000..a93bc21 --- /dev/null +++ b/compiler/v1/src/tests/match_enum.rey @@ -0,0 +1,27 @@ +enum Direction { + North, + South, + East, + West, + Up, +} + +func run(): Void { + var d = North; + var out = 0; + + match d { + Direction::North => { out = 1; }, + Direction::South => { out = 2; }, + East => { out = 3; }, + West => { out = 4; }, + Up => { out = 5; }, + _ => { out = 99; }, + } + + if (out != 1) { + var fail = 1 / 0; + } +} + +run(); diff --git a/compiler/v1/src/tests/match_struct.rey b/compiler/v1/src/tests/match_struct.rey new file mode 100644 index 0000000..2249141 --- /dev/null +++ b/compiler/v1/src/tests/match_struct.rey @@ -0,0 +1,21 @@ +struct Point { + x: int, + y: int, +} + +func run(): Void { + var p = Point { x: 1, y: 2 }; + var ok = false; + + match p { + Point { x: 1, y: 2 } => { ok = true; }, + Point { x: 1, y: 3 } => { ok = false; }, + _ => { ok = false; }, + } + + if (!ok) { + var fail = 1 / 0; + } +} + +run(); diff --git a/compiler/v1/src/tests/struct_field_mutation.rey b/compiler/v1/src/tests/struct_field_mutation.rey new file mode 100644 index 0000000..e1655c4 --- /dev/null +++ b/compiler/v1/src/tests/struct_field_mutation.rey @@ -0,0 +1,13 @@ +struct Counter { + pub value: int, +} + +func main(): Void { + var c = Counter { value: 1 }; + c.value = 2; + c.value += 3; + + if (c.value != 5) { + var fail = 1 / 0; + } +} diff --git a/compiler/v1/src/tests/struct_field_mutation_nested_error.rey b/compiler/v1/src/tests/struct_field_mutation_nested_error.rey new file mode 100644 index 0000000..04907fe --- /dev/null +++ b/compiler/v1/src/tests/struct_field_mutation_nested_error.rey @@ -0,0 +1,12 @@ +struct Inner { + pub value: int, +} + +struct Outer { + pub inner: Inner, +} + +func main(): Void { + var o = Outer { inner: Inner { value: 1 } }; + o.inner.value = 2; +} diff --git a/compiler/v1/src/typecheck.rs b/compiler/v1/src/typecheck.rs index e32ae81..691a3f2 100644 --- a/compiler/v1/src/typecheck.rs +++ b/compiler/v1/src/typecheck.rs @@ -555,6 +555,17 @@ impl TypeChecker { // Typecheck each arm's pattern against the expression type for arm in arms { use crate::ast::stmt::Pattern; + fn collectBindings(p: &Pattern, out: &mut Vec) { + match p { + Pattern::Variable(name) => out.push(name.clone()), + Pattern::Struct { fields, .. } => { + for (_, fp) in fields { + collectBindings(fp, out); + } + } + _ => {} + } + } match &arm.pattern { Pattern::Wildcard => {} Pattern::Variable(_) => {} @@ -562,7 +573,8 @@ impl TypeChecker { let lit_ty = match lit { Literal::String(_) => Ty::String, Literal::Char(_) => Ty::Char, - Literal::Number(n) => if n.fract() == 0.0 { Ty::Int } else { Ty::Float }, + Literal::Int(_) => Ty::Int, + Literal::Float(_) => Ty::Float, Literal::Bool(_) => Ty::Bool, Literal::Null => Ty::Null, }; @@ -576,11 +588,16 @@ impl TypeChecker { Pattern::EnumVariant(_, _) => { // Enum patterns are always valid for now } + Pattern::Struct { .. } => { + // Struct patterns are accepted for now (field checks happen at runtime). + } } // Typecheck the arm body self.pushScope(); - if let Pattern::Variable(var_name) = &arm.pattern { - self.define(var_name, expr_ty.clone(), false); + let mut bindings = Vec::new(); + collectBindings(&arm.pattern, &mut bindings); + for name in bindings { + self.define(&name, Ty::Any, false); } for s in &arm.body { self.checkStmt(s)?; @@ -597,13 +614,8 @@ impl TypeChecker { Expr::Literal { value: lit, .. } => Ok(match lit { Literal::String(_) => Ty::String, Literal::Char(_) => Ty::Char, - Literal::Number(n) => { - if n.fract() == 0.0 { - Ty::Int - } else { - Ty::Float - } - } + Literal::Int(_) => Ty::Int, + Literal::Float(_) => Ty::Float, Literal::Bool(_) => Ty::Bool, Literal::Null => Ty::Null, }), diff --git a/primer.md b/primer.md index 59b23d3..9fe7bd2 100644 --- a/primer.md +++ b/primer.md @@ -1,48 +1,36 @@ # Primer — rey-lang -Last updated: Mar 23, 2026 (session end) +Last updated: Mar 26, 2026 (session end) ## Session objective -v0.1.0 release prep from pre-release state. +Ship v0.1.1 patches (match/imports/eval) and add `rey-compiler/` bootstrap skeleton. ## What was done -- Completed syntax audit against current parser/runtime behavior. -- Read and audited all files under `compiler/v1/src/` and `languages/samples/Rey.rey`. -- `examples/` directory is not present in this repo; example-style runtime checks were executed through `compiler/v1/src/tests/` and `languages/samples/Rey.rey`. -- Rewrote `syntax.md` to match implemented behavior, including: - - function visibility (`func`, `pub func`, `export pub func`) - - file/module import syntax and resolver rules - - current struct/static-method behavior - - actual implemented operators/types/control-flow/forms - - removed outdated claims -- Code cleanup: - - removed warning sources (unused imports/vars, dead method, unnecessary mut) - - fixed parser static-call bug (`StructName.create(...)`) - - fixed `module::item` parser regression in import parsing -- Fixture updates: - - updated `compiler/v1/src/tests/test_rand.rey` to pass under current type checking -- Verification: - - `cargo build` passes cleanly with zero warnings - - `cargo test` passes - - all `compiler/v1/src/tests/*.rey` run successfully (with scripted input for `io.rey`) - - `languages/samples/Rey.rey` runs successfully - - import fixtures validated (`tests/imports/success` and error cases) -- Release prep assets: - - added root `RELEASE.md` (v0.0.1-pre -> v0.1.0) - - bumped `compiler/v1/Cargo.toml` version to `0.1.0` - - updated version references in `README.md` - - built release binary and packaged: - - `releases/0.1.0/rey-v0-macos-arm64` - - `releases/0.1.0/RELEASE.md` -- Updated `CHANGELOG.md` and refreshed `CLAUDE.md` for current v0.1.0 context. +- Match fixes: + - Added struct patterns (`StructName { field: pattern }`) and fixed matching on struct instances. + - Fixed enum matching for both qualified (`Enum::Variant`) and unqualified (`Variant`) patterns. + - Added `.rey` regression programs plus a small `cargo test` harness to execute them. +- Import fixes: + - Grouped imports now track per-item spans so missing names highlight the specific item. + - Added fixtures and a `cargo test` check for grouped-missing-symbol. + - Added fixture and test for module `main.rey` importing a sibling file (nested resolution). +- Evaluator/runtime fixes: + - Numbers now preserve `int` vs `float` at runtime (lexer -> AST literal -> `Value`), fixing integer division and mixed int/float division semantics. + - External mutation of `pub` struct fields works (`obj.field = ...`, `obj.field += ...`). + - Nested field assignment (e.g. `obj.inner.field = ...`) now errors clearly. + - Added `.rey` regression programs for division and field mutation. +- `rey-compiler/`: + - Added API-only bootstrap skeleton with the requested file layout and public signatures. +- Docs: + - Added `compiler/README.md` to document v1 vs bootstrap. + - Updated `syntax.md` for v0.1.1 behavior deltas. +- Versioning: + - Bumped `compiler/v1/Cargo.toml` to `0.1.1`. + - Added a `CHANGELOG.md` entry for 2026-03-26. ## Current state -- Working tree contains v0.1.0 release-prep changes ready to commit. -- Compiler builds/tests cleanly. -- Release notes and packaged binary for `0.1.0` are staged in repo paths. +- v0.1.1 patch series is complete on `codex` and validated via `cargo test`. +- `rey-compiler/` skeleton exists (API only). ## Next steps after this session -- Commit release prep changes. -- Push contributor branch and open release PR. -- Optional follow-up for v0.2.0 planning: - - generics design - - closure/runtime ergonomics improvements +- Merge PR for v0.1.1. +- Decide next v0.2.0 compiler/runtime milestone list (match-as-expression, type system, better stdlib). diff --git a/rey-compiler/README.md b/rey-compiler/README.md new file mode 100644 index 0000000..5718f57 --- /dev/null +++ b/rey-compiler/README.md @@ -0,0 +1,19 @@ +# rey-compiler (bootstrap, v5.0) + +This folder is the start of a compiler written in Rey, intended to eventually +compile Rey itself (a "Rey-in-Rey" bootstrap compiler). + +Right now this is **API surface only**: it defines the core structs/enums and +the public function signatures for the lexer, parser, typechecker, codegen, and +diagnostics layers. Implementations will come in later milestones once the v1 +Rust interpreter is stable enough to host and iterate on the bootstrap. + +How this fits the roadmap: +- `compiler/v1/` is the current Rust interpreter (the shipping compiler/runtime today). +- `rey-compiler/` is the long-term self-hosted compiler target (v5.0 bootstrap). + +Status: +- Types: defined +- Public APIs: defined +- Implementations: not started + diff --git a/rey-compiler/main.rey b/rey-compiler/main.rey new file mode 100644 index 0000000..69f5432 --- /dev/null +++ b/rey-compiler/main.rey @@ -0,0 +1,46 @@ +// v5 bootstrap entrypoints (api only) + +enum Target { + Vm, + Native, + Js, +} + +struct CompileOptions { + pub target: Target, + pub optimize: bool, + pub emitTokens: bool, + pub emitAst: bool, + pub emitTypedAst: bool, + pub emitIr: bool, +} + +enum ArtifactKind { + Tokens, + Ast, + TypedAst, + Ir, + Bytecode, + Text, +} + +struct Artifact { + pub kind: ArtifactKind, + pub path: String, + pub text: String, +} + +struct CompileResult { + pub ok: bool, + pub diagnostics: [Any], + pub artifacts: [Artifact], +} + +export pub func compileFile(entryPath: String, options: CompileOptions): CompileResult { + return null; +} + +export pub func compileSource(source: String, options: CompileOptions): CompileResult { + return null; +} + diff --git a/rey-compiler/src/codegen/main.rey b/rey-compiler/src/codegen/main.rey new file mode 100644 index 0000000..918a3bb --- /dev/null +++ b/rey-compiler/src/codegen/main.rey @@ -0,0 +1,40 @@ +enum IrKind { + Module, + Function, + Block, + Instr, +} + +struct IrNode { + pub kind: IrKind, + pub tag: String, + pub data: {String: Any}, +} + +enum Backend { + Bytecode, + C, + Llvm, + Js, +} + +struct CodegenOptions { + pub backend: Backend, + pub optimize: bool, + pub debug: bool, +} + +struct CodegenResult { + pub ok: bool, + pub ir: [IrNode], + pub diagnostics: [Any], +} + +export pub func lowerToIr(typedProgram: Any): CodegenResult { + return null; +} + +export pub func emit(typedProgram: Any, options: CodegenOptions): Any { + return null; +} + diff --git a/rey-compiler/src/diagnostics/main.rey b/rey-compiler/src/diagnostics/main.rey new file mode 100644 index 0000000..191d5db --- /dev/null +++ b/rey-compiler/src/diagnostics/main.rey @@ -0,0 +1,40 @@ +enum Severity { + Error, + Warning, + Note, +} + +struct Span { + pub start: int, + pub end: int, + pub line: int, + pub column: int, +} + +struct Diagnostic { + pub severity: Severity, + pub code: String, + pub message: String, + pub filePath: String, + pub span: Span, +} + +struct DiagnosticBag { + pub items: [Diagnostic], +} + +export pub func newBag(): DiagnosticBag { + return null; +} + +export pub func push(bag: DiagnosticBag, diag: Diagnostic): Void { +} + +export pub func hasErrors(bag: DiagnosticBag): bool { + return null; +} + +export pub func formatDiagnostic(diag: Diagnostic): String { + return null; +} + diff --git a/rey-compiler/src/lexer/main.rey b/rey-compiler/src/lexer/main.rey new file mode 100644 index 0000000..7f66e1f --- /dev/null +++ b/rey-compiler/src/lexer/main.rey @@ -0,0 +1,19 @@ +struct Lexer { + pub source: String, + pub pos: int, + pub line: int, + pub column: int, +} + +export pub func newLexer(source: String): Lexer { + return null; +} + +export pub func nextToken(lexer: Lexer): Any { + return null; +} + +export pub func lexAll(source: String): [Any] { + return null; +} + diff --git a/rey-compiler/src/lexer/token.rey b/rey-compiler/src/lexer/token.rey new file mode 100644 index 0000000..3b250a4 --- /dev/null +++ b/rey-compiler/src/lexer/token.rey @@ -0,0 +1,87 @@ +enum TokenKind { + Identifier, + StringLiteral, + CharLiteral, + NumberLiteral, + + // keywords + Var, + Const, + Func, + Struct, + Enum, + Import, + Export, + Pub, + Return, + If, + Else, + While, + For, + In, + Break, + Continue, + Match, + InstanceOf, + True, + False, + Null, + + // punctuation/operators + LeftParen, + RightParen, + LeftBrace, + RightBrace, + LeftBracket, + RightBracket, + Comma, + Dot, + Semicolon, + Colon, + ColonColon, + Arrow, + Equal, + EqualEqual, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, + Plus, + Minus, + Star, + Slash, + Percent, + AndAnd, + OrOr, + Not, + + Eof, +} + +struct Span { + pub start: int, + pub end: int, + pub line: int, + pub column: int, +} + +struct Token { + pub kind: TokenKind, + pub lexeme: String, + pub span: Span, +} + +enum LexErrorKind { + UnexpectedCharacter, + UnterminatedString, + UnterminatedChar, + InvalidNumber, +} + +struct LexError { + pub kind: LexErrorKind, + pub message: String, + pub span: Span, +} + diff --git a/rey-compiler/src/parser/ast.rey b/rey-compiler/src/parser/ast.rey new file mode 100644 index 0000000..d30155a --- /dev/null +++ b/rey-compiler/src/parser/ast.rey @@ -0,0 +1,42 @@ +// A minimal tagged-struct AST representation (api only). + +enum NodeKind { + Program, + Stmt, + Expr, + Type, + Pattern, +} + +struct Span { + pub start: int, + pub end: int, + pub line: int, + pub column: int, +} + +struct Node { + pub kind: NodeKind, + pub tag: String, + pub span: Span, + pub data: {String: Any}, +} + +enum LiteralKind { + String, + Char, + Int, + Float, + Bool, + Null, +} + +struct Literal { + pub kind: LiteralKind, + pub text: String, +} + +struct Program { + pub statements: [Node], +} + diff --git a/rey-compiler/src/parser/main.rey b/rey-compiler/src/parser/main.rey new file mode 100644 index 0000000..e6b4411 --- /dev/null +++ b/rey-compiler/src/parser/main.rey @@ -0,0 +1,31 @@ +struct ParserOptions { + pub allowTrailingCommas: bool, + pub allowImplicitSemicolons: bool, +} + +struct Parser { + pub tokens: [Any], + pub current: int, + pub options: ParserOptions, +} + +enum ParseErrorKind { + UnexpectedToken, + ExpectedToken, + UnexpectedEof, +} + +struct ParseError { + pub kind: ParseErrorKind, + pub message: String, + pub span: Any, +} + +export pub func newParser(tokens: [Any], options: ParserOptions): Parser { + return null; +} + +export pub func parseProgram(p: Parser): Any { + return null; +} + diff --git a/rey-compiler/src/typecheck/main.rey b/rey-compiler/src/typecheck/main.rey new file mode 100644 index 0000000..697dcbc --- /dev/null +++ b/rey-compiler/src/typecheck/main.rey @@ -0,0 +1,56 @@ +enum TypeKind { + Any, + Void, + Bool, + Int, + Float, + String, + Char, + Array, + Dict, + Tuple, + Struct, + Enum, + Function, + Union, +} + +struct Type { + pub kind: TypeKind, + pub name: String, + pub params: [Type], +} + +struct Symbol { + pub name: String, + pub ty: Type, + pub isConst: bool, +} + +struct TypeEnv { + pub symbols: {String: Symbol}, +} + +enum TypeErrorKind { + UndefinedName, + NotCallable, + NotAssignable, + Mismatch, +} + +struct TypeError { + pub kind: TypeErrorKind, + pub message: String, + pub span: Any, +} + +struct TypecheckResult { + pub ok: bool, + pub typedProgram: Any, + pub diagnostics: [Any], +} + +export pub func typecheck(program: Any): TypecheckResult { + return null; +} + diff --git a/syntax.md b/syntax.md index 4515046..6c61e6c 100644 --- a/syntax.md +++ b/syntax.md @@ -1,4 +1,4 @@ -# Rey Language Syntax Reference (v0.1.0) +# Rey Language Syntax Reference (v0.1.1) This document reflects the behavior currently implemented in `compiler/v1`. @@ -24,6 +24,9 @@ var y: int = 20; const pi: float = 3.14; ``` +- `var` declares a mutable variable +- `const` declares an immutable variable (cannot be reassigned) + Implemented primitive type names: - `int` - `uint` @@ -42,6 +45,11 @@ Implemented type forms: - Dictionary: `{String:int}` - Union: `int | String` +Comments: +```rey +// single-line comment +``` + Tuples are supported as literals and index access: ```rey @@ -55,6 +63,10 @@ println(t.2); Arithmetic: - `+`, `-`, `*`, `/`, `%` +Division rules: +- `int / int` performs integer division (truncates) +- any `float` operand produces a `float` result (`10.0 / 3.0`, `10 / 3.0`) + Comparison: - `==`, `!=`, `<`, `<=`, `>`, `>=` @@ -182,6 +194,9 @@ println(pop(xs)); println(xs.length()); ``` +Array methods: +- `length()` + Dictionaries (identifier or string keys in literals): ```rey @@ -191,6 +206,9 @@ println(user["id"]); user.name = "ReyLang"; ``` +Dictionary methods: +- `length()` + ## Strings Regular and multiline strings: @@ -202,6 +220,11 @@ line 2 """; ``` +Char literals: +```rey +var c: char = 'x'; +``` + Interpolation: ```rey @@ -244,6 +267,11 @@ Struct literal: var p = Player { name: "Hero", health: 100 }; ``` +Field mutation: +- `obj.field = value` works for `pub` struct fields +- `obj.field += value` works for `pub` struct fields +- nested field assignment like `obj.inner.field = value` is currently rejected + Implemented method behavior: - Instance method calls inject fields into method scope by field name. - Mutated field names are written back to the instance. @@ -273,7 +301,8 @@ match dir { ``` Pattern kinds: -- enum variant (`Type::Variant`) +- enum variant (`Type::Variant` or unqualified `Variant`) +- struct pattern (`StructName { field: pattern, ... }`) - literal (`1`, `"x"`, `true`, `null`) - variable binding (`n`) - wildcard (`_`) @@ -283,13 +312,13 @@ Global built-ins: - `print(...)` - `println(...)` - `input()` / `input(promptString)` -- `len(value)` +- `len(value)` — works on strings, arrays, dictionaries - `push(array, value)` - `pop(array)` - `abs(number)` -- `max(a, b)` -- `min(a, b)` -- `random()` +- `max(a, b)` — two numbers +- `min(a, b)` — two numbers +- `random()` — returns float in [0, 1) ## Diagnostics Compiler and runtime errors are printed with category labels such as: diff --git a/tests/imports/errors/group_missing_symbol.rey b/tests/imports/errors/group_missing_symbol.rey new file mode 100644 index 0000000..d61b296 --- /dev/null +++ b/tests/imports/errors/group_missing_symbol.rey @@ -0,0 +1,3 @@ +import actuator.{name, nope}; + +println("should-not-run"); diff --git a/tests/imports/success/nested_resolution.rey b/tests/imports/success/nested_resolution.rey new file mode 100644 index 0000000..d939ec5 --- /dev/null +++ b/tests/imports/success/nested_resolution.rey @@ -0,0 +1,8 @@ +import nestedmod; + +func main(): Void { + var v = nestedmod.entry(); + if (v != "ok") { + var fail = 1 / 0; + } +} diff --git a/tests/imports/success/nestedmod/helper.rey b/tests/imports/success/nestedmod/helper.rey new file mode 100644 index 0000000..457eeba --- /dev/null +++ b/tests/imports/success/nestedmod/helper.rey @@ -0,0 +1,3 @@ +export pub func foo() { + return "ok"; +} diff --git a/tests/imports/success/nestedmod/main.rey b/tests/imports/success/nestedmod/main.rey new file mode 100644 index 0000000..36a142c --- /dev/null +++ b/tests/imports/success/nestedmod/main.rey @@ -0,0 +1,5 @@ +import helper.foo; + +export pub func entry() { + return foo(); +}