Introduction
The egon language is a toy statically typed interpreted language. It exists purely as a learning project.
Project Goals
- Design a language's grammar from the ground up
- To learn various forms of static code analyisis
- Type Checking
- Type Inference
- Linting
- Formatting
- Identifier resolution
- etc...
- Implementing a language server with more than error diagnostics
- Go to definition
- Go to usages
- Semantic tokens
- Refactoring
- etc...
Language Goals
- Errors should be compile/analyze time as much as possible
- Prefer expressions over statements
- Exhaustive pattern matching
- No exceptions/handling e.g.
Result - No null types e.g.
Option - Expressive macros e.g. Rust's
macro_rules! - Default immutability with explicit mutability
- Traits/Typeclasses
- Algebraic Data Types e.g. Rust's
enum - Destructuring
- Async/Coroutines
- Doc comments
- Modules
Inspirations
Rust
Rust is wonderful language and a defining influence on the Egon language. The language is challenging (e.g. traits vs interfaces/classes, borrowing, lifetimes) but with experience those things fade away and the simplicity of the language really shines. It's the simplicity of the language compiled with it's
Typescript
Typescript is a modern wonder providing an expressive type system for code that will run in a dynamically typed runtime evironment and has to interact with dynamically typed code. It's that type system that the Egon language draws inspiration from but to provide it in a statically typed runtime environment.
Resources
Here are some resources around designing and implementing a programming language.
- https://www.craftinginterpreters.com/
- https://www.reddit.com/r/ProgrammingLanguages/
- https://www.reddit.com/r/Compilers/
Basics
Variables
Variables store a piece of data. We can declare a variable using the let keyword.
let a = 123;
You can reassign the value later.
a = 456;
Constants
Constants are like variables but they can not be reassigned.
const a = 123;
a = 456;
// SyntaxError: `a` is a const value and can't be reassigned
Types
Egon is a statically typed language providing type errors at compile time.
let a = 123;
a = "testing";
// TypeError: mismatched types: expected type `number` but received `string`
The type was inferred when declaring a but an explicit type can be defined.
let b: string = 123;
a = "testing";
// TypeError: mismatched types: expected type `string` but received `number`
Types
numberstringbool()list<T>tuple<T, U, ...>function<tuple<Arg0, Arg1, ...>, R>range
Statements vs Expressions
So far we've seen assignment statements, value expressions, block expressions. How does Egon define statements vs expressions?
Expressions
An expression is code that executes and generates a value.
Unit Expression
The () "unit" expression is a type with a singular value. It represents when there is no value present. Blocks without a return value return the value of ().
let a: number = {};
// TypeError: mismatched types: expected type `number` but received `()`
Literal Expression
Literal expressions generate the value types: number (example: 123), string (example: "Hello"), and bool (example: false).
true;
false;
100;
15.25;
-60;
-123.456;
"Hello!";
Identifier Expression
TODO
Block Expression
Block expressions (delimited by {}) create a new scope containing statements and an optional return expression.
let a = 123;
let result: number = {
let b = 100;
a + b
};
b; // TypeError: `b` is not defined
Return Value
The last expression of a block is the block's implicit return value. An explicit value can be returned using the return statement.
// Implicit return
{
[1, 2, 3]
};
// Explicit return
{
return [1, 2, 3];
};
List Expression
List expressions generate a growable collection of same typed values.
[1, 2, 3, false];
// TypeError: mismatched types: expected type `number` but received `bool`
Tuple Expression
Tuple expressions generate a fixed sized collection of mixed typed values.
(1, "foo", true,);
Infix Expression
Infix expressions operate on two expressions (left and right) then return result value.
// Return `number`
10 + 10;
1 / 10;
5 * 20;
20 - 10;
10 % 5;
// Return `bool`
10 > 2;
5 < 50;
1 <= 10;
10 >= 1;
false != true;
"foo" == "foo";
Prefix Expression
Prefix expressions operate on a single expression (right) then return resulting value.
// Return `bool`
!true;
!false;
// Return `number`
-10;
Assign Expression
Assign expressions assign a value to an identifier then return the value.
let a = 123;
a = 456; // This is the assign expression
If Expression
If expressions evalutate a condition expression then return either one value or the else value.
let is_less = if 10 < 100 { true } else { false };
if true {
} else {
}; // Semicolon is required
Function Expression
Function expressions return a new anonynous function.
(a: number, b: number): number => {
a + b
}
Range Expression
Range expressions return a value with inclusive start and exclusive end values.
1..25;
..5;
2..;
Range expressions can also use inclusive end values but prepending =;
1..=25;
Type Expression
TODO
Call Expressions
Call a callable expressions (e.g. function) with arguments.
fn sum (a: number, b: number): number => {
a + b
}
let result = sum(5, 5);
assert_type result, number;
Non-Callables
Calling non-callables will result in an error.
123();
// TypeError: number is not callable
Statements
A statement is code that executes but doesn't generate a value. Statements are often postfixed with a semicolon but not always.
Expression Statement
A common statement is an expression statement. It executes the expression then disposes of it's value.
123;
"example";
true;
false;
Assignment Statement
Assignment statements declare variables and constants.
let greeting = "Hello";
const value = 123;
Types are inferred but can be explicited specified.
let is_example: bool = true;
const values: list<number> = [123, 456, 789];
unknown, causing a compile error.
Return Statement
A return statement executes the return value expression then set's it as it's containing block's return value.
{
return true;
};
return statements can not be used outside of block expressions.
Function Statement
fn statements declare a new named function.
fn sum (a: number, b: number): number => {
a + b
}
Type Alias Statement
type statements allow aliasing types with new names.
type NumberList = list<number>;
Assert Type Statement
assert_type statements perform compile time type assertions. This is mostly for testing type inference while developing the language.
let a = 123;
assert_type a, number;
let b = { true };
assert_type b, string;
// TypeError: mismatched types: expected type `string` but received `bool`
Tooling
CLI
Command
The egon command can lex, parse, and verify Egon code. If working from the repo then just cli can be used.
Lexing
Returns a list of tokens from Egon code.
egon lex ./path/to/file.eg
Parsing
Returns an AST from Egon code.
egon parse ./path/to/file.eg
Verify
Analyze Egon code and return any errors.
egon verify ./path/to/file.eg
Language Server
Command
The egon-lsp starts the Egon language server. If working from the repo then just lsp can be used.
egon-lsp
VS Code Extension
The extension supports syntax highlighting, snippets, and integration with the language server.
Usage
- Uninstall any previous version of the extension
- Run
just clean build - Install the latest build
./vsc/out/egon-language-*.vsix - Open any egon file
*.eg
EBNF
This is a rough translation of the current grammar. It may be incorrect. Please refer to the grammar file (LALRPOP).
Module = Stmt+
Stmt =
| StmtExpr
| StmtAssertType
| StmtReturn
| StmtAssignVariable
| StmtAssignConst
| StmtTypeAlias
| TypeDefStmt
| StmtFn
ExprStmt = Expr ";"
AssertTypeStmt = "assert_type" Expr "," Expr ";"
ReturnStmt = "return" Expr ";"
StmtAssignVariable =
| "let" Identifier ":" Type Expr ";"
| "let" Identifier ":" Type Expr "=" Expr ";"
| "let" Identifier "=" Expr ";"
StmtAssignConst =
| "const" Identifier ":" Type "=" Expr ";"
| "const" Identifier "=" Expr ";"
StmtTypeAlias = "type" Identifier "=" Type ";"
StmtFn = "fn" Identifier "(" ParamList ")" ":" Type "=>" Block
Expr =
| ExprUnit
| ExprLiteral
| ExprAssign
| ExprIf
| ExprCall
| ExprInfix
| ExprPrefix
| ExprList
| ExprTuple
| ExprRange
| ExprBlock
| "(" Expr ")"
ExprUnit = "(" ")"
ExprLiteral = Bool | Number | String;
ExprAssign = Identifier = Expr
ExprIf =
| "if" Expr Block
| "if" Expr Block else Block
| "if" Expr Block else ExprIf
ExprCall = |
Expr "(" ")"
Expr "(" Expr ("," Expr)* ")"
ExprInfix = Expr InfixOp Expr
InfixOp =
|"+" | "-" | "*" | "/" | "%"
| "and" | "or"
| "<" | ">" | "<=" | ">=" | "==" | "!="
ExprPrefix = "-" | "!" Expr
ExprList = "[" Expr ("," Expr)* "]"
ExprTuple = "(" Expr ("," Expr)* "," ")"
ExprRange = Expr ".." Expr
ExprBlock = "{" Stmt* (Expr)? "}"
ParamList = Param ("," Param)*
Param = Identifier ":" Type
Type = Identifier (<"," Type>*)?
Identifier = <identifier>
String = <string>
Number = <number>
Bool = "true" | "false"