diff --git a/Cargo.lock b/Cargo.lock index 216be5c..5fd68de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "compiler" version = "0.1.0" @@ -38,9 +44,53 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objects" version = "0.1.0" +dependencies = [ + "num-bigint", + "num-rational", + "num-traits", +] [[package]] name = "peg" @@ -114,7 +164,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn", ] [[package]] diff --git a/crates/compiler/src/analysis.rs b/crates/compiler/src/analysis.rs index d17bc7a..9dea769 100644 --- a/crates/compiler/src/analysis.rs +++ b/crates/compiler/src/analysis.rs @@ -167,6 +167,16 @@ pub fn analyze(root: &AstRoot, options: &CompileOptions) -> Result { root_scope.expression(expression, &globals)? } + StmtKind::If(_) | StmtKind::While(_) => { + let statements = std::slice::from_ref(statement); + collect_locals( + statements, + &mut declared, + &mut root_scope.scope.layout.names, + )?; + globals.extend(root_scope.scope.layout.names.iter().cloned()); + root_scope.statements(statements, &globals)?; + } StmtKind::Return(_) => { return Err(error(statement.span, "return outside a function")); } @@ -207,21 +217,38 @@ fn function_scope(function: &Function, globals: &BTreeSet) -> Result, + layout: &mut Vec, +) -> Result<(), Diagnostic> { + for statement in statements { match &statement.kind { - StmtKind::Binding(binding) => { - declare(&mut locals, &binding.name.text, binding.name.span)?; - builder.scope.layout.locals.push(binding.name.text.clone()); + StmtKind::Binding(b) => { + declare(locals, &b.name.text, b.name.span)?; + layout.push(b.name.text.clone()); } - StmtKind::Assignment(assignment) => { - if locals.insert(assignment.name.text.clone()) { - builder - .scope - .layout - .locals - .push(assignment.name.text.clone()); + StmtKind::Assignment(a) => { + if locals.insert(a.name.text.clone()) { + layout.push(a.name.text.clone()); } } + StmtKind::If(b) => { + collect_locals(&b.body.statements, locals, layout)?; + if let Some(other) = &b.otherwise { + collect_locals(&other.statements, locals, layout)?; + } + } + StmtKind::While(b) => collect_locals(&b.body.statements, locals, layout)?, StmtKind::Declaration(_) => { return Err(error( statement.span, @@ -231,39 +258,7 @@ fn function_scope(function: &Function, globals: &BTreeSet) -> Result {} } } - let mut returned = false; - for statement in &function.body.statements { - if returned { - return Err(error( - statement.span, - "statement after return is unreachable", - )); - } - match &statement.kind { - StmtKind::Binding(binding) => { - builder.expression(&binding.value, globals)?; - builder.initialized.insert(binding.name.text.clone()); - } - StmtKind::Assignment(assignment) => { - builder.expression(&assignment.value, globals)?; - builder.initialized.insert(assignment.name.text.clone()); - } - StmtKind::Return(statement) => { - if let Some(value) = &statement.value { - builder.expression(value, globals)?; - } - returned = true; - } - StmtKind::MemberAssignment(assignment) => { - builder.expression(&assignment.object, globals)?; - builder.expression(&assignment.value, globals)?; - builder.intern(&assignment.name.text); - } - StmtKind::Expression(expression) => builder.expression(expression, globals)?, - StmtKind::Declaration(_) => unreachable!("nested declaration rejected above"), - } - } - Ok(builder.scope) + Ok(()) } struct Builder { @@ -300,6 +295,88 @@ impl Builder { } } + /// Function-scoped locals; branches merge definite initialization. A loop + /// may execute zero times, so its writes do not initialize later reads. + fn statements( + &mut self, + statements: &[frontend::ast::Stmt], + globals: &BTreeSet, + ) -> Result { + let mut returned = false; + for statement in statements { + if returned { + return Err(error( + statement.span, + "statement after return is unreachable", + )); + } + match &statement.kind { + StmtKind::Binding(b) => { + self.expression(&b.value, globals)?; + self.initialized.insert(b.name.text.clone()); + if self.scope.kind != ScopeKind::Function { + self.intern(&b.name.text); + } + } + StmtKind::Assignment(a) => { + self.expression(&a.value, globals)?; + self.initialized.insert(a.name.text.clone()); + if self.scope.kind != ScopeKind::Function { + self.intern(&a.name.text); + } + } + StmtKind::MemberAssignment(a) => { + self.expression(&a.object, globals)?; + self.expression(&a.value, globals)?; + self.intern(&a.name.text); + } + StmtKind::Expression(e) => self.expression(e, globals)?, + StmtKind::Return(r) => { + if self.scope.kind != ScopeKind::Function { + return Err(error(statement.span, "return outside a function")); + } + if let Some(e) = &r.value { + self.expression(e, globals)?; + } + returned = true; + } + StmtKind::If(b) => { + self.expression(&b.condition, globals)?; + let before = self.initialized.clone(); + let yes_return = self.statements(&b.body.statements, globals)?; + let yes = self.initialized.clone(); + self.initialized = before; + let no_return = if let Some(other) = &b.otherwise { + self.statements(&other.statements, globals)? + } else { + false + }; + self.initialized = if yes_return { + self.initialized.clone() + } else if no_return { + yes + } else { + yes.intersection(&self.initialized).cloned().collect() + }; + returned = yes_return && no_return; + } + StmtKind::While(b) => { + self.expression(&b.condition, globals)?; + let before = self.initialized.clone(); + self.statements(&b.body.statements, globals)?; + self.initialized = before; + } + StmtKind::Declaration(_) => { + return Err(error( + statement.span, + "nested declarations are not compiled yet", + )); + } + } + } + Ok(returned) + } + fn expression( &mut self, expression: &Expr, diff --git a/crates/compiler/src/codegen.rs b/crates/compiler/src/codegen.rs index cb44739..215c762 100644 --- a/crates/compiler/src/codegen.rs +++ b/crates/compiler/src/codegen.rs @@ -2,7 +2,7 @@ use endor_core::{ CodeLayout, DecodedInstruction, EndorCode, EndorNumber, Instruction, ObjectRef, Opcode, Value, Vm, VmError, }; -use frontend::ast::{AstRoot, Decl, DeclKind, Expr, ExprKind, Stmt, StmtKind, UnaryOp}; +use frontend::ast::{AstRoot, BinaryOp, Decl, DeclKind, Expr, ExprKind, Stmt, StmtKind, UnaryOp}; use frontend::{Diagnostic, DiagnosticKind, SourceUnit, Span}; use crate::{NameBinding, Scope, ScopeKind, SymbolTable, diagnostic}; @@ -115,6 +115,7 @@ struct Emitter<'a> { source: &'a SourceUnit, instructions: Vec, constants: Vec, + jumps: Vec<(usize, usize)>, children: Vec<(usize, CompiledCode)>, } @@ -125,6 +126,7 @@ impl<'a> Emitter<'a> { source, instructions: Vec::new(), constants: Vec::new(), + jumps: Vec::new(), children: Vec::new(), } } @@ -249,6 +251,36 @@ impl<'a> Emitter<'a> { fn statement(&mut self, statement: &Stmt, symbols: &SymbolTable) -> Result<(), Diagnostic> { match &statement.kind { + StmtKind::If(branch) => { + self.expression(&branch.condition)?; + let conditional = self.instructions.len(); + self.emit(Opcode::PopJumpIfFalse, 0)?; + for statement in &branch.body.statements { + self.statement(statement, symbols)?; + } + let end = self.instructions.len(); + self.emit(Opcode::JumpForward, 0)?; + self.jumps.push((conditional, self.instructions.len())); + if let Some(otherwise) = &branch.otherwise { + for statement in &otherwise.statements { + self.statement(statement, symbols)?; + } + } + self.jumps.push((end, self.instructions.len())); + } + StmtKind::While(loop_) => { + let start = self.instructions.len(); + self.expression(&loop_.condition)?; + let exit = self.instructions.len(); + self.emit(Opcode::PopJumpIfFalse, 0)?; + for statement in &loop_.body.statements { + self.statement(statement, symbols)?; + } + let back = self.instructions.len(); + self.emit(Opcode::JumpBackward, 0)?; + self.jumps.push((back, start)); + self.jumps.push((exit, self.instructions.len())); + } StmtKind::Binding(binding) => self.store_binding(&binding.name.text, &binding.value)?, StmtKind::Assignment(assignment) => { self.store_binding(&assignment.name.text, &assignment.value)? @@ -308,22 +340,38 @@ impl<'a> Emitter<'a> { self.expression(object)?; self.emit(Opcode::LoadAttr, self.name_index(&name.text))?; } - ExprKind::Unary { .. } => { - // Literal sign folding preserves arbitrarily large integers - // and decimal spelling; this is not general numeric arithmetic. - let Some(spelling) = signed_literal(expression, false) else { - return Err(error( - expression.span, - "numeric unary operations are not implemented; only signed number literals are supported", - )); - }; - self.number(&spelling, expression.span)?; + ExprKind::Unary { operand, .. } => { + if let Some(spelling) = signed_literal(expression, false) { + self.number(&spelling, expression.span)?; + } else { + self.expression(operand)?; + self.simple(Opcode::UnaryNegate); + } } - ExprKind::Binary { .. } => { - return Err(error( - expression.span, - "numeric arithmetic bytecode is not implemented yet", - )); + ExprKind::Binary { + operation, + left, + right, + } => { + self.expression(left)?; + self.expression(right)?; + match operation { + BinaryOp::Add => self.emit(Opcode::BinaryArithmetic, 0)?, + BinaryOp::Subtract => self.emit(Opcode::BinaryArithmetic, 1)?, + BinaryOp::Multiply => self.emit(Opcode::BinaryArithmetic, 2)?, + BinaryOp::Divide => self.emit(Opcode::BinaryArithmetic, 3)?, + _ => self + .instructions + .push(Instruction::compare(match operation { + BinaryOp::Equal => endor_core::CompareOp::Eq, + BinaryOp::NotEqual => endor_core::CompareOp::Ne, + BinaryOp::Less => endor_core::CompareOp::Lt, + BinaryOp::LessEqual => endor_core::CompareOp::Le, + BinaryOp::Greater => endor_core::CompareOp::Gt, + BinaryOp::GreaterEqual => endor_core::CompareOp::Ge, + _ => unreachable!(), + })), + } } ExprKind::Symbol(_) => { return Err(error( @@ -345,7 +393,42 @@ impl<'a> Emitter<'a> { self.constant(Value::Number(value)) } - fn finish(self) -> Result { + fn finish(mut self) -> Result { + // Labels refer to logical instructions. Resolve relative code-unit + // offsets after EXTENDED_ARG expansion, to a fixed point. + for attempt in 0..64 { + let mut offsets = vec![0_usize]; + for i in &self.instructions { + let units = if i.argument <= 0xff { + 1 + } else if i.argument <= 0xffff { + 2 + } else if i.argument <= 0xffffff { + 3 + } else { + 4 + }; + offsets.push(offsets.last().unwrap() + units); + } + let mut changed = false; + for &(index, target) in &self.jumps { + let distance = if self.instructions[index].opcode == Opcode::JumpBackward { + offsets[index + 1] - offsets[target] + } else { + offsets[target] - offsets[index + 1] + }; + let operand = u32::try_from(distance) + .map_err(|_| error(self.scope.span, "jump exceeds u32"))?; + changed |= self.instructions[index].argument != operand; + self.instructions[index].argument = operand; + } + if !changed { + break; + } + if attempt == 63 { + return Err(error(self.scope.span, "jump layout did not converge")); + } + } let line = self .scope .span diff --git a/crates/compiler/tests/classes.rs b/crates/compiler/tests/classes.rs index a124527..71e12e4 100644 --- a/crates/compiler/tests/classes.rs +++ b/crates/compiler/tests/classes.rs @@ -262,11 +262,22 @@ fn declared_field_checks_cover_construction_inheritance_writes_and_gc() { r#" class Base { count: Number } class Child: Base { label: String } -value = Child(9, "ok") class Holder { child: Base } -holder = Holder(value) class Unknown { value: Missing } "#, + ParseMode::Module, + ); + // Interactive intentionally accepts one complete statement per source unit. + execute( + &mut vm, + globals, + r#"value = Child(9, "ok")"#, + ParseMode::Interactive, + ); + execute( + &mut vm, + globals, + "holder = Holder(value)", ParseMode::Interactive, ); let Value::Object(value) = vm @@ -334,7 +345,7 @@ fn checked_fields_keep_declaring_self_type_and_can_be_opted_out() { class Link { next: Link } class Seed: Link {} "#, - ParseMode::Interactive, + ParseMode::Module, ); // A recursive nonnullable field can be initialized by the trusted host in two steps. let Value::Object(link) = vm diff --git a/crates/compiler/tests/pipeline.rs b/crates/compiler/tests/pipeline.rs index 98d98c8..ab644a2 100644 --- a/crates/compiler/tests/pipeline.rs +++ b/crates/compiler/tests/pipeline.rs @@ -345,18 +345,10 @@ fn extended_operands_work_for_large_local_and_constant_tables() { #[test] fn unsupported_execution_features_are_compile_errors_instead_of_fake_results() { - for (text, mode) in [ - ("1 + 2", ParseMode::Expression), - (".title", ParseMode::Expression), - ( - "function f(value: Number): Number { return -value }", - ParseMode::Module, - ), - ] { - let source = EndorReader::from_text("", text); - let error = compile_source(&source, mode, &CompileOptions::default()).unwrap_err(); - assert_eq!(error.diagnostic.kind, DiagnosticKind::Compilation); - } + let source = EndorReader::from_text("", ".title"); + let error = + compile_source(&source, ParseMode::Expression, &CompileOptions::default()).unwrap_err(); + assert_eq!(error.diagnostic.kind, DiagnosticKind::Compilation); } #[test] @@ -482,3 +474,79 @@ fn source_signatures_survive_template_load_and_enable_runtime_checks() { assert_eq!(vm.object_count(), baseline); cleanup(&mut vm, code, globals); } + +#[test] +fn source_control_flow_and_exact_arithmetic_execute_real_bytecode() { + let mut vm = Vm::new().unwrap(); + let text = "function main(): Number { i=0 total=0 while i<20 { if i<10 {total=total+i} else {total=total-i} i=i+1 } return total }"; + let (code, globals) = module(&mut vm, &compile(text, ParseMode::Module)); + let main = function(&vm, globals, "main"); + let result = vm.call(main, &[]).unwrap(); + assert_eq!(result, Value::Number((-100_i64).into())); + vm.release_value(result).unwrap(); + cleanup(&mut vm, code, globals); + for (text, expected) in [ + ("0.1 + 0.2 == 0.3", Value::Bool(true)), + ( + "100000000000000000000 + 1 > 100000000000000000000", + Value::Bool(true), + ), + ("1 / 3 + 1 / 3 + 1 / 3", Value::Number(1_i64.into())), + ("-(2 + 3)", Value::Number((-5_i64).into())), + ] { + let code = compile(text, ParseMode::Expression).load(&mut vm).unwrap(); + let globals = vm.new_dict().unwrap(); + let result = vm.execute(code, globals).unwrap(); + assert_eq!(result, expected); + vm.release_value(result).unwrap(); + vm.release(code).unwrap(); + vm.release(globals).unwrap(); + } +} + +#[test] +fn control_flow_definite_initialization_large_jumps_and_loop_budgets() { + let mut body = String::from("function main(): Number { i=0 while i<2 { if i==0 {"); + for _ in 0..300 { + body.push_str("i=i+0 "); + } + body.push_str("} i=i+1 } return i }"); + let mut vm = Vm::new().unwrap(); + let (code, globals) = module(&mut vm, &compile(&body, ParseMode::Module)); + let result = vm.call(function(&vm, globals, "main"), &[]).unwrap(); + assert_eq!(result, Value::Number(2_i64.into())); + vm.release_value(result).unwrap(); + cleanup(&mut vm, code, globals); + for text in [ + "function f(): Number { if 1 {x=2} return x }", + "function f(): Number {while 0 {x=2} return x}", + ] { + assert!( + compile_source( + &EndorReader::from_text("", text), + ParseMode::Module, + &CompileOptions::default() + ) + .is_err() + ); + } + let (code, globals) = module( + &mut vm, + &compile( + "function main(): Unit { while 1 { value=2 } }", + ParseMode::Module, + ), + ); + assert!(matches!( + vm.call_with_limits( + function(&vm, globals, "main"), + &[], + ExecutionLimits { + max_frames: 10, + max_instructions: 100 + } + ), + Err(VmError::ExecutionLimit("instructions")) + )); + cleanup(&mut vm, code, globals); +} diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index 2c8ef63..534ee20 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -901,14 +901,38 @@ impl Vm { }) } + fn number_value<'a>( + &'a self, + value: &'a Value, + ) -> Result, VmError> { + Ok(match value { + Value::Number(n) => Some(n), + Value::Object(o) => self + .object(*o)? + .payload() + .as_any() + .downcast_ref::() + .map(|n| n.value()), + _ => None, + }) + } + fn compare_values( &self, left: &Value, right: &Value, operation: CompareOp, ) -> Result { - if matches!(left, Value::Number(_)) || matches!(right, Value::Number(_)) { - return Err(VmError::UnsupportedOperation("numeric comparison")); + let (left_number, right_number) = (self.number_value(left)?, self.number_value(right)?); + if let (Some(left), Some(right)) = (left_number, right_number) { + return left.numeric_compare(right, operation); + } + if left_number.is_some() || right_number.is_some() { + return match operation { + CompareOp::Eq => Ok(false), + CompareOp::Ne => Ok(true), + _ => Err(VmError::UnsupportedOperation("mixed numeric ordering")), + }; } if let (Some(left), Some(right)) = (self.text_value(left)?, self.text_value(right)?) { return Ok(match operation { @@ -1242,6 +1266,45 @@ impl Vm { }, ))); } + Opcode::BinaryArithmetic => { + if decoded.instruction.argument == 0 { + let left = Self::stack_value(frame, frame.stack.len() - 2)?; + let right = Self::top_value(frame)?; + if let (Value::Text(a), Value::Text(b)) = (left, right) { + if a.len().saturating_add(b.len()) > 1_048_576 { + return Err(VmError::InvalidArgument( + "text concatenation exceeds operation budget", + )); + } + self.discard(frame, 2); + frame.stack.push(StackEntry::Value(Value::Text(a + &b))); + continue; + } + } + let left = Self::stack_value(frame, frame.stack.len() - 2)?; + let right = Self::top_value(frame)?; + let left = self + .number_value(&left)? + .ok_or(VmError::ObjectKindMismatch("Number"))?; + let right = self + .number_value(&right)? + .ok_or(VmError::ObjectKindMismatch("Number"))?; + let result = left.arithmetic( + right, + crate::ArithmeticOp::from_argument(decoded.instruction.argument).unwrap(), + )?; + self.discard(frame, 2); + frame.stack.push(StackEntry::Value(Value::Number(result))); + } + Opcode::UnaryNegate => { + let value = Self::top_value(frame)?; + let value = self + .number_value(&value)? + .ok_or(VmError::ObjectKindMismatch("Number"))?; + let result = value.negated()?; + self.discard(frame, 1); + frame.stack.push(StackEntry::Value(Value::Number(result))); + } Opcode::CompareOp => { let left = Self::stack_value(frame, frame.stack.len() - 2)?; let right = Self::top_value(frame)?; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 8b747f9..cc744b5 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -52,13 +52,13 @@ pub use objects::{ RootTypes, TypeConstructor, TypedMethodRequirement, }; pub use objects::{ - CallArity, CallEffect, CallSignature, CallType, ChangeBatch, ChangeKey, CodeLayout, CompareOp, - ContextNativeFunction, DecodedInstruction, EndorBaseExceptionObject, EndorCellObject, - EndorCode, EndorCodeObject, EndorDecimal, EndorDictObject, EndorFunctionObject, EndorInteger, - EndorNativeCall, EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorObject, - EndorRational, EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, NativeCall, - NativeCallable, NativeContext, NativeFunction, NativeResult, ObjectChange, ObjectHeader, - ObjectPayload, ObjectRef, Opcode, ReadAccess, ReadDependency, ReadSet, Record, SourceSignature, - TrackedReads, Value, VmError, WeakObjectRef, + ArithmeticOp, CallArity, CallEffect, CallSignature, CallType, ChangeBatch, ChangeKey, + CodeLayout, CompareOp, ContextNativeFunction, DecodedInstruction, EndorBaseExceptionObject, + EndorCellObject, EndorCode, EndorCodeObject, EndorDecimal, EndorDictObject, + EndorFunctionObject, EndorInteger, EndorNativeCall, EndorNativeFunctionObject, EndorNumber, + EndorNumberObject, EndorObject, EndorRational, EndorReal, EndorStringObject, + EndorTracebackEntry, Instruction, NativeCall, NativeCallable, NativeContext, NativeFunction, + NativeResult, ObjectChange, ObjectHeader, ObjectPayload, ObjectRef, Opcode, ReadAccess, + ReadDependency, ReadSet, Record, SourceSignature, TrackedReads, Value, VmError, WeakObjectRef, }; pub use vm::Vm; diff --git a/crates/core/tests/execution.rs b/crates/core/tests/execution.rs index 58a1a39..7ba32dd 100644 --- a/crates/core/tests/execution.rs +++ b/crates/core/tests/execution.rs @@ -1593,7 +1593,22 @@ fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() { use endor_core::CoroutineProgress; let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); - let code = return_constant(&mut vm, Value::Nil); + // Reaching ReturnValue at the limit succeeds; keep a third instruction + // beyond the hard limit to exercise failure before the next Pending yield. + let code = vm + .new_code( + EndorCode::from_instructions( + &[ + Instruction::simple(Opcode::Nop), + Instruction::new(Opcode::LoadConst, 0), + Instruction::simple(Opcode::ReturnValue), + ], + vec![Value::Nil], + CodeLayout::default(), + ) + .unwrap(), + ) + .unwrap(); let function = vm .new_function("limited", code, globals, vec![], vec![]) .unwrap(); @@ -1603,7 +1618,9 @@ fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() { &[], ExecutionLimits { max_frames: 4, - max_instructions: 1, + // The second resume reaches the cumulative limit before + // the third instruction, without replaying the first. + max_instructions: 2, }, ) .unwrap(); diff --git a/crates/frontend/Endor.asdl b/crates/frontend/Endor.asdl index 395356a..b7ede21 100644 --- a/crates/frontend/Endor.asdl +++ b/crates/frontend/Endor.asdl @@ -21,12 +21,16 @@ module Endor { StmtKind = Binding(Binding binding) | Assignment(Assignment assignment) | MemberAssignment(MemberAssignment assignment) + | If(IfStatement statement) + | While(WhileStatement statement) | Return(ReturnStatement statement) | Expression(Expr expression) | Declaration(Decl declaration) Binding = (Name name, Expr value, span span) Assignment = (Name name, Expr value, span span) MemberAssignment = (Expr object, Name name, Expr value, span span) + IfStatement = (Expr condition, Block body, Block? otherwise, span span) + WhileStatement = (Expr condition, Block body, span span) ReturnStatement = (Expr? value, span span) InteractiveInput = (Stmt* statements, span span) @@ -41,5 +45,5 @@ module Endor { | Member(Expr object, Name name) | Call(Expr callee, Expr* arguments) UnaryOp = Negate - BinaryOp = Add | Subtract | Multiply | Divide + BinaryOp = Add | Subtract | Multiply | Divide | Equal | NotEqual | Less | LessEqual | Greater | GreaterEqual } diff --git a/crates/frontend/editor/ast.schema.json b/crates/frontend/editor/ast.schema.json index d6c0f36..2df1ea2 100644 --- a/crates/frontend/editor/ast.schema.json +++ b/crates/frontend/editor/ast.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:endor:ast:sha256:c7c10c6a61158810fa55044f1aa79f3558ac18ca3be59cbf7f2499b5add8df5b", + "$id": "urn:endor:ast:sha256:21b0349ac295d9bc8cedcad8f07d8e2d45cf220e19e7c2fad10269555fcfd71a", "$ref": "#/$defs/AstDocument", "$defs": { "Span": { @@ -27,7 +27,7 @@ "type": "object", "properties": { "schema": { - "const": "urn:endor:ast:sha256:c7c10c6a61158810fa55044f1aa79f3558ac18ca3be59cbf7f2499b5add8df5b" + "const": "urn:endor:ast:sha256:21b0349ac295d9bc8cedcad8f07d8e2d45cf220e19e7c2fad10269555fcfd71a" }, "root": { "$ref": "#/$defs/AstRoot" @@ -354,6 +354,38 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "kind": { + "const": "If" + }, + "data": { + "$ref": "#/$defs/IfStatement" + } + }, + "required": [ + "kind", + "data" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "While" + }, + "data": { + "$ref": "#/$defs/WhileStatement" + } + }, + "required": [ + "kind", + "data" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -468,6 +500,56 @@ ], "additionalProperties": false }, + "IfStatement": { + "type": "object", + "properties": { + "condition": { + "$ref": "#/$defs/Expr" + }, + "body": { + "$ref": "#/$defs/Block" + }, + "otherwise": { + "anyOf": [ + { + "$ref": "#/$defs/Block" + }, + { + "type": "null" + } + ] + }, + "span": { + "$ref": "#/$defs/Span" + } + }, + "required": [ + "condition", + "body", + "span" + ], + "additionalProperties": false + }, + "WhileStatement": { + "type": "object", + "properties": { + "condition": { + "$ref": "#/$defs/Expr" + }, + "body": { + "$ref": "#/$defs/Block" + }, + "span": { + "$ref": "#/$defs/Span" + } + }, + "required": [ + "condition", + "body", + "span" + ], + "additionalProperties": false + }, "ReturnStatement": { "type": "object", "properties": { @@ -742,7 +824,13 @@ "Add", "Subtract", "Multiply", - "Divide" + "Divide", + "Equal", + "NotEqual", + "Less", + "LessEqual", + "Greater", + "GreaterEqual" ] } } diff --git a/crates/frontend/editor/ast.ts b/crates/frontend/editor/ast.ts index f4a5f89..8f83e3e 100644 --- a/crates/frontend/editor/ast.ts +++ b/crates/frontend/editor/ast.ts @@ -1,6 +1,6 @@ // Generated by scripts/generate_ast.py from Endor.asdl. Do not edit. -export const AST_SCHEMA_ID = "urn:endor:ast:sha256:c7c10c6a61158810fa55044f1aa79f3558ac18ca3be59cbf7f2499b5add8df5b" as const; +export const AST_SCHEMA_ID = "urn:endor:ast:sha256:21b0349ac295d9bc8cedcad8f07d8e2d45cf220e19e7c2fad10269555fcfd71a" as const; export interface Span { start: number; end: number; } export type AstDocument = { schema: typeof AST_SCHEMA_ID; root: AstRoot; }; export type AstRoot = { kind: "Module"; data: Program; } | { kind: "Expression"; data: Expr; } | { kind: "Interactive"; data: InteractiveInput; }; @@ -13,16 +13,18 @@ export type Function = { name: Name; parameters: Array; return_type: export type Parameter = { name: Name; type_name: Name; span: Span; }; export type Block = { statements: Array; span: Span; }; export type Stmt = { kind: StmtKind; span: Span; }; -export type StmtKind = { kind: "Binding"; data: Binding; } | { kind: "Assignment"; data: Assignment; } | { kind: "MemberAssignment"; data: MemberAssignment; } | { kind: "Return"; data: ReturnStatement; } | { kind: "Expression"; data: Expr; } | { kind: "Declaration"; data: Decl; }; +export type StmtKind = { kind: "Binding"; data: Binding; } | { kind: "Assignment"; data: Assignment; } | { kind: "MemberAssignment"; data: MemberAssignment; } | { kind: "If"; data: IfStatement; } | { kind: "While"; data: WhileStatement; } | { kind: "Return"; data: ReturnStatement; } | { kind: "Expression"; data: Expr; } | { kind: "Declaration"; data: Decl; }; export type Binding = { name: Name; value: Expr; span: Span; }; export type Assignment = { name: Name; value: Expr; span: Span; }; export type MemberAssignment = { object: Expr; name: Name; value: Expr; span: Span; }; +export type IfStatement = { condition: Expr; body: Block; otherwise?: Block | null; span: Span; }; +export type WhileStatement = { condition: Expr; body: Block; span: Span; }; export type ReturnStatement = { value?: Expr | null; span: Span; }; export type InteractiveInput = { statements: Array; span: Span; }; export type Expr = { kind: ExprKind; span: Span; }; export type ExprKind = { kind: "Name"; data: string; } | { kind: "Number"; data: string; } | { kind: "String"; data: string; } | { kind: "Symbol"; data: Name; } | { kind: "Await"; data: Expr; } | { kind: "Unary"; data: { operation: UnaryOp; operand: Expr; }; } | { kind: "Binary"; data: { operation: BinaryOp; left: Expr; right: Expr; }; } | { kind: "Member"; data: { object: Expr; name: Name; }; } | { kind: "Call"; data: { callee: Expr; arguments: Array; }; }; export type UnaryOp = "Negate"; -export type BinaryOp = "Add" | "Subtract" | "Multiply" | "Divide"; +export type BinaryOp = "Add" | "Subtract" | "Multiply" | "Divide" | "Equal" | "NotEqual" | "Less" | "LessEqual" | "Greater" | "GreaterEqual"; export interface Visitor { visitSpan?(node: Span): boolean | void; visitAstDocument?(node: AstDocument): boolean | void; @@ -40,6 +42,8 @@ visitStmtKind?(node: StmtKind): boolean | void; visitBinding?(node: Binding): boolean | void; visitAssignment?(node: Assignment): boolean | void; visitMemberAssignment?(node: MemberAssignment): boolean | void; +visitIfStatement?(node: IfStatement): boolean | void; +visitWhileStatement?(node: WhileStatement): boolean | void; visitReturnStatement?(node: ReturnStatement): boolean | void; visitInteractiveInput?(node: InteractiveInput): boolean | void; visitExpr?(node: Expr): boolean | void; @@ -135,6 +139,12 @@ break; case "MemberAssignment": walkMemberAssignment(node.data, visitor); break; +case "If": +walkIfStatement(node.data, visitor); +break; +case "While": +walkWhileStatement(node.data, visitor); +break; case "Return": walkReturnStatement(node.data, visitor); break; @@ -165,6 +175,19 @@ walkName(node.name, visitor); walkExpr(node.value, visitor); walkSpan(node.span, visitor); } +export function walkIfStatement(node: IfStatement, visitor: Visitor): void { +if (visitor.visitIfStatement?.(node) === false) return; +walkExpr(node.condition, visitor); +walkBlock(node.body, visitor); +if (node.otherwise != null) walkBlock(node.otherwise, visitor); +walkSpan(node.span, visitor); +} +export function walkWhileStatement(node: WhileStatement, visitor: Visitor): void { +if (visitor.visitWhileStatement?.(node) === false) return; +walkExpr(node.condition, visitor); +walkBlock(node.body, visitor); +walkSpan(node.span, visitor); +} export function walkReturnStatement(node: ReturnStatement, visitor: Visitor): void { if (visitor.visitReturnStatement?.(node) === false) return; if (node.value != null) walkExpr(node.value, visitor); diff --git a/crates/frontend/src/ast/generated.rs b/crates/frontend/src/ast/generated.rs index 8350ce2..c6c4a38 100644 --- a/crates/frontend/src/ast/generated.rs +++ b/crates/frontend/src/ast/generated.rs @@ -3,7 +3,7 @@ use super::Spanned; use crate::Span; pub const AST_SCHEMA_ID: &str = - "urn:endor:ast:sha256:c7c10c6a61158810fa55044f1aa79f3558ac18ca3be59cbf7f2499b5add8df5b"; + "urn:endor:ast:sha256:21b0349ac295d9bc8cedcad8f07d8e2d45cf220e19e7c2fad10269555fcfd71a"; #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct AstDocument { @@ -141,6 +141,8 @@ pub enum StmtKind { Binding(Binding), Assignment(Assignment), MemberAssignment(MemberAssignment), + If(IfStatement), + While(WhileStatement), Return(ReturnStatement), Expression(Expr), Declaration(Decl), @@ -151,6 +153,8 @@ impl Spanned for StmtKind { Self::Binding(node) => node.span(), Self::Assignment(node) => node.span(), Self::MemberAssignment(node) => node.span(), + Self::If(node) => node.span(), + Self::While(node) => node.span(), Self::Return(node) => node.span(), Self::Expression(node) => node.span(), Self::Declaration(node) => node.span(), @@ -196,6 +200,31 @@ impl Spanned for MemberAssignment { } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] +pub struct IfStatement { + pub condition: Expr, + pub body: Block, + pub otherwise: Option, + pub span: Span, +} +impl Spanned for IfStatement { + fn span(&self) -> Span { + self.span + } +} +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WhileStatement { + pub condition: Expr, + pub body: Block, + pub span: Span, +} +impl Spanned for WhileStatement { + fn span(&self) -> Span { + self.span + } +} +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] pub struct ReturnStatement { pub value: Option, pub span: Span, @@ -263,4 +292,10 @@ pub enum BinaryOp { Subtract, Multiply, Divide, + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, } diff --git a/crates/frontend/src/ast/visitor.rs b/crates/frontend/src/ast/visitor.rs index b430f7d..c8bd1ea 100644 --- a/crates/frontend/src/ast/visitor.rs +++ b/crates/frontend/src/ast/visitor.rs @@ -2,8 +2,8 @@ use super::{ Assignment, AstDocument, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr, - ExprKind, Function, InteractiveInput, MemberAssignment, Name, Parameter, Program, - ReturnStatement, Spanned, Stmt, StmtKind, UnaryOp, + ExprKind, Function, IfStatement, InteractiveInput, MemberAssignment, Name, Parameter, Program, + ReturnStatement, Spanned, Stmt, StmtKind, UnaryOp, WhileStatement, }; use crate::Span; pub trait Visitor { @@ -102,6 +102,18 @@ pub trait Visitor { self.leave_node(); result } + fn visit_if_statement(&mut self, node: &IfStatement) -> Result<(), Self::Error> { + self.enter_node(node.span())?; + let result = walk_if_statement(self, node); + self.leave_node(); + result + } + fn visit_while_statement(&mut self, node: &WhileStatement) -> Result<(), Self::Error> { + self.enter_node(node.span())?; + let result = walk_while_statement(self, node); + self.leave_node(); + result + } fn visit_return_statement(&mut self, node: &ReturnStatement) -> Result<(), Self::Error> { self.enter_node(node.span())?; let result = walk_return_statement(self, node); @@ -243,6 +255,12 @@ pub fn walk_stmt_kind( StmtKind::MemberAssignment(value) => { visitor.visit_member_assignment(value)?; } + StmtKind::If(value) => { + visitor.visit_if_statement(value)?; + } + StmtKind::While(value) => { + visitor.visit_while_statement(value)?; + } StmtKind::Return(value) => { visitor.visit_return_statement(value)?; } @@ -280,6 +298,27 @@ pub fn walk_member_assignment( visitor.visit_span(&node.span)?; Ok(()) } +pub fn walk_if_statement( + visitor: &mut V, + node: &IfStatement, +) -> Result<(), V::Error> { + visitor.visit_expr(&node.condition)?; + visitor.visit_block(&node.body)?; + if let Some(value) = &node.otherwise { + visitor.visit_block(value)?; + } + visitor.visit_span(&node.span)?; + Ok(()) +} +pub fn walk_while_statement( + visitor: &mut V, + node: &WhileStatement, +) -> Result<(), V::Error> { + visitor.visit_expr(&node.condition)?; + visitor.visit_block(&node.body)?; + visitor.visit_span(&node.span)?; + Ok(()) +} pub fn walk_return_statement( visitor: &mut V, node: &ReturnStatement, diff --git a/crates/frontend/src/grammar.peg b/crates/frontend/src/grammar.peg index db41e03..914faa3 100644 --- a/crates/frontend/src/grammar.peg +++ b/crates/frontend/src/grammar.peg @@ -78,6 +78,7 @@ peg::parser! { = declaration:declaration() { Stmt { span: declaration.span, kind: StmtKind::Declaration(declaration) } } + / control:control_statement() { control } / binding:binding() { binding_statement(binding) } / assignment:member_assignment() { assignment } / assignment:assignment() { assignment } @@ -104,13 +105,25 @@ peg::parser! { } rule local_statement() -> Stmt - = binding:binding() { binding_statement(binding) } + = control:control_statement() { control } + / binding:binding() { binding_statement(binding) } / assignment:member_assignment() { assignment } / assignment:assignment() { assignment } / !return_keyword() expression:expression() { Stmt { span: expression.span, kind: StmtKind::Expression(expression) } } + rule control_statement() -> Stmt + = start:keyword("if") condition:expression() body:block() + otherwise:(keyword("else") body:block() { body })? { + let span=start.through(otherwise.as_ref().map_or(body.span,|b|b.span)); + Stmt {span,kind:StmtKind::If(IfStatement {condition,body,otherwise,span})} + } + / start:keyword("while") condition:expression() body:block() { + let span=start.through(body.span); + Stmt {span,kind:StmtKind::While(WhileStatement {condition,body,span})} + } + rule assignment() -> Stmt = name:name() equal() value:expression() { let span = name.span.through(value.span); @@ -140,6 +153,13 @@ peg::parser! { } pub rule expression() -> Expr = precedence! { + left:(@) [Token {kind:TokenKind::EqualEqual,..}] right:@ {binary(left,BinaryOp::Equal,right)} + left:(@) [Token {kind:TokenKind::NotEqual,..}] right:@ {binary(left,BinaryOp::NotEqual,right)} + left:(@) [Token {kind:TokenKind::Less,..}] right:@ {binary(left,BinaryOp::Less,right)} + left:(@) [Token {kind:TokenKind::LessEqual,..}] right:@ {binary(left,BinaryOp::LessEqual,right)} + left:(@) [Token {kind:TokenKind::Greater,..}] right:@ {binary(left,BinaryOp::Greater,right)} + left:(@) [Token {kind:TokenKind::GreaterEqual,..}] right:@ {binary(left,BinaryOp::GreaterEqual,right)} + -- left:(@) plus() right:@ { binary(left, BinaryOp::Add, right) } left:(@) minus() right:@ { binary(left, BinaryOp::Subtract, right) } -- diff --git a/crates/frontend/src/lexer.peg b/crates/frontend/src/lexer.peg index cb32503..c3240f4 100644 --- a/crates/frontend/src/lexer.peg +++ b/crates/frontend/src/lexer.peg @@ -59,6 +59,12 @@ peg::parser! { / ":" { TokenKind::Colon } / "," { TokenKind::Comma } / "." { TokenKind::Dot } + / "==" { TokenKind::EqualEqual } + / "!=" { TokenKind::NotEqual } + / "<=" { TokenKind::LessEqual } + / ">=" { TokenKind::GreaterEqual } + / "<" { TokenKind::Less } + / ">" { TokenKind::Greater } / "=" { TokenKind::Equal } / "+" { TokenKind::Plus } / "-" { TokenKind::Minus } diff --git a/crates/frontend/src/parser.rs b/crates/frontend/src/parser.rs index 3ef9fed..6722bf4 100644 --- a/crates/frontend/src/parser.rs +++ b/crates/frontend/src/parser.rs @@ -1,7 +1,7 @@ use crate::ast::{ Assignment, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr, ExprKind, Function, - InteractiveInput, MemberAssignment, Name, Parameter, Program, ReturnStatement, Stmt, StmtKind, - UnaryOp, + IfStatement, InteractiveInput, MemberAssignment, Name, Parameter, Program, ReturnStatement, + Stmt, StmtKind, UnaryOp, WhileStatement, }; use crate::lexer::decode_string; use crate::{Diagnostic, DiagnosticKind, Span, Token, TokenKind, lex}; diff --git a/crates/frontend/src/token.rs b/crates/frontend/src/token.rs index 83de62b..6b2a129 100644 --- a/crates/frontend/src/token.rs +++ b/crates/frontend/src/token.rs @@ -16,6 +16,12 @@ pub enum TokenKind { Comma, Dot, Equal, + EqualEqual, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, Plus, Minus, Star, diff --git a/crates/frontend/tests/syntax.rs b/crates/frontend/tests/syntax.rs index 668e478..07aed3a 100644 --- a/crates/frontend/tests/syntax.rs +++ b/crates/frontend/tests/syntax.rs @@ -272,7 +272,7 @@ fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax( } for source in [ "", "x y", "1 +", "+1", ".5", "1.", "f(1,)", "f(,1)", "f(1 2)", "(1", "obj.", "[1]", - "a == b", "1.2.3", + "1.2.3", ] { assert!(parse_expression(source).is_err(), "{source}"); } diff --git a/crates/objects/Cargo.toml b/crates/objects/Cargo.toml index 7ecfd7c..ca27fd1 100644 --- a/crates/objects/Cargo.toml +++ b/crates/objects/Cargo.toml @@ -7,3 +7,8 @@ license.workspace = true [lints] workspace = true + +[dependencies] +num-bigint = "0.4" +num-rational = "0.4" +num-traits = "0.2" diff --git a/crates/objects/src/code.rs b/crates/objects/src/code.rs index 577935a..e63e1b9 100644 --- a/crates/objects/src/code.rs +++ b/crates/objects/src/code.rs @@ -258,6 +258,8 @@ pub(crate) fn read_only_opcode(opcode: Opcode) -> bool { | Opcode::PopJumpIfTrue | Opcode::ToBool | Opcode::UnaryNot + | Opcode::BinaryArithmetic + | Opcode::UnaryNegate | Opcode::CompareOp | Opcode::LoadDeref | Opcode::LoadClosure @@ -354,6 +356,7 @@ fn verify( | Opcode::LoadAttr | Opcode::StoreAttr => index < layout.names.len(), Opcode::CompareOp => compare_operation(argument).is_some(), + Opcode::BinaryArithmetic => argument <= 3, Opcode::Copy => argument >= 1, Opcode::Swap => argument >= 2, Opcode::JumpForward @@ -394,8 +397,12 @@ fn verify( | Opcode::StoreDeref | Opcode::PopJumpIfFalse | Opcode::PopJumpIfTrue => (1, 1, 0), - Opcode::ToBool | Opcode::UnaryNot | Opcode::LoadAttr | Opcode::Await => (1, 1, 1), - Opcode::CompareOp => (2, 2, 1), + Opcode::ToBool + | Opcode::UnaryNot + | Opcode::LoadAttr + | Opcode::Await + | Opcode::UnaryNegate => (1, 1, 1), + Opcode::CompareOp | Opcode::BinaryArithmetic => (2, 2, 1), Opcode::StoreAttr => (2, 2, 0), Opcode::Copy => (argument, 0, 1), Opcode::Swap => (argument, 0, 0), diff --git a/crates/objects/src/instruction.rs b/crates/objects/src/instruction.rs index 8084afd..0d3d17f 100644 --- a/crates/objects/src/instruction.rs +++ b/crates/objects/src/instruction.rs @@ -36,6 +36,8 @@ pub enum Opcode { MakeFunction = 28, BuildClass = 29, Await = 30, + BinaryArithmetic = 31, + UnaryNegate = 32, ExtendedArg = 255, } @@ -73,6 +75,8 @@ impl Opcode { 28 => Self::MakeFunction, 29 => Self::BuildClass, 30 => Self::Await, + 31 => Self::BinaryArithmetic, + 32 => Self::UnaryNegate, 255 => Self::ExtendedArg, _ => return None, }) @@ -88,6 +92,7 @@ impl Opcode { | Self::ToBool | Self::UnaryNot | Self::Await + | Self::UnaryNegate | Self::BuildClass ) } diff --git a/crates/objects/src/lib.rs b/crates/objects/src/lib.rs index 2bcea71..270ab2e 100644 --- a/crates/objects/src/lib.rs +++ b/crates/objects/src/lib.rs @@ -40,7 +40,8 @@ pub use native::{ NativeContext, NativeResult, }; pub use number::{ - EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal, + ArithmeticOp, EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, + EndorReal, }; pub use object::{ AwaitPoll, CompareOp, EndorNativeCall, EndorNativeFunctionObject, EndorObject, NativeCall, diff --git a/crates/objects/src/number.rs b/crates/objects/src/number.rs index f1d09b3..1e9449f 100644 --- a/crates/objects/src/number.rs +++ b/crates/objects/src/number.rs @@ -1,5 +1,6 @@ //! Lossless number representations. Arithmetic and numeric equivalence are -//! deliberately separate from representation equality. +//! deliberately separate from representation equality; real operations preserve +//! exact values unless a binary-float operand explicitly selects float mode. use std::any::Any; use std::fmt; @@ -39,7 +40,16 @@ impl EndorInteger { } pub fn to_i64(&self) -> Option { - self.to_string().parse().ok() + let magnitude = self.digits.parse::().ok()?; + if self.negative { + if magnitude == (i64::MAX as u64) + 1 { + Some(i64::MIN) + } else { + i64::try_from(magnitude).ok()?.checked_neg() + } + } else { + i64::try_from(magnitude).ok() + } } } @@ -299,9 +309,12 @@ impl ObjectPayload for EndorNumberObject { Err(VmError::UnsupportedOperation("numeric hash")) } - fn compare(&self, other: &dyn ObjectPayload, _op: CompareOp) -> Result, VmError> { - if other.as_any().is::() { - return Err(VmError::UnsupportedOperation("numeric comparison")); + fn compare(&self, other: &dyn ObjectPayload, op: CompareOp) -> Result, VmError> { + if let Some(other) = other.as_any().downcast_ref::() { + if self.value.as_real().is_none() || other.value.as_real().is_none() { + return Err(VmError::UnsupportedOperation("numeric comparison")); + } + return self.value.numeric_compare(&other.value, op).map(Some); } Ok(None) } @@ -318,3 +331,222 @@ fn split_sign(text: &str) -> (bool, &str) { fn is_digits(text: &str) -> bool { !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()) } + +/// Source arithmetic is exact for integers/decimals/rationals. A binary-float +/// operand explicitly selects IEEE floating-point arithmetic, as used by host +/// geometry/math APIs. Complex arithmetic remains unsupported. +#[derive(Debug, Clone, Copy)] +pub enum ArithmeticOp { + Add, + Subtract, + Multiply, + Divide, +} +impl ArithmeticOp { + pub fn from_argument(value: u32) -> Option { + match value { + 0 => Some(Self::Add), + 1 => Some(Self::Subtract), + 2 => Some(Self::Multiply), + 3 => Some(Self::Divide), + _ => None, + } + } +} +impl EndorNumber { + pub fn negated(&self) -> Result { + if let Some(EndorReal::BinaryFloat(bits)) = self.as_real() { + return Ok(Self::Real(EndorReal::BinaryFloat(bits ^ (1_u64 << 63)))); + } + Self::from(0_i64).arithmetic(self, ArithmeticOp::Subtract) + } + /// Explicit narrowing for graphics/FFI boundaries; exact values themselves + /// retain their representation. Non-real or non-finite values are rejected. + pub fn to_f64(&self) -> Result { + use num_traits::ToPrimitive; + let real = self + .as_real() + .ok_or(VmError::UnsupportedOperation("complex to float"))?; + let value = if let EndorReal::BinaryFloat(bits) = real { + f64::from_bits(*bits) + } else { + exact_real(real)?.to_f64().ok_or(VmError::InvalidArgument( + "number exceeds binary float range", + ))? + }; + if !value.is_finite() { + return Err(VmError::InvalidArgument("non-finite float conversion")); + } + Ok(value) + } + pub fn arithmetic(&self, other: &Self, op: ArithmeticOp) -> Result { + if let (Some(a), Some(b)) = (self.as_i64(), other.as_i64()) { + let integer = match op { + ArithmeticOp::Add => a.checked_add(b), + ArithmeticOp::Subtract => a.checked_sub(b), + ArithmeticOp::Multiply => a.checked_mul(b), + ArithmeticOp::Divide => a + .checked_rem(b) + .filter(|&r| r == 0) + .and_then(|_| a.checked_div(b)), + }; + if let Some(result) = integer { + return Ok(Self::from(result)); + } + } + use num_traits::{ToPrimitive, Zero}; + let (a, b) = ( + self.as_real() + .ok_or(VmError::UnsupportedOperation("complex arithmetic"))?, + other + .as_real() + .ok_or(VmError::UnsupportedOperation("complex arithmetic"))?, + ); + if matches!(a, EndorReal::BinaryFloat(_)) || matches!(b, EndorReal::BinaryFloat(_)) { + let convert = |n: &EndorReal| -> Result { + if let EndorReal::BinaryFloat(bits) = n { + Ok(f64::from_bits(*bits)) + } else { + exact_real(n)?.to_f64().ok_or(VmError::InvalidArgument( + "number exceeds binary float range", + )) + } + }; + let (a, b) = (convert(a)?, convert(b)?); + if matches!(op, ArithmeticOp::Divide) && b == 0. { + return Err(VmError::InvalidArgument("division by zero")); + } + let result = match op { + ArithmeticOp::Add => a + b, + ArithmeticOp::Subtract => a - b, + ArithmeticOp::Multiply => a * b, + ArithmeticOp::Divide => a / b, + }; + if !result.is_finite() { + return Err(VmError::InvalidArgument("non-finite arithmetic result")); + } + return Ok(Self::binary_float(result)); + } + let (a, b) = (exact_real(a)?, exact_real(b)?); + if matches!(op, ArithmeticOp::Divide) && b.is_zero() { + return Err(VmError::InvalidArgument("division by zero")); + } + let result = match op { + ArithmeticOp::Add => a + b, + ArithmeticOp::Subtract => a - b, + ArithmeticOp::Multiply => a * b, + ArithmeticOp::Divide => a / b, + }; + if result.numer().bits() > 16_384 || result.denom().bits() > 16_384 { + return Err(VmError::InvalidArgument( + "arithmetic result exceeds operation budget", + )); + } + if result.is_integer() { + Self::integer(&result.to_integer().to_string()) + } else { + Self::rational(&result.numer().to_string(), &result.denom().to_string()) + } + } + pub fn numeric_compare(&self, other: &Self, op: CompareOp) -> Result { + use std::cmp::Ordering; + let (a, b) = ( + self.as_real() + .ok_or(VmError::UnsupportedOperation("complex comparison"))?, + other + .as_real() + .ok_or(VmError::UnsupportedOperation("complex comparison"))?, + ); + if let (Some(a), Some(b)) = (self.as_i64(), other.as_i64()) { + return Ok(match op { + CompareOp::Eq => a == b, + CompareOp::Ne => a != b, + CompareOp::Lt => a < b, + CompareOp::Le => a <= b, + CompareOp::Gt => a > b, + CompareOp::Ge => a >= b, + }); + } + // Finite binary floats compare as exact binary rationals, so a large + // integer never loses precision merely to answer an ordering question. + let order = if let (EndorReal::BinaryFloat(a), EndorReal::BinaryFloat(b)) = (a, b) { + f64::from_bits(*a).partial_cmp(&f64::from_bits(*b)) + } else { + let nonfinite = |n: &EndorReal| match n { + EndorReal::BinaryFloat(bits) if !f64::from_bits(*bits).is_finite() => { + Some(f64::from_bits(*bits)) + } + _ => None, + }; + match (nonfinite(a), nonfinite(b)) { + (Some(a), _) if a.is_nan() => None, + (_, Some(b)) if b.is_nan() => None, + (Some(a), _) => Some(if a.is_sign_positive() { + Ordering::Greater + } else { + Ordering::Less + }), + (_, Some(b)) => Some(if b.is_sign_positive() { + Ordering::Less + } else { + Ordering::Greater + }), + _ => Some(exact_real(a)?.cmp(&exact_real(b)?)), + } + }; + Ok(match op { + CompareOp::Eq => order == Some(Ordering::Equal), + CompareOp::Ne => order != Some(Ordering::Equal), + CompareOp::Lt => order == Some(Ordering::Less), + CompareOp::Le => matches!(order, Some(Ordering::Less | Ordering::Equal)), + CompareOp::Gt => order == Some(Ordering::Greater), + CompareOp::Ge => matches!(order, Some(Ordering::Greater | Ordering::Equal)), + }) + } +} +fn exact_real(value: &EndorReal) -> Result { + use num_bigint::BigInt; + use num_rational::BigRational; + let integer = |s: &str| -> Result { + if s.len() > 4096 { + return Err(VmError::InvalidArgument( + "numeric operand exceeds operation budget", + )); + } + s.parse() + .map_err(|_| VmError::InvalidArgument("invalid exact number")) + }; + match value { + EndorReal::Integer(n) => Ok(BigRational::from_integer(integer(&n.to_string())?)), + EndorReal::Rational(n) => Ok(BigRational::new( + integer(&n.numerator.to_string())?, + integer(&n.denominator.to_string())?, + )), + EndorReal::BinaryFloat(bits) => BigRational::from_float(f64::from_bits(*bits)) + .ok_or(VmError::InvalidArgument("non-finite exact conversion")), + EndorReal::Decimal(n) => { + let parts: Vec<_> = n.literal.split(['e', 'E']).collect(); + let exponent = if parts.len() == 2 { + parts[1].parse::().map_err(|_| { + VmError::InvalidArgument("numeric exponent exceeds operation budget") + })? + } else { + 0 + }; + let decimals = parts[0].split_once('.').map_or(0, |(_, s)| s.len()) as i64; + let power = i64::from(exponent) - decimals; + if power.abs() > 4096 { + return Err(VmError::InvalidArgument( + "numeric exponent exceeds operation budget", + )); + } + let digits = integer(&parts[0].replace('.', ""))?; + let factor = BigInt::from(10).pow(power.unsigned_abs() as u32); + Ok(if power < 0 { + BigRational::new(digits, factor) + } else { + BigRational::from_integer(digits * factor) + }) + } + } +} diff --git a/crates/objects/tests/arithmetic.rs b/crates/objects/tests/arithmetic.rs new file mode 100644 index 0000000..3fe55f9 --- /dev/null +++ b/crates/objects/tests/arithmetic.rs @@ -0,0 +1,58 @@ +use objects::{ArithmeticOp, CompareOp, EndorNumber, EndorReal}; +fn equal(a: &EndorNumber, b: &EndorNumber) { + assert!(a.numeric_compare(b, CompareOp::Eq).unwrap()); +} +#[test] +fn exact_real_arithmetic_keeps_large_values_and_cross_representation_equivalence() { + let tenth = EndorNumber::decimal("0.10").unwrap(); + let fifth = EndorNumber::rational("2", "10").unwrap(); + equal( + &tenth.arithmetic(&fifth, ArithmeticOp::Add).unwrap(), + &EndorNumber::decimal("0.3").unwrap(), + ); + let large = EndorNumber::integer("9223372036854775807").unwrap(); + equal( + &large.arithmetic(&1_i64.into(), ArithmeticOp::Add).unwrap(), + &EndorNumber::integer("9223372036854775808").unwrap(), + ); + let min = EndorNumber::integer("-9223372036854775808").unwrap(); + assert_eq!(min.as_i64(), Some(i64::MIN)); + equal( + &min.negated().unwrap(), + &EndorNumber::integer("9223372036854775808").unwrap(), + ); + equal( + &EndorNumber::rational("2", "6").unwrap(), + &EndorNumber::rational("1", "3").unwrap(), + ); + assert!( + EndorNumber::integer("9007199254740993") + .unwrap() + .numeric_compare(&EndorNumber::binary_float(9007199254740992.), CompareOp::Gt) + .unwrap() + ); +} +#[test] +fn ieee_signed_zero_nan_and_operation_limits_are_explicit() { + let negative = EndorNumber::binary_float(0.).negated().unwrap(); + assert_eq!( + negative.as_real(), + Some(&EndorReal::BinaryFloat((-0.0_f64).to_bits())) + ); + let nan = EndorNumber::binary_float(f64::NAN); + assert!(!nan.numeric_compare(&nan, CompareOp::Eq).unwrap()); + assert!(nan.numeric_compare(&nan, CompareOp::Ne).unwrap()); + assert!( + EndorNumber::from(1_i64) + .arithmetic(&0_i64.into(), ArithmeticOp::Divide) + .is_err() + ); + assert!( + EndorNumber::binary_float(f64::MAX) + .arithmetic(&2_i64.into(), ArithmeticOp::Multiply) + .is_err() + ); + let huge = EndorNumber::decimal("1e99999999999999999").unwrap(); + assert_eq!(huge.to_string(), "1e99999999999999999"); + assert!(huge.arithmetic(&1_i64.into(), ArithmeticOp::Add).is_err()); +} diff --git a/docs/runtime/builtins.md b/docs/runtime/builtins.md index 687b174..3310d0a 100644 --- a/docs/runtime/builtins.md +++ b/docs/runtime/builtins.md @@ -75,7 +75,9 @@ fn main() -> Result<(), endor_core::VmError> { `EndorInteger`、`EndorDecimal`、`EndorRational` 是内部表示结构,不是三种语言数字类。构造器验证输入;分数不能有零分母;十进制指数不转换为机器整数,所以不会因指数很长而溢出。构造任意大整数或精确小数时传文本;已经被宿主转换为 f64 而丢失的精度无法恢复。整数的 `as_i64` 是显式、可失败的窄化读取,不会静默截断。 -首版不实现四则运算、排序、跨表示数值相等和数值哈希。Rust 的 PartialEq / Eq 只检查表示是否相同,不能用于解释语言的 `==`:`1.0` 与 `1.00` 保留不同小数位数;`2/6` 与 `1/3` 尚未求数值等价;NaN 位模式也被保留。VM 对两个数字对象的数值比较与哈希显式报不支持。因此数字现在可以作为字典的值,暂不能作为字典键,避免先定义错误的键相等规则。没有用“全部转换 f64”的方式补齐这些操作。 +实数四则运算和六种比较已通过统一 `EndorNumber` 实现,立即值和装箱 Number 共用语义。整数、十进制与有理数走精确运算,结果为整数或约分有理数;不将它们统一转换为 f64。任一操作数为 BinaryFloat 时显式选择二进制浮点运算;混合比较则把有限浮点数看作精确二进制有理数,避免大整数在比较时丢精度。NaN 比较遵守无序语义;一元负号保留浮点负零。复数算术/比较与数值哈希仍不支持。Rust 的 PartialEq/Eq 继续比较表示,不替代语言数值相等;字典键语义未改变,数字仍不能当键。 + +运算有单次资源预算:精确输入最多 4096 字符、十进制指数绝对值最多 4096、结果分子与分母各不超过 16384 位。超限报错但不改变原数的无损表示。除零和非有限浮点算术结果报错。`to_f64()` 是供图形/FFI 边界显式窄化的接口,不更改原数。 这里的“各类数字”指上述五种可表示形式,不包括符号表达式、无限展开的精确 π、带单位数量、任意位宽二进制浮点等全部数学对象;这些可在未来扩展表示协议。 diff --git a/docs/runtime/bytecode.md b/docs/runtime/bytecode.md index ff48e72..101840d 100644 --- a/docs/runtime/bytecode.md +++ b/docs/runtime/bytecode.md @@ -164,7 +164,7 @@ fn main() -> Result<(), endor_core::VmError> { 运行错误仍返回 `VmError`。`last_traceback()` 另提供从外到内的函数名、文件、代码首行和出错指令偏移;它保存独立元数据,不保活已经清理的帧。首行是代码声明的 first_line,不是由行号表推导出的实际错误源码行。成功的新一次调用会清空旧记录。尚无语言异常对象传播、raise/try/except 或完整源码行号表。 -暂未实现:源码闭包编译、关键字/可变参数、生成器/协程、迭代器专用指令、数值算术、自动方法绑定、指令特化和 JIT。 +暂未实现:源码闭包编译、关键字/可变参数、生成器/协程、迭代器专用指令、复数算术、自动方法绑定、指令特化和 JIT。 ## 验证 @@ -204,3 +204,10 @@ LoadAttr 对实例自有属性返回原值;缺失时沿类型父链找方法 在独立coroutine中,Pending保持已求值的栈顶、当前帧和返回契约,返回CoroutineProgress::Waiting { awaitable, instructions };awaitable是由暂停帧保活的借用身份。宿主等待条件满足后再次resume,仅重查同一个Await,不重放操作数表达式、前缀写入或IO请求。每次实际执行Await计入同一硬预算;宿主不应对未就绪对象反复resume来轮询。完成结果与错误沿普通执行/清理路径传播。非coroutine调用允许立即Ready的await,Pending则明确报错,不阻塞线程。调度、取消传播和循环等待检测属于宿主;本引擎不依赖Tessera Task。 LoadGlobal按文本名字分别观察函数globals及缺失时的captured builtins,不因无关全局写入而失效。可恢复只读Complete新增dependencies,reads保留兼容保守列表;精确验证/所有权见native-calls.md。 + + +## 实数运算指令 + +`BinaryArithmetic`(31,operand 0/1/2/3)消费两个 Number 并产生一个 Number,分别对应加减乘除;operand 0 也接受两个直接文本值进行有界连接(最多 1 MiB)。`UnaryNegate`(32,无 operand)保持 1 入 1 出。验证器检查算术 operand 和栈深度;两者是无外部写入的运算,已纳入保守只读证明。失败沿同一个 VM 栈清理及 traceback 路径传播。 + +源码 if/else 和 while 使用既有条件跳转、正向跳转和回跳,不引入宿主循环。Compiler 先记录逻辑指令标签,再迭代求解 EXTENDED_ARG 展开后的 code-unit 距离,避免大分支错跳到参数前缀。每次真实循环迭代仍消耗同一累计预算并支持已有协作暂停。 diff --git a/docs/runtime/compiler.md b/docs/runtime/compiler.md index ecea6f3..ff91962 100644 --- a/docs/runtime/compiler.md +++ b/docs/runtime/compiler.md @@ -26,7 +26,9 @@ CompileOptions.globals 显式声明宿主会提供的 globals/builtins 名称 | 源结构 | 生成方式 | | --- | --- | | 字符串、整数、十进制常量 | 放入常量表并 LoadConst;数字经现有 EndorNumber 解析,不转换为 f64 | -| 数字字面量前的一个或多个负号 | 折叠符号为精确常量;不是任意表达式的一元算术 | +| 一元负号 | 字面量折叠保留拼写;其他表达式发出 UnaryNegate | +| 四则运算、比较 | BinaryArithmetic / CompareOp;实数精确语义见 builtins | +| if / else、while | 函数局部初始化按分支合流,循环可执行零次;相对跳转经过 EXTENDED_ARG 布局解析 | | 局部读取/let/赋值 | LoadFast/StoreFast;参数和局部名称在 CodeLayout 中 | | 全局读取/赋值 | LoadGlobal/StoreGlobal,使用该代码对象的 names 表 | | 函数声明 | 子代码模板 → LoadConst(code) → MakeFunction(0, 0) → StoreGlobal | @@ -38,7 +40,7 @@ CompileOptions.globals 显式声明宿主会提供的 globals/builtins 名称 源码 parser 接受绑定/名称及属性赋值/表达式语句与可选末尾带值 return;空 return 仍仅由 AST 调用方构造。模块初始化发布函数/类并返回 nil,不自动调用 main。Lyra 宿主选择入口属于宿主行为。 -二元算术、非字面量的一元负号和符号值 .name 目前产生明确 Compilation 诊断,不伪造运行结果。导入、Agent、条件/循环、关键字参数等仍未实现。函数参数个数和对象是否可调用由 VM 在运行时检查。 +符号值 .name 仍产生明确 Compilation 诊断。四则运算、一元负号、六种比较、if/else 与 while 已接入真实字节码。导入、Agent、关键字参数、布尔运算符及 break/continue 等仍未实现。函数参数个数和对象是否可调用由 VM 在运行时检查。 ## 装载、执行与所有权 @@ -61,3 +63,5 @@ lyra run [--entry name] 编译模块、执行初始化、调用无参 Await表达式参与普通名称/初始化分析,先编译operand,再发出无参数Await opcode。该链路保存实际结果的栈位置,不通过回放函数实现等待;没有新增async声明或静态awaitable类型检查。语法与运行上下文见frontend及bytecode主题。 + +控制流中的局部变量仍为函数作用域。if 两边都能到达后续语句时,仅保留两边都已初始化的名字;一边 return 时采用仍可到达路径的初始化集合。while 中新初始化的名字不能被当作循环后一定存在。循环共享正常调用、只读调用或 coroutine 的累计指令预算,不建立第二套执行器。 diff --git a/docs/runtime/frontend.md b/docs/runtime/frontend.md index 2bc29b8..ad8108a 100644 --- a/docs/runtime/frontend.md +++ b/docs/runtime/frontend.md @@ -68,7 +68,7 @@ parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函 符号字面量、名称与成员分别保留为独立 AST 结构。true、false、Nil 等词目前只是普通名称,没有额外的源码字面量或内建值绑定。 -use、namespace、trait、agent、完整类型语法、条件/循环、比较/布尔运算和闭包声明等尚未加入首版。当前语法可修改;扩展时同步 AST、PEG 规则、样例和测试,不将旧草案视作不可修改的 ABI。 +use、namespace、trait、agent、完整类型语法、布尔运算、break/continue和闭包声明等尚未加入首版。当前语法可修改;扩展时同步 AST、PEG 规则、样例和测试,不将旧草案视作不可修改的 ABI。 ## 诊断和命令 @@ -76,7 +76,7 @@ use、namespace、trait、agent、完整类型语法、条件/循环、比较 `lyra parse ` 与 `lyra parse -` 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出来源/行/列和诊断并返回非零退出码;`-` 表示从 stdin 读到 EOF,不是 REPL。阅读与来源包装规则见 [阅读器](reader.md)。可用 --mode 选择 module/expression/interactive,用 --json 输出带 schema 标识的 AstDocument;普通输出为 AstRoot Debug 文本,稳定交换格式见 [AST 模型](ast.md)。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 [运行](../operations/README.md) 和 [验证](../operations/validation.md)。 -当前 Compiler 将 AST 经模块/函数名称分析转换为现有字节码,再交给 VM 解释执行。这是解释器内部的编译/执行准备步骤,不要求使用者先构建程序,见 [执行与版本模型](../architecture/execution-and-versions.md)。MakeFunction 已接入源码函数编译,但解析接受的二元算术和符号值仍没有执行语义;编译器会给出明确诊断,见 [编译链路](compiler.md)。源码闭包尚未实现。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。 +当前 Compiler 将 AST 经模块/函数名称分析转换为现有字节码,再交给 VM 解释执行。这是解释器内部的编译/执行准备步骤,不要求使用者先构建程序,见 [执行与版本模型](../architecture/execution-and-versions.md)。MakeFunction 已接入源码函数编译,四则运算、比较和条件/循环已可执行;符号值仍会给出明确编译诊断,见 [编译链路](compiler.md)。源码闭包尚未实现。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。 裸赋值 `name = expression` 创建或更新当前作用域中的名称;它不是表达式,不能链式赋值,属性目标使用独立 MemberAssignment 节点;索引目标尚不支持。`let` 仍表示声明,保留重复声明检查;名称与初始化规则见 [Compiler](compiler.md)。两种源码写法产生独立的 Binding/Assignment 节点,宿主不改写输入。 @@ -85,3 +85,20 @@ use、namespace、trait、agent、完整类型语法、条件/循环、比较 `await expression` 解析为 ExprKind::Await(operand),优先级与前缀负号同级,成员/调用绑定更紧;例如await task.result()等待该调用的返回对象,(await task).field先等待task。await按表达式前缀位置识别,不引入async函数声明。执行是否允许等待由调用上下文决定,见字节码VM的Await章节。 + + +## 条件与循环 + +```endor +function sum(limit: Number): Number { + i = 0 + total = 0 + while i < limit { + if i < 10 { total = total + i } else { total = total - i } + i = i + 1 + } + return total +} +``` + +比较包括 `== != < <= > >=`,优先级低于四则运算;比较链暂按左结合解析,不提供 Python 式链式比较。条件使用运行时真值;字面量 true/false、and/or/not、for、break/continue 和 else-if 简写暂未加入。可使用比较结果、宿主 Bool,或 0/非零数值。if/while 可出现在函数体与单条 Interactive 输入中;Module 仍只收声明,Interactive 仍只接受一个完整语句。else 接一个花括号块,嵌套 if 写在块内。