From 3d1d0ff6912e8c12725cec1c178f908c53f12d44 Mon Sep 17 00:00:00 2001 From: Rob Pike Date: Wed, 28 Jun 2023 17:57:35 +1000 Subject: [PATCH] ivy: add set operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Well, what "set operations" mean in APL, which is not quite what math wants because duplicate entries are allowed. See https://aplwiki.com/wiki/Intersection for some background. The operations are union, intersect, and unique. Implementing these required an interesting internal change, because to check set membership, one must be able to do 'a' == 1 but prior to this CL, this expression gave an error because binary operations promote the types before the operation, and chars and ints cannot be compared. To get around this, and honestly to fix what was arguably a bug anyway, I changed exec/context.go to handle == and != specially, before promotion happens. That required a new function, EvalCharEqual, in the value package. (While working this out, I noticed a similar hack for "text", which could be handled more cleanly as part of the operator's definition.) So that's solved, but it gets harder. In ivy, unlike in APL, there are many ways to say "one": an integer, a float, a rational, a big int, even a complex. But when doing those set operations, you need to be sure you don't make 1.0 and 1 be different values. Also, you need to be able ask if any element of a vector is equal to any other element, which means asking ridiculous things like 'a' == 1j3 so that must work too. It does now, it didn't before. But the hardest part is that to make any of this efficient, you need a fast way to see whether a value is contained in a vector; otherwise you end up with n² time. @rsc put in a 'membership' function that serves this purpose well, sorting the vector and doing binary search, but that requires the existence of an ordering, and not only can you not reasonably ask if 'a' < 1 you certainly can't ask if 1j2 < 1j3 because < doesn't work at all on complex numbers. I therefore added OrderedCompare, which is a compare (signum) operator that introduces a total ordering over all scalar values, while honoring the desire that 1 == 1j0 == 1.0 == 1/1. To do this, we say that all chars sort under all other scalars, and that all non-real complex numbers sort above all other scalars, while complex numbers themselves are ordered first by real part, and if that is equal, by imaginary part. I then modified membership to use OrderedCompare instead of == and >=, and there you have it. That also fixes a latent issue that could have arisen with crazy cases involving membership even before sets arose. For the record, the naive n² algorithm handles this calculation: x=iota 100*1000; y = x intersect x in about 2 minutes on my Mac Studio; the current code takes about 30ms, which is not fast but is plenty fast enough. I believe the structure is mostly in place now to provide these operations for major blocks of matrices, but that's for another day. Tip of the hat to @Blackmane for suggestion the addition of unique and prodding me into a few fun days that also fixed some internal problems. --- config/config.go | 2 +- doc.go | 142 +++++++++--------- exec/context.go | 12 +- mobile/help.go | 125 ++++++++-------- order_test.go | 236 ++++++++++++++++++++++++++++++ parse/function.go | 2 +- parse/help.go | 292 +++++++++++++++++++------------------ parse/parse.go | 10 ++ testdata/binary_vector.ivy | 93 ++++++++++++ testdata/help.ivy | 2 +- testdata/unary_vector.ivy | 19 +++ value/asin.go | 14 +- value/asinh.go | 8 +- value/bigfloat.go | 2 +- value/bigint.go | 2 +- value/bigrat.go | 2 +- value/binary.go | 52 +++++-- value/char.go | 2 +- value/complex.go | 16 +- value/const.go | 6 +- value/eval.go | 27 +++- value/int.go | 2 +- value/log.go | 4 +- value/matrix.go | 1 - value/power.go | 6 +- value/sets.go | 252 ++++++++++++++++++++++++++++++++ value/sin.go | 6 +- value/sinh.go | 6 +- value/sqrt.go | 4 +- value/unary.go | 26 +++- value/value.go | 2 +- value/vector.go | 4 +- 32 files changed, 1035 insertions(+), 344 deletions(-) create mode 100644 order_test.go create mode 100644 value/sets.go diff --git a/config/config.go b/config/config.go index fc8b583..8c55f34 100644 --- a/config/config.go +++ b/config/config.go @@ -314,7 +314,7 @@ func formatDuration(d float64, units string) string { s = s[:len(s)-4] } return s + units - + } // Base returns the input and output bases. diff --git a/doc.go b/doc.go index 7f9ef7a..d660e64 100644 --- a/doc.go +++ b/doc.go @@ -7,7 +7,6 @@ // )help still works properly. /* - Ivy is an interpreter for an APL-like language. It is a plaything and a work in progress. @@ -76,10 +75,11 @@ Unary operators Not ∼B not Logical: not 1 is 0, not 0 is 1 Absolute value ∣B abs Magnitude of B Index generator ⍳B iota Vector of the first B integers + Unique ∩B unique Remove all duplicate elements from B Exponential ⋆B ** e to the B power Negation −B - Changes sign of B Identity +B + No change to B - Signum ×B sgn ¯1 if B<0; 0 if B=0; 1 if B>0 + Signum ×B sgn -1 if B<0; 0 if B=0; 1 if B>0 Reciprocal ÷B / 1 divided by B Ravel ,B , Reshapes B into a vector Matrix inverse ⌹B Inverse of matrix B @@ -114,66 +114,68 @@ Unary operators Binary operators - Name APL Ivy Meaning - Add A+B + Sum of A and B - Subtract A−B - A minus B - Multiply A×B * A multiplied by B - Divide A÷B / A divided by B (exact rational division) - div A divided by B (Euclidean) - idiv A divided by B (Go) - Exponentiation A⋆B ** A raised to the B power - Circle A○B Trigonometric functions of B selected by A - A=1: sin(B) A=2: cos(B) A=3: tan(B); ¯A for inverse - sin sin(B); ivy uses traditional name. - cos cos(B); ivy uses traditional name. - tan tan(B); ivy uses traditional name. - Deal A?B ? A distinct integers selected randomly from the first B integers - Membership A∈B in 1 for elements of A present in B; 0 where not. - Maximum A⌈B max The greater value of A or B - Minimum A⌊B min The smaller value of A or B - Reshape A⍴B rho Array of shape A with data B - Take A↑B take Select the first (or last) A elements of B according to ×A - Drop A↓B drop Remove the first (or last) A elements of B according to ×A - Decode A⊥B decode Value of a polynomial whose coefficients are B at A - Encode A⊤B encode Base-A representation of the value of B - Residue A∣B B modulo A - mod A modulo B (Euclidean) - imod A modulo B (Go) - Catenation A,B , Elements of B appended to the elements of A - Expansion A\B fill Insert zeros (or blanks) in B corresponding to zeros in A - In ivy: abs(A) gives count, A <= 0 inserts zero (or blank) - Compression A/B sel Select elements in B corresponding to ones in A - In ivy: abs(A) gives count, A <= 0 inserts zero - Index of A⍳B iota The location (index) of B in A; 1+⌈/⍳⍴A if not found - In ivy: origin-1 if not found (i.e. 0 if one-indexed) - Matrix divide A⌹B Solution to system of linear equations Ax = B - Rotation A⌽B rot The elements of B are rotated A positions left - Rotation A⊖B flip The elements of B are rotated A positions along the first axis - Logarithm A⍟B log Logarithm of B to base A - Dyadic format A⍕B text Format B into a character matrix according to A - A is the textual format (see format special command); - otherwise result depends on length of A: - 1 gives decimal count, 2 gives width and decimal count, - 3 gives width, decimal count, and style ('d', 'e', 'f', etc.). - General transpose A⍉B transp The axes of B are ordered by A - Combinations A!B ! Number of combinations of B taken A at a time - Less than A= Comparison: 1 if true, 0 if false - Greater than A>B > Comparison: 1 if true, 0 if false - Not equal A≠B != Comparison: 1 if true, 0 if false - Or A∨B or Logic: 0 if A and B are 0; 1 otherwise - And A∧B and Logic: 1 if A and B are 1; 0 otherwise - Nor A⍱B nor Logic: 1 if both A and B are 0; otherwise 0 - Nand A⍲B nand Logic: 0 if both A and B are 1; otherwise 1 - Xor xor Logic: 1 if A != B; otherwise 0 - Bitwise and & Bitwise A and B (integer only) - Bitwise or | Bitwise A or B (integer only) - Bitwise xor ^ Bitwise A exclusive or B (integer only) - Left shift << A shifted left B bits (integer only) - Right Shift >> A shifted right B bits (integer only) - Complex construction j The complex number A+Bi + Name APL Ivy Meaning + Add A+B + Sum of A and B + Subtract A−B - A minus B + Multiply A×B * A multiplied by B + Divide A÷B / A divided by B (exact rational division) + div A divided by B (Euclidean) + idiv A divided by B (Go) + Exponentiation A⋆B ** A raised to the B power + Circle A○B Trigonometric functions of B selected by A + A=1: sin(B) A=2: cos(B) A=3: tan(B); ¯A for inverse + sin sin(B); ivy uses traditional name. + cos cos(B); ivy uses traditional name. + tan tan(B); ivy uses traditional name. + Deal A?B ? A distinct integers selected randomly from the first B integers + Membership A∈B in 1 for elements of A present in B; 0 where not. + Intersection A∩B intersect A with all elements that are also in B removed + Union A∪B union A followed by all members of B not already in A + Maximum A⌈B max The greater value of A or B + Minimum A⌊B min The smaller value of A or B + Reshape A⍴B rho Array of shape A with data B + Take A↑B take Select the first (or last) A elements of B according to sgn A + Drop A↓B drop Remove the first (or last) A elements of B according to sgn A + Decode A⊥B decode Value of a polynomial whose coefficients are B at A + Encode A⊤B encode Base-A representation of the value of B + Residue A∣B B modulo A + mod A modulo B (Euclidean) + imod A modulo B (Go) + Catenation A,B , Elements of B appended to the elements of A + Expansion A\B fill Insert zeros (or blanks) in B corresponding to zeros in A + In ivy: abs(A) gives count, A <= 0 inserts zero (or blank) + Compression A/B sel Select elements in B corresponding to ones in A + In ivy: abs(A) gives count, A <= 0 inserts zero + Index of A⍳B iota The location (index) of B in A; 1+⌈/⍳⍴A if not found + In ivy: origin-1 if not found (i.e. 0 if one-indexed) + Matrix divide A⌹B Solution to system of linear equations Ax = B + Rotation A⌽B rot The elements of B are rotated A positions left + Rotation A⊖B flip The elements of B are rotated A positions along the first axis + Logarithm A⍟B log Logarithm of B to base A + Dyadic format A⍕B text Format B into a character matrix according to A + A is the textual format (see format special command); + otherwise result depends on length of A: + 1 gives decimal count, 2 gives width and decimal count, + 3 gives width, decimal count, and style ('d', 'e', 'f', etc.). + General transpose A⍉B transp The axes of B are ordered by A + Combinations A!B ! Number of combinations of B taken A at a time + Less than A= Comparison: 1 if true, 0 if false + Greater than A>B > Comparison: 1 if true, 0 if false + Not equal A≠B != Comparison: 1 if true, 0 if false + Or A∨B or Logic: 0 if A and B are 0; 1 otherwise + And A∧B and Logic: 1 if A and B are 1; 0 otherwise + Nor A⍱B nor Logic: 1 if both A and B are 0; otherwise 0 + Nand A⍲B nand Logic: 0 if both A and B are 1; otherwise 1 + Xor xor Logic: 1 if A != B; otherwise 0 + Bitwise and & Bitwise A and B (integer only) + Bitwise or | Bitwise A or B (integer only) + Bitwise xor ^ Bitwise A exclusive or B (integer only) + Left shift << A shifted left B bits (integer only) + Right Shift >> A shifted right B bits (integer only) + Complex construction j The complex number A+Bi Operators and axis indicator @@ -195,19 +197,19 @@ Type-converting operations for complex numbers, the result is (float A)j(float B) -Pre-defined constants +# Pre-defined constants The constants e (base of natural logarithms) and pi (π) are pre-defined to high precision, about 3000 decimal digits truncated according to the floating point precision setting. -Character data +# Character data Strings are vectors of "chars", which are Unicode code points (not bytes). Syntactically, string literals are very similar to those in Go, with back-quoted raw strings and double-quoted interpreted strings. Unlike Go, single-quoted strings are equivalent to double-quoted, a nod to APL syntax. A string with a single char -is just a singleton char value; all others are vectors. Thus ``, "", and '' are +is just a singleton char value; all others are vectors. Thus “, "", and ” are empty vectors, `a`, "a", and 'a' are equivalent representations of a single char, and `ab`, `a` `b`, "ab", "a" "b", 'ab', and 'a' 'b' are equivalent representations of a two-char vector. @@ -224,7 +226,7 @@ legal but arithmetic is not, and chars cannot be converted automatically into ot singleton values (ints, floats, and so on). The unary operators char and code enable transcoding between integer and char values. -User-defined operators +# User-defined operators Users can define unary and binary operators, which then behave just like built-in operators. Both a unary and a binary operator may be defined for the @@ -250,16 +252,19 @@ operator has a lower precedence than any other operator; in effect it breaks the line into two separate expressions. Example: average of a vector (unary): + op avg x = (+/x)/rho x avg iota 11 result: 6 Example: n largest entries in a vector (binary): + op n largest x = n take x[down x] 3 largest 7 1 3 24 1 5 12 5 51 result: 51 24 12 Example: multiline operator definition (binary): + op a sum b = a = a+b a @@ -268,11 +273,13 @@ Example: multiline operator definition (binary): result: 1 2 3 4 5 6 7 Example: primes less than N (unary): + op primes N = (not T in T o.* T) sel T = 1 drop iota N primes 50 result: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 Example: greatest common divisor (binary): + op a gcd b = a == b: a a > b: b gcd a-b @@ -310,7 +317,7 @@ To write to a global without reading it first, insert an unused read. total last result: 12 3 -Special commands +# Special commands Ivy accepts a number of special commands, introduced by a right paren at the beginning of the line. Most report the current value if a new value @@ -374,6 +381,5 @@ base 10 and must be non-negative on input. (Unimplemented on mobile.) ) seed 0 Set the seed for the ? operator. - */ package main diff --git a/exec/context.go b/exec/context.go index 39cba45..fd6ece9 100644 --- a/exec/context.go +++ b/exec/context.go @@ -27,9 +27,9 @@ type Context struct { Globals Symtab - // UnaryFn maps the names of unary functions (ops) to their implemenations. + // UnaryFn maps the names of unary functions (ops) to their implementations. UnaryFn map[string]*Function - // BinaryFn maps the names of binary functions (ops) to their implemenations. + // BinaryFn maps the names of binary functions (ops) to their implementations. BinaryFn map[string]*Function // Defs is a list of defined ops, in time order. It is used when saving the // Context to a file. @@ -152,6 +152,14 @@ func (c *Context) UserDefined(op string, isBinary bool) bool { // EvalBinary evaluates a binary operator, including products. func (c *Context) EvalBinary(left value.Value, op string, right value.Value) value.Value { + // Special handling for the equal and non-equal operators, which must avoid + // type conversions involving Char. + if op == "==" || op == "!=" { + v, ok := value.EvalCharEqual(left, op == "==", right) + if ok { + return v + } + } if strings.Contains(op, ".") { return value.Product(c, left, op, right) } diff --git a/mobile/help.go b/mobile/help.go index e0fa532..9cebf1c 100644 --- a/mobile/help.go +++ b/mobile/help.go @@ -82,10 +82,11 @@ Shape ⍴B rho Number of components in each dimension of B Not ∼B not Logical: not 1 is 0, not 0 is 1 Absolute value ∣B abs Magnitude of B Index generator ⍳B iota Vector of the first B integers +Unique ∩B unique Remove all duplicate elements from B Exponential ⋆B ** e to the B power Negation −B - Changes sign of B Identity +B + No change to B -Signum ×B sgn ¯1 if B<0; 0 if B=0; 1 if B>0 +Signum ×B sgn -1 if B<0; 0 if B=0; 1 if B>0 Reciprocal ÷B / 1 divided by B Ravel ,B , Reshapes B into a vector Matrix inverse ⌹B Inverse of matrix B @@ -119,66 +120,68 @@ Imaginary part imag Imaginary component of the value Phase phase Phase of the value in the complex plane (-π to π)

