Skip to content

Commit

Permalink
Typechecker: try-else improvements (#535)
Browse files Browse the repository at this point in the history
* Typechecker: try-else improvements

This change allows try-expressions to be used in functions which do not
necessarily return a Result value, if there is an else-clause attached.

* Typechecker/Compiler: Support Option try

Try expressions can now work on values with type `T?`.
  • Loading branch information
kengorab authored Jan 13, 2025
1 parent c4b117a commit d322cec
Show file tree
Hide file tree
Showing 18 changed files with 1,223 additions and 52 deletions.
23 changes: 13 additions & 10 deletions projects/compiler/example.abra
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
val m = {
a: 1,
b: 2,
c: 3,
s: 4
}
println(m.size)
println(m)
func some(): Int? = Some(123)
func none(): Int? = None

func f12(): Result<Int, Int> {
var acc = 0
while acc < 10 {
val x = try none() else return Some(12)
acc += x
}

for (k, v) in m {
println(k, v)
Some(acc)
}

/// Expect: Option.Some(value: 12)
println(f12())
47 changes: 32 additions & 15 deletions projects/compiler/src/compiler.abra
Original file line number Diff line number Diff line change
Expand Up @@ -1803,23 +1803,32 @@ export type Compiler {
}
TypedAstNodeKind.Try(expr, elseClause) => {
val exprVal = try self._compileExpression(expr)
val isErr = try self._emitResultValueIsOkVariant(exprVal, negate: true)
val isOptionTry = !!self._typeIsOption(expr.ty)
val isUnhappyPath = if isOptionTry {
try self._emitOptValueIsSomeVariant(exprVal, negate: true)
} else {
try self._emitResultValueIsOkVariant(exprVal, negate: true)
}

val labelIsErr = self._currentFn.block.addLabel("isErr")
val labelIsUnhappyPath = self._currentFn.block.addLabel("isUnhappyPath")
val labelCont = self._currentFn.block.addLabel("cont")

if elseClause |clause| {
val phiCases: (Label, Value)[] = []

val labelIsOk = self._currentFn.block.addLabel("isOk")
self._currentFn.block.buildJnz(isErr, labelIsErr, labelIsOk)
val labelIsHappyPath = self._currentFn.block.addLabel("isOk")
self._currentFn.block.buildJnz(isUnhappyPath, labelIsUnhappyPath, labelIsHappyPath)

self._currentFn.block.registerLabel(labelIsErr)
self._currentFn.block.registerLabel(labelIsUnhappyPath)
if clause.pattern |(bindingPattern, vars)| {
val variables = vars.keyBy(v => v.label.name)

val errorQbeType = try self._getQbeTypeForTypeExpect(clause.errorType, "unacceptable type", None)
val bindingVal = try self._emitOptValueGetValue(errorQbeType, exprVal)
val bindingVal = if isOptionTry {
exprVal
} else {
try self._emitResultValueGetValue(errorQbeType, exprVal)
}
try self._compileBindingPattern(bindingPattern, variables, Some(bindingVal))
}
for node, idx in clause.block {
Expand All @@ -1837,27 +1846,35 @@ export type Compiler {
self._currentFn.block.buildJmp(labelCont)
}

self._currentFn.block.registerLabel(labelIsOk)
val okQbeType = try self._getQbeTypeForTypeExpect(node.ty, "unacceptable type", None)
val okVal = try self._emitOptValueGetValue(okQbeType, exprVal)
self._currentFn.block.registerLabel(labelIsHappyPath)
val happyPathQbeType = try self._getQbeTypeForTypeExpect(node.ty, "unacceptable type", None)
val happyPathVal = if isOptionTry {
try self._emitOptValueGetValue(happyPathQbeType, exprVal)
} else {
try self._emitResultValueGetValue(happyPathQbeType, exprVal)
}
val label = self._currentFn.block.currentLabel
phiCases.push((label, okVal))
phiCases.push((label, happyPathVal))
self._currentFn.block.buildJmp(labelCont)

self._currentFn.block.registerLabel(labelCont)
val res = try self._currentFn.block.buildPhi(phiCases) else |e| return qbeError(e)
Ok(res)
} else {
self._currentFn.block.buildJnz(isErr, labelIsErr, labelCont)
self._currentFn.block.buildJnz(isUnhappyPath, labelIsUnhappyPath, labelCont)

self._currentFn.block.registerLabel(labelIsErr)
self._currentFn.block.registerLabel(labelIsUnhappyPath)
self._currentFn.block.buildReturn(Some(exprVal))

self._currentFn.block.registerLabel(labelCont)
val okValTy = try self._getQbeTypeForTypeExpect(node.ty, "unacceptable type", None)
val okValue = try self._emitResultValueGetValue(okValTy, exprVal)
val happyPathQbeType = try self._getQbeTypeForTypeExpect(node.ty, "unacceptable type", None)
val happyPathVal = if isOptionTry {
try self._emitOptValueGetValue(happyPathQbeType, exprVal)
} else {
try self._emitResultValueGetValue(happyPathQbeType, exprVal)
}

Ok(okValue)
Ok(happyPathVal)
}
}
_ => unreachable("node must be a statement")
Expand Down
75 changes: 55 additions & 20 deletions projects/compiler/src/typechecker.abra
Original file line number Diff line number Diff line change
Expand Up @@ -1370,10 +1370,14 @@ type TypeError {
InvalidTryLocationReason.NotWithinFunction => {
lines.push("Try expressions can only be used inside of function bodies")
}
InvalidTryLocationReason.InvalidFunctionReturnType(fnLabel, tryType, returnType) => {
InvalidTryLocationReason.InvalidFunctionReturnType(fnLabel, tryType, returnType, isResult) => {
lines.push("The containing function '${fnLabel.name}' has return type '${returnType.repr()}', which is incompatible with the try expression's type '${tryType.repr()}'.")
lines.push(self._getCursorLine(fnLabel.position, contents))
lines.push("To be compatible, the function must return a Result whose error type matches that of the try expression")
if isResult {
lines.push("To be compatible, '${fnLabel.name}' must return a Result whose error type matches that of the try expression")
} else {
lines.push("To be compatible, '${fnLabel.name}' must return an Option type")
}
}
}
}
Expand Down Expand Up @@ -1450,7 +1454,7 @@ enum InvalidDestructuringReason {

enum InvalidTryLocationReason {
NotWithinFunction
InvalidFunctionReturnType(fnLabel: Label, tryType: Type, returnType: Type)
InvalidFunctionReturnType(fnLabel: Label, tryType: Type, returnType: Type, isResult: Bool)
}

enum TypeErrorKind {
Expand Down Expand Up @@ -5194,29 +5198,41 @@ export type Typechecker {
// TODO: support top-level try (the error case would just exit the program)
val currentFn = if self.currentFunction |f| f else return Err(TypeError(position: token.position, kind: TypeErrorKind.InvalidTryLocation(InvalidTryLocationReason.NotWithinFunction)))

val (_, retErrType) = if self._typeIsResult(currentFn.returnType) |t| t else {
// A bit wasteful perhaps, but attempt to get the type of the try expression's subject without the hint, for a nicer error message
val typedExpr = try self._typecheckExpression(expr, None)
return Err(TypeError(position: token.position, kind: TypeErrorKind.InvalidTryLocation(InvalidTryLocationReason.InvalidFunctionReturnType(fnLabel: currentFn.label, tryType: typedExpr.ty, returnType: currentFn.returnType))))
}
val (typedExpr, returnTypeResultErr) = if self._typeIsResult(currentFn.returnType) |(_, retErrType)| {
val hint = if typeHint |typeHint| { Some(Type(kind: TypeKind.Instance(StructOrEnum.Enum(self.project.preludeResultEnum), [typeHint, retErrType]))) } else None
val typedExpr = try self._typecheckExpression(expr, hint)

(typedExpr, Some(retErrType))
} else if self._typeIsOption(currentFn.returnType) |inner| {
val hint = if typeHint |typeHint| { Some(Type(kind: TypeKind.Instance(StructOrEnum.Enum(self.project.preludeOptionEnum), [typeHint]))) } else None
val typedExpr = try self._typecheckExpression(expr, hint)

val hint = if typeHint |typeHint| {
Some(Type(kind: TypeKind.Instance(StructOrEnum.Enum(self.project.preludeResultEnum), [typeHint, retErrType])))
(typedExpr, None)
} else if elseClause {
val typedExpr = try self._typecheckExpression(expr, None)
(typedExpr, None)
} else {
None
val typedExpr = try self._typecheckExpression(expr, None)
val isResult = !!self._typeIsResult(typedExpr.ty)
return Err(TypeError(position: token.position, kind: TypeErrorKind.InvalidTryLocation(InvalidTryLocationReason.InvalidFunctionReturnType(fnLabel: currentFn.label, tryType: typedExpr.ty, returnType: currentFn.returnType, isResult: isResult))))
}
val typedExpr = try self._typecheckExpression(expr, hint)

// TODO: other Try-able types
val (tryValType, tryErrType) = if self._typeIsResult(typedExpr.ty) |t| t else return Err(TypeError(position: typedExpr.token.position, kind: TypeErrorKind.InvalidTryTarget(typedExpr.ty)))

val typedElseClause = if elseClause |(elseToken, bindingPattern, elseBlock)| {
val (tryValType, typedElseClause) = if elseClause |(elseToken, bindingPattern, elseBlock)| {
val isStatement = if typeHint |hint| hint.kind == TypeKind.PrimitiveUnit else false

val (tryValType, tryErrType) = if self._typeIsResult(typedExpr.ty) |t| {
t
} else if self._typeIsOption(typedExpr.ty) |inner| {
(inner, typedExpr.ty)
} else {
return Err(TypeError(position: typedExpr.token.position, kind: TypeErrorKind.InvalidTryTarget(typedExpr.ty)))
}

val prevScope = self.currentScope
self.currentScope = self.currentScope.makeChild("try_else", ScopeKind.Try)

val elseBindingPattern = if bindingPattern |pat| {
// TODO: Emit warning if expression is an Option type, since the binding would always equal `None`
val variables = try self._typecheckBindingPattern(false, pat, tryErrType)
Some((pat, variables))
} else {
Expand Down Expand Up @@ -5255,11 +5271,30 @@ export type Typechecker {

self.currentScope = prevScope

Some(TypedTryElseClause(pattern: elseBindingPattern, errorType: tryErrType, block: typedNodes, terminator: terminator))
} else if !self._typeSatisfiesRequired(ty: retErrType, required: tryErrType) {
return Err(TypeError(position: token.position, kind: TypeErrorKind.TryReturnTypeMismatch(fnLabel: currentFn.label, tryType: typedExpr.ty, tryErrType: tryErrType, retErrType: retErrType)))
val elseClause = TypedTryElseClause(pattern: elseBindingPattern, errorType: tryErrType, block: typedNodes, terminator: terminator)
(tryValType, Some(elseClause))
} else {
None
val tryValType = if self._typeIsResult(typedExpr.ty) |(exprOkType, exprErrType)| {
if returnTypeResultErr |retErrType| {
if !self._typeSatisfiesRequired(ty: retErrType, required: exprErrType) {
return Err(TypeError(position: token.position, kind: TypeErrorKind.TryReturnTypeMismatch(fnLabel: currentFn.label, tryType: typedExpr.ty, tryErrType: exprErrType, retErrType: retErrType)))
} else {
exprOkType
}
} else {
return Err(TypeError(position: token.position, kind: TypeErrorKind.InvalidTryLocation(InvalidTryLocationReason.InvalidFunctionReturnType(fnLabel: currentFn.label, tryType: typedExpr.ty, returnType: currentFn.returnType, isResult: true))))
}
} else if self._typeIsOption(typedExpr.ty) |inner| {
if returnTypeResultErr {
return Err(TypeError(position: token.position, kind: TypeErrorKind.InvalidTryLocation(InvalidTryLocationReason.InvalidFunctionReturnType(fnLabel: currentFn.label, tryType: typedExpr.ty, returnType: currentFn.returnType, isResult: false))))
} else {
inner
}
} else {
return Err(TypeError(position: typedExpr.token.position, kind: TypeErrorKind.InvalidTryTarget(typedExpr.ty)))
}

(tryValType, None)
}

Ok(TypedAstNode(token: token, ty: tryValType, kind: TypedAstNodeKind.Try(typedExpr, typedElseClause)))
Expand Down
Loading

0 comments on commit d322cec

Please sign in to comment.