├── .gitignore ├── .editorconfig ├── shard.yml ├── examples ├── 02-factorial.fy ├── 03-factorial-iter.fy ├── 04-class.fy ├── 05-loop-break-continue.fy ├── 06-array.fy └── 01-basic.fy ├── .github └── workflows │ └── crystal.yml ├── src ├── interpreter │ └── context.cr ├── exceptions.cr ├── fayrant.cr ├── parser │ ├── token.cr │ ├── lexer.cr │ └── parser.cr ├── value.cr └── ast │ ├── statement.cr │ └── expression.cr ├── LICENSE ├── spec ├── interpreter │ └── context_spec.cr ├── ast │ └── expression_spec.cr ├── value_spec.cr └── parser │ ├── lexer_spec.cr │ └── parser_spec.cr ├── grammar.ebnf └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | 4 | /docs/ 5 | /lib/ 6 | /bin/ 7 | /.shards/ 8 | *.dwarf 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: fayrant-lang 2 | version: 0.1.0 3 | 4 | authors: 5 | - Florian3k 6 | - MaciejWitkowskiDev 7 | - Ph0enixKM 8 | 9 | targets: 10 | fayrant: 11 | main: src/fayrant.cr 12 | 13 | crystal: 1.0.0 14 | 15 | license: MIT 16 | -------------------------------------------------------------------------------- /examples/02-factorial.fy: -------------------------------------------------------------------------------- 1 | func factorial_rec(x) { 2 | if (x < 1) { 3 | return 1; 4 | } 5 | return x * factorial_rec(x - 1); 6 | } 7 | 8 | var x = #input(); 9 | if (x == null | x < 0) { 10 | print("Invalid number"); 11 | } else { 12 | print("Rec: {factorial_rec(x)}"); 13 | } 14 | -------------------------------------------------------------------------------- /examples/03-factorial-iter.fy: -------------------------------------------------------------------------------- 1 | func factorial_iter(x) { 2 | if (x < 1) { 3 | return 1; 4 | } 5 | var res = 1; 6 | for (; x > 1; x -= 1) { 7 | res *= x; 8 | } 9 | return res; 10 | } 11 | 12 | var x = #input(); 13 | if (x == null | x < 0) { 14 | print("Invalid number"); 15 | } else { 16 | print("Iter: {factorial_iter(x)}"); 17 | } 18 | -------------------------------------------------------------------------------- /examples/04-class.fy: -------------------------------------------------------------------------------- 1 | class Example { 2 | constructor(a, b) { 3 | this.x = a; 4 | this.y = b; 5 | } 6 | 7 | func test(a, b) { 8 | return this.x * a + this.y * b; 9 | } 10 | } 11 | 12 | var ob = Example(2, 3); 13 | ob.c = Example(4, 5); 14 | ob.c.d = Example(4, 5); 15 | ob.c.d.e = 9; 16 | ob.c.d.e *= 2; 17 | print("hello {ob.c.d.e}"); 18 | print(ob.test(5, 7)); 19 | -------------------------------------------------------------------------------- /examples/05-loop-break-continue.fy: -------------------------------------------------------------------------------- 1 | for (var x = 1; x < 10; x += 1) { 2 | if (x == 4) { 3 | continue; 4 | } 5 | print("for -> {x}"); 6 | if (x == 7) { 7 | break; 8 | } 9 | } 10 | 11 | var x = 7; 12 | while (true) { 13 | if (x == 0) { 14 | break; 15 | } 16 | print("while -> {x}"); 17 | if (x == 5) { 18 | x -= 3; 19 | continue; 20 | } 21 | x -= 1; 22 | } 23 | -------------------------------------------------------------------------------- /examples/06-array.fy: -------------------------------------------------------------------------------- 1 | var arr = Array(1, 2, 3, 4); 2 | 3 | for (var i = 0; i < arr.size(); i += 1) { 4 | print("i -> {arr.get(i)}"); 5 | } 6 | 7 | arr.set(1, 7); 8 | arr.push(5); 9 | 10 | print("------"); 11 | 12 | for (var i = 0; i < arr.size(); i += 1) { 13 | print("i -> {arr.get(i)}"); 14 | } 15 | print("pop -> {arr.pop()}"); 16 | print("pop -> {arr.pop()}"); 17 | print("pop -> {arr.pop()}"); 18 | -------------------------------------------------------------------------------- /.github/workflows/crystal.yml: -------------------------------------------------------------------------------- 1 | name: Crystal CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | container: 15 | image: crystallang/crystal 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | # - name: Install dependencies 20 | # run: shards install 21 | - name: Run tests 22 | run: crystal spec 23 | -------------------------------------------------------------------------------- /src/interpreter/context.cr: -------------------------------------------------------------------------------- 1 | require "../value.cr" 2 | 3 | module FayrantLang 4 | class Context 5 | def initialize(@parentContext : Context | Nil = nil) 6 | @vars = Hash(String, AnyValue).new 7 | end 8 | 9 | def get_var(name : String) : AnyValue 10 | if @vars.has_key?(name) 11 | @vars[name] 12 | elsif ctx = @parentContext 13 | ctx.get_var name 14 | else 15 | raise UndefinedVarError.new name 16 | end 17 | end 18 | 19 | def set_var(name : String, value : AnyValue) 20 | if @vars.has_key?(name) 21 | @vars[name] = value 22 | elsif ctx = @parentContext 23 | ctx.set_var name, value 24 | else 25 | raise UndefinedVarError.new name 26 | end 27 | end 28 | 29 | def create_var(name : String, value : AnyValue) 30 | if @vars.has_key?(name) 31 | raise DefinedVarError.new name 32 | else 33 | @vars[name] = value 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Florian3k 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/exceptions.cr: -------------------------------------------------------------------------------- 1 | require "./value.cr" 2 | 3 | module FayrantLang 4 | class SyntaxError < Exception 5 | def initialize(reason : String) 6 | super reason 7 | end 8 | end 9 | 10 | abstract class ExecutionError < Exception 11 | def initialize(reason : String) 12 | super reason 13 | end 14 | end 15 | 16 | class ArityMismatchError < ExecutionError 17 | def initialize(@expected : Int32, @actual : Int32) 18 | super "ArityMismatchError: expected #{@expected} arguments, instead got #{@actual}" 19 | end 20 | end 21 | 22 | class TypeError < ExecutionError 23 | def initialize(@expected : ValueType, @actual : ValueType) 24 | super "TypeError: expected type #{@expected}, instead got #{@actual}" 25 | end 26 | 27 | def initialize(@expected : String, @actual : String) 28 | super "TypeError: expected type #{@expected}, instead got #{@actual}" 29 | end 30 | end 31 | 32 | class UndefinedVarError < ExecutionError 33 | def initialize(@name : String) 34 | super "UndefinedVarError: variable #{@name} is not defined" 35 | end 36 | end 37 | 38 | class DefinedVarError < ExecutionError 39 | def initialize(@name : String) 40 | super "DefinedVarError: variable #{@name} is already defined" 41 | end 42 | end 43 | 44 | class ArithmeticError < ExecutionError 45 | def initialize(reason : String) 46 | super "ArithmeticError: #{reason}" 47 | end 48 | end 49 | 50 | class StatementError < ExecutionError 51 | def initialize(reason : String) 52 | super "StatementError: #{reason}" 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /src/fayrant.cr: -------------------------------------------------------------------------------- 1 | require "./parser/lexer.cr" 2 | require "./parser/parser.cr" 3 | require "./interpreter/context.cr" 4 | 5 | include FayrantLang 6 | 7 | def initial_context 8 | ctx = Context.new 9 | initial_vars = [ 10 | { 11 | "print", 12 | BuiltinFunction.new 1 do |args| 13 | puts args[0].to_string 14 | NullValue.new 15 | end, 16 | }, 17 | { 18 | "input", 19 | BuiltinFunction.new 0 do |args| 20 | str = gets 21 | if str.is_a?(Nil) 22 | raise Exception.new "Reading input failed" 23 | end 24 | StringValue.new str 25 | end, 26 | }, 27 | { 28 | "Array", 29 | BuiltinFunction.new -1 do |args| 30 | ArrayObjectValue.new args 31 | end, 32 | }, 33 | ] 34 | 35 | initial_vars.each do |name_var| 36 | ctx.create_var(name_var[0], name_var[1]) 37 | end 38 | 39 | ctx 40 | end 41 | 42 | case ARGV.size 43 | when 0 44 | ctx = initial_context 45 | loop do 46 | input = gets 47 | if input == ".quit" || input.is_a?(Nil) 48 | break 49 | end 50 | program = Parser.new(Lexer.new(input).scan_tokens).parse_program 51 | program.each do |statement| 52 | statement.exec(ctx) 53 | end 54 | end 55 | when 1 56 | ctx = initial_context 57 | input = File.read(ARGV[0]) 58 | program = Parser.new(Lexer.new(input).scan_tokens).parse_program 59 | program.each do |statement| 60 | statement.exec(ctx) 61 | end 62 | else 63 | puts "Error: Too many arguents" 64 | puts " Use: fayrant - to run interactive repl" 65 | puts " Use: fayrant - to run file.fy" 66 | end 67 | -------------------------------------------------------------------------------- /spec/interpreter/context_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../src/interpreter/context.cr" 3 | 4 | include FayrantLang 5 | 6 | describe "FayrantLang Context" do 7 | it "should return created variable" do 8 | ctx = Context.new 9 | 10 | ctx.create_var "asdf", NumberValue.new(7) 11 | (ctx.get_var("asdf") == NumberValue.new(7)).should eq true 12 | 13 | ctx.set_var "asdf", NumberValue.new(8) 14 | (ctx.get_var("asdf") == NumberValue.new(8)).should eq true 15 | end 16 | 17 | it "should return variable from parent context" do 18 | parent = Context.new 19 | parent.create_var "asdf", NumberValue.new(7) 20 | ctx = Context.new parent 21 | 22 | (ctx.get_var("asdf") == NumberValue.new(7)).should eq true 23 | end 24 | 25 | it "should return most recent variable in context" do 26 | parent = Context.new 27 | ctx = Context.new parent 28 | 29 | parent.create_var "asdf", NumberValue.new(7) 30 | ctx.create_var "asdf", NumberValue.new(8) 31 | 32 | (ctx.get_var("asdf") == NumberValue.new(8)).should eq true 33 | end 34 | 35 | it "should override only most recent variable in context" do 36 | parent = Context.new 37 | ctx = Context.new parent 38 | 39 | parent.create_var "asdf", NumberValue.new(7) 40 | ctx.create_var "asdf", NumberValue.new(8) 41 | 42 | (parent.get_var("asdf") == NumberValue.new(7)).should eq true 43 | (ctx.get_var("asdf") == NumberValue.new(8)).should eq true 44 | 45 | ctx.set_var "asdf", NumberValue.new(9) 46 | 47 | (parent.get_var("asdf") == NumberValue.new(7)).should eq true 48 | (ctx.get_var("asdf") == NumberValue.new(9)).should eq true 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/parser/token.cr: -------------------------------------------------------------------------------- 1 | module FayrantLang 2 | enum TokenType 3 | FUNC 4 | CLASS 5 | CONSTRUCTOR 6 | IF 7 | ELSE 8 | WHILE 9 | FOR 10 | VAR 11 | RETURN 12 | BREAK 13 | CONTINUE 14 | 15 | IDENTIFIER 16 | 17 | L_BRACE 18 | R_BRACE 19 | L_PAREN 20 | R_PAREN 21 | DOT 22 | COMMA 23 | SEMICOLON 24 | QUOTE 25 | 26 | EQUAL 27 | EQUAL_PLUS 28 | EQUAL_MINUS 29 | EQUAL_TIMES 30 | EQUAL_DIV 31 | EQUAL_DIV_INV 32 | EQUAL_MOD 33 | EQUAL_EXPT 34 | EQUAL_AND 35 | EQUAL_OR 36 | EQUAL_CONCAT 37 | 38 | OP_MINUS # unary and binary 39 | OP_NEG # unary 40 | OP_TO_STR # unary 41 | OP_TO_NUM # unary 42 | OP_PLUS 43 | OP_TIMES 44 | OP_DIV 45 | OP_DIV_INV 46 | OP_MOD 47 | OP_EXPT 48 | OP_AND 49 | OP_OR 50 | OP_GT 51 | OP_LT 52 | OP_GE 53 | OP_LE 54 | OP_EQ 55 | OP_NEQ 56 | OP_CONCAT 57 | 58 | STRING_FRAGMENT 59 | NUMBER 60 | TRUE 61 | FALSE 62 | NULL 63 | end 64 | 65 | class Token 66 | getter type 67 | getter lexeme 68 | getter loc 69 | 70 | def initialize(@type : TokenType, @lexeme : String, @loc : Location) 71 | end 72 | 73 | def ==(other : Token) 74 | type == other.type && lexeme == other.lexeme && loc == other.loc 75 | end 76 | end 77 | 78 | class Location 79 | getter line 80 | getter from 81 | getter to 82 | 83 | def initialize(@line : Int32, @from : Int32, @to : Int32) 84 | end 85 | 86 | def ==(other : Location) 87 | line == other.line && from == other.from && to == other.to 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/ast/expression_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../src/ast/expression.cr" 3 | require "../../src/interpreter/context.cr" 4 | 5 | include FayrantLang 6 | include AST 7 | 8 | describe "FayrantLang::AST Expressions" do 9 | describe "LiteralExpr" do 10 | describe "BooleanLiteralExpr" do 11 | it "two BooleanLiteralExpr should be comparable" do 12 | ex1 = BooleanLiteralExpr.new true 13 | ex2 = BooleanLiteralExpr.new true 14 | ex3 = BooleanLiteralExpr.new false 15 | ex4 = BooleanLiteralExpr.new false 16 | 17 | ex1.should eq ex1 18 | ex1.should eq ex2 19 | ex2.should_not eq ex3 20 | ex3.should eq ex4 21 | ex4.should eq ex4 22 | end 23 | 24 | it "BooleanLiteralExpr should evaluate to BooleanValue" do 25 | ex1 = BooleanLiteralExpr.new true 26 | ex2 = BooleanLiteralExpr.new false 27 | 28 | ex1.eval(Context.new).should eq BooleanValue.new true 29 | ex1.eval(Context.new).should_not eq BooleanValue.new false 30 | ex2.eval(Context.new).should eq BooleanValue.new false 31 | end 32 | end 33 | 34 | describe "NumberLiteralExpr" do 35 | it "two NumberLiteralExpr should be comparable" do 36 | ex1 = NumberLiteralExpr.new 3 37 | ex2 = NumberLiteralExpr.new 3 38 | ex3 = NumberLiteralExpr.new 7 39 | ex4 = NumberLiteralExpr.new 7 40 | 41 | ex1.should eq ex1 42 | ex1.should eq ex2 43 | ex2.should_not eq ex3 44 | ex3.should eq ex4 45 | ex4.should eq ex4 46 | end 47 | 48 | it "NumberLiteralExpr should evaluate to NumberValue" do 49 | ex1 = NumberLiteralExpr.new 3 50 | ex2 = NumberLiteralExpr.new 7 51 | 52 | ex1.eval(Context.new).should eq NumberValue.new 3 53 | ex1.eval(Context.new).should_not eq NumberValue.new 7 54 | ex2.eval(Context.new).should eq NumberValue.new 7 55 | end 56 | end 57 | 58 | describe "NullLiteralExpr" do 59 | it "two NullLiteralExpr should be equal" do 60 | ex1 = NullLiteralExpr.new 61 | ex2 = NullLiteralExpr.new 62 | 63 | ex1.should eq ex2 64 | end 65 | 66 | it "NullLiteralExpr should evaluate to NullValue" do 67 | ex = NullLiteralExpr.new 68 | ex.eval(Context.new).should eq NullValue.new 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /examples/01-basic.fy: -------------------------------------------------------------------------------- 1 | ~ this is comment 2 | 3 | ~ variable declaration 4 | var something; 5 | var something2 = 123; 6 | 7 | ~ simple types 8 | something = null; 9 | 10 | something = true; 11 | something = false; 12 | 13 | something = 1234; ~ all numbers are 64-bit floating point (double) 14 | something = 3.14159; 15 | something = 0b100001011001; ~ binary literal 16 | something = 0xDeadC0de; ~ hexadecimal literal 17 | 18 | something = "simple string"; 19 | something = "string with \n \t \\ \" \{ \} escapes"; 20 | something = "string with unicode escapes \u{65} \u{0x41}"; 21 | something = "string with interpolation { 2 + 3 + something2 }"; 22 | something = "string { "with { "nested" } interpolation" }"; 23 | 24 | ~ unary and binary operators 25 | something = !something; 26 | something = -something; 27 | something = @1234; ~ conversion to string 28 | something = #"1234"; ~ conversion to number 29 | 30 | ~ arithmetic 31 | something = 1 + 2 - 3; 32 | something = 1 * 2 + 3 / 4; 33 | something = 4 \ 3; ~ same as 3 / 4; 34 | something = 2 ^ 3 ^ 4; ~ exponentiation 35 | something = 5 % 3; 36 | 37 | ~ comparison 38 | something = 2 < 3; 39 | something = 2 > 3; 40 | something = 2 <= 3; 41 | something = 2 >= 3; 42 | 43 | something = 2 == 3; 44 | something = 2 != 3; 45 | something = "asd" == "def"; 46 | something = "asd" == 3; 47 | something = "asd" == null; 48 | 49 | something = "Hello " ++ "world!"; ~ string concatenation 50 | 51 | ~ logical 52 | something = true & false; 53 | something = true | false; 54 | 55 | ~ assignment operators 56 | something = 1; 57 | something += 2; 58 | something -= 3; 59 | something *= 4; 60 | something /= 5; 61 | something \= 6; 62 | something %= 7; 63 | something ^= 8; 64 | something &= true; 65 | something |= false; 66 | something ++= "asdf"; 67 | 68 | ~ basic io 69 | something = input(); ~ returns string 70 | print(something); ~ prints anything 71 | 72 | ~ control flow 73 | 74 | if (something > 5) { 75 | print("hello"); 76 | } 77 | 78 | if (something == 7) { 79 | print("seven"); 80 | } else { 81 | print("not seven"); 82 | } 83 | 84 | while (something > 0) { 85 | something -= 1; 86 | } 87 | 88 | for (var i = 0; i < 3; i += 1) { 89 | print(i); 90 | } 91 | 92 | while (true) { 93 | if (true) { 94 | break; 95 | } 96 | } 97 | 98 | for (var i = 0; i < 3; i += 1) { 99 | if (i == 2) { 100 | continue; 101 | } 102 | print(i); 103 | } -------------------------------------------------------------------------------- /grammar.ebnf: -------------------------------------------------------------------------------- 1 | # Entrypoint 2 | program = { statement } 3 | 4 | 5 | statement = globalStatement 6 | | localStatement 7 | 8 | globalStatement = functionDeclaration 9 | | classDeclaration 10 | | localStatement 11 | 12 | localStatement = ifStatement 13 | | whileStatement 14 | | forStatement 15 | | variableStatement 16 | | assignmentStatement 17 | | returnStatement 18 | | breakStatement 19 | | continueStatement 20 | | expressionStatement 21 | 22 | 23 | anyBody = "{" { localStatement } "}" 24 | 25 | 26 | functionDeclaration = "func" IDENTIFIER "(" paramsList ")" anyBody 27 | 28 | paramsList = [ IDENTIFIER { "," IDENTIFIER } ] 29 | 30 | 31 | classDeclaration = "class" IDENTIFIER "{" constructorDeclaration { methodDeclaration } "}" 32 | 33 | constructorDeclaration = "constructor" "(" paramsList ")" anyBody 34 | 35 | methodDeclaration = functionDeclaration 36 | 37 | 38 | ifStatement = "if" "(" expression ")" anyBody [ "else" anyBody ] 39 | 40 | 41 | whileStatement = "while" "(" expression ")" anyBody 42 | 43 | 44 | forInit = variableStatement 45 | | assignmentStatement 46 | | expressionStatement 47 | 48 | forIncrement = assignmentStatementNoSemicolon 49 | | expression 50 | 51 | forStatement = "for" "(" forInit expression ";" forIncrement ")" anyBody 52 | 53 | 54 | variableStatement = "var" IDENTIFIER "=" expression ";" 55 | 56 | 57 | assignmentStatement = assignmentStatementNoSemicolon ";" 58 | assignmentStatementNoSemicolon = IDENTIFIER { "." IDENTIFIER } ASSIGNMENT_OP expression 59 | 60 | ASSIGNMENT_OP = "=" | "+=" | "-=" | "*=" | "/=" | "\\=" | "%=" | "^=" | "&=" | "|=" | "++=" | 61 | 62 | 63 | returnStatement = "return" [ expression ] ";" 64 | 65 | breakStatement = "break" ";" 66 | 67 | continueStatement = "continue" ";" 68 | 69 | expressionStatement = expression ";" 70 | 71 | 72 | expression = literalExpression 73 | | variableExpression 74 | | groupingExpression 75 | | unaryExpression 76 | | binaryExpression 77 | | functionCallExpression 78 | | objectAccessExpression 79 | 80 | variableExpression = IDENTIFIER 81 | 82 | groupingExpression = "(" expression ")" 83 | 84 | literalExpression = NUMBER | STRING | BOOLEAN | NULL 85 | 86 | unaryExpression = UN_OP expression 87 | 88 | UN_OP = "-" | "!" | "@" | "#" 89 | 90 | binaryExpression = expression BIN_OP expression 91 | 92 | BIN_OP = "+" | "-" | "*" | "/" | "\\" | "%" | "^" | "&" | "|" | ">" | "<" | "<=" | ">=" | "==" | "!=" | "++" 93 | 94 | functionCallExpression = expression "(" callParamsList ")" 95 | 96 | callParamsList = [ expression { "," expression } ] 97 | 98 | objectAccessExpression = expression "." IDENTIFIER 99 | 100 | 101 | IDENTIFIER = ID_ALPHA { (ID_ALPHA | DIGIT_10 | ANY_UNICODE_EMOJI) } 102 | ID_ALPHA = LETTER | "_" 103 | 104 | NUMBER = NUMBER_2 | NUMBER_10 | NUMBER_16 105 | 106 | NUMBER_2 = "0b" DIGIT_2 { DIGIT_2 } 107 | NUMBER_10 = NUMBER_10_INT [ "." NUMBER_10_INT ] 108 | NUMBER_10_INT = DIGIT_10 { DIGIT_10 } 109 | NUMBER_16 = "0x" DIGIT_16 { DIGIT_16 } 110 | 111 | DIGIT_2 = "0" | "1" 112 | DIGIT_10 = DIGIT_2 | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" 113 | DIGIT_16 = DIGIT_10 | "a" | "b" | "c" | "d" | "e" | "f" | "A" | "B" | "C" | "D" | "E" | "F" 114 | 115 | STRING = "\"" STRING_ELEM "\"" 116 | 117 | STRING_ELEM = ALLOWED_CHARACTER | INTERPOLATION | ESCAPE_SEQUENCE 118 | 119 | ALLOWED_CHARACTER = ... # any unicode character except for: \ " { } 120 | 121 | INTERPOLATION = "{" expression "}" 122 | 123 | ESCAPE_SEQUENCE = "\\" ESCAPE_CONTENT 124 | ESCAPE_CONTENT = "r" | "n" | "\\" | "\"" | "{" | "}" 125 | | ( "u{" ( NUMBER_2 | NUMBER_10_INT | NUMBER_16 ) "}" ) 126 | 127 | BOOLEAN = "true" | "false" 128 | 129 | NULL = null 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎩 Fayrant-lang 2 | ### Simple, interpreted, dynamically-typed programming language 3 | 4 | Authors: 5 | - Mikołaj Fornal [@Florian3k](https://github.com/Florian3k) 6 | - Paweł Karaś [@Ph0enixKM](https://github.com/Ph0enixKM) 7 | - Maciej Witkowski [@MaciejWitkowskiDev](https://github.com/MaciejWitkowskiDev) 8 | 9 | ## Basic syntax 10 | 11 | ```= 12 | ~ this is comment 13 | 14 | ~ variable declaration 15 | var something; 16 | var something2 = 123; 17 | 18 | ~ simple types 19 | something = null; 20 | 21 | something = true; 22 | something = false; 23 | 24 | something = 1234; ~ all numbers are 64-bit floating point (double) 25 | something = 3.14159; 26 | something = 0b100001011001; ~ binary literal 27 | something = 0xDeadC0de; ~ hexadecimal literal 28 | 29 | something = "simple string"; 30 | something = "string with \n \t \\ \" \{ \} escapes"; 31 | something = "string with unicode escapes \u{65} \u{0x41}"; 32 | something = "string with interpolation { 2 + 3 + something2 }"; 33 | something = "string { "with { "nested" } interpolation" }"; 34 | 35 | ~ unary and binary operators 36 | something = !something; 37 | something = -something; 38 | something = @1234; ~ conversion to string 39 | something = #"1234"; ~ conversion to number 40 | 41 | ~ arithmetic 42 | something = 1 + 2 - 3; 43 | something = 1 * 2 + 3 / 4; 44 | something = 4 \ 3; ~ same as 3 / 4; 45 | something = 2 ^ 3 ^ 4; ~ exponentiation 46 | something = 5 % 3; 47 | 48 | ~ comparison 49 | something = 2 < 3; 50 | something = 2 > 3; 51 | something = 2 <= 3; 52 | something = 2 >= 3; 53 | 54 | something = 2 == 3; 55 | something = 2 != 3; 56 | something = "asd" == "def"; 57 | something = "asd" == 3; 58 | something = "asd" == null; 59 | 60 | something = "Hello " ++ "world!"; ~ string concatenation 61 | 62 | ~ logical 63 | something = true & false; 64 | something = true | false; 65 | 66 | ~ assignment operators 67 | something = 1; 68 | something += 2; 69 | something -= 3; 70 | something *= 4; 71 | something /= 5; 72 | something \= 6; 73 | something %= 7; 74 | something ^= 8; 75 | something &= true; 76 | something |= false; 77 | something ++= "asdf"; 78 | 79 | ~ basic io 80 | something = input(); ~ returns string 81 | print(something); ~ prints anything 82 | 83 | ~ control flow 84 | 85 | if (something > 5) { 86 | print("hello"); 87 | } 88 | 89 | if (something == 7) { 90 | print("seven"); 91 | } else { 92 | print("not seven"); 93 | } 94 | 95 | while (something > 0) { 96 | something -= 1; 97 | } 98 | 99 | for (var i = 0; i < 3; i += 1) { 100 | print(i); 101 | } 102 | 103 | while (true) { 104 | if (true) { 105 | break; 106 | } 107 | } 108 | 109 | for (var i = 0; i < 3; i += 1) { 110 | if (i == 2) { 111 | continue; 112 | } 113 | print(i); 114 | } 115 | ``` 116 | 117 | ### Functions example 118 | ```= 119 | func factorial_iter(x) { 120 | if (x < 1) { 121 | return 1; 122 | } 123 | var res = 1; 124 | while (x > 1) { 125 | res *= x; 126 | x -= 1; 127 | } 128 | return res; 129 | } 130 | 131 | func factorial_rec(x) { 132 | if (x < 1) { 133 | return 1; 134 | } 135 | return x * factorial_rec(x - 1); 136 | } 137 | 138 | var x = #input(); 139 | if (x == null | x < 1) { 140 | print("Invalid number"); 141 | return; 142 | } 143 | print("Iter: {factorial_iter(x)}"); 144 | print("Rec: {factorial_rec(x)}"); 145 | ``` 146 | 147 | ### Class example 148 | ```= 149 | class Vec2d { 150 | constructor(x, y) { 151 | this.x = x; 152 | this.y = y; 153 | } 154 | 155 | func plus(other) { 156 | return Vec2d(this.x + other.x, this.y + other.y); 157 | } 158 | 159 | func norm() { 160 | return (this.x ^ 2 + this.y ^ 2) ^ 0.5; 161 | } 162 | 163 | func str() { 164 | return "Vec[{this.x},{this.y}]"; 165 | } 166 | } 167 | 168 | var v1 = Vec2d(3, 4); 169 | var v2 = Vec2d(4, 5); 170 | var v3 = v1.plus(v2); 171 | 172 | print("{v1.str()} + {v2.str()} = {v2.str()}") 173 | ``` 174 | 175 | ## FAQ: 176 | 177 | ### Why Fayrant name? 178 | It's a tribute to this outstanding beverage 179 | 180 | ![](https://i.imgur.com/7Ni6osS.png) 181 | -------------------------------------------------------------------------------- /spec/value_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/value.cr" 3 | 4 | include FayrantLang 5 | 6 | describe "FayrantLang Values" do 7 | describe "NullValue" do 8 | it ".get* should throw TypeError" do 9 | expect_raises(TypeError, "TypeError: expected type Boolean, instead got Null") do 10 | NullValue.new.get_boolean 11 | end 12 | 13 | expect_raises(TypeError, "TypeError: expected type Number, instead got Null") do 14 | NullValue.new.get_number 15 | end 16 | 17 | expect_raises(TypeError, "TypeError: expected type String, instead got Null") do 18 | NullValue.new.get_string 19 | end 20 | 21 | expect_raises(TypeError, "TypeError: expected type Object, instead got Null") do 22 | NullValue.new.get_object 23 | end 24 | 25 | expect_raises(TypeError, "TypeError: expected type Function, instead got Null") do 26 | NullValue.new.get_function 27 | end 28 | end 29 | end 30 | 31 | describe "BooleanValue" do 32 | it ".get_boolean should return given boolean" do 33 | bool = BooleanValue.new true 34 | bool.get_boolean.should eq true 35 | 36 | bool2 = BooleanValue.new false 37 | bool2.get_boolean.should eq false 38 | end 39 | 40 | it ".type should return Boolean type" do 41 | bool = BooleanValue.new true 42 | bool.type.should eq ValueType::Boolean 43 | end 44 | 45 | it ".get_number should throw TypeError" do 46 | expect_raises(TypeError, "TypeError: expected type Number, instead got Boolean") do 47 | bool = BooleanValue.new true 48 | bool.get_number 49 | end 50 | end 51 | end 52 | 53 | describe "NumberValue" do 54 | it ".get_number should return given number" do 55 | num = NumberValue.new 7 56 | num.get_number.should eq 7 57 | end 58 | 59 | it ".type should return Number type" do 60 | num = NumberValue.new 7 61 | num.type.should eq ValueType::Number 62 | end 63 | end 64 | 65 | describe "StringValue" do 66 | it ".get_string should return given string" do 67 | str = StringValue.new "testing" 68 | str.get_string.should eq "testing" 69 | end 70 | 71 | it ".type should return String type" do 72 | str = StringValue.new "testing" 73 | str.type.should eq ValueType::String 74 | end 75 | end 76 | 77 | describe "ObjectValue" do 78 | empty_methods = Hash(String, FunctionDeclarationStatement).new 79 | empty_ctx = Context.new 80 | it ".get_object should return the object itself" do 81 | obj = ObjectValue.new "SomeClass", empty_methods, empty_ctx 82 | (obj.get_object == obj).should eq true 83 | end 84 | 85 | it ".type should return Object type" do 86 | obj = ObjectValue.new "SomeClass", empty_methods, empty_ctx 87 | obj.type.should eq ValueType::Object 88 | end 89 | 90 | it "Object should be equal only to itself" do 91 | obj1 = ObjectValue.new "SomeClass", empty_methods, empty_ctx 92 | obj2 = ObjectValue.new "SomeClass", empty_methods, empty_ctx 93 | (obj1 == obj1).should eq true 94 | (obj2 == obj2).should eq true 95 | (obj1 == obj2).should eq false 96 | end 97 | end 98 | 99 | describe "BuiltinFunction" do 100 | it ".get_function should return the function itself" do 101 | fn = BuiltinFunction.new 0 do |args| 102 | NullValue.new 103 | end 104 | (fn.get_function == fn).should eq true 105 | end 106 | 107 | it "Function should be equal only to itself" do 108 | fn1 = BuiltinFunction.new 0 do |args| 109 | NullValue.new 110 | end 111 | fn2 = BuiltinFunction.new 0 do |args| 112 | NullValue.new 113 | end 114 | (fn1 == fn1).should eq true 115 | (fn2 == fn2).should eq true 116 | (fn1 == fn2).should eq false 117 | end 118 | 119 | it ".call should return correct value" do 120 | fn = BuiltinFunction.new 1 do |args| 121 | NumberValue.new(args[0].get_number + 7) 122 | end 123 | ret = fn.call([NumberValue.new 6] of AnyValue) 124 | (ret == NumberValue.new 13).should eq true 125 | (ret == NumberValue.new 14).should eq false 126 | end 127 | 128 | it ".call should throw on arity mismatch" do 129 | fn = BuiltinFunction.new 1 do |args| 130 | NumberValue.new(args[0].get_number + 7) 131 | end 132 | expect_raises(ArityMismatchError, "ArityMismatchError: expected 1 arguments, instead got 0") do 133 | ret = fn.call([] of AnyValue) 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/parser/lexer_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../src/parser/lexer.cr" 3 | 4 | include FayrantLang 5 | 6 | describe "FayrantLang Lexer" do 7 | it "should tokenize 'var test = 2 + 3.14;'" do 8 | input = "var test = 2 + 3.14;" 9 | # 0123456789 10 | # 0123456789 11 | result = Lexer.new(input).scan_tokens 12 | expected = [ 13 | Token.new(TokenType::VAR, "var", Location.new 1, 0, 2), 14 | Token.new(TokenType::IDENTIFIER, "test", Location.new 1, 4, 7), 15 | Token.new(TokenType::EQUAL, "=", Location.new 1, 9, 9), 16 | Token.new(TokenType::NUMBER, "2", Location.new 1, 11, 11), 17 | Token.new(TokenType::OP_PLUS, "+", Location.new 1, 13, 13), 18 | Token.new(TokenType::NUMBER, "3.14", Location.new 1, 15, 18), 19 | Token.new(TokenType::SEMICOLON, ";", Location.new 1, 19, 19), 20 | ] 21 | result.zip?(expected) do |res, exp| 22 | res.should eq exp 23 | end 24 | end 25 | 26 | it "should tokenize '(2 + 2) ++ 0xDEADBEEF;'" do 27 | input = "(2 + 2) ++ 0xDEADBEEF;" 28 | # 0123456789 01 29 | # 0123456789 30 | result = Lexer.new(input).scan_tokens 31 | expected = [ 32 | Token.new(TokenType::L_PAREN, "(", Location.new 1, 0, 0), 33 | Token.new(TokenType::NUMBER, "2", Location.new 1, 1, 1), 34 | Token.new(TokenType::OP_PLUS, "+", Location.new 1, 3, 3), 35 | Token.new(TokenType::NUMBER, "2", Location.new 1, 5, 5), 36 | Token.new(TokenType::R_PAREN, ")", Location.new 1, 6, 6), 37 | Token.new(TokenType::OP_CONCAT, "++", Location.new 1, 8, 9), 38 | Token.new(TokenType::NUMBER, "0xDEADBEEF", Location.new 1, 11, 20), 39 | Token.new(TokenType::SEMICOLON, ";", Location.new 1, 21, 21), 40 | ] 41 | result.zip?(expected) do |res, exp| 42 | res.should eq exp 43 | end 44 | end 45 | 46 | # multiline code 47 | 48 | it "should tokenize '\"test\";'" do 49 | input = "\"test\";" 50 | # 01234 56 51 | result = Lexer.new(input).scan_tokens 52 | expected = [ 53 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 0, 0), 54 | Token.new(TokenType::STRING_FRAGMENT, "test", Location.new 1, 1, 4), 55 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 5, 5), 56 | Token.new(TokenType::SEMICOLON, ";", Location.new 1, 6, 6), 57 | ] 58 | result.zip?(expected) do |res, exp| 59 | res.should eq exp 60 | end 61 | end 62 | 63 | it "should tokenize '\"te\\n\\\\st\";'" do 64 | input = "\"te\\n\\\\st\";" 65 | # 012 34 5 678 90 66 | result = Lexer.new(input).scan_tokens 67 | expected = [ 68 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 0, 0), 69 | Token.new(TokenType::STRING_FRAGMENT, "te\n\\st", Location.new 1, 1, 8), 70 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 9, 9), 71 | Token.new(TokenType::SEMICOLON, ";", Location.new 1, 10, 10), 72 | ] 73 | result.zip?(expected) do |res, exp| 74 | res.should eq exp 75 | end 76 | end 77 | 78 | it "should tokenize '\"test1{ \"test2{\"test3\" }test4\"} test5\"'" do 79 | input = "\"test1{ \"test2{\"test3\" }test4\"} test5\"" 80 | # 01234567 89 0 12345678 9 81 | # 01234 56789 0123456 7 82 | result = Lexer.new(input).scan_tokens 83 | expected = [ 84 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 0, 0), 85 | Token.new(TokenType::STRING_FRAGMENT, "test1", Location.new 1, 1, 5), 86 | Token.new(TokenType::L_BRACE, "{", Location.new 1, 6, 6), 87 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 8, 8), 88 | Token.new(TokenType::STRING_FRAGMENT, "test2", Location.new 1, 9, 13), 89 | Token.new(TokenType::L_BRACE, "{", Location.new 1, 14, 14), 90 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 15, 15), 91 | Token.new(TokenType::STRING_FRAGMENT, "test3", Location.new 1, 16, 20), 92 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 21, 21), 93 | Token.new(TokenType::R_BRACE, "}", Location.new 1, 23, 23), 94 | Token.new(TokenType::STRING_FRAGMENT, "test4", Location.new 1, 24, 28), 95 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 29, 29), 96 | Token.new(TokenType::R_BRACE, "}", Location.new 1, 30, 30), 97 | Token.new(TokenType::STRING_FRAGMENT, " test5", Location.new 1, 31, 36), 98 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 37, 37), 99 | ] 100 | result.zip?(expected) do |res, exp| 101 | res.should eq exp 102 | end 103 | end 104 | 105 | it "should tokenize '\"This is scary, \\u{65}\\u{0x41}\\u{0b01000001}!\";'" do 106 | input = "\"This is scary, \\u{65}\\u{0x41}\\u{0b01000001}!\";" 107 | result = Lexer.new(input).scan_tokens 108 | expected = [ 109 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 0, 0), 110 | Token.new(TokenType::STRING_FRAGMENT, "This is scary, AAA!", Location.new 1, 1, 44), 111 | Token.new(TokenType::QUOTE, "\"", Location.new 1, 45, 45), 112 | Token.new(TokenType::SEMICOLON, ";", Location.new 1, 46, 46), 113 | ] 114 | result.zip?(expected) do |res, exp| 115 | res.should eq exp 116 | end 117 | end 118 | 119 | it "should tokenize simple tokens" do 120 | lexer = Lexer.new "" 121 | lexer.test_single_token("va").should eq TokenType::IDENTIFIER 122 | lexer.test_single_token("var").should eq TokenType::VAR 123 | lexer.test_single_token("vars").should eq TokenType::IDENTIFIER 124 | 125 | lexer.test_single_token("++").should eq TokenType::OP_CONCAT 126 | lexer.test_single_token("+").should eq TokenType::OP_PLUS 127 | 128 | lexer.test_single_token("=").should eq TokenType::EQUAL 129 | lexer.test_single_token("==").should eq TokenType::OP_EQ 130 | 131 | lexer.test_single_token("!").should eq TokenType::OP_NEG 132 | lexer.test_single_token("!=").should eq TokenType::OP_NEQ 133 | 134 | lexer.test_single_token("<").should eq TokenType::OP_LT 135 | lexer.test_single_token("<=").should eq TokenType::OP_LE 136 | 137 | lexer.test_single_token(">").should eq TokenType::OP_GT 138 | lexer.test_single_token(">=").should eq TokenType::OP_GE 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /src/value.cr: -------------------------------------------------------------------------------- 1 | require "uuid" 2 | require "./exceptions" 3 | 4 | module FayrantLang 5 | enum ValueType 6 | Null 7 | Boolean 8 | Number 9 | String 10 | Object 11 | Function 12 | end 13 | 14 | abstract class AnyValue 15 | getter type 16 | 17 | def initialize(@type : ValueType) 18 | end 19 | 20 | abstract def to_string 21 | 22 | def get_boolean : Bool 23 | raise TypeError.new ValueType::Boolean, @type 24 | end 25 | 26 | def get_number : Float64 27 | raise TypeError.new ValueType::Number, @type 28 | end 29 | 30 | def get_string : String 31 | raise TypeError.new ValueType::String, @type 32 | end 33 | 34 | def get_object : ObjectValue 35 | raise TypeError.new ValueType::Object, @type 36 | end 37 | 38 | def get_function : FunctionValue 39 | raise TypeError.new ValueType::Function, @type 40 | end 41 | 42 | def ==(other) 43 | false 44 | end 45 | end 46 | 47 | class NullValue < AnyValue 48 | def initialize 49 | super ValueType::Null 50 | end 51 | 52 | def to_string 53 | "null" 54 | end 55 | 56 | def ==(other : NullValue) 57 | true 58 | end 59 | end 60 | 61 | class BooleanValue < AnyValue 62 | getter value 63 | 64 | def initialize(@value : Bool) 65 | super ValueType::Boolean 66 | end 67 | 68 | def to_string 69 | @value.to_s 70 | end 71 | 72 | def get_boolean 73 | @value 74 | end 75 | 76 | def ==(other : BooleanValue) 77 | @value == other.value 78 | end 79 | end 80 | 81 | class NumberValue < AnyValue 82 | getter value 83 | 84 | def initialize(@value : Float64) 85 | super ValueType::Number 86 | end 87 | 88 | def to_string 89 | @value.to_s 90 | end 91 | 92 | def get_number 93 | @value 94 | end 95 | 96 | def ==(other : NumberValue) 97 | @value == other.value 98 | end 99 | end 100 | 101 | class StringValue < AnyValue 102 | getter value 103 | 104 | def initialize(@value : String) 105 | super ValueType::String 106 | end 107 | 108 | def to_string 109 | @value 110 | end 111 | 112 | def get_string 113 | @value 114 | end 115 | 116 | def ==(other : StringValue) 117 | @value == other.value 118 | end 119 | end 120 | 121 | class ObjectValue < AnyValue 122 | getter fields 123 | getter uuid 124 | 125 | def initialize( 126 | @className : String, 127 | @methods : Hash(String, FunctionDeclarationStatement), 128 | @methods_ctx : Context 129 | ) 130 | super ValueType::Object 131 | @native_methods = Hash(String, BuiltinFunction).new 132 | @fields = Hash(String, AnyValue).new 133 | @uuid = UUID.random 134 | end 135 | 136 | def get_field(name : String) 137 | if fields.has_key?(name) 138 | fields[name] 139 | elsif @native_methods.has_key?(name) 140 | @native_methods[name] 141 | elsif @methods.has_key?(name) 142 | method = @methods[name] 143 | obj_fn = BuiltinFunction.new method.params.size do |args| 144 | obj_ctx = Context.new @methods_ctx 145 | fn = UserFunction.new method.params, method.body, obj_ctx 146 | fn.call(args) 147 | end 148 | obj_fn 149 | else 150 | NullValue.new 151 | end 152 | end 153 | 154 | def set_field(name : String, value : AnyValue) 155 | fields[name] = value 156 | end 157 | 158 | def to_string 159 | "[object #{@className}]" 160 | end 161 | 162 | def get_object 163 | self 164 | end 165 | 166 | def ==(other : ObjectValue) 167 | uuid == other.uuid 168 | end 169 | end 170 | 171 | class ArrayObjectValue < ObjectValue 172 | def initialize(@array : Array(AnyValue)) 173 | super "Array", Hash(String, FunctionDeclarationStatement).new, Context.new 174 | @native_methods = { 175 | "size" => BuiltinFunction.new 0 do |args| 176 | NumberValue.new @array.size.to_f 177 | end, 178 | "get" => BuiltinFunction.new 1 do |args| 179 | array[args[0].get_number.to_i] 180 | end, 181 | "set" => BuiltinFunction.new 2 do |args| 182 | array[args[0].get_number.to_i] = args[1] 183 | NullValue.new 184 | end, 185 | "push" => BuiltinFunction.new 1 do |args| 186 | array.push(args[0]) 187 | NullValue.new 188 | end, 189 | "pop" => BuiltinFunction.new 0 do |args| 190 | array.pop 191 | end, 192 | } 193 | end 194 | end 195 | 196 | abstract class FunctionValue < AnyValue 197 | getter arity 198 | getter uuid 199 | 200 | def initialize(@arity : Int32) 201 | super ValueType::Function 202 | @uuid = UUID.random 203 | end 204 | 205 | def get_function 206 | self 207 | end 208 | 209 | def to_string 210 | "[Function]" 211 | end 212 | 213 | def ==(other : FunctionValue) 214 | uuid == other.uuid 215 | end 216 | 217 | abstract def call(args : Array(AnyValue)) : AnyValue 218 | end 219 | 220 | class UserFunction < FunctionValue 221 | def initialize(@params : Array(String), @body : Array(Statement), @ctx : Context) 222 | super params.size 223 | end 224 | 225 | def call(args : Array(AnyValue)) : AnyValue 226 | fn_ctx = Context.new @ctx 227 | @params.zip(args) do |param, value| 228 | fn_ctx.create_var(param, value) 229 | end 230 | res = exec_body @body, fn_ctx 231 | case res[0] 232 | when ExecResult::NONE 233 | NullValue.new 234 | when ExecResult::RETURN 235 | res[1] 236 | when ExecResult::BREAK 237 | raise StatementError.new "break is not allowed outside of loop" 238 | when ExecResult::CONTINUE 239 | raise StatementError.new "continue is not allowed outside of loop" 240 | else 241 | raise Exception.new "UNREACHABLE CODE" 242 | end 243 | end 244 | end 245 | 246 | class BuiltinFunction < FunctionValue 247 | def initialize(arity : Int32, &body : Array(AnyValue) -> AnyValue) 248 | super arity 249 | @body = body 250 | end 251 | 252 | def call(args : Array(AnyValue)) : AnyValue 253 | if @arity != -1 && @arity != args.size 254 | raise ArityMismatchError.new @arity, args.size 255 | end 256 | @body.call(args) 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /src/ast/statement.cr: -------------------------------------------------------------------------------- 1 | require "./expression.cr" 2 | 3 | module FayrantLang 4 | module AST 5 | enum ExecResult 6 | NONE 7 | RETURN 8 | BREAK 9 | CONTINUE 10 | end 11 | 12 | def none_result 13 | {ExecResult::NONE, NullValue.new} 14 | end 15 | 16 | def exec_body(body : Array(Statement), ctx : Context) : {ExecResult, AnyValue} 17 | init = none_result 18 | body.reduce init do |res, statement| 19 | res[0] == ExecResult::NONE ? statement.exec(ctx) : res 20 | end 21 | end 22 | 23 | abstract class Statement 24 | abstract def exec(ctx : Context) : {ExecResult, AnyValue} 25 | 26 | def ==(other) 27 | false 28 | end 29 | end 30 | 31 | class FunctionDeclarationStatement < Statement 32 | getter name 33 | getter params 34 | getter body 35 | 36 | def initialize(@name : String, @params : Array(String), @body : Array(Statement)) 37 | end 38 | 39 | def exec(ctx : Context) : {ExecResult, AnyValue} 40 | fn = UserFunction.new params, body, ctx 41 | ctx.create_var(@name, fn) 42 | none_result 43 | end 44 | 45 | def ==(other : FunctionDeclarationStatement) 46 | name == other.name && params == other.params && body == other.body 47 | end 48 | end 49 | 50 | class ClassDeclarationStatement < Statement 51 | getter name 52 | getter ctor_params 53 | getter ctor_body 54 | getter methods 55 | 56 | def initialize( 57 | @name : String, 58 | @ctor_params : Array(String), 59 | @ctor_body : Array(Statement), 60 | @methods : Array(FunctionDeclarationStatement) 61 | ) 62 | end 63 | 64 | def exec(ctx : Context) : {ExecResult, AnyValue} 65 | methods_hash = Hash(String, FunctionDeclarationStatement).new 66 | methods.each do |method| 67 | methods_hash[method.name] = method 68 | end 69 | class_fn = BuiltinFunction.new ctor_params.size do |args| 70 | obj_ctx = Context.new ctx 71 | obj = ObjectValue.new name, methods_hash, obj_ctx 72 | obj_ctx.create_var("this", obj) 73 | ctor = UserFunction.new ctor_params, ctor_body, obj_ctx 74 | ctor.call(args) 75 | obj 76 | end 77 | 78 | ctx.create_var name, class_fn 79 | none_result 80 | end 81 | 82 | def ==(other : ClassDeclarationStatement) 83 | name == other.name && 84 | ctor_params == other.ctor_params && 85 | ctor_body == other.ctor_body && 86 | methods == other.methods 87 | end 88 | end 89 | 90 | class IfStatement < Statement 91 | getter cond 92 | getter true_body 93 | getter false_body 94 | 95 | def initialize(@cond : Expr, @true_body : Array(Statement), @false_body : Array(Statement)) 96 | end 97 | 98 | def exec(ctx : Context) : {ExecResult, AnyValue} 99 | body = 100 | if cond.eval(ctx).get_boolean 101 | true_body 102 | else 103 | false_body 104 | end 105 | inner_ctx = Context.new ctx 106 | exec_body body, inner_ctx 107 | end 108 | 109 | def ==(other : IfStatement) 110 | cond == other.cond && true_body == other.true_body && false_body == other.false_body 111 | end 112 | end 113 | 114 | class WhileStatement < Statement 115 | getter cond 116 | getter body 117 | 118 | def initialize(@cond : Expr, @body : Array(Statement)) 119 | end 120 | 121 | def exec(ctx : Context) : {ExecResult, AnyValue} 122 | while cond.eval(ctx).get_boolean 123 | inner_ctx = Context.new ctx 124 | res = exec_body body, inner_ctx 125 | if res[0] == ExecResult::BREAK 126 | break 127 | elsif res[0] == ExecResult::RETURN 128 | return res 129 | end 130 | end 131 | none_result 132 | end 133 | 134 | def ==(other : WhileStatement) 135 | cond == other.cond && body == other.body 136 | end 137 | end 138 | 139 | class ForStatement < Statement 140 | getter init 141 | getter cond 142 | getter step 143 | getter body 144 | 145 | def initialize(@init : Statement, @cond : Expr, @step : Statement, @body : Array(Statement)) 146 | end 147 | 148 | def exec(ctx : Context) : {ExecResult, AnyValue} 149 | outer_ctx = Context.new ctx 150 | init.exec(outer_ctx) 151 | while cond.eval(outer_ctx).get_boolean 152 | inner_ctx = Context.new outer_ctx 153 | res = exec_body body, inner_ctx 154 | if res[0] == ExecResult::BREAK 155 | break 156 | elsif res[0] == ExecResult::RETURN 157 | return res 158 | end 159 | step.exec(outer_ctx) 160 | end 161 | none_result 162 | end 163 | 164 | def ==(other : ForStatement) 165 | init == other.init && cond == other.cond && step == other.step && body == other.body 166 | end 167 | end 168 | 169 | class VariableDeclarationStatement < Statement 170 | getter name 171 | getter expr 172 | 173 | def initialize(@name : String, @expr : Expr) 174 | end 175 | 176 | def exec(ctx : Context) : {ExecResult, AnyValue} 177 | ctx.create_var(name, expr.eval(ctx)) 178 | none_result 179 | end 180 | 181 | def ==(other : VariableDeclarationStatement) 182 | name == other.name && expr == other.expr 183 | end 184 | end 185 | 186 | class VariableAssignmentStatement < Statement 187 | getter name 188 | getter expr 189 | 190 | def initialize(@name : String, @expr : Expr) 191 | end 192 | 193 | def exec(ctx : Context) : {ExecResult, AnyValue} 194 | ctx.set_var(name, expr.eval(ctx)) 195 | none_result 196 | end 197 | 198 | def ==(other : VariableAssignmentStatement) 199 | name == other.name && expr == other.expr 200 | end 201 | end 202 | 203 | class ObjectFieldAssignmentStatement < Statement 204 | getter obj_expr 205 | getter field_name 206 | getter expr 207 | 208 | def initialize(@obj_expr : Expr, @field_name : String, @expr : Expr) 209 | end 210 | 211 | def exec(ctx : Context) : {ExecResult, AnyValue} 212 | obj = obj_expr.eval(ctx).get_object 213 | obj.set_field(field_name, expr.eval(ctx)) 214 | none_result 215 | end 216 | 217 | def ==(other : ObjectFieldAssignmentStatement) 218 | obj_expr == other.obj_expr && field_name == other.field_name && expr == other.expr 219 | end 220 | end 221 | 222 | class ReturnStatement < Statement 223 | getter expr 224 | 225 | def initialize(@expr : Expr) 226 | end 227 | 228 | def exec(ctx : Context) : {ExecResult, AnyValue} 229 | retval = expr.eval(ctx) 230 | {ExecResult::RETURN, retval} 231 | end 232 | 233 | def ==(other : ReturnStatement) 234 | expr == other.expr 235 | end 236 | end 237 | 238 | class BreakStatement < Statement 239 | def exec(ctx : Context) : {ExecResult, AnyValue} 240 | {ExecResult::BREAK, NullValue.new} 241 | end 242 | 243 | def ==(other : BreakStatement) 244 | true 245 | end 246 | end 247 | 248 | class ContinueStatement < Statement 249 | def exec(ctx : Context) : {ExecResult, AnyValue} 250 | {ExecResult::CONTINUE, NullValue.new} 251 | end 252 | 253 | def ==(other : ContinueStatement) 254 | true 255 | end 256 | end 257 | 258 | class ExprStatement < Statement 259 | getter expr 260 | 261 | def initialize(@expr : Expr) 262 | end 263 | 264 | def exec(ctx : Context) : {ExecResult, AnyValue} 265 | expr.eval(ctx) 266 | none_result 267 | end 268 | 269 | def ==(other : ExprStatement) 270 | expr == other.expr 271 | end 272 | end 273 | 274 | class EmptyStatement < Statement 275 | def exec(ctx : Context) : {ExecResult, AnyValue} 276 | none_result 277 | end 278 | 279 | def ==(other : EmptyStatement) 280 | true 281 | end 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /src/parser/lexer.cr: -------------------------------------------------------------------------------- 1 | require "./token.cr" 2 | 3 | module FayrantLang 4 | class Lexer 5 | @@regexes : Array(Tuple(Regex, TokenType)) = [ 6 | { 7 | /^(func)\b/, TokenType::FUNC, 8 | }, { 9 | /^(class)\b/, TokenType::CLASS, 10 | }, { 11 | /^(constructor)\b/, TokenType::CONSTRUCTOR, 12 | }, { 13 | /^(if)\b/, TokenType::IF, 14 | }, { 15 | /^(else)\b/, TokenType::ELSE, 16 | }, { 17 | /^(while)\b/, TokenType::WHILE, 18 | }, { 19 | /^(for)\b/, TokenType::FOR, 20 | }, { 21 | /^(var)\b/, TokenType::VAR, 22 | }, { 23 | /^(return)\b/, TokenType::RETURN, 24 | }, { 25 | /^(break)\b/, TokenType::BREAK, 26 | }, { 27 | /^(continue)\b/, TokenType::CONTINUE, 28 | }, { 29 | /^(true)\b/, TokenType::TRUE, 30 | }, { 31 | /^(false)\b/, TokenType::FALSE, 32 | }, { 33 | /^(null)\b/, TokenType::NULL, 34 | }, { 35 | /^([a-zA-Z_][a-zA-Z0-9_]*)/, TokenType::IDENTIFIER, 36 | }, { 37 | /^(\{)/, TokenType::L_BRACE, 38 | }, { 39 | /^(\})/, TokenType::R_BRACE, 40 | }, { 41 | /^(\()/, TokenType::L_PAREN, 42 | # some weird thing going on with syntax highlightning, ignore this line )/ 43 | }, { 44 | /^(\))/, TokenType::R_PAREN, 45 | }, { 46 | /^(\.)/, TokenType::DOT, 47 | }, { 48 | /^(,)/, TokenType::COMMA, 49 | }, { 50 | /^(;)/, TokenType::SEMICOLON, 51 | }, { 52 | /^(")/, TokenType::QUOTE, 53 | }, { 54 | /^(==)/, TokenType::OP_EQ, 55 | # must be before = 56 | }, { 57 | /^(=)/, TokenType::EQUAL, 58 | }, { 59 | /^(\+=)/, TokenType::EQUAL_PLUS, 60 | }, { 61 | /^(-=)/, TokenType::EQUAL_MINUS, 62 | }, { 63 | /^(\*=)/, TokenType::EQUAL_TIMES, 64 | }, { 65 | /^(\/=)/, TokenType::EQUAL_DIV, 66 | }, { 67 | /^(\\=)/, TokenType::EQUAL_DIV_INV, 68 | }, { 69 | /^(%=)/, TokenType::EQUAL_MOD, 70 | }, { 71 | /^(\^=)/, TokenType::EQUAL_EXPT, 72 | }, { 73 | /^(&=)/, TokenType::EQUAL_AND, 74 | }, { 75 | /^(\|=)/, TokenType::EQUAL_OR, 76 | }, { 77 | /^(\+\+=)/, TokenType::EQUAL_CONCAT, 78 | }, { 79 | /^(-)/, TokenType::OP_MINUS, 80 | }, { 81 | /^(!=)/, TokenType::OP_NEQ, 82 | # must be before ! 83 | }, { 84 | /^(!)/, TokenType::OP_NEG, 85 | }, { 86 | /^(@)/, TokenType::OP_TO_STR, 87 | }, { 88 | /^(#)/, TokenType::OP_TO_NUM, 89 | }, { 90 | /^(\+\+)/, TokenType::OP_CONCAT, 91 | # must be before + token 92 | }, { 93 | /^(\+)/, TokenType::OP_PLUS, 94 | }, { 95 | /^(\*)/, TokenType::OP_TIMES, 96 | }, { 97 | /^(\/)/, TokenType::OP_DIV, 98 | }, { 99 | /^(\\)/, TokenType::OP_DIV_INV, 100 | }, { 101 | /^(%)/, TokenType::OP_MOD, 102 | }, { 103 | /^(\^)/, TokenType::OP_EXPT, 104 | }, { 105 | /^(&)/, TokenType::OP_AND, 106 | }, { 107 | /^(\|)/, TokenType::OP_OR, 108 | }, { 109 | /^(>=)/, TokenType::OP_GE, 110 | # must be before > 111 | }, { 112 | /^(<=)/, TokenType::OP_LE, 113 | # must be before < 114 | }, { 115 | /^(>)/, TokenType::OP_GT, 116 | }, { 117 | /^(<)/, TokenType::OP_LT, 118 | }, { 119 | /^(0b[01]+)/, TokenType::NUMBER, 120 | }, { 121 | /^(0x[0-9a-fA-F]+)/, TokenType::NUMBER, 122 | }, { 123 | /^(\d+(\.\d+)?)/, TokenType::NUMBER, 124 | }, 125 | ] 126 | 127 | enum LexerContext 128 | InsideString 129 | InsideBrace 130 | end 131 | 132 | def initialize(@text : String) 133 | @index = 0 134 | @line = 1 135 | @char_in_line = 1 136 | @contextStack = [] of LexerContext 137 | @tokens = [] of Token 138 | end 139 | 140 | def match_str(regex : Regex, token_type : TokenType) 141 | if match = regex.match(@text[@index..-1]) 142 | {match[0], token_type} 143 | end 144 | end 145 | 146 | def scan_tokens : Array(Token) 147 | while @index < @text.size 148 | if @contextStack.size == 0 149 | scan_tokens_default 150 | elsif @contextStack[-1] == LexerContext::InsideString 151 | scan_tokens_inside_string 152 | elsif @contextStack[-1] == LexerContext::InsideBrace 153 | scan_tokens_inside_brace 154 | end 155 | end 156 | 157 | return @tokens 158 | end 159 | 160 | def scan_tokens_default 161 | char = @text[@index] 162 | if char == ' ' || char == '\t' 163 | @index += 1 164 | @char_in_line += 1 165 | elsif char == '\n' 166 | @index += 1 167 | @line += 1 168 | @char_in_line = 1 169 | elsif char == '~' 170 | while char != '\n' 171 | @index += 1 172 | if @index >= @text.size 173 | break 174 | end 175 | char = @text[@index] 176 | @char_in_line += 1 177 | end 178 | @index += 1 179 | @char_in_line += 1 180 | elsif found = @@regexes.map { |re, tt| match_str re, tt }.compact[0]? 181 | match = found[0] 182 | token_type = found[1] 183 | len = match.size 184 | 185 | loc = Location.new @line, @index, @index + len - 1 186 | token = Token.new token_type, match, loc 187 | @tokens << token 188 | 189 | if token_type == TokenType::QUOTE 190 | @contextStack << LexerContext::InsideString 191 | end 192 | 193 | @index += len 194 | @char_in_line += len 195 | else 196 | raise SyntaxError.new "Unexpected token #{@text[@index]} in line #{@line} at position #{@char_in_line}!" 197 | end 198 | end 199 | 200 | def scan_tokens_inside_string 201 | line = @line 202 | str_start = @index 203 | str_end = @index 204 | str_frag = "" 205 | local_token : Token | Nil = nil 206 | 207 | while true 208 | if @index >= @text.size 209 | puts @tokens 210 | raise SyntaxError.new "Unexpected end of string at index #{@index}" 211 | end 212 | char = @text[@index] 213 | 214 | if char == '"' 215 | @contextStack.pop 216 | loc = Location.new @line, @index, @index 217 | local_token = Token.new TokenType::QUOTE, "\"", loc 218 | break 219 | elsif char == '{' 220 | @contextStack << LexerContext::InsideBrace 221 | loc = Location.new @line, @index, @index 222 | local_token = Token.new TokenType::L_BRACE, "{", loc 223 | break 224 | elsif char == '\\' 225 | @index += 1 226 | if @index >= @text.size 227 | raise SyntaxError.new "Unexpected end of string at #{@index} index!" 228 | end 229 | char2 = @text[@index] 230 | map = { 231 | 'n' => '\n', 232 | 't' => '\t', 233 | '"' => '"', 234 | '\\' => '\\', 235 | '{' => '{', 236 | '}' => '}', 237 | } 238 | if char2 == 'u' 239 | buffer = "" 240 | @index += 1 241 | if @text[@index] != '{' 242 | raise SyntaxError.new "Expected { after \\u at index #{@index}!" 243 | end 244 | loop do 245 | @index += 1 246 | char3 = @text[@index] 247 | break if char3 == '}' 248 | buffer += char3 249 | end 250 | case buffer[0..1] 251 | when "0x" 252 | str_frag += buffer[2..].to_i(16).chr 253 | when "0b" 254 | str_frag += buffer[2..].to_i(2).chr 255 | else 256 | str_frag += buffer.to_i.chr 257 | end 258 | elsif map.has_key? char2 259 | str_frag += map[char2] 260 | else 261 | raise SyntaxError.new "Escaping #{char2} is not supported!" 262 | end 263 | elsif char == '}' 264 | raise SyntaxError.new "Unexpected } at index #{@index}!" 265 | else 266 | if char == "\n" 267 | @line += 1 268 | @char_in_line = 1 269 | end 270 | str_frag += char 271 | end 272 | 273 | str_end = @index 274 | @index += 1 275 | end 276 | 277 | if str_end != str_start 278 | loc = Location.new line, str_start, str_end 279 | token = Token.new TokenType::STRING_FRAGMENT, str_frag, loc 280 | @tokens << token 281 | end 282 | if local_token 283 | @index += 1 284 | @tokens << local_token 285 | end 286 | end 287 | 288 | def scan_tokens_inside_brace 289 | char = @text[@index] 290 | if char == '{' 291 | @contextStack << LexerContext::InsideBrace 292 | elsif char == '}' 293 | @contextStack.pop 294 | end 295 | scan_tokens_default 296 | end 297 | 298 | def test_single_token(str : String) : TokenType | Nil 299 | res = @@regexes.find { |re, tt| re =~ str } 300 | if !res.is_a? Nil 301 | return res[1] 302 | end 303 | end 304 | end 305 | end 306 | -------------------------------------------------------------------------------- /src/ast/expression.cr: -------------------------------------------------------------------------------- 1 | require "../value.cr" 2 | 3 | module FayrantLang 4 | module AST 5 | abstract class Expr 6 | abstract def eval(ctx : Context) : AnyValue 7 | 8 | def ==(other) 9 | false 10 | end 11 | end 12 | 13 | abstract class LiteralExpr(T) < Expr 14 | getter value 15 | 16 | def initialize(@value : T) 17 | end 18 | 19 | def ==(other : LiteralExpr(T)) 20 | value == other.value 21 | end 22 | end 23 | 24 | class BooleanLiteralExpr < LiteralExpr(Bool) 25 | def initialize(value : Bool) 26 | super 27 | end 28 | 29 | def eval(ctx : Context) : BooleanValue 30 | BooleanValue.new @value 31 | end 32 | end 33 | 34 | class NumberLiteralExpr < LiteralExpr(Float64) 35 | def initialize(value : Float64) 36 | super 37 | end 38 | 39 | def eval(ctx : Context) : NumberValue 40 | NumberValue.new @value 41 | end 42 | end 43 | 44 | class NullLiteralExpr < LiteralExpr(Nil) 45 | def initialize(value : Nil = nil) 46 | super 47 | end 48 | 49 | def eval(ctx : Context) : NullValue 50 | NullValue.new 51 | end 52 | end 53 | 54 | class StringLiteralExpr < Expr 55 | getter fragments 56 | 57 | def initialize(@fragments : Array(StringFragment)) 58 | end 59 | 60 | def eval(ctx : Context) : AnyValue 61 | StringValue.new fragments.join("") { |frag| frag.eval(ctx) } 62 | end 63 | 64 | def ==(other : StringLiteralExpr) 65 | fragments == other.fragments 66 | end 67 | end 68 | 69 | abstract class StringFragment 70 | abstract def eval(ctx : Context) : String 71 | 72 | def ==(other) 73 | false 74 | end 75 | end 76 | 77 | class StringLiteralFragment < StringFragment 78 | getter str 79 | 80 | def initialize(@str : String) 81 | end 82 | 83 | def eval(ctx : Context) : String 84 | str 85 | end 86 | 87 | def ==(other : StringLiteralFragment) 88 | str == other.str 89 | end 90 | end 91 | 92 | class StringInterpolationFragment < StringFragment 93 | getter expr 94 | 95 | def initialize(@expr : Expr) 96 | end 97 | 98 | def eval(ctx : Context) : String 99 | UnaryExprToString.new(expr).eval(ctx).get_string 100 | end 101 | 102 | def ==(other : StringInterpolationFragment) 103 | expr == other.expr 104 | end 105 | end 106 | 107 | class VariableExpr < Expr 108 | getter name 109 | 110 | def initialize(@name : String) 111 | end 112 | 113 | def eval(ctx : Context) : AnyValue 114 | ctx.get_var(name) 115 | end 116 | 117 | def ==(other : VariableExpr) 118 | name == other.name 119 | end 120 | end 121 | 122 | abstract class UnaryExpr < Expr 123 | getter expr 124 | 125 | def initialize(@expr : Expr) 126 | end 127 | end 128 | 129 | class UnaryExprMinus < UnaryExpr 130 | def initialize(expr : Expr) 131 | super 132 | end 133 | 134 | def eval(ctx : Context) : NumberValue 135 | NumberValue.new -@expr.eval(ctx).get_number 136 | end 137 | 138 | def ==(other : UnaryExprMinus) 139 | expr == other.expr 140 | end 141 | end 142 | 143 | class UnaryExprNegation < UnaryExpr 144 | def initialize(expr : Expr) 145 | super 146 | end 147 | 148 | def eval(ctx : Context) : BooleanValue 149 | BooleanValue.new !@expr.eval(ctx).get_boolean 150 | end 151 | 152 | def ==(other : UnaryExprNegation) 153 | expr == other.expr 154 | end 155 | end 156 | 157 | class UnaryExprToString < UnaryExpr 158 | def initialize(expr : Expr) 159 | super 160 | end 161 | 162 | def eval(ctx : Context) : AnyValue 163 | StringValue.new @expr.eval(ctx).to_string 164 | end 165 | 166 | def ==(other : UnaryExprToString) 167 | expr == other.expr 168 | end 169 | end 170 | 171 | class UnaryExprToNumber < UnaryExpr 172 | def initialize(expr : Expr) 173 | super 174 | end 175 | 176 | def eval(ctx : Context) : AnyValue 177 | val = @expr.eval(ctx) 178 | case val.type 179 | when ValueType::Number 180 | NumberValue.new val.get_number 181 | when ValueType::Boolean 182 | NumberValue.new val.get_boolean ? 1.0 : 0.0 183 | when ValueType::String 184 | val = val.get_string.to_f64? 185 | if val.is_a?(Float64) 186 | NumberValue.new val 187 | else 188 | NullValue.new 189 | end 190 | else 191 | NullValue.new 192 | end 193 | end 194 | 195 | def ==(other : UnaryExprToNumber) 196 | expr == other.expr 197 | end 198 | end 199 | 200 | abstract class BinaryExpr < Expr 201 | getter lhs 202 | getter rhs 203 | 204 | def initialize(@lhs : Expr, @rhs : Expr) 205 | end 206 | end 207 | 208 | class BinaryExprPlus < BinaryExpr 209 | def initialize(lhs : Expr, rhs : Expr) 210 | super 211 | end 212 | 213 | def eval(ctx : Context) : NumberValue 214 | NumberValue.new @lhs.eval(ctx).get_number + @rhs.eval(ctx).get_number 215 | end 216 | 217 | def ==(other : BinaryExprPlus) 218 | lhs == other.lhs && rhs == other.rhs 219 | end 220 | end 221 | 222 | class BinaryExprMinus < BinaryExpr 223 | def initialize(lhs : Expr, rhs : Expr) 224 | super 225 | end 226 | 227 | def eval(ctx : Context) : NumberValue 228 | NumberValue.new @lhs.eval(ctx).get_number - @rhs.eval(ctx).get_number 229 | end 230 | 231 | def ==(other : BinaryExprMinus) 232 | lhs == other.lhs && rhs == other.rhs 233 | end 234 | end 235 | 236 | class BinaryExprMult < BinaryExpr 237 | def initialize(lhs : Expr, rhs : Expr) 238 | super 239 | end 240 | 241 | def eval(ctx : Context) : NumberValue 242 | NumberValue.new @lhs.eval(ctx).get_number * @rhs.eval(ctx).get_number 243 | end 244 | 245 | def ==(other : BinaryExprMult) 246 | lhs == other.lhs && rhs == other.rhs 247 | end 248 | end 249 | 250 | class BinaryExprDiv < BinaryExpr 251 | def initialize(lhs : Expr, rhs : Expr) 252 | super 253 | end 254 | 255 | def eval(ctx : Context) : NumberValue 256 | rval = @rhs.eval(ctx).get_number 257 | unless rval == 0 258 | NumberValue.new @lhs.eval(ctx).get_number / rval 259 | else 260 | raise ArithmeticError.new "Division by 0" 261 | end 262 | end 263 | 264 | def ==(other : BinaryExprDiv) 265 | lhs == other.lhs && rhs == other.rhs 266 | end 267 | end 268 | 269 | class BinaryExprDivInv < BinaryExpr 270 | def initialize(lhs : Expr, rhs : Expr) 271 | super 272 | end 273 | 274 | def eval(ctx : Context) : NumberValue 275 | lval = @lhs.eval(ctx).get_number 276 | unless lval == 0 277 | NumberValue.new @rhs.eval(ctx).get_number / lval 278 | else 279 | raise ArithmeticError.new "Division by 0" 280 | end 281 | end 282 | 283 | def ==(other : BinaryExprDivInv) 284 | lhs == other.lhs && rhs == other.rhs 285 | end 286 | end 287 | 288 | class BinaryExprMod < BinaryExpr 289 | def initialize(lhs : Expr, rhs : Expr) 290 | super 291 | end 292 | 293 | def eval(ctx : Context) : NumberValue 294 | rval = @rhs.eval(ctx).get_number 295 | unless rval == 0 296 | NumberValue.new @lhs.eval(ctx).get_number % rval 297 | else 298 | raise ArithmeticError.new "Division by 0" 299 | end 300 | end 301 | 302 | def ==(other : BinaryExprMod) 303 | lhs == other.lhs && rhs == other.rhs 304 | end 305 | end 306 | 307 | class BinaryExprExpt < BinaryExpr 308 | def initialize(lhs : Expr, rhs : Expr) 309 | super 310 | end 311 | 312 | def eval(ctx : Context) : NumberValue 313 | # TODO check for exceptions 314 | NumberValue.new @lhs.eval(ctx).get_number ** @rhs.eval(ctx).get_number 315 | end 316 | 317 | def ==(other : BinaryExprExpt) 318 | lhs == other.lhs && rhs == other.rhs 319 | end 320 | end 321 | 322 | class BinaryExprAnd < BinaryExpr 323 | def initialize(lhs : Expr, rhs : Expr) 324 | super 325 | end 326 | 327 | def eval(ctx : Context) : BooleanValue 328 | BooleanValue.new @lhs.eval(ctx).get_boolean && @rhs.eval(ctx).get_boolean 329 | end 330 | 331 | def ==(other : BinaryExprAnd) 332 | lhs == other.lhs && rhs == other.rhs 333 | end 334 | end 335 | 336 | class BinaryExprOr < BinaryExpr 337 | def initialize(lhs : Expr, rhs : Expr) 338 | super 339 | end 340 | 341 | def eval(ctx : Context) : BooleanValue 342 | BooleanValue.new @lhs.eval(ctx).get_boolean || @rhs.eval(ctx).get_boolean 343 | end 344 | 345 | def ==(other : BinaryExprOr) 346 | lhs == other.lhs && rhs == other.rhs 347 | end 348 | end 349 | 350 | class BinaryExprGt < BinaryExpr 351 | def initialize(lhs : Expr, rhs : Expr) 352 | super 353 | end 354 | 355 | def eval(ctx : Context) : BooleanValue 356 | BooleanValue.new @lhs.eval(ctx).get_number > @rhs.eval(ctx).get_number 357 | end 358 | 359 | def ==(other : BinaryExprGt) 360 | lhs == other.lhs && rhs == other.rhs 361 | end 362 | end 363 | 364 | class BinaryExprLt < BinaryExpr 365 | def initialize(lhs : Expr, rhs : Expr) 366 | super 367 | end 368 | 369 | def eval(ctx : Context) : BooleanValue 370 | BooleanValue.new @lhs.eval(ctx).get_number < @rhs.eval(ctx).get_number 371 | end 372 | 373 | def ==(other : BinaryExprLt) 374 | lhs == other.lhs && rhs == other.rhs 375 | end 376 | end 377 | 378 | class BinaryExprGe < BinaryExpr 379 | def initialize(lhs : Expr, rhs : Expr) 380 | super 381 | end 382 | 383 | def eval(ctx : Context) : BooleanValue 384 | BooleanValue.new @lhs.eval(ctx).get_number >= @rhs.eval(ctx).get_number 385 | end 386 | 387 | def ==(other : BinaryExprGe) 388 | lhs == other.lhs && rhs == other.rhs 389 | end 390 | end 391 | 392 | class BinaryExprLe < BinaryExpr 393 | def initialize(lhs : Expr, rhs : Expr) 394 | super 395 | end 396 | 397 | def eval(ctx : Context) : BooleanValue 398 | BooleanValue.new @lhs.eval(ctx).get_number <= @rhs.eval(ctx).get_number 399 | end 400 | 401 | def ==(other : BinaryExprLe) 402 | lhs == other.lhs && rhs == other.rhs 403 | end 404 | end 405 | 406 | class BinaryExprEq < BinaryExpr 407 | def initialize(lhs : Expr, rhs : Expr) 408 | super 409 | end 410 | 411 | def eval(ctx : Context) : BooleanValue 412 | BooleanValue.new @lhs.eval(ctx) == @rhs.eval(ctx) 413 | end 414 | 415 | def ==(other : BinaryExprEq) 416 | lhs == other.lhs && rhs == other.rhs 417 | end 418 | end 419 | 420 | class BinaryExprNeq < BinaryExpr 421 | def initialize(lhs : Expr, rhs : Expr) 422 | super 423 | end 424 | 425 | def eval(ctx : Context) : BooleanValue 426 | BooleanValue.new @lhs.eval(ctx) != @rhs.eval(ctx) 427 | end 428 | 429 | def ==(other : BinaryExprNeq) 430 | lhs == other.lhs && rhs == other.rhs 431 | end 432 | end 433 | 434 | class BinaryExprConcat < BinaryExpr 435 | def initialize(lhs : Expr, rhs : Expr) 436 | super 437 | end 438 | 439 | def eval(ctx : Context) : StringValue 440 | StringValue.new @lhs.eval(ctx).to_string + @rhs.eval(ctx).to_string 441 | end 442 | 443 | def ==(other : BinaryExprConcat) 444 | lhs == other.lhs && rhs == other.rhs 445 | end 446 | end 447 | 448 | class FunctionCallExpr < Expr 449 | getter fn 450 | getter args 451 | 452 | def initialize(@fn : Expr, @args : Array(Expr)) 453 | end 454 | 455 | def eval(ctx : Context) : AnyValue 456 | fn.eval(ctx).get_function.call(args.map { |expr| expr.eval(ctx) }) 457 | end 458 | 459 | def ==(other : FunctionCallExpr) 460 | fn == other.fn && args == other.args 461 | end 462 | end 463 | 464 | class ObjectAccessExpr < Expr 465 | getter obj 466 | getter field 467 | 468 | def initialize(@obj : Expr, @field : String) 469 | end 470 | 471 | def eval(ctx : Context) : AnyValue 472 | obj.eval(ctx).get_object.get_field(field) 473 | end 474 | 475 | def ==(other : ObjectAccessExpr) 476 | obj == other.obj && field == other.field 477 | end 478 | end 479 | end 480 | end 481 | -------------------------------------------------------------------------------- /spec/parser/parser_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../src/parser/parser.cr" 3 | require "../../src/parser/lexer.cr" 4 | 5 | include FayrantLang 6 | 7 | describe "FayrantLang Parser" do 8 | it "should parse '2 + 3 / 4;'" do 9 | tokens = Lexer.new("2 + 3 / 4;").scan_tokens 10 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 11 | expected = BinaryExprPlus.new( 12 | NumberLiteralExpr.new(2), 13 | BinaryExprDiv.new( 14 | NumberLiteralExpr.new(3), 15 | NumberLiteralExpr.new(4) 16 | ) 17 | ) 18 | result.should eq expected 19 | end 20 | 21 | it "should parse '(2 + 3) / 4;'" do 22 | tokens = Lexer.new("(2 + 3) / 4;").scan_tokens 23 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 24 | expected = BinaryExprDiv.new( 25 | BinaryExprPlus.new( 26 | NumberLiteralExpr.new(2), 27 | NumberLiteralExpr.new(3)), 28 | NumberLiteralExpr.new(4) 29 | ) 30 | result.should eq expected 31 | end 32 | 33 | it "should parse '0xC0DE + 0b1010;'" do 34 | tokens = Lexer.new("0xC0DE + 0b1010;").scan_tokens 35 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 36 | expected = BinaryExprPlus.new( 37 | NumberLiteralExpr.new(49374), 38 | NumberLiteralExpr.new(10) 39 | ) 40 | result.should eq expected 41 | end 42 | 43 | it "should parse '2 + @#!-7;'" do 44 | tokens = Lexer.new("2 + @#!-7;").scan_tokens 45 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 46 | expected = 47 | BinaryExprPlus.new( 48 | NumberLiteralExpr.new(2), 49 | UnaryExprToString.new( 50 | UnaryExprToNumber.new( 51 | UnaryExprNegation.new( 52 | UnaryExprMinus.new( 53 | NumberLiteralExpr.new(7), 54 | ) 55 | ) 56 | ) 57 | ) 58 | ) 59 | result.should eq expected 60 | end 61 | 62 | it "should parse '1 \\ 2 / 3 \\ 4;'" do 63 | tokens = Lexer.new("1 \\ 2 / 3 \\ 4;").scan_tokens 64 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 65 | expected = BinaryExprDivInv.new( 66 | NumberLiteralExpr.new(1), 67 | BinaryExprDivInv.new( 68 | BinaryExprDiv.new( 69 | NumberLiteralExpr.new(2), 70 | NumberLiteralExpr.new(3) 71 | ), 72 | NumberLiteralExpr.new(4) 73 | ) 74 | ) 75 | result.should eq expected 76 | end 77 | 78 | it "should parse '1 & 2 | 3 | 4 & 5;'" do 79 | tokens = Lexer.new("1 & 2 | 3 | 4 & 5;").scan_tokens 80 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 81 | expected = BinaryExprOr.new( 82 | BinaryExprOr.new( 83 | BinaryExprAnd.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)), 84 | NumberLiteralExpr.new(3), 85 | ), 86 | BinaryExprAnd.new(NumberLiteralExpr.new(4), NumberLiteralExpr.new(5)), 87 | ) 88 | result.should eq expected 89 | end 90 | 91 | it "should parse '1 > 2 | 3 <= 4;'" do 92 | tokens = Lexer.new("1 > 2 | 3 <= 4;").scan_tokens 93 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 94 | expected = BinaryExprOr.new( 95 | BinaryExprGt.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)), 96 | BinaryExprLe.new(NumberLiteralExpr.new(3), NumberLiteralExpr.new(4)), 97 | ) 98 | result.should eq expected 99 | end 100 | 101 | it "should parse '1 ++ 2 ++ 3;'" do 102 | tokens = Lexer.new("1 ++ 2 ++ 3;").scan_tokens 103 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 104 | expected = BinaryExprConcat.new( 105 | BinaryExprConcat.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)), 106 | NumberLiteralExpr.new(3), 107 | ) 108 | result.should eq expected 109 | end 110 | 111 | it "should parse '2 ^ 3 ^ 4;'" do 112 | tokens = Lexer.new("2 ^ 3 ^ 4;").scan_tokens 113 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 114 | expected = BinaryExprExpt.new( 115 | NumberLiteralExpr.new(2), 116 | BinaryExprExpt.new(NumberLiteralExpr.new(3), NumberLiteralExpr.new(4)), 117 | ) 118 | result.should eq expected 119 | end 120 | 121 | it "should parse '1.a(2, 3 + 4).b();'" do 122 | tokens = Lexer.new("1.a(2, 3 + 4).b();").scan_tokens 123 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 124 | expected = FunctionCallExpr.new( 125 | ObjectAccessExpr.new( 126 | FunctionCallExpr.new( 127 | ObjectAccessExpr.new(NumberLiteralExpr.new(1), "a"), 128 | [ 129 | NumberLiteralExpr.new(2), 130 | BinaryExprPlus.new(NumberLiteralExpr.new(3), NumberLiteralExpr.new(4)), 131 | ], 132 | ), 133 | "b" 134 | ), 135 | [] of Expr, 136 | ) 137 | result.should eq expected 138 | end 139 | 140 | it "should parse 'a.b + c * d;'" do 141 | tokens = Lexer.new("a.b + c * d;").scan_tokens 142 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 143 | expected = BinaryExprPlus.new( 144 | ObjectAccessExpr.new(VariableExpr.new("a"), "b"), 145 | BinaryExprMult.new(VariableExpr.new("c"), VariableExpr.new("d")), 146 | ) 147 | result.should eq expected 148 | end 149 | 150 | it "should parse 'a + b; 1 + 2;'" do 151 | tokens = Lexer.new("a + b; 1 + 2;").scan_tokens 152 | result = Parser.new(tokens).parse_program 153 | expected = [ 154 | ExprStatement.new( 155 | BinaryExprPlus.new(VariableExpr.new("a"), VariableExpr.new("b")) 156 | ), 157 | ExprStatement.new( 158 | BinaryExprPlus.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)) 159 | ), 160 | ] 161 | result.zip?(expected) do |res, exp| 162 | res.should eq exp 163 | end 164 | end 165 | 166 | it "should parse 'var x = 1 + 2;'" do 167 | tokens = Lexer.new("var x = 1 + 2;").scan_tokens 168 | result = Parser.new(tokens).parse_program[0].as(VariableDeclarationStatement) 169 | expected = VariableDeclarationStatement.new( 170 | "x", 171 | BinaryExprPlus.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)), 172 | ) 173 | result.should eq expected 174 | end 175 | 176 | it "should parse '\"abc{ 1 + 2 }def\";'" do 177 | tokens = Lexer.new("\"abc{ 1 + 2 }def\";").scan_tokens 178 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 179 | expected = StringLiteralExpr.new([ 180 | StringLiteralFragment.new("abc"), 181 | StringInterpolationFragment.new( 182 | BinaryExprPlus.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)) 183 | ), 184 | StringLiteralFragment.new("def"), 185 | ]) 186 | result.should eq expected 187 | end 188 | 189 | it "should parse '\"abc{ \"def{ 1 + 2 }\" ++ \"ghi\" }jkl\";'" do 190 | tokens = Lexer.new("\"abc{ \"def{ 1 + 2 }\" ++ \"ghi\" }jkl\";").scan_tokens 191 | result = Parser.new(tokens).parse_program[0].as(ExprStatement).expr 192 | expected = StringLiteralExpr.new([ 193 | StringLiteralFragment.new("abc"), 194 | StringInterpolationFragment.new( 195 | BinaryExprConcat.new( 196 | StringLiteralExpr.new([ 197 | StringLiteralFragment.new("def"), 198 | StringInterpolationFragment.new( 199 | BinaryExprPlus.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)) 200 | ), 201 | ]), 202 | StringLiteralExpr.new([StringLiteralFragment.new("ghi")] of StringFragment) 203 | ), 204 | ), 205 | StringLiteralFragment.new("jkl"), 206 | ]) 207 | result.should eq expected 208 | end 209 | 210 | it "should parse 'if (true) { print(5); 7; }'" do 211 | tokens = Lexer.new("if (true) { print(5); 7; }").scan_tokens 212 | result = Parser.new(tokens).parse_program[0].as(IfStatement) 213 | expected = IfStatement.new( 214 | BooleanLiteralExpr.new(true), 215 | [ 216 | ExprStatement.new( 217 | FunctionCallExpr.new( 218 | VariableExpr.new("print"), 219 | [NumberLiteralExpr.new(5)] of Expr, 220 | ) 221 | ), 222 | ExprStatement.new(NumberLiteralExpr.new(7)), 223 | ] of Statement, 224 | [] of Statement, 225 | ) 226 | result.should eq expected 227 | end 228 | 229 | it "should parse 'if (true) { } else { print(5); 7; }'" do 230 | tokens = Lexer.new("if (true) { } else { print(5); 7; }").scan_tokens 231 | result = Parser.new(tokens).parse_program[0].as(IfStatement) 232 | expected = IfStatement.new( 233 | BooleanLiteralExpr.new(true), 234 | [] of Statement, 235 | [ 236 | ExprStatement.new( 237 | FunctionCallExpr.new( 238 | VariableExpr.new("print"), 239 | [NumberLiteralExpr.new(5)] of Expr, 240 | ) 241 | ), 242 | ExprStatement.new(NumberLiteralExpr.new(7)), 243 | ] of Statement, 244 | ) 245 | result.should eq expected 246 | end 247 | 248 | it "should parse 'while (true) { print(5); }'" do 249 | tokens = Lexer.new("while (true) { print(5); }").scan_tokens 250 | result = Parser.new(tokens).parse_program[0].as(WhileStatement) 251 | expected = WhileStatement.new( 252 | BooleanLiteralExpr.new(true), 253 | [ 254 | ExprStatement.new( 255 | FunctionCallExpr.new( 256 | VariableExpr.new("print"), 257 | [NumberLiteralExpr.new(5)] of Expr, 258 | ) 259 | ), 260 | ] of Statement, 261 | ) 262 | result.should eq expected 263 | end 264 | 265 | it "should parse 'for (;;) { print(5); }'" do 266 | tokens = Lexer.new("for (;;) { print(5); }").scan_tokens 267 | result = Parser.new(tokens).parse_program[0].as(ForStatement) 268 | expected = ForStatement.new( 269 | EmptyStatement.new, 270 | BooleanLiteralExpr.new(true), 271 | EmptyStatement.new, 272 | [ 273 | ExprStatement.new( 274 | FunctionCallExpr.new( 275 | VariableExpr.new("print"), 276 | [NumberLiteralExpr.new(5)] of Expr, 277 | ) 278 | ), 279 | ] of Statement, 280 | ) 281 | result.should eq expected 282 | end 283 | 284 | it "should parse 'for (var x = 0; x < 5; x += 1) { print(x); }'" do 285 | tokens = Lexer.new("for (var x = 0; x < 5; x += 1) { print(x); }").scan_tokens 286 | result = Parser.new(tokens).parse_program[0].as(ForStatement) 287 | expected = ForStatement.new( 288 | VariableDeclarationStatement.new( 289 | "x", 290 | NumberLiteralExpr.new(0), 291 | ), 292 | BinaryExprLt.new(VariableExpr.new("x"), NumberLiteralExpr.new(5)), 293 | VariableAssignmentStatement.new( 294 | "x", 295 | BinaryExprPlus.new(VariableExpr.new("x"), NumberLiteralExpr.new(1)) 296 | ), 297 | [ 298 | ExprStatement.new( 299 | FunctionCallExpr.new( 300 | VariableExpr.new("print"), 301 | [VariableExpr.new("x")] of Expr, 302 | ) 303 | ), 304 | ] of Statement, 305 | ) 306 | result.should eq expected 307 | end 308 | 309 | it "should parse 'break;'" do 310 | tokens = Lexer.new("break;").scan_tokens 311 | result = Parser.new(tokens).parse_program[0].as(BreakStatement) 312 | expected = BreakStatement.new 313 | result.should eq expected 314 | end 315 | 316 | it "should parse 'continue;'" do 317 | tokens = Lexer.new("continue;").scan_tokens 318 | result = Parser.new(tokens).parse_program[0].as(ContinueStatement) 319 | expected = ContinueStatement.new 320 | result.should eq expected 321 | end 322 | 323 | it "should parse simple function" do 324 | tokens = Lexer.new("func test() {}").scan_tokens 325 | result = Parser.new(tokens).parse_program[0].as(FunctionDeclarationStatement) 326 | expected = FunctionDeclarationStatement.new( 327 | "test", 328 | [] of String, 329 | [] of Statement, 330 | ) 331 | result.should eq expected 332 | end 333 | 334 | it "should parse function with params and return statement" do 335 | input = "func test(a, b) {\n" \ 336 | " var x = a + b; \n" \ 337 | " return x + 3; \n" \ 338 | " return; \n" \ 339 | "}" 340 | tokens = Lexer.new(input).scan_tokens 341 | result = Parser.new(tokens).parse_program[0].as(FunctionDeclarationStatement) 342 | expected = FunctionDeclarationStatement.new( 343 | "test", 344 | ["a", "b"], 345 | [ 346 | VariableDeclarationStatement.new( 347 | "x", 348 | BinaryExprPlus.new(VariableExpr.new("a"), VariableExpr.new("b")), 349 | ), 350 | ReturnStatement.new( 351 | BinaryExprPlus.new(VariableExpr.new("x"), NumberLiteralExpr.new(3)) 352 | ), 353 | ReturnStatement.new(NullLiteralExpr.new), 354 | ], 355 | ) 356 | result.should eq expected 357 | end 358 | 359 | it "should parse 'x = 1 + 2;'" do 360 | tokens = Lexer.new("x = 1 + 2;").scan_tokens 361 | result = Parser.new(tokens).parse_program[0].as(VariableAssignmentStatement) 362 | expected = VariableAssignmentStatement.new( 363 | "x", 364 | BinaryExprPlus.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)), 365 | ) 366 | result.should eq expected 367 | end 368 | 369 | it "should parse 'x += 2;'" do 370 | tokens = Lexer.new("x += 2;").scan_tokens 371 | result = Parser.new(tokens).parse_program[0].as(VariableAssignmentStatement) 372 | expected = VariableAssignmentStatement.new( 373 | "x", 374 | BinaryExprPlus.new(VariableExpr.new("x"), NumberLiteralExpr.new(2)), 375 | ) 376 | result.should eq expected 377 | end 378 | 379 | it "should parse 'this.x = 1 + 2;'" do 380 | tokens = Lexer.new("this.x = 1 + 2;").scan_tokens 381 | result = Parser.new(tokens).parse_program[0].as(ObjectFieldAssignmentStatement) 382 | expected = ObjectFieldAssignmentStatement.new( 383 | VariableExpr.new("this"), 384 | "x", 385 | BinaryExprPlus.new(NumberLiteralExpr.new(1), NumberLiteralExpr.new(2)), 386 | ) 387 | result.should eq expected 388 | end 389 | 390 | it "should parse 'this.x += 2;'" do 391 | tokens = Lexer.new("this.x += 2;").scan_tokens 392 | result = Parser.new(tokens).parse_program[0].as(ObjectFieldAssignmentStatement) 393 | expected = ObjectFieldAssignmentStatement.new( 394 | VariableExpr.new("this"), 395 | "x", 396 | BinaryExprPlus.new( 397 | ObjectAccessExpr.new(VariableExpr.new("this"), "x"), 398 | NumberLiteralExpr.new(2) 399 | ), 400 | ) 401 | result.should eq expected 402 | end 403 | 404 | it "should parse class declaration" do 405 | input = "class TestClass { \n" \ 406 | " constructor(a, b) { \n" \ 407 | " this.x = a; \n" \ 408 | " this.y = b; \n" \ 409 | " } \n" \ 410 | " func test() { } \n" \ 411 | " func test2(a, b) { \n" \ 412 | " return this.x * a;\n" \ 413 | " } \n" \ 414 | "} \n" 415 | tokens = Lexer.new(input).scan_tokens 416 | result = Parser.new(tokens).parse_program[0].as(ClassDeclarationStatement) 417 | expected = ClassDeclarationStatement.new( 418 | "TestClass", 419 | ["a", "b"], 420 | [ 421 | ObjectFieldAssignmentStatement.new(VariableExpr.new("this"), "x", VariableExpr.new("a")), 422 | ObjectFieldAssignmentStatement.new(VariableExpr.new("this"), "y", VariableExpr.new("b")), 423 | ] of Statement, 424 | [ 425 | FunctionDeclarationStatement.new("test", [] of String, [] of Statement), 426 | FunctionDeclarationStatement.new( 427 | "test2", 428 | ["a", "b"], 429 | [ 430 | ReturnStatement.new( 431 | BinaryExprMult.new( 432 | ObjectAccessExpr.new( 433 | VariableExpr.new("this"), 434 | "x" 435 | ), 436 | VariableExpr.new("a"), 437 | ) 438 | ), 439 | ] of Statement 440 | ), 441 | ] 442 | ) 443 | result.should eq expected 444 | end 445 | end 446 | -------------------------------------------------------------------------------- /src/parser/parser.cr: -------------------------------------------------------------------------------- 1 | require "../ast/expression.cr" 2 | require "../ast/statement.cr" 3 | 4 | module FayrantLang 5 | include AST 6 | 7 | class Parser 8 | def initialize(@tokens : Array(Token)) 9 | @index = 0 10 | end 11 | 12 | def parse_program 13 | statements = [] of Statement 14 | until eof 15 | statements << parse_statement 16 | end 17 | statements 18 | end 19 | 20 | private def parse_statement : Statement 21 | case current_token.type 22 | when TokenType::FUNC 23 | parse_function_statement 24 | when TokenType::CLASS 25 | parse_class_statement 26 | when TokenType::IF 27 | parse_if_statement 28 | when TokenType::WHILE 29 | parse_while_statement 30 | when TokenType::FOR 31 | parse_for_statement 32 | when TokenType::VAR 33 | parse_var_statement 34 | when TokenType::RETURN 35 | parse_return_statement 36 | when TokenType::BREAK 37 | parse_break_statement 38 | when TokenType::CONTINUE 39 | parse_continue_statement 40 | else 41 | parse_expr_or_assignment_statement 42 | end 43 | end 44 | 45 | private def parse_function_statement 46 | consume_token TokenType::FUNC 47 | name_token = consume_token TokenType::IDENTIFIER 48 | params = parse_params 49 | body = parse_body 50 | FunctionDeclarationStatement.new name_token.lexeme, params, body 51 | end 52 | 53 | private def parse_class_statement 54 | consume_token TokenType::CLASS 55 | name_token = consume_token TokenType::IDENTIFIER 56 | consume_token TokenType::L_BRACE 57 | 58 | consume_token TokenType::CONSTRUCTOR 59 | ctor_params = parse_params 60 | ctor_body = parse_body 61 | 62 | methods = [] of FunctionDeclarationStatement 63 | while current_token.type != TokenType::R_BRACE 64 | methods << parse_function_statement 65 | end 66 | 67 | consume_token TokenType::R_BRACE 68 | 69 | ClassDeclarationStatement.new name_token.lexeme, ctor_params, ctor_body, methods 70 | end 71 | 72 | private def parse_if_statement 73 | consume_token TokenType::IF 74 | consume_token TokenType::L_PAREN 75 | cond = parse_expr 76 | consume_token TokenType::R_PAREN 77 | true_body = parse_body 78 | false_body = [] of Statement 79 | if !eof && current_token.type == TokenType::ELSE 80 | consume_token TokenType::ELSE 81 | false_body = parse_body 82 | end 83 | IfStatement.new cond, true_body, false_body 84 | end 85 | 86 | private def parse_while_statement 87 | consume_token TokenType::WHILE 88 | consume_token TokenType::L_PAREN 89 | cond = parse_expr 90 | consume_token TokenType::R_PAREN 91 | body = parse_body 92 | WhileStatement.new cond, body 93 | end 94 | 95 | private def parse_for_statement 96 | consume_token TokenType::FOR 97 | consume_token TokenType::L_PAREN 98 | 99 | init = 100 | case current_token.type 101 | when TokenType::SEMICOLON 102 | consume_token TokenType::SEMICOLON 103 | EmptyStatement.new 104 | when TokenType::VAR 105 | parse_var_statement 106 | else 107 | parse_expr_or_assignment_statement 108 | end 109 | 110 | cond = 111 | if current_token.type == TokenType::SEMICOLON 112 | BooleanLiteralExpr.new true 113 | else 114 | parse_expr 115 | end 116 | consume_token TokenType::SEMICOLON 117 | 118 | step = 119 | if current_token.type == TokenType::R_PAREN 120 | EmptyStatement.new 121 | else 122 | parse_expr_or_assignment_statement_no_semicolon 123 | end 124 | 125 | consume_token TokenType::R_PAREN 126 | 127 | body = parse_body 128 | 129 | ForStatement.new init, cond, step, body 130 | end 131 | 132 | private def parse_var_statement 133 | consume_token TokenType::VAR 134 | token = consume_token TokenType::IDENTIFIER 135 | expr = 136 | if current_token == TokenType::SEMICOLON 137 | NullLiteralExpr.new 138 | else 139 | consume_token TokenType::EQUAL 140 | parse_expr 141 | end 142 | consume_token TokenType::SEMICOLON 143 | VariableDeclarationStatement.new token.lexeme, expr 144 | end 145 | 146 | private def parse_expr_or_assignment_statement : Statement 147 | statement = parse_expr_or_assignment_statement_no_semicolon 148 | consume_token TokenType::SEMICOLON 149 | statement 150 | end 151 | 152 | private def parse_expr_or_assignment_statement_no_semicolon : Statement 153 | expr = parse_expr 154 | case 155 | when current_token.type == TokenType::SEMICOLON 156 | ExprStatement.new expr 157 | when current_token.type == TokenType::R_PAREN 158 | ExprStatement.new expr 159 | when expr.is_a?(VariableExpr) 160 | rhs_expr = parse_assignment_statement_expr expr 161 | VariableAssignmentStatement.new expr.name, rhs_expr 162 | when expr.is_a?(ObjectAccessExpr) 163 | rhs_expr = parse_assignment_statement_expr expr 164 | ObjectFieldAssignmentStatement.new expr.obj, expr.field, rhs_expr 165 | else 166 | raise SyntaxError.new "Expected semicolon or assignment operator" 167 | EmptyStatement.new 168 | end 169 | end 170 | 171 | private def parse_assignment_statement_expr(lhs : Expr) : Expr 172 | map = { 173 | TokenType::EQUAL => ->(rhs : Expr) { rhs }, 174 | TokenType::EQUAL_PLUS => ->(rhs : Expr) { BinaryExprPlus.new(lhs, rhs) }, 175 | TokenType::EQUAL_MINUS => ->(rhs : Expr) { BinaryExprMinus.new(lhs, rhs) }, 176 | TokenType::EQUAL_TIMES => ->(rhs : Expr) { BinaryExprMult.new(lhs, rhs) }, 177 | TokenType::EQUAL_DIV => ->(rhs : Expr) { BinaryExprDiv.new(lhs, rhs) }, 178 | TokenType::EQUAL_DIV_INV => ->(rhs : Expr) { BinaryExprDivInv.new(lhs, rhs) }, 179 | TokenType::EQUAL_MOD => ->(rhs : Expr) { BinaryExprMod.new(lhs, rhs) }, 180 | TokenType::EQUAL_EXPT => ->(rhs : Expr) { BinaryExprExpt.new(lhs, rhs) }, 181 | TokenType::EQUAL_AND => ->(rhs : Expr) { BinaryExprAnd.new(lhs, rhs) }, 182 | TokenType::EQUAL_OR => ->(rhs : Expr) { BinaryExprOr.new(lhs, rhs) }, 183 | TokenType::EQUAL_CONCAT => ->(rhs : Expr) { BinaryExprConcat.new(lhs, rhs) }, 184 | } 185 | token = current_token 186 | unless map.has_key?(token.type) 187 | raise SyntaxError.new "Unexpected token #{token.type}: #{token.lexeme}, expected assignment operator" 188 | end 189 | assign_op = consume_token token.type 190 | rhs = parse_expr 191 | map[token.type].call(rhs) 192 | end 193 | 194 | private def parse_return_statement 195 | consume_token TokenType::RETURN 196 | expr = NullLiteralExpr.new 197 | if current_token.type != TokenType::SEMICOLON 198 | expr = parse_expr 199 | end 200 | consume_token TokenType::SEMICOLON 201 | ReturnStatement.new expr 202 | end 203 | 204 | private def parse_break_statement 205 | consume_token TokenType::BREAK 206 | consume_token TokenType::SEMICOLON 207 | BreakStatement.new 208 | end 209 | 210 | private def parse_continue_statement 211 | consume_token TokenType::CONTINUE 212 | consume_token TokenType::SEMICOLON 213 | ContinueStatement.new 214 | end 215 | 216 | private def parse_body 217 | consume_token TokenType::L_BRACE 218 | statements = [] of Statement 219 | while current_token.type != TokenType::R_BRACE 220 | statements << parse_statement 221 | end 222 | consume_token TokenType::R_BRACE 223 | statements 224 | end 225 | 226 | private def parse_params 227 | consume_token TokenType::L_PAREN 228 | params = [] of Token 229 | while current_token.type != TokenType::R_PAREN 230 | params << consume_token TokenType::IDENTIFIER 231 | if current_token.type == TokenType::R_PAREN 232 | break 233 | end 234 | consume_token TokenType::COMMA 235 | end 236 | consume_token TokenType::R_PAREN 237 | params.map { |param| param.lexeme } 238 | end 239 | 240 | private def parse_expr 241 | parse_expr_or 242 | end 243 | 244 | private def parse_expr_or 245 | expr = parse_expr_and 246 | while true 247 | if current_token.type == TokenType::OP_OR 248 | consume_token TokenType::OP_OR 249 | expr = BinaryExprOr.new expr, parse_expr_and 250 | else 251 | break 252 | end 253 | end 254 | expr 255 | end 256 | 257 | private def parse_expr_and 258 | expr = parse_expr_equality 259 | while true 260 | if current_token.type == TokenType::OP_AND 261 | consume_token TokenType::OP_AND 262 | expr = BinaryExprAnd.new expr, parse_expr_equality 263 | else 264 | break 265 | end 266 | end 267 | expr 268 | end 269 | 270 | private def parse_expr_equality 271 | expr = parse_expr_compare 272 | while true 273 | case current_token.type 274 | when TokenType::OP_EQ 275 | consume_token TokenType::OP_EQ 276 | expr = BinaryExprEq.new expr, parse_expr_compare 277 | when TokenType::OP_NEQ 278 | consume_token TokenType::OP_NEQ 279 | expr = BinaryExprNeq.new expr, parse_expr_compare 280 | else 281 | break 282 | end 283 | end 284 | expr 285 | end 286 | 287 | private def parse_expr_compare 288 | expr = parse_expr_concat 289 | while true 290 | case current_token.type 291 | when TokenType::OP_GT 292 | consume_token TokenType::OP_GT 293 | expr = BinaryExprGt.new expr, parse_expr_concat 294 | when TokenType::OP_LT 295 | consume_token TokenType::OP_LT 296 | expr = BinaryExprLt.new expr, parse_expr_concat 297 | when TokenType::OP_GE 298 | consume_token TokenType::OP_GE 299 | expr = BinaryExprGe.new expr, parse_expr_concat 300 | when TokenType::OP_LE 301 | consume_token TokenType::OP_LE 302 | expr = BinaryExprLe.new expr, parse_expr_concat 303 | else 304 | break 305 | end 306 | end 307 | expr 308 | end 309 | 310 | private def parse_expr_concat 311 | expr = parse_expr_plus_minus 312 | while true 313 | if current_token.type == TokenType::OP_CONCAT 314 | consume_token TokenType::OP_CONCAT 315 | expr = BinaryExprConcat.new expr, parse_expr_plus_minus 316 | else 317 | break 318 | end 319 | end 320 | expr 321 | end 322 | 323 | private def parse_expr_plus_minus 324 | expr = parse_expr_div_inv 325 | while true 326 | case current_token.type 327 | when TokenType::OP_PLUS 328 | consume_token TokenType::OP_PLUS 329 | expr = BinaryExprPlus.new expr, parse_expr_div_inv 330 | when TokenType::OP_MINUS 331 | consume_token TokenType::OP_MINUS 332 | expr = BinaryExprMinus.new expr, parse_expr_div_inv 333 | else 334 | break 335 | end 336 | end 337 | expr 338 | end 339 | 340 | private def parse_expr_div_inv 341 | expr = parse_expr_times_div_mod 342 | if current_token.type == TokenType::OP_DIV_INV 343 | consume_token TokenType::OP_DIV_INV 344 | BinaryExprDivInv.new expr, parse_expr_div_inv 345 | else 346 | expr 347 | end 348 | end 349 | 350 | private def parse_expr_times_div_mod 351 | expr = parse_expr_expt 352 | while true 353 | case current_token.type 354 | when TokenType::OP_TIMES 355 | consume_token TokenType::OP_TIMES 356 | expr = BinaryExprMult.new expr, parse_expr_expt 357 | when TokenType::OP_DIV 358 | consume_token TokenType::OP_DIV 359 | expr = BinaryExprDiv.new expr, parse_expr_expt 360 | when TokenType::OP_MOD 361 | consume_token TokenType::OP_MOD 362 | expr = BinaryExprMod.new expr, parse_expr_expt 363 | else 364 | break 365 | end 366 | end 367 | expr 368 | end 369 | 370 | private def parse_expr_expt 371 | expr = parse_expr_unary 372 | if current_token.type == TokenType::OP_EXPT 373 | consume_token TokenType::OP_EXPT 374 | BinaryExprExpt.new expr, parse_expr_expt 375 | else 376 | expr 377 | end 378 | end 379 | 380 | private def parse_expr_unary 381 | case current_token.type 382 | when TokenType::OP_MINUS 383 | consume_token TokenType::OP_MINUS 384 | UnaryExprMinus.new parse_expr_unary 385 | when TokenType::OP_NEG 386 | consume_token TokenType::OP_NEG 387 | UnaryExprNegation.new parse_expr_unary 388 | when TokenType::OP_TO_STR 389 | consume_token TokenType::OP_TO_STR 390 | UnaryExprToString.new parse_expr_unary 391 | when TokenType::OP_TO_NUM 392 | consume_token TokenType::OP_TO_NUM 393 | UnaryExprToNumber.new parse_expr_unary 394 | else 395 | parse_expr_call_access 396 | end 397 | end 398 | 399 | private def parse_expr_call_access 400 | expr = parse_expr_basic 401 | while true 402 | case current_token.type 403 | when TokenType::DOT 404 | consume_token TokenType::DOT 405 | identifier = consume_token TokenType::IDENTIFIER 406 | expr = ObjectAccessExpr.new expr, identifier.lexeme 407 | when TokenType::L_PAREN 408 | consume_token TokenType::L_PAREN 409 | args = [] of Expr 410 | while current_token.type != TokenType::R_PAREN 411 | args << parse_expr 412 | if current_token.type == TokenType::R_PAREN 413 | break 414 | end 415 | consume_token TokenType::COMMA 416 | end 417 | consume_token TokenType::R_PAREN 418 | expr = FunctionCallExpr.new expr, args 419 | else 420 | break 421 | end 422 | end 423 | expr 424 | end 425 | 426 | private def parse_expr_basic 427 | case current_token.type 428 | when TokenType::L_PAREN 429 | consume_token TokenType::L_PAREN 430 | expr = parse_expr 431 | consume_token TokenType::R_PAREN 432 | expr 433 | when TokenType::IDENTIFIER 434 | token = consume_token TokenType::IDENTIFIER 435 | VariableExpr.new token.lexeme 436 | when TokenType::NUMBER 437 | token = consume_token TokenType::NUMBER 438 | case token.lexeme[0..1] 439 | when "0x" 440 | NumberLiteralExpr.new token.lexeme[2..].to_i(16).to_f 441 | when "0b" 442 | NumberLiteralExpr.new token.lexeme[2..].to_i(2).to_f 443 | else 444 | NumberLiteralExpr.new token.lexeme.to_f 445 | end 446 | when TokenType::TRUE 447 | consume_token TokenType::TRUE 448 | BooleanLiteralExpr.new true 449 | when TokenType::FALSE 450 | consume_token TokenType::FALSE 451 | BooleanLiteralExpr.new false 452 | when TokenType::NULL 453 | consume_token TokenType::NULL 454 | NullLiteralExpr.new 455 | when TokenType::QUOTE 456 | parse_string 457 | else 458 | raise SyntaxError.new "Unexpected token #{current_token.type}: #{current_token.lexeme} " 459 | end 460 | end 461 | 462 | private def parse_string 463 | consume_token TokenType::QUOTE 464 | fragments = [] of StringFragment 465 | while current_token.type != TokenType::QUOTE 466 | case current_token.type 467 | when TokenType::STRING_FRAGMENT 468 | token = consume_token TokenType::STRING_FRAGMENT 469 | fragments << StringLiteralFragment.new token.lexeme 470 | when TokenType::L_BRACE 471 | consume_token TokenType::L_BRACE 472 | fragments << StringInterpolationFragment.new parse_expr 473 | consume_token TokenType::R_BRACE 474 | else 475 | raise SyntaxError.new "Unexpected token #{current_token.type}: #{current_token.lexeme} " 476 | end 477 | end 478 | consume_token TokenType::QUOTE 479 | StringLiteralExpr.new fragments 480 | end 481 | 482 | private def eof 483 | return @index >= @tokens.size 484 | end 485 | 486 | private def current_token 487 | return @tokens[@index] 488 | end 489 | 490 | private def consume_token(tt : TokenType) 491 | if eof 492 | raise SyntaxError.new "Unexpected end of input, expected #{tt}" 493 | elsif @tokens[@index].type == tt 494 | @index += 1 495 | return @tokens[@index - 1] 496 | else 497 | raise SyntaxError.new "Unexpected token #{current_token.type}: #{current_token.lexeme}, expected #{tt}" 498 | end 499 | end 500 | end 501 | end 502 | --------------------------------------------------------------------------------