From 05efdfd5173bb23c7a2da64f06ffa84c289d2e21 Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Thu, 29 Jun 2023 11:05:12 +1000 Subject: [PATCH] Add structural comparison --- README.md | 9 +- inputs/passing/structureCmp.ts | 39 +++++ valuescript_vm/src/operations.rs | 143 +++++++++++++++++- website/src/playground/files/index.ts | 1 + .../root/tutorial/structuralComparison.ts | 22 +++ 5 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 inputs/passing/structureCmp.ts create mode 100644 website/src/playground/files/root/tutorial/structuralComparison.ts diff --git a/README.md b/README.md index f7101ec8..e1bee8f8 100644 --- a/README.md +++ b/README.md @@ -434,6 +434,10 @@ not the subset of ValueScript that has actually been implemented. - Iterators - Spread operator on iterables - Generators +- Structural comparison (not yet for class instances) + - `{} === {} -> true` + - JS: `-> false` + - This is a value semantics thing - objects don't have identity - Many unusual JS things: - `[] + [] -> ""` - `[10, 1, 3].sort() -> [1, 10, 3]` @@ -468,10 +472,7 @@ not the subset of ValueScript that has actually been implemented. - Uses `.toString()` to get the source code and compiles and runs it in WebAssembly - C libraries, and bindings for python etc -- Structural comparison - - `{} === {} -> true` - - JS: `-> false` - - This is a value semantics thing - objects don't have identity +- Comparison of class instances and functions - Object spreading - Rest params - Async functions diff --git a/inputs/passing/structureCmp.ts b/inputs/passing/structureCmp.ts new file mode 100644 index 00000000..d8fc83bb --- /dev/null +++ b/inputs/passing/structureCmp.ts @@ -0,0 +1,39 @@ +//! test_output(24) + +export default function () { + let count = 0; + + const cases: [unknown, unknown, { loose: boolean; strict: boolean }][] = [ + [[], [], { loose: true, strict: true }], + [[], [1], { loose: false, strict: false }], + [[1, 2, 3], [1, 2, 3], { loose: true, strict: true }], + [[1, 2, 3], [1, "2", 3], { loose: true, strict: false }], + [{}, {}, { loose: true, strict: true }], + [{}, { x: 1 }, { loose: false, strict: false }], + [{}, { [Symbol.iterator]: 1 }, { loose: false, strict: false }], + [{ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 3 }, { loose: true, strict: true }], + [{ x: 1, y: 2, z: 3 }, { x: 1, y: "2", z: 3 }, { + loose: true, + strict: false, + }], + [[[[[[1]]]]], [[[[[1]]]]], { loose: true, strict: true }], + [[[[[["1"]]]]], [[[[[1]]]]], { loose: true, strict: false }], + [null, undefined, { loose: true, strict: false }], + ]; + + for (const [left, right, { loose, strict }] of cases) { + if ((left == right) === loose) { + count++; + } else { + throw new Error(`Expected ${left} == ${right} to be ${loose}`); + } + + if ((left === right) === strict) { + count++; + } else { + throw new Error(`Expected ${left} === ${right} to be ${strict}`); + } + } + + return count; +} diff --git a/valuescript_vm/src/operations.rs b/valuescript_vm/src/operations.rs index 90dc5b87..c5e3ffbb 100644 --- a/valuescript_vm/src/operations.rs +++ b/valuescript_vm/src/operations.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::mem::take; use std::rc::Rc; use std::str::FromStr; @@ -124,8 +125,64 @@ pub fn op_eq_impl(left: &Val, right: &Val) -> Result { (Val::Bool(left_bool), Val::Bool(right_bool)) => left_bool == right_bool, (Val::Number(left_number), Val::Number(right_number)) => left_number == right_number, (Val::String(left_string), Val::String(right_string)) => left_string == right_string, + (Val::Number(left_number), Val::String(right_string)) => { + left_number.to_string() == **right_string + } + (Val::String(left_string), Val::Number(right_number)) => { + **left_string == right_number.to_string() + } (Val::BigInt(left_bigint), Val::BigInt(right_bigint)) => left_bigint == right_bigint, + (Val::Array(left_array), Val::Array(right_array)) => 'b: { + if &**left_array as *const _ == &**right_array as *const _ { + break 'b true; + } + + let len = left_array.elements.len(); + + if right_array.elements.len() != len { + break 'b false; + } + + for (left_item, right_item) in left_array.elements.iter().zip(right_array.elements.iter()) { + if !op_eq_impl(left_item, right_item)? { + break 'b false; + } + } + + true + } + (Val::Object(left_object), Val::Object(right_object)) => 'b: { + if left_object.prototype.is_some() || right_object.prototype.is_some() { + return Err("TODO: class instance comparison".to_internal_error()); + } + + if &**left_object as *const _ == &**right_object as *const _ { + break 'b true; + } + + if !compare_btrees( + &left_object.string_map, + &right_object.string_map, + op_eq_impl, + )? { + break 'b false; + } + + if !compare_btrees( + &left_object.symbol_map, + &right_object.symbol_map, + op_eq_impl, + )? { + break 'b false; + } + + true + } _ => { + if left.is_truthy() != right.is_truthy() { + return Ok(false); + } + return Err( format!( "TODO: op== with other types ({}, {})", @@ -133,11 +190,34 @@ pub fn op_eq_impl(left: &Val, right: &Val) -> Result { right.codify() ) .to_internal_error(), - ) + ); } }) } +fn compare_btrees( + left: &BTreeMap, + right: &BTreeMap, + cmp: Cmp, +) -> Result +where + Cmp: Fn(&Val, &Val) -> Result, +{ + let symbol_len = left.len(); + + if right.len() != symbol_len { + return Ok(false); + } + + for (left_value, right_value) in left.values().zip(right.values()) { + if !cmp(left_value, right_value)? { + return Ok(false); + } + } + + Ok(true) +} + pub fn op_eq(left: &Val, right: &Val) -> Result { Ok(Val::Bool(op_eq_impl(left, right)?)) } @@ -154,12 +234,65 @@ pub fn op_triple_eq_impl(left: &Val, right: &Val) -> Result { (Val::Number(left_number), Val::Number(right_number)) => left_number == right_number, (Val::String(left_string), Val::String(right_string)) => left_string == right_string, (Val::BigInt(left_bigint), Val::BigInt(right_bigint)) => left_bigint == right_bigint, - _ => { + (Val::Array(left_array), Val::Array(right_array)) => 'b: { + if &**left_array as *const _ == &**right_array as *const _ { + break 'b true; + } + + let len = left_array.elements.len(); + + if right_array.elements.len() != len { + break 'b false; + } + + for (left_item, right_item) in left_array.elements.iter().zip(right_array.elements.iter()) { + if !op_triple_eq_impl(left_item, right_item)? { + break 'b false; + } + } + + true + } + (Val::Object(left_object), Val::Object(right_object)) => 'b: { + if left_object.prototype.is_some() || right_object.prototype.is_some() { + return Err("TODO: class instance comparison".to_internal_error()); + } + + if &**left_object as *const _ == &**right_object as *const _ { + break 'b true; + } + + if !compare_btrees( + &left_object.string_map, + &right_object.string_map, + op_triple_eq_impl, + )? { + break 'b false; + } + + if !compare_btrees( + &left_object.symbol_map, + &right_object.symbol_map, + op_triple_eq_impl, + )? { + break 'b false; + } + + true + } + (Val::Static(..) | Val::Dynamic(..) | Val::CopyCounter(..), _) + | (_, Val::Static(..) | Val::Dynamic(..) | Val::CopyCounter(..)) => { if left.typeof_() != right.typeof_() { - false - } else { - return Err("TODO: op=== with other types".to_internal_error()); + return Ok(false); } + + return Err( + format!("TODO: op=== with special types ({}, {})", left, right).to_internal_error(), + ); + } + _ => { + assert!(left.typeof_() != right.typeof_()); + false } }) } diff --git a/website/src/playground/files/index.ts b/website/src/playground/files/index.ts index e37ab424..48d25d88 100644 --- a/website/src/playground/files/index.ts +++ b/website/src/playground/files/index.ts @@ -11,6 +11,7 @@ export const orderedFiles = [ "/tutorial/revertOnCatch.ts", "/tutorial/strings.ts", "/tutorial/const.ts", + "/tutorial/structuralComparison.ts", "/tutorial/binaryTree.ts", "/tutorial/specialFunctions.ts", "/tutorial/treeShaking.ts", diff --git a/website/src/playground/files/root/tutorial/structuralComparison.ts b/website/src/playground/files/root/tutorial/structuralComparison.ts new file mode 100644 index 00000000..241d8210 --- /dev/null +++ b/website/src/playground/files/root/tutorial/structuralComparison.ts @@ -0,0 +1,22 @@ +// Also due to value semantics, arrays and objects are compared on their content instead of their +// identity. + +export default function () { + return vec3(-5, 7, 12) === vec3(-5, 7, 12); + // JavaScript: false + // ValueScript: true +} + +function vec3(x: number, y: number, z: number) { + return { x, y, z }; +} + +// Caveat: +// - TypeScript will emit an error for expressions like `[a, b] === [c, d]`. +// +// This is unfortunate. It's to protect you from accidentally expecting structural comparison in JS, +// but in ValueScript, that's exactly how it *does* work. +// +// ValueScript will still happily evaluate these expressions (ValueScript doesn't use the TS +// compiler), but you might want to rewrite these expressions as `eq([a, b], [c, d])` until +// dedicated ValueScript intellisense is available.