Binary operators -

Name                  APL   Ivy     Meaning
-Add                   A+B   +       Sum of A and B
-Subtract              A−B   -       A minus B
-Multiply              A×B   *       A multiplied by B
-Divide                A÷B   /       A divided by B (exact rational division)
-                            div     A divided by B (Euclidean)
-                            idiv    A divided by B (Go)
-Exponentiation        A⋆B   **      A raised to the B power
-Circle                A○B           Trigonometric functions of B selected by A
-                                    A=1: sin(B) A=2: cos(B) A=3: tan(B); ¯A for inverse
-                            sin     sin(B); ivy uses traditional name.
-                            cos     cos(B); ivy uses traditional name.
-                            tan     tan(B); ivy uses traditional name.
-Deal                  A?B   ?       A distinct integers selected randomly from the first B integers
-Membership            A∈B   in      1 for elements of A present in B; 0 where not.
-Maximum               A⌈B   max     The greater value of A or B
-Minimum               A⌊B   min     The smaller value of A or B
-Reshape               A⍴B   rho     Array of shape A with data B
-Take                  A↑B   take    Select the first (or last) A elements of B according to ×A
-Drop                  A↓B   drop    Remove the first (or last) A elements of B according to ×A
-Decode                A⊥B   decode  Value of a polynomial whose coefficients are B at A
-Encode                A⊤B   encode  Base-A representation of the value of B
-Residue               A∣B           B modulo A
-                            mod     A modulo B (Euclidean)
-                            imod    A modulo B (Go)
-Catenation            A,B   ,       Elements of B appended to the elements of A
-Expansion             A\B   fill    Insert zeros (or blanks) in B corresponding to zeros in A
-                                    In ivy: abs(A) gives count, A <= 0 inserts zero (or blank)
-Compression           A/B   sel     Select elements in B corresponding to ones in A
-                                    In ivy: abs(A) gives count, A <= 0 inserts zero
-Index of              A⍳B   iota    The location (index) of B in A; 1+⌈/⍳⍴A if not found
-                                    In ivy: origin-1 if not found (i.e. 0 if one-indexed)
-Matrix divide         A⌹B           Solution to system of linear equations Ax = B
-Rotation              A⌽B   rot     The elements of B are rotated A positions left
-Rotation              A⊖B   flip    The elements of B are rotated A positions along the first axis
-Logarithm             A⍟B   log     Logarithm of B to base A
-Dyadic format         A⍕B   text    Format B into a character matrix according to A
-                                    A is the textual format (see format special command);
-                                    otherwise result depends on length of A:
-                                    1 gives decimal count, 2 gives width and decimal count,
-                                    3 gives width, decimal count, and style ('d', 'e', 'f', etc.).
-General transpose     A⍉B   transp  The axes of B are ordered by A
-Combinations          A!B   !       Number of combinations of B taken A at a time
-Less than             A<B   <       Comparison: 1 if true, 0 if false
-Less than or equal    A≤B   <=      Comparison: 1 if true, 0 if false
-Equal                 A=B   ==      Comparison: 1 if true, 0 if false
-Greater than or equal A≥B   >=      Comparison: 1 if true, 0 if false
-Greater than          A>B   >       Comparison: 1 if true, 0 if false
-Not equal             A≠B   !=      Comparison: 1 if true, 0 if false
-Or                    A∨B   or      Logic: 0 if A and B are 0; 1 otherwise
-And                   A∧B   and     Logic: 1 if A and B are 1; 0 otherwise
-Nor                   A⍱B   nor     Logic: 1 if both A and B are 0; otherwise 0
-Nand                  A⍲B   nand    Logic: 0 if both A and B are 1; otherwise 1
-Xor                         xor     Logic: 1 if A != B; otherwise 0
-Bitwise and                 &       Bitwise A and B (integer only)
-Bitwise or                  |       Bitwise A or B (integer only)
-Bitwise xor                 ^       Bitwise A exclusive or B (integer only)
-Left shift                  <<      A shifted left B bits (integer only)
-Right Shift                 >>      A shifted right B bits (integer only)
-Complex construction        j       The complex number A+Bi
+
Name                  APL   Ivy       Meaning
+Add                   A+B   +         Sum of A and B
+Subtract              A−B   -         A minus B
+Multiply              A×B   *         A multiplied by B
+Divide                A÷B   /         A divided by B (exact rational division)
+                            div       A divided by B (Euclidean)
+                            idiv      A divided by B (Go)
+Exponentiation        A⋆B   **        A raised to the B power
+Circle                A○B             Trigonometric functions of B selected by A
+                                      A=1: sin(B) A=2: cos(B) A=3: tan(B); ¯A for inverse
+                            sin       sin(B); ivy uses traditional name.
+                            cos       cos(B); ivy uses traditional name.
+                            tan       tan(B); ivy uses traditional name.
+Deal                  A?B   ?         A distinct integers selected randomly from the first B integers
+Membership            A∈B   in        1 for elements of A present in B; 0 where not.
+Intersection          A∩B   intersect A with all elements that are also in B removed
+Union                 A∪B   union     A followed by all members of B not already in A
+Maximum               A⌈B   max       The greater value of A or B
+Minimum               A⌊B   min       The smaller value of A or B
+Reshape               A⍴B   rho       Array of shape A with data B
+Take                  A↑B   take      Select the first (or last) A elements of B according to sgn A
+Drop                  A↓B   drop      Remove the first (or last) A elements of B according to sgn A
+Decode                A⊥B   decode    Value of a polynomial whose coefficients are B at A
+Encode                A⊤B   encode    Base-A representation of the value of B
+Residue               A∣B              B modulo A
+                            mod       A modulo B (Euclidean)
+                            imod      A modulo B (Go)
+Catenation            A,B   ,         Elements of B appended to the elements of A
+Expansion             A\B   fill      Insert zeros (or blanks) in B corresponding to zeros in A
+                                      In ivy: abs(A) gives count, A <= 0 inserts zero (or blank)
+Compression           A/B   sel       Select elements in B corresponding to ones in A
+                                      In ivy: abs(A) gives count, A <= 0 inserts zero
+Index of              A⍳B   iota      The location (index) of B in A; 1+⌈/⍳⍴A if not found
+                                      In ivy: origin-1 if not found (i.e. 0 if one-indexed)
+Matrix divide         A⌹B             Solution to system of linear equations Ax = B
+Rotation              A⌽B   rot       The elements of B are rotated A positions left
+Rotation              A⊖B   flip      The elements of B are rotated A positions along the first axis
+Logarithm             A⍟B   log       Logarithm of B to base A
+Dyadic format         A⍕B   text      Format B into a character matrix according to A
+                                      A is the textual format (see format special command);
+                                      otherwise result depends on length of A:
+                                      1 gives decimal count, 2 gives width and decimal count,
+                                      3 gives width, decimal count, and style ('d', 'e', 'f', etc.).
+General transpose     A⍉B   transp    The axes of B are ordered by A
+Combinations          A!B   !         Number of combinations of B taken A at a time
+Less than             A<B   <         Comparison: 1 if true, 0 if false
+Less than or equal    A≤B   <=        Comparison: 1 if true, 0 if false
+Equal                 A=B   ==        Comparison: 1 if true, 0 if false
+Greater than or equal A≥B   >=        Comparison: 1 if true, 0 if false
+Greater than          A>B   >         Comparison: 1 if true, 0 if false
+Not equal             A≠B   !=        Comparison: 1 if true, 0 if false
+Or                    A∨B   or        Logic: 0 if A and B are 0; 1 otherwise
+And                   A∧B   and       Logic: 1 if A and B are 1; 0 otherwise
+Nor                   A⍱B   nor       Logic: 1 if both A and B are 0; otherwise 0
+Nand                  A⍲B   nand      Logic: 0 if both A and B are 1; otherwise 1
+Xor                         xor       Logic: 1 if A != B; otherwise 0
+Bitwise and                 &         Bitwise A and B (integer only)
+Bitwise or                  |         Bitwise A or B (integer only)
+Bitwise xor                 ^         Bitwise A exclusive or B (integer only)
+Left shift                  <<        A shifted left B bits (integer only)
+Right Shift                 >>        A shifted right B bits (integer only)
+Complex construction        j         The complex number A+Bi
 

