Yep.

What I have been up to.

A lot of bugfixes. That’s it. I’m not going into details, but just a whole lot of bugfixes in the Parser, Lexer, Interpreter, types. Basically everything.

This post is not going to go into detail about the changes to ezr², but rather the awesome test programs I’ve come up with!

Considering that there are no built-in functions for the types like string.Length, string.LastIndexOf, etc. I’ve had to create some brute-forced solutions for that… >:)

A Simple Calculator.

This is a very simple calculator I made. It’s not very good, but it was good enough for some testing.

function get_input with message: "", skip_spaces: true do
    raw_input: get(message)

    input_components: []

    index: 0

    number: 0
    was_previous_number: false

    while true do
        try do
            char: raw_input < index
        catch value_out_of_range_error do
            if was_previous_number do input_components + number

            stop
        catch as other_error do
            throw_error(other_error)
        end

        if char >= 48 and char <= 57 do
            if not was_previous_number do number: 0

            number: (number * 10) + (char - 48)
            was_previous_number: true
        else do
            if was_previous_number do input_components + number

            was_previous_number: false

            if char ! ` ` or not skip_spaces do input_components + char
        end

        index :+ 1
    end

    return input_components
end

function calculate with input_components do
    result: nothing

    index: 0

    number: 0
    operator: nothing

    while true do
        try do
            component: input_components < index
        catch value_out_of_range_error do
            stop
        catch as other_error do
            throw_error(other_error)
        end

        if type_name_of(component) = "integer" do
            if result = nothing do result: component else do number: component

            if operator ! nothing do
                if operator = `+` do
                    result :+ number
                else if operator = `-` do
                    result :- number
                else if operator = `x` do
                    result :* number
                else if operator = `/` do
                    result :/ number
                end

                operator = nothing
            end
        else do
            operator: component
        end

        index:+ 1
    end

    return result
end

while true do
    input: get_input("Enter calculation: ")
    try do
        if input < 0 = `q` do stop
    end

    show(calculate(input))
end

An Interpreted Calculator!

I’ve made a lexer, parser and interpreter in ezr²! It doesn’t do much, but it can do mathematical calculations!

Warning: It’s ~600 lines long!

define static
    object utils do
        function char_is_valid_letter with char do (char >= 97 and char <= 122) or (char >= 65 and char <= 90) or char = `_`

        function string_index_of with string, char, start_index do
            string_length: string_length(string)

            if start_index < 0 or start_index >= string_length do return -1
            count from start_index to string_length - start_index as i do
                if string < i = char do return i
            end

            return -1
        end

        function string_last_index_of with string, char, start_index do
            string_length: string_length(string)

            if start_index < 0 or start_index >= string_length do return -1

            last_index: -1
            count from start_index to string_length - start_index as i do
                if string < i = char do last_index: i
            end

            return last_index
        end

        function string_length with string do
            length: 0

            while true do
                try do
                    string < length
                catch value_out_of_range_error do
                    stop
                end

                length:+ 1
            end

            return length
        end

        function string_with_underline with start_position, end_position do
            text: start_position.script
            text_length: string_length(text)

            start_index: start_position.index
            if start_index < text_length and text < start_index = `\n` and end_position.index - start_index = 1 do
                start_index: if start_index - 2 < 0 do 0 else do start_index - 2
            end

            start: string_last_index_of(text, `\n`, if start_index < text_length - 1 do start_index else do text_length - 1) + 1
        
            while start < text_length and text < start in (` `, `\t`, `\n`) do
                start:+ 1
            end

            end_: string_index_of(text, `\n`, start)
            if end_ = -1 do end_: text_length - 1

            result: '  '
            result + (text < (start, end_))
            result + `\n`

            loop_iterations: (if start_position.index - start < 0 do 0 else do start_position.index - start) + 2
            
            count to loop_iterations do
                result + ` `
            end

            loop_iterations: if end_position.index - start_position.index < 1 do 1 else do end_position.index - start_position.index
            count to loop_iterations do
                result + `~`
            end

            result + `\n`
            return result
        end
    end
