Cuttlefish's sacred texts

The docs are a bit of a mess right now, go check here

The article is incomplete for now, don’t worry, I’m working on it

This isn’t the most interesting article on CF, but it has all the info you need to make your own CF compiler / interpreter !
If your language fits all these requirements, it is considered as an official copy of CF.
Feel free to Ctrl+F properties you need info on.
Current version: 0.3.0

Philosophy

CF is a multiparadigm language, that supports functionnal features, and tries to make the devs write good, error-proof code.
It can be compiled to any target of your choice, and made compatible with any other language, as long as all the criteria below are respected.

Types

All data types are immutable by default, please check Mut for more info

Primary Types

  • i16 is a 16-bits whole number

  • Int is a 32-bits whole number

  • Long is a 64-bits whole number

  • Float16/32/64 is a 16/32/64-bits decimal number

  • Num is a Float x, where x depends on the implementation (usually 32)

  • Char is a standard utf-8 character

  • String is a list of Char of variable length

    Composite Types

  • (X, Y, ..) or Tuple, is a fixed-length, fixed-type list of values

  • [X] is a list of type X, variable length

  • X, .. ->> Y is a function, taking arguments of types on the left, outputting types on the right

  • (..) means priority. ex: (X ->> Y) ->> Z

  • X | Y means a type X or Y

  • x | y means a type which can only have values x or y

    you can have X | y where X is a type and y is a value

    Standard Monads

  • Maybe:

    Maybe X is here to try and make you error-handling less painful, and avoid your program crashing. A value of type Maybe X can be either Nothing, which would indicate a problem in code (ie div by 0) without crashing it, or Just x, where x is any value of type X.

    1
    2
    3
    safe_div :: Num, Num ->> Maybe Num
    _, 0 => Nothing -- you can't divide by 0
    a, b => Just a / b -- perform division which will not crash
  • Mut:

    By default, every piece of data is immutable. If you want to change it at any point, you have to assign it to Mut.
    My has 2 variants, Mut X & Const X. By default, any value of type X is also considered Const X.
    You can convert any data from one to the other.

    1
    2
    3
    4
    5
    do:
    var = Mut 0
    ++ var -- modifies just fine
    var = Const var
    ++ var -- yields a compile-time error (or runtime if interpreted)

    Even under Mut, a tuple can’t have values pushed to it, though its properties can be mutated. The only exception is when the tuple ends with ...X.

  • Empty

    is for when you don’t really need any value here. One of the only monads that doesn’t have multiple keywords at all. Empty is the type of an empty tuple, and an empty String / Char / List.

    data Empty = Const () | Const [] | Const "" | Const ''

    1
    2
    3
    4
    5
    6
    two :: Empty ->> i16
    Empty => 2
    () => 2
    x => 2 -- these are all valid definitions

    two () -- is a way to call it
  • IO

    is the dirty Monad. To make any program that influences the outer world, you need to use it. As soon as your function does more than going through lambdas & creating local variables, it is a side-effect, and should be indicated somewhere in the output type. As an input, it means you are reading data from somewhere.

    1
    2
    print<A: Show> :: A ->> IO
    readline :: IO ->> String

    IO, .. ->> .. or .. ->> IO, .. should lead to an error, as it is terrible code, and we don’t allow that.

  • Show

    Show is implemented for all primary types, (quite obvious, I won’t detail it here). It is a typeclass including a .show property, which returns the value as a string.
    The String function can be defined as:

    1
    2
    String<A: (Show)> :: A ->> String
    a => a.show ()
  • Map

    Map is one of the proudest features of CF, it operation from an iterable to every single element. You can get a map of any axis a by using the 'a operation.
    You can also unmap a value by casting it.

    1
    2
    3
    4
    5
    6
    7
    List <- [0..7]'0 + 5 == [5, 6, 7, 8, 9, 10, 11]

    list = [[1, 2], [3, 4]]
    list'0 == Map ([1, 2], [3, 4])
    list'1 == Map ([1, 3], [2, 4])
    list'1'0 + 1 == Map (Map (2, 4), Map(3, 5))
    List <- list'1'0 + 1 == [[2, 4], [3, 5]]
  • Iterable X

    Iterable of type X is a typeclass for anything that accepts indexing, and being turned into a Map, or a List

    1
    2
    3
    4
    typeclass Iterable X defines
    Map :: Iterable X ->> Map X
    List :: Iterable X ->> [X]
    Iterable[i16] :: X
  • @

    Is the antimonad. @ x gets rid of the hightest-level monad of the value.
    @X x gets rid of the monad X on the value.

    1
    2
    3
    x = Just Map [0,]
    x1 = @ x -- Map [0,]
    x2 = @Map x -- Just [0,]

