A compiler for the LavaScript programming language that transpiles to JavaScript, written in Rust.
- Object-oriented programming with class-based inheritance
- Static type checking
- Variable initialization checking
- Return checking for non-void functions
- Method overloading
- Traditional non-S-expression syntax
src/
├── main.rs # Entry point
├── lexer/ # Lexical analysis
├── parser/ # Syntax analysis
├── ast/ # Abstract Syntax Tree definitions
├── typechecker/ # Type checking and semantic analysis
├── codegen/ # JavaScript code generation
└── error/ # Error handling and reporting
- Install Rust and Cargo using rustup
- Clone this repository
- Run
cargo buildto build the project - Run
cargo testto run the test suite
cargo run -- input.lsThis will compile the LavaScript source file input.lava and output JavaScript code.
// Example LavaScript code
class Animal {
init() {}
meth speak() -> Int {return 0; }
}
class Cat extends Animal {
init(name: Str, my_num: Int) { super(); }
meth speak() -> Int { return 1; }
}
fun greet(name: Str) -> Void {
println(name);
}
let cat: Animal = new Cat();
cat.speak();
greet("Hello");- Lexer (100%)
- Parser (100%)
- Type Checker (0%)
- Code Generator (100%)
string-char = ALPHA / DIGIT / SP / %x21 / %x23-26 / %x28-3F / %x40-5B
/ %x5D-60 / %x7B-7E
/ %x5C %x6E ; \n
/ %x5C %x74 ; \t
/ %x5C %x72 ; \r
/ %x5C %x22 ; \"
/ %x5C %x27 ; \'
/ %x5C %x5C ; \\
string-literal = %x22 *string-char %x22
; hex notation %x31-39 for digits 1-9
integer-literal = "0" / (%x31-39 *DIGIT)
identifier = 1*ALPHA *(DIGIT / "_")
var = identifier
funcname = identifier
classname = identifier
methodname = identifier
str = string-literal
i = integer-literal
type = "Int" / "Str" / "Boolean" / "Void" / classname
comma-exp = [exp *("," exp)]
primary-exp = var
/ str
/ i
/ "(" exp ")"
/ "this"
/ "true"
/ "false"
/ "println" "(" exp ")"
/ "print" "(" exp ")"
/ funcname "(" comma-exp ")"
/ "new" classname "(" comma-exp ")"
call-exp = primary-exp *("." methodname "(" comma-exp ")")
unary-exp = "!" unary-exp
/ "-" unary-exp
/ "+" unary-exp
/ call-exp
mult-exp = unary-exp *(("*" / "/") unary-exp)
add-exp = mult-exp *(("+" / "-") mult-exp)
comparison-exp = add-exp *(("<" / ">" / "<=" / ">=" / "==" / "!=") add-exp)
and-exp = comparison-exp *(("&&") comparison-exp)
or-exp = and-exp *(("||") and-exp)
exp = or-exp
vardec = "let" var ":" type
paramdec = var ":" type
comma-vardec = [vardec *("," vardec)]
comma-paramdec = [paramdec *("," paramdec)]
stmt = exp ";"
/ vardec ";"
/ var "=" exp ";"
/ "let" var ":" type "=" exp ";"
/ "while" "(" exp ")" stmt
/ "break" ";"
/ "return" [exp] ";"
/ "if" "(" exp ")" stmt *("else" stmt)
/ "{" *stmt "}"
funcdef = "fun" funcname "(" comma-paramdec ")" "->" type "{" *stmt "}"
methoddef = "meth" methodname "(" comma-paramdec ")" "->" type "{" *stmt "}"
constructor = "init" "(" comma-paramdec ")" "{" *("super" "(" comma-exp ")" ";") *stmt "}"
classdef = "class" classname ["extends" classname] "{" *(vardec ";") constructor *methoddef "}"
program = *(classdef / funcdef) 1*stmtWe chose Rust as the implementation language for our compiler for educational purposes. None of us had extensive experience with Rust beforehand, and we saw this project as an opportunity to explore its unique features, though it did cause a lot more headaches than using other languages. Rust being a mildly popular language for memory safety gave it a nice idea for building something complex and low-level like a compiler. We realized that writing a compiler in Rust would not be the past of least resistance, but the challenge aligned with our goals of developing a deeper understanding of both the language and low-level systems programming.
We selected JavaScript as the target language for our compiler because of its dynamically typed structure, which posed interesting challenges for static analysis and code transformation, more on that later. JavaScript is a language we're all familiar with, which allowed us to focus on the core compiler logic without needing to constantly reference obscure language behaviors. Its flexible syntax helped show that we were in for a bumpy ride, but an educational one filled with lots of useful lessons learned.
As you can see, when you compile the language, it requires the function type to be specified as such: -> DataType. We unfortunately didn't have enough time to implement all aspects of the compiler, specifically the type checker, so the language has to be typed since type inference is not supported, and developers must explicitly annotate all function return types. Not the end of the world.
Additionally, due to constraints in how the statement parser was implemented, class constructors are slightly unintuitive to write, requiring a block statement inside the already existing block statement to compile properly. However, with proper documentation, it is hopeful that the user will be able to adjust to this.
Some members of the team did not find Rust to be very intuitive to use. However, its pattern matching capabilities, as well as enforcement of good programming conventions, made for a much more streamlined workflow than if another systems language, such as C, was used.
Testing while developing helped fix mistakes and avoid huge scope creep.
Having our targeted language be a statically-typed one may have been easier, but overall JavaScript wasn't bad at all.
Communication was key, as figuring out who developed what had to be well defined, as misunderstanding from miscommunication did come up.\
MIT