src/main.cpp
author David Anderson <dvander@alliedmods.net>
Sat Nov 17 18:41:01 2012 -0800 (2012-11-17)
changeset 195 c1ba166f3fc4
parent 184 387d76b1e981
child 196 9904633b1dfc
permissions -rw-r--r--
Massive SemA/BC refactoring to support better type systems. Read on for more.

The previous compiler had a simple pipeline, best outlined as:
(1) Parsing and name binding -> AST
(2) Storage allocation -> AST
(3) Semantic Analysis (SemA) -> AST
(4) Bytecode Compilation (BC) -> Code

This pipeline, unfortunately, had two problems. First, name binding cannot be performed in one pass if we wish to have multi-file support in the form of modules. Name binding must be two-pass.

Second, because SemA was only capable of annotating AST nodes with a single type, most coercion work had to be duplicated in the BC phase, as coercion was not explicit in the SemA output. This made introducing new type rules extremely difficult, and all but precluded concepts like operator overloading (or overloading at all).

This patch rewrites most of Keima's backend. Of interest are the following changes:
(1) CompileContext has been refactored around future multi-file support.
(2) Parsing no longer performs any name binding.
(3) The grammar for type-and-name has been changed from:
(Label | Identifier)? Identifier
to:
Type? Identifier
where:
Type ::= OldType | NewType
OldType ::= Label
NewType ::= Identifier (. NewType)*


Although we do not implement the full production for Type yet, this
distinction is important. A type identifier is now affixed to the AST
as an Expression (right now, always a NameProxy), so it can fully
participate in name binding.

(4) Immediately after parsing, the AST goes through a NamePopulation phase.
This phase creates Scope objects for every scope which declares a name,
and if those names are declaring types or functions, a Symbol is created
and entered into the scope. Symbols entirely replace the old BoundName
classes.

(5) After NamePopulation comes NameBinding. This phase performs three steps:
(a) Links Scope objects together, to form a tree.
(b) Binds any free name to an existing name in the scope chain.
(c) Creates and registers any Symbols for names which have not yet been
added to the scope (for example, local variables).

In accordance, all "allocation" and "binding" concepts have been removed
from Scopes, which are now lightweight container classes.

(6) SemA now generates bytecode for each statement in the AST. Expression
compilation is performed via a separate mechanism called HIR (High-level
Intermediate Representation). To analyze an expression, SemA will walk
the AST, and produce a HIR object for each node. HIR is typed, and SemA
may insert coercion nodes, or even expand an AST node into multiple HIR
nodes. The result of evaluating an expression in SemA is therefore the
the root of a HIR tree, which SemA then sends to the HIRTranslator, which
performs bytecode generation.

As before, SemA still performs full semantic analysis. However, not it
also produces each function's bytecode.

This split allows us to decompose potentially complex semantics into
a more fine-grained, AST-like structure, which can have very simple
code-generation logic. For example, (1.0 + 5) might look like:

HAdd(HFloat(1.0), HConvert(HInteger(5), <Float>))

As part of this decomposition, many opcodes have been removed. Rather than
using typed jumps, we now expand jumps into longer tests. For example:
jge.f <label>
becomes:
ge.f
cvt.f2b
jt <label>

This simplifies the pipeline, and JITs should be able to melt the added
work away.

HIR is not used at a statement level, and HIR does not have any concept
of control-flow.

(7) The old BytecodeCompiler has been removed, as its work is now split
between SemA and HIRTranslator.


In addition, some minor refactorings have taken place:

(1) Opcodes.tbl no longer hardcodes numbers (aaah).
(2) Type has been split into smaller, typed structs.
(3) BytecodeEmitter no longer relies on Pools or RootScopes.
(4) SemA is now responsible for variable/storage allocation.
(5) Native table are now global, rather than per-module. As such, native
declarations now result in a Native object (which still requires a
CALLNATIVE opcode), which is an index into the global table. Natives
must be bound globally. This is in preparation for module support.
(6) Publics are no longer tracked, but rather registered via a global
callback upon module load. This callback can be set by embedders. This
is in preparation for multi-file support.
(7) The BytecodeEmitter now performs Symbol allocations itself, and it
also generates Code objects itself, which removes a good deal of
complexity.

