Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ff1c68f5f |
Generated
+51
-1
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compiler"
|
name = "compiler"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -38,9 +44,53 @@ version = "2.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
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]]
|
[[package]]
|
||||||
name = "objects"
|
name = "objects"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peg"
|
name = "peg"
|
||||||
@@ -114,7 +164,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.9",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+121
-44
@@ -167,6 +167,16 @@ pub fn analyze(root: &AstRoot, options: &CompileOptions) -> Result<SymbolTable,
|
|||||||
StmtKind::Expression(expression) => {
|
StmtKind::Expression(expression) => {
|
||||||
root_scope.expression(expression, &globals)?
|
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(_) => {
|
StmtKind::Return(_) => {
|
||||||
return Err(error(statement.span, "return outside a function"));
|
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());
|
.push(parameter.name.text.clone());
|
||||||
builder.initialized.insert(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 {
|
match &statement.kind {
|
||||||
StmtKind::Binding(binding) => {
|
StmtKind::Binding(b) => {
|
||||||
declare(&mut locals, &binding.name.text, binding.name.span)?;
|
declare(locals, &b.name.text, b.name.span)?;
|
||||||
builder.scope.layout.locals.push(binding.name.text.clone());
|
layout.push(b.name.text.clone());
|
||||||
}
|
}
|
||||||
StmtKind::Assignment(assignment) => {
|
StmtKind::Assignment(a) => {
|
||||||
if locals.insert(assignment.name.text.clone()) {
|
if locals.insert(a.name.text.clone()) {
|
||||||
builder
|
layout.push(a.name.text.clone());
|
||||||
.scope
|
|
||||||
.layout
|
|
||||||
.locals
|
|
||||||
.push(assignment.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(_) => {
|
StmtKind::Declaration(_) => {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
statement.span,
|
statement.span,
|
||||||
@@ -231,39 +258,7 @@ fn function_scope(function: &Function, globals: &BTreeSet<String>) -> Result<Sco
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut returned = false;
|
Ok(())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Builder {
|
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(
|
fn expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
expression: &Expr,
|
expression: &Expr,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use endor_core::{
|
|||||||
CodeLayout, DecodedInstruction, EndorCode, EndorNumber, Instruction, ObjectRef, Opcode, Value,
|
CodeLayout, DecodedInstruction, EndorCode, EndorNumber, Instruction, ObjectRef, Opcode, Value,
|
||||||
Vm, VmError,
|
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 frontend::{Diagnostic, DiagnosticKind, SourceUnit, Span};
|
||||||
|
|
||||||
use crate::{NameBinding, Scope, ScopeKind, SymbolTable, diagnostic};
|
use crate::{NameBinding, Scope, ScopeKind, SymbolTable, diagnostic};
|
||||||
@@ -115,6 +115,7 @@ struct Emitter<'a> {
|
|||||||
source: &'a SourceUnit,
|
source: &'a SourceUnit,
|
||||||
instructions: Vec<Instruction>,
|
instructions: Vec<Instruction>,
|
||||||
constants: Vec<Value>,
|
constants: Vec<Value>,
|
||||||
|
jumps: Vec<(usize, usize)>,
|
||||||
children: Vec<(usize, CompiledCode)>,
|
children: Vec<(usize, CompiledCode)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ impl<'a> Emitter<'a> {
|
|||||||
source,
|
source,
|
||||||
instructions: Vec::new(),
|
instructions: Vec::new(),
|
||||||
constants: Vec::new(),
|
constants: Vec::new(),
|
||||||
|
jumps: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,6 +251,36 @@ impl<'a> Emitter<'a> {
|
|||||||
|
|
||||||
fn statement(&mut self, statement: &Stmt, symbols: &SymbolTable) -> Result<(), Diagnostic> {
|
fn statement(&mut self, statement: &Stmt, symbols: &SymbolTable) -> Result<(), Diagnostic> {
|
||||||
match &statement.kind {
|
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::Binding(binding) => self.store_binding(&binding.name.text, &binding.value)?,
|
||||||
StmtKind::Assignment(assignment) => {
|
StmtKind::Assignment(assignment) => {
|
||||||
self.store_binding(&assignment.name.text, &assignment.value)?
|
self.store_binding(&assignment.name.text, &assignment.value)?
|
||||||
@@ -308,22 +340,38 @@ impl<'a> Emitter<'a> {
|
|||||||
self.expression(object)?;
|
self.expression(object)?;
|
||||||
self.emit(Opcode::LoadAttr, self.name_index(&name.text))?;
|
self.emit(Opcode::LoadAttr, self.name_index(&name.text))?;
|
||||||
}
|
}
|
||||||
ExprKind::Unary { .. } => {
|
ExprKind::Unary { operand, .. } => {
|
||||||
// Literal sign folding preserves arbitrarily large integers
|
if let Some(spelling) = signed_literal(expression, false) {
|
||||||
// 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)?;
|
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(_) => {
|
ExprKind::Symbol(_) => {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
@@ -345,7 +393,42 @@ impl<'a> Emitter<'a> {
|
|||||||
self.constant(Value::Number(value))
|
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
|
let line = self
|
||||||
.scope
|
.scope
|
||||||
.span
|
.span
|
||||||
|
|||||||
@@ -262,11 +262,22 @@ fn declared_field_checks_cover_construction_inheritance_writes_and_gc() {
|
|||||||
r#"
|
r#"
|
||||||
class Base { count: Number }
|
class Base { count: Number }
|
||||||
class Child: Base { label: String }
|
class Child: Base { label: String }
|
||||||
value = Child(9, "ok")
|
|
||||||
class Holder { child: Base }
|
class Holder { child: Base }
|
||||||
holder = Holder(value)
|
|
||||||
class Unknown { value: Missing }
|
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,
|
ParseMode::Interactive,
|
||||||
);
|
);
|
||||||
let Value::Object(value) = vm
|
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 Link { next: Link }
|
||||||
class Seed: Link {}
|
class Seed: Link {}
|
||||||
"#,
|
"#,
|
||||||
ParseMode::Interactive,
|
ParseMode::Module,
|
||||||
);
|
);
|
||||||
// A recursive nonnullable field can be initialized by the trusted host in two steps.
|
// A recursive nonnullable field can be initialized by the trusted host in two steps.
|
||||||
let Value::Object(link) = vm
|
let Value::Object(link) = vm
|
||||||
|
|||||||
@@ -345,18 +345,10 @@ fn extended_operands_work_for_large_local_and_constant_tables() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_execution_features_are_compile_errors_instead_of_fake_results() {
|
fn unsupported_execution_features_are_compile_errors_instead_of_fake_results() {
|
||||||
for (text, mode) in [
|
let source = EndorReader::from_text("<unsupported>", ".title");
|
||||||
("1 + 2", ParseMode::Expression),
|
let error =
|
||||||
(".title", ParseMode::Expression),
|
compile_source(&source, ParseMode::Expression, &CompileOptions::default()).unwrap_err();
|
||||||
(
|
|
||||||
"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);
|
assert_eq!(error.diagnostic.kind, DiagnosticKind::Compilation);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -482,3 +474,79 @@ fn source_signatures_survive_template_load_and_enable_runtime_checks() {
|
|||||||
assert_eq!(vm.object_count(), baseline);
|
assert_eq!(vm.object_count(), baseline);
|
||||||
cleanup(&mut vm, code, globals);
|
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(
|
fn compare_values(
|
||||||
&self,
|
&self,
|
||||||
left: &Value,
|
left: &Value,
|
||||||
right: &Value,
|
right: &Value,
|
||||||
operation: CompareOp,
|
operation: CompareOp,
|
||||||
) -> Result<bool, VmError> {
|
) -> Result<bool, VmError> {
|
||||||
if matches!(left, Value::Number(_)) || matches!(right, Value::Number(_)) {
|
let (left_number, right_number) = (self.number_value(left)?, self.number_value(right)?);
|
||||||
return Err(VmError::UnsupportedOperation("numeric comparison"));
|
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)?) {
|
if let (Some(left), Some(right)) = (self.text_value(left)?, self.text_value(right)?) {
|
||||||
return Ok(match operation {
|
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 => {
|
Opcode::CompareOp => {
|
||||||
let left = Self::stack_value(frame, frame.stack.len() - 2)?;
|
let left = Self::stack_value(frame, frame.stack.len() - 2)?;
|
||||||
let right = Self::top_value(frame)?;
|
let right = Self::top_value(frame)?;
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ pub use objects::{
|
|||||||
RootTypes, TypeConstructor, TypedMethodRequirement,
|
RootTypes, TypeConstructor, TypedMethodRequirement,
|
||||||
};
|
};
|
||||||
pub use objects::{
|
pub use objects::{
|
||||||
CallArity, CallEffect, CallSignature, CallType, ChangeBatch, ChangeKey, CodeLayout, CompareOp,
|
ArithmeticOp, CallArity, CallEffect, CallSignature, CallType, ChangeBatch, ChangeKey,
|
||||||
ContextNativeFunction, DecodedInstruction, EndorBaseExceptionObject, EndorCellObject,
|
CodeLayout, CompareOp, ContextNativeFunction, DecodedInstruction, EndorBaseExceptionObject,
|
||||||
EndorCode, EndorCodeObject, EndorDecimal, EndorDictObject, EndorFunctionObject, EndorInteger,
|
EndorCellObject, EndorCode, EndorCodeObject, EndorDecimal, EndorDictObject,
|
||||||
EndorNativeCall, EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorObject,
|
EndorFunctionObject, EndorInteger, EndorNativeCall, EndorNativeFunctionObject, EndorNumber,
|
||||||
EndorRational, EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, NativeCall,
|
EndorNumberObject, EndorObject, EndorRational, EndorReal, EndorStringObject,
|
||||||
NativeCallable, NativeContext, NativeFunction, NativeResult, ObjectChange, ObjectHeader,
|
EndorTracebackEntry, Instruction, NativeCall, NativeCallable, NativeContext, NativeFunction,
|
||||||
ObjectPayload, ObjectRef, Opcode, ReadAccess, ReadDependency, ReadSet, Record, SourceSignature,
|
NativeResult, ObjectChange, ObjectHeader, ObjectPayload, ObjectRef, Opcode, ReadAccess,
|
||||||
TrackedReads, Value, VmError, WeakObjectRef,
|
ReadDependency, ReadSet, Record, SourceSignature, TrackedReads, Value, VmError, WeakObjectRef,
|
||||||
};
|
};
|
||||||
pub use vm::Vm;
|
pub use vm::Vm;
|
||||||
|
|||||||
@@ -1593,7 +1593,22 @@ fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() {
|
|||||||
use endor_core::CoroutineProgress;
|
use endor_core::CoroutineProgress;
|
||||||
let mut vm = Vm::new().unwrap();
|
let mut vm = Vm::new().unwrap();
|
||||||
let globals = vm.new_dict().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
|
let function = vm
|
||||||
.new_function("limited", code, globals, vec![], vec![])
|
.new_function("limited", code, globals, vec![], vec![])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1603,7 +1618,9 @@ fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() {
|
|||||||
&[],
|
&[],
|
||||||
ExecutionLimits {
|
ExecutionLimits {
|
||||||
max_frames: 4,
|
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();
|
.unwrap();
|
||||||
|
|||||||
@@ -21,12 +21,16 @@ module Endor {
|
|||||||
StmtKind = Binding(Binding binding)
|
StmtKind = Binding(Binding binding)
|
||||||
| Assignment(Assignment assignment)
|
| Assignment(Assignment assignment)
|
||||||
| MemberAssignment(MemberAssignment assignment)
|
| MemberAssignment(MemberAssignment assignment)
|
||||||
|
| If(IfStatement statement)
|
||||||
|
| While(WhileStatement statement)
|
||||||
| Return(ReturnStatement statement)
|
| Return(ReturnStatement statement)
|
||||||
| Expression(Expr expression)
|
| Expression(Expr expression)
|
||||||
| Declaration(Decl declaration)
|
| Declaration(Decl declaration)
|
||||||
Binding = (Name name, Expr value, span span)
|
Binding = (Name name, Expr value, span span)
|
||||||
Assignment = (Name name, Expr value, span span)
|
Assignment = (Name name, Expr value, span span)
|
||||||
MemberAssignment = (Expr object, 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)
|
ReturnStatement = (Expr? value, span span)
|
||||||
InteractiveInput = (Stmt* statements, span span)
|
InteractiveInput = (Stmt* statements, span span)
|
||||||
|
|
||||||
@@ -41,5 +45,5 @@ module Endor {
|
|||||||
| Member(Expr object, Name name)
|
| Member(Expr object, Name name)
|
||||||
| Call(Expr callee, Expr* arguments)
|
| Call(Expr callee, Expr* arguments)
|
||||||
UnaryOp = Negate
|
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",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "urn:endor:ast:sha256:c7c10c6a61158810fa55044f1aa79f3558ac18ca3be59cbf7f2499b5add8df5b",
|
"$id": "urn:endor:ast:sha256:21b0349ac295d9bc8cedcad8f07d8e2d45cf220e19e7c2fad10269555fcfd71a",
|
||||||
"$ref": "#/$defs/AstDocument",
|
"$ref": "#/$defs/AstDocument",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"Span": {
|
"Span": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"const": "urn:endor:ast:sha256:c7c10c6a61158810fa55044f1aa79f3558ac18ca3be59cbf7f2499b5add8df5b"
|
"const": "urn:endor:ast:sha256:21b0349ac295d9bc8cedcad8f07d8e2d45cf220e19e7c2fad10269555fcfd71a"
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"$ref": "#/$defs/AstRoot"
|
"$ref": "#/$defs/AstRoot"
|
||||||
@@ -354,6 +354,38 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"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",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -468,6 +500,56 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"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": {
|
"ReturnStatement": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -742,7 +824,13 @@
|
|||||||
"Add",
|
"Add",
|
||||||
"Subtract",
|
"Subtract",
|
||||||
"Multiply",
|
"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.
|
// 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 interface Span { start: number; end: number; }
|
||||||
export type AstDocument = { schema: typeof AST_SCHEMA_ID; root: AstRoot; };
|
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; };
|
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 Parameter = { name: Name; type_name: Name; span: Span; };
|
||||||
export type Block = { statements: Array<Stmt>; span: Span; };
|
export type Block = { statements: Array<Stmt>; span: Span; };
|
||||||
export type Stmt = { kind: StmtKind; 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 Binding = { name: Name; value: Expr; span: Span; };
|
||||||
export type Assignment = { 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 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 ReturnStatement = { value?: Expr | null; span: Span; };
|
||||||
export type InteractiveInput = { statements: Array<Stmt>; span: Span; };
|
export type InteractiveInput = { statements: Array<Stmt>; span: Span; };
|
||||||
export type Expr = { kind: ExprKind; 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 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 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 {
|
export interface Visitor {
|
||||||
visitSpan?(node: Span): boolean | void;
|
visitSpan?(node: Span): boolean | void;
|
||||||
visitAstDocument?(node: AstDocument): boolean | void;
|
visitAstDocument?(node: AstDocument): boolean | void;
|
||||||
@@ -40,6 +42,8 @@ visitStmtKind?(node: StmtKind): boolean | void;
|
|||||||
visitBinding?(node: Binding): boolean | void;
|
visitBinding?(node: Binding): boolean | void;
|
||||||
visitAssignment?(node: Assignment): boolean | void;
|
visitAssignment?(node: Assignment): boolean | void;
|
||||||
visitMemberAssignment?(node: MemberAssignment): boolean | void;
|
visitMemberAssignment?(node: MemberAssignment): boolean | void;
|
||||||
|
visitIfStatement?(node: IfStatement): boolean | void;
|
||||||
|
visitWhileStatement?(node: WhileStatement): boolean | void;
|
||||||
visitReturnStatement?(node: ReturnStatement): boolean | void;
|
visitReturnStatement?(node: ReturnStatement): boolean | void;
|
||||||
visitInteractiveInput?(node: InteractiveInput): boolean | void;
|
visitInteractiveInput?(node: InteractiveInput): boolean | void;
|
||||||
visitExpr?(node: Expr): boolean | void;
|
visitExpr?(node: Expr): boolean | void;
|
||||||
@@ -135,6 +139,12 @@ break;
|
|||||||
case "MemberAssignment":
|
case "MemberAssignment":
|
||||||
walkMemberAssignment(node.data, visitor);
|
walkMemberAssignment(node.data, visitor);
|
||||||
break;
|
break;
|
||||||
|
case "If":
|
||||||
|
walkIfStatement(node.data, visitor);
|
||||||
|
break;
|
||||||
|
case "While":
|
||||||
|
walkWhileStatement(node.data, visitor);
|
||||||
|
break;
|
||||||
case "Return":
|
case "Return":
|
||||||
walkReturnStatement(node.data, visitor);
|
walkReturnStatement(node.data, visitor);
|
||||||
break;
|
break;
|
||||||
@@ -165,6 +175,19 @@ walkName(node.name, visitor);
|
|||||||
walkExpr(node.value, visitor);
|
walkExpr(node.value, visitor);
|
||||||
walkSpan(node.span, 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 {
|
export function walkReturnStatement(node: ReturnStatement, visitor: Visitor): void {
|
||||||
if (visitor.visitReturnStatement?.(node) === false) return;
|
if (visitor.visitReturnStatement?.(node) === false) return;
|
||||||
if (node.value != null) walkExpr(node.value, visitor);
|
if (node.value != null) walkExpr(node.value, visitor);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use super::Spanned;
|
use super::Spanned;
|
||||||
use crate::Span;
|
use crate::Span;
|
||||||
pub const AST_SCHEMA_ID: &str =
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AstDocument {
|
pub struct AstDocument {
|
||||||
@@ -141,6 +141,8 @@ pub enum StmtKind {
|
|||||||
Binding(Binding),
|
Binding(Binding),
|
||||||
Assignment(Assignment),
|
Assignment(Assignment),
|
||||||
MemberAssignment(MemberAssignment),
|
MemberAssignment(MemberAssignment),
|
||||||
|
If(IfStatement),
|
||||||
|
While(WhileStatement),
|
||||||
Return(ReturnStatement),
|
Return(ReturnStatement),
|
||||||
Expression(Expr),
|
Expression(Expr),
|
||||||
Declaration(Decl),
|
Declaration(Decl),
|
||||||
@@ -151,6 +153,8 @@ impl Spanned for StmtKind {
|
|||||||
Self::Binding(node) => node.span(),
|
Self::Binding(node) => node.span(),
|
||||||
Self::Assignment(node) => node.span(),
|
Self::Assignment(node) => node.span(),
|
||||||
Self::MemberAssignment(node) => node.span(),
|
Self::MemberAssignment(node) => node.span(),
|
||||||
|
Self::If(node) => node.span(),
|
||||||
|
Self::While(node) => node.span(),
|
||||||
Self::Return(node) => node.span(),
|
Self::Return(node) => node.span(),
|
||||||
Self::Expression(node) => node.span(),
|
Self::Expression(node) => node.span(),
|
||||||
Self::Declaration(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)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[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 struct ReturnStatement {
|
||||||
pub value: Option<Expr>,
|
pub value: Option<Expr>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
@@ -263,4 +292,10 @@ pub enum BinaryOp {
|
|||||||
Subtract,
|
Subtract,
|
||||||
Multiply,
|
Multiply,
|
||||||
Divide,
|
Divide,
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterEqual,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Assignment, AstDocument, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr,
|
Assignment, AstDocument, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr,
|
||||||
ExprKind, Function, InteractiveInput, MemberAssignment, Name, Parameter, Program,
|
ExprKind, Function, IfStatement, InteractiveInput, MemberAssignment, Name, Parameter, Program,
|
||||||
ReturnStatement, Spanned, Stmt, StmtKind, UnaryOp,
|
ReturnStatement, Spanned, Stmt, StmtKind, UnaryOp, WhileStatement,
|
||||||
};
|
};
|
||||||
use crate::Span;
|
use crate::Span;
|
||||||
pub trait Visitor {
|
pub trait Visitor {
|
||||||
@@ -102,6 +102,18 @@ pub trait Visitor {
|
|||||||
self.leave_node();
|
self.leave_node();
|
||||||
result
|
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> {
|
fn visit_return_statement(&mut self, node: &ReturnStatement) -> Result<(), Self::Error> {
|
||||||
self.enter_node(node.span())?;
|
self.enter_node(node.span())?;
|
||||||
let result = walk_return_statement(self, node);
|
let result = walk_return_statement(self, node);
|
||||||
@@ -243,6 +255,12 @@ pub fn walk_stmt_kind<V: Visitor + ?Sized>(
|
|||||||
StmtKind::MemberAssignment(value) => {
|
StmtKind::MemberAssignment(value) => {
|
||||||
visitor.visit_member_assignment(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) => {
|
StmtKind::Return(value) => {
|
||||||
visitor.visit_return_statement(value)?;
|
visitor.visit_return_statement(value)?;
|
||||||
}
|
}
|
||||||
@@ -280,6 +298,27 @@ pub fn walk_member_assignment<V: Visitor + ?Sized>(
|
|||||||
visitor.visit_span(&node.span)?;
|
visitor.visit_span(&node.span)?;
|
||||||
Ok(())
|
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>(
|
pub fn walk_return_statement<V: Visitor + ?Sized>(
|
||||||
visitor: &mut V,
|
visitor: &mut V,
|
||||||
node: &ReturnStatement,
|
node: &ReturnStatement,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ peg::parser! {
|
|||||||
= declaration:declaration() {
|
= declaration:declaration() {
|
||||||
Stmt { span: declaration.span, kind: StmtKind::Declaration(declaration) }
|
Stmt { span: declaration.span, kind: StmtKind::Declaration(declaration) }
|
||||||
}
|
}
|
||||||
|
/ control:control_statement() { control }
|
||||||
/ binding:binding() { binding_statement(binding) }
|
/ binding:binding() { binding_statement(binding) }
|
||||||
/ assignment:member_assignment() { assignment }
|
/ assignment:member_assignment() { assignment }
|
||||||
/ assignment:assignment() { assignment }
|
/ assignment:assignment() { assignment }
|
||||||
@@ -104,13 +105,25 @@ peg::parser! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rule local_statement() -> Stmt
|
rule local_statement() -> Stmt
|
||||||
= binding:binding() { binding_statement(binding) }
|
= control:control_statement() { control }
|
||||||
|
/ binding:binding() { binding_statement(binding) }
|
||||||
/ assignment:member_assignment() { assignment }
|
/ assignment:member_assignment() { assignment }
|
||||||
/ assignment:assignment() { assignment }
|
/ assignment:assignment() { assignment }
|
||||||
/ !return_keyword() expression:expression() {
|
/ !return_keyword() expression:expression() {
|
||||||
Stmt { span: expression.span, kind: StmtKind::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
|
rule assignment() -> Stmt
|
||||||
= name:name() equal() value:expression() {
|
= name:name() equal() value:expression() {
|
||||||
let span = name.span.through(value.span);
|
let span = name.span.through(value.span);
|
||||||
@@ -140,6 +153,13 @@ peg::parser! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub rule expression() -> Expr = precedence! {
|
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:(@) plus() right:@ { binary(left, BinaryOp::Add, right) }
|
||||||
left:(@) minus() right:@ { binary(left, BinaryOp::Subtract, right) }
|
left:(@) minus() right:@ { binary(left, BinaryOp::Subtract, right) }
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ peg::parser! {
|
|||||||
/ ":" { TokenKind::Colon }
|
/ ":" { TokenKind::Colon }
|
||||||
/ "," { TokenKind::Comma }
|
/ "," { TokenKind::Comma }
|
||||||
/ "." { TokenKind::Dot }
|
/ "." { TokenKind::Dot }
|
||||||
|
/ "==" { TokenKind::EqualEqual }
|
||||||
|
/ "!=" { TokenKind::NotEqual }
|
||||||
|
/ "<=" { TokenKind::LessEqual }
|
||||||
|
/ ">=" { TokenKind::GreaterEqual }
|
||||||
|
/ "<" { TokenKind::Less }
|
||||||
|
/ ">" { TokenKind::Greater }
|
||||||
/ "=" { TokenKind::Equal }
|
/ "=" { TokenKind::Equal }
|
||||||
/ "+" { TokenKind::Plus }
|
/ "+" { TokenKind::Plus }
|
||||||
/ "-" { TokenKind::Minus }
|
/ "-" { TokenKind::Minus }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::ast::{
|
use crate::ast::{
|
||||||
Assignment, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr, ExprKind, Function,
|
Assignment, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr, ExprKind, Function,
|
||||||
InteractiveInput, MemberAssignment, Name, Parameter, Program, ReturnStatement, Stmt, StmtKind,
|
IfStatement, InteractiveInput, MemberAssignment, Name, Parameter, Program, ReturnStatement,
|
||||||
UnaryOp,
|
Stmt, StmtKind, UnaryOp, WhileStatement,
|
||||||
};
|
};
|
||||||
use crate::lexer::decode_string;
|
use crate::lexer::decode_string;
|
||||||
use crate::{Diagnostic, DiagnosticKind, Span, Token, TokenKind, lex};
|
use crate::{Diagnostic, DiagnosticKind, Span, Token, TokenKind, lex};
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ pub enum TokenKind {
|
|||||||
Comma,
|
Comma,
|
||||||
Dot,
|
Dot,
|
||||||
Equal,
|
Equal,
|
||||||
|
EqualEqual,
|
||||||
|
NotEqual,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterEqual,
|
||||||
Plus,
|
Plus,
|
||||||
Minus,
|
Minus,
|
||||||
Star,
|
Star,
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax(
|
|||||||
}
|
}
|
||||||
for source in [
|
for source in [
|
||||||
"", "x y", "1 +", "+1", ".5", "1.", "f(1,)", "f(,1)", "f(1 2)", "(1", "obj.", "[1]",
|
"", "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}");
|
assert!(parse_expression(source).is_err(), "{source}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ license.workspace = true
|
|||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
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::PopJumpIfTrue
|
||||||
| Opcode::ToBool
|
| Opcode::ToBool
|
||||||
| Opcode::UnaryNot
|
| Opcode::UnaryNot
|
||||||
|
| Opcode::BinaryArithmetic
|
||||||
|
| Opcode::UnaryNegate
|
||||||
| Opcode::CompareOp
|
| Opcode::CompareOp
|
||||||
| Opcode::LoadDeref
|
| Opcode::LoadDeref
|
||||||
| Opcode::LoadClosure
|
| Opcode::LoadClosure
|
||||||
@@ -354,6 +356,7 @@ fn verify(
|
|||||||
| Opcode::LoadAttr
|
| Opcode::LoadAttr
|
||||||
| Opcode::StoreAttr => index < layout.names.len(),
|
| Opcode::StoreAttr => index < layout.names.len(),
|
||||||
Opcode::CompareOp => compare_operation(argument).is_some(),
|
Opcode::CompareOp => compare_operation(argument).is_some(),
|
||||||
|
Opcode::BinaryArithmetic => argument <= 3,
|
||||||
Opcode::Copy => argument >= 1,
|
Opcode::Copy => argument >= 1,
|
||||||
Opcode::Swap => argument >= 2,
|
Opcode::Swap => argument >= 2,
|
||||||
Opcode::JumpForward
|
Opcode::JumpForward
|
||||||
@@ -394,8 +397,12 @@ fn verify(
|
|||||||
| Opcode::StoreDeref
|
| Opcode::StoreDeref
|
||||||
| Opcode::PopJumpIfFalse
|
| Opcode::PopJumpIfFalse
|
||||||
| Opcode::PopJumpIfTrue => (1, 1, 0),
|
| Opcode::PopJumpIfTrue => (1, 1, 0),
|
||||||
Opcode::ToBool | Opcode::UnaryNot | Opcode::LoadAttr | Opcode::Await => (1, 1, 1),
|
Opcode::ToBool
|
||||||
Opcode::CompareOp => (2, 2, 1),
|
| Opcode::UnaryNot
|
||||||
|
| Opcode::LoadAttr
|
||||||
|
| Opcode::Await
|
||||||
|
| Opcode::UnaryNegate => (1, 1, 1),
|
||||||
|
Opcode::CompareOp | Opcode::BinaryArithmetic => (2, 2, 1),
|
||||||
Opcode::StoreAttr => (2, 2, 0),
|
Opcode::StoreAttr => (2, 2, 0),
|
||||||
Opcode::Copy => (argument, 0, 1),
|
Opcode::Copy => (argument, 0, 1),
|
||||||
Opcode::Swap => (argument, 0, 0),
|
Opcode::Swap => (argument, 0, 0),
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ pub enum Opcode {
|
|||||||
MakeFunction = 28,
|
MakeFunction = 28,
|
||||||
BuildClass = 29,
|
BuildClass = 29,
|
||||||
Await = 30,
|
Await = 30,
|
||||||
|
BinaryArithmetic = 31,
|
||||||
|
UnaryNegate = 32,
|
||||||
ExtendedArg = 255,
|
ExtendedArg = 255,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +75,8 @@ impl Opcode {
|
|||||||
28 => Self::MakeFunction,
|
28 => Self::MakeFunction,
|
||||||
29 => Self::BuildClass,
|
29 => Self::BuildClass,
|
||||||
30 => Self::Await,
|
30 => Self::Await,
|
||||||
|
31 => Self::BinaryArithmetic,
|
||||||
|
32 => Self::UnaryNegate,
|
||||||
255 => Self::ExtendedArg,
|
255 => Self::ExtendedArg,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
@@ -88,6 +92,7 @@ impl Opcode {
|
|||||||
| Self::ToBool
|
| Self::ToBool
|
||||||
| Self::UnaryNot
|
| Self::UnaryNot
|
||||||
| Self::Await
|
| Self::Await
|
||||||
|
| Self::UnaryNegate
|
||||||
| Self::BuildClass
|
| Self::BuildClass
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ pub use native::{
|
|||||||
NativeContext, NativeResult,
|
NativeContext, NativeResult,
|
||||||
};
|
};
|
||||||
pub use number::{
|
pub use number::{
|
||||||
EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal,
|
ArithmeticOp, EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational,
|
||||||
|
EndorReal,
|
||||||
};
|
};
|
||||||
pub use object::{
|
pub use object::{
|
||||||
AwaitPoll, CompareOp, EndorNativeCall, EndorNativeFunctionObject, EndorObject, NativeCall,
|
AwaitPoll, CompareOp, EndorNativeCall, EndorNativeFunctionObject, EndorObject, NativeCall,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Lossless number representations. Arithmetic and numeric equivalence are
|
//! 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::any::Any;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -39,7 +40,16 @@ impl EndorInteger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_i64(&self) -> Option<i64> {
|
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"))
|
Err(VmError::UnsupportedOperation("numeric hash"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compare(&self, other: &dyn ObjectPayload, _op: CompareOp) -> Result<Option<bool>, VmError> {
|
fn compare(&self, other: &dyn ObjectPayload, op: CompareOp) -> Result<Option<bool>, VmError> {
|
||||||
if other.as_any().is::<Self>() {
|
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 Err(VmError::UnsupportedOperation("numeric comparison"));
|
||||||
}
|
}
|
||||||
|
return self.value.numeric_compare(&other.value, op).map(Some);
|
||||||
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,3 +331,222 @@ fn split_sign(text: &str) -> (bool, &str) {
|
|||||||
fn is_digits(text: &str) -> bool {
|
fn is_digits(text: &str) -> bool {
|
||||||
!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())
|
!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> {
|
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);
|
return Ok(left == right);
|
||||||
}
|
}
|
||||||
match (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> {
|
pub fn new_cell(&mut self, value: Option<Value>) -> Result<ObjectRef, VmError> {
|
||||||
self.allocate(EndorCellObject::new(value))
|
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,
|
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]
|
#[test]
|
||||||
fn standalone_storage_preserves_identity_validates_edges_and_collects_cycles() {
|
fn standalone_storage_preserves_identity_validates_edges_and_collects_cycles() {
|
||||||
let mut store = ObjectStore::new().unwrap();
|
let mut store = ObjectStore::new().unwrap();
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ fn main() -> Result<(), endor_core::VmError> {
|
|||||||
|
|
||||||
`EndorInteger`、`EndorDecimal`、`EndorRational` 是内部表示结构,不是三种语言数字类。构造器验证输入;分数不能有零分母;十进制指数不转换为机器整数,所以不会因指数很长而溢出。构造任意大整数或精确小数时传文本;已经被宿主转换为 f64 而丢失的精度无法恢复。整数的 `as_i64` 是显式、可失败的窄化读取,不会静默截断。
|
`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 或完整源码行号表。
|
运行错误仍返回 `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。
|
在独立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。
|
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 |
|
| 字符串、整数、十进制常量 | 放入常量表并 LoadConst;数字经现有 EndorNumber 解析,不转换为 f64 |
|
||||||
| 数字字面量前的一个或多个负号 | 折叠符号为精确常量;不是任意表达式的一元算术 |
|
| 一元负号 | 字面量折叠保留拼写;其他表达式发出 UnaryNegate |
|
||||||
|
| 四则运算、比较 | BinaryArithmetic / CompareOp;实数精确语义见 builtins |
|
||||||
|
| if / else、while | 函数局部初始化按分支合流,循环可执行零次;相对跳转经过 EXTENDED_ARG 布局解析 |
|
||||||
| 局部读取/let/赋值 | LoadFast/StoreFast;参数和局部名称在 CodeLayout 中 |
|
| 局部读取/let/赋值 | LoadFast/StoreFast;参数和局部名称在 CodeLayout 中 |
|
||||||
| 全局读取/赋值 | LoadGlobal/StoreGlobal,使用该代码对象的 names 表 |
|
| 全局读取/赋值 | LoadGlobal/StoreGlobal,使用该代码对象的 names 表 |
|
||||||
| 函数声明 | 子代码模板 → LoadConst(code) → MakeFunction(0, 0) → StoreGlobal |
|
| 函数声明 | 子代码模板 → LoadConst(code) → MakeFunction(0, 0) → StoreGlobal |
|
||||||
@@ -38,7 +40,7 @@ CompileOptions.globals 显式声明宿主会提供的 globals/builtins 名称
|
|||||||
|
|
||||||
源码 parser 接受绑定/名称及属性赋值/表达式语句与可选末尾带值 return;空 return 仍仅由 AST 调用方构造。模块初始化发布函数/类并返回 nil,不自动调用 main。Lyra 宿主选择入口属于宿主行为。
|
源码 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主题。
|
Await表达式参与普通名称/初始化分析,先编译operand,再发出无参数Await opcode。该链路保存实际结果的栈位置,不通过回放函数实现等待;没有新增async声明或静态awaitable类型检查。语法与运行上下文见frontend及bytecode主题。
|
||||||
|
|
||||||
|
控制流中的局部变量仍为函数作用域。if 两边都能到达后续语句时,仅保留两边都已初始化的名字;一边 return 时采用仍可到达路径的初始化集合。while 中新初始化的名字不能被当作循环后一定存在。循环共享正常调用、只读调用或 coroutine 的累计指令预算,不建立第二套执行器。
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函
|
|||||||
|
|
||||||
符号字面量、名称与成员分别保留为独立 AST 结构。true、false、Nil 等词目前只是普通名称,没有额外的源码字面量或内建值绑定。
|
符号字面量、名称与成员分别保留为独立 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)。
|
`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 节点,宿主不改写输入。
|
裸赋值 `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章节。
|
`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