Attila's Blog

About writing your own programming language in C, Go and Swift

Implementing boolean expression support

Apr 11, 2019 Comments

Categories: aspl

Last time we modified ASPL’s grammar to support boolean expressions. It’s time to implement it in the VM as well.

Let’s see the required steps we have to take to implement boolean expressions.

  1. We need 2 new AST node types, one for boolean expressions, one for nil
  2. We also need 2 new AST nodes for the two above
  3. We need to change the parser to recognize the new tokens
  4. We need to create new opcodes to support bytecode generation
  5. We need to change the compiler to emit bytecode for the two new AST nodes
  6. We need to extend Value to support boolean and nil values
  7. We need to extend the VM to execute the new opcodes
  8. And finally, we change our debugger to print out info about the new opcodes

Maybe it seems a bit complex, but the good news is that from now going on, we have to follow the exact same steps when we introduce support for new features, so next time it’s gonna be really easy to do it.

Now let’s see the steps one by one.

1. Adding new AST node types

We add the two new AST node types, NODE_BOOLEAN and NODE_NIL.

typedef enum ast_node_type_t {
    NODE_INTEGER,
    NODE_DOUBLE,
    NODE_BINARY_INFIX_OPERATOR,
    NODE_UNARY_PREFIX_OPERATOR,
    NODE_BOOLEAN,
    NODE_NIL,
} AstNodeType;

2. Adding two new AST nodes

First, we need storage for the boolean values, so we modify AstNode.

typedef struct ast_node_t {
    AstNodeType type;
    Token* token;
    union {
        struct {
            AstNode* left;
            AstNode* right;
        } binary_infix;
        struct {
            AstNode* right;
        } unary_prefix;
        int64_t integer;
        long double double_precision;
        bool boolean;
    } as;
} AstNode;

Then we add two new functions to create these new AST nodes.

This is the declaration of the functions:

extern AstNode* ast_node_make_boolean(Token* token);
extern AstNode* ast_node_make_nil(Token* token);

And the definition:

AstNode* ast_node_make_boolean(Token* token) {
    AstNode* node = NEW(AstNode);

    node->type = NODE_BOOLEAN;
    node->token = token;
    node->as.boolean = token->type == TOKEN_TRUE ? true : false;

    return node;
}

AstNode* ast_node_make_nil(Token* token) {
    AstNode* node = NEW(AstNode);

    node->type = NODE_NIL;
    node->token = token;

    return node;
}

Of course, we also need to be able to free these nodes, so add that as well:

static void ast_node_free_boolean_node(AstNode* node) {
    token_free(node->token);
    DELETE_TYPE(AstNode, node);
}

static void ast_node_free_nil_node(AstNode* node) {
    token_free(node->token);
    DELETE_TYPE(AstNode, node);
}

void ast_node_free(AstNode* node) {
    switch (node->type) {
        case NODE_INTEGER:
            ast_node_free_integer_node(node);
            break;
        case NODE_DOUBLE:
            ast_node_free_double_node(node);
            break;
        case NODE_BINARY_INFIX_OPERATOR:
            ast_node_free_binary_infix_node(node);
            break;
        case NODE_UNARY_PREFIX_OPERATOR:
            ast_node_free_unary_prefix_node(node);
            break;
        case NODE_BOOLEAN:
            ast_node_free_boolean_node(node);
            break;
        case NODE_NIL:
            ast_node_free_nil_node(node);
            break;
    }
}

3. Change the parser to recognize new tokens

First, we need to add functions that generate AST nodes from our new tokens.

static AstNode* parse_nil_literal_expression(ParsingContext* context) {
    Token* token = get_previous_token(context);
    return ast_node_make_nil(token);
}

static AstNode* parse_true_literal_expression(ParsingContext* context) {
    Token* token = get_previous_token(context);
    return ast_node_make_boolean(token);
}

static AstNode* parse_false_literal_expression(ParsingContext* context) {
    Token* token = get_previous_token(context);
    return ast_node_make_boolean(token);
}

After that, we need to modify the parse rule table to include the new parsing functions.

