Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

115 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

antro

A compiler project for an experimental programming language called antro language which is still in development. This project is purely educational and full or experimentation (for now) as the language cannot be used for any industry work in its current form. Therefore, the sole aim of this project is to show and teach the skills required of designing computer languages and implementing them.

The Antro project is implemented as a predictive recursive descent parser with a lookahead of 2 tokens (at most) and some backtracking to deal with parts of the grammar that aren't left-factored (... to be fixed).

The Regular Grammar as details for the Tokenizer and the Context-Free Grammar as production rules for the Parser (both written in EBNF format) as well as other details of the algorithm used to implement the recursive descent strategy of the parser.

FrontEnd Design

  • Tokenizer (lexical analysis)
  • Parser (syntactic analysis)
  • Executor (a concurrent thread-safe blocking queue for passing tokens from the Tokenizer to the Parser)

Backend Design

Make use of the LLVM IR Builder for IR (intermediate representation) generation via a Java library alongside the LLVM Module and LLVM Context and then convert the IR to machine code using an LLVM Codegen toolchain.

What is a Regular Grammar ?

  • A regular grammar is the set of all strings generated by a grammar which all contain characters of the alphabet (or other single-character symbols) as defined by that grammar and which result to tokens (terminal symbols).

What is a Context-Free Grammar ?

  • A context-free grammar (CFG) is a set of recursive rewriting rules (or productions) used to generate patterns of strings. A CFG consists of the following components: a set of terminal symbols, which are the characters of the alphabet that appear in the strings generated by the grammar as well as a set of non-terminals.

Sample program written in antro


	require: "sys.module/io"; # `print(...)` is defined here
	require: "asserts.module"; # `UNSAFE_type(...)` is defined here
	require: "errors.module"; # `.Err` trait + the `Error` impl is defined here

	def: MAX 200;

	begin: (void) void ->> .Err
	  # A novel programming language design for error handling (antro)
	  # This uses the concept of "chained exceptions" behind the scenes 
   	  # i.e. within the antro runtime.
	  
	  var error = call: Error::new("Program crashed");
	  
	  var ty = call: UNSAFE_factorUpBy2(MAX) -> eject_on error -> use {
 	    if (error:isEjected) {
	      call: print(f"{error:message} - {error:context:cause}");
	    }
		
  	    call: print("A fatal error occurred");

		panic_on error;
	  };
	  
	  call: print(f"{ty}");
	end;

	def: UNSAFE_factorUpBy2(x .int) .int ->> .Err {
	  var y, g = true;
      var error = call: Error::new("could not factor value by 2");

	   if (x > 0) {
	     y = (x / 2) * 4;
	   } else {
	     g = false;
	   }

	    y = call: UNSAFE_convertToFactor(g, x) -> eject_on error;
	    retn y;
	};

	def: UNSAFE_convertToFactor(c .bool, d .int) .int ->> .Err {
	  var error_message_prefix = "Argument type error: ";

	  defer -> invariants {
		call: print("leaving `UNSAFE_convertToFactor(...)` function");
      }

	  invariants {
	     error_message_prefix += "calling `UNSAFE_convertToFactor(..)` ~ ";
	     var next_error_message = "";
	     var error_message = error_message_prefix + "`c` is not a boolean";
	
	     var error = call: Error::new(error_message);
	     call: UNSAFE_type(c, "boolean") -> eject_on error;
	
	     next_error_message = error_message_prefix + "`d` is not a number";
	
	     var _error = call: Error::new(next_error_message);
	     call: UNSAFE_type(d, "number") -> eject_on _error;
	  }

	  # before the `retn` (return) statement below executes...
	  # ... we call the invariants below πŸ‘‡πŸΎπŸ‘‡πŸΎ
	  
	  # Antro bakes invariants right into the...
	  # ... programming model of the language πŸ’―

	  retn c ? d * 2 : 0; # ternary operator
	};

Though the above program doesn't do anything useful for now (i.e. the parser as currently written does not yet produce an Abstract Syntax Tree - AST nor does it provide an Immediate Representation - IR), one can still get to understand the basics of what's going on.

About

Data Types

In antro, all data types are prefixed with a . (dot) character. The built-in types are as follows:

  • .int for an integer type
  • .uint8 for an unsigned integer type (8 bits)
  • .uint16 for an unsigned integer type (16 bits)
  • .uint32 for an unsigned integer type (32 bits)
  • .uint64 for an unsigned integer type (64 bits)
  • .float for a float type
  • .double for a double type
  • .long for a long type
  • .ulong for an unsigned long type
  • .char for an signed char type
  • .byte for a unsigned char type
  • .str for a string type
  • .bool for a boolean type
  • .Error for an error type
  • .File for a file type

Module/File Imports

The require keyword is used to require/import a module (i.e. a folder) or a single source (i.e. a file) as an implicit dependency. For example, require: "sys.module/io"; is a statement that requires/imports a folder named "sys.module" and a sub folder named io. This sys.module folder is defined as a standard library entry which directly contain a root.antro file. The folder can also contain other antro source files and folders.

Entry Point Definition

The begin keyword is used to defined the entry point of the antro program. It is completed by the end keyword.

Other Definitions

The def keyword is used to define variables in the global scope (i.e. outside functions) that cannot be changed. When using def, it doesn't matter if the variable is defined in a global scope or local scope, it will always be a globally-scoped variable. Also, variables created with the def keyword cannot have their values changed/mutated but only copied into a variable whose value can be changed/mutated. NOTE: Antro makes use of lexical scoping.

