diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for unique-lang
+
+## 0.1.0.0 -- 2022-07-28
+
+* First published version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2022 Owen Bechtel
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Unique
+
+Unique is an esoteric programming language where each number can only occur once. Every variable in Unique is stored in a single data structure: a stack of integer arrays. You can find a description of how the language works [here](https://owenbechtel.com/unique).
diff --git a/app/Examples.hs b/app/Examples.hs
new file mode 100644
--- /dev/null
+++ b/app/Examples.hs
@@ -0,0 +1,86 @@
+{-
+  (c) 2022 Owen Bechtel
+  License: MIT (see LICENSE file)
+-}
+
+{-# LANGUAGE QuasiQuotes #-}
+
+module Examples (copy) where
+
+import NeatInterpolation (text)
+
+import Data.Text (Text)
+import qualified Data.Text.IO as IO
+
+copy :: IO ()
+copy = do
+  IO.writeFile "truth-machine.uniq" truthMachine
+  IO.writeFile "fibonacci.uniq" fibonacci
+  IO.writeFile "hello-world.uniq" helloWorld
+  putStrLn "Created truth-machine.uniq"
+  putStrLn "Created fibonacci.uniq"
+  putStrLn "Created hello-world.uniq"
+
+truthMachine :: Text
+truthMachine = [text|
+  # get input
+  42
+
+  # print 1 indefinitely
+  0 11 [
+    70 -70 + 1 50 49 - 4
+    69 -69 + 3 [ 24 20 - 64 60 - 44 ] 34 9
+  ] 16 18 +
+
+  # print 0
+  68 -68 + 84 80 - [
+    67 -67 + 91 90 - 66 -66 + 21 23 +
+  ] 15 19 +
+
+  # create if/else statement
+  8
+  |]
+
+fibonacci :: Text
+fibonacci = [text|
+  # add 1 0 1 to stack
+  0 3 [ 1 50 -50 + 61 60 - ] 6
+
+  51 -51 + 11 [
+    # save a copy of the top number
+    4 73 70 - 74 71 -
+
+    # add and print
+    10 84 80 - 85 81 - 44
+
+    # stop when numbers get too large
+    52 -52 + 63 62 - 1000000000000000000 26
+  ] 34
+
+  # create while loop
+  9
+  |]
+
+helloWorld :: Text
+helloWorld = [text|
+  # add array of characters to stack
+  0 14 [
+    10
+    33     # !
+    100    # d
+    108    # l
+    114    # r
+    111    # o
+    119    # w
+    32
+    44     # ,
+    37 3 * # o
+    27 4 * # l
+    12 9 * # l
+    101    # e
+    104    # h
+  ]
+
+  # print characters
+  43
+  |]
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,403 @@
+{-
+  (c) 2022 Owen Bechtel
+  License: MIT (see LICENSE file)
+-}
+
+{-# LANGUAGE LambdaCase #-}
+
+module Main (main) where
+
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
+import Control.Monad.Trans.State (StateT, runStateT, gets, modify)
+
+import Text.Read (readMaybe)
+import Data.List (nub)
+import Data.Char (ord, chr)
+import Data.Function (on)
+import Data.Bifunctor (first, second)
+import Control.Applicative (liftA2)
+import Control.Monad (foldM, void, when)
+import Control.Exception (IOException, catch)
+import System.Environment (getArgs, getProgName)
+
+import qualified Examples
+
+main :: IO ()
+main = getArgs >>= \case
+  ["run", path] -> runFile path
+  ["examples"] -> Examples.copy
+  _ -> do
+    name <- getProgName
+    putStrLn "Unique version 0.1.0.0"
+    putStrLn "Commands: "
+    putStrLn ("  " ++ name ++ " run <file>      interpret a Unique file")
+    putStrLn ("  " ++ name ++ " examples        create example programs")
+
+
+runFile :: FilePath -> IO ()
+runFile path = safeReadFile path >>= \case
+  Nothing -> putStrLn ("ERROR: file '" ++ path ++ "' does not exist")
+  Just program -> do
+    runUnique (runProgram program) >>= \case
+      Left err -> putStrLn ("ERROR: " ++ err)
+      Right () -> return ()
+
+safeReadFile :: FilePath -> IO (Maybe String)
+safeReadFile path =
+  fmap Just (readFile path) `catch`
+  \e -> return (const Nothing (e :: IOException))
+
+type Stack = [[Int]]
+data Mode = CommandMode | LengthMode | PushMode Int
+type Unique = ExceptT String (StateT (Stack, Mode) IO)
+
+runUnique :: Unique a -> IO (Either String a)
+runUnique u = fmap fst
+  (runStateT (runExceptT u) ([], CommandMode))
+
+fromIO :: IO a -> Unique a
+fromIO = lift . lift
+
+getStack :: Unique Stack
+getStack = lift (gets fst)
+
+putStack :: Stack -> Unique ()
+putStack = modifyStack . const
+
+modifyStack :: (Stack -> Stack) -> Unique ()
+modifyStack = lift . modify . first
+
+getMode :: Unique Mode
+getMode = lift (gets snd)
+
+putMode :: Mode -> Unique ()
+putMode = modifyMode . const
+
+modifyMode :: (Mode -> Mode) -> Unique ()
+modifyMode = lift . modify . second
+
+runProgram :: String -> Unique ()
+runProgram program = do
+  case evalProgram (removeComments program) of
+    Left err -> throwE err
+    Right commands -> execCommands commands
+
+removeComments :: String -> String
+removeComments =
+  unlines . map removeComment . lines
+  where removeComment = takeWhile (/= '#')
+
+execCommands :: [Int] -> Unique ()
+execCommands commands = do
+  mapM_ execute commands
+  mode <- getMode
+  case mode of
+    CommandMode -> return ()
+    _ -> throwE "fewer numbers in array than expected"
+
+execute :: Int -> Unique ()
+execute num = getMode >>= \case
+  CommandMode -> execCommand num
+  LengthMode ->
+    if num <= 0
+      then throwE "array length must be positive"
+      else putMode (PushMode num)
+  PushMode len -> do
+    modifyStack (insert num)
+    putMode (if len == 1
+      then CommandMode
+      else PushMode (len - 1))
+
+execCommand :: Int -> Unique ()
+execCommand = \case
+  --PUSH
+  0 -> do
+    push []
+    putMode LengthMode
+
+  --DELETE
+  1 -> void pop
+
+  --SWAP
+  2 -> do
+    xs <- pop
+    ys <- pop
+    push xs
+    push ys
+
+  --ROTATE
+  3 -> do
+    xs <- pop
+    ys <- pop
+    zs <- pop
+    push ys
+    push xs
+    push zs
+
+  --DUPLICATE
+  4 -> do
+    xs <- pop
+    push xs
+    push xs
+
+  --APPEND
+  5 -> do
+    xs <- pop
+    ys <- pop
+    push (xs ++ ys)
+
+  --UNFOLD
+  6 -> do
+    xs <- pop
+    mapM_ (push . return) (reverse xs)
+
+  --IF
+  7 -> do
+    commands <- pop
+    conditions <- pop
+    when (all truthy conditions)
+      (execCommands commands)
+
+  --IF ELSE
+  8 -> do
+    elseCommands <- pop
+    commands <- pop
+    conditions <- pop
+    if all truthy conditions
+      then execCommands commands
+      else execCommands elseCommands
+
+  --WHILE
+  9 -> do
+    commands <- pop
+    whileLoop commands
+
+  --ADD
+  10 -> apOp (+)
+  11 -> zipOp (+)
+
+  --SUBTRACT
+  12 -> apOp (-)
+  13 -> zipOp (-)
+
+  --MULTIPLY
+  14 -> apOp (*)
+  15 -> zipOp (*)
+
+  --DIVIDE
+  16 -> apOpM divide
+  17 -> zipOpM divide
+
+  --MODULO
+  18 -> apOpM modulo
+  19 -> zipOpM modulo
+
+  --POWER
+  20 -> apOpM power
+  21 -> zipOpM power
+
+  --OR
+  22 -> boolApOp ((||) `on` truthy)
+  23 -> boolZipOp ((||) `on` truthy)
+
+  --AND
+  24 -> boolApOp ((&&) `on` truthy)
+  25 -> boolZipOp ((&&) `on` truthy)
+
+  --LESS THAN
+  26 -> boolApOp (<)
+  27 -> boolZipOp (<)
+
+  --GREATER THAN
+  28 -> boolApOp (>)
+  29 -> boolZipOp (>)
+
+  --EQUAL TO
+  30 -> boolApOp (==)
+  31 -> boolZipOp (==)
+
+  --NEGATE
+  32 -> do
+    xs <- pop
+    push (map negate xs)
+
+  --NOT
+  33 -> do
+    xs <- pop
+    push (map (toInt . not . truthy) xs)
+
+  --REVERSE
+  34 -> do
+    xs <- pop
+    push (reverse xs)
+
+  --LENGTH
+  35 -> foldOp length
+
+  --SUM
+  36 -> foldOp sum
+
+  --PRODUCT
+  37 -> foldOp product
+
+  --ANY
+  38 -> foldOp (toInt . any truthy)
+
+  --ALL
+  39 -> foldOp (toInt . all truthy)
+
+  --INPUT CHARACTER
+  40 -> do
+    ch <- fromIO getChar
+    push [ord ch]
+
+  --INPUT STRING
+  41 -> do
+    str <- fromIO getLine
+    push (map ord str)
+
+  --INPUT NUMBER
+  42 -> do
+    num <- fromIO getLine
+    case readMaybe num of
+      Nothing -> throwE "input is not a number"
+      Just x -> push [x]
+
+  --OUTPUT CHARACTERS
+  43 -> do
+    xs <- pop
+    fromIO (putStr (map chr xs))
+
+  --OUTPUT NUMBERS
+  44 -> do
+    xs <- pop
+    fromIO (mapM_ print xs)
+
+  --OTHER
+  num -> throwE
+    ("no command associated with number '" ++ show num ++ "'")
+
+truthy :: Int -> Bool
+truthy = (/= 0)
+
+toInt :: Bool -> Int
+toInt False = 0
+toInt True = 1
+
+insert :: Int -> Stack -> Stack
+insert num = \case
+  [] -> [[num]]
+  ([] : stack) -> [num] : stack
+  (xs : stack) -> (num : xs) : stack
+
+pop :: Unique [Int]
+pop = getStack >>= \case
+  [] -> throwE "no values in stack to pop"
+  (xs : new) -> do
+    putStack new
+    return xs
+
+push :: [Int] -> Unique ()
+push xs = modifyStack (xs:)
+
+apOp :: (Int -> Int -> Int) -> Unique ()
+apOp f = do
+  xs <- pop
+  ys <- pop
+  push (liftA2 f ys xs)
+
+zipOp :: (Int -> Int -> Int) -> Unique ()
+zipOp f = do
+  xs <- pop
+  ys <- pop
+  push (zipWith f ys xs)
+
+apOpM :: (Int -> Int -> Unique Int) -> Unique ()
+apOpM f = do
+  xs <- pop
+  ys <- pop
+  res <- sequence (liftA2 f ys xs)
+  push res
+
+zipOpM :: (Int -> Int -> Unique Int) -> Unique ()
+zipOpM f = do
+  xs <- pop
+  ys <- pop
+  res <- sequence (zipWith f ys xs)
+  push res
+
+boolApOp :: (Int -> Int -> Bool) -> Unique ()
+boolApOp f = apOp (\x y -> toInt (f x y))
+
+boolZipOp :: (Int -> Int -> Bool) -> Unique ()
+boolZipOp f = zipOp (\x y -> toInt (f x y))
+
+foldOp :: ([Int] -> Int) -> Unique ()
+foldOp f = do
+  xs <- pop
+  push [f xs]
+
+divide :: Int -> Int -> Unique Int
+divide x y =
+  if y == 0
+    then throwE "division by zero"
+    else return (div x y)
+
+modulo :: Int -> Int -> Unique Int
+modulo x y =
+  if y == 0
+    then throwE "modulo by zero"
+    else return (mod x y)
+
+power :: Int -> Int -> Unique Int
+power x y =
+  if y < 0
+    then throwE "negative exponent"
+    else return (x ^ y)
+
+whileLoop :: [Int] -> Unique ()
+whileLoop commands = do
+  conditions <- pop
+  when (all truthy conditions) $ do
+    execCommands commands
+    whileLoop commands
+
+data Symbol = Lit Int | Add | Subtract | Multiply | Ignore
+
+evalProgram :: String -> Either String [Int]
+evalProgram program = do
+  symbols <- mapM readSymbol (words program)
+  if getNums symbols == nub (getNums symbols)
+  then reverse <$> foldM execSymbol [] symbols
+    else Left "number occurs more than once"
+
+getNums :: [Symbol] -> [Int]
+getNums = \case
+  [] -> []
+  (Lit x : ss) -> x : getNums ss
+  (_ : ss) -> getNums ss
+
+readSymbol :: String -> Either String Symbol
+readSymbol = \case
+  "+" -> return Add
+  "-" -> return Subtract
+  "*" -> return Multiply
+  "[" -> return Ignore
+  "]" -> return Ignore
+  sym -> case readMaybe sym of
+    Nothing -> Left ("cannot parse symbol '" ++ sym ++ "'")
+    Just x -> return (Lit x)
+
+execSymbol :: [Int] -> Symbol -> Either String [Int]
+execSymbol xs = \case
+  Lit x -> return (x : xs)
+  Add -> operation (+) "add"
+  Subtract -> operation (-) "subtract"
+  Multiply -> operation (*) "multiply"
+  Ignore -> return xs
+  where
+    operation f name =
+      case xs of
+        (x : y : t) -> return (f y x : t)
+        _ -> Left ("not enough numbers to " ++ name)
diff --git a/unique-lang.cabal b/unique-lang.cabal
new file mode 100644
--- /dev/null
+++ b/unique-lang.cabal
@@ -0,0 +1,34 @@
+cabal-version: 2.4
+name:          unique-lang
+version:       0.1.0.0
+author:        Owen Bechtel
+maintainer:    ombspring@gmail.com
+
+category: Compilers/Interpreters
+synopsis: Esoteric programming language where each number can only appear once
+
+description:
+  Unique is an esoteric programming language where each number can only occur once. Every variable in Unique is stored in a single data structure: a stack of integer arrays. You can find a description of how the language works [here](https://owenbechtel.com/unique).
+
+homepage:           https://github.com/UnaryPlus/unique
+bug-reports:        https://github.com/UnaryPlus/unique/issues
+license:            MIT
+license-file:       LICENSE
+extra-source-files: CHANGELOG.md, README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/UnaryPlus/unique.git
+
+executable unique
+    hs-source-dirs:   app
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    main-is:          Main.hs
+    other-modules:    Examples
+
+    build-depends:
+      base >= 4.11 && < 5,
+      transformers >= 0.4 && < 0.7,
+      text >= 0.7 && < 2.1,
+      neat-interpolation >= 0.1.1 && < 0.6