Type Mods

  • ...Type is any number of Type

  • Type * x the same as “Type, Type, ..“, x times

    Casts

    If a cast if possible, it can be called using the type name as a function. Float 3 will result in 3.0

    Type Specifics

  • i16, Int, Long are represented by a number. If casted from a Float, the number is rounded.

  • Float16/32/64, Num are represented by a number, containing a dot. leading and trailing dots are allowed, as in 0. or .9. Can be casted from all number types.

  • Num accepts any number type, and automatically does the casting.

  • Char is represented by a single char surrounded with ''. Can be casted to String

  • String is represented by text surrounded with ""

    Numbers in Strings can be casted to Float / Int types, in base 10. The conversion will fail in case of a misformed string.

    Custom Types

  • Data

    1. You can define a new data type using the data keyword.

    data NewType = X, where X is any type, ie (String, Int, Num * 2, ...Int)

    you can only have 1 ..., in the last element of the type

    Those new types are just sugar, and can be inferred from the corresponding primaries.

    Creating a new data type creates a new constructor, which takes in as an argument the corresponding composite primaries. i.e NewType ("", 0, 0., 0., 2, 3)

    Also, these properties can be accessed in multiple ways:

    • my_new_var[0] would then access the string, since under the hood it’s just a tuple, in that case
    • You can define keywords while assigning the properties, ie data Point = (Num x, Num y), you can then access my_point.x
    1. You can then define custom properties of that type
      1
      2
      3
      data Vector = (Num x, Num y)
      Vector.magnitude = sqrt(Vector.x**2 + Vector.y**2)
      Vector.dims = 2
      Under the hood, it declares functions, taking in the vector object, computing the value. Types are inferred. You can consider those as macros, as they cannot rely on storing data in the individual object.

    TypeClasses

    I’ll use Haskell’s example, which can be found here, for now.

    Defining Typeclasses

    1
    2
    3
    4
    5
    typeclass Eq<A> defines
    (==) :: A, A ->> Bool
    (/=) :: A, A ->> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

    Implementing TypeClasses

    1
    2
    3
    4
    5
    6
    data TrafficLight = (Red | Yellow | Green)
    implement TrafficLight : Eq
    Red == Red = True
    Green == Green = True
    Yellow == Yellow = True
    _ === _ == False

    Checking for TypeClasses

    In a generic expression, you can make sure your class is compatible with your typeclasses. Here, we can use the Typeclass / Monad Show, which simply has 1 method, which turns a value to a string.

    1
    2
    print <A : (Show,)> :: A ->> IO
    x => IO.log <- x.show ()

Data Flow

All values not part of Mut are immutable by default.

Namespace

Namespaces are nothing more than a way of organising you code, and scoping more easily.

1
2
3
4
5
6
7
namespace Game
play_turn :: Board, Action ->> Board
...
player1_health = 100
Game.player_two_health = 100
Game.end_game :: Empty ->> IO
...

Scoping

Scopes are created manually too. At top-level, or in top-level do, you have access to anything global. In functions though, you only have access to what you manually scope using with.

You can use with long_name as short_name to make references less painful to type

Scoping an object, custom datatype, or namespace will scope all its properties

Generics

When declaring a function, or a class, you can use generics to make the instance apply to any type.

1
two_tuple<A, B> :: A, B ->> (A, B)

You can also force the input type to be part of a monad / typeclass.

1
print_map<A: (Show, Map)> :: A ->> IO

Destructuring

A list / tuple can always be destructured.
All last elements can be grouped using ....
... works like in JS for destructuring lists, it cannot be used outside of a tuple / list.

1
2
3
(a, b, c) = (100, 40, 30)
(x, ...xs) = [1..10]
flat_list = [1, 2, 3, ...[4, 5, 6]]

Functions

Functions are values of type X, .. ->> Y. They can be defined at top-level, first by type, then by a sequence of lambdas, which will be pattern-checked, the first corresponding one will be executed.

You can define a name as an operator by wrapping it into ().

Techical info

Under the hood, if possible, references should be passed in functions, as the inputs aren’t mutable, to save on space, since copying is useless.

Type

1
func :: Num, String ->> (Bool, Float64)

Lambdas

A lambda is defined with a list of arguments (which can be destructured), a “lambda operator”, and a single return value

Lambda Operators

A lambda operator is any operator that takes in 2 values, followed by > ie =>, +>, *>, >\>, etc.
The lambda will then apply that operator (not =) to the input / expression on the right, and return the result.

1
2
3
x => x * 2 -- returns 2 times x
x *> 2 -- same
x +> x -- same

I recommend => in most cases, as others are just shorthands

Pattern-Checks

There are two types of pattern checks:

  1. value checks:
    1
    a, 0 => Nothing -- will trigger if the second arg is 0
  2. boolean checks
    1
    a, b | b == 0 => Nothing -- will trigger if the condition on the right is met

