Skip to content

Commit

Permalink
[Sema] add dataflow analysis for late initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
isuckatcs committed Jul 5, 2024
1 parent 462c76c commit 4baa9bb
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 10 deletions.
2 changes: 2 additions & 0 deletions include/sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class Sema {
// FIXME: Consider moving this a different class
bool runFlowSensitiveChecks(const ResolvedFunctionDecl &fn);
bool checkReturnOnAllPaths(const ResolvedFunctionDecl &fn, const CFG &cfg);
bool checkVariableInitialization(const ResolvedFunctionDecl &fn,
const CFG &cfg);

public:
explicit Sema(std::vector<std::unique_ptr<FunctionDecl>> sourceFile)
Expand Down
103 changes: 99 additions & 4 deletions src/sema.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include <algorithm>
#include <cassert>
#include <map>

#include "cfg.h"
#include "sema.h"
Expand All @@ -10,6 +12,7 @@ bool Sema::runFlowSensitiveChecks(const ResolvedFunctionDecl &fn) {

bool error = false;
error |= checkReturnOnAllPaths(fn, cfg);
error |= checkVariableInitialization(fn, cfg);

return error;
};
Expand Down Expand Up @@ -57,6 +60,102 @@ bool Sema::checkReturnOnAllPaths(const ResolvedFunctionDecl &fn,
return exitReached || returnCount == 0;
}

bool Sema::checkVariableInitialization(const ResolvedFunctionDecl &fn,
const CFG &cfg) {

enum class State { Bottom, Unassigned, Assigned, Top };

auto joinStates = [](State s1, State s2) {
if (s1 == State::Top)
return s1;

if (s2 == State::Top)
return s2;

if (s1 == State::Bottom)
return s2;

if (s2 == State::Bottom)
return s1;

if (s1 == s2)
return s1;

return State::Top;
};

using Lattice = std::map<const ResolvedVarDecl *, State>;

std::vector<Lattice> curLattices(cfg.basicBlocks.size());
std::map<const SourceLocation *, std::string> pendingErrors;

bool changed = true;
while (changed) {
changed = false;

for (int bb = cfg.entry; bb != cfg.exit; --bb) {
const auto &[preds, succs, stmts] = cfg.basicBlocks[bb];

Lattice tmp;
for (auto &&pred : preds) {
for (auto &&[decl, state] : curLattices[pred.first]) {
if (tmp.count(decl))
tmp[decl] = joinStates(tmp[decl], state);
else
tmp[decl] = state;
}
}

auto stmtsReversed = stmts;
std::reverse(stmtsReversed.begin(), stmtsReversed.end());

for (auto &&stmt : stmtsReversed) {
if (const auto *declStmt =
dynamic_cast<const ResolvedDeclStmt *>(stmt)) {
tmp[declStmt->varDecl.get()] = declStmt->varDecl->initializer
? State::Assigned
: State::Unassigned;
} else if (const auto *assignment =
dynamic_cast<const ResolvedAssignment *>(stmt)) {
const auto *varDecl =
dynamic_cast<const ResolvedVarDecl *>(assignment->variable->decl);

if (!varDecl)
continue;

if (!varDecl->isMutable && tmp.count(varDecl) &&
tmp[varDecl] != State::Unassigned)
pendingErrors[&assignment->location] =
'\'' + varDecl->identifier + "' cannot be mutated";

tmp[varDecl] = State::Assigned;
} else if (const auto *declRefExpr =
dynamic_cast<const ResolvedDeclRefExpr *>(stmt)) {
const auto *varDecl =
dynamic_cast<const ResolvedVarDecl *>(declRefExpr->decl);

if (!varDecl || !tmp.count(varDecl))
continue;

if (tmp[varDecl] != State::Assigned)
pendingErrors[&declRefExpr->location] =
'\'' + varDecl->identifier + "' is not initialized";
}
}

if (curLattices[bb] != tmp) {
curLattices[bb] = tmp;
changed = true;
}
}
}

for (auto &&[loc, msg] : pendingErrors)
error(*loc, msg);

return !pendingErrors.empty();
}

bool Sema::insertDeclToCurrentScope(ResolvedDecl &decl) {
const auto &[foundDecl, scopeIdx] = lookupDecl(decl.identifier);

Expand Down Expand Up @@ -284,10 +383,6 @@ Sema::resolveAssignment(const Assignment &assignment) {
auto *var = dynamic_cast<const ResolvedVarDecl *>(resolvedLHS->decl);
assert(var && "assignment LHS is not a variable");

// The variable is not late initialized, so we can error out safely.
if (var->initializer && !var->isMutable)
return error(assignment.location, "immutable variables cannot be assigned");

if (resolvedRHS->type != resolvedLHS->type)
return error(resolvedRHS->location,
"assigned value type doesn't match variable type");
Expand Down
7 changes: 2 additions & 5 deletions test/constexpr/special_cases.al
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// RUN: compiler %s -res-dump 2>&1 | filecheck %s
fn binaryLhsKnown(): number {
fn binaryLhsKnown(y: number): number {
var x: number = 2.1;
let y: number;

return (0.0 && y) + (1.0 || x);
}
Expand Down Expand Up @@ -46,9 +45,7 @@ fn call(): void {
// CHECK-NEXT: ResolvedBlock
// CHECK-NEXT: ResolvedReturnStmt

fn lhsKnownRhsNot(): number {
let y: number;

fn lhsKnownRhsNot(y: number): number {
return 1.0 && y;
}
// CHECK: ResolvedReturnStmt
Expand Down
1 change: 0 additions & 1 deletion test/sema/assignment.al
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ fn main(): void {

let y: number = 3.0;

// CHECK: [[# @LINE + 1 ]]:5: error: immutable variables cannot be assigned
y = 3.0;

// CHECK: [[# @LINE + 1 ]]:5: error: expected to call function 'bar'
Expand Down
118 changes: 118 additions & 0 deletions test/sema/late_init.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// RUN: compiler %s -res-dump
fn main(): void {
let x: number;

if 1.0 {
x = 2.0;
} else {
x = 3.0;
}

x;
}

fn modifyImmutable(): void {
let x: number = 1.0;

if 1.0 {
// CHECK: [[# @LINE + 1 ]]:9: error: 'x' cannot be mutated
x = 1.0;
} else {
// CHECK: [[# @LINE + 1 ]]:9: error: 'x' cannot be mutated
x = 2.0;
}

// CHECK: [[# @LINE + 1 ]]:5: error: 'x' cannot be mutated
x = 3.0;
}

fn uninitOneBranch(): void {
let x: number;

if 1.0 {
let x: number;
x = 1.0; // init inner 'x'
} else {
x = 2.0;
}

// CHECK: [[# @LINE + 1 ]]:5: error: 'x' is not initialized
x;
}

fn initEveryBranch(): void {
var x: number;

if 1.0 {
x = 1.0;
} else {
x = 2.0;
}

// CHECK-NOT: [[# @LINE + 1 ]]:5: error
x;
}

fn initConditionVar(): void {
var x: number;

// CHECK: [[# @LINE + 1 ]]:15: error: 'x' is not initialized
if 1.0 || x {
x = 1.0;
// CHECK: [[# @LINE + 1 ]]:22: error: 'x' is not initialized
} else if 0.0 && x == 2.0 {
x = 3.0;
} else {
x = 2.0;
}

// CHECK-NOT: [[# @LINE + 1 ]]:5: error
x;
}

fn loop(): void {
let x: number;
var y: number;

while 1.0 {
// CHECK: [[# @LINE + 1 ]]:9: error: 'x' cannot be mutated
x = 1.0;

// CHECK-NOT: [[# @LINE + 1 ]]:9: error
y = 1.0;
}

// CHECK: [[# @LINE + 1 ]]:5: error: 'x' is not initialized
x;
// CHECK: [[# @LINE + 1 ]]:5: error: 'y' is not initialized
y;
}

fn shadowInitialization(): void {
let x: number;

if 1.0 {
let x: number;
x = 1.0;
} else {
x = 2.0;
}

// CHECK: [[# @LINE + 1 ]]:5: error: 'x' is not initialized
x;
}

fn shadowInitialized(): void {
let x: number;

if 1.0 {
x = 1.0;
let x: number;
// CHECK: [[# @LINE + 1 ]]:9: error: 'x' is not initialized
x;
} else {
x = 2.0;
}

x;
}

0 comments on commit 4baa9bb

Please sign in to comment.