end

object position do
    define static
        private _none: nothing

        constant function none do
            if _none = nothing do
                _none: position(-1, -1, "", "")
            end

            return _none 
        end
    end

    constant function initialize with index, line, file, script do
        this.index: index
        this.line: line
        this.file: file
        this.script: script
    end

    function advance with current_char: `\0` do
        this.index:+ 1
        
        if current_char = `\n` do
            this.line :+ 1
        end
    end

    function to_string do type_name_of(this) + `(` + this.index + ", " + this.line + `)`
end

object token do
    constant function initialize with type, value, start_position, end_position: nothing do
        this.type: type
        this.value: value
        this.start_position: start_position

        if end_position = nothing do
            this.end_position: copy(this.start_position)
            this.end_position.advance()
        else do
            this.end_position: end_position
        end
    end

    constant function to_string do
        return type_name_of(this) + `(` + this.type + ", " + this.value + `)`
    end
end

object exception do
    define static constant
        invalid_grammar: "Invalid grammar"
        unexpected_character: "Unexpected character"
        undefined_variable: "Undefined variable"
    end

    constant function initialize with title, details, start_position, end_position: nothing do
        this.title: title
        this.details: details
        this.start_position: start_position

        if end_position ! nothing do
            this.end_position: end_position
        else do
            this.end_position: copy(start_position)
            this.end_position.advance()
        end
    end

    function to_string do this.title
        + " in " + this.start_position.file
        + ", line " + this.start_position.line
        + ": " + this.details + `\n`
        + utils.string_with_underline(this.start_position, this.end_position)
end

object lexer do
    constant function initialize with file, script do
        define private
            this.file: file
            this.script: script

            this.position: position(-1, 1, this.file, this.script)
            this.current_char: `\0`
            this.reached_end: false
        end

        advance()
    end

    private function advance do
        this.position.advance(this.current_char)

        try do
            this.current_char: this.script < this.position.index
        catch value_out_of_range_error do
            this.reached_end: true
        end
    end

    function tokenize do
        error: nothing
        tokens: []

        while not this.reached_end do
            if this.current_char = `\r`
                or this.current_char = `\t`
                or this.current_char = ` ` do
                advance()
            else if this.current_char in (`\n`, `;`) do
                tokens + compile_new_lines()
            else if this.current_char in (`+`, `-`, `*`, `/`, `%`, `^`, `(`, `)`) do
                tokens + token(this.current_char, "", copy(this.position))
                advance()
            else if this.current_char >= 48 and this.current_char <= 57 do
                tokens + compile_number()
            else if utils.char_is_valid_letter(this.current_char) do
                tokens + compile_identifier()
            else do
                error: exception(exception.unexpected_character, "Unexpected character '" + this.current_char + `'`, copy(this.position))
                stop
            end
        end

        tokens + token("endoffile", "", copy(this.position))
        return (tokens, error)
    end

    private function compile_new_lines do
        while not this.reached_end and this.current_char in (`\n`, `;`) do
            advance()
        end

        return token("newline", "", copy(this.position))
    end

    private function compile_identifier do
        identifier: ''
        start_position: copy(this.position)

        while not this.reached_end and utils.char_is_valid_letter(this.current_char) do
            identifier + this.current_char
            advance()
        end

        if identifier = "def" do
            return token(identifier, "", start_position, copy(this.position))
        else do
            return token("identifier", identifier, start_position, copy(this.position))
        end
    end

    private function compile_number do
        start_position: copy(this.position)
        number: 0
        
        while not this.reached_end and this.current_char >= 48 and this.current_char <= 57 do
            number: (number * 10) + (this.current_char - 48)
            advance()
        end

        return token("integer", number, start_position, copy(this.position))
    end
end

object node do
    constant function initialize with type, data, start_position, end_position do
        this.type: type
        this.data: data
        this.start_position: start_position
        this.end_position: end_position
    end
    
    constant function to_string do
        return type_name_of(this) + `(` + this.type + ", " + this.data + `)`
    end