Pipelines

A pipeline can be used along with ~>, they’re is a list of sequential lambdas, in curly brackets.

1
2
3
4
x ~> {
x => x * 2,
y => y / 2,
} -- is a valid pipeline, wrapped in a usable lambda using ~>

Do blocks

do blocks can either be used at top-level code, or in lambdas using ~>
They allow the CF imperative syntax to be used in them.
They can return a value, and be used in normal functions.

Top level do blocks have total access over the global scope, and can IO

Scope

Functions are manually scoped. They cannot access anything not expressed with with

1
2
my_cool_function :: Num ->> Num
with other_cool_function, other_other_cool_function as oocf

Overloading

If some of your definitions don’t fit the type of the function, you can declare it multiple times with different types.

Partial Application

Calling a function without all the arguments will results in another callable function.

A function cannot be defined with a variable number of arguments

1
2
3
add :: Num, Num ->> Num
a, b => a+b
add_five = add 5

Using <x?>, you can choose which arguments you leave blank, and which ones you fill in, where x is the new position of the argument.

1
2
3
4
5
6
div :: Num, Num ->> num
div_by_8 = div <0?> 8

func_with_4_args :: Num, Bool, String, i16 ->> Bool
other_func_with_one_arg = <1?> <0?> <2?> 5
-- that function is now of type Bool, Num, String ->> Bool

Operators

  • + - * / ** // %

    All the mathematical operators work as expected. (same as python)

Also, here are the definitions for strings.

1
2
(+) :: String, String ->> String
(*) :: String, Num ->> String
  • And &&

    And is the standard “and” logic operation

    1
    (&&) :: Bool, Bool ->> Bool
  • Or ||

    Or is the standard “or” logic operation

    1
    (||) :: Bool, Bool ->> Bool
  • :=

    This is the type check operator

    1
    0 := Num -- true
  • Input Operator ~

    1
    2
    (~)<T, U> :: T, (T ->> U) ->> U
    x, f => f x
  • Any |\

    Returns True if any of the elements evaluate to True.

    1
    2
    3
    (|\)<A: Iterable a> :: A, (a ->> Bool) ->> Bool
    (|\)<A: Iterable Bool> :: A ->> Bool
    [0, 1, 2] |\ (1==) -- True

    The any keyword / function acts the same, but the 2 inputs are reversed.

  • All &\

    Returns True if all the elements evaluate to True

    1
    2
    3
    (&\)<A: Iterable a> :: A, (a ->> Bool) ->> Bool
    (&\)<A: Iterable Bool> :: A ->> Bool
    [12, 15, 23] &\ (x => x > 5) -- True

    The all keyword / function acts the same, but the 2 inputs are reversed.

  • Foldl >\

    Returns a single value, by mapping through an iterable from the left.

    1
    (>\)<A: Iterable a, B> :: A, (B, a ->> B), B ->> B

    The foldl keyword / function acts the same, but the 2 first inputs are reversed.

  • Foldr <\

    Returns a single value, by mapping through an iterable from the right.

    1
    (<\)<A: Iterable a, B> :: A, (B, a ->> B), B ->> B

    The foldl keyword / function acts the same, but the 2 first inputs are reversed.

  • Filter \\

    Filter gets rid of every element which doesn’t satisfy a condition

    1
    (\\)<A: Iterable a> :: A, (a ->> Bool) ->> A

    The filter keyword / function acts the same, but the 2 inputs are reversed.

  • Map @\

    Map applies a function over a whole array

    1
    2
    (@\)<A: Iterable a, B> :: A, (a ->> B) ->> [B]
    x, f => @ <- f x'0

    The map keyword / function acts the same, but the 2 inputs are reversed.

Classes

You can define a class (which are a bit different from new types) using the class keyword.
Every class need a new method, which returns Make, the new type.
You can instantiate new instances using $

1
2
3
4
5
6
7
8
9
10
11
12
13
class Cat
new :: String ->> Make Cat
name ~> do:
this = Make Cat
this.name = name

meow :: Cat ->> IO
this ~> do:
IO.log <- f"{this.name} said meow !"

do:
my_pet = Cat $ "Felix"
my_pet.meow ()

You can then extend other classes, and inherit their properties.

1
2
3
4
5
class CoolCat : Cat
new :: String ->> Make CoolCat
name ~> do:
this = Make CoolCat (name,)
this.cool = True

You can, just like for data types, assign properties outside of the class definition

1
2
3
CoolCat.roar :: CoolCat ->> IO
this ~> do:
IO.log <- f"{this.name} roared"

You also can make properties static, though using a normal namespace is recommended

1
2
3
class Game
...
get_constant :: Static (Empty ->> Num)

Syntax

Functions

A function’s syntax is as follows

