The goal of this project is to make a language that is as similar as possible to plain English. Natural language is ambiguous, so we apply a series of rules and constraints to it. With them, we end up with a language that looks like English, but that is stable enough to be understood by a computer.
Of course, nothing can stop you from writing code that is obfuscated or not similar at all to a text in English, but it's up to you to write EPL programs in a readable way.
Here is a snippet with some sentences that the interpreter can understand:
Let L be the list from 0 to 5.
Add 1 to each number in L.
Print each element in L plus 2.
Here is the full documentation for the language's syntax.
Here is the documentation for the language's prelude.
Here are some examples of EPL programs.
Natural language is structurally ambiguous in ways that a normal context-free grammar can't resolve. Spaces are allowed in variable and function names, and since identifiers can contain spaces, the lexer has no reliable token boundary to split on. To solve that, the first-stage fuzzy parser produces a partial AST leaving some intentionally unresolved nodes, producing multiple candidate parses that the second-stage solver then filters down to a single one using type constraints and the context of defined variables and functions.
The language features a type inference system and supports polymorphic functions. It also implements pass-by-reference arguments. The solver promotes values to references when the function signature requires it, so the caller doesn't need to annotate it explicitly.
A special value type enables list comprehensions embedded in imperative syntax. It supports nested iterators across nested lists, so the interpreter can understand sentences like "let L be a list of numbers containing each number in M plus 2.".
Function definitions can have unnamed parameters, and they are identified by type instead. The solver generates aliases for them while preventing collisions, so they can be referenced in a natural way in the function body. That way, a function that defines how to "add a number to another number", has access to "the 1st number" and "the 2nd number".
The heap stores all non-primitive values as entries indexed by address. A mark-and-sweep garbage collector is used to free memory that is no longer reachable, and runs when live memory exceeds a threshold. Iterators create live values that are not reachable from any named variable, so they are registered as GC roots for the duration of the computation, to avoid them from being freed mid-build. Also, lists are represented as lists of addresses instead of values, allowing passing a specific item by reference to another function.
The language supports conditionals, for and while loops, and exception handling with try/catch.
When a program fails to run, the interpreter prints a detailed error message pointing at the sentence and the specific value within it that caused the error. To aid with understanding errors related to the disambiguation process, there's a distinction between them, telling whether the solver found no ways to parse the sentence, or if it found possible parsings but none of them type-checked. In addition, warnings are issued when more than one type-checking parsing is found, which means that the type system wasn't enough to fully disambiguate a sentence.
- Finding a more natural syntax for specifying function return types.
- Importing other files as libraries.
- Records.
- Interactive environment.
- Special variable
itfor referencing the result of the previous sentence. - Built-in filtering with callbacks as arguments.
- Allowing user-defined generic functions.
Requirements: Stack
To run a program, for example program.epl, simply use:
stack run -- program.epl
The -h or --help option displays usage help.
The -s or --silent option runs the interpreter without printing warnings or success messages.
