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

You might want to review the getting started section of the developer guide to get things installed and working.

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

  • number
  • string
  • bool
  • ()
  • 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,);
The trailing comma is required at the moment.

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 };
Semicolons are required when using if expressions as statements.
if true { 
} else { 
}; // Semicolon is required

Function Expression

Function expressions return a new anonynous function.

(a: number, b: number): number => {
    a + b
}
Explicit return types are required at this time!

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];
If types can't be inferred the type becomes 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
}
Explicit return types are required at this time!

Type Alias Statement

type statements allow aliasing types with new names.

type NumberList = list<number>;
Alias names must be capitalized!

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

You might want to review the getting started section of the developer guide to get things installed and working.

CLI

Source

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

Source

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

Source

The extension supports syntax highlighting, snippets, and integration with the language server.

Usage

  1. Uninstall any previous version of the extension
  2. Run just clean build
  3. Install the latest build ./vsc/out/egon-language-*.vsix
  4. 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"