mirror of
https://github.com/voltrevo/ValueScript.git
synced 2026-01-14 16:08:02 -05:00
Kal (refactor)
This commit is contained in:
@@ -214,7 +214,10 @@ impl FunctionCompiler {
|
||||
}
|
||||
|
||||
pub fn release_reg(&mut self, reg: &Register) {
|
||||
self.reg_allocator.release(reg);
|
||||
// Note: We no longer release registers back into the name allocator. See
|
||||
// `NameAllocator::release` for more information.
|
||||
// self.reg_allocator.release(reg);
|
||||
|
||||
self.current.body.push(FnLine::Release(reg.clone()));
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,11 @@ impl RegAllocator {
|
||||
Register::named(name)
|
||||
}
|
||||
|
||||
// TODO: We used to release names back into the allocator a lot. However, the optimizer now
|
||||
// extends lifetimes, which means re-purposing a register is no longer safe. The optimizer needs
|
||||
// to do its own analysis to restore register re-use, because we're now using way too many.
|
||||
// Anyway, marking this as dead code for now but maybe it should just be removed.
|
||||
#[allow(dead_code)]
|
||||
pub fn release(&mut self, reg: &Register) {
|
||||
match reg.is_named() {
|
||||
true => self.alloc.release(®.name),
|
||||
|
||||
500
valuescript_compiler/src/optimization/kal.rs
Normal file
500
valuescript_compiler/src/optimization/kal.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
use num_bigint::BigInt;
|
||||
use valuescript_vm::{
|
||||
operations,
|
||||
vs_object::VsObject,
|
||||
vs_value::{ToVal, Val},
|
||||
};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
asm::{self, Builtin, Number, Pointer, Register, Value},
|
||||
instruction::Instruction,
|
||||
};
|
||||
|
||||
use super::try_to_kal::TryToKal;
|
||||
|
||||
/**
|
||||
* Kal: Knowledge about a Val.
|
||||
*
|
||||
* This is used by the optimizer to make simplifications. It's a broader and more complex version
|
||||
* of Val, since every (non-external) Val can be represented as a Kal, and Kal can also represent
|
||||
* partial information about a Val, such as being a number or being equal to another register.
|
||||
*
|
||||
* This is similar to a type system. However, a type system has the constraint of needing to be
|
||||
* consistent and sensible so the programmer can use it. Kal has the advantage of only being used
|
||||
* for optimization, so we can do a lot more heuristic things like knowing when an index is within
|
||||
* the bounds of an array (without needing to nail down exactly when and why we know that in a
|
||||
* consistent way). It is also 100% mandatory that Kal is always accurate/sound (otherwise we'll
|
||||
* change program behavior due to believing false things), whereas sometimes type systems (notably
|
||||
* TypeScript) are not.
|
||||
*/
|
||||
#[derive(Clone)]
|
||||
pub enum Kal {
|
||||
Unknown,
|
||||
Void,
|
||||
Undefined,
|
||||
Null,
|
||||
Bool(bool),
|
||||
Number(Number),
|
||||
BigInt(BigInt),
|
||||
String(String),
|
||||
Array(Box<Array>),
|
||||
Object(Box<Object>),
|
||||
Register(Register),
|
||||
Pointer(Pointer),
|
||||
Builtin(Builtin),
|
||||
}
|
||||
|
||||
impl Default for Kal {
|
||||
fn default() -> Self {
|
||||
Kal::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Array {
|
||||
pub values: Vec<Kal>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Object {
|
||||
pub properties: Vec<(Kal, Kal)>,
|
||||
}
|
||||
|
||||
impl Kal {
|
||||
fn visit_kals_mut<F>(&mut self, visit: &mut F)
|
||||
where
|
||||
F: FnMut(&mut Kal) -> (),
|
||||
{
|
||||
visit(self);
|
||||
|
||||
match self {
|
||||
Kal::Array(array) => {
|
||||
for item in &mut array.values {
|
||||
item.visit_kals_mut(visit);
|
||||
}
|
||||
}
|
||||
Kal::Object(object) => {
|
||||
for (k, v) in &mut object.properties {
|
||||
k.visit_kals_mut(visit);
|
||||
v.visit_kals_mut(visit);
|
||||
}
|
||||
}
|
||||
Kal::Unknown => {}
|
||||
Kal::Void => {}
|
||||
Kal::Undefined => {}
|
||||
Kal::Null => {}
|
||||
Kal::Bool(..) => {}
|
||||
Kal::Number(..) => {}
|
||||
Kal::BigInt(..) => {}
|
||||
Kal::String(..) => {}
|
||||
Kal::Register(..) => {}
|
||||
Kal::Pointer(..) => {}
|
||||
Kal::Builtin(..) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_value(value: &Value) -> Self {
|
||||
match value {
|
||||
Value::Void => Kal::Void,
|
||||
Value::Undefined => Kal::Undefined,
|
||||
Value::Null => Kal::Null,
|
||||
Value::Bool(bool) => Kal::Bool(*bool),
|
||||
Value::Number(Number(x)) => Kal::Number(Number(*x)),
|
||||
Value::BigInt(bi) => Kal::BigInt(bi.clone()),
|
||||
Value::String(string) => Kal::String(string.clone()),
|
||||
Value::Array(array) => Kal::Array(Box::new(Array {
|
||||
values: array.values.iter().map(Kal::from_value).collect(),
|
||||
})),
|
||||
Value::Object(object) => Kal::Object(Box::new(Object {
|
||||
properties: object
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(k, v)| (Kal::from_value(k), Kal::from_value(v)))
|
||||
.collect(),
|
||||
})),
|
||||
Value::Register(reg) => Kal::Register(reg.clone()),
|
||||
Value::Pointer(p) => Kal::Pointer(p.clone()),
|
||||
Value::Builtin(b) => Kal::Builtin(b.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Option<Value> {
|
||||
match self {
|
||||
Kal::Unknown => None,
|
||||
Kal::Void => Some(Value::Void),
|
||||
Kal::Undefined => Some(Value::Undefined),
|
||||
Kal::Null => Some(Value::Null),
|
||||
Kal::Bool(x) => Some(Value::Bool(*x)),
|
||||
Kal::Number(Number(x)) => Some(Value::Number(Number(*x))),
|
||||
Kal::BigInt(x) => Some(Value::BigInt(x.clone())),
|
||||
Kal::String(x) => Some(Value::String(x.clone())),
|
||||
Kal::Array(x) => Some(Value::Array(Box::new(asm::Array {
|
||||
values: {
|
||||
let mut values = Vec::<asm::Value>::new();
|
||||
|
||||
for k in &x.values {
|
||||
match k.to_value() {
|
||||
Some(v) => values.push(v),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
},
|
||||
}))),
|
||||
Kal::Object(x) => Some(Value::Object(Box::new(asm::Object {
|
||||
properties: {
|
||||
let mut properties = Vec::<(asm::Value, asm::Value)>::new();
|
||||
|
||||
for (k, v) in &x.properties {
|
||||
let k = match k.to_value() {
|
||||
Some(k) => k,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let v = match v.to_value() {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
properties.push((k, v));
|
||||
}
|
||||
|
||||
properties
|
||||
},
|
||||
}))),
|
||||
Kal::Register(x) => Some(Value::Register(x.clone())),
|
||||
Kal::Pointer(x) => Some(Value::Pointer(x.clone())),
|
||||
Kal::Builtin(x) => Some(Value::Builtin(x.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_to_val(self) -> Option<Val> {
|
||||
Some(match self {
|
||||
Kal::Unknown => return None,
|
||||
Kal::Undefined => Val::Undefined,
|
||||
Kal::Null => Val::Null,
|
||||
Kal::Bool(b) => b.to_val(),
|
||||
Kal::Number(Number(n)) => n.to_val(),
|
||||
Kal::BigInt(n) => n.to_val(),
|
||||
Kal::String(s) => s.to_val(),
|
||||
Kal::Array(arr) => {
|
||||
let mut result = Vec::<Val>::new();
|
||||
|
||||
for value in arr.values {
|
||||
result.push(value.try_to_val()?);
|
||||
}
|
||||
|
||||
result.to_val()
|
||||
}
|
||||
Kal::Object(obj) => {
|
||||
let mut string_map = BTreeMap::<String, Val>::new();
|
||||
|
||||
for (key, value) in obj.properties {
|
||||
string_map.insert(key.try_to_val()?.to_string(), value.try_to_val()?);
|
||||
}
|
||||
|
||||
VsObject {
|
||||
string_map,
|
||||
symbol_map: Default::default(),
|
||||
prototype: None,
|
||||
}
|
||||
.to_val()
|
||||
}
|
||||
|
||||
Kal::Void | Kal::Register(..) | Kal::Pointer(..) | Kal::Builtin(..) => {
|
||||
return None;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FnState {
|
||||
pub mutable_this_established: bool,
|
||||
pub registers: BTreeMap<String, Kal>,
|
||||
}
|
||||
|
||||
impl FnState {
|
||||
fn get_mut(&mut self, reg_name: String) -> &mut Kal {
|
||||
self.registers.entry(reg_name).or_default()
|
||||
}
|
||||
|
||||
fn get(&mut self, reg_name: String) -> &Kal {
|
||||
self.get_mut(reg_name)
|
||||
}
|
||||
|
||||
pub fn set(&mut self, reg_name: String, kal: Kal) {
|
||||
*self.get_mut(reg_name.clone()) = kal;
|
||||
self.handle_reg_changed(®_name);
|
||||
}
|
||||
|
||||
fn handle_reg_changed(&mut self, changed_reg: &String) {
|
||||
for kal in self.registers.values_mut() {
|
||||
kal.visit_kals_mut(&mut |sub_kal| {
|
||||
if let Kal::Register(reg) = sub_kal {
|
||||
if reg.name == *changed_reg {
|
||||
*sub_kal = Kal::Unknown;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_instruction(&mut self, instr: &mut Instruction) {
|
||||
use Instruction::*;
|
||||
|
||||
match instr {
|
||||
End => {}
|
||||
Mov(arg, dst) => {
|
||||
let arg = self.eval_arg(arg);
|
||||
self.set(dst.name.clone(), arg);
|
||||
}
|
||||
|
||||
OpInc(reg) => {
|
||||
// TODO: Use apply_binary_op?
|
||||
|
||||
let new_value = match self.get(reg.name.clone()) {
|
||||
Kal::Number(Number(x)) => Kal::Number(Number(x + 1.0)),
|
||||
Kal::BigInt(x) => Kal::BigInt(x + BigInt::from(1)),
|
||||
_ => Kal::Unknown,
|
||||
};
|
||||
|
||||
self.set(reg.name.clone(), new_value);
|
||||
}
|
||||
OpDec(reg) => {
|
||||
// TODO: Use apply_binary_op?
|
||||
|
||||
let new_value = match self.get(reg.name.clone()) {
|
||||
Kal::Number(Number(x)) => Kal::Number(Number(x - 1.0)),
|
||||
Kal::BigInt(x) => Kal::BigInt(x - BigInt::from(1)),
|
||||
_ => Kal::Unknown,
|
||||
};
|
||||
|
||||
self.set(reg.name.clone(), new_value);
|
||||
}
|
||||
|
||||
OpNot(a1, dst) => self.apply_unary_op(a1, dst, operations::op_not),
|
||||
OpBitNot(a1, dst) => self.apply_unary_op(a1, dst, operations::op_bit_not),
|
||||
TypeOf(a1, dst) => self.apply_unary_op(a1, dst, operations::op_typeof),
|
||||
UnaryPlus(a1, dst) => self.apply_unary_op(a1, dst, operations::op_unary_plus),
|
||||
UnaryMinus(a1, dst) => self.apply_unary_op(a1, dst, operations::op_unary_minus),
|
||||
Import(a1, dst) | ImportStar(a1, dst) | Cat(a1, dst) => {
|
||||
self.eval_arg(a1);
|
||||
|
||||
// TODO: cat
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
|
||||
Yield(a1, dst) | YieldStar(a1, dst) => {
|
||||
self.eval_arg(a1);
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
|
||||
Throw(a1) => {
|
||||
self.eval_arg(a1);
|
||||
}
|
||||
|
||||
OpPlus(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_plus),
|
||||
OpMinus(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_minus),
|
||||
OpMul(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_mul),
|
||||
OpDiv(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_div),
|
||||
OpMod(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_mod),
|
||||
OpExp(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_exp),
|
||||
OpEq(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_eq),
|
||||
OpNe(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_ne),
|
||||
OpTripleEq(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_triple_eq),
|
||||
OpTripleNe(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_triple_ne),
|
||||
OpAnd(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_and),
|
||||
OpOr(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_or),
|
||||
OpLess(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_less),
|
||||
OpLessEq(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_less_eq),
|
||||
OpGreater(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_greater),
|
||||
OpGreaterEq(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_greater_eq),
|
||||
OpNullishCoalesce(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_nullish_coalesce)
|
||||
}
|
||||
OpOptionalChain(a1, a2, dst) => {
|
||||
self.eval_arg(a1);
|
||||
self.eval_arg(a2);
|
||||
|
||||
// self.apply_binary_op(a1, a2, dst, operations::op_optional_chain)
|
||||
// TODO: op_optional_chain takes mut lhs to optimize, but breaks this pattern
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
OpBitAnd(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_bit_and),
|
||||
OpBitOr(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_bit_or),
|
||||
OpBitXor(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_bit_xor),
|
||||
OpLeftShift(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_left_shift),
|
||||
OpRightShift(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_right_shift),
|
||||
OpRightShiftUnsigned(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_right_shift_unsigned)
|
||||
}
|
||||
InstanceOf(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_instance_of),
|
||||
In(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_in),
|
||||
|
||||
Call(a1, a2, dst)
|
||||
| Bind(a1, a2, dst)
|
||||
| Sub(a1, a2, dst)
|
||||
| SubMov(a1, a2, dst)
|
||||
| New(a1, a2, dst) => {
|
||||
self.eval_arg(a1);
|
||||
self.eval_arg(a2);
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
|
||||
Apply(a, this, a3, dst) | SubCall(this, a, a3, dst) | ThisSubCall(this, a, a3, dst) => {
|
||||
// TODO: Consider ordering here (only .take consideration (?))
|
||||
self.eval_arg(a);
|
||||
self.eval_arg(a3);
|
||||
|
||||
self.set(this.name.clone(), Kal::Unknown);
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
|
||||
ConstSubCall(a1, a2, a3, dst) => {
|
||||
self.eval_arg(a1);
|
||||
self.eval_arg(a2);
|
||||
self.eval_arg(a3);
|
||||
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
|
||||
JmpIf(a1, _) => {
|
||||
self.eval_arg(a1);
|
||||
}
|
||||
|
||||
Jmp(_) => {}
|
||||
SetCatch(_, _) => {}
|
||||
UnsetCatch => {}
|
||||
RequireMutableThis => {
|
||||
self.mutable_this_established = true;
|
||||
}
|
||||
Next(iter, dst) => {
|
||||
self.set(iter.name.clone(), Kal::Unknown);
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
UnpackIterRes(iter_res, value_reg, done) => {
|
||||
self.set(iter_res.name.clone(), Kal::Void);
|
||||
self.set(value_reg.name.clone(), Kal::Unknown);
|
||||
self.set(done.name.clone(), Kal::Unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_arg(&mut self, arg: &mut Value) -> Kal {
|
||||
match arg {
|
||||
Value::Void
|
||||
| Value::Undefined
|
||||
| Value::Null
|
||||
| Value::Bool(_)
|
||||
| Value::Number(_)
|
||||
| Value::BigInt(_)
|
||||
| Value::String(_)
|
||||
| Value::Pointer(_)
|
||||
| Value::Builtin(_) => Kal::from_value(arg),
|
||||
Value::Array(array) => {
|
||||
let mut values = Vec::<Kal>::new();
|
||||
|
||||
for item in &mut array.values {
|
||||
values.push(self.eval_arg(item));
|
||||
}
|
||||
|
||||
Kal::Array(Box::new(Array { values }))
|
||||
}
|
||||
Value::Object(object) => {
|
||||
let mut properties = Vec::<(Kal, Kal)>::new();
|
||||
|
||||
for (k, v) in &mut object.properties {
|
||||
let k = self.eval_arg(k);
|
||||
let v = self.eval_arg(v);
|
||||
|
||||
properties.push((k, v));
|
||||
}
|
||||
|
||||
Kal::Object(Box::new(Object { properties }))
|
||||
}
|
||||
Value::Register(reg) => {
|
||||
let kal = self.get(reg.name.clone()).clone();
|
||||
|
||||
let is_take = reg.take;
|
||||
|
||||
if is_take {
|
||||
self.set(reg.name.clone(), Kal::Void);
|
||||
}
|
||||
|
||||
match kal.to_value() {
|
||||
Some(v) => {
|
||||
// Note: if `reg.take` was true, then we're removing that take operation from the
|
||||
// register here. This should be ok because well-formed programs should never read from
|
||||
// a taken register, but we might need to revise this in the future. It definitely means
|
||||
// it's possible for the optimizer to break hand-written assembly.
|
||||
*arg = v;
|
||||
|
||||
kal
|
||||
}
|
||||
None => match is_take {
|
||||
true => Kal::Unknown,
|
||||
false => Kal::Register(reg.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_unary_op(&mut self, arg: &mut Value, dst: &Register, op: fn(input: &Val) -> Val) {
|
||||
match self.apply_unary_op_impl(arg, dst, op) {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_unary_op_impl(
|
||||
&mut self,
|
||||
arg: &mut Value,
|
||||
dst: &Register,
|
||||
op: fn(input: &Val) -> Val,
|
||||
) -> Option<()> {
|
||||
let arg = self.eval_arg(arg).try_to_val()?;
|
||||
let kal = op(&arg).try_to_kal()?;
|
||||
|
||||
self.set(dst.name.clone(), kal);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn apply_binary_op(
|
||||
&mut self,
|
||||
left: &mut Value,
|
||||
right: &mut Value,
|
||||
dst: &Register,
|
||||
op: fn(left: &Val, right: &Val) -> Result<Val, Val>,
|
||||
) {
|
||||
match self.apply_binary_op_impl(left, right, dst, op) {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
self.set(dst.name.clone(), Kal::Unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_binary_op_impl(
|
||||
&mut self,
|
||||
left: &mut Value,
|
||||
right: &mut Value,
|
||||
dst: &Register,
|
||||
op: fn(left: &Val, right: &Val) -> Result<Val, Val>,
|
||||
) -> Option<()> {
|
||||
let left = self.eval_arg(left).try_to_val()?;
|
||||
let right = self.eval_arg(right).try_to_val()?;
|
||||
let kal = op(&left, &right).ok()?.try_to_kal()?;
|
||||
|
||||
self.set(dst.name.clone(), kal);
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
mod collapse_pointers_of_pointers;
|
||||
mod extract_constants;
|
||||
mod kal;
|
||||
mod optimize;
|
||||
mod remove_meta_lines;
|
||||
mod remove_noops;
|
||||
mod shake_tree;
|
||||
mod simplify;
|
||||
pub mod try_to_kal;
|
||||
pub mod try_to_val;
|
||||
pub mod try_to_value;
|
||||
|
||||
pub use optimize::optimize;
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::mem::take;
|
||||
|
||||
use num_bigint::BigInt;
|
||||
use valuescript_vm::{operations, vs_value::Val};
|
||||
use crate::asm::{DefinitionContent, FnLine, Function, Instruction, Module, Register};
|
||||
|
||||
use crate::{
|
||||
asm::{DefinitionContent, FnLine, Function, Instruction, Module, Number, Register, Value},
|
||||
instruction::InstructionFieldMut,
|
||||
TryToVal,
|
||||
};
|
||||
|
||||
use super::try_to_value::TryToValue;
|
||||
use super::kal::FnState;
|
||||
|
||||
pub fn simplify(module: &mut Module) {
|
||||
for defn in &mut module.definitions {
|
||||
@@ -22,367 +15,128 @@ pub fn simplify(module: &mut Module) {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FnState {
|
||||
mutable_this_established: bool,
|
||||
registers: HashMap<String, Value>,
|
||||
fn handle_mutation_releases(body: &mut Vec<FnLine>, i: usize) {
|
||||
let mut calls = Vec::<(Register, usize)>::new();
|
||||
|
||||
match &mut body[i] {
|
||||
FnLine::Instruction(instr) => {
|
||||
let mut skips_needed = 0;
|
||||
|
||||
instr.visit_registers_mut_rev(&mut |rvm| {
|
||||
skips_needed += 1;
|
||||
|
||||
if rvm.write && !rvm.read {
|
||||
calls.push((rvm.register.clone(), skips_needed));
|
||||
}
|
||||
});
|
||||
}
|
||||
FnLine::Release(_) | FnLine::Label(_) | FnLine::Empty | FnLine::Comment(_) => {}
|
||||
};
|
||||
|
||||
for (released_reg, skips) in calls {
|
||||
handle_release(body, i, released_reg.clone(), skips);
|
||||
}
|
||||
}
|
||||
|
||||
impl FnState {
|
||||
fn clear(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
fn handle_release(
|
||||
body: &mut Vec<FnLine>,
|
||||
i: usize,
|
||||
released_reg: Register,
|
||||
skips_needed: usize,
|
||||
) -> bool {
|
||||
let mut j = i + 1;
|
||||
let mut skips = 0;
|
||||
let mut taken = false;
|
||||
while j > 0 {
|
||||
j -= 1;
|
||||
|
||||
fn simplify_line(&self, line: &mut FnLine) {
|
||||
match line {
|
||||
FnLine::Instruction(instr) => {
|
||||
if let Instruction::RequireMutableThis = instr {
|
||||
if self.mutable_this_established {
|
||||
*line = FnLine::Comment(line.to_string());
|
||||
}
|
||||
} else {
|
||||
instr.visit_fields_mut(&mut |field| match field {
|
||||
InstructionFieldMut::Value(arg) => {
|
||||
self.simplify_arg(arg);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
FnLine::Label(..) | FnLine::Empty | FnLine::Comment(..) | FnLine::Release(..) => {}
|
||||
let instr = match &mut body[j] {
|
||||
FnLine::Instruction(instr) => instr,
|
||||
FnLine::Label(_) => return false,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if is_jmp_instr(instr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify_arg(&self, arg: &mut Value) {
|
||||
arg.visit_values_mut(&mut |value| {
|
||||
if let Value::Register(reg) = value {
|
||||
if let Some(new_value) = self.registers.get(®.name) {
|
||||
*value = new_value.clone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let mut write_found = false;
|
||||
|
||||
fn apply_line(&mut self, line: &FnLine) {
|
||||
match line {
|
||||
FnLine::Instruction(instr) => match instr {
|
||||
Instruction::End => {}
|
||||
Instruction::Mov(a1, dst) => {
|
||||
self.set_register(dst, Some(a1.clone()));
|
||||
if !taken {
|
||||
instr.visit_registers_mut_rev(&mut |rvm| {
|
||||
if skips < skips_needed {
|
||||
skips += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
Instruction::OpInc(reg) => {
|
||||
// TODO: Use apply_binary_op?
|
||||
|
||||
let new_value = match self.registers.get(®.name) {
|
||||
Some(Value::Number(Number(x))) => Some(Value::Number(Number(x + 1.0))),
|
||||
Some(Value::BigInt(x)) => Some(Value::BigInt(x + BigInt::from(1))),
|
||||
Some(_) | None => None,
|
||||
};
|
||||
|
||||
self.set_register(reg, new_value);
|
||||
}
|
||||
Instruction::OpDec(reg) => {
|
||||
let new_value = match self.registers.get(®.name) {
|
||||
Some(Value::Number(Number(x))) => Some(Value::Number(Number(x - 1.0))),
|
||||
Some(Value::BigInt(x)) => Some(Value::BigInt(x - BigInt::from(1))),
|
||||
Some(_) | None => None,
|
||||
};
|
||||
|
||||
self.set_register(reg, new_value);
|
||||
if rvm.register.name != released_reg.name {
|
||||
return;
|
||||
}
|
||||
|
||||
Instruction::OpNot(a1, dst) => self.apply_unary_op(a1, dst, operations::op_not),
|
||||
Instruction::OpBitNot(a1, dst) => self.apply_unary_op(a1, dst, operations::op_bit_not),
|
||||
Instruction::TypeOf(a1, dst) => self.apply_unary_op(a1, dst, operations::op_typeof),
|
||||
Instruction::UnaryPlus(a1, dst) => self.apply_unary_op(a1, dst, operations::op_unary_plus),
|
||||
Instruction::UnaryMinus(a1, dst) => {
|
||||
self.apply_unary_op(a1, dst, operations::op_unary_minus)
|
||||
}
|
||||
Instruction::Import(_a1, dst)
|
||||
| Instruction::ImportStar(_a1, dst)
|
||||
| Instruction::Cat(_a1, dst) => {
|
||||
// TODO: cat
|
||||
self.set_register(dst, None);
|
||||
if !taken && !rvm.write {
|
||||
*rvm.register = rvm.register.take();
|
||||
taken = true;
|
||||
}
|
||||
|
||||
Instruction::Yield(_a1, dst) | Instruction::YieldStar(_a1, dst) => {
|
||||
self.set_register(dst, None)
|
||||
}
|
||||
if !write_found && rvm.write {
|
||||
write_found = true;
|
||||
|
||||
Instruction::Throw(_a1) => {}
|
||||
|
||||
Instruction::OpPlus(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_plus),
|
||||
Instruction::OpMinus(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_minus)
|
||||
}
|
||||
Instruction::OpMul(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_mul),
|
||||
Instruction::OpDiv(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_div),
|
||||
Instruction::OpMod(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_mod),
|
||||
Instruction::OpExp(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_exp),
|
||||
Instruction::OpEq(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_eq),
|
||||
Instruction::OpNe(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_ne),
|
||||
Instruction::OpTripleEq(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_triple_eq)
|
||||
}
|
||||
Instruction::OpTripleNe(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_triple_ne)
|
||||
}
|
||||
Instruction::OpAnd(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_and),
|
||||
Instruction::OpOr(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_or),
|
||||
Instruction::OpLess(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_less),
|
||||
Instruction::OpLessEq(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_less_eq)
|
||||
}
|
||||
Instruction::OpGreater(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_greater)
|
||||
}
|
||||
Instruction::OpGreaterEq(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_greater_eq)
|
||||
}
|
||||
Instruction::OpNullishCoalesce(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_nullish_coalesce)
|
||||
}
|
||||
Instruction::OpOptionalChain(_a1, _a2, dst) => {
|
||||
// self.apply_binary_op(a1, a2, dst, operations::op_optional_chain)
|
||||
// TODO: op_optional_chain takes mut lhs to optimize, but breaks this pattern
|
||||
self.set_register(dst, None);
|
||||
}
|
||||
Instruction::OpBitAnd(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_bit_and)
|
||||
}
|
||||
Instruction::OpBitOr(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_bit_or)
|
||||
}
|
||||
Instruction::OpBitXor(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_bit_xor)
|
||||
}
|
||||
Instruction::OpLeftShift(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_left_shift)
|
||||
}
|
||||
Instruction::OpRightShift(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_right_shift)
|
||||
}
|
||||
Instruction::OpRightShiftUnsigned(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_right_shift_unsigned)
|
||||
}
|
||||
Instruction::InstanceOf(a1, a2, dst) => {
|
||||
self.apply_binary_op(a1, a2, dst, operations::op_instance_of)
|
||||
}
|
||||
Instruction::In(a1, a2, dst) => self.apply_binary_op(a1, a2, dst, operations::op_in),
|
||||
|
||||
Instruction::Call(_a1, _a2, dst)
|
||||
| Instruction::Bind(_a1, _a2, dst)
|
||||
| Instruction::Sub(_a1, _a2, dst)
|
||||
| Instruction::SubMov(_a1, _a2, dst)
|
||||
| Instruction::New(_a1, _a2, dst) => {
|
||||
self.set_register(dst, None);
|
||||
}
|
||||
|
||||
Instruction::Apply(_a, this, _a3, dst)
|
||||
| Instruction::SubCall(this, _a, _a3, dst)
|
||||
| Instruction::ThisSubCall(this, _a, _a3, dst) => {
|
||||
self.set_register(this, None);
|
||||
self.set_register(dst, None);
|
||||
}
|
||||
|
||||
Instruction::ConstSubCall(_a1, _a2, _a3, dst) => self.set_register(dst, None),
|
||||
|
||||
Instruction::JmpIf(_a1, _) => {}
|
||||
|
||||
Instruction::Jmp(_) => {}
|
||||
Instruction::SetCatch(_, _) => {}
|
||||
Instruction::UnsetCatch => {}
|
||||
Instruction::RequireMutableThis => {
|
||||
self.mutable_this_established = true;
|
||||
}
|
||||
Instruction::Next(iter, dst) => {
|
||||
self.set_register(iter, None);
|
||||
self.set_register(dst, None);
|
||||
}
|
||||
Instruction::UnpackIterRes(iter_res, value_reg, done) => {
|
||||
self.set_register(iter_res, None);
|
||||
self.set_register(value_reg, None);
|
||||
self.set_register(done, None);
|
||||
}
|
||||
},
|
||||
FnLine::Label(..) => self.clear(),
|
||||
FnLine::Empty | FnLine::Comment(..) => {}
|
||||
FnLine::Release(reg) => {
|
||||
self.set_register(reg, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_register(&mut self, reg: &Register, value: Option<Value>) {
|
||||
let mut registers_to_clear = HashSet::<String>::new();
|
||||
|
||||
for (k, v) in &mut self.registers {
|
||||
v.visit_values_mut(&mut |value| {
|
||||
if let Value::Register(reg_value) = value {
|
||||
if reg_value.name == reg.name {
|
||||
registers_to_clear.insert(k.clone());
|
||||
if !rvm.read && !taken {
|
||||
*rvm.register = Register::ignore();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for reg_to_clear in registers_to_clear {
|
||||
self.registers.remove(®_to_clear);
|
||||
}
|
||||
|
||||
match value {
|
||||
Some(value) => self.registers.insert(reg.name.clone(), value),
|
||||
None => self.registers.remove(®.name),
|
||||
};
|
||||
}
|
||||
|
||||
fn apply_unary_op(&mut self, arg: &Value, dst: &Register, op: fn(input: &Val) -> Val) {
|
||||
match self.apply_unary_op_impl(arg, dst, op) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
self.set_register(dst, None);
|
||||
}
|
||||
if write_found {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_unary_op_impl(
|
||||
&mut self,
|
||||
arg: &Value,
|
||||
dst: &Register,
|
||||
op: fn(input: &Val) -> Val,
|
||||
) -> Result<(), Val> {
|
||||
let arg = arg.clone().try_to_val()?;
|
||||
let value = op(&arg).try_to_value()?;
|
||||
taken
|
||||
}
|
||||
|
||||
self.set_register(dst, Some(value));
|
||||
fn simplify_fn(mut state: FnState, fn_: &mut Function) {
|
||||
let mut pending_releases = Vec::<Register>::new();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_binary_op(
|
||||
&mut self,
|
||||
left: &Value,
|
||||
right: &Value,
|
||||
dst: &Register,
|
||||
op: fn(left: &Val, right: &Val) -> Result<Val, Val>,
|
||||
) {
|
||||
match self.apply_binary_op_impl(left, right, dst, op) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
self.set_register(dst, None);
|
||||
for i in 0..fn_.body.len() {
|
||||
if let FnLine::Instruction(Instruction::RequireMutableThis) = &fn_.body[i] {
|
||||
if state.mutable_this_established {
|
||||
fn_.body[i] = FnLine::Comment(fn_.body[i].to_string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_binary_op_impl(
|
||||
&mut self,
|
||||
left: &Value,
|
||||
right: &Value,
|
||||
dst: &Register,
|
||||
op: fn(left: &Val, right: &Val) -> Result<Val, Val>,
|
||||
) -> Result<(), Val> {
|
||||
let left = left.clone().try_to_val()?;
|
||||
let right = right.clone().try_to_val()?;
|
||||
let value = op(&left, &right)?.try_to_value()?;
|
||||
|
||||
self.set_register(dst, Some(value));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_releases(&self, body: &mut Vec<FnLine>, i: usize) {
|
||||
let mut calls = Vec::<(Register, usize)>::new();
|
||||
|
||||
match &mut body[i] {
|
||||
FnLine::Instruction(instr) => {
|
||||
let mut skips_needed = 0;
|
||||
|
||||
instr.visit_registers_mut_rev(&mut |rvm| {
|
||||
skips_needed += 1;
|
||||
|
||||
if rvm.write && !rvm.read {
|
||||
calls.push((rvm.register.clone(), skips_needed));
|
||||
}
|
||||
});
|
||||
if is_jmp_or_label(&fn_.body[i]) && i > 0 {
|
||||
for released_reg in take(&mut pending_releases) {
|
||||
handle_release(&mut fn_.body, i - 1, released_reg, 0);
|
||||
}
|
||||
FnLine::Release(released_reg) => {
|
||||
calls.push((released_reg.clone(), 0));
|
||||
}
|
||||
FnLine::Label(_) | FnLine::Empty | FnLine::Comment(_) => {}
|
||||
};
|
||||
|
||||
for (released_reg, skips) in calls {
|
||||
self.handle_releases_impl(body, i, released_reg, skips);
|
||||
}
|
||||
|
||||
match &mut fn_.body[i] {
|
||||
FnLine::Instruction(instr) => state.eval_instruction(instr),
|
||||
FnLine::Label(_) => state = FnState::default(),
|
||||
FnLine::Empty | FnLine::Comment(_) => {}
|
||||
FnLine::Release(reg) => pending_releases.push(reg.clone()),
|
||||
}
|
||||
|
||||
handle_mutation_releases(&mut fn_.body, i);
|
||||
}
|
||||
|
||||
fn handle_releases_impl(
|
||||
&self,
|
||||
body: &mut Vec<FnLine>,
|
||||
i: usize,
|
||||
released_reg: Register,
|
||||
skips_needed: usize,
|
||||
) {
|
||||
let mut j = i + 1;
|
||||
let mut skips = 0;
|
||||
let mut taken = false;
|
||||
while j > 0 {
|
||||
j -= 1;
|
||||
if !fn_.body.is_empty() {
|
||||
let last_i = fn_.body.len() - 1;
|
||||
|
||||
let instr = match &mut body[j] {
|
||||
FnLine::Instruction(instr) => instr,
|
||||
FnLine::Label(_) => return,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if is_jmp_instr(instr) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut write_found = false;
|
||||
|
||||
if !taken {
|
||||
instr.visit_registers_mut_rev(&mut |rvm| {
|
||||
if skips < skips_needed {
|
||||
skips += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if rvm.register.name != released_reg.name {
|
||||
return;
|
||||
}
|
||||
|
||||
if !taken && !rvm.write {
|
||||
*rvm.register = rvm.register.take();
|
||||
taken = true;
|
||||
}
|
||||
|
||||
if !write_found && rvm.write {
|
||||
write_found = true;
|
||||
|
||||
if !rvm.read && !taken {
|
||||
*rvm.register = Register::ignore();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if write_found {
|
||||
break;
|
||||
}
|
||||
for released_reg in pending_releases {
|
||||
handle_release(&mut fn_.body, last_i, released_reg, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify_fn(mut state: FnState, fn_: &mut Function) {
|
||||
for i in 0..fn_.body.len() {
|
||||
let line = &mut fn_.body[i];
|
||||
|
||||
state.simplify_line(line);
|
||||
state.apply_line(line);
|
||||
|
||||
state.handle_releases(&mut fn_.body, i);
|
||||
fn is_jmp_or_label(line: &FnLine) -> bool {
|
||||
match line {
|
||||
FnLine::Instruction(instr) => is_jmp_instr(instr),
|
||||
FnLine::Label(_) => true,
|
||||
FnLine::Empty | FnLine::Comment(_) | FnLine::Release(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
valuescript_compiler/src/optimization/try_to_kal.rs
Normal file
67
valuescript_compiler/src/optimization/try_to_kal.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use valuescript_vm::{vs_value::Val, VsSymbol};
|
||||
|
||||
use crate::asm::{Builtin, Number};
|
||||
|
||||
use super::kal::{Array, Kal, Object};
|
||||
|
||||
pub trait TryToKal {
|
||||
fn try_to_kal(&self) -> Option<Kal>;
|
||||
}
|
||||
|
||||
impl TryToKal for Val {
|
||||
fn try_to_kal(&self) -> Option<Kal> {
|
||||
Some(match self {
|
||||
Val::Void => Kal::Undefined,
|
||||
Val::Undefined => Kal::Undefined,
|
||||
Val::Null => Kal::Null,
|
||||
Val::Bool(b) => Kal::Bool(*b),
|
||||
Val::Number(n) => Kal::Number(Number(*n)),
|
||||
Val::BigInt(n) => Kal::BigInt(n.clone()),
|
||||
Val::Symbol(sym) => match sym {
|
||||
VsSymbol::ITERATOR => Kal::Builtin(Builtin {
|
||||
name: "SymbolIterator".to_string(),
|
||||
}),
|
||||
},
|
||||
Val::String(s) => Kal::String(s.to_string()),
|
||||
Val::Array(arr) => {
|
||||
let mut values = Vec::<Kal>::new();
|
||||
|
||||
for value in &arr.elements {
|
||||
values.push(value.try_to_kal()?);
|
||||
}
|
||||
|
||||
Kal::Array(Box::new(Array { values }))
|
||||
}
|
||||
Val::Object(obj) => {
|
||||
if obj.prototype.is_some() {
|
||||
// TODO: convert object with prototype to Kal
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut properties = Vec::<(Kal, Kal)>::new();
|
||||
|
||||
for (k, v) in &obj.symbol_map {
|
||||
let k = match k {
|
||||
VsSymbol::ITERATOR => Kal::Builtin(Builtin {
|
||||
name: "SymbolIterator".to_string(),
|
||||
}),
|
||||
};
|
||||
|
||||
properties.push((k, v.try_to_kal()?));
|
||||
}
|
||||
|
||||
for (k, v) in &obj.string_map {
|
||||
properties.push((Kal::String(k.clone()), v.try_to_kal()?));
|
||||
}
|
||||
|
||||
Kal::Object(Box::new(Object { properties }))
|
||||
}
|
||||
// TODO: support more of these
|
||||
Val::Function(..)
|
||||
| Val::Class(..)
|
||||
| Val::Static(..)
|
||||
| Val::Dynamic(..)
|
||||
| Val::CopyCounter(..) => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use valuescript_vm::{
|
||||
vs_value::{ToVal, Val},
|
||||
VsSymbol,
|
||||
};
|
||||
|
||||
use crate::asm::{Array, Builtin, Number, Object, Value};
|
||||
|
||||
pub trait TryToValue {
|
||||
fn try_to_value(&self) -> Result<Value, Val>;
|
||||
}
|
||||
|
||||
impl TryToValue for Val {
|
||||
fn try_to_value(&self) -> Result<Value, Val> {
|
||||
Ok(match self {
|
||||
Val::Void => Value::Undefined,
|
||||
Val::Undefined => Value::Undefined,
|
||||
Val::Null => Value::Null,
|
||||
Val::Bool(b) => Value::Bool(*b),
|
||||
Val::Number(n) => Value::Number(Number(*n)),
|
||||
Val::BigInt(n) => Value::BigInt(n.clone()),
|
||||
Val::Symbol(sym) => match sym {
|
||||
VsSymbol::ITERATOR => Value::Builtin(Builtin {
|
||||
name: "SymbolIterator".to_string(),
|
||||
}),
|
||||
},
|
||||
Val::String(s) => Value::String(s.to_string()),
|
||||
Val::Array(arr) => {
|
||||
let mut values = Vec::<Value>::new();
|
||||
|
||||
for value in &arr.elements {
|
||||
values.push(value.try_to_value()?);
|
||||
}
|
||||
|
||||
Value::Array(Box::new(Array { values }))
|
||||
}
|
||||
Val::Object(obj) => {
|
||||
if obj.prototype.is_some() {
|
||||
return Err("can't (yet?) convert object with prototype to Value".to_val());
|
||||
}
|
||||
|
||||
let mut properties = Vec::<(Value, Value)>::new();
|
||||
|
||||
for (k, v) in &obj.symbol_map {
|
||||
let k = match k {
|
||||
VsSymbol::ITERATOR => Value::Builtin(Builtin {
|
||||
name: "SymbolIterator".to_string(),
|
||||
}),
|
||||
};
|
||||
|
||||
properties.push((k, v.try_to_value()?));
|
||||
}
|
||||
|
||||
for (k, v) in &obj.string_map {
|
||||
properties.push((Value::String(k.clone()), v.try_to_value()?));
|
||||
}
|
||||
|
||||
Value::Object(Box::new(Object { properties }))
|
||||
}
|
||||
Val::Function(..)
|
||||
| Val::Class(..)
|
||||
| Val::Static(..)
|
||||
| Val::Dynamic(..)
|
||||
| Val::CopyCounter(..) => return Err("TODO: support more of these".to_val()),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user