Skip to content

Commit

Permalink
Try-else expressions (#481)
Browse files Browse the repository at this point in the history
Add an optional else-clause to try expressions which can transform the
possible resulting error value of a try expression into a new value. The
type of this value must match the success type of the try expression; it
can also return a new value.
  • Loading branch information
kengorab authored Nov 3, 2024
1 parent d2ca0c0 commit 4d8f2d1
Show file tree
Hide file tree
Showing 29 changed files with 2,620 additions and 37 deletions.
11 changes: 8 additions & 3 deletions projects/compiler/example.abra
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
func ok(): Result<Int, String> = Ok(123)
func err(): Result<Int, String> = Err("foo")

func f1(): Result<Int, String> {
val x = try ok()
func f1(): Result<Int, Int> {
var acc = 0
while acc < 10 {
val x = try err() else break
println(x)
acc += x
}

Ok(x + 1)
Ok(acc)
}

/// Expect: Result.Ok(value: 124)
Expand Down
60 changes: 51 additions & 9 deletions projects/compiler/src/compiler.abra
Original file line number Diff line number Diff line change
Expand Up @@ -1742,22 +1742,64 @@ export type Compiler {
val res = try self._compileMatch(node, isStatement, expr, cases)
if res |res| Ok(res) else return unreachable("match expression needs a resulting value", node.token.position)
}
TypedAstNodeKind.Try(expr) => {
TypedAstNodeKind.Try(expr, elseClause) => {
val exprVal = try self._compileExpression(expr)
val isErr = try self._emitResultValueIsOkVariant(exprVal, negate: true)

val labelIsErr = self._currentFn.block.addLabel("isErr")
val labelCont = self._currentFn.block.addLabel("cont")
self._currentFn.block.buildJnz(isErr, labelIsErr, labelCont)

self._currentFn.block.registerLabel(labelIsErr)
self._currentFn.block.buildReturn(Some(exprVal))
if elseClause |clause| {
val phiCases: (Label, Value)[] = []

self._currentFn.block.registerLabel(labelCont)
val okValTy = try self._getQbeTypeForTypeExpect(node.ty, "unacceptable type", None)
val okValue = try self._emitResultValueGetValue(okValTy, exprVal)
val labelIsOk = self._currentFn.block.addLabel("isOk")
self._currentFn.block.buildJnz(isErr, labelIsErr, labelIsOk)

self._currentFn.block.registerLabel(labelIsErr)
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)
try self._compileBindingPattern(bindingPattern, variables, Some(bindingVal))
}
for node, idx in clause.block {
val res = try self._compileStatement(node)
if idx == clause.block.length - 1 {
if res |res| {
val label = self._currentFn.block.currentLabel
phiCases.push((label, res))
} else if !clause.terminator {
return unreachable("last statement in try-else block has no value and is not a terminator", node.token.position)
}
}
}
if !clause.terminator {
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)
val label = self._currentFn.block.currentLabel
phiCases.push((label, okVal))
self._currentFn.block.buildJmp(labelCont)

self._currentFn.block.registerLabel(labelCont)
val res = match self._currentFn.block.buildPhi(phiCases) { Ok(v) => v, Err(e) => return qbeError(e) }
Ok(res)
} else {
self._currentFn.block.buildJnz(isErr, labelIsErr, labelCont)

self._currentFn.block.registerLabel(labelIsErr)
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)

Ok(okValue)
Ok(okValue)
}
}
_ => unreachable("node must be a statement", node.token.position)
}
Expand Down Expand Up @@ -4274,7 +4316,7 @@ export type Compiler {
// in place of the type. 16 chars seems like a good balance between long enough that there won't be any conflicts, but also
// short enough that there's enough room for this type to be used as a generic in other types without making that type's or
// function's identifier too long.
if name.length >= 80 {
if name.length >= 70 {
val alias = if self._aliasedTypeNames[name] |alias| alias else {
val replacement = String.random(16)
self._aliasedTypeNames[name] = replacement
Expand Down
23 changes: 21 additions & 2 deletions projects/compiler/src/parser.abra
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export enum AstNodeKind {
Assignment(expr: AstNode, op: AssignOp, mode: AssignmentMode)
If(condition: AstNode, conditionBinding: BindingPattern?, ifBlock: AstNode[], elseBlock: AstNode[]?)
Match(expr: AstNode, cases: MatchCase[])
Try(expr: AstNode)
Try(expr: AstNode, elseClause: (Token, BindingPattern?, AstNode[])?)
While(condition: AstNode, conditionBinding: BindingPattern?, block: AstNode[])
For(itemPattern: BindingPattern, indexPattern: BindingPattern?, iterator: AstNode, block: AstNode[])
BindingDeclaration(value: BindingDeclarationNode)
Expand Down Expand Up @@ -1476,7 +1476,26 @@ export type Parser {
val token = try self._expectNext()
val expr = try self._parseExpression()

Ok(AstNode(token: token, kind: AstNodeKind.Try(expr)))
val elseClause = if self._peek() |token| {
if token.kind == TokenKind.Else {
val elseToken = try self._expectNextTokenKind(TokenKind.Else)
val nextToken = try self._expectPeek()
val binding = if nextToken.kind == TokenKind.Pipe {
self._advance() // consume opening '|' token
val pat = try self._parseBindingPattern()
try self._expectNextTokenKind(TokenKind.Pipe)
Some(pat)
} else {
None
}

val elseBlock = try self._parseBlockOrSingleExpression(allowTerminators: true)

Some((elseToken, binding, elseBlock))
} else None
} else None

Ok(AstNode(token: token, kind: AstNodeKind.Try(expr, elseClause)))
}

func _parseWhileLoop(self): Result<AstNode, ParseError> {
Expand Down
22 changes: 21 additions & 1 deletion projects/compiler/src/test_utils.abra
Original file line number Diff line number Diff line change
Expand Up @@ -719,10 +719,30 @@ func printAstNodeKindAsJson(kind: AstNodeKind, indentLevelStart: Int, currentInd
println("$fieldsIndent]")
}
}
AstNodeKind.Try(expr) => {
AstNodeKind.Try(expr, elseClause) => {
println("$fieldsIndent\"name\": \"try\",")
print("$fieldsIndent\"expr\": ")
printAstNodeAsJson(expr, 0, currentIndentLevel + 1)
if elseClause |(_, bindingPattern, block)| {
if bindingPattern |pat| {
print(",\n$fieldsIndent\"elseBindingPattern\": ")
printBindingPatternAsJson(pat, 0, currentIndentLevel + 1)
} else {
print(",\n$fieldsIndent\"elseBindingPattern\": null")
}

if block.isEmpty() {
println(",\n$fieldsIndent\"elseBlock\": []")
} else {
println(",\n$fieldsIndent\"elseBlock\": [")
for node, idx in block {
printAstNodeAsJson(node, currentIndentLevel + 2, currentIndentLevel + 2)
val comma = if idx != block.length - 1 "," else ""
println("$comma")
}
print("$fieldsIndent]")
}
}
println()
}
AstNodeKind.While(condition, conditionBinding, block) => {
Expand Down
86 changes: 72 additions & 14 deletions projects/compiler/src/typechecker.abra
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export enum ScopeKind {
While
For
Type
Try
}

export type Scope {
Expand Down Expand Up @@ -739,6 +740,13 @@ export type TypedMatchCase {
terminator: Terminator?
}

type TypedTryElseClause {
pattern: (BindingPattern, Variable[])?
errorType: Type
block: TypedAstNode[]
terminator: Terminator?
}

export enum TypedAstNodeKind {
Literal(value: LiteralAstNode)
StringInterpolation(exprs: TypedAstNode[])
Expand All @@ -757,7 +765,7 @@ export enum TypedAstNodeKind {
Assignment(mode: TypedAssignmentMode, op: AssignOp, expr: TypedAstNode)
If(isStatement: Bool, typedCondition: TypedAstNode, conditionBinding: (BindingPattern, Variable[])?, typedIfBlock: TypedAstNode[], ifBlockTerminator: Terminator?, typedElseBlock: TypedAstNode[], elseBlockTerminator: Terminator?)
Match(isStatement: Bool, expr: TypedAstNode, cases: TypedMatchCase[])
Try(typedExpr: TypedAstNode)
Try(typedExpr: TypedAstNode, typedElseClause: TypedTryElseClause?)
While(typedCondition: TypedAstNode, conditionBinding: (BindingPattern, Variable[])?, block: TypedAstNode[], terminator: Terminator?)
For(typedIterator: TypedAstNode, itemBinding: (BindingPattern, Variable[]), indexBinding: Variable?, block: TypedAstNode[])
BindingDeclaration(node: TypedBindingDeclarationNode)
Expand Down Expand Up @@ -980,8 +988,8 @@ type TypeError {
}
}
}
TypeErrorKind.MissingRequiredIfExprBlock(clause, missing) => {
lines.push("Incomplete if-expression")
TypeErrorKind.MissingRequiredBlock(exprKind, clause, missing) => {
lines.push("Incomplete $exprKind expression")
lines.push(self._getCursorLine(self.position, contents))
if missing {
lines.push("The $clause-block must exist and contain a value")
Expand Down Expand Up @@ -1409,7 +1417,7 @@ enum TypeErrorKind {
IllegalNonConstantEnumVariant
IllegalValueType(ty: Type, purpose: String)
IllegalControlFlowType(ty: Type, purpose: String)
MissingRequiredIfExprBlock(clause: String, missing: Bool)
MissingRequiredBlock(exprKind: String, clause: String, missing: Bool)
InvalidParamPosition(purpose: String)
DuplicateParameter(name: String)
InvalidVarargType(ty: Type)
Expand Down Expand Up @@ -3360,6 +3368,10 @@ export type Typechecker {
scope = sc.parent
continue
}
ScopeKind.Try => {
scope = sc.parent
continue
}
ScopeKind.While => true
ScopeKind.For => true
}
Expand Down Expand Up @@ -3473,7 +3485,7 @@ export type Typechecker {
self.currentScope.terminator = Terminator.combine(ifBlockTerminator, elseBlockTerminator)

val ty = if !isStatement {
val t = if ifType |ifType| {
if ifType |ifType| {
if typedElseBlock[-1] |lastElseNode| {
val elseType = lastElseNode.ty

Expand All @@ -3488,15 +3500,13 @@ export type Typechecker {
return Err(TypeError(position: lastElseNode.token.position, kind: TypeErrorKind.TypeMismatch([ifType], elseType)))
}

val t = if ifType.kind == TypeKind.Never { elseType } else { ifType }
t
if ifType.kind == TypeKind.Never { elseType } else { ifType }
} else {
return Err(TypeError(position: token.position, kind: TypeErrorKind.MissingRequiredIfExprBlock(clause: "else", missing: !hasElseBlock)))
return Err(TypeError(position: token.position, kind: TypeErrorKind.MissingRequiredBlock(exprKind: "if-else", clause: "else", missing: !hasElseBlock)))
}
} else {
return Err(TypeError(position: token.position, kind: TypeErrorKind.MissingRequiredIfExprBlock(clause: "if", missing: false)))
return Err(TypeError(position: token.position, kind: TypeErrorKind.MissingRequiredBlock(exprKind: "if-else", clause: "if", missing: false)))
}
t
} else {
Type(kind: TypeKind.PrimitiveUnit)
}
Expand Down Expand Up @@ -3789,7 +3799,7 @@ export type Typechecker {
AstNodeKind.Lambda(value) => self._typecheckLambda(token, value, typeHint)
AstNodeKind.If(condition, conditionBinding, ifBlock, elseBlock) => self._typecheckIf(token, condition, conditionBinding, ifBlock, elseBlock, typeHint)
AstNodeKind.Match(subject, cases) => self._typecheckMatch(token, subject, cases, typeHint)
AstNodeKind.Try(expr) => self._typecheckTry(token, expr, typeHint)
AstNodeKind.Try(expr, elseClause) => self._typecheckTry(token, expr, elseClause, typeHint)
_ => unreachable(node.token.position, "all other node types should have already been handled")
}

Expand Down Expand Up @@ -4881,7 +4891,7 @@ export type Typechecker {
Ok(TypedAstNode(token: token, ty: lambdaTy, kind: TypedAstNodeKind.Lambda(fn, typeHint)))
}

func _typecheckTry(self, token: Token, expr: AstNode, typeHint: Type?): Result<TypedAstNode, TypeError> {
func _typecheckTry(self, token: Token, expr: AstNode, elseClause: (Token, BindingPattern?, AstNode[])?, typeHint: Type?): Result<TypedAstNode, TypeError> {
// 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)))

Expand All @@ -4901,11 +4911,59 @@ export type Typechecker {
// 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)))

if !self._typeSatisfiesRequired(ty: retErrType, required: tryErrType) {
val typedElseClause = if elseClause |(elseToken, bindingPattern, elseBlock)| {
val isStatement = if typeHint |hint| hint.kind == TypeKind.PrimitiveUnit else false

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

val elseBindingPattern = if bindingPattern |pat| {
val variables = try self._typecheckBindingPattern(false, pat, tryErrType)
Some((pat, variables))
} else {
None
}

val typedNodes: TypedAstNode[] = []
for node, idx in elseBlock {
if self.currentScope.terminator return Err(TypeError(position: node.token.position, kind: TypeErrorKind.UnreachableCode))

val typedNode = if idx == elseBlock.length - 1 && !isStatement {
try self._typecheckExpressionOrTerminator(node, typeHint)
} else {
try self._typecheckStatement(node, None)
}
typedNodes.push(typedNode)
}
val terminator = self.currentScope.terminator

if typedNodes[-1] |lastElseNode| {
val elseType = lastElseNode.ty

if tryValType.hasUnfilledHoles() {
tryValType.tryFillHoles(elseType)
}
if elseType.hasUnfilledHoles() {
elseType.tryFillHoles(tryValType)
}

if !self._typeSatisfiesRequired(ty: elseType, required: tryValType) {
return Err(TypeError(position: lastElseNode.token.position, kind: TypeErrorKind.TypeMismatch([tryValType], elseType)))
}
} else {
return Err(TypeError(position: elseToken.position, kind: TypeErrorKind.MissingRequiredBlock(exprKind: "try-else", clause: "else", missing: false)))
}

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)))
} else {
None
}

Ok(TypedAstNode(token: token, ty: tryValType, kind: TypedAstNodeKind.Try(typedExpr)))
Ok(TypedAstNode(token: token, ty: tryValType, kind: TypedAstNodeKind.Try(typedExpr, typedElseClause)))
}
}

Expand Down
34 changes: 30 additions & 4 deletions projects/compiler/src/typechecker_test_utils.abra
Original file line number Diff line number Diff line change
Expand Up @@ -673,11 +673,36 @@ export type Jsonifier {
})
println()
}
TypedAstNodeKind.Try(expr) => {
TypedAstNodeKind.Try(expr, elseClause) => {
self.println("\"kind\": \"try\",")

self.print("\"expr\": ")
self.printNode(expr)
if elseClause |elseClause| {
println(",")
self.print("\"elseBindingPattern\": ")
self.opt(elseClause.pattern, _pair => {
val (pat, vars) = _pair
println("{")
self.indentInc()

self.print("\"bindingPattern\": ")
printBindingPatternAsJson(pat, 0, self.indentLevel)
println(",")

self.print("\"variables\": ")
self.array(vars, v => self.printVariable(v))
println()

self.indentDec()
self.print("}")
})

println(",")

self.print("\"elseBlock\": ")
self.array(elseClause.block, n => self.printNode(n))
}
println()
}

Expand All @@ -690,16 +715,17 @@ export type Jsonifier {
println(",")

self.print("\"conditionBindingPattern\": ")
self.opt(conditionBindingPattern, pair => {
self.opt(conditionBindingPattern, _pair => {
val (pat, vars) = _pair
println("{")
self.indentInc()

self.print("\"bindingPattern\": ")
printBindingPatternAsJson(pair[0], 0, self.indentLevel)
printBindingPatternAsJson(pat, 0, self.indentLevel)
println(",")

self.print("\"variables\": ")
self.array(pair[1], v => self.printVariable(v))
self.array(vars, v => self.printVariable(v))
println()

self.indentDec()
Expand Down
Loading

0 comments on commit 4d8f2d1

Please sign in to comment.