dorkhub

thorn

a pure lazy functional programming language to make ASCII art animations (and other things too)

olekawaii
Rust502 forksGPL-3.0updated 4 days ago
git clone https://github.com/olekawaii/thorn.gitolekawaii/thorn
Read this file the way it was intended:
https://codeberg.org/olekawaii/thorn/raw/branch/main/README

--------------------------   INTRODUCTION   ----------------------------
         ,.       ,.
       ,A$$$,   .A$$c
      f"Y$$$$ ,&M$$$$L         thorn is a pure, lazy, statically-typed
         $$$$p"  "$$$$         functional programming language for
         $$$$     $$$$         making colorful hand-drown ASCII art
         $$$$     $$$$         animations.
         $$$$     $$$f
         $$$$    .$$F          It's still in active development. If
         $$$$   ,$$'           you encounter any bugs or issues,
         $$$$  ,B'             please let me know. Send me an email to
         $$$R,f"               olekawaii@yahoo.com or make an issue
         $RY*'                 at https://codeberg.org/olekawaii/thorn
       .$F"
      eF`                      Make sure to check out some examples
     !$       _                in examples/ and std/
     'e.    ,$$;
       "*wew7*"

------------------------------   WHY   ---------------------------------

What's the point of this language? The point is to have
  * Lovely error messages
  * Cool syntax
  * Easy serialization and deserialization
  * Simplicity and no modules (I encourage circular imports)

A goal I have is to make the thorn source code as pretty as the actual
ASCII art. See for yourself (snippet from examples/dragon/main.th)

define center_head as the list frame loop art xii ix
   )  (     ...7..7.....    )  (    ....7..7....     )  (   .....7..7...
  ((__))    ..776677....   ((__))   ...776677...    ((__))  ....776677..
  /o..o\    ..636636....   /o..o\   ...636636...    /o..o\  ....636636..
 ( -vv- )   .6|6776|6...  ( -vv- )  ..6|6776|6..   ( -vv- ) ...6|6776|6.
  \ __. \   ..6|666|6...   ) __ (   ...6|66|6...   / .__ /  ...6|666|6..
   )     \  ...6|||||6..  /      \  ..6||||||6..  /     (   ..6|||||6...
  /       \ ..6|||||||6. /        \ .6||||||||6. /       \  .6|||||||6..
 '         ).6|||||||||6(          )6||||||||||6(         ` 6|||||||||6.
          ' ..||||||||6.            .||||||||||. `          .6||||||||..

The source code looks nice because
  * No special characters are allowed in the code (this includes
    paranthesis) to make a nice contrast with the ASCII art blocks,
    which are made up almost entirely of special charecters.
  * Drawings are isolated in their own blocks, so they're never
    interrupted by escaped characters or colors.

The paranthesis are not needed because
  * If you know the types of everything, function arguments can be
    grouped with prefix Polish notation. 3 * (1 + 2) -> * 3 + 1 2
  * Indentation acts like paranthesis. This allows you to pass lambdas
    and match expressions directly as function arguments. It can also
    help with type inference.

The error messages are lovely because
  * The errors show the highlighted line, an explenation, the previous
    line for context, and all the other stuff you'd expect.
  * You get both compile-time and run-time errors in the same format.
    That includes runtime errors like evaluation of an undefined or
    bottom expression.
  * There are no paranthesis, meaning all errors are found left to
    right. If you get, say, a type error at the end of a line, you
    know that everything that came before it was correct, not needing
    to worry about it. There's also no concept of misplacing
    paranthesis, which are a pain to debug in Haskell.
  * There are no warnings, only errors. Unused variables and bad
    indentation are caught before an ugly error message can surface.

--------------------------   INSTALLATION   ----------------------------

thorn has few dependencies. Assuming you have cargo and ghc, you can
build it with

    $ ./build.sh

