2Module : IRTS.JavaScript.Codegen
3Description : The JavaScript common code generator.
6Maintainer : The Idris Community.
8{-# LANGUAGE OverloadedStrings #-}
10module IRTS.JavaScript.Codegen( codegenJs
16import IRTS.CodegenCommon
17import IRTS.JavaScript.AST
18import IRTS.JavaScript.LangTransforms
19import IRTS.JavaScript.Name
20import IRTS.JavaScript.PrimOp
21import IRTS.JavaScript.Specialize
27import Control.Monad.Trans.State
29import Data.Map.Strict (Map)
30import qualified Data.Map.Strict as Map
31import Data.Maybe (fromJust)
33import qualified Data.Set as Set
34import Data.Text (Text)
35import qualified Data.Text as T
36import qualified Data.Text.IO as TIO
37import System.Directory (doesFileExist)
38import System.Environment
41import Control.Applicative (pure, (<$>))
44import Data.Generics.Uniplate.Data
46import GHC.Generics (Generic)
48data CGStats = CGStats { usedBigInt :: Bool
49 , partialApplications :: Set Partial
50 , hiddenClasses :: Set HiddenClass
54emptyStats = CGStats { partialApplications = Set.empty
55 , hiddenClasses = Set.empty
59data CGConf = CGConf { header :: Text
62 , extraRunTime :: String
66getInclude :: FilePath -> IO Text
69 libs <- getIdrisLibDir
70 let libPath = libs </> p
71 exitsInLib <- doesFileExist libPath
76getIncludes :: [FilePath] -> IO Text
78 incs <- mapM getInclude l
79 return $ T.intercalate "\n\n" incs
81includeLibs :: [String] -> String
88 concatMap (\lib -> "var " ++ (repl <$> lib) ++ " = require(\"" ++ lib ++"\");\n")
90isYes :: Maybe String -> Bool
91isYes (Just "Y") = True
92isYes (Just "y") = True
96codegenJs :: CGConf -> CodeGenerator
99 debug <- isYes <$> lookupEnv "IDRISJS_DEBUG"
100 let defs = Map.fromList $ liftDecls ci
101 let used = Map.elems $ removeDeadCode defs [sMN 0 "runMain"]
104 writeFile (outputFile ci ++ ".LDeclsDebug") $ (unlines $ intersperse "" $ map show used) ++ "\n\n\n"
105 putStrLn $ "Finished calculating used"
108 let (out, stats) = doCodegen conf defs used
110 path <- getIdrisJSRTSDir
111 jsbn <- if usedBigInt stats
112 then TIO.readFile $ path </> jsbnPath conf
115 runtimeCommon <- TIO.readFile $ path </> "Runtime-common.js"
116 extraRT <- TIO.readFile $ path </> (extraRunTime conf)
118 includes <- getIncludes $ includes ci
119 let libs = T.pack $ includeLibs $ compileLibs ci
120 TIO.writeFile (outputFile ci) $ T.concat [ header conf
121 , "\"use strict\";\n\n"
124 , runtimeCommon, "\n"
127 -- external libraries
131 , doPartials (partialApplications stats), "\n"
132 , doHiddenClasses (hiddenClasses stats), "\n"
134 , jsName (sMN 0 "runMain"), "();\n"
139doPartials :: Set Partial -> Text
141 T.intercalate "\n" (map f $ Set.toList x)
143 f p@(Partial n i j) =
144 let vars1 = map (T.pack . ("x"++) . show) [1..i]
145 vars2 = map (T.pack . ("x"++) . show) [(i+1)..j]
147 JsFun (jsNamePartial p) vars1 $ JsReturn $
148 jsCurryLam vars2 (jsAppN (jsName n) (map JsVar (vars1 ++ vars2)) )
150doHiddenClasses :: Set HiddenClass -> Text
152 T.intercalate "\n" (map f $ Set.toList x)
154 f p@(HiddenClass n id 0) = jsStmt2Text $ JsDecConst (jsNameHiddenClass p) $ JsObj [("type", JsInt id)]
155 f p@(HiddenClass n id arity) =
156 let vars = map dataPartName $ take arity [1..]
158 JsFun (jsNameHiddenClass p) vars $ JsSeq (JsSet (JsProp JsThis "type") (JsInt id)) $ seqJs
159 $ map (\tv -> JsSet (JsProp JsThis tv) (JsVar tv)) vars
161doCodegen :: CGConf -> Map Name LDecl -> [LDecl] -> (Text, CGStats)
162doCodegen conf defs decls =
163 let xs = map (doCodegenDecl conf defs) decls
164 groupCGStats x y = CGStats { partialApplications = partialApplications x `Set.union` partialApplications y
165 , hiddenClasses = hiddenClasses x `Set.union` hiddenClasses y
166 , usedBigInt = usedBigInt x || usedBigInt y
168 in (T.intercalate "\n" $ map fst xs, foldl' groupCGStats emptyStats (map snd xs) )
170doCodegenDecl :: CGConf -> Map Name LDecl -> LDecl -> (Text, CGStats)
171doCodegenDecl conf defs (LFun _ n args def) =
172 let (ast, stats) = cgFun conf defs n args def
173 in (T.concat [jsStmt2Text (JsComment $ T.pack $ show n), "\n", jsStmt2Text ast], stats)
174doCodegenDecl conf defs (LConstructor n i sz) = ("", emptyStats)
176seqJs :: [JsStmt] -> JsStmt
178seqJs (x:xs) = JsSeq x (seqJs xs)
181data CGBodyState = CGBodyState { defs :: Map Name LDecl
183 , reWrittenNames :: Map.Map Name JsExpr
184 , currentFnNameAndArgs :: (Text, [Text])
185 , usedArgsTailCallOptim :: Set (Text, Text)
189 , partialApps :: Set Partial
190 , hiddenCls :: Set HiddenClass
193getNewCGName :: State CGBodyState Text
197 let v = lastIntName st + 1
198 put $ st {lastIntName = v}
199 return $ jsNameGenerated v
201addPartial :: Partial -> State CGBodyState ()
203 modify (\s -> s {partialApps = Set.insert p (partialApps s) })
205addHiddenClass :: HiddenClass -> State CGBodyState ()
207 modify (\s -> s {hiddenCls = Set.insert p (hiddenCls s) })
209addUsedArgsTailCallOptim :: Set (Text, Text) -> State CGBodyState ()
210addUsedArgsTailCallOptim p =
211 modify (\s -> s {usedArgsTailCallOptim = Set.union p (usedArgsTailCallOptim s) })
213getNewCGNames :: Int -> State CGBodyState [Text]
215 mapM (\_ -> getNewCGName) [1..n]
217getConsId :: Name -> State CGBodyState (Int, Int)
221 case Map.lookup n (defs st) of
222 Just (LConstructor _ conId arity) -> pure (conId, arity)
223 _ -> error $ "Internal JS Backend error " ++ showCG n ++ " is not a constructor."
225getArgList :: Name -> State CGBodyState (Maybe [Name])
229 case Map.lookup n (defs st) of
230 Just (LFun _ _ a _) -> pure $ Just a
233data BodyResTarget = ReturnBT
239cgFun :: CGConf -> Map Name LDecl -> Name -> [Name] -> LExp -> (JsStmt, CGStats)
240cgFun cnf dfs n args def = do
241 let fnName = jsName n
242 let argNames = map jsName args
243 let ((decs, res),st) = runState
244 (cgBody ReturnBT def)
245 (CGBodyState { defs = dfs
247 , reWrittenNames = Map.empty
248 , currentFnNameAndArgs = (fnName, argNames)
249 , usedArgsTailCallOptim = Set.empty
253 , partialApps = Set.empty
254 , hiddenCls = Set.empty
257 let body = if isTailRec st then JsSeq (declareUsedOptimArgs $ usedArgsTailCallOptim st) (JsForever ((seqJs decs) `JsSeq` res)) else (seqJs decs) `JsSeq` res
258 let fn = JsFun fnName argNames body
259 let state' = CGStats { partialApplications = partialApps st
260 , hiddenClasses = hiddenCls st
261 , usedBigInt = usedITBig st
265getSwitchJs :: JsExpr -> [LAlt] -> JsExpr
267 if any conCase alts then JsArrayProj (JsInt 0) x
268 else if any constBigIntCase alts then JsForeign "%0.toString()" [x]
270 where conCase (LConCase _ _ _ _) = True
272 constBigIntCase (LConstCase (BI _) _) = True
273 constBigIntCase _ = False
275addRT :: BodyResTarget -> JsExpr -> JsStmt
276addRT ReturnBT x = JsReturn x
277addRT (DecBT n) x = JsDecLet n x
278addRT (DecConstBT n) x = JsDecConst n x
279addRT (SetBT n) x = JsSet (JsVar n) x
280addRT GetExpBT x = JsExprStmt x
282declareUsedOptimArgs :: Set (Text, Text) -> JsStmt
283declareUsedOptimArgs x = seqJs $ map (\(x,y) -> JsDecLet x (JsVar y) ) (Set.toList x)
285tailCallOptimRefreshArgs :: [(Text, JsExpr)] -> Set Text -> ((JsStmt, JsStmt), Set (Text, Text))
286tailCallOptimRefreshArgs [] s = ((JsEmpty, JsEmpty), Set.empty)
287tailCallOptimRefreshArgs ((n,x):r) s =
288 let ((y1,y2), y3) = tailCallOptimRefreshArgs r (Set.insert n s) --
289 in if Set.null $ (Set.fromList [ z | z <- universeBi x ]) `Set.intersection` s then
290 ((y1, jsSetVar n x `JsSeq` y2), y3)
292 let n' = jsTailCallOptimName n
293 in ((jsSetVar n' x `JsSeq` y1, jsSetVar n (JsVar n') `JsSeq` y2), Set.insert (n',n) y3)
295cgName :: Name -> State CGBodyState JsExpr
298 case Map.lookup b (reWrittenNames st) of
300 _ -> pure $ JsVar $ jsName b
302cgBody :: BodyResTarget -> LExp -> State CGBodyState ([JsStmt], JsStmt)
305 (LCase _ (LOp oper [x, y]) [LConstCase (I 0) (LCon _ _ ff []), LDefaultCase (LCon _ _ tt [])])
306 | (ff == qualifyN "Prelude.Bool" "False" &&
307 tt == qualifyN "Prelude.Bool" "True") ->
308 case (Map.lookup oper primDB) of
309 Just (needBI, pti, c) | pti == PTBool -> do
310 z <- mapM (cgBody GetExpBT) [x, y]
311 when needBI setUsedITBig
312 let res = jsPrimCoerce pti PTBool $ c $ map (jsStmt2Expr . snd) z
313 pure $ (concat $ map fst z, addRT rt res)
315 (LCase _ e [LConCase _ n _ (LCon _ _ tt []), LDefaultCase (LCon _ _ ff [])])
316 | (ff == qualifyN "Prelude.Bool" "False" &&
317 tt == qualifyN "Prelude.Bool" "True") -> do
318 (d, v) <- cgBody GetExpBT e
319 test <- formConTest n (jsStmt2Expr v)
320 pure $ (d, addRT rt $ JsUniOp (T.pack "!") $ JsUniOp (T.pack "!") test)
321 (LCase _ e [LConCase _ n _ (LCon _ _ tt []), LConCase _ _ _ (LCon _ _ ff [])])
322 | (ff == qualifyN "Prelude.Bool" "False" &&
323 tt == qualifyN "Prelude.Bool" "True") -> do
324 (d, v) <- cgBody GetExpBT e
325 test <- formConTest n (jsStmt2Expr v)
326 pure $ (d, addRT rt $ JsUniOp (T.pack "!") $ JsUniOp (T.pack "!") test)
327 (LCase _ e [LConCase _ n _ (LCon _ _ ff []), LDefaultCase (LCon _ _ tt [])])
328 | (ff == qualifyN "Prelude.Bool" "False" &&
329 tt == qualifyN "Prelude.Bool" "True") -> do
330 (d, v) <- cgBody GetExpBT e
331 test <- formConTest n (jsStmt2Expr v)
332 pure $ (d, addRT rt $ JsUniOp (T.pack "!") test)
333 (LCase _ e [LConCase _ n _ (LCon _ _ ff []), LConCase _ _ _ (LCon _ _ tt [])])
334 | (ff == qualifyN "Prelude.Bool" "False" &&
335 tt == qualifyN "Prelude.Bool" "True") -> do
336 (d, v) <- cgBody GetExpBT e
337 test <- formConTest n (jsStmt2Expr v)
338 pure $ (d, addRT rt $ JsUniOp (T.pack "!") test)
339 (LCase f e [LConCase nf ff [] alt, LConCase nt tt [] conseq])
340 | (ff == qualifyN "Prelude.Bool" "False" &&
341 tt == qualifyN "Prelude.Bool" "True") ->
342 cgBody' rt $ LCase f e [LConCase nt tt [] conseq, LConCase nf ff [] alt]
343 expr -> cgBody' rt expr
345cgBody' :: BodyResTarget -> LExp -> State CGBodyState ([JsStmt], JsStmt)
346cgBody' rt (LV (Glob n)) =
348 argsFn <- getArgList n
350 Just a -> cgBody' rt (LApp False (LV (Glob n)) [])
353 pure $ ([], addRT rt n')
354cgBody' rt (LApp tailcall (LV (Glob fn)) args) =
356 let fname = jsName fn
358 let (currFn, argN) = currentFnNameAndArgs st
359 z <- mapM (cgBody GetExpBT) args
360 let argVals = map (jsStmt2Expr . snd) z
361 let preDecs = concat $ map fst z
362 case (fname == currFn && (length args) == (length argN), rt) of
365 modify (\x-> x {isTailRec = True})
366 let ((y1,y2), y3) = tailCallOptimRefreshArgs (zip argN argVals) Set.empty
367 addUsedArgsTailCallOptim y3
368 pure (preDecs, y1 `JsSeq` y2)
370 app <- formApp fn argVals
371 pure (preDecs, addRT rt app)
373cgBody' rt (LForce (LLazyApp n args)) = cgBody rt (LApp False (LV (Glob n)) args)
374cgBody' rt (LLazyApp n args) =
376 (d,v) <- cgBody ReturnBT (LApp False (LV (Glob n)) args)
377 pure ([], addRT rt $ jsLazy $ jsStmt2Expr $ JsSeq (seqJs d) v)
378cgBody' rt (LForce e) =
380 (d,v) <- cgBody GetExpBT e
381 pure (d, addRT rt $ JsForce $ jsStmt2Expr v)
382cgBody' rt (LLet n v sc) =
384 (d1, v1) <- cgBody (DecConstBT $ jsName n) v
385 (d2, v2) <- cgBody rt sc
386 pure $ ((d1 ++ v1 : d2), v2)
387cgBody' rt (LProj e i) =
389 (d, v) <- cgBody GetExpBT e
390 pure $ (d, addRT rt $ JsArrayProj (JsInt $ i+1) $ jsStmt2Expr v)
391cgBody' rt (LCon _ conId n args) =
393 z <- mapM (cgBody GetExpBT) args
394 con <- formCon n (map (jsStmt2Expr . snd) z)
395 pure $ (concat $ map fst z, addRT rt con)
396cgBody' rt (LCase _ e alts) = do
397 (d, v) <- cgBody GetExpBT e
398 resName <- getNewCGName
400 case (all altHasNoProj alts && length alts <= 2, v) of
401 (True, _) -> pure (JsEmpty, jsStmt2Expr v)
402 (False, JsExprStmt (JsVar n)) -> pure (JsEmpty, jsStmt2Expr v)
404 swName <- getNewCGName
405 pure (JsDecConst swName $ jsStmt2Expr v, JsVar swName)
406 sw' <- cgIfTree rt resName entry alts
410 Nothing -> JsExprStmt JsNull
412 ReturnBT -> pure (d ++ [decSw], sw)
413 (DecBT nvar) -> pure (d ++ [decSw, JsDecLet nvar JsNull], sw)
414 (DecConstBT nvar) -> pure (d ++ [decSw, JsDecLet nvar JsNull], sw)
415 (SetBT nvar) -> pure (d ++ [decSw], sw)
418 (d ++ [decSw, JsDecLet resName JsNull, sw], JsExprStmt $ JsVar resName)
419cgBody' rt (LConst c) =
422 pure ([], (addRT rt) $ cst)
423cgBody' rt (LOp op args) =
425 z <- mapM (cgBody GetExpBT) args
426 res <- cgOp op (map (jsStmt2Expr . snd) z)
427 pure $ (concat $ map fst z, addRT rt $ res)
428cgBody' rt LNothing = pure ([], addRT rt JsNull)
429cgBody' rt (LError x) = pure ([], JsError $ JsStr x)
430cgBody' rt x@(LForeign dres (FStr code) args ) =
432 z <- mapM (cgBody GetExpBT) (map snd args)
433 jsArgs <- sequence $ map cgForeignArg (zip (map fst args) (map (jsStmt2Expr . snd) z))
434 jsDres <- cgForeignRes dres $ JsForeign (T.pack code) jsArgs
435 pure $ (concat $ map fst z, addRT rt $ jsDres)
436cgBody' _ x = error $ "Instruction " ++ show x ++ " not compilable yet"
438altsRT :: Text -> BodyResTarget -> BodyResTarget
439altsRT rn ReturnBT = ReturnBT
440altsRT rn (DecBT n) = SetBT n
441altsRT rn (SetBT n) = SetBT n
442altsRT rn (DecConstBT n) = SetBT n
443altsRT rn GetExpBT = SetBT rn
445altHasNoProj :: LAlt -> Bool
446altHasNoProj (LConCase _ _ args _) = args == []
449formApp :: Name -> [JsExpr] -> State CGBodyState JsExpr
450formApp fn argVals = case specialCall fn of
451 Just (arity, g) | arity == length argVals -> pure $ g argVals
453 argsFn <- getArgList fn
456 Nothing -> pure $ jsCurryApp fname argVals
458 let lenAgFn = length agFn
459 let lenArgs = length argVals
460 case compare lenAgFn lenArgs of
461 EQ -> pure $ JsApp fname argVals
462 LT -> pure $ jsCurryApp (JsApp fname (take lenAgFn argVals)) (drop lenAgFn argVals)
464 let part = Partial fn lenArgs lenAgFn
466 pure $ jsAppN (jsNamePartial part) argVals
468formCon :: Name -> [JsExpr] -> State CGBodyState JsExpr
470 case specialCased n of
471 Just (ctor, test, match) -> pure $ ctor args
473 (conId, arity) <- getConsId n
474 let hc = HiddenClass n conId arity
476 pure $ if (arity > 0)
477 then JsNew (JsVar $ jsNameHiddenClass hc) args
478 else JsVar $ jsNameHiddenClass hc
480formConTest :: Name -> JsExpr -> State CGBodyState JsExpr
482 case specialCased n of
483 Just (ctor, test, match) -> pure $ test x
485 (conId, arity) <- getConsId n
486 pure $ JsBinOp "===" (JsProp x (T.pack "type")) (JsInt conId)
488 -- then pure $ JsBinOp "===" (JsProp x (T.pack "type")) (JsInt conId)
489 -- else pure $ JsBinOp "===" x (JsInt conId)
491formProj :: Name -> JsExpr -> Int -> JsExpr
493 case specialCased n of
494 Just (ctor, test, proj) -> proj v i
495 Nothing -> JsProp v (dataPartName i)
497smartif :: JsExpr -> JsStmt -> Maybe JsStmt -> JsStmt
498smartif cond conseq (Just alt) = JsIf cond conseq (Just alt)
499smartif cond conseq Nothing = conseq
501formConstTest :: JsExpr -> Const -> State CGBodyState JsExpr
502formConstTest scrvar t = case t of
505 cgOp' PTBool (LEq (ATInt ITBig)) [scrvar, t']
508 pure $ JsBinOp "===" scrvar t'
510cgIfTree :: BodyResTarget
514 -> State CGBodyState (Maybe JsStmt)
515cgIfTree _ _ _ [] = pure Nothing
516cgIfTree rt resName scrvar ((LConstCase t exp):r) = do
517 (d, v) <- cgBody (altsRT resName rt) exp
518 alternatives <- cgIfTree rt resName scrvar r
519 test <- formConstTest scrvar t
521 smartif test (JsSeq (seqJs d) v) alternatives
522cgIfTree rt resName scrvar ((LDefaultCase exp):r) = do
523 (d, v) <- cgBody (altsRT resName rt) exp
524 pure $ Just $ JsSeq (seqJs d) v
525cgIfTree rt resName scrvar ((LConCase _ n args exp):r) = do
526 alternatives <- cgIfTree rt resName scrvar r
527 test <- formConTest n scrvar
529 let rwn = reWrittenNames st
534 (\m (n, j) -> Map.insert n (formProj n scrvar j) m)
538 (d, v) <- cgBody (altsRT resName rt) exp
540 put $ st1 {reWrittenNames = rwn}
541 let branchBody = JsSeq (seqJs d) v
542 pure $ Just $ smartif test branchBody alternatives
545cgForeignArg :: (FDesc, JsExpr) -> State CGBodyState JsExpr
546cgForeignArg (FApp (UN "JS_IntT") _, v) = pure v
547cgForeignArg (FCon (UN "JS_Str"), v) = pure v
548cgForeignArg (FCon (UN "JS_Ptr"), v) = pure v
549cgForeignArg (FCon (UN "JS_Unit"), v) = pure v
550cgForeignArg (FCon (UN "JS_Float"), v) = pure v
551cgForeignArg (FApp (UN "JS_FnT") [_,FApp (UN "JS_Fn") [_,_, a, FApp (UN "JS_FnBase") [_,b]]], f) =
553cgForeignArg (FApp (UN "JS_FnT") [_,FApp (UN "JS_Fn") [_,_, a, FApp (UN "JS_FnIO") [_,_, b]]], f) =
555 jsx <- cgForeignArg (a, JsVar "x")
556 jsres <- cgForeignRes b $ jsCurryApp (jsCurryApp f [jsx]) [JsNull]
557 pure $ JsLambda ["x"] $ JsReturn jsres
558cgForeignArg (desc, _) =
561 error $ "Foreign arg type " ++ show desc ++ " not supported. While generating function " ++ (show $ fst $ currentFnNameAndArgs st)
563cgForeignRes :: FDesc -> JsExpr -> State CGBodyState JsExpr
564cgForeignRes (FApp (UN "JS_IntT") _) x = pure x
565cgForeignRes (FCon (UN "JS_Unit")) x = pure x
566cgForeignRes (FCon (UN "JS_Str")) x = pure x
567cgForeignRes (FCon (UN "JS_Ptr")) x = pure x
568cgForeignRes (FCon (UN "JS_Float")) x = pure x
569cgForeignRes desc val =
572 error $ "Foreign return type " ++ show desc ++ " not supported. While generating function " ++ (show $ fst $ currentFnNameAndArgs st)
574setUsedITBig :: State CGBodyState ()
575setUsedITBig = modify (\s -> s {usedITBig = True})
578cgConst :: Const -> State CGBodyState JsExpr
579cgConst (I i) = pure $ JsInt i
583 pure $ JsForeign "new $JSRTS.jsbn.BigInteger(%0)" [JsStr $ show i]
584cgConst (Ch c) = pure $ JsStr [c]
585cgConst (Str s) = pure $ JsStr s
586cgConst (Fl f) = pure $ JsDouble f
587cgConst (B8 x) = pure $ JsForeign (T.pack $ show x ++ " & 0xFF") []
588cgConst (B16 x) = pure $ JsForeign (T.pack $ show x ++ " & 0xFFFF") []
589cgConst (B32 x) = pure $ JsForeign (T.pack $ show x ++ "|0" ) []
593 pure $ JsForeign "new $JSRTS.jsbn.BigInteger(%0).and(new $JSRTS.jsbn.BigInteger(%1))" [JsStr $ show x, JsStr $ show 0xFFFFFFFFFFFFFFFF]
594cgConst x | isTypeConst x = pure $ JsInt 0
595cgConst x = error $ "Constant " ++ show x ++ " not compilable yet"
597cgOp :: PrimFn -> [JsExpr] -> State CGBodyState JsExpr
600cgOp' :: JsPrimTy -> PrimFn -> [JsExpr] -> State CGBodyState JsExpr
601cgOp' pt (LExternal name) _ | name == sUN "prim__null" = pure JsNull
602cgOp' pt (LExternal name) [l,r] | name == sUN "prim__eqPtr" = pure $ JsBinOp "==" l r
603cgOp' pt op exps = case Map.lookup op primDB of
604 Just (useBigInt, pti, combinator) -> do
605 when useBigInt setUsedITBig
606 pure $ jsPrimCoerce pti pt $ combinator exps
607 Nothing -> error ("Operator " ++ show (op, exps) ++ " not implemented")
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list