├── README.md ├── SUMMARY.md ├── book.json ├── part0 ├── initiatives.md └── overview.md ├── part1 ├── .DS_Store ├── comma.md ├── comments.md ├── data-types.md ├── functions.md ├── generics.md ├── lang-items.md ├── mods.md ├── terms.md └── visibility.md ├── part2 ├── const.md ├── named-modes.md └── records.md └── part3 ├── dependent-types.md ├── do-notation.md ├── gadts.md ├── memory.md ├── more-generics.md ├── phase-polymorphism.md ├── spreading.md └── universes.md /README.md: -------------------------------------------------------------------------------- 1 | # Ende 2 | 3 | [![License: CC BY-SA 4.0](https://licensebuttons.net/l/by-sa/4.0/80x15.png)](http://creativecommons.org/licenses/by-sa/4.0/) 4 | 5 | I just want to share what I've come up with about the design of a new programming language, and here it is. 6 | This language has a very strong type system and its functions are call-by-value. 7 | Currently this language aims to be a functional general-purpose language, that is, to make higher abstraction possible but still retain the ability to write imperative code. 8 | Anyone is very welcome to steal some ideas or write an implementation for it (but don't use the exact same name for the language). 9 | 10 | [Gitbook](https://www.gitbook.com/book/andyshiue/ende-the-programming-language/details) 11 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | ## Part 0 - Foreword 4 | 5 | * [Introduction](README.md) 6 | * [Overview](part0/overview.md) 7 | * [Initiatives](part0/initiatives.md) 8 | 9 | ## Part I - Basics 10 | 11 | * [Comments](part1/comments.md) 12 | * [Terms and Statements](part1/terms.md) 13 | * [Functions](part1/functions.md) 14 | * [lambdas](part1/functions.md#lambdas) 15 | * [Lang Items](part1/lang-items.md) 16 | * [User-Defined Data Types](part1/data-types.md) 17 | * [Visibility](part1/visibility.md) 18 | * [mods](part1/mods.md) 19 | * [Semicolon](part1/comma.md) 20 | * [Generics](part1/generics.md) 21 | 22 | ## Part II - Featured 23 | 24 | * [Named Modes](part2/named-modes.md) 25 | * [More General records](part2/records.md) 26 | * [Simple impls](part2/records.md#simple-impls) 27 | * [impl Functions](part2/records.md#impl-functions) 28 | * [Supertrait Arguments](part2/records.md#supertrait-arguments) 29 | * [Associated Values](part2/records.md#associated-values) 30 | * [Auto impls](part2/records.md#auto-impls) 31 | * [Visibility of impls](part2/records.md#visibility-of-impls) 32 | * [const](part2/const.md) 33 | * [const data](part2/const.md#const-data) 34 | * [A const Version of factorial](part2/const.md#a-const-version-of-factorial) 35 | 36 | ## Part III - Advanced 37 | 38 | * [More Powerful Generics](part3/more-generics.md) 39 | * [Do Notation](part3/do-notation.md) 40 | * [Spreading](part3/spreading.md) 41 | * [Phase Polymorphism](part3/phase-polymorphism.md) 42 | * [The Problem](part3/phase-polymorphism.md#the-problem) 43 | * [The Solution](part3/phase-polymorphism.md#the-solution) 44 | * [Memory Management](part3/memory.md) 45 | * [Heap Allocation](part3/memory.md#heap-allocation) 46 | * [GADTs](part3/gadts.md) 47 | * [Dependent Types](part3/dependent-types.md) 48 | * [with](part3/dependent-types.md#with) 49 | * [Universes](part3/universes.md) 50 | * [Hierarchies](part3/universes.md#hierarchies) 51 | * [Unordered](part3/universes.md#unordered) 52 | 53 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["autocover"], 3 | "pluginsConfig": { 4 | "autocover": { 5 | "title": "Ende the Programming Language", 6 | "author": "Andy Shiue", 7 | "font": { 8 | "color": "#FFFFFF" 9 | }, 10 | "background": { 11 | "color": "#000000" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /part0/initiatives.md: -------------------------------------------------------------------------------- 1 | # Initiatives 2 | 3 | The first \(somehow\) functional programming language I learned was Scala. 4 | In Scala, functions are classes, and functions with different arities are represented with different classes. 5 | This kind of ad-hoc solution not only violates the DRY \(Don't Repeat Yourself\) principle, but is also [limited](http://stackoverflow.com/questions/4152223/why-are-scala-functions-limited-to-22-parameters) up to some [fixed arity](http://www.scala-lang.org/api/2.12.0/scala/Function22.html). 6 | Something came into my mind slowly: programming is all about **abstraction**, can't we abstract over arities, just like we do with types? 7 | The language, Rust, I learned more lately, also lacks the ability to be generic over arities. 8 | However, the developers now are wiser than before, and they hide the deficit as an implementation detail, [expecting it to be implemented in the future](https://github.com/rust-lang/rfcs/issues/376). 9 | Interestingly, the macros of Rust support abstraction over arities in a really clever way, but I doubt it could be applied to normal functions. 10 | Some users of Rust have purposed another feature called [named arguments](https://internals.rust-lang.org/t/pre-rfc-named-arguments/3831) that Scala natively supports. 11 | Although being a feature more-or-less syntax-wise, it's also one that could improve readability. 12 | 13 | At the same time of learning those reportedly more "practical" languages, I'm also being interested in those said to be more "academic" ones, e.g. Haskell, Idris, Agda. 14 | All of those languages is leaning toward an interesting and super powerful fancy idea: dependent types. 15 | As I've said, dependent types blur the distinction between compile time and runtime, but perhaps _too much_. 16 | Why not forcing some computation to happen at compile time for the sake of efficiency? 17 | I've heard that one of the Rust's main competitor, D, has this kind of feature called [CTFE \(Compile Time Function Evaluation\)](https://tour.dlang.org/tour/en/gems/compile-time-function-evaluation-ctfe). 18 | Despite Haskell's plan to implement dependent types at the end, it would also seperate between parameters sent in at compile time and runtime. 19 | Even Idris, already having dependent types, also have some [erasure](http://docs.idris-lang.org/en/latest/reference/erasure.html) mechanism to erase terms at compile time. 20 | All of that implies we still want the distinction between phases, and a powerful type system needs not sacrifice it. 21 | 22 | I'm especially fascinated by the language Agda. 23 | To me, it seems like the grand unified theory that describes everything in the same way, that is, to make them _first-class_. 24 | Entities are all abiding by the same laws, nontheless, there's still some way to distinguish between them, in the way of _modes_. 25 | I'm not sure if the author of Agda realized it could be extended to support all the features I mentioned above. 26 | I've even never heard of a word to describe that feature either, so I coined it. 27 | I believe modes are the idea that solves all of these problems above in a coherent way. 28 | The interaction between modes also gives rise to another novel feature that a language named PureScript supports, namely [row polymorphism](https://leanpub.com/purescript/read#leanpub-auto-record-patterns-and-row-polymorphism). 29 | 30 | To sum up, I found some similarities between the languages I love and abstracted over some concepts in them with modes and extended it to the maximum I could imagine. 31 | Much of the following content will be center on it. 32 | 33 | -------------------------------------------------------------------------------- /part0/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This article was initially designed to be implemented from the beginning to the end, but it has already been too long. 4 | As some people suggested, I provide a brief overview here. 5 | 6 | The core feature of Ende is **modes**. 7 | Modes are ways to pass arguments to functions. 8 | In current programming languages, the boundary between phases, which mean compile time and runtime, are either not mixed well or too blurry. 9 | The former includes more conservative languages like Java or Go. 10 | The latter are basically those dependently-typed languages. 11 | The lack of compile-time constructs makes higher abstraction at compile time impossible, but dependent types make the phase of which the term is evaluated unpredictable. 12 | The idea of modes is basically genericity over phases. 13 | In complement to modes, you can specify stuff that works only at runtime or either at runtime or compile time using the `const` system of Ende. 14 | 15 | Being concrete, below are some examples that would make use of modes and the `const` system: 16 | 17 | 1. If you want iterate between some fixed numbers, and the operation you want to do is known at compile time \(which is the majority of cases\), the compiler would simply instantiate operations again and again iteratively between the fixed numbers you chose. 18 | Moreover, the same interface can also be used at runtime. 19 | Therefore, if you want to range over some indices only known at runtime, the change of the code would be minimal or even none. 20 | 21 | 2. Have you heard of writing a multiplication table with template meta-programming in C++? 22 | Because of the backwards compatibility wart with C, you have to use weird template-level syntax to write those functional code. 23 | In Ende, they can be written in a clean way and as I've said, the code could also be used at runtime. 24 | 25 | 3. Regular expressions, web template languages, SQL commands, etc. can be precompiled on demand in a type-safe manner, so you don't have to waste time at runtime. 26 | And as you would guess, you can also do them at runtime with the same API. 27 | 28 | The presence of modes makes almost everything in Ende first-class, which means they can be passed as arguments or returned. 29 | They include: 30 | 31 | 1. all functions 32 | 2. all structs 33 | 3. all data constructors 34 | 4. all traits 35 | 5. all implementations of traits 36 | 6. all modules 37 | 7. all types 38 | 39 | As you will see, Ende achieves the unification of different concepts with carefully designed syntax and semantics. 40 | Someone might find the similarities between your favorite programming language and Ende. 41 | I actually took lots of ideas from Rust, Scala, Agda, Idris, etc. 42 | Feel free to skip some of the initial paragraphs if you think they're obvious enough and reread them if later you find that you can't parse some of the syntax. 43 | 44 | -------------------------------------------------------------------------------- /part1/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyShiue/Ende-readme/fe9677d3b34b9e26a4134f1c14c39d9bb8c57880/part1/.DS_Store -------------------------------------------------------------------------------- /part1/comma.md: -------------------------------------------------------------------------------- 1 | # Semicolon 2 | 3 | Instead of resorting to indentation, we can use semicolons to seperate stuff. 4 | Therefore, The above `Three` can also be written as: 5 | 6 | ``` 7 | @allPub: 8 | data Three = one; two; three 9 | ``` 10 | 11 | The rule is that whenever we see a semicolon either with an indentation after a new line after it or not, we append a clause to the innermost possible place in the syntax tree. 12 | If there isn't some indentation after a new line in comparison to the previous line, it's parsed as a sibling clause of the previous one. 13 | Below are some examples: 14 | 15 | 1. This definition of `Three` is the same as the above ones: 16 | 17 | ``` 18 | @allPub: 19 | data Three = one; 20 | two; 21 | three; 22 | data SomeOtherData = someOtherData 23 | ``` 24 | 25 | 2. This would generate a parsing error: 26 | 27 | ``` 28 | @allPub: 29 | data Three = 30 | a; 31 | b; 32 | c 33 | ``` 34 | 35 | 3. This would also generate a parsing error when the parser encounters `b`: 36 | 37 | ``` 38 | @allPub: 39 | data Three = a; 40 | b; 41 | c 42 | ``` 43 | 44 | You can write 2 semicolons in a row to go out of one layer of the syntax, so `data First = first;; data Second = second` in the same line is valid syntax. 45 | More consecutive semicolons can be used to go out of several layers of the syntax in a similar fashion. 46 | 47 | Also, there's a way to escape all indentation after the next line. 48 | You can write `\` at the end of the line to do it as in C. 49 | 50 | -------------------------------------------------------------------------------- /part1/comments.md: -------------------------------------------------------------------------------- 1 | # Comments 2 | 3 | There are 2 kinds of comments in Ende. 4 | The first is line comments: they start with `\\` and extend toward the end of the line. 5 | The other one is block comments. 6 | They can span over multiple lines. 7 | They start with `\\@` and end with `@\\`. 8 | -------------------------------------------------------------------------------- /part1/data-types.md: -------------------------------------------------------------------------------- 1 | # User-Defined Data Types 2 | 3 | Data types are items and can be defined in function bodies. 4 | They can be defined with the keyword `data`. 5 | They can be seen as `enum`s in Rust and are more powerful than that of C/Java. 6 | I'll first consider its easiest usage, though. 7 | Let's show how to define a C/Java-like `enum` in Ende: 8 | 9 | ``` 10 | @allPub: \\ I will explain this is the next chapter. 11 | data Unit = unit 12 | @allPub: 13 | data Bool = 14 | true 15 | false 16 | @allPub: 17 | data Season = 18 | spring 19 | summer 20 | autumn 21 | winter 22 | ``` 23 | 24 | In Ende, `data` can have **variants**. 25 | Variants are the possible values of the defined type. 26 | Variants are namespaced; in order to access the variant `spring`, you need to write 27 | 28 | ``` 29 | let season = Season::spring 30 | ``` 31 | 32 | instead of 33 | 34 | ``` 35 | \\ let season = spring \\ Doesn't compile. 36 | ``` 37 | 38 | `data` variants could be matched through pattern matching: 39 | 40 | ``` 41 | fn isSummer(season : Season) -> Bool = 42 | match season of 43 | Season::summer => Bool::true 44 | _ => Bool::false 45 | ``` 46 | 47 | `match`es are always irrefutable in Ende. 48 | By the way, there is an alternative `fn` syntax to do top-level pattern matching. 49 | We write `match` in the last clause in the function body in this case. 50 | 51 | ``` 52 | fn isSummer(Season) -> Bool where match 53 | (Season::summer) => Bool::true 54 | (_) => Bool::false 55 | ``` 56 | 57 | Here, we directly match the arguments of the `fn`; now we don't need to pick a name for the argument of type `Season`; we can write `fn isSummer(Season)` in lieu of `fn isSummer(_ : Season)` in this case. 58 | I'm going to provide another example for clarity: 59 | 60 | ``` 61 | fn and(Bool; Bool) -> Bool where match 62 | (Bool::true; b) => b 63 | (_; _) => Bool::false 64 | ``` 65 | 66 | Variants may also take parameters. 67 | The `OptionI32` below is a type that could possibly carry an `I32`. 68 | 69 | ``` 70 | @allPub: 71 | data OptionI32 = 72 | some(I32) 73 | none 74 | ``` 75 | 76 | Parameters of variants can also be pattern matched: 77 | 78 | ``` 79 | fn unwrapOr42(OptionI32) -> I32 where match 80 | (OptionI32::some(i)) => i 81 | (_) => 42i32 82 | ``` 83 | 84 | Parameters of variants can be named, that is, to be indexed by a string: 85 | 86 | ``` 87 | @allPub: 88 | data Point = new { 89 | "x" -: I32 90 | "y" -: I32 91 | } 92 | ``` 93 | 94 | A `data` with a single variant is called a record. 95 | The compiler treats records different from normal `data` in subtle ways. 96 | Fields \(named arguments\) of an instance of a record can either be accessed by its name after a dot \(`.`\) or by pattern matching. 97 | 98 | ``` 99 | fn getX1(p : Point) -> I32 = p."x" 100 | fn getX2(Point) -> I32 where match 101 | (Point::new { "x" -: result 102 | "y" -: _ }) => result 103 | ``` 104 | 105 | The syntax of pattern matching a record or constructing an instance of it is the same as declaring one: 106 | 107 | ``` 108 | let center = Point::new { 109 | "x" -: 0i32 110 | "y" -: 0i32 111 | } 112 | ``` 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /part1/functions.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | Functions are the simplest concept that has the ability to encapsulate information in functional programming languages; function calls are terms. 4 | The declaration of a function is an _item_. 5 | Items are basically some constructs that can appear at the top level of the source code. 6 | 7 | Every application needs to have an entry, traditionally called `main`. 8 | In Ende, `main` is a function of type `() -> IO[Unit]`. 9 | That is, a function that accepts no inputs and returns something that has something to do with `IO`. 10 | Now, let's write the well-known _Hello, world!_ program in Ende. 11 | 12 | ``` 13 | fn main() -> IO[Unit] = putStrLn("Hello, world!") 14 | ``` 15 | 16 | Nothing special. 17 | The syntax is inspired by Rust; the keyword `fn` is used to declare a function. 18 | Unlike Rust, there is an equal sign \(`=`\) after the type of the function. 19 | 20 | ``` 21 | fn factorial(n : U32) -> U32 = 22 | if n == 0u32 23 | then 1u32 24 | else n * factorial(n - 1) 25 | ``` 26 | 27 | All operators in Ende are just normal functions with special names. 28 | For instance, the exponential operator `^^` could be defined as: 29 | 30 | ``` 31 | fn _^^_(_0_ : U32; _1_ : U32) -> U32 = 32 | if _1_ == 0u32 33 | then 1u32 34 | else _0_ * id(_0_ ^^ id(_1_ - 1u32)) 35 | ``` 36 | 37 | In the above example, the underscores in the function name `_^^_` represent where the arguments go when it's applied as an operator. 38 | Each argument `_n_` is a binding of the argument of the nth underscore. 39 | The allowed symbols in a variable name include `!`, `$`, `%`, `&`, `*`, `+`, `.`, `/`, `<`, `=`, `>`, `?`, `^`, `|`, `-`, `~`, `:`, `,`. 40 | Of course you cannot redefine built-in syntax such as `=`, `->` or `:` because that would cause parsing ambiguity. 41 | 42 | \(TBD: fixity\) 43 | 44 | In all of the above examples, the definition of the function is just a single term. 45 | There are two ways to write more complicated function. 46 | The first is to write `where` instead of `=` after the return type of the function; we call the code inside it _inside the function body_. 47 | The valid constructs inside the function body are `let ... = ...` and local function definitions only visible inside the function body. 48 | 49 | ``` 50 | fn getBMI() -> F64 where 51 | let weight = 100f64 52 | let height = 200f64 53 | fn calcBMI(w : F64; h : F64) -> F64 = w / h / h 54 | return calcBMI(weight; height) 55 | ``` 56 | 57 | The last clause in a function body must be `return` something. 58 | 59 | Another way to write a complicated function is to use do-notation; the valid clauses of which are those I called statements. 60 | You need them to do several `IO` operations at once. 61 | 62 | ``` 63 | fn greeting() -> IO[Unit] = do 64 | putStr("Hello, ") 65 | let name <- mut("Andy") 66 | putStr(name) 67 | putStrLn(".") 68 | putStr("Hello, ") 69 | name := "Sandy" 70 | putStr(name) 71 | putStrLn(".") 72 | ``` 73 | 74 | You will see do-notation can be used much more generally than writing `IO` code. 75 | 76 | ## lambdas 77 | 78 | Lambdas are unnamed function literals. 79 | Like normal functions, lambdas must specify their return types. 80 | Lambdas are written similar to a function but without the name. 81 | 82 | ``` 83 | let abs = 84 | fn(n : I32) -> I32 = 85 | if n >= 0i32 86 | then n 87 | else -n 88 | ``` 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /part1/generics.md: -------------------------------------------------------------------------------- 1 | # Generics 2 | 3 | We've seen an `OptionI32` type above. 4 | In practice, we want an `Option` type that is generic over all types, not just `I32`. 5 | But let's start with a simplest generic function: `id`. 6 | `id` simply returns its only argument. 7 | 8 | ``` 9 | #pub: 10 | fn id[T](t : T) -> T = t 11 | ``` 12 | 13 | Here, I introduced another sort of delimiter while defining the function: brackets \(`[]`\). 14 | Different sorts of delimiters after the function name represent different _modes_ in which the parameters are passed. 15 | **Modes** are a very important feature in Ende; different modes serve as different purposes and have different characteristics. 16 | We say that the arguments inside the parentheses \(`()`\) \(`t` in the above example\) are arguments in the **normal mode**. 17 | In contrast, arguments inside the brackets \(`[]`\) \(`T` in the above example\) are arguments in the `const`** mode**. 18 | For now, you just have to know that arguments in `const` mode have to be supplied at compile time and can be inferred, so you can write `id(0i32)` instead of the more verbose `id[I32](0i32)`. 19 | 20 | I haven't mentioned function types, have I? 21 | Function types are literally the types of functions and are written as `(A; B; C; ...) -> R`. 22 | `A`,`B`,`C`, ... are the types of the arguments, and `R` is the return type. 23 | As a side note, similar to the ones in C++/Scala/Rust, arguments in normal mode cannot be curried in Ende. 24 | However, arguments in the `const` mode can: 25 | 26 | ``` 27 | \\ They are different: 28 | 29 | foo(a; b) 30 | foo(a)(b) 31 | 32 | \\ But they aren't: 33 | 34 | bar[A; B] 35 | bar[A][B] 36 | ``` 37 | 38 | This is because of the way that current machines work. 39 | At runtime, functions can have several arguments natively. 40 | If arguments in the normal mode were curryable, the compiler would have to return lambdas often or generate several partially applied copies of the original function. 41 | Arguments in the `const` mode are curryable, though, because that performance at compile time isn't that important, and programmers are supposed to do heavy calculation at runtime. 42 | 43 | Back to generics, here is a `compose` function, which `compose`s its 2 function arguments. 44 | 45 | ``` 46 | #pub: 47 | fn compose[A; B; C](f : (B) -> C; g: (A) -> B)(x : A) -> C = f(g(x)) 48 | ``` 49 | 50 | And here is the definition of a generic `Option` type: 51 | 52 | ``` 53 | @allPub: 54 | data Option[T] = some(T); none 55 | ``` 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /part1/lang-items.md: -------------------------------------------------------------------------------- 1 | # Lang Items 2 | 3 | In Rust and also in Ende, there's a concept of _lang items_. 4 | Lang items are items that are treated specially, having something to do with the compiler. 5 | I won't introduce a keyword for lang items but instead use _annotation_s to mark them. 6 | Annotations are written before the annotated item, start with an at sign \(`#`\) and is followed by a colon \(`:`\). 7 | 8 | ``` 9 | #lang("whatever"): 10 | fn whatever() -> Unit = unit 11 | ``` 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /part1/mods.md: -------------------------------------------------------------------------------- 1 | # `mod`s 2 | 3 | `mod`s are containers of items, and `mod`s themselves are also items, so `mod`s can be nested. 4 | You can define `mod`s in function bodies. 5 | We use the keyword `mod` to declare a module. 6 | 7 | ``` 8 | #pub: 9 | mod module where 10 | fn getUnit() -> Unit = Unit::unit 11 | @allPub: 12 | data Three = 13 | one 14 | two 15 | three 16 | #pub: 17 | mod inner where 18 | @allPub: 19 | data Circle = new { "radius" -: U32 } 20 | ``` 21 | 22 | items in a `mod` could only mention previous items including itself, unless wrapped in `mutual` blocks, in which any one of them can mention any one of the others. 23 | 24 | ``` 25 | #pub: 26 | mod another where 27 | mutual \\ This is required. 28 | data A = 29 | itself 30 | theOther(B) \\ Here it uses a type that is declared later. 31 | data B = 32 | itself 33 | theOther(A) 34 | ``` 35 | 36 | A `mod` can be declared without `where`. 37 | 38 | ``` 39 | mod somewhereElse 40 | ``` 41 | 42 | If the compiler sees such `mod`, the compiler will look for `./somewhereElse.ende` and `./somewhereElse/mod.ende` to read its content. 43 | If neither or both are presented, the compiler emits an error. 44 | 45 | There are two ways to access an item in a `mod`. 46 | One is to write its fully qualified name, e.g. `module::inner::Circle`. 47 | The other way is to `use` it. 48 | 49 | ``` 50 | use module::inner::Circle 51 | use module::inner::Circle::new 52 | 53 | fn getCircle() -> Circle = new { "radius" -: 1u32 } 54 | ``` 55 | 56 | An underscore \(`_`\) can be used as a wildcard to import everything in a module or in a `data`, so instead of writing 3 `use`s to import `one`, `two` and `three`, you can write 57 | 58 | ``` 59 | use module::Three::_ 60 | ``` 61 | 62 | `mod`s are also first-class value of type `Mod`: 63 | 64 | ``` 65 | #lang("Mod"): #pub: 66 | const data Mod 67 | ``` 68 | 69 | Here, `const` means the instances of `Mod` can only exist at compile time. 70 | I'll show how to manipulate `data` types at compile time later. 71 | 72 | -------------------------------------------------------------------------------- /part1/terms.md: -------------------------------------------------------------------------------- 1 | # Terms and Statements 2 | 3 | At the very beginning, let me introduce the terms of the language. 4 | Terms can be seen as trees that build up values. 5 | Terms include: 6 | 7 | 1. **Literal**s: 8 | There are several built-in types in Ende. 9 | They include numbers and strings. 10 | Boolean values still won't be introduced yet but will be defined as a data type. 11 | 12 | 1. **Integers**: 13 | There would be several types of integers of different size, being either signed or not. 14 | A literal of type `I32` would be written as `42i32`. 15 | `42` is its actual value, and `i32` means it's a 32-bit signed integer. 16 | Similarly, `666u64` would be a 64-bit unsigned integer. 17 | 18 | 2. **Floats**: 19 | Literals of floats would be similar to ones of integers, e.g. `2.71828f32`. 20 | 21 | 3. **Strings**: 22 | String literals are written in a pair of double quotation marks \(`""`\), in which you can escape some special characters as you do in other normal languages. 23 | 24 | 2. Applications of operators: 25 | When one talks about operators, we usually think of infix operators. 26 | But in actual implementations, operators of any fixity could be introduced. 27 | 28 | Examples would be `1u32 + 1u32`, `42u32 * 666u32`, `1u32 == 2u32`, etc. 29 | Fixity of operators should follow the common sense. 30 | If you need parentheses to group expressions, write `id()`, e.g. `id(2u32 + 2u32) * 2u32`. 31 | As you will see, this is actually not special syntax, but just a call to the identity function. 32 | 33 | 3. Variables, which are going to be discussed immediately. 34 | 35 | Throughout the article, more and more other kinds of terms will be introduced. 36 | 37 | Ende intends to be usable as an imperative language, so there are statements including control structures. 38 | Statements include: 39 | 40 | 1. `let`** binding**: 41 | A `let` binding binds its right-hand-side value to a variable. 42 | There are 2 flavors of `let` bindings, which can be seen as a mutable one and an immutable one respectively currently. 43 | `let` is by default immutable. 44 | For example, `let meaningOfLife = 42i32` binds `42i32` to a variable called `meaningOfLife`. 45 | As you can see, variables are written in camel case in Ende. 46 | Mutable variables can be declared in a different syntax. 47 | So, `let count <- mut(0i32)` binds `0i32` to a variable named `count`. 48 | 49 | 2. **mutation**: 50 | The value of a mutable variable can be mutated. 51 | `count := 1i32` changes the value of `count` to `1i32`. 52 | Mutating an immutable variable generates a compile error. 53 | 54 | 3. `while`** loop**: 55 | Normal `while` loop similar to other imperative languages. 56 | 57 | ``` 58 | while count == 0i32 then do 59 | forever() 60 | ``` 61 | 62 | The value a `while` loop returns carries no data. 63 | 64 | Now, introduce `if`. 65 | Unlike `while`, `if` can return something that isn't trivial. 66 | So we can write code like: 67 | 68 | ``` 69 | let number = if count == 0i32 then 42u32 else 666u32 70 | ``` 71 | 72 | Of course, `if` can also be used in an old-style, C-like fashion. 73 | 74 | ``` 75 | let number <- mut(0u32) 76 | if count == 0i32 77 | then number := 42u32 78 | else number := 666u32 79 | ``` 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /part1/visibility.md: -------------------------------------------------------------------------------- 1 | # Visibility 2 | 3 | All items, `data` variants and fields of records are by default private. 4 | Items and variants being private means we can't use them outside the module. 5 | Fields being private means we can't access it through the dot \(`.`\) notation or pattern matching; we can't construct an instance of that record outside either so we must rely on the `pub fn` it provides to initiate the record. 6 | 7 | You can write `#pub:` in front of the item to make something visible, examples include: 8 | 9 | ``` 10 | #pub: 11 | fn zero() -> I32 = 0i32 12 | #pub: 13 | data One = one 14 | ``` 15 | 16 | If you want to make all variants and fields of a `data` type public, you can write `@allPub:` in front of the `data`. 17 | You usually want this for `data` that aren't records. 18 | 19 | ``` 20 | @allPub: 21 | data Shape = 22 | circle 23 | triangle 24 | square 25 | ``` 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /part2/const.md: -------------------------------------------------------------------------------- 1 | # `const` 2 | 3 | You may found that I wrote `const`_ mode_ instead of _const mode_ throughout the article. 4 | This is intentional. 5 | `const` is an important keyword in Ende, and is highly related to the `const` mode. 6 | The basic idea is that a `const`_ something_ is a _something_ that can be done at compile time. 7 | So a `const` value \(a constant\) is a value known at compile time, and a `const fn` is a function that could be run at compile time. 8 | 9 | Now, let's go through all kinds of terms I introduced and see if they are constants. 10 | 11 | 1. **Literals**: They are definitely constants. 12 | 13 | 2. **Operator applications**: Operators are just functions with special names, and functions are discussed below. 14 | 15 | 3. **Variables**: 16 | A variable is a constant if it's declared as `const varName`. 17 | For example, in `const meaningOfLife = 42i32`, `meaningOfLife` is a constant. 18 | By contrast, in `let devil = 666i32`, `devil` **isn't** a constant. 19 | Of course, the right hand side of the `const` binding must be a constant as well. 20 | 21 | 4. **Control structures**: 22 | See the section about do-notation below. 23 | Specificly, The value of the entire `while` loop is not a constant \(actually because it's not a `const fn`\). 24 | `if`is special that the value of an `if` construct is a constant if and only if either of the conditions below is met. 25 | 26 | 1. The term immediately after `if` is a constant and evaluates to `Bool::true`, and the term after `then` is a constant. 27 | 28 | 2. The term immediately after `if` is a constant and evaluates to `Bool::false`, and the term after `else` is a constant. 29 | 30 | 5. **Functions**: 31 | The types of the parameters and the return type of any functions must be constants. 32 | Functions you declared are always constants, but there's a difference between `const fn`s and normal functions. 33 | `const fn`s are functions that could be run at compile time \(also at runtime\). 34 | The return value of a `const fn` is a constant if and only if all of its arguments are constants in that invocation. 35 | The operations you can do in the body of a `const fn` are more limited. 36 | You can only: 37 | 38 | 1. Declare a variable with `const`. 39 | 40 | 2. Declare a variable with `let`: 41 | The right-hand-side after the equal sign \(`=`\) must be a constant. 42 | Note that declaring a variable with `const` and `let` are also different in a `const fn`. 43 | They are both constants in a `const fn`. 44 | But a variable declared with `const` cannot depend on the arguments that could possibly not be known at compiletime, while a variable declared with `let` can. 45 | The gist of the design is to make changing a non-`const` function to a `const fn` \(or conversely\) the most seamless. 46 | 47 | 3. Normal terms: 48 | Each subterm in them must be a constant. 49 | 50 | 4. Function calls: 51 | Only `const fn`s can be called. 52 | 53 | `const fn`s are also checked to be _total_, meaning they don't recurse forever. 54 | A constant that evaluates to a `const fn` is also a `const fn`. 55 | If a `const fn` has multiple argument lists in normal mode, supplying one or more but not all lists of arguments outputs another `const fn`. 56 | There are also `const fn` lambdas. 57 | The syntax for it should be obvious. 58 | 59 | 6. `data`: 60 | In normal `data`, all variants are constants. 61 | In addition, all variants with parameters are `const fn`s. 62 | If you get a part of constant `data` by pattern matching or field accessing, what you get would still be a constant. 63 | 64 | 7. `impl`**s**: 65 | Did I mention `impl`s are first-class citizens of Ende? 66 | They can be returned and passed as arguments too. 67 | `impl`s are always constants; all `impl`s mentioned before are also `const fn`s that are guaranteed to be total. 68 | Nevertheless, Auto `impl`s need not be `const fn`s. 69 | Auto `impl`s that operate only at runtime are moreover annotated `#dyn:`. 70 | They exist because sometimes we can never get the value of the parameter at compile time, e.g. dereferencing a pointer into its underlying type. 71 | 72 | ## `const data` 73 | 74 | A data type marked `const` cannot have any instances at runtime. 75 | How is it useful then? 76 | It must have something to do with the compiler! 77 | And `Mod` is an example. 78 | It's special that it could be `use`d. 79 | 80 | What you can do in a `use` statement is what you can do in a `const fn`, so you can write something like: 81 | 82 | ``` 83 | use if isWindows then os::win else os::nix 84 | ``` 85 | 86 | You can write `const fn` accepting or returning `Mod`s, but they are only usable at compile time. 87 | And `mod`s can have parameters in the `const` or the instance mode: 88 | 89 | ``` 90 | mod Magic[magicNumber : I32] where 91 | \\ Use the magic number. 92 | ``` 93 | 94 | Such `mod`s are `const fn`s the return type of which is `Mod`. 95 | 96 | ## A `const` Version of `factorial` 97 | 98 | I'll define a `const fn factorial` in this subsection. 99 | The implementation of this `factorial` is different from the `U32` version above. 100 | This is not to suggest Ende has overloading; they're just declared in different namespaces. 101 | 102 | The problem with `U32` is that it's not defined recursively, so it would be harder for the computer to figure out if the functions using it terminate. 103 | The solution is to use a recursively defined data type: `Nat` 104 | 105 | ``` 106 | #lang("Nat"): @allPub: 107 | data Nat = zero; succ(Nat) 108 | ``` 109 | 110 | `Nat` is special that it has a literal form. 111 | Literal of type `Nat` has a suffix `nat`. 112 | 113 | And here's the `const fn factorial`: 114 | 115 | ``` 116 | const fn factorial(n : Nat) -> Nat where match 117 | (0nat) => 1nat 118 | (Nat::succ(m)) => n * factorial(m) 119 | ``` 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /part2/named-modes.md: -------------------------------------------------------------------------------- 1 | # Named Modes 2 | 3 | Compare this record in Ende 4 | 5 | ``` 6 | data Example = new { 7 | "example" -: Unit 8 | } 9 | ``` 10 | 11 | and this `struct` in Rust: 12 | 13 | ```rust 14 | struct Example { 15 | example: () 16 | } 17 | ``` 18 | 19 | Why do we need to write `new` after the equal sign \(`=`\)? 20 | It's not a keyword! 21 | The intention is to disambiguate between the type `Example` and the constructor of it. 22 | In Rust, which doesn't have dependent types, `Example` can mean both, and a usage of `Example` can always be resolved, but not in a language in which types are first-class. 23 | 24 | Variants in `data` have types, what is the type of `new` then? 25 | In order to assign a type to it, we need new modes in which parameters are named. 26 | Let's call them **named mode**s. 27 | The named normal mode is similar to the normal mode, except that the parameters are named and unordered; the named `const` mode corresponds to the `const` mode. 28 | The above `new` is now of the type `{"example" -: Unit} -> Example`. 29 | To be more general, now we can also have struct variants and arbitrary functions accepting named parameters. 30 | 31 | -------------------------------------------------------------------------------- /part2/records.md: -------------------------------------------------------------------------------- 1 | # More General records 2 | 3 | Records in Ende can not only act as Rust `struct`s but also Rust `trait`s or Java `interface`s or Haskell `class`es. 4 | I'll call them traits in the rest of the article if they are used like a Rust `trait`. 5 | Here, I'm going to write a `Monoid` trait. 6 | Of course, it's just another record. 7 | 8 | ``` 9 | @allPub: 10 | data Monoid[T] = new { 11 | "unit" -: T 12 | "_++_" -: (T; T) -> T 13 | } 14 | ``` 15 | 16 | To implement a trait, I introduce another keyword `impl`. 17 | All `impl`s are items and can be defined locally in a function body. 18 | Unlike the `impl`s in Rust or `instance`s in Haskell, `impl`s in Ende are always named. 19 | 20 | ## Simple `impl`s 21 | 22 | The `i32Monoid` below is a simple `impl`: 23 | 24 | ``` 25 | #pub: 26 | impl i32Monoid : Monoid[I32] = Monoid::new { 27 | "unit" -: 0i32 28 | "_++_" -: fn(left : I32; right : I32) -> I32 = left + right 29 | } 30 | ``` 31 | 32 | If `i32Monoid` is in scope, we can call the `_++_` method on `I32`: 33 | 34 | ``` 35 | \\ They are equivalent: 36 | 37 | let sum1 = i32Monoid."_++_"(1i32, 2i32) 38 | let sum2 = 1i32#_++_(2i32) 39 | ``` 40 | 41 | In order to write a function that is generic over types implementing a trait, the third mode is introduced. 42 | It's called the **instance mode**, and is delimited by `[()]`. 43 | Arguments in the instance mode can also be inferred, however not by looking at other arguments, but by searching for appropriate `impl`s. 44 | Here is a function generic over types implementing `Monoid`; it sums up all the values in a `List` using `Monoid`'s `_++_` method: 45 | 46 | ``` 47 | fn sum[T][(Monoid[T])](List[T]) -> T where match 48 | (List::nil) => unit 49 | (List::cons(head; tail)) => head ++ concat(tail) 50 | ``` 51 | 52 | You can see that when you put an argument inside the instance mode, the fields of it are automatically brought into scope without the quotation marks \(except those using weird characters that would be invalid identifiers in the syntax\); it's just a syntax sugar. 53 | 54 | When you write `sum(list)`, the compiler automatically chooses the right `impl` of `Monoid`; if there are 0 or more than 1 choices, the compiler generates an error. 55 | Nonetheless, you can explicitly provide a specific `impl`, delimiting which in `[()]`: 56 | 57 | ``` 58 | let _ = concat[(i32Monoid)](i32Vec) 59 | ``` 60 | 61 | ## `impl` Functions 62 | 63 | `impl` functions are literally, `impl`s that are functions. 64 | `impl`s need to be functions mainly because of 2 reasons: 65 | 66 | 1. It's generic over arguments in the `const` mode. 67 | 2. It's generic over arguments in the instance mode. 68 | 69 | I'm going to provide an `impl` function generic over arguments in both modes. 70 | First, define a `Group` trait. 71 | 72 | ``` 73 | @allPub: 74 | data Group[T] = new { 75 | "unit" -: T 76 | "_++_" -: (T; T) -> T 77 | "inverse" -: (T) -> T 78 | } 79 | ``` 80 | 81 | We know all `Group`s are `Monoid`s, so all instances of `Group[T]` should be able to be automatically converted to instances of `Monoid[T]`. 82 | How do we automatically convert stuff? 83 | The answer is obviously, again, using `impl`s. 84 | 85 | ``` 86 | #pub: 87 | impl groupToMonoid[T][(Group[T])] -> Monoid[T] = Monoid::new { 88 | "unit" -: unit 89 | "_++_" -: _++_ 90 | } 91 | ``` 92 | 93 | `groupToMonoid` is indeed an `impl` function. 94 | 95 | ## Supertrait Arguments 96 | 97 | What we don't have yet is the ability to define one trait to be a supertrait of another one. 98 | In other words, asserting if you implement a trait, another trait must also be implemented. 99 | You may ask, isn't the above `impl` functions enough? 100 | Not really. 101 | Imagine if you want to define an `Abelian` trait, the code you need to add would be: 102 | 103 | ``` 104 | @allPub: 105 | data Abelian[T] = new { 106 | "unit" -: T 107 | "_++_" -: (T; T) -> T 108 | "inverse" -: (T) -> T 109 | } 110 | 111 | #pub: 112 | impl abelianToGroup[T][(Abelian[T])] -> Group[T] = Group::new { 113 | "unit" -: unit 114 | "_++_" -: _++_ 115 | "inverse" -: inverse 116 | } 117 | ``` 118 | 119 | That's a lot of boilerplate! 120 | The code is very similar to implementing `Monoid`s for `Group`s; we have to write this kind of code again and again while creating an inheritance tree of traits. 121 | That is not tolerable. 122 | Can't we just store a `Group[T]` inside an `Abelian[T]`? 123 | Yes, we can! 124 | Moreover, we want to call the methods or more generally use the fields from the supertraits without accessing the fields explicitly. 125 | **Supertrait argument**s let us do all of that. 126 | 127 | In order to use this feature, the trait `Group` and `Abelian` can be rewritten as below: 128 | 129 | @allPub: 130 | data Group[T] = new[(Monoid[T])] { 131 | "inverse" -: (T) -> T 132 | } 133 | @allPub: 134 | data Abelian[T] = new[(Group[T])] 135 | 136 | \\ The `impl`s also have to be changed ... 137 | 138 | As you can see, it's simply the instance mode after the constructor. 139 | 140 | Searching of supertrait arguments isn't guaranteed to terminate because one could recurse on them, though. 141 | 142 | ## Associated Values 143 | 144 | Fields of a record can also be dependent on the others. 145 | They are different from normal _input parameters_ in that they don't determine the `impl` chosen but the `impl`s determine them. 146 | They are _output parameters_. 147 | For example, imagine if I want to define a trait `Add` for all types that implement the `_+_` operator. 148 | To achieve maximal flexibility, I want to make types of the the left-hand-side, right-hand-side, and the returned terms possibly different. 149 | It means it has to be generic over these 3 types. 150 | So maybe the trait could be like: 151 | 152 | ``` 153 | @allPub: 154 | data Add[L; R; Output] = add { 155 | "_+_" -: (L; R) -> Output 156 | } 157 | ``` 158 | 159 | However, the trait is problematic because we can provide both instances of `Add[L; R; A]` and `Add[L; R; B]`; if I write `id(l : L) + id(r : R)`, the compiler wouldn't be able to know if the type of the result would be `A` or `B`. 160 | This suggests that the `Output` type should not be an input parameter but rather an output one determined uniquely by `L` and `R`. 161 | The correct trait should be: 162 | 163 | ``` 164 | @allPub: 165 | data Add[L, R] = add[Output] { 166 | "_+_" -: (L; R) -> Output 167 | } 168 | ``` 169 | 170 | The `impl` ought to specify the `Output` type. 171 | If you want to access the output type specified by an instance of a trait, simply do a pattern matching. 172 | Or you can put them inside the named `const` mode to make calling it with the dot syntax possible. 173 | 174 | ## Auto `impl`s 175 | 176 | Automatic `impl`s are `impl`s with a parameter in the normal mode. 177 | The annotation `#auto:` indicates they are `impl`s that will be automatically inserted. 178 | For example, if one wants to overload string literals, they could provide an auto `impl` from `Str` to whatever type they want. 179 | For now, let's assume that type is called `StrLike`. 180 | The Ende source code would be something like: 181 | 182 | ``` 183 | #pub: #auto: 184 | impl strLike(Str) -> StrLike = ... 185 | ``` 186 | 187 | Auto `impl`s could be inserted at any node in the AST of a term if the expected type doesn't match the actual type, so if a term `str` occurs in the source code, it could possibly be transformed to `str#strLike()` anywhere. 188 | Auto `impl`s wouldn't be inserted more than once at a particular node, however, which means the following code doesn't type check. 189 | 190 | #auto: 191 | firstToSecond(First) -> Second = ... 192 | #auto: 193 | secondToThird(Second) -> Third = ... 194 | 195 | fn manipulateThird(third : Third) -> Third = third 196 | 197 | let second : Second = ... 198 | manipulateThird(first) \\ It works. 199 | 200 | let first : First = ... 201 | \\ manipulateThird(first) \\ `first` cannot be transformed to value of type `Third`. 202 | 203 | \(TBD: auto `impl` in an instance argument\) 204 | 205 | ## Visibility of `impl`s 206 | 207 | The visibility of `impl`s are the same as that of `fn`s. 208 | Because people usually want to import all `impl`s in a `mod`, I purpose a little syntax to do that: 209 | 210 | ``` 211 | use some_module::impl 212 | ``` 213 | 214 | Syntax for importing every `fn`/`data`/`mod` in a `mod` could be implemented likewise. 215 | 216 | -------------------------------------------------------------------------------- /part3/dependent-types.md: -------------------------------------------------------------------------------- 1 | # Dependent Types 2 | 3 | In the above examples, we've seen types depending on values at compile time, but not values at runtime. 4 | In order to be fully dependently-typed, another mode called the **pi mode** has to be introduced: 5 | 6 | | ordered | normal | `const` | instance | pi | 7 | | :---: | :---: | :---: | :---: | :---: | 8 | | `T` means | `(_ : T)` | `[T : _]` | `[(_ : T)]` | `([T : _])` | 9 | | type inference | no | yes | no | yes | 10 | | phase \(if not `const fn`\) | runtime | compile time | compile time | runtime | 11 | | curryable | no | yes | yes | yes | 12 | | argument inference | no | yes | proof search | no | 13 | | can be dependent on | no | yes | yes | yes | 14 | | **unordered** | `{t : T}` | `{[t : T]}` | `{[(t : T)]}` | `{([t : T])}` | 15 | | type inference | no | no | no | no | 16 | | phase \(if not `const fn`\) | runtime | compile time | compile time | runtime | 17 | | curryable | no | no | no | no | 18 | | argument inference | no | yes | proof search | no | 19 | | can be dependent on | no | yes | yes | yes | 20 | 21 | `(T)` and `[(T)]` mean `(_ : T)` and `[(_ : T)]`, respectively, but `[T]` and `([T])` mean `[T : _]` and `([T : _])`, respectively. 22 | Types of arguments in the `const` modes and the pi ones can be inferred. 23 | Usually arguments in the normal mode or pi mode are supplied at runtime, but not arguments in the `const` or the instance modes. 24 | Modes except the normal mode are curryable because it would be more convenient then. 25 | Arguments in the normal mode cannot be inferred and cannot be dependent on obviously. 26 | Arguments in all argument lists can only be dependent on arguments in previous argument lists. 27 | 28 | Existential types as an example of pi-types: 29 | 30 | ``` 31 | @allPub: 32 | data Sigma[A][B : ([A]) -> Type] = new([a : A])(B([a])) 33 | ``` 34 | 35 | \(TBD: `data` from the `const` mode to the pi one\) 36 | 37 | ## `with` 38 | 39 | The value of one argument of a dependent function might depend on another argument in pi mode. 40 | We need to be able to match against several arguments together. 41 | Introduce **views**, through `with` clauses. 42 | Top-level pattern matching can include arms with `with` clauses. 43 | Here's an example: 44 | 45 | \(TBD\) 46 | 47 | -------------------------------------------------------------------------------- /part3/do-notation.md: -------------------------------------------------------------------------------- 1 | # Do Notation 2 | 3 | We've seen `Functor`s, and traditional Haskell tutorial would probably go directly to applicatives and monads, and I'm also going to do that. 4 | 5 | The definition of applicative would be: 6 | 7 | ``` 8 | @allPub: 9 | data Applicative[F : [Type] -> Type] = new[(Functor[F])] { 10 | "pure" -: [A](A) -> F[A] 11 | "_<*>_" -: [From; To](_0_ : F[(From) -> To]; _1_ : F[From]) -> F[To] 12 | } 13 | ``` 14 | 15 | And monad would be: 16 | 17 | ``` 18 | @allPub: 19 | data Monad[F : [Type] -> Type] = new[(Applicative[F])] { 20 | "bind" -: [From; To](F[From]; (From) -> F[To]) -> F[To] 21 | } 22 | ``` 23 | 24 | If you know haskell you must know this `Monad` trait is very very very important. 25 | They can be used to model effects such as logging, error handling, mutable state, non-determinism, etc. 26 | `Monad`s are so important that there's also some special syntax, namely _do-notation_, to make writing monadic code easier. 27 | In Ende, I also want the do-notation, but a more general one than Haskell's, in that it doesn't necessarily have to desugar to the `bind` operation of `Monad`, but **any** method with `#lang("bind"):`. 28 | This is what Idris did because this kind of flexibility is needed when we need something stronger or something that resembles a `Monad` but isn't actually one. 29 | 30 | Below are the valid commands in the do-notation in Ende: 31 | 32 | 1. normal binding: 33 | 34 | ``` 35 | let plain = something 36 | ``` 37 | 38 | 2. pseudo-unwrapping: 39 | 40 | ``` 41 | let unwrapped <- something 42 | ``` 43 | 44 | 3. no binding: 45 | 46 | ``` 47 | any term 48 | ``` 49 | 50 | The one-to-one correspondence between Ende's syntax and Haskell's \(and the desugaring\) should be straightforward. 51 | 52 | -------------------------------------------------------------------------------- /part3/gadts.md: -------------------------------------------------------------------------------- 1 | # GADTs 2 | 3 | Normal `data` types are called ADTs in Haskell. 4 | GADTs are the **G**eneralized version of ADTs. 5 | GADTs let you define inductive families, that is, to be specific on the return types of the variants. 6 | We can define a GADT by writing `where` instead of an equal sign \(`=`\) after the parameters of the `data`. 7 | 8 | ``` 9 | @allPub: 10 | data Array[_ : Nat; T] where 11 | nil : Array[0nat; T] 12 | cons : [n : Nat](T; Array[n; T]) -> Array[Nat::succ(n); T] 13 | ``` 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /part3/memory.md: -------------------------------------------------------------------------------- 1 | # Memory Management 2 | 3 | Until now, the language is still fairly compatible with system programming. 4 | Functions are ... well, functions; non-capturing lambdas are function pointers. 5 | `data` are enums in C each with a tag indicating its variant to make them type safe but records specifically can be implemented as structs in C: I didn't say recursive types work. 6 | Surely some form of recursive type must be implemented in order to make Ende really useful, but I think it should be done with explicit pointer; I will discuss on it later. 7 | `mod`s are modules. 8 | Generics are monomorphized at compile time. 9 | `impl`s have nothing to do with runtime. 10 | 11 | ## Heap Allocation 12 | 13 | Perhaps the most important topic in memory management is heap allocation. 14 | In order to make Ende a system programming language, users of the language must be able to manipulate raw pointers. 15 | But in addition to it, there are supposed to be a higher-level interface for normal programmers since manipulating raw pointers are highly unsafe thus error-prone. 16 | I've considered 3 different approaches in the past: 17 | 18 | 1. **The Rust Approach**: 19 | the closest to the bare metal and theoretically the most efficient but requires lots and lots of lang items. 20 | 21 | 2. **The Swift Approach**: 22 | Doesn't require a garbage collector (GC), but instead implicitly using reference counting everywhere. 23 | Cycles between reference counted (RC) pointers could cause memory leaks and users have to be very careful about it. 24 | Dereferencing an unowned pointer could fail. 25 | 26 | 3. **The Java Approach**: 27 | GC everywhere. 28 | The safest solution the easiest to use, but one sometimes needs to *stop the world*. 29 | Bad for application needing low latency. 30 | 31 | I prefered the approach of using RC *explicitly* at the beginning, but found out that doesn't work quite well. 32 | One would end up need to write out RC almost everywhere. 33 | Although that would arguably make Ende *Rust++*, I think explicit is better than implicit and by far Rust's solution is the best one also because implementing ownership doesn't prevent Ende from implementing explicit RC/GC. 34 | I'll try to descibe the compiler work needed in the rest of this section. 35 | 36 | (TBD) 37 | -------------------------------------------------------------------------------- /part3/more-generics.md: -------------------------------------------------------------------------------- 1 | # More Powerful Generics 2 | 3 | Arguments in `const` mode need not be types; they can be constants as well. 4 | In fact, **any** constants can be passed to a function in the `const` mode. 5 | So I can write something like: 6 | 7 | ``` 8 | const fn factorial[m : Nat] -> Nat where match 9 | [0nat] => 1nat 10 | [Nat::succ(n)] => m * factorial(n) 11 | ``` 12 | 13 | But then we lose the ability to call `factorial` at runtime. 14 | 15 | Actually, types are also first-class citizens of Ende. 16 | That's why there could be associated types. 17 | `data` types _per se_ are constants. 18 | In Ende, we can also be generic over type constructors, which are `const fn`s at the type level. 19 | That is, to pass higher-kinded types around. 20 | Normal types have kind `Type`. 21 | However, `Option` is a type constructor and is of kind `[Type] -> Type`, which means it is a function from a type at compile time to another type. 22 | Here's the `Functor` trait. 23 | 24 | ``` 25 | @allPub: 26 | data Functor[F : [Type] -> Type] = new { 27 | "map" -: [From; To](self : F[From]; (From) -> To) -> F[To] 28 | } 29 | ``` 30 | 31 | Types of arguments in `const` mode are not always inferred to be `Type`, they can be inferred to be types or kinds of higher-kinded types as well. 32 | For example, if you want to write a function generic over functors, you don't need to explicitly write down the kind of `F`: 33 | 34 | ``` 35 | fn doSomethingAboutFunctors[F; A][(Functor[F])](F[A]) -> Whatever 36 | ``` 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /part3/phase-polymorphism.md: -------------------------------------------------------------------------------- 1 | # Phase Polymorphism 2 | 3 | ## The Problem 4 | 5 | `const` still isn't flexible enough in some situations. 6 | See the following 2 examples for instance: 7 | 8 | 1. **The **`curry`** function**: 9 | Having variadic arguments, we should be able to define a function that curries the arguments of another function. 10 | That is, if the function `func` has type `(I32; U32; F32) -> Bool`, `curry(func)` should have type `(I32)(U32)(F32) -> Bool`. 11 | 12 | let me show you how to define such function. 13 | 14 | \\ Don't ask what `???` is for now. 15 | \\ Just pretend it's magic; I'll debunk it later. 16 | \\ The type system still isn't necessarily strong enough to actually write it down. 17 | 18 | \\ We can also pattern match the tuple types instead of the values of the tuple types.) 19 | const fn CurriedFuncType(Type; .._ : Ordered[''Type]) -> ??? where match 20 | (Ret) => Ret 21 | (Ret; Head, ..Tail) => (Head) -> CurriedFuncType(Ret; ..Tail) 22 | 23 | #pub: 24 | const fn curry[Args : Ordered[''Type]; Ret](func : (..Args) -> Ret) 25 | -> CurriedFuncType(Ret; ..Args) where match 26 | (fn() -> Ret = ret) => 27 | ret 28 | [varargs (Head; ..Tail)](fn(head : Head; ..tail : Tail) -> Ret = ret) => 29 | fn(head : Head) -> CurriedFuncType(Ret; Tail) = curry(fn(..tail : Tail) -> Ret = ret) 30 | 31 | What's problematic about it? 32 | The problem is: 33 | 34 | > If a `const fn` has multiple argument lists in normal mode, supplying one or more but not all lists of arguments outputs another `const fn`. 35 | 36 | If the `func` is a constant but **is not** a `const fn`, because the above `curry` function is a `const fn`, `curry(func)` **is** also a `const fn`. 37 | Now there's a contradiction, so the compiler should not accept the definition of `curry`. 38 | But we want it to be a `const fn`! 39 | Moreover, if the input `func` is a `const fn`, we want the output function to be also a `const fn`. 40 | 41 | 2. **Lazy evaluation**: 42 | If mixfix operators are implemented, `if_then_else` can be implemented as a function, but we now need a way to say that some of the parameters are passed in lazily: 43 | 44 | ``` 45 | #pub: 46 | const fn if_then_else_[T](_0_ : Bool; _1_ : Lazy[T]; _2_ : Lazy[T]) -> T = 47 | match _0_ { 48 | Bool::true => _1_#force() 49 | Bool::false => _2_#force() 50 | } 51 | ``` 52 | 53 | However, this is not quite right, either. 54 | Because if `_0_` evaluates to `true`, the constness of `_2_` doesn't matter to the constness of the whole function. 55 | 56 | ## The Solution 57 | 58 | The solution is to give the users finer control of the `const` system. 59 | Instead of writing `const var` or plain `var`, we can write `phase(...) var` to specify its phase further. 60 | 61 | \(TBD\) 62 | 63 | -------------------------------------------------------------------------------- /part3/spreading.md: -------------------------------------------------------------------------------- 1 | # Spreading 2 | 3 | Usually, we don't want to make arguments in normal mode curryable. 4 | But sometimes we do want to supply variable length of arguments. 5 | There is a kind of mechanism in Ende to make genericity over arity doable. 6 | Because I want to make variadic arguments as flexible as possible, it might be a little bit harder to understand. 7 | I need to introduce a new kind of types called **tuple types**. 8 | They are not really the same as tuples in Rust or Haskell. 9 | Tuple types are type-level lists. 10 | For example, `varargs (Unit; Bool)` is a tuple type, and `varargs (I32; F32; U64)` is another tuple type. 11 | What is the kind of all tuple types then? 12 | It's called the **ordered variadic type**, and is written `Ordered[''Type]`. 13 | 14 | Now, I'm going to show you how to write a function accepting arbitrarily many arguments. 15 | For clarity, let's consider a rather easy example first. 16 | The function `sum` sums up all the `I32`s in the argument list no matter how many arguments there are. 17 | First, I have to define a helper function for it: 18 | 19 | ``` 20 | #pub: 21 | const fn FreshTuple[_ : Nat](Type) -> Ordered[''Type] where match 22 | [0nat](_) => varargs () 23 | [Nat::succ(n)](T) => varargs (T; ..FreshTuple[n](T)) 24 | ``` 25 | 26 | A special operator `..` was used; it's called the **spread operator**, and its purpose is to literally spread the arguments in a tuple type. 27 | A spreaded tuple therefore becomes _naked_ without the `varargs()` outside. 28 | For instance, now focus on the `FreshTuple` function above. 29 | 30 | * `FreshTuple[0nat](T)` is an empty tuple type. 31 | * `FreshTuple[1nat](T)` = `varargs (T; ..FreshTuple[0nat](T))` = `varargs (T)`. 32 | * `FreshTuple[2nat](T)` = `varargs (T; ..FreshTuple[1nat](T))` = `varargs (T; ..varargs (T))` = `varargs (T; T)`. 33 | 34 | So `FreshTuple[n](T)` is `T` repeated for `n` times. 35 | 36 | What are the types of the arguments of the `sum` function? 37 | They are `I32` repeated for arbitrarily many times! 38 | Now you can see how `FreshTuple` could be useful. 39 | We can accept a `FreshTuple(I32)` and spread it, leaving how many times it's repeated inferred. 40 | It would be easier to understand it by providing the concrete case than describing it in words: 41 | 42 | ``` 43 | const fn sum(.._ : FreshTuple(I32)) -> I32 where match 44 | () => 0i32 45 | (head; ..tail) => head + sum(tail) 46 | ``` 47 | 48 | In contrast to the ordered variadic type, there is `Row[''Type]`, which is a special kind of the **unordered variadic type**, which need not be ordered when deconstructing it. 49 | First, we need another helper function. 50 | 51 | \\ `Array[n; T]` is the type of arrays length of which is `n` and elements of which are of type `T`. 52 | #pub: 53 | const fn FreshRow[n : Nat][_ : Array[n; Str]](Type) -> Row[''Type] where match 54 | [0nat; Array::nil](_) => varargs {} 55 | [Nat::succ(n); Array::cons(head; tail)](T) => varargs { head -: T; ..FreshRow[n, tail](T) } 56 | 57 | After that we can write functions generic over named arguments. 58 | However, if you want to use the result of calling `FreshRow` more than once, you can't simply call them several times because the inferred arguments need not be the same. 59 | If you want them to be the same without writing down all the arguments in the `const` mode concretely, here's the trick: 60 | 61 | ``` 62 | @allPub: 63 | data Replicate[T; n : Nat] = new { 64 | "Args" -: Tuple[''Type] 65 | } 66 | 67 | #pub: 68 | impl replicate[T; n : Nat] -> Replicate[T; n] = Replicate::new { 69 | "Args" -: match n 70 | 0nat => varargs {} 71 | Nat::succ(n) => varargs (T; ..replicate[T, n]."Args") 72 | } 73 | ``` 74 | 75 | You define not the helper function but a trait recording the arguments. 76 | Here's how you could use it: 77 | 78 | ``` 79 | const fn sum[(Replicate[I32])](.._ : Args) -> I32 where match 80 | () => 0i32 81 | (head; ..tail) => head + sum(tail) 82 | ``` 83 | 84 | This trick is especially important with name modes. 85 | The corresponding `Replicate` trait of name modes would be: 86 | 87 | ``` 88 | @allPub: 89 | data Replicate[T; n : Nat][_ : Array[n, Str]] = new { 90 | "Args" -: Row[''Type] 91 | } 92 | 93 | #pub: 94 | impl replicate[T; n : Nat; arr : Array[n, Str]] -> Replicate[T; n; arr] = Replicate::new { 95 | "Args" -: match n 96 | 0nat => varargs {} 97 | Nat::succ(n) => match arr 98 | Array::cons(head; tail) => 99 | varargs { head -: T; ..replicate[T; n; tail]."Args" } 100 | } 101 | ``` 102 | 103 | Below is a structural `data` type. 104 | 105 | ``` 106 | data Structral[R : Row[''Type]] = structural { ..R } 107 | ``` 108 | 109 | You can add a field to `Structural`: 110 | 111 | ``` 112 | fn addField[T][(Replicate[T])](Structural { ..Args }) 113 | -> Structural { "foo" -: Int; ..Args } = ... 114 | ``` 115 | 116 | Or remove a field of it: 117 | 118 | ``` 119 | fn removeField[T][(Replicate[T])](Structural { "bar" -: Int; ..Args }) 120 | -> Structural { ..Args } = ... 121 | ``` 122 | 123 | The ability to add and remove fields is called _row polymorphism_. 124 | You can also pattern match the fields in name modes to achieve limited reflection. 125 | 126 | -------------------------------------------------------------------------------- /part3/universes.md: -------------------------------------------------------------------------------- 1 | # Universes 2 | 3 | What's the type of `Type` and type constructors? 4 | In Ende, `Type` is actually not a single type, but a series of types. 5 | You can think that `Type` has a hidden parameter that is a natural number. 6 | The `Type` that all of us are familiar about is `Type<0>`, but the type of `Type<0>` is `Type<1>`, the type of which is `Type<2>`, and going on and on. 7 | What about the types of function types? 8 | The answer is: 9 | 10 | ``` 11 | A : Type B : Type 12 | -------------------------- 13 | A -> B : Type 14 | ``` 15 | 16 | Imagine if we want to accept a potentially infinite list of arguments types of which are `Int, Type<0>, Int, Type<0>, Int, Type<0> ...`. 17 | How do we write a helper function to generate the tuple type? 18 | The variadic type of the return type of the function cannot be `Ordered[''Type<0>]` because the type of `Type<0>` cannot be `Type<0>`. 19 | The answer is to make universes cumulative: 20 | 21 | ``` 22 | m < n 23 | ------------------ 24 | Type <: Type 25 | ``` 26 | 27 | and 28 | 29 | ``` 30 | T : Type Type <: Type 31 | --------------------------------- 32 | T : Type 33 | ``` 34 | 35 | A term of a variadic type is a list of types the types of all of which are the same: 36 | 37 | ``` 38 | T1 : U T2 : U T3 : U ... 39 | --------------------------------------- 40 | varargs (T1; T2; T3 ...) : Ordered[''U] 41 | ``` 42 | 43 | Now that `Int : Type<1>`, so the type of `varargs (Int; Type<0>; Int; Type<0>; Int; Type<0> ...)` can be `Ordered[''Type<1>]`. 44 | 45 | ## Hierarchies 46 | 47 | We can see that 48 | 49 | ``` 50 | A <: B 51 | ---------------------------- 52 | Ordered[''A] <: Ordered[''B] 53 | ``` 54 | 55 | i.e. variadic types are covariant. 56 | Now we have at least 2 different hierarchies of universes, one is 57 | 58 | ``` 59 | Type<0> : Type<1> : Type<2> : Type<3> ... 60 | ``` 61 | 62 | , another one is 63 | 64 | ``` 65 | Ordered[''Type<0>] : Ordered[''Type<1>] : Ordered[''Type<2>] : Ordered[''Type<3>] ... 66 | ``` 67 | 68 | which could also be written 69 | 70 | ``` 71 | Ordered[''Type]<0> : Ordered[''Type]<1> : Ordered[''Type]<2> : Ordered[''Type]<3> ... 72 | ``` 73 | 74 | In reality there are infinite hierarchies because `Ordered[''Ordered[''Type]]` and so on are also hierarchies. 75 | It sounds reasonable to say that all the universes I mentioned are so called _small_ universes the type of which is `Universe<0>`. 76 | `Universe` is special in that elements of it can inherit another one. 77 | `Ordered` would become a constructor from `Universe` to `Universe` then. 78 | 79 | ``` 80 | #lang("Ordered"): @allPub: 81 | universe Ordered[''U : Universe] = 82 | nil 83 | cons(U; Ordered[''U]) 84 | ``` 85 | 86 | Now types of function types could be: 87 | 88 | ``` 89 | A : U1 ''U1 : Universe B : U2 ''U2 : Universe 90 | ------------------------------------------------------------------ 91 | A -> B : U2 92 | ``` 93 | 94 | If either the argument type or the return type is `Universe<0>`, perhaps we need `Universe<1>`, and we can go up until infinity. 95 | I don't know if what's beyond would be useful. 96 | 97 | ## `Unordered` 98 | 99 | Row types are defined in terms of the unordered universe constructor, the definition of which is the same as `Ordered` but the compiler handles spreading of them differently: 100 | 101 | ``` 102 | #lang("Unordered"): @allPub: 103 | universe Unordered[''U : Universe] = 104 | nil 105 | cons(U; Unordered[''U]) 106 | 107 | @allPub: 108 | universe KeyValue[Label; ''U : Universe] = 109 | _-:_(_0_ : Label; _1_ : U) 110 | 111 | #pub: 112 | fn Row[Code : Universe] -> Unordered[''KeyValue[Str; Code]] = 113 | Unordered[''KeyValue[Str; Code]] 114 | ``` 115 | 116 | Terms the type of which are `Type<0>` are types. 117 | What is the rule to determine if a term belongs to a custom universe? 118 | Below, I clarify the rule for it. 119 | To be more general, I introduce *generalized types*, the type of which are any universes. 120 | Inductively, a term is a generalized type iff 121 | 122 | 1. It's a type, or 123 | 2. It's a variant of a custom universe, and each of its fields are either a generalized type, or a term the type of the type of which is `Type`. --------------------------------------------------------------------------------