static ParseRule parse_rules[] = {
        {parse_grouping_expression,              NULL,                             PREC_CALL},    // TOKEN_LEFT_PAREN,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_RIGHT_PAREN,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_LEFT_BRACE,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_RIGHT_BRACE,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_COMMA,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_DOT,
        {parse_unary_prefix_operator_expression, parse_binary_operator_expression, PREC_TERM},    // TOKEN_MINUS,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_MINUS_MINUS,
        {NULL,                                   parse_binary_operator_expression, PREC_TERM},    // TOKEN_PLUS,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_PLUS_PLUS,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_SEMICOLON,
        {NULL,                                   parse_binary_operator_expression, PREC_FACTOR},  // TOKEN_SLASH,
        {NULL,                                   parse_binary_operator_expression, PREC_FACTOR},  // TOKEN_STAR,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_QUESTION,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_COLON,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_AND_AND,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_PIPE_PIPE,

        {parse_unary_prefix_operator_expression, NULL,                             PREC_NONE},          // TOKEN_BANG,
        {NULL,                                   parse_binary_operator_expression, PREC_EQUALITY},      // TOKEN_BANG_EQUAL,
        {NULL,                                   NULL,                             PREC_NONE},          // TOKEN_EQUAL,
        {NULL,                                   parse_binary_operator_expression, PREC_EQUALITY},      // TOKEN_EQUAL_EQUAL,
        {NULL,                                   parse_binary_operator_expression, PREC_COMPARISON},    // TOKEN_GREATER,
        {NULL,                                   parse_binary_operator_expression, PREC_COMPARISON},    // TOKEN_GREATER_EQUAL,
        {NULL,                                   parse_binary_operator_expression, PREC_COMPARISON},    // TOKEN_LESS,
        {NULL,                                   parse_binary_operator_expression, PREC_COMPARISON},    // TOKEN_LESS_EQUAL,

        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_IDENTIFIER,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_STRING,
        {parse_number_literal_expression,        NULL,                             PREC_NONE},    // TOKEN_NUMBER,

        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_CLASS,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_ELSE,
        {parse_false_literal_expression,         NULL,                             PREC_NONE},    // TOKEN_FALSE,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_FUNC,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_FOR,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_IF,
        {parse_nil_literal_expression,           NULL,                             PREC_NONE},    // TOKEN_NIL,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_PRINT,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_RETURN,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_SUPER,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_THIS,
        {parse_true_literal_expression,          NULL,                             PREC_NONE},    // TOKEN_TRUE,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_VAR,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_WHILE,

        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_ERROR,
        {NULL,                                   NULL,                             PREC_NONE},    // TOKEN_EOF
};

4. Adding new opcodes

Probably this is the easiest part. We add the following new opcodes: OP_NIL, OP_TRUE, OP_FALSE, OP_NOT, OP_EQUAL, OP_LESS, OP_GREATER.

typedef enum opcode_t {
    OP_NOP = 0,
    OP_CONSTANT = 1,
    OP_ADD = 2,
    OP_SUBTRACT = 3,
    OP_MULTIPLY = 4,
    OP_DIVIDE = 5,
    OP_NEGATE = 6,
    OP_RETURN = 7,
    OP_NIL = 8,
    OP_TRUE = 9,
    OP_FALSE = 10,
    OP_NOT = 11,
    OP_EQUAL = 12,
    OP_LESS = 13,
    OP_GREATER = 14,
} Opcode;

5. Change the compiler to emit the new opcodes

This has three parts. First we add support for compiling the new reserved words true, false, and nil.

static void compile_boolean_node(CompilationContext* context) {
    context->current_node->as.boolean ? emit_byte(context, OP_TRUE) : emit_byte(context, OP_FALSE);
}

static void compile_nil_node(CompilationContext* context) {
    emit_byte(context, OP_NIL);
}

static void compile_ast_node(CompilationContext* context) {
    switch (context->current_node->type) {
        case NODE_INTEGER:
            compile_integer_node(context);
            break;
        case NODE_DOUBLE:
            compile_double_node(context);
            break;
        case NODE_BINARY_INFIX_OPERATOR:
            compile_binary_infix_node(context);
            break;
        case NODE_UNARY_PREFIX_OPERATOR:
            compile_unary_prefix_node(context);
            break;
        case NODE_BOOLEAN:
            compile_boolean_node(context);
            break;
        case NODE_NIL:
            compile_nil_node(context);
            break;
    }
}

In the second part we add support for <, <=, >, >=, ==, and !=. These are all binary infix operators, so we have to modify our existing compile_binary_infix_node function.

static void compile_binary_infix_node(CompilationContext* context) {
    AstNode* current_node = context->current_node;

    context->current_node = current_node->as.binary_infix.left;
    compile_ast_node(context);

    context->current_node = current_node->as.binary_infix.right;
    compile_ast_node(context);

    context->current_node = current_node;

    switch (current_node->token->type) {
        case TOKEN_PLUS:
            emit_byte(context, OP_ADD);
            break;
        case TOKEN_MINUS:
            emit_byte(context, OP_SUBTRACT);
            break;
        case TOKEN_STAR:
            emit_byte(context, OP_MULTIPLY);
            break;
        case TOKEN_SLASH:
            emit_byte(context, OP_DIVIDE);
            break;
        case TOKEN_BANG_EQUAL:
            emit_bytes(context, OP_EQUAL, OP_NOT);
            break;
        case TOKEN_EQUAL_EQUAL:
            emit_byte(context, OP_EQUAL);
            break;
        case TOKEN_GREATER_EQUAL:
            emit_bytes(context, OP_LESS, OP_NOT);
            break;
        case TOKEN_GREATER:
            emit_byte(context, OP_GREATER);
            break;
        case TOKEN_LESS_EQUAL:
            emit_bytes(context, OP_GREATER, OP_NOT);
            break;
        case TOKEN_LESS:
            emit_byte(context, OP_LESS);
            break;
        default:
            break;
    }
}

There are a couple of interesting bits here. You can see that >= is implemented as not less than, and <= becomes not greater than. These are going to be easier to implement in the VM when we add execution, that is the reason for not using a direct translation. Also note that we did not invent new opcodes for every operation, but rather we try to keep the minimal number of different opcodes and combine them if necessary to achieve the same effect. This is for educational purposes only, if performance would be the main goal we would implement different opcodes for each operation.

In the third part we add support for negation as well. Negation is a unary prefix operator so we modify compile_unary_prefix_node.

static void compile_unary_prefix_node(CompilationContext* context) {
    AstNode* current_node = context->current_node;

    context->current_node = current_node->as.unary_prefix.right;
    compile_ast_node(context);

    context->current_node = current_node;

    switch (context->current_node->token->type) {
        case TOKEN_MINUS:
            emit_byte(context, OP_NEGATE);
            break;
        case TOKEN_BANG:
            emit_byte(context, OP_NOT);
            break;
        default:
            break;
    }
}

6. Extending Value

First, we add some macros to make our lives easier later.

#define IS_BOOLEAN(value)   (value)->type == VAL_BOOLEAN
#define IS_NIL(value)       (value)->type == VAL_NIL

#define AS_BOOLEAN(value)   (value)->as.boolean_value

Then, we add two new value types, one for nil, one for boolean.

typedef enum value_type_t {
    VAL_DOUBLE,
    VAL_INTEGER,
    VAL_BOOLEAN,
    VAL_NIL,
} ValueType;

We also need to store boolean values somewhere in our tagged union, so let’s make some space for them.

typedef struct value_t {
    ValueType type;
    union {
        long double double_value;
        int64_t integer_value;
        bool boolean_value;
    } as;
} Value;

Finally, add some functions to create these values, and one to check values for equality.

Declaration:

extern Value* value_new_boolean_value(bool value);
extern Value* value_new_nil_value(bool value);
extern bool value_equals(const Value* one, const Value* other);

Definition for creating new values:

Value* value_new_boolean_value(bool value) {
    Value* v = NEW(Value);

    v->type = VAL_BOOLEAN;
    v->as.boolean_value = value;

    return v;
}

Value* value_new_nil_value(bool value) {
    Value* v = NEW(Value);

    v->type = VAL_NIL;
    v->as.integer_value = 0;

    return v;
}

Again, we need to ensure that we can print and debug them as well, so let’s add it to value_print.

void value_print(const Value* value) {
    switch (value->type) {
        case VAL_INTEGER:
            fprintf(stdout, "%lld", AS_INTEGER(value));
            break;
        case VAL_DOUBLE:
            fprintf(stdout, "%Lf", AS_DOUBLE(value));
            break;
        case VAL_BOOLEAN:
            fprintf(stdout, "%s", AS_BOOLEAN(value) ? "true" : "false");
            break;
        case VAL_NIL:
            fprintf(stdout, "nil");
            break;
        default:
            fprintf(stdout, "unknown");
            break;
    }
}

And finally, we implement the equality check.

bool value_equals(const Value* one, const Value* other) {
    assert(one != NULL && other != NULL);

    if (one->type != other->type) {
        return false;
    }

    switch (one->type) {
        case VAL_NIL:
            return true;
        case VAL_BOOLEAN:
            return AS_BOOLEAN(one) == AS_BOOLEAN(other);
        case VAL_INTEGER:
            return AS_INTEGER(one) == AS_INTEGER(other);
        case VAL_DOUBLE:
            return AS_DOUBLE(one) == AS_DOUBLE(other);
        default:
            return false;
    }
}

7. Extending the VM

This is probably the most complicated part of the whole process. First, we need to write some helper functions that will help us do comparisons and boolean checks. Also to make comparison work we add a small enum to indicate what kind of comparison we are making.

Add this to vm_functions.h.

typedef enum vm_binary_comparison_t {
    VM_BIN_COMP_LESS,
    VM_BIN_COMP_GREATER,
} VmBinaryComparison;

We also add it to our forward_declarations.h

// vm_functions.h
typedef enum vm_binary_operation_t VmBinaryOperation;
typedef enum vm_binary_comparison_t VmBinaryComparison;

And then we declare out helper functions:

extern void vm_binary_comparison(Vm* vm, VmBinaryComparison operation, bool* should_return, VmInterpretResult* result);
extern bool vm_value_is_false(Value* value);

Let’s define vm_binary_comparison.

void vm_binary_comparison(Vm* vm, VmBinaryComparison operation, bool* should_return, VmInterpretResult* result) {
    if (IS_NUMBER(vm_peek(vm, 0)) && IS_NUMBER(vm_peek(vm, 1))) {
        Value* b = vm_pop(vm);
        Value* a = vm_pop(vm);

        if (IS_DOUBLE(a) && !(IS_DOUBLE(b))) {
            b = value_new_double_value((long double) AS_INTEGER(b));
        }

        if (IS_DOUBLE(b) && !(IS_DOUBLE(a))) {
            a = value_new_double_value((long double) AS_INTEGER(a));
        }

        Value* new_value = NULL;

        switch (operation) {
            case VM_BIN_COMP_LESS:
                if (IS_DOUBLE(a) && IS_DOUBLE(b)) {
                    new_value = value_new_boolean_value(AS_DOUBLE(a) < AS_DOUBLE(b));
                } else {
                    new_value = value_new_boolean_value(AS_INTEGER(a) < AS_INTEGER(b));
                }
                break;
            case VM_BIN_COMP_GREATER:
                if (IS_DOUBLE(a) && IS_DOUBLE(b)) {
                    new_value = value_new_boolean_value(AS_DOUBLE(a) > AS_DOUBLE(b));
                } else {
                    new_value = value_new_boolean_value(AS_INTEGER(a) > AS_INTEGER(b));
                }
                break;
            default:
                break;
        }

        vm_push(vm, new_value);

        return;
    }

    *should_return = true;
    *result = VM_INTERPRET_RUNTIME_ERROR;
    vm_runtime_error(vm, "Operands must be numbers");
}

Let’s quickly recap what we really do here:

  • Pop two values from the stack, if they are not numbers we fail with an error because our comparison operators < and > will work only on numbers.
  • Make sure the two numbers are of the same type, convert them if necessary (between double and integer)
  • Do the comparison
  • Push the result onto the stack

Next is the function that checks if a value is false.

bool vm_value_is_false(Value* value) {
    return IS_NIL(value) || (IS_BOOLEAN(value) && !AS_BOOLEAN(value));
}

In this one the only interesting piece is that we treat nil as false in boolean comparisons.

And the last piece is to write the actual opcode handlers in vm_opcode_functions.c

static void op_true(Vm* vm, bool* should_return, VmInterpretResult* result) {
    Value* new_value = value_new_boolean_value(true);
    vm_push(vm, new_value);
}

static void op_false(Vm* vm, bool* should_return, VmInterpretResult* result) {
    Value* new_value = value_new_boolean_value(false);
    vm_push(vm, new_value);
}

static void op_nil(Vm* vm, bool* should_return, VmInterpretResult* result) {
    Value* new_value = value_new_nil_value(vm);
    vm_push(vm, new_value);
}

static void op_not(Vm* vm, bool* should_return, VmInterpretResult* result) {
    Value* value = vm_pop(vm);
    value = value_new_boolean_value(vm_value_is_false(value));
    vm_push(vm, value);
}