Finally, a test harness has been added. This harness is a python script which finds *.test files, recursively, in the tests folder. For each file it runs the corresponding .sp. The contents of the .test file must match stdout+stderr.
dvander@163
     1
/* vim: set ts=4 sw=4 tw=99 et:
dvander@163
     2
 *
dvander@71
     3
 * Copyright (C) 2012 David Anderson
dvander@71
     4
 *
dvander@163
     5
 * This file is part of SourcePawn.
dvander@71
     6
 *
dvander@163
     7
 * SourcePawn is free software: you can redistribute it and/or modify it under
dvander@71
     8
 * the terms of the GNU General Public License as published by the Free
dvander@71
     9
 * Software Foundation, either version 3 of the License, or (at your option)
dvander@71
    10
 * any later version.
dvander@71
    11
 * 
dvander@163
    12
 * SourcePawn is distributed in the hope that it will be useful, but WITHOUT ANY
dvander@71
    13
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
dvander@71
    14
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
dvander@71
    15
 *
dvander@71
    16
 * You should have received a copy of the GNU General Public License along with
dvander@163
    17
 * SourcePawn. If not, see http://www.gnu.org/licenses/.
dvander@71
    18
 */
dvander@71
    19
#include <stdio.h>
dvander@161
    20
#include <stdlib.h>
dvander@163
    21
#include <string.h>
dvander@71
    22
#include "../include/keima.h"
dvander@71
    23
dvander@161
    24
using namespace Ke;
dvander@71
    25
dvander@161
    26
class MessageReporter : public IMessageReporter
dvander@69
    27
{
dvander@69
    28
  public:
dvander@69
    29
    void onOutOfMemory() {
dvander@69
    30
        fprintf(stderr, "Out of memory!\n");
dvander@69
    31
    }
dvander@69
    32
    void onUncaughtException(const char *message) {
dvander@69
    33
        fprintf(stderr, "Uncaught exception: %s\n", message);
dvander@69
    34
dvander@161
    35
        IStackFrameIterator *iter = Keima::NewStackFrameIterator();
dvander@69
    36
        unsigned index = 0;
dvander@174
    37
        while (!iter->done() && index < 10) {
dvander@69
    38
            fprintf(stderr, "[%d] %s (%s line %d)\n",
dvander@69
    39
                    index,
dvander@69
    40
                    iter->function(),
dvander@69
    41
                    iter->source(),
dvander@69
    42
                    iter->lineNumber());
dvander@69
    43
            iter->prev();
dvander@69
    44
            index++;
dvander@69
    45
        }
dvander@69
    46
    }
dvander@71
    47
    virtual void beginCompilationErrors(const char *file) {
dvander@71
    48
        fprintf(stderr, "\nDisplaying compilation errors for \"%s\"\n", file);
dvander@71
    49
    }
dvander@71
    50
    virtual void reportCompilationError(const char *message, unsigned line, unsigned col) {
dvander@71
    51
        fprintf(stderr, "  line %d, col %d: %s\n", line, col, message);
dvander@71
    52
    }
dvander@71
    53
    virtual void endCompilationErrors() {
dvander@71
    54
    }
dvander@71
    55
} s_Reporter;
dvander@71
    56
dvander@195
    57
class MainCollector : public Ke::IPublicFunctionListener
dvander@195
    58
{
dvander@195
    59
  public:
dvander@195
    60
    MainCollector()
dvander@195
    61
    {
dvander@195
    62
    }
dvander@195
    63
    void registerPublic(Handle<Function> fun) {
dvander@195
    64
        Local<String> str = fun->name();
dvander@195
    65
        if (str && strcmp(str->chars(), "main") == 0)
dvander@195
    66
            fun_ = fun;
dvander@195
    67
    }
dvander@195
    68
dvander@195
    69
    void unregisterPublic(Handle<Function> fun) {
dvander@195
    70
        if (fun_ == fun)
dvander@195
    71
            fun_ = Handle<Function>();
dvander@195
    72
    }
dvander@195
    73
dvander@195
    74
    Handle<Function> fun() const {
dvander@195
    75
        return fun_;
dvander@195
    76
    }
dvander@195
    77
dvander@195
    78
  private:
dvander@195
    79
    Root<Function> fun_;
dvander@195
    80
};
dvander@195
    81
