Equipped with this knowledge, we can now implement all the parts. First, we must create the definition of the AST node for a variable that’s stored in the include/llvm/tinylang/AST/AST.h file. Besides support for the LLVM-style RTTI, the base class stores the name of the declaration, the location of the name, and a pointer to the enclosing declaration. The latter is required during code generation of nested procedures. The Decl base class is declared as follows:

class Decl {
public:
enum DeclKind { DK_Module, DK_Const, DK_Type,
DK_Var, DK_Param, DK_Proc };
private:
const DeclKind Kind;
protected:
Decl *EnclosingDecL;
SMLoc Loc;
StringRef Name;
public:
Decl(DeclKind Kind, Decl *EnclosingDecL, SMLoc Loc,
StringRef Name)
: Kind(Kind), EnclosingDecL(EnclosingDecL), Loc(Loc),
Name(Name) {}
DeclKind getKind() const { return Kind; }
SMLoc getLocation() { return Loc; }
StringRef getName() { return Name; }
Decl *getEnclosingDecl() { return EnclosingDecL; }
};

The declaration for a variable only adds a pointer to the type declaration:

class TypeDeclaration;
class VariableDeclaration : public Decl {
TypeDeclaration *Ty;
public:
VariableDeclaration(Decl *EnclosingDecL, SMLoc Loc,
StringRef Name, TypeDeclaration *Ty)
: Decl(DK_Var, EnclosingDecL, Loc, Name), Ty(Ty) {}
TypeDeclaration *getType() { return Ty; }
static bool classof(const Decl *D) {
return D->getKind() == DK_Var;
}
};

The method in the parser needs to be extended with a semantic action and variables for collected information:

bool Parser::parseVariableDeclaration(DeclList &Decls) {
auto _errorhandler = [this] {
while (!Tok.is(tok::semi)) {
advance();
if (Tok.is(tok::eof)) return true;
}
return false;
};
Decl *D = nullptr; IdentList Ids;
if (parseIdentList(Ids)) return _errorhandler();
if (consume(tok::colon)) return _errorhandler();
if (parseQualident(D)) return _errorhandler();
Actions.actOnVariableDeclaration(Decls, Ids, D);
return false;
}

DeclList is a list of declarations, std::vector, and IdentList is a list of locations and identifiers, std::vector>.
The parseQualident() method returns a declaration, which in this case is expected to be a type declaration.
The parser class knows an instance of the semantic analyzer class, Sema, that’s stored in the Actions member. A call to actOnVariableDeclaration() runs the semantic analyzer and the AST construction. The implementation is in the lib/Sema/Sema.cpp file:

void Sema::actOnVariableDeclaration(DeclList &Decls,
IdentList &Ids,
Decl *D) {
if (TypeDeclaration *Ty = dyn_cast(D)) {
for (auto &[Loc, Name] : Ids) {
auto *Decl = new VariableDeclaration(CurrentDecl, Loc,
Name, Ty);
if (CurrentScope->insert(Decl))
Decls.push_back(Decl);
else
Diags.report(Loc, diag::err_symbold_declared, Name);
}
} else if (!Ids.empty()) {
SMLoc Loc = Ids.front().first;
Diags.report(Loc, diag::err_vardecl_requires_type);
}
}

The type declaration is checked with llvm::dyn_cast. If it is not a type declaration, then an error message is printed. Otherwise, for each name in the Ids list, VariableDeclaration is instantiated and added to the list of declarations. If adding the variable to the current scope fails because the name is already declared, then an error message is printed as well.
Most of the other entities are constructed in the same way – the complexity of the semantic analysis is the only difference. More work is required for modules and procedures because they open a new scope. Opening a new scope is easy: only a new Scope object must be instantiated. As soon as the module or procedure has been parsed, the scope must be removed.

Leave a Reply

Your email address will not be published. Required fields are marked *