feat: 让 Endor 源码执行实数运算、比较与条件循环 #2

Merged
Zayden-Jung merged 1 commits from codex/script-control-flow into main 2026-11-12 03:06:12 +00:00
28 changed files with 1045 additions and 117 deletions
Generated
+51 -1
View File
@@ -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]]
+121 -44
View File
@@ -167,6 +167,16 @@ pub fn analyze(root: &AstRoot, options: &CompileOptions) -> Result<SymbolTable,
StmtKind::Expression(expression) => {
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<String>) -> Result<Sco
.push(parameter.name.text.clone());
builder.initialized.insert(parameter.name.text.clone());
}
for statement in &function.body.statements {
collect_locals(
&function.body.statements,
&mut locals,
&mut builder.scope.layout.locals,
)?;
builder.statements(&function.body.statements, globals)?;
Ok(builder.scope)
}
fn collect_locals(
statements: &[frontend::ast::Stmt],
locals: &mut BTreeSet<String>,
layout: &mut Vec<String>,
) -> 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<String>) -> Result<Sco
_ => {}
}
}
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<String>,
) -> Result<bool, Diagnostic> {
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,
+100 -17
View File
@@ -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<Instruction>,
constants: Vec<Value>,
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<CompiledCode, Diagnostic> {
fn finish(mut self) -> Result<CompiledCode, Diagnostic> {
// 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
+14 -3
View File
@@ -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
+80 -12
View File
@@ -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("<unsupported>", text);
let error = compile_source(&source, mode, &CompileOptions::default()).unwrap_err();
assert_eq!(error.diagnostic.kind, DiagnosticKind::Compilation);
}
let source = EndorReader::from_text("<unsupported>", ".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("<test>", 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);
}
+65 -2
View File
@@ -901,14 +901,38 @@ impl Vm {
})
}
fn number_value<'a>(
&'a self,
value: &'a Value,
) -> Result<Option<&'a crate::EndorNumber>, VmError> {
Ok(match value {
Value::Number(n) => Some(n),
Value::Object(o) => self
.object(*o)?
.payload()
.as_any()
.downcast_ref::<crate::EndorNumberObject>()
.map(|n| n.value()),
_ => None,
})
}
fn compare_values(
&self,
left: &Value,
right: &Value,
operation: CompareOp,
) -> Result<bool, VmError> {
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)?;
+8 -8
View File
@@ -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;
+19 -2
View File
@@ -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();
+5 -1
View File
@@ -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
}
+91 -3
View File
@@ -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"
]
}
}
+26 -3
View File
@@ -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<Parameter>; return_type:
export type Parameter = { name: Name; type_name: Name; span: Span; };
export type Block = { statements: Array<Stmt>; 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<Stmt>; 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<Expr>; }; };
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);
+36 -1
View File
@@ -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<Block>,
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<Expr>,
pub span: Span,
@@ -263,4 +292,10 @@ pub enum BinaryOp {
Subtract,
Multiply,
Divide,
Equal,
NotEqual,
Less,
LessEqual,
Greater,
GreaterEqual,
}
+41 -2
View File
@@ -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<V: Visitor + ?Sized>(
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<V: Visitor + ?Sized>(
visitor.visit_span(&node.span)?;
Ok(())
}
pub fn walk_if_statement<V: Visitor + ?Sized>(
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<V: Visitor + ?Sized>(
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<V: Visitor + ?Sized>(
visitor: &mut V,
node: &ReturnStatement,
+21 -1
View File
@@ -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) }
--
+6
View File
@@ -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 }
+2 -2
View File
@@ -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};
+6
View File
@@ -16,6 +16,12 @@ pub enum TokenKind {
Comma,
Dot,
Equal,
EqualEqual,
NotEqual,
Less,
LessEqual,
Greater,
GreaterEqual,
Plus,
Minus,
Star,
+1 -1
View File
@@ -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}");
}
+5
View File
@@ -7,3 +7,8 @@ license.workspace = true
[lints]
workspace = true
[dependencies]
num-bigint = "0.4"
num-rational = "0.4"
num-traits = "0.2"
+9 -2
View File
@@ -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),
+5
View File
@@ -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
)
}
+2 -1
View File
@@ -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,
+237 -5
View File
@@ -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<i64> {
self.to_string().parse().ok()
let magnitude = self.digits.parse::<u64>().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<Option<bool>, VmError> {
if other.as_any().is::<Self>() {
return Err(VmError::UnsupportedOperation("numeric comparison"));
fn compare(&self, other: &dyn ObjectPayload, op: CompareOp) -> Result<Option<bool>, VmError> {
if let Some(other) = other.as_any().downcast_ref::<Self>() {
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<Self> {
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<Self, VmError> {
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<f64, VmError> {
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<Self, VmError> {
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<f64, VmError> {
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<bool, VmError> {
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<num_rational::BigRational, VmError> {
use num_bigint::BigInt;
use num_rational::BigRational;
let integer = |s: &str| -> Result<BigInt, VmError> {
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::<i32>().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)
})
}
}
}
+58
View File
@@ -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());
}
+3 -1
View File
@@ -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 边界显式窄化的接口,不更改原数。
这里的“各类数字”指上述五种可表示形式,不包括符号表达式、无限展开的精确 π、带单位数量、任意位宽二进制浮点等全部数学对象;这些可在未来扩展表示协议。
+8 -1
View File
@@ -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 距离,避免大分支错跳到参数前缀。每次真实循环迭代仍消耗同一累计预算并支持已有协作暂停。
+6 -2
View File
@@ -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] <file|-> 编译模块、执行初始化、调用无参
Await表达式参与普通名称/初始化分析,先编译operand,再发出无参数Await opcode。该链路保存实际结果的栈位置,不通过回放函数实现等待;没有新增async声明或静态awaitable类型检查。语法与运行上下文见frontend及bytecode主题。
控制流中的局部变量仍为函数作用域。if 两边都能到达后续语句时,仅保留两边都已初始化的名字;一边 return 时采用仍可到达路径的初始化集合。while 中新初始化的名字不能被当作循环后一定存在。循环共享正常调用、只读调用或 coroutine 的累计指令预算,不建立第二套执行器。
+19 -2
View File
@@ -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 <file>` 与 `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 写在块内。