dvander@163
    82
intptr_t PrintFloat(void *blah, intptr_t *params)
dvander@71
    83
{
dvander@163
    84
    fprintf(stdout, "%f", *reinterpret_cast<float *>(&params[1]));
dvander@163
    85
    return 0;
dvander@163
    86
}
dvander@71
    87
dvander@163
    88
intptr_t PrintNum(void *blah, intptr_t *params)
dvander@163
    89
{
dvander@163
    90
    fprintf(stdout, "%i", *reinterpret_cast<int *>(&params[1]));
dvander@163
    91
    return 0;
dvander@163
    92
}
dvander@71
    93
dvander@163
    94
intptr_t PrintString(void *blah, intptr_t *params)
dvander@71
    95
{
dvander@163
    96
    fprintf(stdout, "%s", *reinterpret_cast<const char **>(&params[1]));
dvander@163
    97
    return 0;
dvander@71
    98
}
dvander@71
    99
dvander@71
   100
int main(int argc, char **argv)
dvander@71
   101
{
dvander@71
   102
    if (argc < 2) {
dvander@71
   103
        fprintf(stderr, "Usage: <file>\n");
dvander@71
   104
        return 1;
dvander@71
   105
    }
dvander@71
   106
dvander@161
   107
    if (!Keima::Initialize())
dvander@71
   108
        return 1;
dvander@163
   109
dvander@163
   110
    int rval = 0;
dvander@163
   111
dvander@161
   112
    Keima::InstallMessageReporter(&s_Reporter);
dvander@71
   113
    {
dvander@195
   114
        MainCollector entry;
dvander@195
   115
        Keima::SetPublicFunctionListener(&entry);
dvander@195
   116
dvander@161
   117
        HandleScope scope;
dvander@195
   118
        Compile(argv[1]);
dvander@71
   119
dvander@163
   120
        Keima::CollectGarbage();
dvander@163
   121
dvander@195
   122
        Keima::BindNative("PrintFloat", PrintFloat);
dvander@195
   123
        Keima::BindNative("PrintNum", PrintNum);
dvander@195
   124
        Keima::BindNative("PrintString", PrintString);
dvander@163
   125
dvander@195
   126
        Handle<Function> fun = entry.fun();
dvander@163
   127
dvander@163
   128
        if (!fun) {
dvander@195
   129
            fprintf(stdout, "Not running; no public 'main'.\n");
dvander@163
   130
        } else {
dvander@163
   131
            if (!Keima::Invoke(fun, NULL, 0, true, &rval))
dvander@163
   132
                rval = 1;
dvander@163
   133
        }
dvander@71
   134
    }
dvander@71
   135
dvander@161
   136
    Keima::Shutdown();
dvander@163
   137
dvander@163
   138
    return rval;
dvander@71
   139
}
danderson@92
   140
danderson@92
   141
/* Overload a few things to prevent libstdc++ linking */
danderson@92
   142
#if defined __linux__ || defined __APPLE__
danderson@92
   143
extern "C" void __cxa_pure_virtual(void)
danderson@92
   144
{
danderson@92
   145
}
danderson@92
   146
danderson@92
   147
void *operator new(size_t size)
danderson@92
   148
{
dvander@163
   149
    return malloc(size);
danderson@92
   150
}
danderson@92
   151
danderson@92
   152
void *operator new[](size_t size) 
danderson@92
   153
{
dvander@163
   154
    return malloc(size);
danderson@92
   155
}
danderson@92
   156
danderson@92
   157
void operator delete(void *ptr) 
danderson@92
   158
{
dvander@163
   159
    free(ptr);
danderson@92
   160
}
danderson@92
   161
danderson@92
   162
void operator delete[](void * ptr)
danderson@92
   163
{
dvander@163
   164
    free(ptr);
danderson@92
   165
}
danderson@92
   166
#endif
danderson@92
   167