Variable Creation

The var keyword is used to define variables or functions within a local scope (i.e. within functions) only. When using the var keyword, it matters that it isn't used in a global scope (i.e. outside functions) else the antro parser will throw a parse error. Also, variables created with the var keyword can have their value changed/mutated.

Multiple Return Values

antro does not have multiple return values. This is a very meticulously determined feature. As time goes on, it will become clear why this decision was made.

Error Handling - Part 1

The eject_on keyword in antro is the equivalent of a catch block in other programming languages like C#, Java and JavaScript. antro does not directly use the try/catch model for error handling. It uses an error to catch other errors that occur higher up on the call stack. In this way, the try/catch block is abstracted away from the source-level (hidden from the programmer) and handled by the antro compiler and runtime.

The semantics for eject_on here is exactly returning immediately from a function once an error is encountered (think throw in languages like Java and TypeScript). This is based on a novel error model designed for antro called Error Ejection.

Error Handling - Part 2

The panic_on keyword is the antro equivalent of panic keyword in Golang which triggers abandonment with no way to recover or stop the propagation of the panic yet merely pause it using a pause block (more on this later).

NOTE: Antro does not support multiple return value NOTE: Antro does not support enums (as they're mostly 'useless' in most languages (like C, Go) that implements them) better to use a struct. NOTE: Antro compiler has a build mode build-flag (i.e. --build-mode) on the CLI that relaxes the enforcement of certain compilation rules:

  • Using --build-mode=dev, any declared yet unused variable does not cause a compilation error
  • Using --build-mode=prod, any variable declaration where the right-hand side is a non-standard library API/non-literal must be typed
  • Using --build-mode=prod, any function definition without an invariants block causes a compilation error
  • Using --build-mode=dev, any call to panic_on (directly or indirectly) outside of a use block does not cause a compilation error

NOTE: Antro only has 2 broad classifications for errors:

  • Recoverable Errors
  • Non-recoverable Errors

Error Handling - Part 3

The use keyword is not the antro equivalent of finally keyword in most C-based programming languages like Java, Python or PHP. However, it is used specifically to recover and try again after a non-panic error has occurred. The use block is not meant to be used as another defer block where a resource is released (e.g. a lock or allocated memory) or where panics are paused (more on this later) and should not be treated as such. It is simple an inline recovery mechanism that allows the programmer to "try again" to return a valid value or otherwise (in an extreme case) panic or eject using panic_on and eject_on respectively.

Finally, antro has checked errors (a much more constrained and healthy implementation of Checked Exceptions in Java. Checked Errors build on top of Error Ejection and ensure that the only error type a function can possibly eject is included in the function signature.

Invariants

The invariants keyword is used to setup invariants within a local scope (i.e. within functions). For the design of antro, i believe that invariants ought to be baked into the programming model (i.e. the programming language design). In the future, i plan to setup macros just like they are used in Rust to make the invariants block shorter and more compact. All function definitions MUST contain an invariants block else the antro runtime will throw an error.

Deferring An Action

The defer keyword is the antro equivalent of the defer keyword in Golang. The defer keyword in antro works in a very specific set of ways. This includes defer being used to execute invariants as a function scope is about to be exited:

	defer -> invariants {
		# More code goes here...
	}

Or defer being used to execute a pause for a panic:

	defer {
		pause (err .Err) {
			# More code goes here...
		}
	}

Within a defer block, trying to access a variable that is in the global scope will result in a compilation error irrespective of the --build-mode (see section on build modes).

Function Output

The retn (return) keyword is used to return a value from a function definition or begin block.

Limiting Scope

The static keyword (similar to same in C programming language) is used in antro to limit the lexical scope access of a function or variable within a module source file.

Structs And Inheritance

The struct keyword is used to create structs in antro just like in Go, C, Odin and Zig. However, the only novel thing is that antro implements is inheritance of an abstract struct but not a type struct. Inheritance in antro is restricted to structs and impls that cannot be instantiated (i.e. they are abstract).

	# A single type struct (think `dict` type in Python)
	
	struct Student {
	  name .str,
      grade .char,
	  age .uint8,
	};

	var student .Student = Student::new(name = "Patrick",grade = "A",age = 11); # no compiler error
	var grad_student = Student::new(name = "Efosa",grade = "B",age = 23); # no compiler error
	# A single abstract struct (not a concrete type)

	struct Student {
	  name .str,
	  age .uint8,
	} as abstract;

	# A single type trait (think `interface` in TypeScript or Java and also an `abstract` class in PHP or Java)
	
	trait Person {
	  inherits Student { name };
	  
	   old .bool,
	   walk (void) void ->> .Error,
	};

	impl Me on Person {
	  
	  init () {
		prv |> old = false;
		pub |> name = "";
	  }

	  prv |> self&: walk (void) void ->> .Error {
		call: print("walk called...");
	  }
	} as abstract;

	var me .Person = Me::new(); # compiler error since `impl Me on Person` is abstract

License

This is released under the MIT license.

Design Inspiration

antro language design was inspired by 9 languages: C, Go, Zig, Python, PHP, Java, Odin, Rust and TypeScript all combined.

About

A compiler project for an experimental programming language called Antro

Topics

Resources

Stars

2 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages