Skip to content

Commit

Permalink
Selfhost: Adding tuple destructuring (#476)
Browse files Browse the repository at this point in the history
* Selfhost: Adding tuple destructuring

Add tuple destructuring to the selfhosted typechecker.

* Whoops, forgot FILE_NAME

* Adding more tests for tuple destructuring

* Whoops, I put the test assertions in the wrong place

* Compiler: Adding support for tuple destructuring
  • Loading branch information
kengorab authored Oct 20, 2024
1 parent ca78e4d commit 580140a
Show file tree
Hide file tree
Showing 50 changed files with 2,567 additions and 172 deletions.
15 changes: 0 additions & 15 deletions .github/workflows/release.yml

This file was deleted.

5 changes: 4 additions & 1 deletion selfhost/example.abra
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
println("hello")
val tuple = ("a", (3.14, true))
println(tuple)
val (a, (three, tTrue)) = tuple
println("a:", a, "three:", three, "tTrue:", tTrue)
143 changes: 81 additions & 62 deletions selfhost/src/compiler.abra
Original file line number Diff line number Diff line change
Expand Up @@ -284,23 +284,20 @@ export type Compiler {
self._currentFn.block.registerLabel(labelThen)
if conditionBinding |_p| {
// TODO: destructuring
val _bindingPattern = _p[0]
val bindingPattern = _p[0]
val vars = _p[1]
val variables = vars.keyBy(v => v.label.name)

val bindingVar = if vars[0] |v| v else return unreachable("Binding pattern requires 1 variable", node.token.position)
val slotName = self._currentFn.block.addVar(variableToVar(bindingVar))
if condExprIsOpt |innerTy| {
val bindingVal = if condExprIsOpt |innerTy| {
val innerQbeType = match self._getQbeTypeForTypeExpect(innerTy, "unacceptable type", None) { Ok(v) => v, Err(e) => return Err(e) }
val optInnerValue = match self._emitOptValueGetValue(innerQbeType, condVal) { Ok(v) => v, Err(e) => return Err(e) }

val slot = self._buildStackAllocForQbeType(innerQbeType, Some(slotName))
self._currentFn.block.buildStore(innerQbeType, optInnerValue, slot)
optInnerValue
} else {
val boolTypeQbe = match self._getQbeTypeForTypeExpect(Type(kind: TypeKind.PrimitiveBool), "bool qbe type should exist") { Ok(v) => v, Err(e) => return Err(e) }
val slot = self._buildStackAllocForQbeType(boolTypeQbe, Some(slotName))
self._currentFn.block.buildStore(boolTypeQbe, Value.Int(1), slot)
Value.Int(1)
}
match self._compileBindingPattern(bindingPattern, variables, Some(bindingVal)) { Ok(v) => v, Err(e) => return Err(e) }
}

for node in ifBlock {
match self._compileStatement(node) { Ok(v) => v, Err(e) => return Err(e) }
}
Expand Down Expand Up @@ -344,23 +341,18 @@ export type Compiler {
self._currentFn.block.registerLabel(loopBodyLabel)
if conditionBinding |_p| {
// TODO: destructuring
val _bindingPattern = _p[0]
val bindingPattern = _p[0]
val vars = _p[1]
val variables = vars.keyBy(v => v.label.name)

// }
val bindingVar = if vars[0] |v| v else return unreachable("Binding pattern requires 1 variable", node.token.position)
val slotName = self._currentFn.block.addVar(variableToVar(bindingVar))
if condExprIsOpt |innerTy| {
val bindingVal = if condExprIsOpt |innerTy| {
val innerQbeType = match self._getQbeTypeForTypeExpect(innerTy, "unacceptable type", None) { Ok(v) => v, Err(e) => return Err(e) }
val optInnerValue = match self._emitOptValueGetValue(innerQbeType, condVal) { Ok(v) => v, Err(e) => return Err(e) }

val slot = self._buildStackAllocForQbeType(innerQbeType, Some(slotName))
self._currentFn.block.buildStore(innerQbeType, optInnerValue, slot)
optInnerValue
} else {
val boolTypeQbe = match self._getQbeTypeForTypeExpect(Type(kind: TypeKind.PrimitiveBool), "bool qbe type should exist") { Ok(v) => v, Err(e) => return Err(e) }
val slot = self._buildStackAllocForQbeType(boolTypeQbe, Some(slotName))
self._currentFn.block.buildStore(boolTypeQbe, Value.Int(1), slot)
Value.Int(1)
}
match self._compileBindingPattern(bindingPattern, variables, Some(bindingVal)) { Ok(v) => v, Err(e) => return Err(e) }
}

for node in block {
Expand Down Expand Up @@ -492,17 +484,18 @@ export type Compiler {
}

self._currentFn.block.registerLabel(loopStartLabel)
val itemBindingVar = if itemBindingPattern[1][0] |v| v else return unreachable("Binding pattern requires 1 variable", node.token.position)
// TODO: destructuring
val iterateePattern = itemBindingPattern[0]
val iterateeBindingVars = itemBindingPattern[1]
val iterateeBindingVariables = iterateeBindingVars.keyBy(v => v.label.name)
val nextRet = match self._currentFn.block.buildCall(Callable.Function(nextFnVal), [iterVal], Some("next_ret")) { Ok(v) => v, Err(e) => return qbeError(e) }

val variantIsOptionSome = match self._emitOptValueIsSomeVariant(nextRet) { Ok(v) => v, Err(e) => return Err(e) }
self._currentFn.block.buildJnz(variantIsOptionSome, loopBodyLabel, loopEndLabel)

self._currentFn.block.registerLabel(loopBodyLabel)
val itemSlotName = self._currentFn.block.addVar(variableToVar(itemBindingVar))
val optInnerValue = match self._emitOptValueGetValue(nextItemQbeTy, nextRet) { Ok(v) => v, Err(e) => return Err(e) }
val itemSlot = self._buildStackAllocForQbeType(nextItemQbeTy, Some(itemSlotName))
self._currentFn.block.buildStore(nextItemQbeTy, optInnerValue, itemSlot)
match self._compileBindingPattern(iterateePattern, iterateeBindingVariables, Some(optInnerValue)) { Ok(v) => v, Err(e) => return Err(e) }
if indexBindingSlot |idxSlot| {
val idxVal = self._currentFn.block.buildLoadL(idxSlot)
val idxIncrVal = match self._currentFn.block.buildAdd(Value.Int(1), idxVal) { Ok(v) => v, Err(e) => return qbeError(e) }
Expand All @@ -521,35 +514,14 @@ export type Compiler {
Ok(None)
}
TypedAstNodeKind.BindingDeclaration(bindingDeclNode) => {
val bindingName = match bindingDeclNode.bindingPattern {
BindingPattern.Variable(label) => label.name
}
val variable = if bindingDeclNode.variables[0] |v| v else return unreachable("binding decl needs at least 1 variable")

val varTy = match self._getQbeTypeForTypeExpect(variable.ty, "unacceptable type for variable", Some(variable.label.position)) { Ok(v) => v, Err(e) => return Err(e) }
val slotName = self._currentFn.block.addVar(variableToVar(variable))

if bindingDeclNode.expr |expr| {
val variables = bindingDeclNode.variables.keyBy(v => v.label.name)
val exprVal = if bindingDeclNode.expr |expr| {
val res = match self._compileExpression(expr) { Ok(v) => v, Err(e) => return Err(e) }
if variable.isExported {
val name = self._exportedVarName(self._currentModule.id, variable.label.name)
val slot = self._builder.addData(QbeData(name: name, kind: QbeDataKind.Constants([(varTy, varTy.zeroValue())])))
self._currentFn.block.buildStore(varTy, res, slot)
} else if variable.isCaptured && variable.mutable {
// Only move captured variables to the heap if they're mutable - captured immutable variables can just have their value present in
// a closure's captures array, but a mutable variable needs an additional layer of indirection to handle possible reassignment.
self._currentFn.block.addComment("move captured mutable '${variable.label.name}' to heap")
val size = varTy.size()
val heapMem = match self._currentFn.block.buildCall(Callable.Function(self._malloc), [Value.Int(size)], Some("${variable.label.name}.mem")) { Ok(v) => v, Err(e) => return qbeError(e) }
self._currentFn.block.buildStore(varTy, res, heapMem)

val slot = self._buildStackAllocForQbeType(QbeType.Pointer, Some(slotName))
self._currentFn.block.buildStore(QbeType.Pointer, heapMem, slot)
} else {
val slot = self._buildStackAllocForQbeType(varTy, Some(slotName))
self._currentFn.block.buildStore(varTy, res, slot)
}
Some(res)
} else {
None
}
match self._compileBindingPattern(bindingDeclNode.bindingPattern, variables, exprVal) { Ok(v) => v, Err(e) => return Err(e) }

Ok(None)
}
Expand Down Expand Up @@ -1774,22 +1746,18 @@ export type Compiler {
self._currentFn.block.registerLabel(labelThen)
if conditionBinding |_p| {
// TODO: destructuring
val _bindingPattern = _p[0]
val bindingPattern = _p[0]
val vars = _p[1]
val variables = vars.keyBy(v => v.label.name)

val bindingVar = if vars[0] |v| v else return unreachable("Binding pattern requires 1 variable", node.token.position)
val slotName = self._currentFn.block.addVar(variableToVar(bindingVar))
if condExprIsOpt |innerTy| {
val bindingVal = if condExprIsOpt |innerTy| {
val innerQbeType = match self._getQbeTypeForTypeExpect(innerTy, "unacceptable type", None) { Ok(v) => v, Err(e) => return Err(e) }
val optInnerValue = match self._emitOptValueGetValue(innerQbeType, condVal) { Ok(v) => v, Err(e) => return Err(e) }

val slot = self._buildStackAllocForQbeType(innerQbeType, Some(slotName))
self._currentFn.block.buildStore(innerQbeType, optInnerValue, slot)
optInnerValue
} else {
val boolTypeQbe = match self._getQbeTypeForTypeExpect(Type(kind: TypeKind.PrimitiveBool), "bool qbe type should exist") { Ok(v) => v, Err(e) => return Err(e) }
val slot = self._buildStackAllocForQbeType(boolTypeQbe, Some(slotName))
self._currentFn.block.buildStore(boolTypeQbe, Value.Int(1), slot)
Value.Int(1)
}
match self._compileBindingPattern(bindingPattern, variables, Some(bindingVal)) { Ok(v) => v, Err(e) => return Err(e) }
}
for node, idx in ifBlock {
val res = match self._compileStatement(node) { Ok(v) => v, Err(e) => return Err(e) }
Expand Down Expand Up @@ -2048,6 +2016,57 @@ export type Compiler {
Ok(result)
}

func _compileBindingPattern(self, pattern: BindingPattern, variables: Map<String, Variable>, exprVal: Value?): Result<Int, CompileError> {
val varName = match pattern {
BindingPattern.Variable(label) => label.name
BindingPattern.Tuple(_, patterns) => {
val tuplePtr = if exprVal |v| v else return Ok(0)

for pat, idx in patterns {
val tupleItemSlot = match self._currentFn.block.buildAdd(Value.Int(idx * QbeType.Pointer.size()), tuplePtr) { Ok(v) => v, Err(e) => return qbeError(e) }
val tupleItemSlotTy = match pat {
BindingPattern.Variable(label) => {
val variable = if variables[label.name] |v| v else return unreachable("expected binding '${label.name}', but missing from variables")
val varTy = match self._getQbeTypeForTypeExpect(variable.ty, "unacceptable type for variable", Some(variable.label.position)) { Ok(v) => v, Err(e) => return Err(e) }
varTy
}
_ => QbeType.Pointer
}
val tupleItemVal = self._currentFn.block.buildLoad(tupleItemSlotTy, tupleItemSlot)
match self._compileBindingPattern(pat, variables, Some(tupleItemVal)) { Ok(v) => v, Err(e) => return Err(e) }
}

return Ok(0)
}
}

val variable = if variables[varName] |v| v else return unreachable("expected binding '$varName', but missing from variables")
val varTy = match self._getQbeTypeForTypeExpect(variable.ty, "unacceptable type for variable", Some(variable.label.position)) { Ok(v) => v, Err(e) => return Err(e) }
val slotName = self._currentFn.block.addVar(variableToVar(variable))

val v = if exprVal |v| v else return Ok(0)
if variable.isExported {
val name = self._exportedVarName(self._currentModule.id, variable.label.name)
val slot = self._builder.addData(QbeData(name: name, kind: QbeDataKind.Constants([(varTy, varTy.zeroValue())])))
self._currentFn.block.buildStore(varTy, v, slot)
} else if variable.isCaptured && variable.mutable {
// Only move captured variables to the heap if they're mutable - captured immutable variables can just have their value present in
// a closure's captures array, but a mutable variable needs an additional layer of indirection to handle possible reassignment.
self._currentFn.block.addComment("move captured mutable '${variable.label.name}' to heap")
val size = varTy.size()
val heapMem = match self._currentFn.block.buildCall(Callable.Function(self._malloc), [Value.Int(size)], Some("${variable.label.name}.mem")) { Ok(v) => v, Err(e) => return qbeError(e) }
self._currentFn.block.buildStore(varTy, v, heapMem)

val slot = self._buildStackAllocForQbeType(QbeType.Pointer, Some(slotName))
self._currentFn.block.buildStore(QbeType.Pointer, heapMem, slot)
} else {
val slot = self._buildStackAllocForQbeType(varTy, Some(slotName))
self._currentFn.block.buildStore(varTy, v, slot)
}

Ok(0)
}

func _getCapturedVarPtr(self, variable: Variable): Result<Value, CompileError> {
if self._currentFunction |fn| {
if self._currentFn._env |env| {
Expand Down
19 changes: 19 additions & 0 deletions selfhost/src/parser.abra
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ export enum TypeIdentifier {

export enum BindingPattern {
Variable(label: Label)
Tuple(lParenTok: Token, patterns: BindingPattern[])

func position(self): Position {
match self {
BindingPattern.Variable(label) => label.position
BindingPattern.Tuple(lParenTok, _) => lParenTok.position
}
}
}

export type BindingDeclarationNode {
Expand Down Expand Up @@ -561,6 +569,17 @@ export type Parser {
val label = match self._expectNextLabel() { Ok(v) => v, Err(e) => return Err(e) }
BindingPattern.Variable(label)
}
TokenKind.LParen => {
val lParenTok = match self._expectNext() { Ok(v) => v, Err(e) => return Err(e) }
val itemsRes = self._commaSeparated(end: TokenKind.RParen, consumeFinal: false, fn: () => self._parseBindingPattern())
val items = match itemsRes { Ok(v) => v, Err(e) => return Err(e) }
val rParenTok = match self._expectNext() { Ok(v) => v, Err(e) => return Err(e) }
if items.isEmpty() {
return Err(ParseError(position: rParenTok.position, kind: ParseErrorKind.ExpectedToken([TokenKind.Ident(""), TokenKind.LParen(true)], token.kind)))
}

BindingPattern.Tuple(lParenTok, items)
}
_ => return Err(ParseError(position: token.position, kind: ParseErrorKind.ExpectedToken([TokenKind.Ident("")], token.kind)))
}

Expand Down
10 changes: 10 additions & 0 deletions selfhost/src/test_utils.abra
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,16 @@ export func printBindingPatternAsJson(bindingPattern: BindingPattern, indentLeve
print("\"variable\",\n$fieldsIndent\"label\": ")
printLabelAsJson(label)
}
BindingPattern.Tuple(_, patterns) => {
println("\"tuple\",\n$fieldsIndent\"patterns\": [")
for pat, idx in patterns {
printBindingPatternAsJson(pat, currentIndentLevel + 2, currentIndentLevel + 2)
if idx != patterns.length - 1 {
println(",")
}
}
print("\n$fieldsIndent]")
}
}
print("\n$endIndent}")
}
Expand Down
Loading

0 comments on commit 580140a

Please sign in to comment.