end

object result do
    constant function initialize do
        reset()
    end

    function success with value do
        this.value: value
    end

    function failure with priority, error do
        if this.error = nothing or this.error_priority <= priority do
            this.error_priority: priority
            this.error: error
        end
    end

    function reset do
        this.error: nothing
        this.value: nothing
        this.advance_count: 0
        this.error_priority: 0
    end
end

object parser do
    constant function initialize with tokens do
        define private
            this.tokens: tokens
            this.current_token: nothing
            this.result: result()

            this.index: -1
            this.reached_end: false
        end

        advance()
    end

    private function advance with advance_count: 1 do
        this.index:+ advance_count
        this.result.advance_count:+ advance_count

        try do
            this.current_token: this.tokens < this.index
        catch value_out_of_range_error do
            this.reached_end: true
        end
    end

    private function reverse with reverse_count: 1 do
        advance(-reverse_count)
    end

    function parse do
        parse_statements()
        if this.result.error = nothing and this.current_token.type ! "endoffile" do
            this.result.failure(10, exception(exception.invalid_grammar, "Did not expect this!", this.current_token.start_position, this.current_token.end_position))
        end

        return this.result
    end

    private function binary_operation with left, right, operators do
        start_position: this.current_token.start_position

        left()
        if this.result.error ! nothing do return

        left_node: this.result.value
        reverse_to: this.result.advance_count

        if this.current_token.type = "newline" do advance()

        while not this.reached_end and this.current_token.type in operators do
            operator: this.current_token.type
            advance()

            if this.current_token.type = "newline" do advance()
            
            right()
            if this.result.error ! nothing do return

            right_node: this.result.value

            left_node: node("binary_operation", (left_node, right_node, operator), start_position, this.current_token.end_position)
            reverse_to: this.result.advance_count

            if this.current_token.type = "newline" do advance()
        end

        reverse(this.result.advance_count - reverse_to)
        this.result.success(left_node)
    end

    private function parse_statements do
        statements: []
        start_position: this.current_token.start_position
        
        if this.current_token.type = "newline" do advance()

        parse_expression()
        if this.result.error ! nothing do return

        statements + this.result.value
        while true do
            if this.current_token.type = "newline" do
                advance()
            else do
                stop
            end

            if this.current_token.type = "endoffile" do stop

            parse_expression()
            if this.result.error ! nothing do return
            
            statements + this.result.value
        end

        this.result.success(node("array", statements, start_position, this.current_token.end_position))
    end

    private function parse_expression do
        start_position: this.current_token.start_position

        if this.current_token.type = "def" do
            advance()

            if this.current_token.type ! "identifier" do
                this.result.failure(10, exception(exception.invalid_grammar, "Expected an identifier as the 'def' keyword was used!", this.current_token.start_position, this.current_token.end_position))
                return
            end

            identifier: this.current_token.value
            advance()

            parse_expression()
            if this.result.error ! nothing do return

            this.result.success(node("assignment", (identifier, this.result.value), start_position, this.current_token.end_position))
            return
        end

        parse_arithmetic_expression()
        if this.result.error ! nothing do
            this.result.failure(4, exception(exception.invalid_grammar, "Expected an expression!", this.current_token.start_position, this.current_token.end_position))
        end
    end

    private function parse_arithmetic_expression do
        binary_operation(parse_term, parse_term, (`+`,`-`))
    end
    
    private function parse_term do
        binary_operation(parse_factor, parse_factor, (`*`,`/`, `%`))
    end

    private function parse_factor do
        start_position: this.current_token.start_position

        operator: this.current_token.type
        if operator in (`+`, `-`) do
            advance()

            parse_factor()
            if this.result.error ! nothing do return

            this.result.success(node("factor", (operator, this.result.value), start_position, this.current_token.end_position))
            return
        end

        parse_power()
    end

    private function parse_power do
        binary_operation(parse_atom, parse_atom, (`^`,))
    end

    private function parse_atom do
        token: this.current_token

        if token.type = "identifier" do
            this.result.success(node("access", token.value, token.start_position, token.end_position))
        else if token.type = "integer" do
            this.result.success(node("integer", token.value, token.start_position, token.end_position))
        else if token.type = `(` do
            advance()

            parse_expression()
            if this.result.error ! nothing do return

            if this.current_token.type ! `)` do
                this.result.failure(10, exception(exception.invalid_grammar, "Expected ')' as you started a parenthetical expression!", this.current_token.start_position, this.current_token.end_position))
                return
            end
        else do
            this.result.failure(4, exception(exception.invalid_grammar, "Expected an integer, identifier, 'def' expression, and so on.", this.current_token.start_position, this.current_token.end_position))
            return
        end

        advance()
    end
