Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ff1c68f5f |
Generated
+51
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
));
|
||||
};
|
||||
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 {
|
||||
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::Binary { .. } => {
|
||||
return Err(error(
|
||||
expression.span,
|
||||
"numeric arithmetic bytecode is not implemented yet",
|
||||
));
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -345,19 +345,11 @@ 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();
|
||||
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]
|
||||
fn compiling_ast_rejects_invalid_source_ranges() {
|
||||
@@ -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
@@ -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)?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) }
|
||||
--
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -16,6 +16,12 @@ pub enum TokenKind {
|
||||
Comma,
|
||||
Dot,
|
||||
Equal,
|
||||
EqualEqual,
|
||||
NotEqual,
|
||||
Less,
|
||||
LessEqual,
|
||||
Greater,
|
||||
GreaterEqual,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -7,3 +7,8 @@ license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
num-bigint = "0.4"
|
||||
num-rational = "0.4"
|
||||
num-traits = "0.2"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,10 +309,13 @@ 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>() {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,19 @@ impl ObjectStore {
|
||||
}
|
||||
|
||||
fn key_equal(&self, left: &Value, right: &Value) -> Result<bool, VmError> {
|
||||
if let (Some(left), Some(right)) = (self.key_text(left)?, self.key_text(right)?) {
|
||||
let text = |value: &Value| -> Result<Option<String>, VmError> {
|
||||
Ok(match value {
|
||||
Value::Text(text) => Some(text.clone()),
|
||||
Value::Object(reference) => self
|
||||
.object(*reference)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.downcast_ref::<EndorStringObject>()
|
||||
.map(|string| string.as_str().to_owned()),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
if let (Some(left), Some(right)) = (text(left)?, text(right)?) {
|
||||
return Ok(left == right);
|
||||
}
|
||||
match (left, right) {
|
||||
@@ -207,19 +219,6 @@ impl ObjectStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn key_text<'a>(&'a self, value: &'a Value) -> Result<Option<&'a str>, VmError> {
|
||||
Ok(match value {
|
||||
Value::Text(text) => Some(text.as_str()),
|
||||
Value::Object(reference) => self
|
||||
.object(*reference)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.downcast_ref::<EndorStringObject>()
|
||||
.map(EndorStringObject::as_str),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_cell(&mut self, value: Option<Value>) -> Result<ObjectRef, VmError> {
|
||||
self.allocate(EndorCellObject::new(value))
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -5,38 +5,6 @@ use objects::{
|
||||
Value, VmError,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn dictionary_text_and_managed_string_keys_share_entries_and_keep_first_identity() {
|
||||
let mut store = ObjectStore::new().unwrap();
|
||||
let dict = store.new_dict().unwrap();
|
||||
for text in ["", "camera", "中文🙂"] {
|
||||
let key = store.new_string(text).unwrap();
|
||||
store
|
||||
.dict_set(dict, Value::Object(key), Value::Bool(false))
|
||||
.unwrap();
|
||||
store
|
||||
.dict_set(dict, Value::Text(text.into()), Value::Bool(true))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store.dict_get_name(dict, text).unwrap(),
|
||||
Some(Value::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
store.dict_items(dict).unwrap().last().unwrap().0,
|
||||
Value::Object(key)
|
||||
);
|
||||
assert_eq!(
|
||||
store.dict_get(dict, &Value::Object(key)).unwrap(),
|
||||
Some(Value::Bool(true))
|
||||
);
|
||||
store.release(key).unwrap();
|
||||
assert!(store.dict_delete(dict, &Value::Text(text.into())).unwrap());
|
||||
assert_eq!(store.dict_get_name(dict, text).unwrap(), None);
|
||||
}
|
||||
store.release(dict).unwrap();
|
||||
assert_eq!(store.object_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_storage_preserves_identity_validates_edges_and_collects_cycles() {
|
||||
let mut store = ObjectStore::new().unwrap();
|
||||
|
||||
@@ -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 边界显式窄化的接口,不更改原数。
|
||||
|
||||
这里的“各类数字”指上述五种可表示形式,不包括符号表达式、无限展开的精确 π、带单位数量、任意位宽二进制浮点等全部数学对象;这些可在未来扩展表示协议。
|
||||
|
||||
|
||||
@@ -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 距离,避免大分支错跳到参数前缀。每次真实循环迭代仍消耗同一累计预算并支持已有协作暂停。
|
||||
|
||||
@@ -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 的累计指令预算,不建立第二套执行器。
|
||||
|
||||
@@ -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 写在块内。
|
||||
|
||||
Reference in New Issue
Block a user