1
2
3
4
5
6
7
8
9
func_name :: Type
with list, of_the as t, functions, we, want_to_scope -- optional
the, lambdas => you_need
same ~> {
for => pipelines,
}
and ~> do:
blocks
where macros = definitions --optional

Functions are called by writing their names and then the arguments’ separated by spaces.

To specify left-association, you can use <-, which means f <- X Y Z = f (X Y Z)

Access Properties

1
2
3
X.Static_prop
x.prop
x["prop"]

Others

Classes, typeclasses & data are specified above.

do blocks are the only other block allowed iat top-level.

“Imperative Mode”

Inside do blocks, you get access to new features.

  • Local variable assignment with =
  • if/elif/else statements, identical to python’s
  • for loops, identical to python’s
  • while loops, identical to python’s

Error Handling

We really value great error handling. You’ll need to have access to 1 log file, for the Complete Error CE, and the terminal to display the Summarized Error SE.

In a do block

The line where this happened should be indicated, as well as the error type. If possible, log all important parts of the scope in the CE, and indicate the call stack (last ~5 elements) in the SE.

In a Pipeline

Print the line, again, with the error type. The whole call stack should be in the CE, along with the values of the inputs of the function at each point in time during the pipeline’s execution.

In any other Function

Print the line, error type, callstack in CE, and this time, you can show the inputs to the function in the SE as well, since there’s only one.

Warnings

  • Using with on a Mut variable
  • Muting without Copying

Modules

Modules can be created in .cf files, and parts (functions, classes, typeclasses, datatypes, and namespaces) of it can be importing following this syntax

1
import {part1, part2} from "file.cf"

Modules should have polymorphism if allowed to be imported from another language ie being able to call a function, access a property, etc.

Standard Library

This is a list of all functions that should be part of the Standard Library, available with or without imports.
These can and should preferably be shared between implementations

IO (non-optional) (no imports required)

  • Log

    is used to print content to the console, on a new line, and also ends the line

    1
    IO.log<A: Show> :: A ->> IO
  • prints data to the console, without starting nor ending with a newline

    1
    IO.print_raw<A: Show> :: A ->> IO
  • Prompt

    is used to get a string input from the console (it prints a prompt beforehand)

    1
    IO.prompt :: String, IO ->> String
  • Raw Input

    is used to get a string input from the console

    1
    IO.raw_input :: IO ->> String
  • ReadFile

    1
    IO.read_file :: IO ->> String
  • WriteFile

    1
    IO.write_file :: String ->> IO

String (non-optional) (no imports required)

  • Split

    is used to split a text at every occurence of a specified substring.

    1
    String.split :: String, String ->> [String]
  • Join

    is used to create a string from an iterable, with a separator

    1
    String.join<A: Iterable a> :: String, A ->> String
  • Lower / Higher

    are used to change the case of a string.

    1
    2
    String.higher = String ->> String
    String.lower = String ->> String

Iterable (non-optional) (no imports required)

  • Fill

    fill returns an iterable with a single value repeated, but the same length/structure as the input.

    1
    2
    fill<A: Iterable a> :: A, a ->> A
    x, v => x @\ (_=> v)
  • Zip

    zip takes in two iterables, and returns a zipped version of the length of the shortest input. zip [1, 2] [3, 4] = [(1, 3), (2, 4)]

    1
    2
    3
    -- 2 defs, from least precise to most precise
    zip<A, B> :: [A], [B] ->> [(A, B)] -- simpler def
    zip<A: (Iterable a), B: (Iterable b)> :: A, B ->> [(a, b)] -- complete def
  • Enumerate

    enumerate takes in 1 iterable, and associates every element with its index

    1
    2
    3
    4
    -- 3 defs, from least precise to most precise
    enumerate :: [A] ->> [(i16, A)]
    enumerate<A: (Iterable a)> :: A -> [(i16, a)]
    enumerate = zip [0..] <0?>

Math (non-optional) (imported)

name: "math.cf"

Re (optional) (imported)

name: "re.cf"

Memory Management

Every return value / variable is stored separately. Arguments to functions are always references. As soon as a value goes out of scope forever (not cited in any with nor used as an argument anymore), it is deleted.
Copy x returns a copy of a variable.

Misc

Ranges

Ranges work just like in Haskell, they are lazy-evaluated and expressed like so

1
2
3
[0..] -- is a lazy-evaluated iterable of all positive integers
[first_elem..last_elem]
[first_elem..second_elem..last_elem]

Misc

  • The file extension is “.cf”
  • Comments are denoted with #, even though -- is used in this article (syntax highlighting uses haskell’s for now).

Conventions, and Idiomatic Code

  • Typing

Variables and functions are written in lowercase.
Classes, Types, Namespaces and Typeclasses start with a capital letter.
Constant / Statics can be in all caps.