about summary refs log tree commit diff
path: root/pkg/lang
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2022-07-11 00:35:59 +0200
committerMel <einebeere@gmail.com>2022-07-11 00:35:59 +0200
commit1b6ef1e43e1ec1107ce29a6438b399352d09fbc2 (patch)
tree20b8cc80564f0c76c9e68e9fb6df6163b06b24f2 /pkg/lang
parentb16b70fd40ffc72ff861afe0517cba0e37ba1145 (diff)
downloadjinx-1b6ef1e43e1ec1107ce29a6438b399352d09fbc2.tar.zst
jinx-1b6ef1e43e1ec1107ce29a6438b399352d09fbc2.zip
Rebuild compiler and code builder with markers
Diffstat (limited to 'pkg/lang')
-rw-r--r--pkg/lang/compiler/compiler.go231
-rw-r--r--pkg/lang/compiler/scope_chain.go226
-rw-r--r--pkg/lang/compiler/symbol.go31
-rw-r--r--pkg/lang/vm/code/builder.go108
-rw-r--r--pkg/lang/vm/code/builder_test.go31
-rw-r--r--pkg/lang/vm/code/errors.go32
-rw-r--r--pkg/lang/vm/code/marker.go23
-rw-r--r--pkg/lang/vm/text/compiler.go2
8 files changed, 479 insertions, 205 deletions
diff --git a/pkg/lang/compiler/compiler.go b/pkg/lang/compiler/compiler.go
index 116db1c..a31ade7 100644
--- a/pkg/lang/compiler/compiler.go
+++ b/pkg/lang/compiler/compiler.go
@@ -31,7 +31,7 @@ func (comp *Compiler) Compile() (code.Code, error) {
 
 	target.AppendOp(code.OpHalt)
 
-	return target.Build(), nil
+	return target.Build()
 }
 
 func (comp *Compiler) compileStmt(t *code.Builder, stmt ast.Stmt) error {
@@ -39,6 +39,12 @@ func (comp *Compiler) compileStmt(t *code.Builder, stmt ast.Stmt) error {
 	switch stmt.Kind {
 	case ast.StmtKindEmpty:
 		// Do nothing.
+	case ast.StmtKindUse:
+		panic("use statements not implemented")
+	case ast.StmtKindFnDecl:
+		panic("function declaration statements not implemented")
+	case ast.StmtKindObjectDecl:
+		panic("object declaration statements not implemented")
 	case ast.StmtKindVarDecl:
 		decl := stmt.Value.(ast.StmtVarDecl)
 		err = comp.compileVarDeclStmt(t, decl)
@@ -51,11 +57,21 @@ func (comp *Compiler) compileStmt(t *code.Builder, stmt ast.Stmt) error {
 	case ast.StmtKindForIn:
 		forCondIn := stmt.Value.(ast.StmtForIn)
 		err = comp.compileForInStmt(t, forCondIn)
+	case ast.StmtKindTry:
+		panic("try statements not implemented")
+	case ast.StmtKindReturn:
+		panic("return statements not implemented")
+	case ast.StmtKindContinue:
+		panic("continue statements not implemented")
+	case ast.StmtKindBreak:
+		panic("break statements not implemented")
+	case ast.StmtKindThrow:
+		panic("throw statements not implemented")
 	case ast.StmtKindExpr:
 		expr := stmt.Value.(ast.StmtExpr).Value
 		err = comp.compileExpr(t, expr)
 	default:
-		panic(fmt.Errorf("statement of kind %v not implemented", stmt.Kind))
+		panic(fmt.Errorf("unknown statement kind: %d", stmt.Kind))
 	}
 
 	return err
@@ -90,54 +106,56 @@ func (comp *Compiler) compileIfStmt(t *code.Builder, ifStmt ast.StmtIf) error {
 	//				   preventing other CondNodes from running. This is missing from the last CondNode.
 	//    Example: `jmp @end`
 
-	subUnits := make([]code.Builder, 0, len(ifStmt.Conds))
+	// First we create all the markers we'll need for the if statement
+	parentMarker := comp.scopes.CreateAnonymousFunctionSubUnit()
 
-	totalLength := 0
+	endMarker := parentMarker.SubMarker("end")
+
+	condMarkers := make([]code.Marker, 0, len(ifStmt.Conds)-1) // We don't need a marker for the first CondNode.
+	for i := 0; i < len(ifStmt.Conds)-1; i++ {
+		condMarker := parentMarker.SubMarker("cond_%d", i+1)
+		condMarkers = append(condMarkers, condMarker)
+	}
 
 	for i, cond := range ifStmt.Conds {
-		// Then block
-		thenTarget := code.NewBuilder()
-		if err := comp.compileBlockNode(&thenTarget, cond.Then); err != nil {
-			return err
-		}
+		isFirst := i == 0
+		isLast := i == len(ifStmt.Conds)-1
 
-		totalLength += thenTarget.Len()
-		if i != len(ifStmt.Conds)-1 {
-			totalLength += lengthOfAJumpInstruction
+		// If we aren't in the first CondNode, the node before it needs a marker to here.
+		if !isFirst {
+			marker := condMarkers[i-1]
+			t.PutMarker(marker)
 		}
 
-		// Condition check
-		conditionTarget := code.NewBuilder()
 		if !cond.Cond.IsEmpty() {
-			if err := comp.compileExpr(&conditionTarget, cond.Cond); err != nil {
+			// Condition check
+			if err := comp.compileExpr(t, cond.Cond); err != nil {
 				return err
 			}
 
-			totalLength += conditionTarget.Len() + lengthOfAJumpInstruction // condjmp
-
-			conditionTarget.AppendOp(code.OpJf)
 			// Condition jump
-			conditionTarget.AppendReferenceToPc(int64(totalLength))
+			t.AppendOp(code.OpJf)
+			if isLast {
+				t.AppendMarkerReference(endMarker)
+			} else {
+				nextCondMarker := condMarkers[i]
+				t.AppendMarkerReference(nextCondMarker)
+			}
 		}
 
-		subUnit := conditionTarget
-		subUnit.AppendBuilder(thenTarget)
-		subUnits = append(subUnits, subUnit)
-	}
-
-	result := code.NewBuilder()
-
-	// Then jumps
-	for i, subUnit := range subUnits {
-		if i != len(ifStmt.Conds)-1 {
-			subUnit.AppendOp(code.OpJmp)
-			subUnit.AppendReferenceToPc(int64(totalLength))
+		// Then block
+		if err := comp.compileBlockNode(t, cond.Then); err != nil {
+			return err
 		}
 
-		result.AppendBuilderWithoutAdjustingReferences(subUnit)
+		// Then jump
+		if !isLast {
+			t.AppendOp(code.OpJmp)
+			t.AppendMarkerReference(endMarker)
+		}
 	}
 
-	t.AppendBuilder(result)
+	t.PutMarker(endMarker)
 
 	return nil
 }
@@ -149,34 +167,34 @@ func (comp *Compiler) compileForCondStmt(t *code.Builder, forCondStmt ast.StmtFo
 	// 3. Do block: Does something
 	// 4. Repeat jump: Jumps back to start
 
-	// Do block
-	doTarget := code.NewBuilder()
-	if err := comp.compileBlockNode(&doTarget, forCondStmt.Do); err != nil {
-		return err
-	}
+	parentMarker := comp.scopes.CreateAnonymousFunctionSubUnit()
+
+	startMarker := parentMarker.SubMarker("start")
+	endMarker := parentMarker.SubMarker("end")
+
+	t.PutMarker(startMarker)
 
-	conditionTarget := code.NewBuilder()
 	if !forCondStmt.Cond.IsEmpty() {
 		// Condition check
-		if err := comp.compileExpr(&conditionTarget, forCondStmt.Cond); err != nil {
+		if err := comp.compileExpr(t, forCondStmt.Cond); err != nil {
 			return err
 		}
 
-		endOfFor := conditionTarget.Len() + doTarget.Len() + lengthOfAJumpInstruction*2
-
-		// Condition jump
-		conditionTarget.AppendOp(code.OpJf)
-		conditionTarget.AppendReferenceToPc(int64(endOfFor))
+		// Condition check
+		t.AppendOp(code.OpJf)
+		t.AppendMarkerReference(endMarker)
 	}
 
-	subUnit := conditionTarget
-	subUnit.AppendBuilder(doTarget)
+	// Do block
+	if err := comp.compileBlockNode(t, forCondStmt.Do); err != nil {
+		return err
+	}
 
 	// Repeat jump
-	subUnit.AppendOp(code.OpJmp)
-	subUnit.AppendReferenceToPc(int64(0)) // Start of the for
+	t.AppendOp(code.OpJmp)
+	t.AppendMarkerReference(startMarker)
 
-	t.AppendBuilder(subUnit)
+	t.PutMarker(endMarker)
 
 	return nil
 }
@@ -216,88 +234,81 @@ func (comp *Compiler) compileForInStmt(t *code.Builder, forInStmt ast.StmtForIn)
 	// @end:
 	// halt
 
+	parentMarker := comp.scopes.CreateAnonymousFunctionSubUnit()
+
+	checkMarker := parentMarker.SubUnit("check")
+	endMarker := parentMarker.SubUnit("end")
+
 	// Preparation
-	preparationTarget := code.NewBuilder()
-	if err := comp.compileExpr(&preparationTarget, forInStmt.Collection); err != nil {
+	if err := comp.compileExpr(t, forInStmt.Collection); err != nil {
 		return err
 	}
 	collectionLocal := comp.scopes.DeclareAnonymous()
 
-	preparationTarget.AppendOp(code.OpPushInt)
-	preparationTarget.AppendInt(0)
+	t.AppendOp(code.OpPushInt)
+	t.AppendInt(0)
 	iLocal := comp.scopes.DeclareAnonymous()
 
-	preparationTarget.AppendOp(code.OpPushNull)
+	t.AppendOp(code.OpPushNull)
 	nameLocal, ok := comp.scopes.Declare(forInStmt.Name.Value)
 	if !ok {
 		return fmt.Errorf("variable %s already declared", forInStmt.Name.Value)
 	}
 
 	// Condition check
-	conditionTarget := code.NewBuilder()
+	t.PutMarker(checkMarker)
 
-	conditionTarget.AppendOp(code.OpGetLocal)
-	conditionTarget.AppendInt(int64(iLocal))
+	t.AppendOp(code.OpGetLocal)
+	t.AppendInt(int64(iLocal))
 
-	conditionTarget.AppendOp(code.OpGetLocal)
-	conditionTarget.AppendInt(int64(collectionLocal))
+	t.AppendOp(code.OpGetLocal)
+	t.AppendInt(int64(collectionLocal))
 
-	conditionTarget.AppendOp(code.OpGetMember)
-	conditionTarget.AppendString("length")
+	t.AppendOp(code.OpGetMember)
+	t.AppendString("length")
 
-	conditionTarget.AppendOp(code.OpCall)
-	conditionTarget.AppendInt(0)
+	t.AppendOp(code.OpCall)
+	t.AppendInt(0)
 
-	conditionTarget.AppendOp(code.OpLt)
+	t.AppendOp(code.OpLt)
 
-	// Do Preparation
-	doPreparationTarget := code.NewBuilder()
+	// Condition jump
+	t.AppendOp(code.OpJf)
+	t.AppendMarkerReference(endMarker)
 
-	doPreparationTarget.AppendOp(code.OpGetLocal)
-	doPreparationTarget.AppendInt(int64(collectionLocal))
+	// Do Preparation
+	t.AppendOp(code.OpGetLocal)
+	t.AppendInt(int64(collectionLocal))
 
-	doPreparationTarget.AppendOp(code.OpGetLocal)
-	doPreparationTarget.AppendInt(int64(iLocal))
+	t.AppendOp(code.OpGetLocal)
+	t.AppendInt(int64(iLocal))
 
-	doPreparationTarget.AppendOp(code.OpIndex)
+	t.AppendOp(code.OpIndex)
 
-	doPreparationTarget.AppendOp(code.OpSetLocal)
-	doPreparationTarget.AppendInt(int64(nameLocal))
+	t.AppendOp(code.OpSetLocal)
+	t.AppendInt(int64(nameLocal))
 
-	doPreparationTarget.AppendOp(code.OpGetLocal)
-	doPreparationTarget.AppendInt(int64(iLocal))
+	t.AppendOp(code.OpGetLocal)
+	t.AppendInt(int64(iLocal))
 
-	doPreparationTarget.AppendOp(code.OpPushInt)
-	doPreparationTarget.AppendInt(1)
+	t.AppendOp(code.OpPushInt)
+	t.AppendInt(1)
 
-	doPreparationTarget.AppendOp(code.OpAdd)
+	t.AppendOp(code.OpAdd)
 
-	doPreparationTarget.AppendOp(code.OpSetLocal)
-	doPreparationTarget.AppendInt(int64(iLocal))
+	t.AppendOp(code.OpSetLocal)
+	t.AppendInt(int64(iLocal))
 
 	// Do block
-	doTarget := code.NewBuilder()
-	if err := comp.compileBlockNode(&doTarget, forInStmt.Do); err != nil {
+	if err := comp.compileBlockNode(t, forInStmt.Do); err != nil {
 		return err
 	}
 
-	// Condition Jump
-
-	endOfFor := preparationTarget.Len() + conditionTarget.Len() + doPreparationTarget.Len() + doTarget.Len() + lengthOfAJumpInstruction*2
-
-	conditionTarget.AppendOp(code.OpJf)
-	conditionTarget.AppendReferenceToPc(int64(endOfFor))
-
-	subUnit := preparationTarget
-	subUnit.AppendBuilderWithoutAdjustingReferences(conditionTarget)
-	subUnit.AppendBuilder(doPreparationTarget)
-	subUnit.AppendBuilder(doTarget)
-
 	// Repeat jump
-	subUnit.AppendOp(code.OpJmp)
-	subUnit.AppendReferenceToPc(int64(preparationTarget.Len()))
+	t.AppendOp(code.OpJmp)
+	t.AppendMarkerReference(checkMarker)
 
-	t.AppendBuilder(subUnit)
+	t.PutMarker(endMarker)
 
 	return nil
 }
@@ -389,17 +400,23 @@ func (comp *Compiler) compileAssignExpr(t *code.Builder, expr ast.ExprBinary) er
 	}
 
 	name := expr.Left.Value.(ast.ExprIdent).Value.Value
-	symbol, ok := comp.scopes.Lookup(name)
+	symbolId, ok := comp.scopes.Lookup(name)
 	if !ok {
 		return fmt.Errorf("variable %s not declared", name)
 	}
 
+	if symbolId.symbolKind != SymbolKindVariable {
+		return fmt.Errorf("can't assign to a %v", symbolId.symbolKind)
+	}
+
+	symbol := comp.scopes.GetVariable(symbolId)
+
 	if err := comp.compileExpr(t, expr.Right); err != nil {
 		return err
 	}
 
 	t.AppendOp(code.OpSetLocal)
-	t.AppendInt(int64(symbol.localIndex))
+	t.AppendInt(int64(symbol.data.localIndex))
 
 	return nil
 }
@@ -478,14 +495,20 @@ func (comp *Compiler) compileArrayLitExpr(t *code.Builder, expr ast.ExprArrayLit
 }
 
 func (comp *Compiler) compileIdentExpr(t *code.Builder, expr ast.ExprIdent) error {
-	symbol, ok := comp.scopes.Lookup(expr.Value.Value)
+	symbolId, ok := comp.scopes.Lookup(expr.Value.Value)
 	if !ok {
 		return fmt.Errorf("undefined symbol %s", expr.Value.Value)
 	}
 
+	if symbolId.symbolKind != SymbolKindVariable {
+		return fmt.Errorf("%v values are not implemeted yet", symbolId.symbolKind)
+	}
+
+	symbol := comp.scopes.GetVariable(symbolId)
+
 	// TODO: Add boundries to check how the symbol should be fetched. (local, env, global, etc.)
 	t.AppendOp(code.OpGetLocal)
-	t.AppendInt(int64(symbol.localIndex))
+	t.AppendInt(int64(symbol.data.localIndex))
 
 	return nil
 }
@@ -531,7 +554,3 @@ func (comp *Compiler) compileBlockNode(t *code.Builder, block ast.BlockNode) err
 
 	return nil
 }
-
-const (
-	lengthOfAJumpInstruction = 9 // The length of a jump Op (jmp, jf, jt) and it's following 64-bit integer.
-)
diff --git a/pkg/lang/compiler/scope_chain.go b/pkg/lang/compiler/scope_chain.go
index 6b7e693..ad176da 100644
--- a/pkg/lang/compiler/scope_chain.go
+++ b/pkg/lang/compiler/scope_chain.go
@@ -1,102 +1,238 @@
 package compiler
 
+import (
+	"fmt"
+	"jinx/pkg/lang/vm/code"
+)
+
 type ScopeID int
 
 type ScopeChain struct {
-	scopes []Scope
+	nameToSymbol map[string]SymbolID
+	scopes       []Scope
 }
 
 func NewScopeChain() ScopeChain {
 	scopes := make([]Scope, 1)
-	scopes[0] = Scope{
-		kind:         ScopeKindGlobal,
-		nameToSymbol: make(map[string]int),
-		symbols:      make([]Symbol, 0),
-	}
+	scopes[0] = NewFunctionScope("") // Top-most scope is a function scope, so it can have sub-units
 
 	return ScopeChain{
-		scopes: scopes,
+		nameToSymbol: make(map[string]SymbolID),
+		scopes:       scopes,
 	}
 }
 
+func (sc *ScopeChain) CurrentScopeID() ScopeID {
+	return ScopeID(len(sc.scopes) - 1)
+}
+
 func (sc *ScopeChain) Current() *Scope {
-	return &sc.scopes[len(sc.scopes)-1]
+	return &sc.scopes[sc.CurrentScopeID()]
 }
 
-func (sc *ScopeChain) Enter(kind ScopeKind) {
-	sc.scopes = append(sc.scopes, Scope{
-		kind:         kind,
-		nameToSymbol: make(map[string]int),
-		symbols:      make([]Symbol, 0),
-	})
+func (sc *ScopeChain) CurrentFunction() *Scope {
+	// TODO: Probably should make this lookup constant by making a seperate array of function scopes
+	for i := len(sc.scopes) - 1; i <= 0; i++ {
+		if sc.scopes[i].kind == ScopeKindFunction {
+			return &sc.scopes[i]
+		}
+	}
+
+	panic("top scope should always be a function scope")
+}
+
+func (sc *ScopeChain) Enter() {
+	sc.scopes = append(sc.scopes, NewNormalScope())
+}
+
+func (sc *ScopeChain) EnterFunction(unitName string) {
+	sc.scopes = append(sc.scopes, NewFunctionScope(unitName))
 }
 
 func (sc *ScopeChain) Exit() {
-	sc.scopes[len(sc.scopes)-1] = Scope{}
-	sc.scopes = sc.scopes[:len(sc.scopes)-1]
+	if sc.CurrentScopeID() == 0 {
+		return
+	}
+
+	sc.scopes[sc.CurrentScopeID()] = Scope{}
+	sc.scopes = sc.scopes[:sc.CurrentScopeID()]
 }
 
 func (sc *ScopeChain) Declare(name string) (int, bool) {
 	// Check whether the symbol is already declared in any of the scopes.
-	for _, scope := range sc.scopes {
-		if _, ok := scope.nameToSymbol[name]; ok {
-			return 0, false
-		}
+	if _, ok := sc.nameToSymbol[name]; ok {
+		return 0, false
 	}
 
 	current := sc.Current()
-	index := len(current.symbols)
+	indexInScope := len(current.variableSymbols)
+
+	symbolID := SymbolID{
+		symbolKind:   SymbolKindVariable,
+		scopeID:      sc.CurrentScopeID(),
+		indexInScope: indexInScope,
+	}
 
 	// Declare the symbol in the current scope.
-	current.symbols = append(current.symbols, Symbol{
-		kind:       SymbolKindVariable,
-		name:       name,
-		localIndex: index,
+	current.variableSymbols = append(current.variableSymbols, Symbol[SymbolVariable]{
+		name: name,
+		data: SymbolVariable{
+			localIndex: indexInScope,
+		},
+	})
+
+	sc.nameToSymbol[name] = symbolID
+
+	return indexInScope, true
+}
+
+func (sc *ScopeChain) DeclareFunction(name string) (code.Marker, bool) {
+	if _, ok := sc.nameToSymbol[name]; ok {
+		return "", false
+	}
+
+	current := sc.Current()
+	index := len(current.functionSymbols)
+
+	symbolID := SymbolID{
+		symbolKind:   SymbolKindFunction,
+		scopeID:      sc.CurrentScopeID(),
+		indexInScope: index,
+	}
+
+	unitName := sc.CreateFunctionSubUnit(name)
+
+	current.functionSymbols = append(current.functionSymbols, Symbol[SymbolFunction]{
+		name: name,
+		data: SymbolFunction{
+			marker: unitName,
+		},
 	})
 
-	current.nameToSymbol[name] = index
+	sc.nameToSymbol[name] = symbolID
 
-	return index, true
+	return unitName, true
 }
 
 func (sc *ScopeChain) DeclareAnonymous() int {
 	current := sc.Current()
-	index := len(current.symbols)
+	index := len(current.variableSymbols)
 
-	// Declare the symbol in the current scope.
-	current.symbols = append(current.symbols, Symbol{
-		kind:       SymbolKindVariable,
-		name:       "",
-		localIndex: index,
+	current.variableSymbols = append(current.variableSymbols, Symbol[SymbolVariable]{
+		name: "", // An anonymous symbol has no name.
+		data: SymbolVariable{
+			localIndex: index,
+		},
 	})
 
 	return index
 }
 
 func (sc *ScopeChain) DeclareTemporary() int {
-	return len(sc.Current().symbols)
+	return len(sc.Current().variableSymbols) // :)
 }
 
-func (sc *ScopeChain) Lookup(name string) (Symbol, bool) {
-	for i := len(sc.scopes) - 1; i >= 0; i-- {
-		if symbol, ok := sc.scopes[i].nameToSymbol[name]; ok {
-			return sc.scopes[i].symbols[symbol], true
-		}
+func (sc *ScopeChain) CreateAnonymousFunctionSubUnit() code.Marker {
+	fnScope := sc.CurrentFunction()
+	data := fnScope.data.(ScopeFunction)
+
+	index := data.subUnitCount
+	data.subUnitCount++
+
+	fnScope.data = data
+
+	return sc.CreateFunctionSubUnit(fmt.Sprintf("anon_%d", index))
+}
+
+func (sc *ScopeChain) CreateFunctionSubUnit(subUnitName string) code.Marker {
+	fnScope := sc.CurrentFunction()
+	data := fnScope.data.(ScopeFunction)
+
+	name := data.unitName
+	if name == "" {
+		name = code.Marker(subUnitName)
+	} else {
+		name = name.SubUnit(subUnitName)
+	}
+
+	return name
+}
+
+func (sc *ScopeChain) Lookup(name string) (SymbolID, bool) {
+	if id, ok := sc.nameToSymbol[name]; ok {
+		return id, true
+	}
+
+	return SymbolID{}, false
+}
+
+func (sc *ScopeChain) GetVariable(id SymbolID) Symbol[SymbolVariable] {
+	if id.symbolKind != SymbolKindVariable {
+		panic("incorrect symbol id kind given")
+	}
+
+	return sc.scopes[id.scopeID].variableSymbols[id.indexInScope]
+}
+
+func (sc *ScopeChain) GetFunction(id SymbolID) Symbol[SymbolVariable] {
+	if id.symbolKind != SymbolKindVariable {
+		panic("incorrect symbol id kind given")
 	}
 
-	return Symbol{}, false
+	return sc.scopes[id.scopeID].variableSymbols[id.indexInScope]
+}
+
+type SymbolID struct {
+	symbolKind   SymbolKind
+	scopeID      ScopeID
+	indexInScope int
 }
 
 type ScopeKind int
 
 const (
-	ScopeKindGlobal ScopeKind = iota
+	ScopeKindNormal ScopeKind = iota
 	ScopeKindFunction
-	ScopeKindBlock
+	ScopeKindLoop
 )
 
 type Scope struct {
-	kind         ScopeKind
-	nameToSymbol map[string]int
-	symbols      []Symbol
+	variableSymbols []Symbol[SymbolVariable]
+	functionSymbols []Symbol[SymbolFunction]
+
+	kind ScopeKind
+	data any
+}
+
+func NewNormalScope() Scope {
+	return Scope{
+		variableSymbols: make([]Symbol[SymbolVariable], 0),
+		functionSymbols: make([]Symbol[SymbolFunction], 0),
+		kind:            ScopeKindNormal,
+		data:            ScopeNormal{},
+	}
+}
+
+func NewFunctionScope(unitName string) Scope {
+	return Scope{
+		variableSymbols: make([]Symbol[SymbolVariable], 0),
+		functionSymbols: make([]Symbol[SymbolFunction], 0),
+		kind:            ScopeKindFunction,
+		data: ScopeFunction{
+			unitName:     code.Marker(unitName),
+			subUnitCount: 0,
+		},
+	}
+}
+
+type ScopeNormal struct{}
+
+type ScopeFunction struct {
+	unitName     code.Marker
+	subUnitCount int
+}
+
+type ScopeLoop struct {
+	breakMarker    code.Marker
+	continueMarker code.Marker
 }
diff --git a/pkg/lang/compiler/symbol.go b/pkg/lang/compiler/symbol.go
index 03838da..d22cdc0 100644
--- a/pkg/lang/compiler/symbol.go
+++ b/pkg/lang/compiler/symbol.go
@@ -1,13 +1,38 @@
 package compiler
 
+import "jinx/pkg/lang/vm/code"
+
 type SymbolKind int
 
 const (
 	SymbolKindVariable SymbolKind = iota
+	SymbolKindFunction
 )
 
-type Symbol struct {
-	kind       SymbolKind
-	name       string
+func (s SymbolKind) String() string {
+	switch s {
+	case SymbolKindVariable:
+		return "variable"
+	case SymbolKindFunction:
+		return "function"
+	default:
+		panic("unknown symbol kind")
+	}
+}
+
+type Symbol[D SymbolData] struct {
+	name string
+	data D
+}
+
+type SymbolData interface {
+	SymbolVariable | SymbolFunction
+}
+
+type SymbolVariable struct {
 	localIndex int
 }
+
+type SymbolFunction struct {
+	marker code.Marker
+}
diff --git a/pkg/lang/vm/code/builder.go b/pkg/lang/vm/code/builder.go
index f027c49..adb2eed 100644
--- a/pkg/lang/vm/code/builder.go
+++ b/pkg/lang/vm/code/builder.go
@@ -2,14 +2,15 @@ package code
 
 import (
 	"encoding/binary"
-	"fmt"
 	"math"
 )
 
 type Builder struct {
-	code           []byte
-	debugInfo      DebugInfo
-	relativePcRefs map[int][]int
+	code      []byte
+	debugInfo DebugInfo
+
+	markers    map[Marker]int
+	markerRefs map[int]Marker
 
 	currentLine int
 	lineStart   int
@@ -17,11 +18,14 @@ type Builder struct {
 
 func NewBuilder() Builder {
 	return Builder{
-		code:           make([]byte, 0, 64),
-		debugInfo:      NewDebugInfo("unknown file"),
-		relativePcRefs: make(map[int][]int),
-		lineStart:      -1,
-		currentLine:    -1,
+		code:      make([]byte, 0, 64),
+		debugInfo: NewDebugInfo("unknown file"),
+
+		markers:    make(map[Marker]int, 0),
+		markerRefs: make(map[int]Marker, 0),
+
+		lineStart:   -1,
+		currentLine: -1,
 	}
 }
 
@@ -43,50 +47,45 @@ func (b *Builder) AppendString(s string) {
 	b.code = append(b.code, 0)
 }
 
-func (b *Builder) AppendReferenceToPc(pc int64) {
-	b.addPcRef(int(pc), b.Len())
-	b.AppendInt(pc)
+const UnfilledMarkerReference = 0x11_5AB07A6ED_600D5
+
+func (b *Builder) AppendMarkerReference(marker Marker) {
+	b.markerRefs[b.Len()] = marker
+	b.AppendInt(UnfilledMarkerReference)
 }
 
 func (b *Builder) AppendRaw(raw Raw) {
 	b.code = append(b.code, raw...)
 }
 
-func (b *Builder) AppendBuilder(other Builder) {
-	code := other.code
-	for pc, refsToPc := range other.relativePcRefs {
-		newPc := b.Len() + pc
-		for _, ref := range refsToPc {
-			valueAtRef := binary.LittleEndian.Uint64(code[ref : ref+8])
-			if int(valueAtRef) != pc {
-				panic(fmt.Errorf("reference to pc in builder does not actually reference pc. (pc: %d, value at reference: %d)", pc, valueAtRef))
+func (b *Builder) AppendBuilder(other Builder) error {
+	for marker, pc := range other.markers {
+		if parentPc, ok := b.markers[marker]; ok {
+			return ErrBuilderAppendMarkerDefinitionClash{
+				Marker:   marker,
+				ParentPc: parentPc,
+				ChildPc:  pc,
 			}
-
-			binary.LittleEndian.PutUint64(code[ref:], uint64(newPc))
-			b.addPcRef(newPc, ref+b.Len())
 		}
-	}
-
-	b.code = append(b.code, code...)
 
-	if other.debugInfo.pcToLine.Len() != 0 || other.currentLine != -1 || other.lineStart != -1 {
-		panic("appending debug infos not implemented yet")
+		b.SetMarker(marker, pc)
 	}
-}
 
-func (b *Builder) AppendBuilderWithoutAdjustingReferences(other Builder) {
-	code := other.code
-	for pc, refsToPc := range other.relativePcRefs {
-		for _, ref := range refsToPc {
-			b.addPcRef(pc, ref+b.Len())
-		}
+	for pc, marker := range other.markerRefs {
+		b.markerRefs[b.Len()+pc] = marker
 	}
 
-	b.code = append(b.code, code...)
+	b.AppendRaw(other.code)
 
-	if other.debugInfo.pcToLine.Len() != 0 || other.currentLine != -1 || other.lineStart != -1 {
-		panic("appending debug infos not implemented yet")
-	}
+	return nil
+}
+
+func (b *Builder) PutMarker(marker Marker) {
+	b.SetMarker(marker, b.Len())
+}
+
+func (b *Builder) SetMarker(marker Marker, pc int) {
+	b.markers[marker] = pc
 }
 
 func (b *Builder) StartLine(line int) {
@@ -127,18 +126,27 @@ func (b *Builder) Len() int {
 	return len(b.code)
 }
 
-func (b *Builder) Build() Code {
-	return New(b.code, b.debugInfo)
-}
+func (b *Builder) Build() (Code, error) {
+	for pc, marker := range b.markerRefs {
+		markerPc, ok := b.markers[marker]
+		if !ok {
+			return Code{}, ErrBuilderUnfulfilledMarker{
+				Marker: marker,
+				Pc:     pc,
+			}
+		}
+
+		valueAtRefPc := binary.LittleEndian.Uint64(b.code[pc : pc+8])
+		if valueAtRefPc != UnfilledMarkerReference {
+			return Code{}, ErrBuilderOverwrittenMarkerReference{
+				Marker:          marker,
+				Pc:              pc,
+				UnexpectedValue: valueAtRefPc,
+			}
+		}
 
-func (b *Builder) addPcRef(pc int, at int) {
-	refs, ok := b.relativePcRefs[pc]
-	if ok {
-		refs = append(refs, at)
-	} else {
-		refs = make([]int, 1)
-		refs[0] = at
+		b.SetInt(pc, int64(markerPc))
 	}
 
-	b.relativePcRefs[pc] = refs
+	return New(b.code, b.debugInfo), nil
 }
diff --git a/pkg/lang/vm/code/builder_test.go b/pkg/lang/vm/code/builder_test.go
new file mode 100644
index 0000000..29f1974
--- /dev/null
+++ b/pkg/lang/vm/code/builder_test.go
@@ -0,0 +1,31 @@
+package code_test
+
+import (
+	"jinx/pkg/lang/vm/code"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMarker(t *testing.T) {
+	builder := code.NewBuilder()
+
+	builder.AppendInt(123)
+	builder.AppendMarkerReference("hello world")
+
+	builder.PutMarker("hello world")
+	builder.AppendInt(123)
+
+	result, err := builder.Build()
+	assert.NoError(t, err)
+
+	expectedBuilder := code.NewBuilder()
+	expectedBuilder.AppendInt(123)
+	expectedBuilder.AppendInt(16)
+	expectedBuilder.AppendInt(123)
+
+	expected, err := expectedBuilder.Build()
+	assert.NoError(t, err)
+
+	assert.Equal(t, expected.Code(), result.Code())
+}
diff --git a/pkg/lang/vm/code/errors.go b/pkg/lang/vm/code/errors.go
new file mode 100644
index 0000000..3261dd9
--- /dev/null
+++ b/pkg/lang/vm/code/errors.go
@@ -0,0 +1,32 @@
+package code
+
+import "fmt"
+
+type ErrBuilderAppendMarkerDefinitionClash struct {
+	Marker   Marker
+	ParentPc int
+	ChildPc  int
+}
+
+func (e ErrBuilderAppendMarkerDefinitionClash) Error() string {
+	return fmt.Sprintf("parent and child builder definitions for marker '%v' clash (parent: %d, child: %d)", e.Marker, e.ParentPc, e.ChildPc)
+}
+
+type ErrBuilderUnfulfilledMarker struct {
+	Marker Marker
+	Pc     int
+}
+
+func (e ErrBuilderUnfulfilledMarker) Error() string {
+	return fmt.Sprintf("encountered unfulfilled marker '%v' at pc %d", e.Marker, e.Pc)
+}
+
+type ErrBuilderOverwrittenMarkerReference struct {
+	Marker          Marker
+	Pc              int
+	UnexpectedValue uint64
+}
+
+func (e ErrBuilderOverwrittenMarkerReference) Error() string {
+	return fmt.Sprintf("marker reference for marker '%v' at pc %d was overwritten (value: %x)", e.Marker, e.Pc, e.UnexpectedValue)
+}
diff --git a/pkg/lang/vm/code/marker.go b/pkg/lang/vm/code/marker.go
new file mode 100644
index 0000000..18b4157
--- /dev/null
+++ b/pkg/lang/vm/code/marker.go
@@ -0,0 +1,23 @@
+package code
+
+import "fmt"
+
+type Marker string
+
+func (m Marker) SubUnit(name string) Marker {
+	return Marker(fmt.Sprintf("%v:%s", m, name))
+}
+
+func (m Marker) SubMarker(format string, args ...any) Marker {
+	part := fmt.Sprintf(format, args...)
+
+	return Marker(fmt.Sprintf("%v_%s", m, part))
+}
+
+func (m Marker) String() string {
+	return string(m)
+}
+
+func (m Marker) IsEmpty() bool {
+	return string(m) == ""
+}
diff --git a/pkg/lang/vm/text/compiler.go b/pkg/lang/vm/text/compiler.go
index 21d8dae..d8b53fd 100644
--- a/pkg/lang/vm/text/compiler.go
+++ b/pkg/lang/vm/text/compiler.go
@@ -56,7 +56,7 @@ func (cpl *Compiler) Compile() (code.Code, error) {
 		return code.Code{}, err
 	}
 
-	return builder.Build(), nil
+	return builder.Build()
 }
 
 func (cpl *Compiler) compileLine() (code.Raw, error) {