Skip to content

Commit 0553761

Browse files
authored
Dyno: adjust compilerError to interrupt resolution of functions (#27004)
Resolves Cray/chapel-private#7229. Depends on #26999. Quoting from that issue: a lot of Chapel production code (including in library modules) looks like this: ```Chapel proc foo(param cond) { if cond then compilerError("This condition should not be met"); codeThatOnlyCompilesIfCondIsFalse(); } ``` These rely on the fact that `codeThatOnlyCompilesIfCondIsFalse()` will not be resolved if an error is already emitted. This means terminating resolution of `foo()`, much like we do with `return` in #26955. This PR implements that. One conceptual curiosity is that this conflicts with Dyno's existing approach of attempting to continue resolving programs to get partial information. Specifically, expressions that emitted errors are marked with `ErroneousType`, and if the rest of the types can be inferred despite the error, we infer them. This is desirable (partial information is useful in editors, for example), but counter to stopping resolution in its tracks. This PR goes for a two-phase handling of `compilerError`, which distinguishes functions that call compiler error directly and functions that simply observe errors emitted by a call. * __For functions that invoke `compilerError` directly__, this PR makes use of the changes in #26955 to stop resolution in its tracks. This requires some communication between the resolver and the other branch-sensitive passes to inform them of the fatal error, which I achieve using a new flag on `ResolvedExpression` called `causedFatalError_`[^1]. * Functions that are meant as "helpers" for invoking `compilerError` (i.e., those that are ignored using the `depth` argument) are considered "direct" callers to `compilerError` for the purposes of this phase. That's because they mostly just serve to augment the `compilerError` in some way. * __For invocations of functions that error__, this PR simply marks the call as `ErroneousType`, without interrupting the resolution of the current function etc. Thus, if we call `foo()`, and `foo()` issues a `compilerError`, we print the error and mark `foo()` as `ErroneousType`, but (as described above) continue resolving with partial information. This way, I believe we can reconcile the expectation of `compilerError` as terminating resolution, while still allowing us to compute partial information in other contexts. See the program tested by this PR: ```Chapel proc foo() { bar(); return 42; } proc bar() { compilerError("Hello"); compilerWarnning("inside bar, after call to compilerError"); } var x = foo(); ``` Here, the error "Hello" immediately terminates resolution of `bar()` (so the "inside bar" warning is never printed). This error is issued on the `bar();` line, but we proceed past this line, and, since the `return 42` doesn't depend on the call to `bar()`, correctly determine that the `var x` is an `int`. ## Testing - [x] dyno tests - [x] paratest - [x] spectests still work as before Reviewed by @benharsh -- thanks! [^1]: I have checked and on my machine, this flag does not cause the `ResolvedExpression` type to increaase in size, presumably due to the way that struct is padded. So this does not cause size / memory penalties .
2 parents 1212363 + 76d416b commit 0553761

File tree

6 files changed

+178
-82
lines changed

6 files changed

+178
-82
lines changed

frontend/include/chpl/resolution/resolution-types.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2408,6 +2408,8 @@ class ResolvedExpression {
24082408
ID toId_;
24092409
// Is this a reference to a compiler-created primitive?
24102410
bool isBuiltin_ = false;
2411+
// Did this expression cause the function to stop being resolved?
2412+
bool causedFatalError_ = false;
24112413

24122414
// For a function call, what is the most specific candidate,
24132415
// or when using return intent overloading, what are the most specific
@@ -2440,6 +2442,9 @@ class ResolvedExpression {
24402442
/** check whether this resolution result refers to a compiler builtin like `bool`. */
24412443
bool isBuiltin() const { return isBuiltin_; }
24422444

2445+
/** check whether this resolution result caused a fatal error. */
2446+
bool causedFatalError() const { return causedFatalError_; }
2447+
24432448
/** For a function call, what is the most specific candidate, or when using
24442449
* return intent overloading, what are the most specific candidates? The
24452450
* choice between these needs to happen later than the main function
@@ -2464,6 +2469,9 @@ class ResolvedExpression {
24642469
/** set the isPrimitive flag */
24652470
void setIsBuiltin(bool isBuiltin) { isBuiltin_ = isBuiltin; }
24662471

2472+
/** set the causedFatalError flag */
2473+
void setCausedFatalError(bool causedFatalError) { causedFatalError_ = causedFatalError; }
2474+
24672475
/** set the toId */
24682476
void setToId(ID toId) { toId_ = toId; }
24692477

@@ -2491,6 +2499,7 @@ class ResolvedExpression {
24912499
return type_ == other.type_ &&
24922500
toId_ == other.toId_ &&
24932501
isBuiltin_ == other.isBuiltin_ &&
2502+
causedFatalError_ == other.causedFatalError_ &&
24942503
mostSpecific_ == other.mostSpecific_ &&
24952504
poiScope_ == other.poiScope_ &&
24962505
associatedActions_ == other.associatedActions_ &&
@@ -2503,6 +2512,7 @@ class ResolvedExpression {
25032512
type_.swap(other.type_);
25042513
toId_.swap(other.toId_);
25052514
std::swap(isBuiltin_, other.isBuiltin_);
2515+
std::swap(causedFatalError_, other.causedFatalError_);
25062516
mostSpecific_.swap(other.mostSpecific_);
25072517
std::swap(poiScope_, other.poiScope_);
25082518
std::swap(associatedActions_, other.associatedActions_);

frontend/lib/resolution/BranchSensitiveVisitor.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,18 @@ struct ControlFlowInfo {
4646
// if this frame hit a 'continue', the loop it continued
4747
const uast::Loop* loopContinues_ = nullptr;
4848

49+
// did we encounter a 'compilerError'? These terminate resolution.
50+
bool fatalError_ = false;
51+
4952
public:
5053
ControlFlowInfo() = default;
5154

5255
/* have we hit a statement that would prevent further execution of a block? */
53-
bool isDoneExecuting() const { return returnsOrThrows_ || loopBreaks_ || loopContinues_; }
56+
bool isDoneExecuting() const { return returnsOrThrows_ || loopBreaks_ || loopContinues_ || fatalError_; }
5457
bool returnsOrThrows() const { return returnsOrThrows_; }
5558
bool breaks() const { return loopBreaks_ != nullptr; }
5659
bool continues() const { return loopContinues_ != nullptr; }
60+
bool fatalError() const { return fatalError_; }
5761

5862
// The below mark functions check if the block is already done executing.
5963
// This is useful because in cases like 'return; continue', we don't want
@@ -63,6 +67,7 @@ struct ControlFlowInfo {
6367
void markReturnOrThrow() { if (!isDoneExecuting()) returnsOrThrows_ = true; }
6468
void markBreak(const uast::Loop* markWith) { if (!isDoneExecuting()) loopBreaks_ = markWith; }
6569
void markContinue(const uast::Loop* markWith) { if (!isDoneExecuting()) loopContinues_ = markWith; }
70+
void markFatalError() { if (!isDoneExecuting()) fatalError_ = true; }
6671

6772
// resetting break and continue is useful for when we are handling 'param' for loops.
6873
// If we hit a continue, we might want to move on to the next iteration of
@@ -89,6 +94,9 @@ struct ControlFlowInfo {
8994
loopContinues_ = nullptr;
9095
}
9196

97+
// errors in one branch are sufficient to infect all branches
98+
fatalError_ |= other.fatalError_;
99+
92100
return *this;
93101
}
94102

@@ -107,6 +115,7 @@ struct ControlFlowInfo {
107115
returnsOrThrows_ |= other.returnsOrThrows_;
108116
loopBreaks_ = other.loopBreaks_;
109117
loopContinues_ = other.loopContinues_;
118+
fatalError_ |= other.fatalError_;
110119
}
111120
}
112121

@@ -119,6 +128,7 @@ struct ControlFlowInfo {
119128
returnsOrThrows_ |= other.returnsOrThrows_;
120129
if (other.loopBreaks_ != loop) loopBreaks_ = other.loopBreaks_;
121130
if (other.loopContinues_ != loop) loopContinues_ = other.loopContinues_;
131+
fatalError_ |= other.fatalError_;
122132
}
123133
}
124134
};
@@ -492,6 +502,12 @@ struct BranchSensitiveVisitor {
492502
currentFrame()->controlFlowInfo.markContinue(loop);
493503
}
494504

505+
/** note that the current frame, if any, has encountered a fatal error */
506+
void markFatalError() {
507+
if (scopeStack.empty()) return;
508+
currentFrame()->controlFlowInfo.markFatalError();
509+
}
510+
495511
/** have we hit a statement that would prevent further execution of the current block / frame? */
496512
bool isDoneExecuting() {
497513
if (scopeStack.empty()) return false;

frontend/lib/resolution/Resolver.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2300,6 +2300,7 @@ bool Resolver::CallResultWrapper::noteResultWithoutError(
23002300
const CallResolutionResult& result,
23012301
optional<ActionAndId> actionAndId) {
23022302
bool needsErrors = false;
2303+
bool markErroneous = false;
23032304

23042305
for (auto& diagnostic : gatherUserDiagnostics(resolver.rc, result)) {
23052306
// The diagnostic's depth means it's aimed for further up the call stack.
@@ -2308,14 +2309,26 @@ bool Resolver::CallResultWrapper::noteResultWithoutError(
23082309
resolver.userDiagnostics.emplace_back(diagnostic.message(),
23092310
diagnostic.kind(),
23102311
diagnostic.depth() - 1);
2312+
2313+
if (diagnostic.kind() == CompilerDiagnostic::ERROR) {
2314+
if (!astForContext->isFnCall()) {
2315+
resolver.context->warning(astForContext, "propagating compiler errors past non-call frames is not supported");
2316+
}
2317+
2318+
// functions that invoke compilerError(), and their helper code skipped
2319+
// via the depth, should not be resolved past the point of the error.
2320+
resolver.markFatalError();
2321+
if (r) r->setCausedFatalError(true);
2322+
}
23112323
} else if (diagnostic.depth() - 1 == 0) {
23122324
// we're asked not to emit errors, and only return if errors are
23132325
// needed.
23142326
needsErrors = true;
2327+
markErroneous = diagnostic.kind() == CompilerDiagnostic::ERROR;
23152328
}
23162329
}
23172330

2318-
if (!result.exprType().hasTypePtr()) {
2331+
if (!result.exprType().hasTypePtr() || markErroneous) {
23192332
if (!actionAndId && r) {
23202333
// Only set the type to erroneous if we're handling an actual user call,
23212334
// and not an associated action.

frontend/lib/resolution/VarScopeVisitor.cpp

Lines changed: 85 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -340,97 +340,102 @@ void VarScopeVisitor::exit(const OpCall* ast, RV& rv) {
340340
exitAst(ast);
341341
}
342342

343-
344343
bool VarScopeVisitor::enter(const FnCall* callAst, RV& rv) {
345344
enterAst(callAst);
346345

347-
if (rv.hasAst(callAst)) {
348-
// Does any return-intent-overload use 'in', 'out', or 'inout'?
349-
// This filter is intended as an optimization.
350-
const MostSpecificCandidates& candidates = rv.byAst(callAst).mostSpecific();
351-
bool anyInOutInout = false;
352-
bool isMethod = false;
353-
for (const MostSpecificCandidate& candidate : candidates) {
354-
if (candidate) {
355-
auto fn = candidate.fn();
356-
if (fn->untyped()->isMethod()) isMethod = true;
357-
358-
int n = fn->numFormals();
359-
for (int i = 0; i < n; i++) {
360-
const QualifiedType& formalQt = fn->formalType(i);
361-
auto kind = formalQt.kind();
362-
if (kind == QualifiedType::OUT ||
363-
kind == QualifiedType::IN || kind == QualifiedType::CONST_IN ||
364-
kind == QualifiedType::INOUT) {
365-
anyInOutInout = true;
366-
break;
367-
}
368-
}
369-
if (anyInOutInout) {
346+
auto rr = rv.byPostorder().byAstOrNull(callAst);
347+
if (!rr) return false;
348+
349+
if (rr->causedFatalError()) {
350+
markFatalError();
351+
return false;
352+
}
353+
354+
// Does any return-intent-overload use 'in', 'out', or 'inout'?
355+
// This filter is intended as an optimization.
356+
const MostSpecificCandidates& candidates = rr->mostSpecific();
357+
bool anyInOutInout = false;
358+
bool isMethod = false;
359+
for (const MostSpecificCandidate& candidate : candidates) {
360+
if (candidate) {
361+
auto fn = candidate.fn();
362+
if (fn->untyped()->isMethod()) isMethod = true;
363+
364+
int n = fn->numFormals();
365+
for (int i = 0; i < n; i++) {
366+
const QualifiedType& formalQt = fn->formalType(i);
367+
auto kind = formalQt.kind();
368+
if (kind == QualifiedType::OUT ||
369+
kind == QualifiedType::IN || kind == QualifiedType::CONST_IN ||
370+
kind == QualifiedType::INOUT) {
371+
anyInOutInout = true;
370372
break;
371373
}
372374
}
375+
if (anyInOutInout) {
376+
break;
377+
}
378+
}
379+
}
380+
381+
if (!anyInOutInout) {
382+
// visit the actuals to gather mentions
383+
for (auto actualAst : callAst->actuals()) {
384+
actualAst->traverse(rv);
385+
}
386+
} else {
387+
// Use FormalActualMap to figure out which variable ID
388+
// is passed to a formal with out/in/inout intent.
389+
// Issue an error if it does not match among return intent overloads.
390+
//
391+
// TODO: Should we store the resolved CallInfo so we don't need to build
392+
// it back up here?
393+
std::vector<const AstNode*> actualAsts;
394+
auto ci = CallInfo::create(context, callAst, rv.byPostorder(),
395+
/* raiseErrors */ false,
396+
&actualAsts);
397+
398+
if (isMethod && ci.isMethodCall() == false) {
399+
// Create a dummy 'this' actual
400+
ci = ci.createWithReceiver(ci, QualifiedType());
401+
actualAsts.insert(actualAsts.begin(), nullptr);
373402
}
374403

375-
if (!anyInOutInout) {
376-
// visit the actuals to gather mentions
377-
for (auto actualAst : callAst->actuals()) {
404+
// compute a vector indicating which actuals are passed to
405+
// an 'out' formal in all return intent overloads
406+
std::vector<QualifiedType> actualFormalTypes;
407+
std::vector<Qualifier> actualFormalIntents;
408+
computeActualFormalIntents(context, candidates, ci, actualAsts,
409+
actualFormalIntents, actualFormalTypes);
410+
411+
int actualIdx = 0;
412+
for (auto actual : ci.actuals()) {
413+
(void) actual; // avoid compilation error about unused variable
414+
415+
const AstNode* actualAst = actualAsts[actualIdx];
416+
Qualifier kind = actualFormalIntents[actualIdx];
417+
418+
// handle an actual that is passed to an 'out'/'in'/'inout' formal
419+
if (actualAst == nullptr) {
420+
CHPL_ASSERT(ci.isMethodCall() && actualIdx == 0);
421+
} else if (kind == Qualifier::OUT) {
422+
handleOutFormal(callAst, actualAst,
423+
actualFormalTypes[actualIdx], rv);
424+
} else if ((kind == Qualifier::IN || kind == Qualifier::CONST_IN) &&
425+
!(ci.name() == "init" && actualIdx == 0)) {
426+
// don't do this for the 'this' argument to 'init', because it
427+
// is not getting copied.
428+
handleInFormal(callAst, actualAst,
429+
actualFormalTypes[actualIdx], rv);
430+
} else if (kind == Qualifier::INOUT) {
431+
handleInoutFormal(callAst, actualAst,
432+
actualFormalTypes[actualIdx], rv);
433+
} else {
434+
// otherwise, visit the actuals to gather mentions
378435
actualAst->traverse(rv);
379436
}
380-
} else {
381-
// Use FormalActualMap to figure out which variable ID
382-
// is passed to a formal with out/in/inout intent.
383-
// Issue an error if it does not match among return intent overloads.
384-
//
385-
// TODO: Should we store the resolved CallInfo so we don't need to build
386-
// it back up here?
387-
std::vector<const AstNode*> actualAsts;
388-
auto ci = CallInfo::create(context, callAst, rv.byPostorder(),
389-
/* raiseErrors */ false,
390-
&actualAsts);
391-
392-
if (isMethod && ci.isMethodCall() == false) {
393-
// Create a dummy 'this' actual
394-
ci = ci.createWithReceiver(ci, QualifiedType());
395-
actualAsts.insert(actualAsts.begin(), nullptr);
396-
}
397437

398-
// compute a vector indicating which actuals are passed to
399-
// an 'out' formal in all return intent overloads
400-
std::vector<QualifiedType> actualFormalTypes;
401-
std::vector<Qualifier> actualFormalIntents;
402-
computeActualFormalIntents(context, candidates, ci, actualAsts,
403-
actualFormalIntents, actualFormalTypes);
404-
405-
int actualIdx = 0;
406-
for (auto actual : ci.actuals()) {
407-
(void) actual; // avoid compilation error about unused variable
408-
409-
const AstNode* actualAst = actualAsts[actualIdx];
410-
Qualifier kind = actualFormalIntents[actualIdx];
411-
412-
// handle an actual that is passed to an 'out'/'in'/'inout' formal
413-
if (actualAst == nullptr) {
414-
CHPL_ASSERT(ci.isMethodCall() && actualIdx == 0);
415-
} else if (kind == Qualifier::OUT) {
416-
handleOutFormal(callAst, actualAst,
417-
actualFormalTypes[actualIdx], rv);
418-
} else if ((kind == Qualifier::IN || kind == Qualifier::CONST_IN) &&
419-
!(ci.name() == "init" && actualIdx == 0)) {
420-
// don't do this for the 'this' argument to 'init', because it
421-
// is not getting copied.
422-
handleInFormal(callAst, actualAst,
423-
actualFormalTypes[actualIdx], rv);
424-
} else if (kind == Qualifier::INOUT) {
425-
handleInoutFormal(callAst, actualAst,
426-
actualFormalTypes[actualIdx], rv);
427-
} else {
428-
// otherwise, visit the actuals to gather mentions
429-
actualAst->traverse(rv);
430-
}
431-
432-
actualIdx++;
433-
}
438+
actualIdx++;
434439
}
435440
}
436441

frontend/lib/resolution/return-type-inference.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ struct ReturnTypeInferrer : BranchSensitiveVisitor<DefaultFrame, ResolvedVisitor
337337
const types::Param* determineIfValue(const uast::AstNode* ast, RV& rv) override;
338338
void traverseNode(const uast::AstNode* ast, RV& rv) override;
339339

340+
bool enter(const FnCall* ast, RV& rv);
341+
void exit(const FnCall* ast, RV& rv);
342+
340343
bool enter(const Function* fn, RV& rv);
341344
void exit(const Function* fn, RV& rv);
342345

@@ -505,6 +508,22 @@ void ReturnTypeInferrer::traverseNode(const uast::AstNode* ast, RV& rv) {
505508
ast->traverse(rv);
506509
}
507510

511+
bool ReturnTypeInferrer::enter(const FnCall* ast, RV& rv) {
512+
enterScope(ast, rv);
513+
514+
if (auto rr = rv.byPostorder().byAstOrNull(ast)) {
515+
if (rr->causedFatalError()) {
516+
markFatalError();
517+
}
518+
}
519+
520+
return true;
521+
}
522+
523+
void ReturnTypeInferrer::exit(const FnCall* ast, RV& rv) {
524+
exitScope(ast, rv);
525+
}
526+
508527
bool ReturnTypeInferrer::enter(const Function* fn, RV& rv) {
509528
return false;
510529
}

0 commit comments

Comments
 (0)