static void op_equal(Vm* vm, bool* should_return, VmInterpretResult* result) {
    Value* first = vm_pop(vm);
    Value* second = vm_pop(vm);

    first = value_new_boolean_value(value_equals(first, second));

    vm_push(vm, first);
}

static void op_less(Vm* vm, bool* should_return, VmInterpretResult* result) {
    vm_binary_comparison(vm, VM_BIN_COMP_LESS, should_return, result);
}

static void op_greater(Vm* vm, bool* should_return, VmInterpretResult* result) {
    vm_binary_comparison(vm, VM_BIN_COMP_GREATER, should_return, result);
}

And then we add them to the list of opcode handlers to make the VM use them.

const VmOpcodeFunction vm_opcode_functions[] = {
        op_nop,             // OP_NOP
        op_constant,        // OP_CONSTANT,
        op_add,             // OP_ADD,
        op_subtract,        // OP_SUBTRACT,
        op_multiply,        // OP_MULTIPLY,
        op_divide,          // OP_DIVIDE,
        op_negate,          // OP_NEGATE,
        op_return,          // OP_RETURN,
        op_nil,             // OP_NIL,
        op_true,            // OP_TRUE,
        op_false,           // OP_FALSE,
        op_not,             // OP_NOT,
        op_equal,           // OP_EQUAL,
        op_less,            // OP_LESS,
        op_greater,         // OP_GREATER,
};

8. Extend debugging

We are almost done, the added features are already fully operational. All we need is to be able to see the result of their work. Let’s extend the debugger to make it happen.

First we change debug.c.

We need to debug boolean and nil nodes.

static void debug_boolean_node(AstNode* node) {
    print_with_indent("BooleanExpressionNode {", 0);
    begin_scope();
    print_with_indent(node->as.boolean ? "true" : "false", 0);
    end_scope();
    print_with_indent("}", 0);
}

static void debug_nil_node(AstNode* node) {
    print_with_indent("NilExpressionNode {", 0);
    begin_scope();
    print_with_indent("nil", 0);
    end_scope();
    print_with_indent("}", 0);
}

void debug_ast_node(AstNode* node) {
    switch (node->type) {
        case NODE_INTEGER:
            debug_integer_node(node);
            break;
        case NODE_DOUBLE:
            debug_double_node(node);
            break;
        case NODE_BINARY_INFIX_OPERATOR:
            debug_binary_infix_node(node);
            break;
        case NODE_UNARY_PREFIX_OPERATOR:
            debug_unary_prefix_node(node);
            break;
        case NODE_BOOLEAN:
            debug_boolean_node(node);
            break;
        case NODE_NIL:
            debug_nil_node(node);
            break;
    }
}

Then we extend debug_disassemble_instruction to include the new opcodes.

int debug_disassemble_instruction(BytecodeArray* array, int offset) {
    printf("%08d ", offset);

    if (offset > 0 && array->lines.values[offset] == array->lines.values[offset - 1]) {
        printf("      | ");
    } else {
        printf("%7lld ", array->lines.values[offset]);
    }

    uint8_t instruction = array->code.values[offset];

    switch (instruction) {
        case OP_NOP:
            return simple_instruction("OP_NOP", offset);
        case OP_CONSTANT:
            return constant_instruction("OP_CONSTANT", array, offset);
        case OP_ADD:
            return simple_instruction("OP_ADD", offset);
        case OP_SUBTRACT:
            return simple_instruction("OP_SUBTRACT", offset);
        case OP_MULTIPLY:
            return simple_instruction("OP_MULTIPLY", offset);
        case OP_DIVIDE:
            return simple_instruction("OP_DIVIDE", offset);
        case OP_NEGATE:
            return simple_instruction("OP_NEGATE", offset);
        case OP_RETURN:
            return simple_instruction("OP_RETURN", offset);
        case OP_NIL:
            return simple_instruction("OP_NIL", offset);
        case OP_TRUE:
            return simple_instruction("OP_TRUE", offset);
        case OP_FALSE:
            return simple_instruction("OP_FALSE", offset);
        case OP_NOT:
            return simple_instruction("OP_NOT", offset);
        case OP_EQUAL:
            return simple_instruction("OP_EQUAL", offset);
        case OP_GREATER:
            return simple_instruction("OP_GREATER", offset);
        case OP_LESS:
            return simple_instruction("OP_LESS", offset);
        default:
            printf("Unknown opcode %d\n", instruction);
            return offset + 1;
    }
}

