From 5a694e3606ff616d9778128e72a3df6c058b360e Mon Sep 17 00:00:00 2001 From: Brent Yorgey Date: Fri, 8 Mar 2024 13:47:46 -0600 Subject: [PATCH] New `:table` command (#376) New `:table` REPL command which can print formatted tables for lists, tuples, or functions. --- disco.cabal | 13 +- src/Disco/Interactive/Commands.hs | 213 +++++++++++++++++++++++++----- src/Disco/Interpret/CESK.hs | 19 ++- src/Disco/Pretty.hs | 8 +- src/Disco/Util.hs | 4 + src/Disco/Value.hs | 21 +-- test/repl-help/expected | 1 + test/table-error/expected | 2 + test/table-error/input | 2 + test/table-function/expected | 133 +++++++++++++++++++ test/table-function/input | 10 ++ test/table-list/expected | 55 ++++++++ test/table-list/input | 13 ++ 13 files changed, 435 insertions(+), 59 deletions(-) create mode 100644 test/table-error/expected create mode 100644 test/table-error/input create mode 100644 test/table-function/expected create mode 100644 test/table-function/input create mode 100644 test/table-list/expected create mode 100644 test/table-list/input diff --git a/disco.cabal b/disco.cabal index 2d839571..0cca2154 100644 --- a/disco.cabal +++ b/disco.cabal @@ -317,6 +317,12 @@ extra-source-files: stack.yaml, repl/*.hs test/syntax-tuples/input test/syntax-types/expected test/syntax-types/input + test/table-error/expected + test/table-error/input + test/table-function/expected + test/table-function/input + test/table-list/expected + test/table-list/input test/types-192/expected test/types-192/input test/types-306/expected @@ -462,7 +468,7 @@ library filepath, directory, mtl >=2.2 && <2.4, - megaparsec >= 6.1.1 && < 9.6, + megaparsec >= 6.1.1 && < 9.7, parser-combinators >= 1.0.0 && < 1.4, prettyprinter >=1.7 && < 1.8, split >= 0.2 && < 0.3, @@ -488,7 +494,8 @@ library optparse-applicative >= 0.12 && < 0.19, -- oeis2 < 1.1, algebraic-graphs >= 0.5 && < 0.8, - pretty-show >= 1.10 && < 1.11 + pretty-show >= 1.10 && < 1.11, + boxes >= 0.1.5 && < 0.2 hs-source-dirs: src default-language: Haskell2010 @@ -505,7 +512,7 @@ executable disco haskeline >=0.8 && <0.9, mtl >=2.2 && <2.4, transformers >= 0.4 && < 0.7, - megaparsec >= 6.1.1 && < 9.6, + megaparsec >= 6.1.1 && < 9.7, containers >= 0.5 && < 0.7, unbound-generics >= 0.3 && < 0.4.3, lens >= 4.14 && < 5.3, diff --git a/src/Disco/Interactive/Commands.hs b/src/Disco/Interactive/Commands.hs index 68409000..d69ad95c 100644 --- a/src/Disco/Interactive/Commands.hs +++ b/src/Disco/Interactive/Commands.hs @@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE StandaloneDeriving #-} -- | @@ -29,38 +28,24 @@ import Control.Lens ( (^.), ) import Control.Monad.Except +import Data.Bifunctor (second) import Data.Char (isSpace) import Data.Coerce -import Data.List (find, isPrefixOf, sortBy) +import Data.List (find, isPrefixOf, sortBy, transpose) import Data.Map ((!)) import qualified Data.Map as M -import Data.Typeable -import System.FilePath (splitFileName) -import Prelude as P - -import Text.Megaparsec hiding (State, runParser) -import qualified Text.Megaparsec.Char as C -import Unbound.Generics.LocallyNameless ( - Name, - name2String, - string2Name, - ) - -import Disco.Effects.Input -import Disco.Effects.LFresh -import Disco.Effects.State -import Polysemy -import Polysemy.Error hiding (try) -import Polysemy.Output -import Polysemy.Reader - import Data.Maybe (mapMaybe, maybeToList) +import Data.Typeable import Disco.AST.Surface import Disco.AST.Typed import Disco.Compile import Disco.Context as Ctx import Disco.Desugar import Disco.Doc +import Disco.Effects.Input +import Disco.Effects.LFresh +import Disco.Effects.State +import Disco.Enumerate (enumerateType) import Disco.Error import Disco.Eval import Disco.Extensions @@ -89,8 +74,23 @@ import Disco.Syntax.Prims ( ) import Disco.Typecheck import Disco.Typecheck.Erase -import Disco.Types (toPolyType, pattern TyString) +import Disco.Types +import Disco.Util (maximum0) import Disco.Value +import Polysemy +import Polysemy.Error hiding (try) +import Polysemy.Output +import Polysemy.Reader +import System.FilePath (splitFileName) +import Text.Megaparsec hiding (State, runParser) +import qualified Text.Megaparsec.Char as C +import qualified Text.PrettyPrint.Boxes as B +import Unbound.Generics.LocallyNameless ( + Name, + name2String, + string2Name, + ) +import Prelude as P ------------------------------------------------------------ -- REPL expression type @@ -106,6 +106,7 @@ data REPLExpr :: CmdTag -> * where Parse :: Term -> REPLExpr 'CParse -- Show the parsed AST Pretty :: Term -> REPLExpr 'CPretty -- Pretty-print a term Print :: Term -> REPLExpr 'CPrint -- Print a string + Table :: Term -> REPLExpr 'CTable -- Print a table Ann :: Term -> REPLExpr 'CAnn -- Show type-annotated term Desugar :: Term -> REPLExpr 'CDesugar -- Show a desugared term Compile :: Term -> REPLExpr 'CCompile -- Show a compiled term @@ -150,6 +151,7 @@ data CmdTag | CParse | CPretty | CPrint + | CTable | CAnn | CDesugar | CCompile @@ -229,6 +231,7 @@ discoCommands = , SomeCmd parseCmd , SomeCmd prettyCmd , SomeCmd printCmd + , SomeCmd tableCmd , SomeCmd reloadCmd , SomeCmd showDefnCmd , SomeCmd typeCheckCmd @@ -538,7 +541,7 @@ handleEval :: REPLExpr 'CEval -> Sem r () handleEval (Eval m) = do - mi <- inputToState @TopInfo $ loadParsedDiscoModule False FromCwdOrStdlib REPLModule m + mi <- loadParsedDiscoModule False FromCwdOrStdlib REPLModule m addToREPLModule mi forM_ (mi ^. miTerms) (mapError EvalErr . evalTerm True . fst) @@ -589,7 +592,7 @@ handleHelp Help = sortedList cmds = sortBy (\(SomeCmd x) (SomeCmd y) -> compare (name x) (name y)) $ filteredCommands cmds showCmd c = text (padRight (helpcmd c) maxlen ++ " " ++ shortHelp c) - longestCmd cmds = maximum $ map (\(SomeCmd c) -> length $ helpcmd c) cmds + longestCmd cmds = maximum0 $ map (\(SomeCmd c) -> length $ helpcmd c) cmds padRight s maxsize = take maxsize (s ++ repeat ' ') -- don't show dev-only commands by default filteredCommands = P.filter (\(SomeCmd c) -> category c == User) @@ -781,6 +784,148 @@ handlePrint (Print t) = do v <- mapError EvalErr . evalTerm False $ at info $ text (vlist vchar v) +------------------------------------------------------------ +-- :table + +tableCmd :: REPLCommand 'CTable +tableCmd = + REPLCommand + { name = "table" + , helpcmd = ":table " + , shortHelp = "Print a formatted table for a list or function" + , category = User + , cmdtype = ColonCmd + , action = handleTable + , parser = Table <$> parseTermOrOp + } + +handleTable :: Members (Error DiscoError ': State TopInfo ': Output (Message ()) ': EvalEffects) r => REPLExpr 'CTable -> Sem r () +handleTable (Table t) = do + (at, ty) <- inputToState . typecheckTop $ inferTop t + v <- mapError EvalErr . evalTerm False $ at + + tydefs <- use @TopInfo (replModInfo . to allTydefs) + info $ runInputConst tydefs $ formatTableFor ty v >>= text + +-- | The max number of rows to show in the output of :table. +maxFunTableRows :: Int +maxFunTableRows = 25 + +-- | Uncurry a type, turning a type of the form A -> B -> ... -> Y -> +-- Z into the pair of types (A * B * ... * Y * Unit, Z). Note we do +-- not optimize away the Unit at the end of the chain, since this +-- needs to be an isomorphism. Otherwise we would not be able to +-- distinguish between e.g. Z and Unit -> Z. +uncurryTy :: Type -> (Type, Type) +uncurryTy (tyA :->: tyB) = (tyA :*: tyAs, tyRes) + where + (tyAs, tyRes) = uncurryTy tyB +uncurryTy ty = (TyUnit, ty) + +-- | Evaluate the application of a curried function to an uncurried +-- input. +evalCurried :: Members EvalEffects r => Type -> Value -> Type -> Value -> Sem r Value +evalCurried (_ :->: tyB) f (_ :*: tyY) v = do + let (v1, v2) = vpair id id v + f' <- evalApp f [v1] + evalCurried tyB f' tyY v2 +evalCurried _ v _ _ = return v + +formatTableFor :: + Members (LFresh ': Input TyDefCtx ': EvalEffects) r => + PolyType -> + Value -> + Sem r String +formatTableFor (Forall bnd) v = lunbind bnd $ \(vars, ty) -> + case vars of + [] -> case ty of + TyList ety -> do + byRows <- mapM (formatCols TopLevel ety) . vlist id $ v + return $ renderTable byRows + (_ :->: _) -> do + let (tyInputs, tyRes) = uncurryTy ty + vs = take (maxFunTableRows + 1) $ enumerateType tyInputs + (tyInputs', stripV) = stripFinalUnit tyInputs + results <- mapM (evalCurried ty v tyInputs) vs + byRows <- + mapM + (formatCols TopLevel (tyInputs' :*: tyRes)) + (zipWith (curry (pairv id id)) (take maxFunTableRows (map stripV vs)) results) + return $ renderTable (byRows ++ [[(B.left, "...")] | length vs == maxFunTableRows + 1]) + _otherTy -> do + tyStr <- prettyStr ty + return $ "Don't know how to make a table for type " ++ tyStr + _vars -> return "Can't make a table for a polymorphic type" + +-- | Strip the unit type from the end of a chain like (tA :*: (tB :*: (tC :*: Unit))), +-- which is an output of 'uncurryTy', and return a function to make the corresponding +-- change to a value of that type. +stripFinalUnit :: Type -> (Type, Value -> Value) +stripFinalUnit (tA :*: TyUnit) = (tA, fst . vpair id id) +stripFinalUnit (tA :*: tB) = (tA :*: tB', pairv id id . second v' . vpair id id) + where + (tB', v') = stripFinalUnit tB +stripFinalUnit ty = (ty, id) + +data Level = TopLevel | NestedPair | InnerLevel + deriving (Eq, Ord, Show) + +-- | Turn a value into a list of formatted columns in a type-directed +-- way. Lists and tuples are only split out into columns if they +-- occur at the top level; lists or tuples nested inside of other +-- data structures are simply pretty-printed. However, note we have +-- to make a special case for nested tuples: if a pair type occurs +-- at the top level we keep recursively splitting out its children +-- into columns as long as they are also pair types. +-- +-- Any value of a type other than a list or tuple is simply +-- pretty-printed. +formatCols :: + (Member LFresh r, Member (Input TyDefCtx) r) => + Level -> + Type -> + Value -> + Sem r [(B.Alignment, String)] +formatCols l (t1 :*: t2) (vpair id id -> (v1, v2)) + | l `elem` [TopLevel, NestedPair] = + (++) <$> formatCols NestedPair t1 v1 <*> formatCols NestedPair t2 v2 +-- Special case for String (= List Char), just print as string value +formatCols TopLevel TyString v = formatColDefault TyString v +-- For any other lists @ top level, print each element in a separate column +formatCols TopLevel (TyList ety) (vlist id -> vs) = + concat <$> mapM (formatCols InnerLevel ety) vs +formatCols _ ty v = formatColDefault ty v + +-- | Default formatting of a typed column value by simply +-- pretty-printing it, and using the alignment appropriate for its +-- type. +formatColDefault :: + (Member (Input TyDefCtx) r, Member LFresh r) => + Type -> + Value -> + Sem r [(B.Alignment, String)] +formatColDefault ty v = (: []) . (alignmentForType ty,) <$> renderDoc (prettyValue ty v) + +alignmentForType :: Type -> B.Alignment +alignmentForType ty | ty `elem` [TyN, TyZ, TyF, TyQ] = B.right +alignmentForType _ = B.left + +-- | Render a table, given as a list of rows, formatting it so that +-- each column is aligned. +renderTable :: [[(B.Alignment, String)]] -> String +renderTable = stripTrailingWS . B.render . B.hsep 2 B.top . map renderCol . transpose . pad + where + pad :: [[(B.Alignment, String)]] -> [[(B.Alignment, String)]] + pad rows = map (padTo (maximum0 . map length $ rows)) rows + padTo n = take n . (++ repeat (B.left, "")) + + renderCol :: [(B.Alignment, String)] -> B.Box + renderCol [] = B.nullBox + renderCol ((align, x) : xs) = B.vcat align . map B.text $ x : map snd xs + + stripTrailingWS = unlines . map stripEnd . lines + stripEnd = reverse . dropWhile isSpace . reverse + ------------------------------------------------------------ -- :reload @@ -837,7 +982,7 @@ handleShowDefn (ShowDefn x) = do let ds = map (pretty' . snd) xdefs ++ maybe [] (pure . pretty' . (name2s,)) mtydef case ds of [] -> text "No definition for" <+> pretty' x - _ -> vcat ds + _nonEmptyList -> vcat ds ------------------------------------------------------------ -- :test @@ -877,7 +1022,7 @@ typeCheckCmd = , category = Dev , cmdtype = ColonCmd , action = inputToState @TopInfo . handleTypeCheck - , parser = parseTypeCheck + , parser = TypeCheck <$> parseTermOrOp } handleTypeCheck :: @@ -888,17 +1033,17 @@ handleTypeCheck (TypeCheck t) = do (_, sig) <- typecheckTop $ inferTop t info $ pretty' t <+> text ":" <+> pretty' sig -parseTypeCheck :: Parser (REPLExpr 'CTypeCheck) -parseTypeCheck = - TypeCheck - <$> ( (try term "expression") - <|> (parseNakedOp "operator") - ) +------------------------------------------------------------ --- In a :type or :doc command, allow naked operators, as in :type + , +-- In :type, :doc, or :table commands, allow naked operators, as in :type + , -- even though + by itself is not a syntactically valid term. -- However, this seems like it may be a common thing for a student to -- ask and there is no reason we can't have this as a special case. +parseTermOrOp :: Parser Term +parseTermOrOp = + (try term "expression") + <|> (parseNakedOp "operator") + parseNakedOp :: Parser Term parseNakedOp = TPrim <$> parseNakedOpPrim diff --git a/src/Disco/Interpret/CESK.hs b/src/Disco/Interpret/CESK.hs index 9b165304..a08e7ea5 100644 --- a/src/Disco/Interpret/CESK.hs +++ b/src/Disco/Interpret/CESK.hs @@ -1,10 +1,6 @@ {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fmax-pmcheck-models=200 #-} ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ - -- | -- Module : Disco.Interpret.CESK -- Copyright : disco team and contributors @@ -18,6 +14,7 @@ module Disco.Interpret.CESK ( runCESK, step, eval, + evalApp, runTest, ) where @@ -275,7 +272,7 @@ appConst k = \case modOp m n | n == 0 = throw DivByZero | otherwise = return $ intv (numerator m `mod` numerator n) - ODivides -> numOp2' (\m n -> return (enumv $ divides m n)) >=> out + ODivides -> numOp2' (\m n -> return (boolv $ divides m n)) >=> out where divides 0 0 = True divides 0 _ = False @@ -284,7 +281,7 @@ appConst k = \case -------------------------------------------------- -- Number theory - OIsPrime -> intOp1 (enumv . isPrime) >=> out + OIsPrime -> intOp1 (boolv . isPrime) >=> out OFactor -> intOp1' primFactor >>> outWithErr where -- Semantics of the @$factor@ prim: turn a natural number into its @@ -341,8 +338,8 @@ appConst k = \case -------------------------------------------------- -- Comparison - OEq -> arity2 $ \v1 v2 -> out $ enumv (valEq v1 v2) - OLt -> arity2 $ \v1 v2 -> out $ enumv (valLt v1 v2) + OEq -> arity2 $ \v1 v2 -> out $ boolv (valEq v1 v2) + OLt -> arity2 $ \v1 v2 -> out $ boolv (valLt v1 v2) -------------------------------------------------- -- Container operations @@ -356,8 +353,8 @@ appConst k = \case cons n (x, k') (zs, m) = ((x, k') : zs, choose n k' * m) OBagElem -> arity2 $ \x -> withBag OBagElem $ - out . enumv . isJust . find (valEq x) . map fst - OListElem -> arity2 $ \x -> out . enumv . isJust . find (valEq x) . vlist id + out . boolv . isJust . find (valEq x) . map fst + OListElem -> arity2 $ \x -> out . boolv . isJust . find (valEq x) . vlist id OEachSet -> arity2 $ \f -> withBag OEachSet $ outWithErr . fmap (VBag . countValues) . mapM (evalApp f . (: []) . fst) @@ -727,7 +724,7 @@ graphSummary = toDiscoAdjMap . reifyGraph resultToBool :: Member (Error EvalError) r => TestResult -> Sem r Value resultToBool (TestResult _ (TestRuntimeError e) _) = throw e -resultToBool (TestResult b _ _) = return $ enumv b +resultToBool (TestResult b _ _) = return $ boolv b notProp :: ValProp -> ValProp notProp (VPDone r) = VPDone (invertPropResult r) diff --git a/src/Disco/Pretty.hs b/src/Disco/Pretty.hs index bcc47ed7..cdfd79d7 100644 --- a/src/Disco/Pretty.hs +++ b/src/Disco/Pretty.hs @@ -129,7 +129,13 @@ instance Pretty BOp where _ -> error $ "BOp " ++ show op ++ " not in bopMap!" -------------------------------------------------- --- Pretty-printing decimals +-- Pretty-printing rationals + decimals + +-- | Pretty-print a rational number as a fraction. +prettyRational :: Rational -> String +prettyRational r + | denominator r == 1 = show (numerator r) + | otherwise = show (numerator r) ++ "/" ++ show (denominator r) -- | Pretty-print a rational number using its decimal expansion, in -- the format @nnn.prefix[rep]...@, with any repeating digits enclosed diff --git a/src/Disco/Util.hs b/src/Disco/Util.hs index 97681a8a..bf259f0b 100644 --- a/src/Disco/Util.hs +++ b/src/Disco/Util.hs @@ -28,3 +28,7 @@ for = flip map m ! k = case M.lookup k m of Nothing -> error $ "key " ++ show k ++ " is not an element in the map" Just v -> v + +maximum0 :: (Num a, Ord a) => [a] -> a +maximum0 [] = 0 +maximum0 xs = maximum xs diff --git a/src/Disco/Value.hs b/src/Disco/Value.hs index 503f6955..83e65cc0 100644 --- a/src/Disco/Value.hs +++ b/src/Disco/Value.hs @@ -30,7 +30,8 @@ module Disco.Value ( vint, charv, vchar, - enumv, + boolv, + vbool, pairv, vpair, listv, @@ -245,10 +246,12 @@ vchar = chr . fromIntegral . vint charv :: Char -> Value charv = intv . fromIntegral . ord --- | Turn any instance of @Enum@ into a @Value@, by creating a --- constructor with an index corresponding to the enum value. -enumv :: Enum e => e -> Value -enumv e = VInj (toEnum $ fromEnum e) VUnit +boolv :: Bool -> Value +boolv e = VInj (toEnum $ fromEnum e) VUnit + +vbool :: Value -> Bool +vbool (VInj s VUnit) = toEnum $ fromEnum s +vbool v = error $ "vbool " ++ show v pairv :: (a -> Value) -> (b -> Value) -> (a, b) -> Value pairv av bv (a, b) = VPair (av a) (bv b) @@ -486,11 +489,9 @@ prettyValue (ty1 :+: _) (VInj L v) = "left" <> prettyVP ty1 v prettyValue (_ :+: ty2) (VInj R v) = "right" <> prettyVP ty2 v prettyValue (_ :+: _) v = error $ "Non-VInj passed with sum type to prettyValue: " ++ show v -prettyValue _ (VNum d r) - | denominator r == 1 = text $ show (numerator r) - | otherwise = text $ case d of - Fraction -> show (numerator r) ++ "/" ++ show (denominator r) - Decimal -> prettyDecimal r +prettyValue _ (VNum d r) = text $ case (d, denominator r == 1) of + (Decimal, False) -> prettyDecimal r + _ -> prettyRational r prettyValue ty@(_ :->: _) _ = prettyPlaceholder ty prettyValue (TySet ty) (VBag xs) = braces $ prettySequence ty "," (map fst xs) prettyValue (TySet _) v = diff --git a/test/repl-help/expected b/test/repl-help/expected index 52e40303..0cb577c1 100644 --- a/test/repl-help/expected +++ b/test/repl-help/expected @@ -8,6 +8,7 @@ Commands available from the prompt: :names Show all names in current scope :print Print a string without the double quotes, interpreting special characters :reload Reloads the most recently loaded file +:table Print a formatted table for a list or function :test Test a property using random examples 2 diff --git a/test/table-error/expected b/test/table-error/expected new file mode 100644 index 00000000..7f5339bf --- /dev/null +++ b/test/table-error/expected @@ -0,0 +1,2 @@ +Don't know how to make a table for type ℕ +Don't know how to make a table for type ℕ × ℕ diff --git a/test/table-error/input b/test/table-error/input new file mode 100644 index 00000000..1f0defac --- /dev/null +++ b/test/table-error/input @@ -0,0 +1,2 @@ +:table 1 +:table (1,2) diff --git a/test/table-function/expected b/test/table-function/expected new file mode 100644 index 00000000..32e1b665 --- /dev/null +++ b/test/table-function/expected @@ -0,0 +1,133 @@ +F F F +F T F +T F F +T T T + +F F F +F T T +T F T +T T T + +F T +T F + +F F F +F T F +T F F +T T T + +F F F +F T F +T F F +T T T + + 0 1 + 1 2 + 2 5 + 3 10 + 4 17 + 5 26 + 6 37 + 7 50 + 8 65 + 9 82 + 10 101 + 11 122 + 12 145 + 13 170 + 14 197 + 15 226 + 16 257 + 17 290 + 18 325 + 19 362 + 20 401 + 21 442 + 22 485 + 23 530 + 24 577 +... + + 0 -1 + 1 0 + -1 0 + 2 3 + -2 3 + 3 8 + -3 8 + 4 15 + -4 15 + 5 24 + -5 24 + 6 35 + -6 35 + 7 48 + -7 48 + 8 63 + -8 63 + 9 80 + -9 80 + 10 99 +-10 99 + 11 120 +-11 120 + 12 143 +-12 143 +... + + 1 3/2 +1/2 1 + 2 5/2 +1/3 5/6 +3/2 2 +2/3 7/6 + 3 7/2 +1/4 3/4 +4/3 11/6 +3/5 11/10 +5/2 3 +2/5 9/10 +5/3 13/6 +3/4 5/4 + 4 9/2 +1/5 7/10 +5/4 7/4 +4/7 15/14 +7/3 17/6 +3/8 7/8 +8/5 21/10 +5/7 17/14 +7/2 4 +2/7 11/14 +7/5 19/10 +... + +[] 0 +[0] 0 +[0, 0] 0 +[1] 1 +[0, 0, 0] 0 +[1, 0] 1 +[2] 2 +[0, 1] 1 +[1, 0, 0] 1 +[2, 0] 2 +[3] 3 +[0, 0, 0, 0] 0 +[1, 1] 2 +[2, 0, 0] 2 +[3, 0] 3 +[4] 4 +[0, 1, 0] 1 +[1, 0, 0, 0] 1 +[2, 1] 3 +[3, 0, 0] 3 +[4, 0] 4 +[5] 5 +[0, 2] 2 +[1, 1, 0] 2 +[2, 0, 0, 0] 2 +... + +■ ■ + diff --git a/test/table-function/input b/test/table-function/input new file mode 100644 index 00000000..b26aa1bc --- /dev/null +++ b/test/table-function/input @@ -0,0 +1,10 @@ +:table ~/\~ +:table ~\/~ +:table not +:table \x. \y. x /\ y +:table /\ +:table \x. x^2 + 1 +:table \x. x^2 - 1 +:table \x. x + 1/2 +:table \x. reduce(~+~, 0, x) +:table \x:Unit. unit diff --git a/test/table-list/expected b/test/table-list/expected new file mode 100644 index 00000000..0c7f330b --- /dev/null +++ b/test/table-list/expected @@ -0,0 +1,55 @@ +1 +2 +3 +4 +5 + + 1 + 10 + 100 + 30 +5046 + +1 2 3 +4 5 +6 7 8 9 + +"hi" +"hello" +"world" +"test" + + 10 "hi" +200 "hello" + 1 "test" + +1 2 +3 4 +5 6 + +1 2 3 +4 5 6 + +1 2 3 +4 5 6 + +1 2 3 4 +5 6 7 8 + +(1, 2) (3, 4) +(5, 6) (7, 8) + +[1, 2] [3, 4] +[5, 6] [7, 8] + +T T T +T F F +F T F +F F F + +'h' +'e' +'l' +'l' +'o' + diff --git a/test/table-list/input b/test/table-list/input new file mode 100644 index 00000000..fd057d65 --- /dev/null +++ b/test/table-list/input @@ -0,0 +1,13 @@ +:table [1,2,3,4,5] +:table [1, 10, 100, 30, 5046] +:table [[1,2,3], [4,5], [6,7,8,9]] +:table ["hi", "hello", "world", "test"] +:table [(10,"hi"), (200, "hello"), (1, "test")] +:table [(1,2), (3,4), (5,6)] +:table [((1,2),3), ((4,5),6)] +:table [(1,(2,3)), (4,(5,6))] +:table [((1,2),(3,4)), ((5,6),(7,8))] +:table [[(1,2), (3,4)], [(5,6), (7,8)]] +:table [([1,2], [3,4]), ([5,6], [7,8])] +:table [(p,q,p /\ q) | p in [T,F], q in [T,F]] +:table "hello"