I'm creating a compiler and really struggling with the semantic analysis phase. I'm not sure how to deal with function overloading in a symbol table. I can't seem to find any resources that describe this particular problem. I think that name mangling must be used somewhere and I'm pretty sure that types from the AST should be converted to strings.
Multiple functions with the same name are allowed to be declared in the same scope as long as each declaration has a different set of parameters. The following snippet is an example in my language (it's quite similar to Swift).
func add(a: Int, b: Int) {
return a + b;
}
func add(a: Float, b: Float) {
return a + b;
}
I don't know how to store functions in the symbol table. This is part of my symbol table data structure.
struct Symbol {};
struct Var final : Symbol {
std::string type;
};
using FuncParams = std::vector<std::string>;
struct Func final : Symbol {
std::string ret;
FuncParams params;
};
using Table = std::unordered_map<std::string, std::unique_ptr<Symbol>>;
struct Scope {
Table table;
Scope *parent = nullptr;
};
using Scopes = std::vector<std::unique_ptr<Scope>>;
I could use a std::unordered_multimap
and store the function name add
as the key and store the names of the parameters in the symbol object. I could use a std::unordered_map
and store the function name decorated with the parameters add_Int_Int
as the key and store the names of the parameters in the symbol object as well.
Also, should Symbol
be a base class or should I put all kinds of symbols in the one Symbol
object? I've seen many examples of using an enum
to differentiate between functions, variables and type declarations but a function stores return type and parameter types. Should I use a tagged union?
I feel like there's a smart and simple way of solving this but I just can't find it.
Update:
I've taken a suggestion from @NeilButterworth and used unmangled function names as the symbol keys and this seems to be the way to go (but what do I know!). Answers to my other questions or some advice on this topic would be appreciated.
The solution is to do what @NeilButtworth said in the comments. But you will want to build some logic around creating "compatible" type trees. Becuase you want a function to have parameters that are compatible. I would recommend some function that returns if a type is a child of another type like isTypeCompatible(actualType,formalType)
. Then from there you can try and find the function that has the most specific signature that matches the call. So f(5)
should call f(int)
since 5 is an explicit int but only an implicit float. One way of doing this is by seeing which signature minimizes the distance up a type tree from the given types and the formal types.
EDIT: By "compatible" type trees it essentially the same as inheritance. Like how if we have g(float) g(5)
works since an int literal is convertible to a float literal. The type trees are much easier to construct with custom user classes since you AST should already construct upon parsing and type checking.