This installs thorn (the interpreter), thorn-to-sh, and thorn-to-gif to
~/.local/bin (make sure it's in your $PATH). It also installs the fonts
used by thorn-to-gif to ~/.local/share/thorn/fonts
                                          _    ___   _
If you have nix, there's a flake         / \   \  \ / \
you can play around with                 \  \   \  '  /
                                       ---`  `---\   /  ,
    $ nix develop                     /___________\  \ / \
                                         /  /      \  /  /
or install it with                  /''''  /        \/  '''\
                                    \--.  /\        /  ----/
    $ nix profile add .               /  /  \      /  /
                                      \ / \  \------------
In the future, I will package          `  /   \ ___   __/
thorn for nixpkgs.                       /  ,  \   \  \
                                         \_/ \__\   \_/

thorn works like a calculator. It parses and compiles the source code
into an internal representation, but afterwards also executes it,
printing the evaluated main function.

    $ thorn . | thorn-to-sh  | sh               # shell animation
    $ thorn . | thorn-to-gif | ffplay -         # gif

Note that thorn expects to find a main.th file in the given directory
or in any of its parent directories. That directory is the root of your
project.

------------------------   SYNTAX OVERVIEW   ---------------------------

--  this is a comment
                            DECLARATIONS
forall NAME... DECLARATION     * declare any local type variables
define NAME as EXPRESSION      * define a global variable
type NAME contains             * type declaration. NAME is a data
    NAME TYPE...                 constructor and TYPE are its arguments
    ...
                             EXPRESSIONS
the TYPE EXPRESSION            * type annotation for rest of block
lambda PATTERN EXPRESSION      * lambda expression
undefined                      * undefined expression
match EXPRESSION               * pattern matching
    case PATTERN EXPRESSION
    ...
                              PATTERNS
NAME                           * capture value into variable name
_                              * drop value
CONSTRUCTOR PATTERNS           * match on constructor and its arguments
bind NAME PATTERN              * in addition, capture value in NAME (@)
either PATTERN PATTERN         * match either pattern (|)

                         FUNCTION APPLICATION

This code defines a function that applies a function to an integer.
The main body, if with paranthesis, would be (apply (add one) three).

    define apply as the
        fn fn int int fn int int
        undefined

    define main as the int apply add one three

What if we wanted to pass a lambda instead of the (add one)? We can't
do (apply (lambda x add one x) three) because there are no paranthesis.
Instead, we need to do

    define main is the int apply
        lambda x add one x
        three

Indentation acts like paranthesis.

------------------------   ALGEBRAIC TYPES   ---------------------------

Thorn has algebraic types and generics. The forall keyword is used to
declare any type variables. Here's a demo of the list and option types

    type bool contains
        true
        false

    forall a type list contains
        nil
        cons a list a

    forall a type option contains
        none
        some a

    define first_bool as the fn list bool option bool
        lambda input_list match input_list
            case nil                           none
            case cons head _tail               some head

The lambda keyword starts a lambda expression, followed by a pattern,
in which the input value will be placed. The match expression
patternmatches on a value, returning the first branch that matched.

In the last line, `cons` is a data constructor and `head/tail` are
variable names for the `a` and `list a` in the list definition. _tail
is prepended with an underscore to drop the value.

    define main as the option bool
        first_bool cons true cons false nil

output:  some true        (thorn's output, if the above is in main.th)

    define main as the option bool first_bool nil

output:  none

-------------------------   DESTRUCTURING   ----------------------------

Consider the following function that finds the distance between two
points. Ignore the undefined types/functions.

    type point contains
        point float float

    define distance as the fn point fn point float
        lambda pa lambda pb match tuple pa pb
            case
                tuple point xa ya point xb yb     -- destructuring pa,pb
                sqrt sum
                    squared diff xb xa
                    squared diff yb ya

This is very verbose because we're patternmatching on all the points to
get their x and y values. However, because the point type only contains
one variant, we can actually patternmatch directly in the lambda
expression like so

    define distance as the fn point fn point float
        lambda point xa ya lambda point xb yb  sqrt sum
            squared diff xb xa
            squared diff yb ya

In fact, lambda, match, and (in the future) let expressions all share
the same syntax and semantics for capturing variables. More patterns can
be found in the syntax overview section.

---------------------------   RECURSION   ------------------------------

There is no mutation and therefore there are no loops. The only way to
loop is to recurse or use a function that recurses.

    define main as the nat sum
        three                          +----------------------+
        succ one                       | one              = 1 |
                                       | succ one         = 2 |
    type nat contains                  | succ (succ one)  = 3 |
        one                            | and so on            |
        succ nat                       +----------------------+

    define three as the nat succ succ one

    define sum as the fn nat fn nat nat
        lambda x match x
            case one         succ
            case succ a      lambda y succ sum a y     -- recursive

output:  succ succ succ succ one

By the way, if you look at the second to last line, the `one` branch
returns a `succ` while the 'succ a' branch returs a lambda
expression. This is because the `succ` data constructor is a function.
(succ one) is of type (nat) but (succ) is of type (fn nat nat).  All
data constructors are curried.

------------------   LAZY EVALUATION  &  THE REPL   --------------------

The whole language is lazily evaluated. Expressions are only evaluated
when patternmatched on and evaluation starts at the main function. Here
are some examples. I'll use the thorn repl to define and test them.

>>> type bool contains
>>>     true                                        .----.----.----.
>>>     false                                       ['   |'   |'   ]
>>>                                               .--.   :    :   .--.
>>> type nat contains                             |' |---.----.---|' |
>>>     one                                       |  |___|____|___|  |
>>>     succ nat                                  :__|            :__|
>>>
>>> define infinity as the nat succ infinity
>>>
>>> forall a b type tuple contains tuple a b
>>>
>>> define are_equal as the
>>>     fn nat fn nat bool
>>>     lambda x lambda y match tuple x y
>>>         case tuple one    one              true
>>>         case tuple succ a succ b           are_equal a b
>>>         case _                             false
>>>
>>> -- Let's see what happens when we evaluate these expressions
>>>
>>> the bool are_equal              -- Despite infinity being infinitely
>>>     succ succ succ one          -- large, we can still work with it
>>>     infinity                    -- because it's evaluated lazily.

the bool false

>>> the bool are_equal              -- The program doesn't eat memory as
>>>     infinity                    -- it hangs.
>>>     infinity                    --

*hangs at runtime*

>>> the bool are_equal              -- undefined is a keyword. It can
>>>     succ one                    -- be used in place of any
>>>     succ succ succ undefined    -- expression.

the bool false

>>> the bool are_equal              -- If undefined is evaluated, the
>>>     succ succ succ one          -- program crashes with an error
>>>     succ succ undefined         -- pointing to it.

RUNTIME ERROR  in /tmp/thorn-repl.th:3:15
  |     succ succ succ one          -- program crashes with an error
3 |     succ succ undefined         -- pointing to it.
                  ^^^^^^^^^  entered undefined code
evaluated an undefined expression

>>> ^D

You can try all of these interactively in the thorn repl with

    $ thorn --repl

Note that thorn is not a scripting language. The repl just lets you
use it like one, similar to GHCi for Haskell.

---------------------------   ART BLOCKS   -----------------------------

The art macro is expanded into regular thorn code during the
tokenization phase. This code defines an animation of a swinging tail.
The animation is 6x4 in size and has 2 frames.

    include video       -- finds and imports video.th

    define main as the list frame dragon_tail

    define dragon_tail as art vi iv
     \   ).6|||6(   / 6|||6.
      ) / ..6|6. \ (  .6|6..
     ( /  .6|6..  \ ) ..6|6.
      V   ..6...   V  ...6..

Every frame is made up of who rectangles, one with the ASCII art and
the other with the corresponding colors. Colors 0-7 correspond to ANSI
colors black-white and `|`/`.` distinguish between spaces and
transparency. The `vi iv` after the art keyword are the width and height
of each frame in roman numerals.

Note that the indentation rules are different for art blocks. if the
indentation of the line with 'art' is zero, so is the art's
indentation.  Otherwise it's the line's indentation + 4.  Yes, the
indentation length is hardcoded to 4. This improves error messages.

There is special syntax for including local/global variables in the
art. To use a color variable you just replace the color number with the
variable name like so (colors defined in std/art/color.th)

    define dragon_tail as the
        fn color list frame           -- expecting the color as
        lambda c art vi iv            -- a function argument
             \   ).c|||c(   / c|||c.
              ) / ..c|c. \ (  .c|c..
             ( /  .c|c..  \ ) ..c|c.
              V   ..c...   V  ...c..

Now, instead of the tail being hardcoded cyan, the calling function
decides its color. Here's the table of supported art syntax

       +------+-------+-----------------------------------------+
       | left | right |                 effect                  |
       +------+-------+-----------------------------------------+
       | CHAR | 0     | Grey                                    |
       | CHAR | 1     | Red                                     |
       | CHAR | 2     | Green                                   |
       | CHAR | 3     | Yellow                                  |
       | CHAR | 4     | Blue                                    |
       | CHAR | 5     | Magenta                                 |
       | CHAR | 6     | Cyan                                    |
       | CHAR | 7     | White                                   |
       | CHAR | VAR   | Uses VAR as the color                   |
       |      | |     | Space cell                              |
       |      | .     | Transparent                             |
       | VAR  | $     | uses VAR as the cell  (ascii + color)   |
       | VAR  | #     | layers the frame VAR on top             |
       | VAR  | ^     | applies fn VAR to the cell below        |
       +------+-------+-----------------------------------------+
       where    VAR  is a single-letter variable name
                CHAR is a visible ASCII character

For complex videos with many colors, it's better to pass a single
palette than all the colors individually. You can do that with a
custom palette type.

    include video

    define main as the list frame dragon_roar default_dragon_p

    type dragon_p contains
        dragon_p
            color    -- skin
            color    -- bone
            color    -- eye
            color    -- wing

    define default_dragon_p as the dragon_p
        dragon_p cyan white yellow green

    define dragon_roar as the
        fn dragon_p list frame
        lambda dragon_p s b e _  entirely shift south art xiii vii
            .            b.............            b............
             ),__        .bbss........ ),__        .bbss........
              ( o`---~~^;..s|essssssss  ( o`---~~^;..s|essssssss
             /   -^V^V^V .s|||sbbbbbb. /    /V'V'V .s||||sbbbbb.
            /  `-----.-' s||sssssssss./  \  \,A,A  s||s||sbbbb..
              /          ||s..........  / `----.-' ||s.ssssssss.
                         .............             .............
            .            b............
             ),__        .bbss........
              ( o`---~~^;..s|essssssss
             /    /V'V'V .s||||sbbbbb.
            /    (       s||||s.......
               \  \,A,A  |||s||sbbbb..
                `----.-' ....ssssssss.

You can see how the core types are implemented in std/ . There are many
useful functions in video.th, list.th, and frame.th . To find the
definition of a function, just run

    $ grep -Rn 'define function_name '

-----------------------------   OUTPUT   -------------------------------

When evaluating a program, the output is the fully-evaluated main
function, a tree of data constructors, a string of words when printed
out. In fact, if you copy-paste the output into the main function, you
will get the same output. To turn this output into something you
actually want, you have to interpret it with an external program.

thorn tries to adhere to the UNIX philosophy. All programs do one thing
and their outputs are intended to be inputs for other programs

    $ thorn --eval 'the list frame chess_set' | thorn-to-sh | sh
                                      _:_
              n,_      o    o  o  c  __!__     o      n,_
    __ _ __  /.  \    / \    \v|v/   \_ _/    / \    /.  \  __ _ __
    [ U U ] (_/)  \  (  /)   <...>   <...>   (  /)  (_/)  \ [ U U ]
     |  -|    /  _/  <...>    )_(     )_(    <...>    /  _/  |  -|
     |___|   _/ \_   _/ \_   _/ \_   _/ \_   _/ \_   _/ \_   |___|
    (_____) (_____) (_____) (_____) (_____) (_____) (_____) (_____)
       _       _       _       _       _       _       _       _
     (  `)   (  `)   (  `)   (  `)   (  `)   (  `)   (  `)   (  `)
     <...>   <...>   <...>   <...>   <...>   <...>   <...>   <...>
     _/ \_   _/ \_   _/ \_   _/ \_   _/ \_   _/ \_   _/ \_   _/ \_
    (_____) (_____) (_____) (_____) (_____) (_____) (_____) (_____)

more like this

search

search projects, people, and tags