Commit 5c31a0a6 authored by Alastair Robertson's avatar Alastair Robertson

Codegen for C structs

parent 2f0c01df
......@@ -110,8 +110,10 @@ public:
class Cast : public Expression {
public:
Cast(const std::string &type, Expression *expr) : cast_type(type), expr(expr) { }
Cast(const std::string &type, bool is_pointer, Expression *expr)
: cast_type(type), is_pointer(is_pointer), expr(expr) { }
std::string cast_type;
bool is_pointer;
Expression *expr;
void accept(Visitor &v) override;
......
......@@ -180,7 +180,7 @@ void CodegenLLVM::visit(Call &call)
static int printf_id = 0;
auto args = std::get<1>(bpftrace_.printf_args_.at(printf_id));
for (SizedType t : args)
for (SizedType &t : args)
{
llvm::Type *ty = b_.GetType(t);
elements.push_back(ty);
......@@ -291,32 +291,112 @@ void CodegenLLVM::visit(Binop &binop)
void CodegenLLVM::visit(Unop &unop)
{
assert(unop.expr->type.type == Type::integer);
unop.expr->accept(*this);
SizedType &type = unop.expr->type;
if (type.type == Type::integer)
{
switch (unop.op) {
case bpftrace::Parser::token::LNOT: expr_ = b_.CreateNot(expr_); break;
case bpftrace::Parser::token::BNOT: expr_ = b_.CreateNeg(expr_); break;
case bpftrace::Parser::token::MUL:
{
AllocaInst *dst = b_.CreateAllocaBPF(unop.expr->type, "deref");
b_.CreateProbeRead(dst, unop.expr->type.size, expr_);
int size = type.size;
if (type.is_pointer)
{
// When dereferencing a 32-bit integer, only read in 32-bits, etc.
size = type.pointee_size;
}
AllocaInst *dst = b_.CreateAllocaBPF(SizedType(type.type, size), "deref");
b_.CreateProbeRead(dst, size, expr_);
expr_ = b_.CreateLoad(dst);
b_.CreateLifetimeEnd(dst);
break;
}
default: abort();
}
}
else if (type.type == Type::cast)
{
// Do nothing
}
else
{
abort();
}
}
void CodegenLLVM::visit(FieldAccess &acc)
{
// TODO
SizedType &type = acc.expr->type;
assert(type.type == Type::cast);
acc.expr->accept(*this);
auto &field = bpftrace_.structs_[type.cast_type].fields[acc.field];
if (type.is_internal)
{
// The struct we are reading from has already been pulled into
// BPF-memory, e.g. by being stored in a map.
// Just read from the correct offset of expr_
Value *src = b_.CreateGEP(expr_, {b_.getInt64(0), b_.getInt64(field.offset)});
if (field.type.type == Type::cast)
{
// TODO This should be do-able without allocating more memory here
AllocaInst *dst = b_.CreateAllocaBPF(field.type, "internal_" + type.cast_type + "." + acc.field);
b_.CreateMemCpy(dst, src, field.type.size, 1);
expr_ = dst;
// TODO clean up dst memory?
}
else if (field.type.type == Type::string)
{
expr_ = src;
}
else
{
expr_ = b_.CreateLoad(b_.GetType(field.type), src);
}
}
else
{
// The struct we are reading from has not been pulled into BPF-memory,
// so expr_ will contain an external pointer to the start of the struct
Value *src = b_.CreateAdd(expr_, b_.getInt64(field.offset));
if (field.type.type == Type::cast && !field.type.is_pointer)
{
// struct X
// {
// struct Y y;
// };
//
// We are trying to access an embedded struct, e.g. "x.y"
//
// Instead of copying the entire struct Y in, we'll just store it as a
// pointer internally and dereference later when necessary.
expr_ = src;
}
else if (field.type.type == Type::string)
{
AllocaInst *dst = b_.CreateAllocaBPF(field.type, type.cast_type + "." + acc.field);
b_.CreateProbeRead(dst, field.type.size, src);
expr_ = dst;
}
else
{
AllocaInst *dst = b_.CreateAllocaBPF(field.type, type.cast_type + "." + acc.field);
b_.CreateProbeRead(dst, field.type.size, src);
expr_ = b_.CreateLoad(dst);
b_.CreateLifetimeEnd(dst);
}
}
}
void CodegenLLVM::visit(Cast &cast)
{
// TODO
cast.expr->accept(*this);
}
void CodegenLLVM::visit(ExprStatement &expr)
......@@ -336,12 +416,32 @@ void CodegenLLVM::visit(AssignMapStatement &assignment)
Value *val, *expr;
expr = expr_;
AllocaInst *key = getMapKey(map);
if (assignment.expr->type.type == Type::string)
if (map.type.type == Type::string)
{
val = expr;
}
else if (map.type.type == Type::cast)
{
if (assignment.expr->type.is_internal)
{
val = expr;
}
else
{
// expr currently contains a pointer to the struct
// We now want to read the entire struct in so we can save it
AllocaInst *dst = b_.CreateAllocaBPF(map.type, map.ident + "_val");
b_.CreateProbeRead(dst, map.type.size, expr);
val = dst;
}
}
else
{
if (map.type.type == Type::integer)
{
// Integers are always stored as 64-bit in map values
expr = b_.CreateIntCast(expr, b_.getInt64Ty(), false);
}
val = b_.CreateAllocaBPF(map.type, map.ident + "_val");
b_.CreateStore(expr, val);
}
......@@ -425,7 +525,7 @@ AllocaInst *CodegenLLVM::getMapKey(Map &map)
{
size += expr->type.size;
}
key = b_.CreateAllocaMapKey(size, map.ident + "_key");
key = b_.CreateAllocaBPF(size, map.ident + "_key");
int offset = 0;
for (Expression *expr : *map.vargs) {
......@@ -455,7 +555,7 @@ AllocaInst *CodegenLLVM::getQuantizeMapKey(Map &map, Value *log2)
{
size += expr->type.size;
}
key = b_.CreateAllocaMapKey(size, map.ident + "_key");
key = b_.CreateAllocaBPF(size, map.ident + "_key");
int offset = 0;
for (Expression *expr : *map.vargs) {
......
......@@ -48,7 +48,7 @@ AllocaInst *IRBuilderBPF::CreateAllocaBPF(const SizedType &stype, const std::str
return CreateAllocaBPF(ty, name);
}
AllocaInst *IRBuilderBPF::CreateAllocaMapKey(int bytes, const std::string &name)
AllocaInst *IRBuilderBPF::CreateAllocaBPF(int bytes, const std::string &name)
{
llvm::Type *ty = ArrayType::get(getInt8Ty(), bytes);
return CreateAllocaBPF(ty, name);
......@@ -57,7 +57,7 @@ AllocaInst *IRBuilderBPF::CreateAllocaMapKey(int bytes, const std::string &name)
llvm::Type *IRBuilderBPF::GetType(const SizedType &stype)
{
llvm::Type *ty;
if (stype.type == Type::string || stype.type == Type::cast)
if (stype.type == Type::string || (stype.type == Type::cast && !stype.is_pointer))
{
ty = ArrayType::get(getInt8Ty(), stype.size);
}
......@@ -121,22 +121,23 @@ Value *IRBuilderBPF::CreateMapLookupElem(Map &map, AllocaInst *key)
CreateCondBr(condition, lookup_success_block, lookup_failure_block);
SetInsertPoint(lookup_success_block);
if (map.type.type == Type::string)
if (map.type.type == Type::string || map.type.type == Type::cast)
CreateMemCpy(value, call, map.type.size, 1);
else
CreateStore(CreateLoad(getInt64Ty(), call), value);
CreateBr(lookup_merge_block);
SetInsertPoint(lookup_failure_block);
if (map.type.type == Type::string)
if (map.type.type == Type::string || map.type.type == Type::cast)
CreateMemSet(value, getInt8(0), map.type.size, 1);
else
CreateStore(getInt64(0), value);
CreateBr(lookup_merge_block);
SetInsertPoint(lookup_merge_block);
if (map.type.type == Type::string)
if (map.type.type == Type::string || map.type.type == Type::cast)
return value;
return CreateLoad(value);
}
......
......@@ -20,7 +20,7 @@ public:
AllocaInst *CreateAllocaBPF(llvm::Type *ty, const std::string &name="");
AllocaInst *CreateAllocaBPF(const SizedType &stype, const std::string &name="");
AllocaInst *CreateAllocaMapKey(int bytes, const std::string &name="");
AllocaInst *CreateAllocaBPF(int bytes, const std::string &name="");
llvm::Type *GetType(const SizedType &stype);
CallInst *CreateBpfPseudoCall(int mapfd);
CallInst *CreateBpfPseudoCall(Map &map);
......
......@@ -101,6 +101,9 @@ void Printer::visit(FieldAccess &acc)
void Printer::visit(Cast &cast)
{
std::string indent(depth_, ' ');
if (cast.is_pointer)
out_ << indent << "(" << cast.cast_type << "*)" << std::endl;
else
out_ << indent << "(" << cast.cast_type << ")" << std::endl;
++depth_;
......
......@@ -231,24 +231,33 @@ void SemanticAnalyser::visit(Unop &unop)
{
unop.expr->accept(*this);
SizedType &type = unop.expr->type;
if (is_final_pass() &&
unop.expr->type.type != Type::integer &&
unop.expr->type.type != Type::cast) {
!(type.type == Type::integer) &&
!(type.type == Type::cast && unop.op == Parser::token::MUL)) {
err_ << "The " << opstr(unop) << " operator can not be used on expressions of type '"
<< unop.expr->type << "'" << std::endl;
<< type << "'" << std::endl;
}
if (unop.op == Parser::token::MUL && unop.expr->type.type == Type::cast) {
std::string cast_type = unop.expr->type.cast_type;
if (cast_type.back() == '*') {
cast_type.pop_back();
unop.type = SizedType(Type::cast, 8, cast_type);
if (unop.op == Parser::token::MUL) {
if (type.type == Type::cast) {
if (type.is_pointer) {
if (bpftrace_.structs_.count(type.cast_type) == 0) {
err_ << "Unknown struct/union: '" << type.cast_type << "'" << std::endl;
return;
}
int cast_size = bpftrace_.structs_[type.cast_type].size;
unop.type = SizedType(Type::cast, cast_size, type.cast_type);
}
else {
err_ << "Can not dereference struct/union of type '" << cast_type << "'. "
err_ << "Can not dereference struct/union of type '" << type.cast_type << "'. "
<< "It is not a pointer." << std::endl;
}
}
else if (type.type == Type::integer) {
unop.type = SizedType(Type::integer, type.size);
}
}
else {
unop.type = SizedType(Type::integer, 8);
}
......@@ -258,30 +267,35 @@ void SemanticAnalyser::visit(FieldAccess &acc)
{
acc.expr->accept(*this);
if (acc.expr->type.type != Type::cast) {
SizedType &type = acc.expr->type;
if (type.type != Type::cast) {
if (is_final_pass()) {
err_ << "Can not access field '" << acc.field
<< "' on expression of type '" << acc.expr->type
<< "' on expression of type '" << type
<< "'" << std::endl;
}
return;
}
std::string cast_type = acc.expr->type.cast_type;
if (cast_type.back() == '*') {
if (type.is_pointer) {
err_ << "Can not access field '" << acc.field << "' on type '"
<< cast_type << "'. Try dereferencing it first, or using '->'"
<< type.cast_type << "'. Try dereferencing it first, or using '->'"
<< std::endl;
return;
}
if (bpftrace_.structs_.count(type.cast_type) == 0) {
err_ << "Unknown struct/union: '" << type.cast_type << "'" << std::endl;
return;
}
auto fields = bpftrace_.structs_[cast_type].fields;
auto fields = bpftrace_.structs_[type.cast_type].fields;
if (fields.count(acc.field) == 0) {
err_ << "Struct/union of type '" << cast_type << "' does not contain "
err_ << "Struct/union of type '" << type.cast_type << "' does not contain "
<< "a field named '" << acc.field << "'" << std::endl;
}
else {
acc.type = fields[acc.field].type;
acc.type.is_internal = type.is_internal;
}
}
......@@ -289,22 +303,20 @@ void SemanticAnalyser::visit(Cast &cast)
{
cast.expr->accept(*this);
std::string cast_type = cast.cast_type;
if (cast_type.back() == '*')
cast_type.pop_back();
if (bpftrace_.structs_.count(cast_type) == 0) {
err_ << "Unknown struct/union: '" << cast_type << "'" << std::endl;
if (bpftrace_.structs_.count(cast.cast_type) == 0) {
err_ << "Unknown struct/union: '" << cast.cast_type << "'" << std::endl;
return;
}
int cast_size;
if (cast.cast_type.back() == '*') {
if (cast.is_pointer) {
cast_size = sizeof(uintptr_t);
}
else {
cast_size = bpftrace_.structs_[cast.cast_type].size;
}
cast.type = SizedType(Type::cast, cast_size, cast.cast_type);
cast.type.is_pointer = cast.is_pointer;
}
void SemanticAnalyser::visit(ExprStatement &expr)
......@@ -338,6 +350,11 @@ void SemanticAnalyser::visit(AssignMapStatement &assignment)
else {
// This map hasn't been seen before
map_val_.insert({map_ident, assignment.expr->type});
if (map_val_[map_ident].type == Type::integer) {
// Store all integer values as 64-bit in maps, so that there will
// be space for any integer to be assigned to the map later
map_val_[map_ident].size = 8;
}
}
if (assignment.expr->type.type == Type::cast) {
......@@ -351,6 +368,7 @@ void SemanticAnalyser::visit(AssignMapStatement &assignment)
}
else {
map_val_[map_ident].cast_type = cast_type;
map_val_[map_ident].is_internal = true;
}
}
}
......
......@@ -93,7 +93,6 @@ void yyerror(bpftrace::Driver &driver, const char *s);
%type <ast::AttachPointList *> attach_points
%type <ast::AttachPoint *> attach_point
%type <std::string> wildcard
%type <std::string> type
%type <std::string> ident
%right ASSIGN
......@@ -190,11 +189,8 @@ expr : INT { $$ = new ast::Integer($1); }
| MUL expr %prec DEREF { $$ = new ast::Unop(token::MUL, $2); }
| expr DOT ident { $$ = new ast::FieldAccess($1, $3); }
| expr PTR ident { $$ = new ast::FieldAccess(new ast::Unop(token::MUL, $1), $3); }
| "(" type ")" expr %prec CAST { $$ = new ast::Cast($2, $4); }
;
type : IDENT { $$ = $1; }
| IDENT MUL { $$ = $1 + "*"; }
| "(" IDENT ")" expr %prec CAST { $$ = new ast::Cast($2, false, $4); }
| "(" IDENT MUL ")" expr %prec CAST { $$ = new ast::Cast($2, true, $5); }
;
ident : IDENT { $$ = $1; }
......
......@@ -13,6 +13,8 @@ std::ostream &operator<<(std::ostream &os, Type type)
std::ostream &operator<<(std::ostream &os, const SizedType &type)
{
os << type.type;
if (type.is_pointer)
os << "*";
return os;
}
......
......@@ -36,6 +36,9 @@ public:
Type type;
size_t size;
std::string cast_type;
bool is_internal = false;
bool is_pointer = false;
size_t pointee_size;
bool operator==(const SizedType &t) const;
};
......
This diff is collapsed.
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "bpftrace.h"
#include "clang_parser.h"
#include "driver.h"
#include "semantic_analyser.h"
......@@ -19,6 +20,9 @@ void test(BPFtrace &bpftrace, Driver &driver, const std::string &input, int expe
{
ASSERT_EQ(driver.parse_str(input), 0);
ClangParser clang;
clang.parse(driver.root_, bpftrace.structs_);
std::stringstream out;
ast::SemanticAnalyser semantics(driver.root_, bpftrace, out);
std::stringstream msg;
......@@ -40,21 +44,7 @@ void test(Driver &driver, const std::string &input, int expected_result=0)
void test(const std::string &input, int expected_result=0)
{
Field field = { SizedType(Type::integer, 8), 0 };
Field mystr = { SizedType(Type::string, 8), 8 };
Field type2_field_ptr = { SizedType(Type::cast, 8, "type2*"), 16 };
Field type2_field = { SizedType(Type::cast, 8, "type2"), 24 };
Struct type1 = { 16, {{"field", field},
{"mystr", mystr},
{"type2ptr", type2_field_ptr},
{"type2", type2_field}} };
Struct type2 = { 8, {{"field", field}} };
BPFtrace bpftrace;
bpftrace.structs_["type1"] = type1;
bpftrace.structs_["type2"] = type2;
Driver driver;
test(bpftrace, driver, input, expected_result);
}
......@@ -217,6 +207,34 @@ TEST(semantic_analyser, variable_type)
EXPECT_EQ(st, assignment->var->type);
}
TEST(semantic_analyser, map_integer_sizes)
{
Driver driver;
std::string structs = "struct type1 { int x; }";
test(driver, structs + "kprobe:f { $x = ((type1)0).x; @x = $x; }", 0);
auto var_assignment = static_cast<ast::AssignVarStatement*>(driver.root_->probes->at(0)->stmts->at(0));
auto map_assignment = static_cast<ast::AssignMapStatement*>(driver.root_->probes->at(0)->stmts->at(1));
EXPECT_EQ(SizedType(Type::integer, 4), var_assignment->var->type);
EXPECT_EQ(SizedType(Type::integer, 8), map_assignment->map->type);
}
TEST(semantic_analyser, unop_dereference)
{
test("kprobe:f { *0; }", 0);
test("struct X { int n; } kprobe:f { $x = (X*)0; *$x; }", 0);
test("struct X { int n; } kprobe:f { $x = (X)0; *$x; }", 1);
test("kprobe:f { *\"0\"; }", 10);
}
TEST(semantic_analyser, unop_not)
{
test("kprobe:f { ~0; }", 0);
test("struct X { int n; } kprobe:f { $x = (X*)0; ~$x; }", 10);
test("struct X { int n; } kprobe:f { $x = (X)0; ~$x; }", 10);
test("kprobe:f { ~\"0\"; }", 10);
}
TEST(semantic_analyser, printf)
{
test("kprobe:f { printf(\"hi\") }", 0);
......@@ -370,24 +388,28 @@ TEST(semantic_analyser, profile)
TEST(semantic_analyser, variable_cast_types)
{
test("kprobe:f { $x = (type1)cpu; $x = (type1)cpu; }", 0);
test("kprobe:f { $x = (type1)cpu; $x = (type2)cpu; }", 1);
std::string structs = "struct type1 { int field; } struct type2 { int field; }";
test(structs + "kprobe:f { $x = (type1)cpu; $x = (type1)cpu; }", 0);
test(structs + "kprobe:f { $x = (type1)cpu; $x = (type2)cpu; }", 1);
}
TEST(semantic_analyser, map_cast_types)
{
test("kprobe:f { @x = (type1)cpu; @x = (type1)cpu; }", 0);
test("kprobe:f { @x = (type1)cpu; @x = (type2)cpu; }", 1);
std::string structs = "struct type1 { int field; } struct type2 { int field; }";
test(structs + "kprobe:f { @x = (type1)cpu; @x = (type1)cpu; }", 0);
test(structs + "kprobe:f { @x = (type1)cpu; @x = (type2)cpu; }", 1);
}
TEST(semantic_analyser, variable_casts_are_local)
{
test("kprobe:f { $x = (type1)cpu } kprobe:g { $x = (type2)cpu; }", 0);
std::string structs = "struct type1 { int field; } struct type2 { int field; }";
test(structs + "kprobe:f { $x = (type1)cpu } kprobe:g { $x = (type2)cpu; }", 0);
}
TEST(semantic_analyser, map_casts_are_global)
{
test("kprobe:f { @x = (type1)cpu } kprobe:g { @x = (type2)cpu; }", 1);
std::string structs = "struct type1 { int field; } struct type2 { int field; }";
test(structs + "kprobe:f { @x = (type1)cpu } kprobe:g { @x = (type2)cpu; }", 1);
}
TEST(semantic_analyser, cast_unknown_type)
......@@ -397,50 +419,76 @@ TEST(semantic_analyser, cast_unknown_type)
TEST(semantic_analyser, field_access)
{
test("kprobe:f { ((type1)cpu).field }", 0);
test("kprobe:f { $x = (type1)cpu; $x.field }", 0);
test("kprobe:f { @x = (type1)cpu; @x.field }", 0);
std::string structs = "struct type1 { int field; }";
test(structs + "kprobe:f { ((type1)cpu).field }", 0);
test(structs + "kprobe:f { $x = (type1)cpu; $x.field }", 0);
test(structs + "kprobe:f { @x = (type1)cpu; @x.field }", 0);
}
TEST(semantic_analyser, field_access_wrong_field)
{
test("kprobe:f { ((type1)cpu).blah }", 1);
test("kprobe:f { $x = (type1)cpu; $x.blah }", 1);
test("kprobe:f { @x = (type1)cpu; @x.blah }", 1);
std::string structs = "struct type1 { int field; }";
test(structs + "kprobe:f { ((type1)cpu).blah }", 1);
test(structs + "kprobe:f { $x = (type1)cpu; $x.blah }", 1);
test(structs + "kprobe:f { @x = (type1)cpu; @x.blah }", 1);
}
TEST(semantic_analyser, field_access_wrong_expr)
{
test("kprobe:f { 1234->field }", 10);
std::string structs = "struct type1 { int field; }";
test(structs + "kprobe:f { 1234->field }", 10);
}
TEST(semantic_analyser, field_access_types)
{
test("kprobe:f { ((type1)0).field == 123 }", 0);
test("kprobe:f { ((type1)0).field == \"abc\" }", 10);
std::string structs = "struct type1 { int field; char mystr[8]; }"
"struct type2 { int field; }";
test("kprobe:f { ((type1)0).mystr == \"abc\" }", 0);
test("kprobe:f { ((type1)0).mystr == 123 }", 10);
test(structs + "kprobe:f { ((type1)0).field == 123 }", 0);
test(structs + "kprobe:f { ((type1)0).field == \"abc\" }", 10);
test("kprobe:f { ((type1)0).field == ((type2)0).field }", 0);
test("kprobe:f { ((type1)0).mystr == ((type2)0).field }", 10);
test(structs + "kprobe:f { ((type1)0).mystr == \"abc\" }", 0);
test(structs + "kprobe:f { ((type1)0).mystr == 123 }", 10);
test(structs + "kprobe:f { ((type1)0).field == ((type2)0).field }", 0);
test(structs + "kprobe:f { ((type1)0).mystr == ((type2)0).field }", 10);
}
TEST(semantic_analyser, field_access_pointer)
{
test("kprobe:f { ((type1*)0)->field }", 0);
test("kprobe:f { ((type1*)0).field }", 1);
test("kprobe:f { *((type1*)0) }", 0);
std::string structs = "struct type1 { int field; }";
test(structs + "kprobe:f { ((type1*)0)->field }", 0);
test(structs + "kprobe:f { ((type1*)0).field }", 1);
test(structs + "kprobe:f { *((type1*)0) }", 0);
}
TEST(semantic_analyser, field_access_sub_struct)
{
test("kprobe:f { ((type1)0).type2ptr->field }", 0);
test("kprobe:f { ((type1)0).type2.field }", 0);
test("kprobe:f { $x = (type2)0; $x = ((type1)0).type2 }", 0);
test("kprobe:f { $x = (type2*)0; $x = ((type1)0).type2ptr }", 0);
test("kprobe:f { $x = (type1)0; $x = ((type1)0).type2 }", 1);
test("kprobe:f { $x = (type1*)0; $x = ((type1)0).type2ptr }", 1);
std::string structs = "struct type1 { struct type2 *type2ptr; struct type2 type2; }"
"struct type2 { int field; }";
test(structs + "kprobe:f { ((type1)0).type2ptr->field }", 0);
test(structs + "kprobe:f { ((type1)0).type2.field }", 0);
test(structs + "kprobe:f { $x = (type2)0; $x = ((type1)0).type2 }", 0);
test(structs + "kprobe:f { $x = (type2*)0; $x = ((type1)0).type2ptr }", 0);
test(structs + "kprobe:f { $x = (type1)0; $x = ((type1)0).type2 }", 1);
test(structs + "kprobe:f { $x = (type1*)0; $x = ((type1)0).type2ptr }", 1);
}
TEST(semantic_analyser, field_access_is_internal)
{
Driver driver;
std::string structs = "struct type1 { int x; }";
test(driver, structs + "kprobe:f { $x = ((type1)0).x }", 0);
auto var_assignment1 = static_cast<ast::AssignVarStatement*>(driver.root_->probes->at(0)->stmts->at(0));
EXPECT_EQ(false, var_assignment1->var->type.is_internal);
test(driver, structs + "kprobe:f { @type1 = (type1)0; $x = @type1.x }", 0);
auto map_assignment = static_cast<ast::AssignMapStatement*>(driver.root_->probes->at(0)->stmts->at(0));
auto var_assignment2 = static_cast<ast::AssignVarStatement*>(driver.root_->probes->at(0)->stmts->at(1));
EXPECT_EQ(true, map_assignment->map->type.is_internal);
EXPECT_EQ(true, var_assignment2->var->type.is_internal);
}
} // namespace semantic_analyser
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment