diff --git a/convex-core/src/main/cvx/convex/core/metadata.cvx b/convex-core/src/main/cvx/convex/core/metadata.cvx index b9669ad19..ef416f942 100644 --- a/convex-core/src/main/cvx/convex/core/metadata.cvx +++ b/convex-core/src/main/cvx/convex/core/metadata.cvx @@ -352,7 +352,7 @@ :return Any}]}} callable? - {:doc {:description "Returns true if the target is callable. If symbol is provided, also checks that this symbol references a callable function in the specified account. See `call`." + {:doc {:description "Returns true if the target can be called. If symbol is provided, checks that this symbol references a callable function in the specified account. See `call`." :errors {:CAST "If the symbol argument is not a Symbol."} :examples [{:code "(callable? actor-address 'function-name)"}] :signature [{:params [target]} @@ -374,21 +374,21 @@ :return Character}]}} coll? - {:doc {:description "Returns `true` if and only if the argument is a collection: List, Map, Set, or Vector. Returns false otherwise." + {:doc {:description "Tests if the argument is a data struture." :examples [{:code "(coll? [1 2 3])"}] :signature [{:params [x] :return Boolean}]}} cond - {:doc {:description ["Performs conditional tests on successive pairs of `test` -> `result`, returning the `result` for the first `test` that succeeds." - "Performs short-circuit evaluation: result expressions that are not used and any test expressions after the first success will not be executed." - "In the case that no test succeeds, a single aditional argument may be added as a fallback value. If no fallback value is available, nil will be returned."] + {:doc {:description ["Performs conditional tests on successive (test, result) pairs, computing the result for the first test that succeeds." + "Result expressions that are not used and any test expressions after the first success will not be executed." + "If no test succeeds, a final expression may be added as a fallback. If no fallback value is available, nil will be returned."] :examples [{:code "(cond test-1 result-1 else-value)"} {:code "(cond test-1 result-1 test-2 result-2 test-3 result--3)"}] :signature [{:params []} {:params [test]} {:params [test result]} - {:params [test result fallback-value]} + {:params [test result fallback]} {:params [test-1 result-1 test-2 result-2 & more]}]} :special true} diff --git a/convex-core/src/main/java/convex/core/lang/Core.java b/convex-core/src/main/java/convex/core/lang/Core.java index 9202f5be1..2ff6597ff 100644 --- a/convex-core/src/main/java/convex/core/lang/Core.java +++ b/convex-core/src/main/java/convex/core/lang/Core.java @@ -670,9 +670,12 @@ public Context invoke(Context context, ACell[] args) { @Override public Context invoke(Context context, ACell[] args) { + // When used as a predicate if (args.length==1) { Address addr = RT.callableAddress(args[0]); - return context.withResult(Juice.LOOKUP,CVMBool.create(addr!=null)); + if (addr==null) return context.withResult(Juice.LOOKUP,CVMBool.FALSE); + AccountStatus as = context.getState().getAccount(addr); + return context.withResult(Juice.LOOKUP,CVMBool.create(as!=null)); } if (args.length != 2) return context.withArityError(rangeArityMessage(1,2, args.length)); diff --git a/convex-core/src/test/java/convex/core/data/prim/DoubleTest.java b/convex-core/src/test/java/convex/core/data/prim/DoubleTest.java index 2d57e0556..ab173e778 100644 --- a/convex-core/src/test/java/convex/core/data/prim/DoubleTest.java +++ b/convex-core/src/test/java/convex/core/data/prim/DoubleTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,6 +13,7 @@ import convex.core.data.Format; import convex.core.data.ObjectsTest; import convex.core.exceptions.BadFormatException; +import convex.core.exceptions.InvalidDataException; public class DoubleTest { @@ -20,7 +22,7 @@ public class DoubleTest { assertSame(CVMDouble.NaN,CVMDouble.create(Double.NaN)); - // Canonical NaN encoding has just high bit set + // Canonical NaN encoding has just zeros as payload assertEquals(Blob.fromHex("1d7ff8000000000000"),nan.getEncoding()); // create coerces to correct NaN @@ -28,6 +30,11 @@ public class DoubleTest { Blob BAD_NAN=Blob.fromHex("1d7ff8000000ffffff"); assertThrows(BadFormatException.class,()->Format.read(BAD_NAN)); + + // We can artificially create a bad NaN, but it is invalid + CVMDouble badNaN=CVMDouble.unsafeCreate(Double.longBitsToDouble(0x7ff8000000ffffffL)); + assertNotEquals(nan,badNaN); + assertThrows(InvalidDataException.class,()->badNaN.validate()); } @Test public void testCompares() { @@ -44,6 +51,7 @@ public class DoubleTest { } @Test public void testEquality() { + // Regular object equality ObjectsTest.doEqualityTests(CVMDouble.ONE, CVMDouble.create(1.0)); ObjectsTest.doEqualityTests(CVMDouble.create(12345.0),CVMDouble.create(12345.0)); diff --git a/convex-core/src/test/java/convex/core/lang/CoreTest.java b/convex-core/src/test/java/convex/core/lang/CoreTest.java index f1dc1a5f3..dd67bd3dd 100644 --- a/convex-core/src/test/java/convex/core/lang/CoreTest.java +++ b/convex-core/src/test/java/convex/core/lang/CoreTest.java @@ -101,7 +101,6 @@ */ public class CoreTest extends ACVMTest { - protected CoreTest() throws IOException { super(BaseTest.STATE); } @@ -654,6 +653,7 @@ public void testEquals() { assertFalse(evalB("(= 1 2)")); assertFalse(evalB("(= 1 nil)")); assertFalse(evalB("(= 1 1.0)")); + assertFalse(evalB("(= false nil)")); assertFalse(evalB("(= \\a \\b)")); assertFalse(evalB("(= :foo :baz)")); assertFalse(evalB("(= :foo 'foo)")); @@ -3904,13 +3904,23 @@ public void testMapPred() { assertFalse(evalB("(map? '(3 4 5))")); assertTrue(evalB("(map? {1 2 3 4})")); assertFalse(evalB("(map? #{1 2})")); + + // This is technically a map since it is a record + assertTrue(evalB("(map? (account #0))")); // this is a record + + // This is technically a map since it is Index + assertTrue(evalB("(map? (index))")); // this is a record + } @Test public void testCollPred() { + // Like Clojure coll?, returns true for any data structure assertFalse(evalB("(coll? nil)")); assertFalse(evalB("(coll? 1)")); assertFalse(evalB("(coll? :foo)")); + + assertTrue(evalB("(coll? {})")); assertTrue(evalB("(coll? [])")); assertTrue(evalB("(coll? ())")); @@ -3921,6 +3931,7 @@ public void testCollPred() { assertTrue(evalB("(coll? {1 2 3 4})")); assertTrue(evalB("(coll? #{1 2})")); assertTrue(evalB("(coll? (index))")); + assertTrue(evalB("(coll? (account #0))")); // this is a record assertTrue(evalB("(coll? (index 0x 0x))")); } @@ -3942,6 +3953,7 @@ public void testEmptyPred() { assertFalse(evalB("(empty? #{[]})")); assertFalse(evalB("(empty? 0)")); + assertFalse(evalB("(empty? false)")); assertFalse(evalB("(empty? :foo)")); assertFalse(evalB("(empty? 'bar)")); } @@ -3949,6 +3961,7 @@ public void testEmptyPred() { @Test public void testSymbolPred() { assertTrue(evalB("(symbol? 'foo)")); + assertTrue(evalB("(symbol? (quote .))")); assertTrue(evalB("(symbol? (symbol :bar))")); assertFalse(evalB("(symbol? (str 1))")); @@ -3963,6 +3976,7 @@ public void testKeywordPred() { assertTrue(evalB("(keyword? (keyword 'bar))")); assertFalse(evalB("(keyword? nil)")); + assertFalse(evalB("(keyword? 'zzz)")); assertFalse(evalB("(keyword? 1)")); assertFalse(evalB("(keyword? [:foo])")); } @@ -3975,6 +3989,7 @@ public void testAddressPred() { assertFalse(evalB("(address? nil)")); assertFalse(evalB("(address? 1)")); + assertFalse(evalB("(address? [#1 #2])")); assertFalse(evalB("(address? \"0a1b2c3d\")")); assertFalse(evalB("(address? (blob *origin*))")); } @@ -4002,7 +4017,7 @@ public void testLongPred() { assertFalse(evalB("(long? nil)")); assertFalse(evalB("(long? 0xFF)")); assertFalse(evalB("(long? [1 2])")); - assertFalse(evalB("(long? 7.0)")); + assertFalse(evalB("(long? 7.0)")); // not a long, even though numerically equivalent to one // big integer boundaries assertTrue(evalB("(long? 9223372036854775807)")); @@ -4018,8 +4033,10 @@ public void testStrPred() { assertTrue(evalB("(str? (str :foo))")); assertTrue(evalB("(str? (str nil))")); assertTrue(evalB("(str? \"\")")); + assertTrue(evalB("(str? \"Hello World\")")); // These are not strings + assertFalse(evalB("(str? \\Q)")); // character is not itself a string assertFalse(evalB("(str? 1)")); assertFalse(evalB("(str? :foo)")); assertFalse(evalB("(str? nil)")); @@ -4724,7 +4741,7 @@ public void testExpand() { assertEquals(Strings.create("foo"), eval("(expand (name :foo) (fn [x e] x))")); assertEquals(CVMLong.create(3), eval("(expand '[1 2 3] (fn [x e] (nth x 2)))")); - assertNull(Syntax.unwrap(eval("(expand nil)"))); + assertNull(eval("(expand nil)")); assertCastError(step("(expand 1 :foo)")); assertCastError(step("(expand { 888 227 723 560} [75 561 258 833])")); @@ -4747,7 +4764,7 @@ public void testExpand_1() { @Test public void testExpandEdgeCases() { - // BAd functions + // Bad functions assertCastError(step("(expand 123 #0 :foo)")); assertCastError(step("(expand 123 #0)")); @@ -4778,6 +4795,7 @@ public void testExpandOnce() { public void testMacro() { Context c=step("(defmacro foo [] :foo)"); assertEquals(Keywords.FOO,eval(c,"(foo)")); + assertEquals(Keywords.FOO,eval(c,"(expand '(foo))")); } @Test @@ -4790,6 +4808,9 @@ public void testQuote() { // interior macros shouldn't get expanded assertEquals(Vectors.of(1,Lists.of(Symbols.IF,4,7),3),eval("(quote [1 (if 4 7) 3])")); + + assertArityError(step ("(quote foo bar)")); + assertArityError(step ("(quote)")); } @Test @@ -4878,7 +4899,10 @@ public void testCallableQ() { assertFalse(evalB(ctx, "(callable? nil)")); assertFalse(evalB(ctx, "(callable? :foo)")); assertFalse(evalB(ctx, "(callable? [])")); - + + // Missing accounts definitely not callable + assertFalse(evalB(ctx, "(callable? #6666666)")); + assertFalse(evalB(ctx, "(callable? #6666666 'something)")); assertCastError(step(ctx, "(callable? caddr :public)")); // not a Symbol assertCastError(step(ctx, "(callable? caddr :random-name)"));