├── .gitignore ├── CHANGELOG.md ├── README.md ├── app ├── AST.hs ├── Main.hs └── Render.hs └── randomart.cabal /.gitignore: -------------------------------------------------------------------------------- 1 | images/ 2 | dist-newstyle/ 3 | *.swp 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for randomart 2 | 3 | ## 0.1.0.0 -- YYYY-mm-dd 4 | 5 | * First version. Released on an unsuspecting world. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # randomart 2 | ![1](https://github.com/user-attachments/assets/a03a2cde-a74c-4dc7-86ad-0d1b4ff4e317) 3 | ![2](https://github.com/user-attachments/assets/053118e3-126a-48ee-95d5-ce9b003443b1) 4 | ![3](https://github.com/user-attachments/assets/9b9cfeee-49ad-465b-b860-9352360408a7) 5 | ![6](https://github.com/user-attachments/assets/fde688fa-8a94-4833-91ac-8eb64834c26e) 6 | 7 | 8 | Implementation of RandomArt based on the algorithm described in [Hash Visualization: a New Technique to improve Real-World Security](https://users.ece.cmu.edu/~adrian/projects/validation/validation.pdf) and Andrej Bauer's original program. 9 | 10 | To get started, make sure you have cabal installed. Cabal comes with GHCup, which you can download [here](https://www.haskell.org/ghcup/). Then clone and run: 11 | ``` 12 | git clone https://github.com/jamesma100/randomart.git 13 | cd randomart 14 | cabal run randomart -- [-d ] [-p ] [-o ] [-s ] 15 | ``` 16 | 17 | where 18 | - ``: depth of randomly generated AST used to render your image 19 | - ``: width/height of the image, e.g. 200 means 200x200 pixels. Currently only square images are supported. 20 | - ``: path of image to be saved, e.g. "my_img.png" 21 | - ``: most important part, any random string to initialize the random number generator 22 | 23 | For example: 24 | ``` 25 | cabal run randomart -- -d 30 -p 200 -o ./my_img.png -s its-2025-and-the-world-is-ending 26 | ``` 27 | Then open `./my_img.png`. You can alter the seed and depth to generate different images. 28 | -------------------------------------------------------------------------------- /app/AST.hs: -------------------------------------------------------------------------------- 1 | module AST where 2 | import Data.Fixed 3 | import Data.List 4 | import System.Random 5 | 6 | -- TODO: try using GADTs here to separate nodes by their type s.t. it doesn't rely on 7 | -- the user-provided grammar to enforce type safety 8 | data Node = 9 | NumberNode Double | 10 | BoolNode Bool | 11 | RandNode | 12 | SinNode Node | 13 | CosNode Node | 14 | TanNode Node | 15 | XNode | 16 | YNode | 17 | AddNode Node Node | 18 | MultNode Node Node | 19 | ModNode Node Node | 20 | ExpNode Double Node | 21 | TripleNode Node Node Node | 22 | GTNode Node Node | 23 | GTENode Node Node | 24 | LTNode Node Node | 25 | LTENode Node Node | 26 | IfNode Node Node Node | 27 | NormNode Node | 28 | NullNode | 29 | RuleNode [Node] 30 | deriving Show 31 | 32 | -- A rule is terminal if none of its children are rules 33 | isTerminal :: Node -> Bool 34 | isTerminal node = not (isRule node) && not (elem True [isRule child | child <- getRules node]) 35 | 36 | isRule :: Node -> Bool 37 | isRule node = case node of 38 | RuleNode _ -> True 39 | SinNode a -> isRule a 40 | CosNode a -> isRule a 41 | TanNode a -> isRule a 42 | AddNode a b -> isRule a || isRule b 43 | MultNode a b -> isRule a || isRule b 44 | ModNode a b -> isRule a || isRule b 45 | ExpNode _ b -> isRule b 46 | TripleNode a b c -> isRule a || isRule b || isRule c 47 | GTNode lhs rhs -> isRule lhs || isRule rhs 48 | GTENode lhs rhs -> isRule lhs || isRule rhs 49 | LTNode lhs rhs -> isRule lhs || isRule rhs 50 | LTENode lhs rhs -> isRule lhs || isRule rhs 51 | IfNode condExpr thenExpr elseExpr -> isRule condExpr || isRule thenExpr || isRule elseExpr 52 | otherwise -> False 53 | 54 | 55 | getArity :: Node -> Int 56 | getArity node = case node of 57 | RuleNode rules -> length rules 58 | otherwise -> 0 59 | 60 | -- get rules from a Node 61 | getRules :: Node -> [Node] 62 | getRules node = case node of 63 | RuleNode rules -> rules 64 | _ -> [node] 65 | 66 | -- maps value [0,1] to [0, index] 67 | randRange :: Int -> Double -> Int 68 | randRange maxVal randVal = round $ randVal * (fromIntegral maxVal) 69 | 70 | type Grammar = [Node] 71 | 72 | -- grammar G = [r1,..., rn] 73 | -- initial rule i 74 | -- depth d 75 | -- list of random numbers randNums with length >= d+1 76 | -- 77 | -- - Random number is selected based on depth 78 | -- 79 | treeGen :: Grammar -> Node -> Int -> StdGen -> (Node, StdGen) 80 | treeGen grammar initialRule depth stdGen 81 | | depth <= 0 && isTerminal ((getRules initialRule) !! 0) = 82 | case (getRules initialRule) !! 0 of 83 | RandNode -> (NumberNode (randNum * 2 - 1), newGen) -- resolve random node 84 | otherwise -> ((getRules initialRule) !! 0, stdGen) 85 | | otherwise = 86 | case () of 87 | () | isTerminal curNode -> case curNode of 88 | RandNode -> (NumberNode (randNum * 2 - 1), newGen) -- resolve random node 89 | otherwise -> (curNode, newGen) 90 | | otherwise -> case curNode of 91 | SinNode a -> (SinNode (fst branch1), snd branch1) 92 | where branch1 = treeGen grammar a (depth-1) newGen 93 | CosNode a -> (CosNode (fst branch1), snd branch1) 94 | where branch1 = treeGen grammar a (depth-1) newGen 95 | TanNode a -> (TanNode (fst branch1), snd branch1) 96 | where branch1 = treeGen grammar a (depth-1) newGen 97 | AddNode a b -> (AddNode (fst branch1) (fst branch2), snd branch2) 98 | where branch1 = treeGen grammar a (depth-1) newGen 99 | branch2 = treeGen grammar b (depth-1) gen1 100 | gen1 = (snd branch1) 101 | MultNode a b -> (MultNode (fst branch1) (fst branch2), snd branch2) 102 | where branch1 = treeGen grammar a (depth-1) newGen 103 | branch2 = treeGen grammar b (depth-1) gen1 104 | gen1 = (snd branch1) 105 | ModNode a b -> (ModNode (fst branch1) (fst branch2), snd branch2) 106 | where branch1 = treeGen grammar a (depth-1) newGen 107 | branch2 = treeGen grammar b (depth-1) gen1 108 | gen1 = (snd branch1) 109 | ExpNode base exp -> (ExpNode base (fst branch1), snd branch1) 110 | where branch1 = treeGen grammar exp (depth-1) newGen 111 | TripleNode a b c -> (TripleNode (fst branch1) (fst branch2) (fst branch3), snd branch3) 112 | where branch1 = treeGen grammar a (depth-1) newGen 113 | branch2 = treeGen grammar b (depth-1) gen1 114 | branch3 = treeGen grammar c (depth-1) gen2 115 | gen1 = (snd branch1) 116 | gen2 = (snd branch2) 117 | GTNode lhs rhs -> (GTNode (fst branch1) (fst branch2), snd branch2) 118 | where branch1 = treeGen grammar lhs (depth-1) newGen 119 | branch2 = treeGen grammar rhs (depth-1) gen1 120 | gen1 = (snd branch1) 121 | GTENode lhs rhs -> (GTENode (fst branch1) (fst branch2), snd branch2) 122 | where branch1 = treeGen grammar lhs (depth-1) newGen 123 | branch2 = treeGen grammar rhs (depth-1) gen1 124 | gen1 = (snd branch1) 125 | LTNode lhs rhs -> (LTNode (fst branch1) (fst branch2), snd branch2) 126 | where branch1 = treeGen grammar lhs (depth-1) newGen 127 | branch2 = treeGen grammar rhs (depth-1) gen1 128 | gen1 = (snd branch1) 129 | LTENode lhs rhs -> (LTENode (fst branch1) (fst branch2), snd branch2) 130 | where branch1 = treeGen grammar lhs (depth-1) newGen 131 | branch2 = treeGen grammar rhs (depth-1) gen1 132 | gen1 = (snd branch1) 133 | IfNode ifExpr thenExpr elseExpr -> (IfNode (fst branch1) (fst branch2) (fst branch3), snd branch3) 134 | where branch1 = treeGen grammar ifExpr (depth-1) newGen 135 | branch2 = treeGen grammar thenExpr (depth-1) gen1 136 | branch3 = treeGen grammar elseExpr (depth-1) gen2 137 | gen1 = (snd branch1) 138 | gen2 = (snd branch2) 139 | RuleNode rules -> treeGen grammar (RuleNode rules) (depth-1) newGen 140 | _ -> (NullNode, stdGen) 141 | where curNode = 142 | if (depth > 0) then (getRules initialRule) !! (randRange (max 0 (arity-1)) randNum) 143 | else (getRules initialRule) !! 0 144 | arity = getArity initialRule 145 | where (randNum, newGen) = uniformR (0::Double, 1::Double) stdGen 146 | 147 | 148 | -- randomList :: Int -> Int -> [Int] 149 | -- randomList seed sz = randomRs (0, sz) (mkStdGen seed) :: [Int] 150 | 151 | nodeEval :: Node -> Double -> Double -> Node 152 | nodeEval (NumberNode val) _ _ = NumberNode val 153 | nodeEval (BoolNode val) _ _ = BoolNode val 154 | nodeEval (SinNode expr) x y = 155 | case (nodeEval expr x y) of 156 | (NumberNode val) -> NumberNode (sin val) 157 | _ -> NullNode 158 | nodeEval (CosNode expr) x y = 159 | case (nodeEval expr x y) of 160 | (NumberNode val) -> NumberNode (cos val) 161 | _ -> NullNode 162 | nodeEval (TanNode expr) x y = 163 | case (nodeEval expr x y) of 164 | (NumberNode val) -> NumberNode (tan val) 165 | _ -> NullNode 166 | nodeEval XNode x _ = NumberNode x 167 | nodeEval YNode _ y = NumberNode y 168 | nodeEval (AddNode lhs rhs) x y = 169 | case ((nodeEval lhs x y), (nodeEval rhs x y)) of 170 | (NumberNode a, NumberNode b) -> NumberNode (a + b) 171 | (_, _) -> NullNode 172 | nodeEval (MultNode lhs rhs) x y = 173 | case ((nodeEval lhs x y), (nodeEval rhs x y)) of 174 | (NumberNode a, NumberNode b) -> NumberNode (a * b) 175 | (_, _) -> NullNode 176 | nodeEval (ModNode lhs rhs) x y = 177 | case (nodeEval lhs x y, nodeEval rhs x y) of 178 | (NumberNode a, NumberNode b) -> 179 | if b > 0 then NumberNode (mod' a b) 180 | else NumberNode 0 181 | (_, _) -> NullNode 182 | nodeEval (ExpNode base exp) x y = 183 | case (nodeEval exp x y) of 184 | (NumberNode exp) -> NumberNode (base ** exp) 185 | _ -> NullNode 186 | nodeEval (TripleNode a b c) x y = 187 | case (nodeEval a x y, nodeEval b x y, nodeEval c x y) of 188 | (NumberNode valA, NumberNode valB, NumberNode valC) -> 189 | TripleNode (NumberNode valA) (NumberNode valB) (NumberNode valC) 190 | (_, _, _) -> NullNode 191 | nodeEval (GTNode lhs rhs) x y = 192 | case (nodeEval lhs x y, nodeEval rhs x y) of 193 | (NumberNode a, NumberNode b) -> 194 | if a > b then BoolNode True 195 | else BoolNode False 196 | (_, _) -> NullNode 197 | nodeEval (GTENode lhs rhs) x y = 198 | case (nodeEval lhs x y, nodeEval rhs x y) of 199 | (NumberNode a, NumberNode b) -> 200 | if a >= b then BoolNode True 201 | else BoolNode False 202 | (_, _) -> NullNode 203 | nodeEval (LTNode lhs rhs) x y = 204 | case (nodeEval lhs x y, nodeEval rhs x y) of 205 | (NumberNode a, NumberNode b) -> 206 | if a < b then BoolNode True 207 | else BoolNode False 208 | (_, _) -> NullNode 209 | nodeEval (LTENode lhs rhs) x y = 210 | case (nodeEval lhs x y, nodeEval rhs x y) of 211 | (NumberNode a, NumberNode b) -> 212 | if a <= b then BoolNode True 213 | else BoolNode False 214 | (_, _) -> NullNode 215 | nodeEval (IfNode condExpr thenExpr elseExpr) x y = 216 | case (nodeEval condExpr x y, nodeEval thenExpr x y, nodeEval elseExpr x y) of 217 | (BoolNode condVal, NumberNode a, NumberNode b) -> 218 | if condVal then (NumberNode a) 219 | else (NumberNode b) 220 | (_, _, _) -> NullNode 221 | --normalizes [-1, 1] -> [0, 1] 222 | nodeEval (NormNode node) x y = 223 | case (nodeEval node x y) of 224 | (NumberNode val) -> (NumberNode ((val + 1) / 2.0)) 225 | _ -> NullNode 226 | nodeEval NullNode _ _ = NullNode 227 | -- nodeEval (RuleNode rules idx) x y = 228 | -- nodeEval (rules!!idx) x y 229 | 230 | 231 | nodeGet :: Node -> (Double, Double, Double) 232 | nodeGet (TripleNode a b c) = 233 | case (a, b, c) of 234 | (NumberNode valA, NumberNode valB, NumberNode valC) -> 235 | (valA, valB, valC) 236 | (_, _, _) -> (0.0, 0.0, 0.0) 237 | 238 | nodePrint :: Node -> String 239 | nodePrint (NumberNode val) = show val 240 | nodePrint (BoolNode val) = show val 241 | nodePrint RandNode = "rand(0, 1)" 242 | nodePrint (SinNode a) = 243 | "sin(" ++ 244 | (nodePrint a) ++ 245 | ")" 246 | nodePrint (CosNode a) = 247 | "cos(" ++ 248 | (nodePrint a) ++ 249 | ")" 250 | nodePrint (TanNode a) = 251 | "tan(" ++ 252 | (nodePrint a) ++ 253 | ")" 254 | nodePrint XNode = "X" 255 | nodePrint YNode = "Y" 256 | nodePrint (AddNode lhs rhs) = 257 | "add(" ++ 258 | (nodePrint lhs) ++ 259 | ", " ++ 260 | (nodePrint rhs) ++ 261 | ")" 262 | nodePrint (MultNode lhs rhs) = 263 | "mult(" ++ 264 | (nodePrint lhs) ++ 265 | ", " ++ 266 | (nodePrint rhs) ++ 267 | ")" 268 | nodePrint (ModNode lhs rhs) = 269 | (nodePrint lhs) ++ 270 | " % " ++ 271 | (nodePrint rhs) 272 | nodePrint (ExpNode base exp) = 273 | (show base) ++ 274 | "^(" ++ 275 | (nodePrint exp) ++ 276 | ")" 277 | nodePrint (TripleNode a b c) = 278 | "(" ++ 279 | (nodePrint a) ++ 280 | ", " ++ 281 | (nodePrint b) ++ 282 | ", " ++ 283 | (nodePrint c) ++ 284 | ")" 285 | nodePrint (GTNode lhs rhs) = 286 | (nodePrint lhs) ++ 287 | " > " ++ 288 | (nodePrint rhs) 289 | nodePrint (GTENode lhs rhs) = 290 | (nodePrint lhs) ++ 291 | " >= " ++ 292 | (nodePrint rhs) 293 | nodePrint (LTNode lhs rhs) = 294 | (nodePrint lhs) ++ 295 | " < " ++ 296 | (nodePrint rhs) 297 | nodePrint (LTENode lhs rhs) = 298 | (nodePrint lhs) ++ 299 | " < " ++ 300 | (nodePrint rhs) 301 | nodePrint (IfNode cond thenVal elseVal) = 302 | "if (" ++ 303 | (nodePrint cond) ++ 304 | ") then " ++ 305 | (nodePrint thenVal) ++ 306 | " else " ++ 307 | (nodePrint elseVal) 308 | nodePrint (NormNode node) = 309 | "norm( " ++ 310 | (nodePrint node) ++ 311 | ")" 312 | nodePrint NullNode = "NULL" 313 | nodePrint (RuleNode rules) = 314 | intercalate " | " [(nodePrint a) | a <- rules] 315 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude as P 4 | import Graphics.Image as I 5 | import System.Random 6 | import AST 7 | import Render 8 | import System.Environment 9 | import System.Exit 10 | import System.Console.GetOpt 11 | import Data.Char 12 | 13 | data Options = Options 14 | { optHelp :: Bool 15 | , optDepth :: Int 16 | , optPixels :: Int 17 | , optOutput :: String 18 | , optSeed :: Int 19 | } deriving Show 20 | 21 | defaultOpts = Options 22 | { optHelp = False 23 | , optDepth = 25 24 | , optPixels = 200 25 | , optOutput = "out.png" 26 | , optSeed = 20250101 27 | } 28 | 29 | 30 | opts :: [OptDescr (Options -> Options)] 31 | opts = 32 | [Option ['h'] ["help"] (NoArg (\ opts -> opts {optHelp = True })) "show help" 33 | ,Option ['d'] ["depth"] (ReqArg (\ d opts -> opts {optDepth = read d}) "DEPTH") "depth of tree" 34 | ,Option ['p'] ["pixels"] (ReqArg (\ p opts -> opts {optPixels = read p} ) "PIXELS") "pixel dimensions" 35 | ,Option ['o'] ["output"] (ReqArg (\ o opts -> opts {optOutput = o}) "OUTPUT") "output filepath" 36 | ,Option ['s'] ["seed"] (ReqArg (\ s opts -> opts {optSeed = P.sum $ P.map ord s}) "SEED") "initial seed"] 37 | 38 | 39 | parseArgs :: [String] -> IO (Options) 40 | parseArgs argv = case getOpt Permute opts argv of 41 | (o, [], []) -> return (foldl (flip id) defaultOpts o) 42 | (_, _, errs) -> ioError (userError (concat errs ++ usage)) 43 | 44 | -- TODO: make grammar configurable 45 | -- grammar def start 46 | ruleA = RuleNode [RandNode, RandNode, XNode, YNode] 47 | ruleC = RuleNode 48 | [ruleA 49 | ,ruleA 50 | ,SinNode (AddNode ruleC ruleC) 51 | ,CosNode (AddNode ruleC ruleC) 52 | ,TanNode (AddNode ruleC ruleC) 53 | ,SinNode (MultNode ruleC ruleC) 54 | ,CosNode (MultNode ruleC ruleC) 55 | ,TanNode (MultNode ruleC ruleC) 56 | ] 57 | ruleE = RuleNode [TripleNode ruleC ruleC ruleC] 58 | grammar = [ruleA, ruleC, ruleE] :: Grammar 59 | -- grammar def end 60 | 61 | usage :: String 62 | usage = "Usage:\n randomart -h\n randomart [-d ] [-p ] [-o ] [-s ]" 63 | 64 | debugFlags :: Options -> IO() 65 | debugFlags flags = do putStrLn $ "Received flags: " ++ (show flags) 66 | 67 | main :: IO () 68 | main = do 69 | flags <- getArgs >>= parseArgs 70 | case (optHelp flags) of 71 | True -> do putStrLn usage 72 | exitWith ExitSuccess 73 | _ -> pure () 74 | let stdGen = mkStdGen (optSeed flags) 75 | let ast = fst (treeGen grammar ruleE (optDepth flags) stdGen) 76 | let evalAst (x, y) = nodeGet (nodeEval ast x y) 77 | let astImage = createImage (optPixels flags) evalAst 78 | writeImage (optOutput flags) astImage 79 | putStrLn $ "Success! Image generated under: " ++ (optOutput flags) 80 | -------------------------------------------------------------------------------- /app/Render.hs: -------------------------------------------------------------------------------- 1 | module Render where 2 | 3 | import Prelude as P 4 | import Graphics.Image as I 5 | import Data.Fixed 6 | 7 | -- 8 | -- Renders an image given a pixel mapping function. Note that the mapping function must 9 | -- map [-1, 1]^2 -> [-1, 1]^3 but the Graphics.Image package expects [0, dim]^2 -> [0, 1]^3 10 | -- so the function must rescale the output from [-1, 1]^3 to [0, 1]^3. The input rescaling 11 | -- is done by `mapOnto`. 12 | -- 13 | -- Example usage: 14 | -- ghci> let grayImage = createImage 200 grayGrad 15 | -- ghci> writeImage "../images/gray.png" grayImage 16 | -- 17 | -- Alternatively, instead of passing in a function, you can interpret an AST node. 18 | {- 19 | let ast = fst (treeGen grammar ruleE 10 randNums 0) 20 | evalAst (x, y) = nodeGet (nodeEval ast x y) 21 | let astImage = createImage 200 evalAst 22 | writeImage "./images/ast_image.png" astImage 23 | -} 24 | 25 | -- normalizes pixel input [0, dim]^2 -> [-1, 1]^2 and flips (row, col) -> (x, y) 26 | mapOnto :: Int -> (Int, Int) -> (Double, Double) 27 | mapOnto dim (r, c) = ( 28 | ((fromIntegral c) - halfDim) / halfDim, 29 | ((fromIntegral r) - halfDim) / halfDim) 30 | where halfDim = fromIntegral dim / 2.0 31 | 32 | -- example function: gray gradient, normalizes output from [-1, 1]^3 to [0, 1]^3 33 | grayGrad :: (Double, Double) -> (Double, Double, Double) 34 | grayGrad (x, y) = ((x + 1) / 2.0, (x + 1) / 2.0, (x + 1) / 2.0) 35 | 36 | -- example function: color gradient, normalizes output from [-1, 1]^3 to [0, 1]^3 37 | colorGrad :: (Double, Double) -> (Double, Double, Double) 38 | colorGrad (x, y) 39 | | x*y >= 0 = ((x + 1) / 2.0, (y + 1) / 2.0, 1.0) 40 | | otherwise = (((mod' x y) + 1) / 2.0, ((mod' x y) + 1) / 2.0, ((mod' x y) + 1) / 2.0) 41 | 42 | -- creates RGB pixel, takes in function :: [-1, 1]^2 -> [0, 1]^3 43 | mkPixel :: (Double, Double, Double) -> Pixel RGB Double 44 | mkPixel (x, y, z) = PixelRGB x y z 45 | 46 | createImage0 :: Int -> ((Double, Double) -> (Double, Double, Double)) -> Image VU RGB Double 47 | createImage0 dim func = makeImageR VU (dim, dim) (mkPixel . func . mapOnto dim) 48 | 49 | createImage :: Int -> ((Double, Double) -> (Double, Double, Double)) -> Image VU RGB Double 50 | createImage dim func = fromLists [ 51 | [(mkPixel . func . mapOnto dim) (i, j) | j <- [1..dim+1]] 52 | | i <- [1..dim+1]] 53 | -------------------------------------------------------------------------------- /randomart.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: randomart 3 | version: 0.1.0.0 4 | 5 | -- A short (one-line) description of the package. 6 | -- synopsis: 7 | 8 | -- A longer description of the package. 9 | -- description: 10 | 11 | -- A URL where users can report bugs. 12 | -- bug-reports: 13 | 14 | -- The license under which the package is released. 15 | -- license: 16 | author: James Ma 17 | maintainer: jamesma100@gmail.com 18 | 19 | -- A copyright notice. 20 | -- copyright: 21 | -- category: 22 | extra-source-files: CHANGELOG.md 23 | 24 | executable randomart 25 | main-is: Main.hs 26 | 27 | -- Modules included in this executable, other than Main. 28 | other-modules: 29 | AST 30 | Render 31 | 32 | -- LANGUAGE extensions used by modules in this package. 33 | -- other-extensions: 34 | build-depends: 35 | base ^>=4.16.4.0, 36 | hip ^>=1.5.6.0, 37 | random ^>=1.2.1.1, 38 | hs-source-dirs: app 39 | default-language: Haskell2010 40 | --------------------------------------------------------------------------------