end

object interpreter do
    constant function initialize do
        this.context: {}
        this.result: result()
    end

    function execute with node do
        this.result.reset()
        visit_node(node)

        return this.result
    end

    private function visit_node with node do
        if node.type = "integer" do
            this.result.success(node.data)
        else if node.type = "factor" do
            operator: node.data < 0
            
            visit_node(node.data < 1)
            if this.result.error ! nothing do return

            if operator = `+` do
                this.result.success(+this.result.value)
            else do
                this.result.success(-this.result.value)
            end
        else if node.type = "binary_operation" do
            visit_binary_operation(node)
        else if node.type = "array" do
            visit_array(node)
        else if node.type = "access" do
            visit_access(node)
        else if node.type = "assignment" do
            visit_assignment(node)
        else do
            throw_error(unexpected_type_error("Unexpected node type: " + node.type))
        end
    end

    private function visit_array with node do
        index: 0
        results: []

        while true do
            try do
                statement: node.data < index
            catch value_out_of_range_error do
                stop
            end

            visit_node(statement)
            if this.result.error ! nothing do return

            results + this.result.value
            index:+ 1
        end

        this.result.success(results)
    end

    private function visit_access with node do
        try do
            value: this.context < node.data
        catch key_not_found_error do
            this.result.failure(10, exception(exception.undefined_variable, "Undefined variable \"" + node.data + "\"!", node.start_position, node.end_position))
            return
        end

        this.result.success(value)
    end

    private function visit_assignment with node do
        visit_node(node.data < 1)
        if this.result.error ! nothing do return

        value: this.result.value
        this.context + (node.data < 0, value)

        this.result.success(value)
    end

    private function visit_binary_operation with node do
        visit_node(node.data < 0)
        if this.result.error ! nothing do return

        left: this.result.value

        visit_node(node.data < 1)
        if this.result.error ! nothing do return

        (right, operator): (this.result.value, node.data < 2)
        if operator = `+` do
            this.result.success(left + right)
        else if operator = `-` do
            this.result.success(left - right)
        else if operator = `*` do
            this.result.success(left * right)
        else if operator = `/` do
            this.result.success(left / right)
        else if operator = `%` do
            this.result.success(left % right)
        else if operator = `^` do
            this.result.success(left ^ right)
        else do
            throw_error(unexpected_type_error("Unexpected binary operator \"" + operator + "\"!"))
        end
    end
end

function main do
    interpreter: interpreter()

    while true do
        input: get(">> ")
        if input = `q` do stop
        
        (tokens, error): lexer("shell", input).tokenize()
        if error ! nothing do
            show("LEXER ERROR! " + error + "\nTokens: " + tokens)
        else do
            show(tokens)
        end

        parse_result: parser(tokens).parse()
        if parse_result.error ! nothing do
            show("PARSER ERROR! " + parse_result.error + "\nNode: " + parse_result.value)
        else do
            show(parse_result.value)
        end

        runtime_result: interpreter.execute(parse_result.value)
        if runtime_result.error ! nothing do
            show("RUNTIME ERROR! " + runtime_result.error + "\nValue: " + runtime_result.value)
        else do
            show(runtime_result.value)
        end
    end
end

main()

Yeah! That’s it for the update!