Operators and axis indicator

Name                APL  Ivy  APL Example  Ivy Example  Meaning (of example)
diff --git a/order_test.go b/order_test.go
new file mode 100644
index 0000000..c265f54
--- /dev/null
+++ b/order_test.go
@@ -0,0 +1,236 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"math/big"
+	"testing"
+
+	"robpike.io/ivy/config"
+	"robpike.io/ivy/exec"
+	"robpike.io/ivy/value"
+)
+
+type orderTest struct {
+	u, v value.Value
+	sgn  int
+}
+
+var (
+	int0 = value.Int(0)
+	int1 = value.Int(1)
+	int2 = value.Int(2)
+	int3 = value.Int(3)
+
+	char1 = value.Char(1)
+	char2 = value.Char(2)
+	char3 = value.Char(3)
+
+	bigInt0 = value.BigInt{big.NewInt(0)}
+	bigInt1 = value.BigInt{big.NewInt(1)}
+	bigInt2 = value.BigInt{big.NewInt(2)}
+	bigInt3 = value.BigInt{big.NewInt(3)}
+
+	bigRat0o1 = value.BigRat{big.NewRat(0, 1)}
+	bigRat1o1 = value.BigRat{big.NewRat(1, 1)}
+	bigRat2o1 = value.BigRat{big.NewRat(2, 1)}
+	bigRat1o7 = value.BigRat{big.NewRat(1, 7)}
+	bigRat2o7 = value.BigRat{big.NewRat(2, 7)}
+	bigRat3o7 = value.BigRat{big.NewRat(3, 7)}
+
+	bigFloat0p0 = value.BigFloat{big.NewFloat(0.0)}
+	bigFloat1p0 = value.BigFloat{big.NewFloat(1.0)}
+	bigFloat2p0 = value.BigFloat{big.NewFloat(2.0)}
+	bigFloat1p5 = value.BigFloat{big.NewFloat(1.5)}
+	bigFloat2p5 = value.BigFloat{big.NewFloat(2.5)}
+	bigFloat3p5 = value.BigFloat{big.NewFloat(3.5)}
+
+	complex1j0 = value.NewComplex(int1, int0)
+	complex1j1 = value.NewComplex(int1, int1)
+	complex1j2 = value.NewComplex(int1, int2) // Same real, bigger imaginary.
+	complex2j1 = value.NewComplex(int2, int1) // Bigger real, lesser imaginary
+	complex2j2 = value.NewComplex(int2, int2) // Same real, bigger imaginary
+)
+
+func TestOrderedCompare(t *testing.T) {
+	var tests = []orderTest{
+		// Same types.
+		// Int
+		{int1, int1, 0},
+		{int1, int2, -1},
+		{int1, int3, -1},
+		{int2, int1, 1},
+		{int2, int2, 0},
+		{int2, int3, -1},
+		{int3, int1, 1},
+		{int3, int2, 1},
+		{int3, int3, 0},
+
+		// Char
+		{char1, char1, 0},
+		{char1, char2, -1},
+		{char1, char3, -1},
+		{char2, char1, 1},
+		{char2, char2, 0},
+		{char2, char3, -1},
+		{char3, char1, 1},
+		{char3, char2, 1},
+		{char3, char3, 0},
+
+		// BigInt
+		{bigInt1, bigInt1, 0},
+		{bigInt1, bigInt2, -1},
+		{bigInt1, bigInt3, -1},
+		{bigInt2, bigInt1, 1},
+		{bigInt2, bigInt2, 0},
+		{bigInt2, bigInt3, -1},
+		{bigInt3, bigInt1, 1},
+		{bigInt3, bigInt2, 1},
+		{bigInt3, bigInt3, 0},
+
+		// BigRat
+		{bigRat1o7, bigRat1o7, 0},
+		{bigRat1o7, bigRat2o7, -1},
+		{bigRat1o7, bigRat3o7, -1},
+		{bigRat2o7, bigRat1o7, 1},
+		{bigRat2o7, bigRat2o7, 0},
+		{bigRat2o7, bigRat3o7, -1},
+		{bigRat3o7, bigRat1o7, 1},
+		{bigRat3o7, bigRat2o7, 1},
+		{bigRat3o7, bigRat3o7, 0},
+
+		// BigFloat
+		{bigFloat1p5, bigFloat1p5, 0},
+		{bigFloat1p5, bigFloat2p5, -1},
+		{bigFloat1p5, bigFloat3p5, -1},
+		{bigFloat2p5, bigFloat1p5, 1},
+		{bigFloat2p5, bigFloat2p5, 0},
+		{bigFloat2p5, bigFloat3p5, -1},
+		{bigFloat3p5, bigFloat1p5, 1},
+		{bigFloat3p5, bigFloat2p5, 1},
+		{bigFloat3p5, bigFloat3p5, 0},
+
+		// Complex
+		{complex1j1, complex1j1, 0},
+		{complex1j1, complex1j2, -1},
+		{complex1j1, complex2j1, -1},
+		{complex1j1, complex2j2, -1},
+		{complex1j2, complex1j1, 1},
+		{complex1j2, complex1j2, 0},
+		{complex1j2, complex2j1, -1},
+		{complex1j2, complex2j2, -1},
+		{complex2j1, complex1j1, 1},
+		{complex2j1, complex1j2, 1},
+		{complex2j1, complex2j1, 0},
+		{complex2j1, complex2j2, -1},
+		{complex2j2, complex1j1, 1},
+		{complex2j2, complex1j2, 1},
+		{complex2j2, complex2j1, 1},
+		{complex2j2, complex2j2, 0},
+
+		// Int less than every possible type.
+		{int0, bigInt1, -1},
+		{int0, bigRat1o1, -1},
+		{int0, bigFloat1p0, -1},
+		{int0, complex1j0, -1},
+
+		// Int equal to every possible type.
+		{int1, bigInt1, 0},
+		{int1, bigRat1o1, 0},
+		{int1, bigFloat1p0, 0},
+		{int1, complex1j0, 0},
+
+		// Int greater than every possible type.
+		{int2, bigInt1, 1},
+		{int2, bigRat1o1, 1},
+		{int2, bigFloat1p0, 1},
+		{int2, complex1j0, 1},
+
+		// BigInt less than every possible type.
+		{bigInt0, int1, -1},
+		{bigInt0, bigRat1o1, -1},
+		{bigInt0, bigFloat1p0, -1},
+		{bigInt0, complex1j0, -1},
+
+		// BigInt equal to every possible type.
+		{bigInt1, int1, 0},
+		{bigInt1, bigRat1o1, 0},
+		{bigInt1, bigFloat1p0, 0},
+		{bigInt1, complex1j0, 0},
+
+		// BigInt greater than every possible type.
+		{bigInt2, int1, 1},
+		{bigInt2, bigRat1o1, 1},
+		{bigInt2, bigFloat1p0, 1},
+		{bigInt2, complex1j0, 1},
+
+		// BigRat less than every possible type.
+		{bigRat0o1, int1, -1},
+		{bigRat0o1, bigInt1, -1},
+		{bigRat0o1, bigFloat1p0, -1},
+		{bigRat0o1, complex1j0, -1},
+
+		// BigRat equal to every possible type.
+		{bigRat1o1, int1, 0},
+		{bigRat1o1, bigInt1, 0},
+		{bigRat1o1, bigFloat1p0, 0},
+		{bigRat1o1, complex1j0, 0},
+
+		// BigRat greater than every possible type.
+		{bigRat2o1, int1, 1},
+		{bigRat2o1, bigInt1, 1},
+		{bigRat2o1, bigFloat1p0, 1},
+		{bigRat2o1, complex1j0, 1},
+
+		// BigFloat less than every possible type.
+		{bigFloat0p0, int1, -1},
+		{bigFloat0p0, bigInt1, -1},
+		{bigFloat0p0, bigFloat1p0, -1},
+		{bigFloat0p0, complex1j0, -1},
+
+		// BigFloat equal to every possible type.
+		{bigFloat1p0, int1, 0},
+		{bigFloat1p0, bigInt1, 0},
+		{bigFloat1p0, bigFloat1p0, 0},
+		{bigFloat1p0, complex1j0, 0},
+
+		// BigFloat greater than every possible type.
+		{bigFloat2p0, int1, 1},
+		{bigFloat2p0, bigInt1, 1},
+		{bigFloat2p0, bigFloat1p0, 1},
+		{bigFloat2p0, complex1j0, 1},
+
+		// Special cases involving char and complex.
+
+		// Char is always less than every other type.
+		{char1, int1, -1},
+		{char1, bigInt1, -1},
+		{char1, bigRat1o1, -1},
+		{char1, bigFloat1p0, -1},
+		{char1, complex1j0, -1},
+
+		// Complex that is actually real is like a float.
+		{complex1j0, int1, 0},
+		{complex1j0, char1, 1}, // Note: can't compare with char. See next block of tests.
+		{complex1j0, bigInt1, 0},
+		{complex1j0, bigRat1o1, 0},
+		{complex1j0, bigFloat1p0, 0},
+
+		// Complex with imaginary part is always greater than every other type.
+		{complex1j1, int1, 1},
+		{complex1j1, char1, 1},
+		{complex1j1, bigInt1, 1},
+		{complex1j1, bigRat1o1, 1},
+		{complex1j1, bigFloat1p0, 1},
+	}
+	var testConf config.Config
+	c := exec.NewContext(&testConf)
+	for _, test := range tests {
+		got := value.OrderedCompare(c, test.u, test.v)
+		if got != test.sgn {
+			t.Errorf("orderedCompare(%T(%v), %T(%v)) = %d, expected %d", test.u, test.u, test.v, test.v, got, test.sgn)
+		}
+	}
+}
diff --git a/parse/function.go b/parse/function.go
index 908a1a9..86cf504 100644
--- a/parse/function.go
+++ b/parse/function.go
@@ -19,9 +19,9 @@ import (
 //	"op" arg name arg '=' statements 
 //
 // statements:
+//
 //	expressionList
 //	'\n' (expressionList '\n')+ '\n' # For multiline definition, ending with blank line.
-//
 func (p *Parser) functionDefn() {
 	p.need(scan.Op)
 	fn := new(exec.Function)
diff --git a/parse/help.go b/parse/help.go
index 85c0909..a034c53 100644
--- a/parse/help.go
+++ b/parse/help.go
@@ -71,10 +71,11 @@ var helpLines = []string{
 	"\tNot               ∼B    not     Logical: not 1 is 0, not 0 is 1",
 	"\tAbsolute value    ∣B    abs     Magnitude of B",
 	"\tIndex generator   ⍳B    iota    Vector of the first B integers",
+	"\tUnique            ∩B    unique  Remove all duplicate elements from B",
 	"\tExponential       ⋆B    **      e to the B power",
 	"\tNegation          −B    -       Changes sign of B",
 	"\tIdentity          +B    +       No change to B",
-	"\tSignum            ×B    sgn     ¯1 if B<0; 0 if B=0; 1 if B>0",
+	"\tSignum            ×B    sgn     -1 if B<0; 0 if B=0; 1 if B>0",
 	"\tReciprocal        ÷B    /       1 divided by B",
 	"\tRavel             ,B    ,       Reshapes B into a vector",
 	"\tMatrix inverse    ⌹B            Inverse of matrix B",
@@ -109,66 +110,68 @@ var helpLines = []string{
 	"",
 	"Binary operators",
 	"",
-	"\tName                  APL   Ivy     Meaning",
-	"\tAdd                   A+B   +       Sum of A and B",
-	"\tSubtract              A−B   -       A minus B",
-	"\tMultiply              A×B   *       A multiplied by B",
-	"\tDivide                A÷B   /       A divided by B (exact rational division)",
-	"\t                            div     A divided by B (Euclidean)",
-	"\t                            idiv    A divided by B (Go)",
-	"\tExponentiation        A⋆B   **      A raised to the B power",
-	"\tCircle                A○B           Trigonometric functions of B selected by A",
-	"\t                                    A=1: sin(B) A=2: cos(B) A=3: tan(B); ¯A for inverse",
-	"\t                            sin     sin(B); ivy uses traditional name.",
-	"\t                            cos     cos(B); ivy uses traditional name.",
-	"\t                            tan     tan(B); ivy uses traditional name.",
-	"\tDeal                  A?B   ?       A distinct integers selected randomly from the first B integers",
-	"\tMembership            A∈B   in      1 for elements of A present in B; 0 where not.",
-	"\tMaximum               A⌈B   max     The greater value of A or B",
-	"\tMinimum               A⌊B   min     The smaller value of A or B",
-	"\tReshape               A⍴B   rho     Array of shape A with data B",
-	"\tTake                  A↑B   take    Select the first (or last) A elements of B according to ×A",
-	"\tDrop                  A↓B   drop    Remove the first (or last) A elements of B according to ×A",
-	"\tDecode                A⊥B   decode  Value of a polynomial whose coefficients are B at A",
-	"\tEncode                A⊤B   encode  Base-A representation of the value of B",
-	"\tResidue               A∣B           B modulo A",
-	"\t                            mod     A modulo B (Euclidean)",
-	"\t                            imod    A modulo B (Go)",
-	"\tCatenation            A,B   ,       Elements of B appended to the elements of A",
-	"\tExpansion             A\\B   fill    Insert zeros (or blanks) in B corresponding to zeros in A",
-	"\t                                    In ivy: abs(A) gives count, A <= 0 inserts zero (or blank)",
-	"\tCompression           A/B   sel     Select elements in B corresponding to ones in A",
-	"\t                                    In ivy: abs(A) gives count, A <= 0 inserts zero",
-	"\tIndex of              A⍳B   iota    The location (index) of B in A; 1+⌈/⍳⍴A if not found",
-	"\t                                    In ivy: origin-1 if not found (i.e. 0 if one-indexed)",
-	"\tMatrix divide         A⌹B           Solution to system of linear equations Ax = B",
-	"\tRotation              A⌽B   rot     The elements of B are rotated A positions left",
-	"\tRotation              A⊖B   flip    The elements of B are rotated A positions along the first axis",
-	"\tLogarithm             A⍟B   log     Logarithm of B to base A",
-	"\tDyadic format         A⍕B   text    Format B into a character matrix according to A",
-	"\t                                    A is the textual format (see format special command);",
-	"\t                                    otherwise result depends on length of A:",
-	"\t                                    1 gives decimal count, 2 gives width and decimal count,",
-	"\t                                    3 gives width, decimal count, and style ('d', 'e', 'f', etc.).",
-	"\tGeneral transpose     A⍉B   transp  The axes of B are ordered by A",
-	"\tCombinations          A!B   !       Number of combinations of B taken A at a time",
-	"\tLess than             A=      Comparison: 1 if true, 0 if false",
-	"\tGreater than          A>B   >       Comparison: 1 if true, 0 if false",
-	"\tNot equal             A≠B   !=      Comparison: 1 if true, 0 if false",
-	"\tOr                    A∨B   or      Logic: 0 if A and B are 0; 1 otherwise",
-	"\tAnd                   A∧B   and     Logic: 1 if A and B are 1; 0 otherwise",
-	"\tNor                   A⍱B   nor     Logic: 1 if both A and B are 0; otherwise 0",
-	"\tNand                  A⍲B   nand    Logic: 0 if both A and B are 1; otherwise 1",
-	"\tXor                         xor     Logic: 1 if A != B; otherwise 0",
-	"\tBitwise and                 &       Bitwise A and B (integer only)",
-	"\tBitwise or                  |       Bitwise A or B (integer only)",
-	"\tBitwise xor                 ^       Bitwise A exclusive or B (integer only)",
-	"\tLeft shift                  <<      A shifted left B bits (integer only)",
-	"\tRight Shift                 >>      A shifted right B bits (integer only)",
-	"\tComplex construction        j       The complex number A+Bi",
+	"\tName                  APL   Ivy       Meaning",
+	"\tAdd                   A+B   +         Sum of A and B",
+	"\tSubtract              A−B   -         A minus B",
+	"\tMultiply              A×B   *         A multiplied by B",
+	"\tDivide                A÷B   /         A divided by B (exact rational division)",
+	"\t                            div       A divided by B (Euclidean)",
+	"\t                            idiv      A divided by B (Go)",
+	"\tExponentiation        A⋆B   **        A raised to the B power",
+	"\tCircle                A○B             Trigonometric functions of B selected by A",
+	"\t                                      A=1: sin(B) A=2: cos(B) A=3: tan(B); ¯A for inverse",
+	"\t                            sin       sin(B); ivy uses traditional name.",
+	"\t                            cos       cos(B); ivy uses traditional name.",
+	"\t                            tan       tan(B); ivy uses traditional name.",
+	"\tDeal                  A?B   ?         A distinct integers selected randomly from the first B integers",
+	"\tMembership            A∈B   in        1 for elements of A present in B; 0 where not.",
+	"\tIntersection          A∩B   intersect A with all elements that are also in B removed",
+	"\tUnion                 A∪B   union     A followed by all members of B not already in A",
+	"\tMaximum               A⌈B   max       The greater value of A or B",
+	"\tMinimum               A⌊B   min       The smaller value of A or B",
+	"\tReshape               A⍴B   rho       Array of shape A with data B",
+	"\tTake                  A↑B   take      Select the first (or last) A elements of B according to sgn A",
+	"\tDrop                  A↓B   drop      Remove the first (or last) A elements of B according to sgn A",
+	"\tDecode                A⊥B   decode    Value of a polynomial whose coefficients are B at A",
+	"\tEncode                A⊤B   encode    Base-A representation of the value of B",
+	"\tResidue               A∣B              B modulo A",
+	"\t                            mod       A modulo B (Euclidean)",
+	"\t                            imod      A modulo B (Go)",
+	"\tCatenation            A,B   ,         Elements of B appended to the elements of A",
+	"\tExpansion             A\\B   fill      Insert zeros (or blanks) in B corresponding to zeros in A",
+	"\t                                      In ivy: abs(A) gives count, A <= 0 inserts zero (or blank)",
+	"\tCompression           A/B   sel       Select elements in B corresponding to ones in A",
+	"\t                                      In ivy: abs(A) gives count, A <= 0 inserts zero",
+	"\tIndex of              A⍳B   iota      The location (index) of B in A; 1+⌈/⍳⍴A if not found",
+	"\t                                      In ivy: origin-1 if not found (i.e. 0 if one-indexed)",
+	"\tMatrix divide         A⌹B             Solution to system of linear equations Ax = B",
+	"\tRotation              A⌽B   rot       The elements of B are rotated A positions left",
+	"\tRotation              A⊖B   flip      The elements of B are rotated A positions along the first axis",
+	"\tLogarithm             A⍟B   log       Logarithm of B to base A",
+	"\tDyadic format         A⍕B   text      Format B into a character matrix according to A",
+	"\t                                      A is the textual format (see format special command);",
+	"\t                                      otherwise result depends on length of A:",
+	"\t                                      1 gives decimal count, 2 gives width and decimal count,",
+	"\t                                      3 gives width, decimal count, and style ('d', 'e', 'f', etc.).",
+	"\tGeneral transpose     A⍉B   transp    The axes of B are ordered by A",
+	"\tCombinations          A!B   !         Number of combinations of B taken A at a time",
+	"\tLess than             A=        Comparison: 1 if true, 0 if false",
+	"\tGreater than          A>B   >         Comparison: 1 if true, 0 if false",
+	"\tNot equal             A≠B   !=        Comparison: 1 if true, 0 if false",
+	"\tOr                    A∨B   or        Logic: 0 if A and B are 0; 1 otherwise",
+	"\tAnd                   A∧B   and       Logic: 1 if A and B are 1; 0 otherwise",
+	"\tNor                   A⍱B   nor       Logic: 1 if both A and B are 0; otherwise 0",
+	"\tNand                  A⍲B   nand      Logic: 0 if both A and B are 1; otherwise 1",
+	"\tXor                         xor       Logic: 1 if A != B; otherwise 0",
+	"\tBitwise and                 &         Bitwise A and B (integer only)",
+	"\tBitwise or                  |         Bitwise A or B (integer only)",
+	"\tBitwise xor                 ^         Bitwise A exclusive or B (integer only)",
+	"\tLeft shift                  <<        A shifted left B bits (integer only)",
+	"\tRight Shift                 >>        A shifted right B bits (integer only)",
+	"\tComplex construction        j         The complex number A+Bi",
 	"",
 	"Operators and axis indicator",
 	"",
@@ -383,92 +386,95 @@ var helpUnary = map[string]helpIndexPair{
 	"not":    {65, 65},
 	"abs":    {66, 66},
 	"iota":   {67, 67},
-	"**":     {68, 68},
-	"-":      {69, 69},
-	"+":      {70, 70},
-	"sgn":    {71, 71},
-	"/":      {72, 72},
-	",":      {73, 73},
-	"log":    {76, 76},
-	"rot":    {77, 77},
-	"flip":   {78, 78},
-	"up":     {79, 79},
-	"down":   {80, 80},
-	"ivy":    {81, 81},
-	"text":   {82, 82},
-	"transp": {83, 83},
-	"!":      {84, 84},
-	"^":      {85, 85},
-	"sqrt":   {86, 86},
-	"sin":    {87, 87},
-	"cos":    {88, 88},
-	"tan":    {89, 89},
-	"asin":   {90, 90},
-	"acos":   {91, 91},
-	"atan":   {92, 92},
-	"sinh":   {93, 93},
-	"cosh":   {94, 94},
-	"tanh":   {95, 95},
-	"asinh":  {96, 96},
-	"acosh":  {97, 97},
-	"atanh":  {98, 98},
-	"j":      {99, 99},
-	"real":   {100, 100},
-	"imag":   {101, 101},
-	"phase":  {102, 102},
-	"code":   {181, 181},
-	"char":   {182, 182},
-	"float":  {183, 185},
+	"unique": {68, 68},
+	"**":     {69, 69},
+	"-":      {70, 70},
+	"+":      {71, 71},
+	"sgn":    {72, 72},
+	"/":      {73, 73},
+	",":      {74, 74},
+	"log":    {77, 77},
+	"rot":    {78, 78},
+	"flip":   {79, 79},
+	"up":     {80, 80},
+	"down":   {81, 81},
+	"ivy":    {82, 82},
+	"text":   {83, 83},
+	"transp": {84, 84},
+	"!":      {85, 85},
+	"^":      {86, 86},
+	"sqrt":   {87, 87},
+	"sin":    {88, 88},
+	"cos":    {89, 89},
+	"tan":    {90, 90},
+	"asin":   {91, 91},
+	"acos":   {92, 92},
+	"atan":   {93, 93},
+	"sinh":   {94, 94},
+	"cosh":   {95, 95},
+	"tanh":   {96, 96},
+	"asinh":  {97, 97},
+	"acosh":  {98, 98},
+	"atanh":  {99, 99},
+	"j":      {100, 100},
+	"real":   {101, 101},
+	"imag":   {102, 102},
+	"phase":  {103, 103},
+	"code":   {184, 184},
+	"char":   {185, 185},
+	"float":  {186, 188},
 }
 
 var helpBinary = map[string]helpIndexPair{
-	"+":      {107, 107},
-	"-":      {108, 108},
-	"*":      {109, 109},
-	"/":      {110, 112},
-	"**":     {113, 113},
-	"?":      {119, 119},
-	"in":     {120, 120},
-	"max":    {121, 121},
-	"min":    {122, 122},
-	"rho":    {123, 123},
-	"take":   {124, 124},
-	"drop":   {125, 125},
-	"decode": {126, 126},
-	"encode": {127, 127},
-	"mod":    {129, 130},
-	",":      {131, 131},
-	"fill":   {132, 133},
-	"sel":    {134, 135},
-	"iota":   {136, 137},
-	"rot":    {139, 139},
-	"flip":   {140, 140},
-	"log":    {141, 141},
-	"text":   {142, 146},
-	"transp": {147, 147},
-	"!":      {148, 148},
-	"<":      {149, 149},
-	"<=":     {150, 150},
-	"==":     {151, 151},
-	">=":     {152, 152},
-	">":      {153, 153},
-	"!=":     {154, 154},
-	"or":     {155, 155},
-	"and":    {156, 156},
-	"nor":    {157, 157},
-	"nand":   {158, 158},
-	"xor":    {159, 159},
-	"&":      {160, 160},
-	"|":      {161, 161},
-	"^":      {162, 162},
-	"<<":     {163, 163},
-	">>":     {164, 164},
-	"j":      {165, 165},
+	"+":         {108, 108},
+	"-":         {109, 109},
+	"*":         {110, 110},
+	"/":         {111, 113},
+	"**":        {114, 114},
+	"?":         {120, 120},
+	"in":        {121, 121},
+	"intersect": {122, 122},
+	"union":     {123, 123},
+	"max":       {124, 124},
+	"min":       {125, 125},
+	"rho":       {126, 126},
+	"take":      {127, 127},
+	"drop":      {128, 128},
+	"decode":    {129, 129},
+	"encode":    {130, 130},
+	"mod":       {132, 133},
+	",":         {134, 134},
+	"fill":      {135, 136},
+	"sel":       {137, 138},
+	"iota":      {139, 140},
+	"rot":       {142, 142},
+	"flip":      {143, 143},
+	"log":       {144, 144},
+	"text":      {145, 149},
+	"transp":    {150, 150},
+	"!":         {151, 151},
+	"<":         {152, 152},
+	"<=":        {153, 153},
+	"==":        {154, 154},
+	">=":        {155, 155},
+	">":         {156, 156},
+	"!=":        {157, 157},
+	"or":        {158, 158},
+	"and":       {159, 159},
+	"nor":       {160, 160},
+	"nand":      {161, 161},
+	"xor":       {162, 162},
+	"&":         {163, 163},
+	"|":         {164, 164},
+	"^":         {165, 165},
+	"<<":        {166, 166},
+	">>":        {167, 167},
+	"j":         {168, 168},
 }
 
 var helpAxis = map[string]helpIndexPair{
-	"/":  {170, 170},
-	"\\": {172, 172},
-	".":  {174, 174},
-	"o.": {175, 175},
+	"/":  {173, 173},
+	"\\": {175, 175},
+	".":  {177, 177},
+	"o.": {178, 178},
 }
diff --git a/parse/parse.go b/parse/parse.go
index 4cad55e..8df1e05 100644
--- a/parse/parse.go
+++ b/parse/parse.go
@@ -352,6 +352,7 @@ func (p *Parser) errorf(format string, args ...interface{}) {
 // The boolean reports whether the line is valid.
 //
 // Line
+//
 //	) special command '\n'
 //	def function defintion
 //	expressionList '\n'
@@ -401,6 +402,7 @@ func (p *Parser) readTokensToNewline() bool {
 }
 
 // expressionList:
+//
 //	statementList 
 func (p *Parser) expressionList() ([]value.Expr, bool) {
 	exprs, ok := p.statementList()
@@ -420,6 +422,7 @@ func (p *Parser) expressionList() ([]value.Expr, bool) {
 }
 
 // statementList:
+//
 //	expr [':' expr] [';' statementList]
 func (p *Parser) statementList() ([]value.Expr, bool) {
 	expr := p.expr()
@@ -448,6 +451,7 @@ func (p *Parser) statementList() ([]value.Expr, bool) {
 }
 
 // expr
+//
 //	operand
 //	operand binop expr
 func (p *Parser) expr() value.Expr {
@@ -490,6 +494,7 @@ func (p *Parser) expr() value.Expr {
 }
 
 // operand
+//
 //	number
 //	char constant
 //	string constant
@@ -525,6 +530,7 @@ func (p *Parser) operand(tok scan.Token, indexOK bool) value.Expr {
 }
 
 // index
+//
 //	expr
 //	expr [ expr ]
 //	expr [ expr ] [ expr ] ....
@@ -545,6 +551,7 @@ func (p *Parser) index(expr value.Expr) value.Expr {
 }
 
 // indexList
+//
 //	[[expr] [';' [expr]] ...]
 func (p *Parser) indexList() []value.Expr {
 	list := []value.Expr{}
@@ -571,11 +578,13 @@ func (p *Parser) indexList() []value.Expr {
 }
 
 // number
+//
 //	integer
 //	rational
 //	string
 //	variable
 //	'(' Expr ')'
+//
 // If the value is a string, value.Expr is nil.
 func (p *Parser) number(tok scan.Token) (expr value.Expr, str string) {
 	var err error
@@ -602,6 +611,7 @@ func (p *Parser) number(tok scan.Token) (expr value.Expr, str string) {
 
 // numberOrVector turns the token and what follows into a numeric Value, possibly a vector.
 // numberOrVector
+//
 //	number
 //	string
 //	numberOrVector...
diff --git a/testdata/binary_vector.ivy b/testdata/binary_vector.ivy
index 0d4f95a..e9db069 100644
--- a/testdata/binary_vector.ivy
+++ b/testdata/binary_vector.ivy
@@ -523,6 +523,15 @@ x = 1 rho 1; rho x[up x]
 'abcde' in 'hello world'
 	0 0 0 1 1
 
+1 in 'abc'
+	0
+
+'a' in 1 2 3
+	0
+
+1 2 'a' 3 in 'a'
+	0 0 1 0
+
 'abc'[3 4 rho iota 3]
 	abca
 	bcab
@@ -645,6 +654,90 @@ circle 10
 10 decode 3 3 rho iota 9
 	147 258 369
 
+1 2 3 union 2 3 4
+	1 2 3 4
+
+# Values compare regardless of type
+1j0 1 (float 1) union 1/1
+	1j0 1 1
+
+'abc' union 'cba'
+	abc
+
+3 union 1 2 3 4
+	3 1 2 4
+
+2 union 1 2 3 4 1 2 3 4
+	2 1 3 4 1 3 4
+
+(iota 0) union (iota 0); "empty"
+	empty
+
+(iota 0) union 1
+	1
+
+(iota 0) union 1 2
+	1 2
+
+1 union (iota 0)
+	1
+
+1 2 union (iota 0)
+	1 2
+
+# Chars are extra tricky
+1 2 'abc' 3 4 union 2 'cba' 5 1 'x'
+	1 2 a b c 3 4 5 x
+
+1j0 2j0 3j1 3j2 union 2 3j1 4 3j3
+	1j0 2j0 3j1 3j2 4 3j3
+
+1 2 3 intersect 2 3 4
+	2 3
+
+3 intersect 3 3 3
+	3
+
+3 intersect 1 2 3 1 2 3
+	3
+
+5 intersect 1 2 3 1 2 3; "empty"
+	empty
+
+# Values compare regardless of type
+1j0 1 (float 1) intersect 1/1
+	1j0 1 1
+
+'abc' intersect 'bcd'
+	bc
+
+(iota 0) intersect iota 0; "empty"
+	empty
+
+(iota 0) intersect 1; "empty"
+	empty
+
+(iota 0) intersect 1 2; "empty"
+	empty
+
+1 intersect iota 0; "empty"
+	empty
+
+1 2 intersect iota 0; "empty"
+	empty
+
+# Chars are extra tricky
+1 2 'abc' 3 4 intersect 2 'ca' 5 1 'x'
+	1 2 a c
+
+1j0 2j0 3j1 3j2 intersect 2 3j1 4 3j3
+	2j0 3j1
+
+# Performance test. Should be almost instantaneous,
+# but naive implementation would be slow.
+x=iota 100000; y=x intersect x; z = x union x; +/y==z
+	100000
+
 # Fixed bug: don't use user-defined functions in core calculations.
 op a mod b = 99
 op a div b = 99
diff --git a/testdata/help.ivy b/testdata/help.ivy
index b46f222..e38b096 100644
--- a/testdata/help.ivy
+++ b/testdata/help.ivy
@@ -12,7 +12,7 @@
 
 	Binary operators:
 		Name                  APL   Ivy     Meaning
-		Reshape               A⍴B   rho     Array of shape A with data B
+		Reshape               A⍴B   rho       Array of shape A with data B
 
 )help about reverse
 	#
diff --git a/testdata/unary_vector.ivy b/testdata/unary_vector.ivy
index 412aaba..31106da 100644
--- a/testdata/unary_vector.ivy
+++ b/testdata/unary_vector.ivy
@@ -104,6 +104,25 @@ flip iota 1
 flip iota 10
 	10 9 8 7 6 5 4 3 2 1
 
+unique iota 5
+	1 2 3 4 5
+
+unique 1 1 2 2
+	1 2
+
+unique 'mississippi'
+	misp
+
+# Choose the "lowest" type of same value
+unique 1j0 1 1/1 (float 1) 2/1 2j0 2 (float 2)
+	1 2
+
+unique 1 'a' 2 'b' 3 'a' 2
+	1 a 2 b 3
+
+unique iota 0; "empty"
+	empty
+
 # Fixed bug: don't use user-defined functions in core calculations.
 op rot x = 99
 flip 1 2 3  # Used rot internally.
diff --git a/value/asin.go b/value/asin.go
index 0cd17b8..c0b8ec0 100644
--- a/value/asin.go
+++ b/value/asin.go
@@ -15,7 +15,7 @@ func asin(c Context, v Value) Value {
 		}
 		v = u.real
 	} else if !inArcRealDomain(v) {
-		return complexAsin(c, newComplex(v, zero))
+		return complexAsin(c, NewComplex(v, zero))
 	}
 	return evalFloatFunc(c, v, floatAsin)
 }
@@ -27,7 +27,7 @@ func acos(c Context, v Value) Value {
 		}
 		v = u.real
 	} else if !inArcRealDomain(v) {
-		return complexAcos(c, newComplex(v, zero))
+		return complexAcos(c, NewComplex(v, zero))
 	}
 	return evalFloatFunc(c, v, floatAcos)
 }
@@ -187,23 +187,23 @@ func floatAtanLarge(c Context, x *big.Float) *big.Float {
 
 func complexAsin(c Context, v Complex) Complex {
 	// Use the formula: asin(v) = -i * log(sqrt(1-v²) + i*v)
-	x := newComplex(one, zero)
+	x := NewComplex(one, zero)
 	x = complexSqrt(c, x.sub(c, v.mul(c, v)))
-	i := newComplex(zero, one)
+	i := NewComplex(zero, one)
 	x = x.add(c, i.mul(c, v))
 	x = complexLog(c, x)
-	return newComplex(zero, minusOne).mul(c, x)
+	return NewComplex(zero, minusOne).mul(c, x)
 }
 
 func complexAcos(c Context, v Complex) Value {
 	// Use the formula: acos(v) = π/2 - asin(v)
-	piBy2 := newComplex(BigFloat{newFloat(c).Set(floatPiBy2)}, BigFloat{floatZero})
+	piBy2 := NewComplex(BigFloat{newFloat(c).Set(floatPiBy2)}, BigFloat{floatZero})
 	return piBy2.sub(c, complexAsin(c, v))
 }
 
 func complexAtan(c Context, v Complex) Value {
 	// Use the formula: atan(v) = 1/2i * log((1-v)/(1+v))
-	i := newComplex(zero, one)
+	i := NewComplex(zero, one)
 	res := i.sub(c, v).div(c, i.add(c, v))
 	res = complexLog(c, res)
 	return res.mul(c, minusOneOverTwoI)
diff --git a/value/asinh.go b/value/asinh.go
index 1d2ab09..1d94207 100644
--- a/value/asinh.go
+++ b/value/asinh.go
@@ -26,7 +26,7 @@ func acosh(c Context, v Value) Value {
 		v = u.real
 	}
 	if compare(v, 1) < 0 {
-		return complexAcosh(c, newComplex(v, zero))
+		return complexAcosh(c, NewComplex(v, zero))
 	}
 	return evalFloatFunc(c, v, floatAcosh)
 }
@@ -39,7 +39,7 @@ func atanh(c Context, v Value) Value {
 		v = u.real
 	}
 	if compare(v, -1) <= 0 || 0 <= compare(v, 1) {
-		return complexAtanh(c, newComplex(v, zero))
+		return complexAtanh(c, NewComplex(v, zero))
 	}
 	return evalFloatFunc(c, v, floatAtanh)
 }
@@ -84,7 +84,7 @@ func floatAtanh(c Context, x *big.Float) *big.Float {
 // complexAsinh computes asinh(x) using the formula asinh(x) = log(x + sqrt(x²+1)).
 func complexAsinh(c Context, x Complex) Complex {
 	z := x.mul(c, x)
-	z = z.add(c, newComplex(one, zero))
+	z = z.add(c, NewComplex(one, zero))
 	z = complexSqrt(c, z)
 	z = z.add(c, x)
 	return complexLog(c, z)
@@ -93,7 +93,7 @@ func complexAsinh(c Context, x Complex) Complex {
 // complexAcosh computes asinh(x) using the formula asinh(x) = log(x + sqrt(x²-1)).
 func complexAcosh(c Context, x Complex) Complex {
 	z := x.mul(c, x)
-	z = z.sub(c, newComplex(one, zero))
+	z = z.sub(c, NewComplex(one, zero))
 	z = complexSqrt(c, z)
 	z = z.add(c, x)
 	return complexLog(c, z)
diff --git a/value/bigfloat.go b/value/bigfloat.go
index 4032b50..03a42a5 100644
--- a/value/bigfloat.go
+++ b/value/bigfloat.go
@@ -140,7 +140,7 @@ func (f BigFloat) toType(op string, conf *config.Config, which valueType) Value
 	case bigFloatType:
 		return f
 	case complexType:
-		return newComplex(f, Int(0))
+		return NewComplex(f, Int(0))
 	case vectorType:
 		return NewVector([]Value{f})
 	case matrixType:
diff --git a/value/bigint.go b/value/bigint.go
index 57eb198..5d8dfc3 100644
--- a/value/bigint.go
+++ b/value/bigint.go
@@ -149,7 +149,7 @@ func (i BigInt) toType(op string, conf *config.Config, which valueType) Value {
 		f := new(big.Float).SetPrec(conf.FloatPrec()).SetInt(i.Int)
 		return BigFloat{f}
 	case complexType:
-		return newComplex(i, Int(0))
+		return NewComplex(i, Int(0))
 	case vectorType:
 		return NewVector([]Value{i})
 	case matrixType:
diff --git a/value/bigrat.go b/value/bigrat.go
index 3f3684e..645c2c6 100644
--- a/value/bigrat.go
+++ b/value/bigrat.go
@@ -179,7 +179,7 @@ func (r BigRat) toType(op string, conf *config.Config, which valueType) Value {
 		f := new(big.Float).SetPrec(conf.FloatPrec()).SetRat(r.Rat)
 		return BigFloat{f}
 	case complexType:
-		return newComplex(r, Int(0))
+		return NewComplex(r, Int(0))
 	case vectorType:
 		return NewVector([]Value{r})
 	case matrixType:
diff --git a/value/binary.go b/value/binary.go
index b08b2d9..9eadd74 100644
--- a/value/binary.go
+++ b/value/binary.go
@@ -201,16 +201,16 @@ func init() {
 			whichType:   binaryArithType,
 			fn: [numType]binaryFn{
 				intType: func(c Context, u, v Value) Value {
-					return newComplex(u, v)
+					return NewComplex(u, v)
 				},
 				bigIntType: func(c Context, u, v Value) Value {
-					return newComplex(u, v)
+					return NewComplex(u, v)
 				},
 				bigRatType: func(c Context, u, v Value) Value {
-					return newComplex(u, v)
+					return NewComplex(u, v)
 				},
 				bigFloatType: func(c Context, u, v Value) Value {
-					return newComplex(u, v)
+					return NewComplex(u, v)
 				},
 			},
 		},
@@ -1504,13 +1504,47 @@ func init() {
 			},
 		},
 
+		// Special cases that mix types, so don't promote them.
 		{
-			// Special case, handled in EvalBinary: don't modify types.
-			name:        "text",
-			elementwise: true,
-			whichType:   nil,
+			name:      "intersect",
+			whichType: noPromoteType,
+			fn: [numType]binaryFn{
+				intType:      intersect,
+				charType:     intersect,
+				bigIntType:   intersect,
+				bigRatType:   intersect,
+				bigFloatType: intersect,
+				complexType:  intersect,
+				vectorType:   intersect,
+			},
+		},
+
+		{
+			name:      "union",
+			whichType: noPromoteType,
+			fn: [numType]binaryFn{
+				intType:      union,
+				charType:     union,
+				bigIntType:   union,
+				bigRatType:   union,
+				bigFloatType: union,
+				complexType:  union,
+				vectorType:   union,
+			},
+		},
+
+		{
+			name:      "text",
+			whichType: noPromoteType,
 			fn: [numType]binaryFn{
-				0: fmtText,
+				intType:      fmtText,
+				charType:     fmtText,
+				bigIntType:   fmtText,
+				bigRatType:   fmtText,
+				bigFloatType: fmtText,
+				complexType:  fmtText,
+				vectorType:   fmtText,
+				matrixType:   fmtText,
 			},
 		},
 	}
diff --git a/value/char.go b/value/char.go
index a12accc..3ba114b 100644
--- a/value/char.go
+++ b/value/char.go
@@ -79,7 +79,7 @@ func ParseString(s string) string {
 }
 
 // unquote is a simplified strconv.Unquote that treats ' and " equally.
-// Raw quotes are Go-like and bounded by ``.
+// Raw quotes are Go-like and bounded by “.
 // The return value is the string and a boolean rather than error, which
 // was almost always the same anyway.
 func unquote(s string) (t string, ok bool) {
diff --git a/value/complex.go b/value/complex.go
index 76e81a4..2c54494 100644
--- a/value/complex.go
+++ b/value/complex.go
@@ -15,7 +15,7 @@ type Complex struct {
 	imag Value
 }
 
-func newComplex(u, v Value) Complex {
+func NewComplex(u, v Value) Complex {
 	if !simpleNumber(u) || !simpleNumber(v) {
 		Errorf("bad complex construction: %v %v", u, v)
 	}
@@ -91,7 +91,7 @@ func (c Complex) shrink() Value {
 // Arithmetic.
 
 func (c Complex) neg(ctx Context) Complex {
-	return newComplex(ctx.EvalUnary("-", c.real), ctx.EvalUnary("-", c.imag))
+	return NewComplex(ctx.EvalUnary("-", c.real), ctx.EvalUnary("-", c.imag))
 }
 
 func (c Complex) recip(ctx Context) Complex {
@@ -101,7 +101,7 @@ func (c Complex) recip(ctx Context) Complex {
 	denom := ctx.EvalBinary(ctx.EvalBinary(c.real, "*", c.real), "+", ctx.EvalBinary(c.imag, "*", c.imag))
 	r := ctx.EvalBinary(c.real, "/", denom)
 	i := ctx.EvalUnary("-", ctx.EvalBinary(c.imag, "/", denom))
-	return newComplex(r, i)
+	return NewComplex(r, i)
 }
 
 func (c Complex) abs(ctx Context) Value {
@@ -141,17 +141,17 @@ func (c Complex) phase(ctx Context) Value {
 }
 
 func (c Complex) add(ctx Context, d Complex) Complex {
-	return newComplex(ctx.EvalBinary(c.real, "+", d.real), ctx.EvalBinary(c.imag, "+", d.imag))
+	return NewComplex(ctx.EvalBinary(c.real, "+", d.real), ctx.EvalBinary(c.imag, "+", d.imag))
 }
 
 func (c Complex) sub(ctx Context, d Complex) Complex {
-	return newComplex(ctx.EvalBinary(c.real, "-", d.real), ctx.EvalBinary(c.imag, "-", d.imag))
+	return NewComplex(ctx.EvalBinary(c.real, "-", d.real), ctx.EvalBinary(c.imag, "-", d.imag))
 }
 
 func (c Complex) mul(ctx Context, d Complex) Complex {
 	r := ctx.EvalBinary(ctx.EvalBinary(c.real, "*", d.real), "-", ctx.EvalBinary(c.imag, "*", d.imag))
 	i := ctx.EvalBinary(ctx.EvalBinary(d.imag, "*", c.real), "+", ctx.EvalBinary(d.real, "*", c.imag))
-	return newComplex(r, i)
+	return NewComplex(r, i)
 }
 
 func (c Complex) div(ctx Context, d Complex) Complex {
@@ -164,12 +164,12 @@ func (c Complex) div(ctx Context, d Complex) Complex {
 		r = ctx.EvalBinary(r, "/", denom)
 		i := ctx.EvalBinary(c.imag, "*", d.real)
 		i = ctx.EvalBinary(i, "/", denom)
-		return newComplex(r, i)
+		return NewComplex(r, i)
 	}
 	denom := ctx.EvalBinary(ctx.EvalBinary(d.real, "*", d.real), "+", ctx.EvalBinary(d.imag, "*", d.imag))
 	r := ctx.EvalBinary(ctx.EvalBinary(c.real, "*", d.real), "+", ctx.EvalBinary(c.imag, "*", d.imag))
 	r = ctx.EvalBinary(r, "/", denom)
 	i := ctx.EvalBinary(ctx.EvalBinary(c.imag, "*", d.real), "-", ctx.EvalBinary(c.real, "*", d.imag))
 	i = ctx.EvalBinary(i, "/", denom)
-	return newComplex(r, i)
+	return NewComplex(r, i)
 }
diff --git a/value/const.go b/value/const.go
index 982af7c..322650f 100644
--- a/value/const.go
+++ b/value/const.go
@@ -46,8 +46,8 @@ var (
 	bigRatTen      = big.NewRat(10, 1)
 	bigRatBillion  = big.NewRat(1e9, 1)
 
-	complexOne       = newComplex(one, zero)
-	complexHalf      = newComplex(BigRat{big.NewRat(1, 2)}, zero)
+	complexOne       = NewComplex(one, zero)
+	complexHalf      = NewComplex(BigRat{big.NewRat(1, 2)}, zero)
 	minusOneOverTwoI Complex
 
 	// set to constPrecision
@@ -126,5 +126,5 @@ func init() {
 	if err != nil {
 		panic(err)
 	}
-	minusOneOverTwoI = newComplex(num, den)
+	minusOneOverTwoI = NewComplex(num, den)
 }
diff --git a/value/eval.go b/value/eval.go
index e8d6f5b..e1e1d49 100644
--- a/value/eval.go
+++ b/value/eval.go
@@ -88,14 +88,6 @@ func whichType(v Value) valueType {
 }
 
 func (op *binaryOp) EvalBinary(c Context, u, v Value) Value {
-	if op.whichType == nil {
-		// At the moment, "text" is the only operator that leaves
-		// both arg types alone. Perhaps more will arrive.
-		if op.name != "text" {
-			Errorf("internal error: nil whichType")
-		}
-		return op.fn[0](c, u, v)
-	}
 	whichU, whichV := op.whichType(whichType(u), whichType(v))
 	conf := c.Config()
 	u = u.toType(op.name, conf, whichU)
@@ -115,6 +107,25 @@ func (op *binaryOp) EvalBinary(c Context, u, v Value) Value {
 	return fn(c, u, v)
 }
 
+// EvalCharEqual handles == and != in a special case:
+// If comparing a scalar against a Char, avoid the conversion.
+// The logic of type promotion in EvalBinary otherwise interferes with comparison
+// because it tries to force scalar types to be the same, and char doesn't convert to
+// any other type.
+func EvalCharEqual(u Value, isEqualOp bool, v Value) (Value, bool) {
+	uType, vType := whichType(u), whichType(v)
+	if uType != vType && uType < vectorType && vType < vectorType {
+		// Two different scalar types. If either is char, we know the answer now.
+		if uType == charType || vType == charType {
+			if isEqualOp {
+				return Int(0), true
+			}
+			return Int(1), true
+		}
+	}
+	return nil, false
+}
+
 // Product computes a compound product, such as an inner product
 // "+.*" or outer product "o.*". The op is known to contain a
 // period. The operands are all at least vectors, and for inner product
diff --git a/value/int.go b/value/int.go
index 030ed18..2fe3075 100644
--- a/value/int.go
+++ b/value/int.go
@@ -151,7 +151,7 @@ func (i Int) toType(op string, conf *config.Config, which valueType) Value {
 	case bigFloatType:
 		return bigFloatInt64(conf, int64(i))
 	case complexType:
-		return newComplex(i, Int(0))
+		return NewComplex(i, Int(0))
 	case vectorType:
 		return NewVector([]Value{i})
 	case matrixType:
diff --git a/value/log.go b/value/log.go
index cac0e98..3e0324e 100644
--- a/value/log.go
+++ b/value/log.go
@@ -12,7 +12,7 @@ func logn(c Context, v Value) Value {
 	negative := isNegative(v)
 	if negative {
 		// Promote to complex. The Complex type is never negative.
-		v = newComplex(v, Int(0))
+		v = NewComplex(v, Int(0))
 	}
 	if u, ok := v.(Complex); ok {
 		if isNegative(u.real) {
@@ -171,5 +171,5 @@ func floatLog(c Context, x *big.Float) *big.Float {
 func complexLog(c Context, v Complex) Complex {
 	abs := v.abs(c)
 	phase := v.phase(c)
-	return newComplex(logn(c, abs), phase)
+	return NewComplex(logn(c, abs), phase)
 }
diff --git a/value/matrix.go b/value/matrix.go
index 94574be..d5f68fa 100644
--- a/value/matrix.go
+++ b/value/matrix.go
@@ -524,7 +524,6 @@ func (m *Matrix) binaryTranspose(c Context, v Vector) *Matrix {
 //	(n ...), (m ...) -> (n+m ...)  # list, list
 //	(1), (n ...) -> (n+1 ...)  # scalar (extended), list
 //	(n ...), (1) -> (n+1 ...)  # list, scalar (extended)
-//
 func (x *Matrix) catenate(y *Matrix) *Matrix {
 	if x.Rank() == 0 || y.Rank() == 0 {
 		Errorf("empty matrix for ,")
diff --git a/value/power.go b/value/power.go
index b2043f8..ce932c0 100644
--- a/value/power.go
+++ b/value/power.go
@@ -41,7 +41,7 @@ func expComplex(c Context, v Complex) Value {
 	eToX := exponential(c.Config(), x)
 	cosY := floatCos(c, y)
 	sinY := floatSin(c, y)
-	return newComplex(BigFloat{cosY.Mul(cosY, eToX)}, BigFloat{sinY.Mul(sinY, eToX)})
+	return NewComplex(BigFloat{cosY.Mul(cosY, eToX)}, BigFloat{sinY.Mul(sinY, eToX)})
 }
 
 // floatPower computes bx to the power of bexp.
@@ -138,8 +138,8 @@ func integerPower(c Context, x *big.Float, exp int64) *big.Float {
 
 // complexIntegerPower returns x**exp where exp is an int64 of size <= intBits.
 func complexIntegerPower(c Context, v Complex, exp int64) Complex {
-	z := newComplex(Int(1), Int(0))
-	y := newComplex(v.real, v.imag)
+	z := NewComplex(Int(1), Int(0))
+	y := NewComplex(v.real, v.imag)
 	// For each loop, we compute xⁿ where n is a power of two.
 	for exp > 0 {
 		if exp&1 == 1 {
diff --git a/value/sets.go b/value/sets.go
new file mode 100644
index 0000000..fb00e94
--- /dev/null
+++ b/value/sets.go
@@ -0,0 +1,252 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package value
+
+import "sort"
+
+// Operations on "sets", which are really just lists that
+// can contain duplicates rather than in the mathematical
+// definition of sets. APL's like that.
+
+func union(c Context, u, v Value) Value {
+	uType := whichType(u)
+	vType := whichType(v)
+	if uType < vectorType && vType < vectorType {
+		// Scalars
+		if scalarEqual(c, u, v) {
+			return u
+		}
+		return NewVector([]Value{u, v})
+	}
+	// Neither can be a matrix.
+	if uType == matrixType || vType == matrixType {
+		Errorf("binary union not implemented on type matrix")
+	}
+	// At least one is a Vector.
+	switch {
+	case vType != vectorType:
+		uu := u.(Vector).Copy()
+		for _, x := range uu {
+			if scalarEqual(c, x, v) {
+				return uu
+			}
+		}
+		return NewVector(append(uu, v))
+	case uType != vectorType:
+		vv := v.(Vector)
+		elems := []Value{u}
+		for _, x := range vv {
+			if !scalarEqual(c, u, x) {
+				elems = append(elems, x)
+			}
+		}
+		return NewVector(elems)
+	default: // Both vectors.
+		uu := u.(Vector).Copy()
+		vv := v.(Vector)
+		present := membership(c, vv, uu)
+		for i, x := range vv {
+			if present[i] != one {
+				uu = append(uu, x)
+			}
+		}
+		return uu
+	}
+}
+
+func intersect(c Context, u, v Value) Value {
+	uType := whichType(u)
+	vType := whichType(v)
+	if uType < vectorType && vType < vectorType {
+		// Scalars
+		if scalarEqual(c, u, v) {
+			return u
+		}
+		return NewVector([]Value{})
+	}
+	// Neither can be a matrix.
+	if uType == matrixType || vType == matrixType {
+		Errorf("binary intersect not implemented on type matrix")
+	}
+	// At least one is a Vector.
+	var elems []Value
+	switch {
+	case vType != vectorType:
+		uu := u.(Vector)
+		for _, x := range uu {
+			if scalarEqual(c, x, v) {
+				elems = append(elems, x)
+			}
+		}
+	case uType != vectorType:
+		vv := v.(Vector)
+		for _, x := range vv {
+			if scalarEqual(c, u, x) {
+				return NewVector([]Value{u})
+			}
+		}
+		return NewVector([]Value{})
+	default: // Both vectors.
+		uu := u.(Vector)
+		present := membership(c, uu, v.(Vector))
+		for i, x := range uu {
+			if present[i] == one {
+				elems = append(elems, x)
+			}
+		}
+	}
+	return NewVector(elems)
+}
+
+func unique(c Context, v Value) Value {
+	vType := whichType(v)
+	if vType < vectorType {
+		// Scalar
+		return v
+	}
+	if vType == matrixType {
+		Errorf("unary unique not implemented on type matrix")
+	}
+	vv := v.(Vector)
+	if len(vv) == 0 {
+		return vv
+	}
+	// We could just sort and dedup, but that loses the original
+	// order of elements in the vector, which must be preserved.
+	type indexedValue struct {
+		i int
+		v Value
+	}
+	sorted := make([]indexedValue, len(vv))
+	for i, x := range vv {
+		sorted[i] = indexedValue{i, x}
+	}
+	// Sort based on the values, preserving index information.
+	sort.Slice(sorted, func(i, j int) bool {
+		c := OrderedCompare(c, sorted[i].v, sorted[j].v)
+		if c == 0 {
+			// Choose lower type. You need to choose one, so pick lowest.
+			return whichType(sorted[i].v) < whichType(sorted[j].v)
+		}
+		return c < 0
+	})
+	// Remove duplicates to make a unique list.
+	prev := sorted[0]
+	uniqued := []indexedValue{prev}
+	for _, x := range sorted[1:] {
+		if OrderedCompare(c, prev.v, x.v) != 0 {
+			uniqued = append(uniqued, x)
+			prev = x
+		}
+	}
+	// Restore the original order by sorting on the indexes.
+	sort.Slice(uniqued, func(i, j int) bool {
+		return uniqued[i].i < uniqued[j].i
+	})
+	elems := make([]Value, len(uniqued))
+	for i, x := range uniqued {
+		elems[i] = x.v
+	}
+	return NewVector(elems)
+}
+
+// scalarEqual is faster(ish) comparison to make set ops more efficient.
+// The arguments must be scalars.
+func scalarEqual(c Context, u, v Value) bool {
+	return OrderedCompare(c, u, v) == 0
+}
+
+// OrderedCompare returns -1, 0, or 1 according to whether u is
+// less than, equal to, or greater than v, according to total ordering
+// rules. Total ordering is not the usual mathematical definition,
+// as we honor things like 1.0 == 1, comparison of int and char
+// is forbidden, and complex numbers do not implement <.
+// Thus we amend the usual orderings:
+// - Char is below all other types
+// - Complex is above all other types, unless on the real line: 1j0 == 1.
+//
+// Exported only for testing, which is done by the parent directory.
+// TODO: Expand to vectors and matrices?
+func OrderedCompare(c Context, u, v Value) int {
+	uType := whichType(u)
+	vType := whichType(v)
+	if uType >= vectorType || vType >= vectorType {
+		Errorf("internal error: non-scalar type %T in orderedCompare", u)
+	}
+	// We know we have scalars.
+	if uType != vType {
+		// If either is a Char, that orders below all others.
+		if uType == charType {
+			return -1
+		}
+		if vType == charType {
+			return 1
+		}
+		// Need to do it the hard way.
+		// If either is a Complex, that orders above all others,
+		// unless it is on the real line.
+		if uC, ok := u.(Complex); ok && uC.isReal() {
+			return OrderedCompare(c, uC.real, v)
+		}
+		if vC, ok := v.(Complex); ok && vC.isReal() {
+			return OrderedCompare(c, u, vC.real)
+		}
+		// If either is still a Complex, that orders above all others.
+		if uType == complexType {
+			return 1
+		}
+		if vType == complexType {
+			return -1
+		}
+		return sgn2(c, u, v)
+	}
+	switch uType {
+	case intType:
+		return sgn2Int(int(u.(Int)), int(v.(Int)))
+	case charType:
+		return sgn2Int(int(u.(Char)), int(v.(Char)))
+	case bigIntType:
+		return u.(BigInt).Cmp(v.(BigInt).Int)
+	case bigRatType:
+		return u.(BigRat).Cmp(v.(BigRat).Rat)
+	case bigFloatType:
+		return u.(BigFloat).Cmp(v.(BigFloat).Float)
+	case complexType:
+		// We can choose an ordering for Complex, even if math can't.
+		// Order by the real part, then the imaginary part.
+		uu, vv := u.(Complex), v.(Complex)
+		s := OrderedCompare(c, uu.real, vv.real)
+		if s != 0 {
+			return s
+		}
+		return OrderedCompare(c, uu.imag, vv.imag)
+
+	}
+	Errorf("internal error: unknown type %T in orderedCompare", u)
+	return -1
+
+}
+
+// sgn2 returns the signum of a-b.
+func sgn2(c Context, a, b Value) int {
+	if c.EvalBinary(a, "<", b) == one {
+		return -1
+	}
+	if c.EvalBinary(a, "==", b) == one {
+		return 0
+	}
+	return 1
+}
+
+// sgn2Int returns the signum of a-b.
+func sgn2Int(a, b int) int {
+	if a < b {
+		return -1
+	}
+	if a == b {
+		return 0
+	}
+	return 1
+}
diff --git a/value/sin.go b/value/sin.go
index 11d4ed2..b40ce30 100644
--- a/value/sin.go
+++ b/value/sin.go
@@ -166,7 +166,7 @@ func complexSin(c Context, v Complex) Value {
 	sinhY := floatSinh(c, y)
 	lhs := sinX.Mul(sinX, coshY)
 	rhs := cosX.Mul(cosX, sinhY)
-	return newComplex(BigFloat{lhs}, BigFloat{rhs}).shrink()
+	return NewComplex(BigFloat{lhs}, BigFloat{rhs}).shrink()
 }
 
 func complexCos(c Context, v Complex) Value {
@@ -179,7 +179,7 @@ func complexCos(c Context, v Complex) Value {
 	sinhY := floatSinh(c, y)
 	lhs := cosX.Mul(cosX, coshY)
 	rhs := sinX.Mul(sinX, sinhY)
-	return newComplex(BigFloat{lhs}, BigFloat{rhs.Neg(rhs)}).shrink()
+	return NewComplex(BigFloat{lhs}, BigFloat{rhs.Neg(rhs)}).shrink()
 }
 
 func complexTan(c Context, v Complex) Value {
@@ -197,5 +197,5 @@ func complexTan(c Context, v Complex) Value {
 	if den.Sign() == 0 {
 		Errorf("tangent is infinite")
 	}
-	return newComplex(BigFloat{sin2X.Quo(sin2X, den)}, BigFloat{sinh2Y.Quo(sinh2Y, den)}).shrink()
+	return NewComplex(BigFloat{sin2X.Quo(sin2X, den)}, BigFloat{sinh2Y.Quo(sinh2Y, den)}).shrink()
 }
diff --git a/value/sinh.go b/value/sinh.go
index 0330e31..e006585 100644
--- a/value/sinh.go
+++ b/value/sinh.go
@@ -123,7 +123,7 @@ func complexSinh(c Context, v Complex) Value {
 	sinY := floatSin(c, y)
 	lhs := sinhX.Mul(sinhX, cosY)
 	rhs := coshX.Mul(coshX, sinY)
-	return newComplex(BigFloat{lhs}, BigFloat{rhs}).shrink()
+	return NewComplex(BigFloat{lhs}, BigFloat{rhs}).shrink()
 }
 
 func complexCosh(c Context, v Complex) Value {
@@ -137,7 +137,7 @@ func complexCosh(c Context, v Complex) Value {
 	sinY := floatSin(c, y)
 	lhs := coshX.Mul(coshX, cosY)
 	rhs := sinhX.Mul(sinhX, sinY)
-	return newComplex(BigFloat{lhs}, BigFloat{rhs}).shrink()
+	return NewComplex(BigFloat{lhs}, BigFloat{rhs}).shrink()
 }
 
 func complexTanh(c Context, v Complex) Value {
@@ -156,5 +156,5 @@ func complexTanh(c Context, v Complex) Value {
 	if den.Sign() == 0 {
 		Errorf("tangent is infinite")
 	}
-	return newComplex(BigFloat{sinh2X.Quo(sinh2X, den)}, BigFloat{sin2Y.Quo(sin2Y, den)}).shrink()
+	return NewComplex(BigFloat{sinh2X.Quo(sinh2X, den)}, BigFloat{sin2Y.Quo(sin2Y, den)}).shrink()
 }
diff --git a/value/sqrt.go b/value/sqrt.go
index a5e5bb1..487ad4d 100644
--- a/value/sqrt.go
+++ b/value/sqrt.go
@@ -16,7 +16,7 @@ func sqrt(c Context, v Value) Value {
 		v = u.real
 	}
 	if isNegative(v) {
-		return newComplex(Int(0), evalFloatFunc(c, c.EvalUnary("-", v), floatSqrt))
+		return NewComplex(Int(0), evalFloatFunc(c, c.EvalUnary("-", v), floatSqrt))
 	}
 	return evalFloatFunc(c, v, floatSqrt)
 }
@@ -40,7 +40,7 @@ func complexSqrt(c Context, v Complex) Complex {
 		i.Neg(i)
 	}
 	// As with normal square roots, we only return the positive root.
-	return newComplex(BigFloat{r}.shrink(), BigFloat{i}.shrink())
+	return NewComplex(BigFloat{r}.shrink(), BigFloat{i}.shrink())
 }
 
 func evalFloatFunc(c Context, v Value, fn func(Context, *big.Float) *big.Float) Value {
diff --git a/value/unary.go b/value/unary.go
index 121135a..95fee05 100644
--- a/value/unary.go
+++ b/value/unary.go
@@ -160,21 +160,21 @@ func init() {
 			elementwise: true,
 			fn: [numType]unaryFn{
 				intType: func(c Context, v Value) Value {
-					return newComplex(Int(0), v)
+					return NewComplex(Int(0), v)
 				},
 				bigIntType: func(c Context, v Value) Value {
-					return newComplex(Int(0), v)
+					return NewComplex(Int(0), v)
 				},
 				bigRatType: func(c Context, v Value) Value {
-					return newComplex(Int(0), v)
+					return NewComplex(Int(0), v)
 				},
 				bigFloatType: func(c Context, v Value) Value {
-					return newComplex(Int(0), v)
+					return NewComplex(Int(0), v)
 				},
 				complexType: func(c Context, v Value) Value {
 					// Multiply by i.
 					u := v.(Complex)
-					return newComplex(c.EvalUnary("-", u.imag), u.real)
+					return NewComplex(c.EvalUnary("-", u.imag), u.real)
 				},
 			},
 		},
@@ -923,10 +923,24 @@ func init() {
 				bigFloatType: floatValueSelf,
 				complexType: func(c Context, v Value) Value {
 					u := v.(Complex)
-					return newComplex(floatValueSelf(c, u.real), floatValueSelf(c, u.imag))
+					return NewComplex(floatValueSelf(c, u.real), floatValueSelf(c, u.imag))
 				},
 			},
 		},
+
+		{
+			name:        "unique",
+			elementwise: false,
+			fn: [numType]unaryFn{
+				intType:      unique,
+				charType:     unique,
+				bigIntType:   unique,
+				bigRatType:   unique,
+				bigFloatType: unique,
+				complexType:  unique,
+				vectorType:   unique,
+			},
+		},
 	}
 
 	for _, op := range ops {
diff --git a/value/value.go b/value/value.go
index 9e7b92f..3eff9b7 100644
--- a/value/value.go
+++ b/value/value.go
@@ -91,7 +91,7 @@ func Parse(conf *config.Config, s string) (Value, error) {
 	switch sep {
 	case "j":
 		// A complex.
-		return newComplex(v1, v2), nil
+		return NewComplex(v1, v2), nil
 	case "/":
 		// A rational. It's tricky.
 		// Common simple case.
diff --git a/value/vector.go b/value/vector.go
index 49b4f7b..24b7da3 100644
--- a/value/vector.go
+++ b/value/vector.go
@@ -178,7 +178,7 @@ func (v Vector) sortedCopy(c Context) Vector {
 	sortedV := make([]Value, len(v))
 	copy(sortedV, v)
 	sort.Slice(sortedV, func(i, j int) bool {
-		return c.EvalBinary(sortedV[i], "<", sortedV[j]) == Int(1)
+		return OrderedCompare(c, sortedV[i], sortedV[j]) < 0
 	})
 	return sortedV
 }
@@ -187,7 +187,7 @@ func (v Vector) sortedCopy(c Context) Vector {
 // sorted order.
 func (v Vector) contains(c Context, x Value) bool {
 	pos := sort.Search(len(v), func(j int) bool {
-		return c.EvalBinary(v[j], ">=", x) == Int(1)
+		return OrderedCompare(c, v[j], x) >= 0
 	})
 	return pos < len(v) && c.EvalBinary(v[pos], "==", x) == Int(1)
 }