Now we go to vm.c and add some debugging as well.

static VmInterpretResult vm_run(Vm* vm) {
#if defined DEBUG_TRACE_EXECUTION
    printf("== <%s> bytecode start ==\n", "main");
    printf("%8s %7s %-16s %14s %s\n", "[offset]", "[line]", "[opcode]", "[constant pos]", "  [constant value]");
#endif

    for (;;) {
#if defined DEBUG_TRACE_EXECUTION
        vm_trace_execution(vm);
#endif

        Opcode instruction = (Opcode) vm_read_byte(vm);
        bool shouldReturn = false;
        VmInterpretResult result = VM_INTERPRET_OK;

        vm_opcode_functions[instruction](vm, &shouldReturn, &result);

        if (shouldReturn) {
#if defined DEBUG_TRACE_EXECUTION
            printf("== <%s> bytecode end ==\n", "main");
#endif
            return result;
        }
    }
}

And we are done!

Now there is only one thing to do, check if everything works as expected. Compile and run the project, then try some number and boolean comparisons.

ASPL REPL

Type 'Ctrl+C', 'quit' or 'q' to exit.

aspl> 1 == 1
== <main> bytecode start ==
[offset]  [line] [opcode]         [constant pos]   [constant value]
== Stack ==    
00000000       1 OP_CONSTANT                   0   '1'
== Stack ==    [1] 
00000002       | OP_CONSTANT                   1   '1'
== Stack ==    [1] [1] 
00000004       | OP_EQUAL
== Stack ==    [true] 
00000005       | OP_RETURN
== <main> bytecode end ==
ASPL REPL

Type 'Ctrl+C', 'quit' or 'q' to exit.

aspl> 2 <= 3
== <main> bytecode start ==
[offset]  [line] [opcode]         [constant pos]   [constant value]
== Stack ==    
00000000       1 OP_CONSTANT                   0   '2'
== Stack ==    [2] 
00000002       | OP_CONSTANT                   1   '3'
== Stack ==    [2] [3] 
00000004       | OP_GREATER
== Stack ==    [false] 
00000005       | OP_NOT
== Stack ==    [true] 
00000006       | OP_RETURN
== <main> bytecode end ==
ASPL REPL

Type 'Ctrl+C', 'quit' or 'q' to exit.

aspl> false == true
== <main> bytecode start ==
[offset]  [line] [opcode]         [constant pos]   [constant value]
== Stack ==    
00000000       1 OP_FALSE
== Stack ==    [false] 
00000001       | OP_TRUE
== Stack ==    [false] [true] 
00000002       | OP_EQUAL
== Stack ==    [false] 
00000003       | OP_RETURN
== <main> bytecode end ==
aspl> true == true
== <main> bytecode start ==
[offset]  [line] [opcode]         [constant pos]   [constant value]
== Stack ==    
00000000       1 OP_TRUE
== Stack ==    [true] 
00000001       | OP_TRUE
== Stack ==    [true] [true] 
00000002       | OP_EQUAL
== Stack ==    [true] 
00000003       | OP_RETURN
== <main> bytecode end ==
aspl> false != true
== <main> bytecode start ==
[offset]  [line] [opcode]         [constant pos]   [constant value]
== Stack ==    
00000000       1 OP_FALSE
== Stack ==    [false] 
00000001       | OP_TRUE
== Stack ==    [false] [true] 
00000002       | OP_EQUAL
== Stack ==    [false] 
00000003       | OP_NOT
== Stack ==    [true] 
00000004       | OP_RETURN
== <main> bytecode end ==
ASPL REPL

Type 'Ctrl+C', 'quit' or 'q' to exit.

aspl> !true
== <main> bytecode start ==
[offset]  [line] [opcode]         [constant pos]   [constant value]
== Stack ==    
00000000       1 OP_TRUE
== Stack ==    [true] 
00000001       | OP_NOT
== Stack ==    [false] 
00000002       | OP_RETURN
== <main> bytecode end ==

Looks like it’s working. Great!

Next time we will start thinking about strings, and the ideas behind their implementation.

Grab the source code of the current state of the project if you like.

Stay tuned. I’ll be back.

Tags: boolean expressions

← Boolean expressions - extending the grammar Implementing strings →

comments powered by Disqus