From 7e05785c091d4e8d4f7cab2e3c4552a2d3cecc21 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 00:46:30 -0400 Subject: [PATCH 01/10] Added switch statements to spec --- design/spec.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/design/spec.md b/design/spec.md index 7f6c06a..a357fe4 100644 --- a/design/spec.md +++ b/design/spec.md @@ -266,8 +266,15 @@ value. Each case takes the form of a declaration, and an associated expression. If the type of the union matches the type of the declaration in the case, the expression is executed and the value of the union is made available to it through the declaration. If the value of the match expression is used, all -possible types in the union must be accounted for. It may be assigned to any -type that satisfies the assignment rules of its first case. +possible types in the union must be accounted for, or it must have a default +case. It may be assigned to any type that satisfies the assignment rules of its +first case. +### Switch +Switch is a control flow branching expression that executes one of several case +expressions depending on the value of the input. It accepts any pointer or +integer type. If the value of the switch expression is used, a default case must +be present. It may be assigned to any type that satisfies the assignment rules +of its first case. ### Loop Loop is a control flow expression that repeats an expression until a break statement is called from within it. The break statement must be given a value @@ -366,7 +373,8 @@ this without hand-writing a parser. -> "if" "then" ["else" ] - -> "match" * + -> "match" * [] + -> "switch" * [] -> "loop" -> "for" [] in -> "[" "break" [] "]" @@ -384,7 +392,9 @@ this without hand-writing a parser. -> "true" | "false" -> ":" - -> "|" + -> "|" + -> "|" + -> "*" -> "[" * "]" [":" ] -> /[a-z][A-Za-z]*/ -> /[A-Z][A-Za-z]*/ From 1f04c3a593bca7a2c2f4f93d972180110f4f2287 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 01:16:41 -0400 Subject: [PATCH 02/10] Added switch statements, default cases to entity --- entity/expression.go | 47 ++++++++++++++++++++++++++++++++++++++++++-- entity/misc.go | 31 +++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/entity/expression.go b/entity/expression.go index 4860e60..226efac 100644 --- a/entity/expression.go +++ b/entity/expression.go @@ -414,12 +414,13 @@ type Match struct { // Syntax Pos errors.Position Value Expression - Cases []*Case + Cases []*MatchCase + Default *DefaultCase // Semantics Ty Type CaseOrder []Hash - CaseMap map[Hash] *Case + CaseMap map[Hash] *MatchCase } func (*Match) expression(){} func (this *Match) Position () errors.Position { return this.Pos } @@ -436,6 +437,48 @@ func (this *Match) String () string { for _, cas := range this.Cases { out += fmt.Sprint(" ", cas) } + if this.Default != nil { + out += fmt.Sprint(" ", this.Default) + } + return out +} + +var _ Expression = &Switch { } +// Switch is a control flow branching expression that executes one of several +// case expressions depending on the value of the input. It accepts any pointer +// or integer type. If the value of the switch expression is used, a default +// case must be present. It may be assigned to any type that satisfies the +// assignment rules of its first case. +type Switch struct { + // Syntax + Pos errors.Position + Value Expression + Cases []*SwitchCase + Default *DefaultCase + + // Semantics + Ty Type + CaseOrder []int64 + CaseMap map[uint64] *MatchCase +} +func (*Switch) expression(){} +func (this *Switch) Position () errors.Position { return this.Pos } +func (this *Switch) Type () Type { return this.Ty } +func (this *Switch) HasExplicitType () bool { + if len(this.Cases) == 0 { + return true + } else { + return this.Cases[0].HasExplicitType() + } +} +func (this *Switch) String () string { + out := fmt.Sprint("switch ", this.Value) + for _, cas := range this.Cases { + out += fmt.Sprint(" ", cas) + } + if this.Default != nil { + out += fmt.Sprint(" ", this.Default) + } return out } diff --git a/entity/misc.go b/entity/misc.go index ba24552..8c0c74c 100644 --- a/entity/misc.go +++ b/entity/misc.go @@ -101,18 +101,41 @@ func (this *Member) String () string { return fmt.Sprint(this.Name, ":", this.Value) } -// Case represents a match case. -type Case struct { +// MatchCase represents a case in a match expression. +type MatchCase struct { Pos errors.Position Declaration *Declaration Expression Scope } -func (this *Case) Position () errors.Position { return this.Pos } -func (this *Case) String () string { +func (this *MatchCase) Position () errors.Position { return this.Pos } +func (this *MatchCase) String () string { return fmt.Sprint("| ", this.Declaration, this.Expression) } +// SwitchCase represents a case in a switch expression. +type SwitchCase struct { + Pos errors.Position + Key Expression + Expression + Scope +} +func (this *SwitchCase) Position () errors.Position { return this.Pos } +func (this *SwitchCase) String () string { + return fmt.Sprint("| ", this.Key, this.Expression) +} + +// DefaultCase represents the default case in a switch or match expression. +type DefaultCase struct { + Pos errors.Position + Expression + Scope +} +func (this *DefaultCase) Position () errors.Position { return this.Pos } +func (this *DefaultCase) String () string { + return fmt.Sprint("* ", this.Expression) +} + // Operator determines which operation to apply to a value in an operation // expression. type Operator int; const ( From e27fb9e79b5a4eadc7d25571910e00616560c0ed Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 01:53:40 -0400 Subject: [PATCH 03/10] Parse default match cases --- parser/fspl/README.md | 1 + parser/fspl/expression.go | 26 +++++++++++++++++++++++--- parser/fspl/parser_test.go | 3 ++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/parser/fspl/README.md b/parser/fspl/README.md index 5ea9097..277e88e 100644 --- a/parser/fspl/README.md +++ b/parser/fspl/README.md @@ -70,6 +70,7 @@ expression, the parser follows this decision tree to determine what to parse: | | 'nil' =LiteralNil | | 'if' =IfElse | | 'match' =Match +| | 'switch' =Switch | | 'loop' =Loop | | +Colon =Declaration | | +DoubleColon =Call diff --git a/parser/fspl/expression.go b/parser/fspl/expression.go index decc3fa..3377030 100644 --- a/parser/fspl/expression.go +++ b/parser/fspl/expression.go @@ -580,18 +580,24 @@ func (this *treeParser) parseMatch () (*entity.Match, error) { if err != nil { return nil, err } for this.Is(lexer.Symbol) && this.ValueIs("|") { - cas, err := this.parseCase() + cas, err := this.parseMatchCase() if err != nil { return nil, err } match.Cases = append(match.Cases, cas) } + if this.Is(lexer.Star) { + defaul, err := this.parseDefaultCase() + if err != nil { return nil, err } + match.Default = defaul + } + return match, nil } -func (this *treeParser) parseCase () (*entity.Case, error) { +func (this *treeParser) parseMatchCase () (*entity.MatchCase, error) { err := this.ExpectValue(lexer.Symbol, "|") if err != nil { return nil, err } - cas := &entity.Case { + cas := &entity.MatchCase { Pos: this.Pos(), } @@ -604,6 +610,20 @@ func (this *treeParser) parseCase () (*entity.Case, error) { return cas, nil } +func (this *treeParser) parseDefaultCase () (*entity.DefaultCase, error) { + err := this.Expect(lexer.Star) + if err != nil { return nil, err } + cas := &entity.DefaultCase { + Pos: this.Pos(), + } + + this.Next() + cas.Expression, err = this.parseExpression() + if err != nil { return nil, err } + + return cas, nil +} + func (this *treeParser) parseLoop () (*entity.Loop, error) { err := this.ExpectValue(lexer.Ident, "loop") if err != nil { return nil, err } diff --git a/parser/fspl/parser_test.go b/parser/fspl/parser_test.go index ab71660..406ee93 100644 --- a/parser/fspl/parser_test.go +++ b/parser/fspl/parser_test.go @@ -74,7 +74,7 @@ testString (test, - [loop]:Int = {i:Int=0 if [< i 3] then return 1 loop {[print 3] if [> i 3] then break 0 i=[++ i]}} - [for s:String] = for i:Index e:Byte in str if [> i 3] then [break e] - [for s:String] = for e:Byte in s [break e] -- [matchToInt u:(| Int F64)]:Int = match u | u:Int u | u:F64 [~ Int u]`, +- [matchToInt u:(| Int F64)]:Int = match u | u:Int u | u:F64 [~ Int u] * 0`, // input ` [var] = sdfdf @@ -127,6 +127,7 @@ testString (test, [matchToInt u:(| Int F64)]:Int = match u | u:Int u | u:F64 [~Int u] + * 0 `) } From d8f82d3646d4a390bba232880e600050acc4c358 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 02:18:18 -0400 Subject: [PATCH 04/10] Parse switch expressions --- parser/fspl/expression.go | 44 ++++++++++++++++++++++++++++++++++++++ parser/fspl/parser_test.go | 8 ++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/parser/fspl/expression.go b/parser/fspl/expression.go index 3377030..1299fe8 100644 --- a/parser/fspl/expression.go +++ b/parser/fspl/expression.go @@ -117,6 +117,7 @@ func (this *treeParser) parseExpressionRootIdent () (entity.Expression, error) { case "nil": return this.parseLiteralNil() case "if": return this.parseIfElse() case "match": return this.parseMatch() + case "switch": return this.parseSwitch() case "loop": return this.parseLoop() case "for": return this.parseFor() default: @@ -610,6 +611,49 @@ func (this *treeParser) parseMatchCase () (*entity.MatchCase, error) { return cas, nil } +func (this *treeParser) parseSwitch () (*entity.Switch, error) { + err := this.ExpectValue(lexer.Ident, "switch") + if err != nil { return nil, err } + switc := &entity.Switch { + Pos: this.Pos(), + } + + err = this.ExpectNextDesc(descriptionExpression, startTokensExpression...) + if err != nil { return nil, err } + switc.Value, err = this.parseExpression() + if err != nil { return nil, err } + + for this.Is(lexer.Symbol) && this.ValueIs("|") { + cas, err := this.parseSwitchCase() + if err != nil { return nil, err } + switc.Cases = append(switc.Cases, cas) + } + + if this.Is(lexer.Star) { + defaul, err := this.parseDefaultCase() + if err != nil { return nil, err } + switc.Default = defaul + } + + return switc, nil +} + +func (this *treeParser) parseSwitchCase () (*entity.SwitchCase, error) { + err := this.ExpectValue(lexer.Symbol, "|") + if err != nil { return nil, err } + cas := &entity.SwitchCase { + Pos: this.Pos(), + } + + this.Next() + cas.Key, err = this.parseExpression() + if err != nil { return nil, err } + cas.Expression, err = this.parseExpression() + if err != nil { return nil, err } + + return cas, nil +} + func (this *treeParser) parseDefaultCase () (*entity.DefaultCase, error) { err := this.Expect(lexer.Star) if err != nil { return nil, err } diff --git a/parser/fspl/parser_test.go b/parser/fspl/parser_test.go index 406ee93..3ceb0c0 100644 --- a/parser/fspl/parser_test.go +++ b/parser/fspl/parser_test.go @@ -74,7 +74,8 @@ testString (test, - [loop]:Int = {i:Int=0 if [< i 3] then return 1 loop {[print 3] if [> i 3] then break 0 i=[++ i]}} - [for s:String] = for i:Index e:Byte in str if [> i 3] then [break e] - [for s:String] = for e:Byte in s [break e] -- [matchToInt u:(| Int F64)]:Int = match u | u:Int u | u:F64 [~ Int u] * 0`, +- [matchToInt u:(| Int F64)]:Int = match u | u:Int u | u:F64 [~ Int u] * 0 +- [switch x:Int] = switch x | 0 5 | 1 4 | 2 3 * 0`, // input ` [var] = sdfdf @@ -128,6 +129,11 @@ testString (test, | u:Int u | u:F64 [~Int u] * 0 +[switch x:Int] = switch x + | 0 5 + | 1 4 + | 2 3 + * 0 `) } From 6916b3b7b1a4e02a8c775f23a65343baa99bee78 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 11:29:24 -0400 Subject: [PATCH 05/10] Analyze match expression default cases --- analyzer/control-flow_test.go | 10 ++++++++++ analyzer/expression.go | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/analyzer/control-flow_test.go b/analyzer/control-flow_test.go index dd9ff34..d2437a2 100644 --- a/analyzer/control-flow_test.go +++ b/analyzer/control-flow_test.go @@ -33,6 +33,16 @@ U: (| Int F64) `) } +func TestMatchDefault (test *testing.T) { +testString (test, +` +U: (| Int F64) +[isInt u:U]:Bool = match u + | u:Int [return true] + * false +`) +} + func TestMatchUnionUnderComplete (test *testing.T) { testString (test, ` diff --git a/analyzer/expression.go b/analyzer/expression.go index 8d87007..a617053 100644 --- a/analyzer/expression.go +++ b/analyzer/expression.go @@ -759,7 +759,21 @@ func (this *Tree) analyzeMatch ( match.CaseMap[hash] = cas } - if into != nil && len(match.Cases) < len(rawUnion.Allowed) { + if match.Default != nil { + this.pushScope(match.Default) + expression, err := this.analyzeExpression ( + into, mode, + match.Default.Expression) + this.popScope() + + if err != nil { return nil, err } + match.Default.Expression = expression + } + + allCasesCovered := + len(match.Cases) == len(rawUnion.Allowed) || + match.Default != nil + if into != nil && !allCasesCovered { return nil, errors.Errorf ( match.Position(), "match does not cover all types within %v", @@ -772,7 +786,7 @@ func (this *Tree) analyzeMatch ( } func (this *Tree) assembleMatchMap (match *entity.Match) (*entity.Match, error) { - match.CaseMap = make(map[entity.Hash] *entity.Case) + match.CaseMap = make(map[entity.Hash] *entity.MatchCase) match.CaseOrder = make([]entity.Hash, len(match.Cases)) for index, cas := range match.Cases { hash := cas.Declaration.Ty.Hash() From a50a5febb9741033fbc74f40d67dbb62ac7bfff7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 12:08:28 -0400 Subject: [PATCH 06/10] Untested analysis of switch statements --- analyzer/assignment.go | 17 +++++++ analyzer/expression-multiplex.go | 2 + analyzer/expression.go | 81 ++++++++++++++++++++++++++++++++ entity/expression.go | 2 +- 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/analyzer/assignment.go b/analyzer/assignment.go index 2c18123..34ceba0 100644 --- a/analyzer/assignment.go +++ b/analyzer/assignment.go @@ -483,6 +483,14 @@ func isInteger (ty entity.Type) bool { } } +// isPointer returns whether or not the specified type is a pointer. +func isPointer (ty entity.Type) bool { + switch ReduceToBase(ty).(type) { + case *entity.TypePointer: return true + default: return false + } +} + // isOrdered returns whether or not the values of the specified type can be // ordered. func isOrdered (ty entity.Type) bool { @@ -515,6 +523,15 @@ func IsUnsigned (ty entity.Type) bool { } } + +// integerWidth returns the width of the type, if it is an integer. +func integerWidth (ty entity.Type) (int, bool) { + switch ty := ReduceToBase(ty).(type) { + case *entity.TypeInt: return ty.Width, true + default: return 0, false + } +} + // inRange returns whether the specified value can fit within the given integer // type. func inRange (ty entity.Type, value int64) bool { diff --git a/analyzer/expression-multiplex.go b/analyzer/expression-multiplex.go index c5ef8be..c346f28 100644 --- a/analyzer/expression-multiplex.go +++ b/analyzer/expression-multiplex.go @@ -46,6 +46,8 @@ func (this *Tree) analyzeExpression ( return this.analyzeIfElse(into, mode, expression) case *entity.Match: return this.analyzeMatch(into, mode, expression) + case *entity.Switch: + return this.analyzeSwitch(into, mode, expression) case *entity.Loop: return this.analyzeLoop(into, mode, expression) case *entity.For: diff --git a/analyzer/expression.go b/analyzer/expression.go index a617053..fb671e2 100644 --- a/analyzer/expression.go +++ b/analyzer/expression.go @@ -803,6 +803,87 @@ func (this *Tree) assembleMatchMap (match *entity.Match) (*entity.Match, error) return match, nil } +func (this *Tree) analyzeSwitch ( + into entity.Type, + mode strictness, + switc *entity.Switch, +) ( + entity.Expression, + error, +) { + value, err := this.analyzeExpression(nil, strict, switc.Value) + if err != nil { return nil, err } + switc.Value = value + width, integerOk := integerWidth(value.Type()) + if !integerOk { + return nil, errors.Errorf ( + value.Position(), "cannot switch on type %v", + value.Type()) + } + + switc.CaseOrder = make([]int64, len(switc.Cases)) + switc.CaseMap = make(map[int64] *entity.SwitchCase) + for index, cas := range switc.Cases { + + this.pushScope(cas) + key, err := this.analyzeExpression ( + value.Type(), strict, + cas.Key) + if err != nil { this.popScope(); return nil, err } + cas.Key = key + expression, err := this.analyzeExpression ( + into, mode, + cas.Expression) + if err != nil { this.popScope(); return nil, err } + cas.Expression = expression + this.popScope() + + var keyInt int64 + switch key := key.(type) { + case *entity.LiteralInt: + keyInt = int64(key.Value) + case *entity.LiteralString: + switch { + case width >= 32: + keyInt = int64(key.ValueUTF32[0]) + case width >= 16: + keyInt = int64(key.ValueUTF16[0]) + default: + keyInt = int64(key.ValueUTF8[0]) + } + default: + return nil, errors.Errorf ( + key.Position(), "%v cannot represent a constant integer", + key) + } + + switc.Cases[index] = cas + switc.CaseMap[keyInt] = cas + switc.CaseOrder[index] = keyInt + } + + if switc.Default != nil { + this.pushScope(switc.Default) + expression, err := this.analyzeExpression ( + into, mode, + switc.Default.Expression) + this.popScope() + + if err != nil { return nil, err } + switc.Default.Expression = expression + } + + if into != nil && switc.Default != nil { + return nil, errors.Errorf ( + switc.Position(), + "switch must have a default case") + } + + switc.Ty = into + + return switc, nil +} + func (this *Tree) analyzeLoop ( into entity.Type, mode strictness, diff --git a/entity/expression.go b/entity/expression.go index 226efac..a5fe8e3 100644 --- a/entity/expression.go +++ b/entity/expression.go @@ -459,7 +459,7 @@ type Switch struct { // Semantics Ty Type CaseOrder []int64 - CaseMap map[uint64] *MatchCase + CaseMap map[int64] *SwitchCase } func (*Switch) expression(){} func (this *Switch) Position () errors.Position { return this.Pos } From f3bdfef5c588d5139104c942abaf21a374c92e3c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 19:49:48 -0400 Subject: [PATCH 07/10] Add analyzer tests to switch statements --- analyzer/control-flow_test.go | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/analyzer/control-flow_test.go b/analyzer/control-flow_test.go index d2437a2..02e9da7 100644 --- a/analyzer/control-flow_test.go +++ b/analyzer/control-flow_test.go @@ -100,6 +100,78 @@ U: (| Int F64 UInt) `) } +func TestSwitch (test *testing.T) { +testString (test, +` +[switch x:Int]:Int = switch x + | 0 5 + | 1 4 + | 2 3 + * 0 +`) +} + +func TestSwitchReturn (test *testing.T) { +testString (test, +` +[is5 x:Int]:Bool = { + switch x | 5 [return true] + false +} +`) +} + +func TestSwitchReturnValueUsed (test *testing.T) { +testString (test, +` +[is5 x:Int]:Bool = switch x + | 5 [return true] + * false +`) +} + +func TestSwitchErrBadLiteral (test *testing.T) { +testStringErr (test, +"cannot use array literal as Int", 4, 4, +` +[switch x:Int]:Int = switch x + | 0 5 + | (1) 4 + | 2 3 + * 0 +`) +} + +func TestSwitchErrNotConstant (test *testing.T) { +testStringErr (test, +"y cannot represent a constant integer", 7, 4, +` +[switch x:Int]:Int = { + y:Int = 1 + + switch x + | 0 5 + | y 4 + | 2 3 + * 0 +} +`) +} + +func TestSwitchErrDuplicate (test *testing.T) { +testStringErr (test, +"65 already listed in match at stream0.fspl:6:2", 7, 4, +` +[switch x:I8]:Int = switch x + | 0 5 + | 1 4 + | 2 3 + | 65 2 + | 'A' 1 + * 0 +`) +} + func TestIfElseReturnValueUsed (test *testing.T) { testString (test, ` From 4931f97496b1e2b2fcc65652f11499a98c158d61 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 19:50:54 -0400 Subject: [PATCH 08/10] Switch statement analysis passes tests --- analyzer/assignment.go | 10 +++++++++- analyzer/expression.go | 27 ++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/analyzer/assignment.go b/analyzer/assignment.go index 34ceba0..94fda08 100644 --- a/analyzer/assignment.go +++ b/analyzer/assignment.go @@ -475,7 +475,7 @@ func isNumeric (ty entity.Type) bool { } } -// isInteger returns whether or not the specified type is an integer. +// isInteger returns whether or not the specified type is an integer or word. func isInteger (ty entity.Type) bool { switch ReduceToBase(ty).(type) { case *entity.TypeInt, *entity.TypeWord: return true @@ -483,6 +483,14 @@ func isInteger (ty entity.Type) bool { } } +// isWord returns whether or not the specified type is a word. +func isWord (ty entity.Type) bool { + switch ReduceToBase(ty).(type) { + case *entity.TypeWord: return true + default: return false + } +} + // isPointer returns whether or not the specified type is a pointer. func isPointer (ty entity.Type) bool { switch ReduceToBase(ty).(type) { diff --git a/analyzer/expression.go b/analyzer/expression.go index fb671e2..88433e5 100644 --- a/analyzer/expression.go +++ b/analyzer/expression.go @@ -815,7 +815,8 @@ func (this *Tree) analyzeSwitch ( if err != nil { return nil, err } switc.Value = value width, integerOk := integerWidth(value.Type()) - if !integerOk { + wordOk := isWord(value.Type()) + if !integerOk && !wordOk { return nil, errors.Errorf ( value.Position(), "cannot switch on type %v", value.Type()) @@ -843,6 +844,14 @@ func (this *Tree) analyzeSwitch ( case *entity.LiteralInt: keyInt = int64(key.Value) case *entity.LiteralString: + if !integerOk { + return nil, errors.Errorf ( + key.Position(), + "cannot use string literal when " + + "switching on %v", + value.Type()) + } + switch { case width >= 32: keyInt = int64(key.ValueUTF32[0]) @@ -852,9 +861,17 @@ func (this *Tree) analyzeSwitch ( keyInt = int64(key.ValueUTF8[0]) } default: - return nil, errors.Errorf ( - key.Position(), "%v cannot represent a constant integer", - key) + return nil, errors.Errorf ( + key.Position(), + "%v cannot represent a constant integer", + key) + } + + if previous, exists := switc.CaseMap[keyInt]; exists { + return nil, errors.Errorf ( + cas.Key.Position(), + "%v already listed in match at %v", + keyInt, previous.Position()) } switc.Cases[index] = cas @@ -873,7 +890,7 @@ func (this *Tree) analyzeSwitch ( switc.Default.Expression = expression } - if into != nil && switc.Default != nil { + if into != nil && switc.Default == nil { return nil, errors.Errorf ( switc.Position(), "switch must have a default case") From 5dda7b2186ff3d9c255d972327e0ab638bb2f2a8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 25 Mar 2024 22:34:24 -0400 Subject: [PATCH 09/10] Generate match statement default cases --- compiler/compiler_test.go | 10 +++ .../data/match-default-print/fspl.mod | 2 + .../data/match-default-print/main.fspl | 11 +++ generator/control-flow_test.go | 86 +++++++++++++++++++ generator/expression.go | 67 ++++++++++----- 5 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 compiler/test-data/data/match-default-print/fspl.mod create mode 100644 compiler/test-data/data/match-default-print/main.fspl diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 3812bbb..9370dde 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -82,6 +82,16 @@ testUnit (test, 0, )} +func TestMatchDefaultPrint (test *testing.T) { +dependencies := []string { + compileDependency(test, "io"), +} +testUnit (test, +"/test-data/data/match-default-print", dependencies, +"", "something else\n", +0, +)} + func TestReturnAssign (test *testing.T) { dependencies := []string { compileDependency(test, "io"), diff --git a/compiler/test-data/data/match-default-print/fspl.mod b/compiler/test-data/data/match-default-print/fspl.mod new file mode 100644 index 0000000..bd8d3c7 --- /dev/null +++ b/compiler/test-data/data/match-default-print/fspl.mod @@ -0,0 +1,2 @@ +'624d4557-5291-4ad7-9283-7c200b9c2942' ++ 'io' diff --git a/compiler/test-data/data/match-default-print/main.fspl b/compiler/test-data/data/match-default-print/main.fspl new file mode 100644 index 0000000..36fc3b8 --- /dev/null +++ b/compiler/test-data/data/match-default-print/main.fspl @@ -0,0 +1,11 @@ +[main]: I32 'main' = { + x:UInt = 7 + [matchToInt x] + 0 +} + +U: (| Int F64 UInt) +[matchToInt u:U] = match u + | u:Int io::[println 'Int'] + | u:F64 io::[println 'F64'] + * io::[println 'something else'] diff --git a/generator/control-flow_test.go b/generator/control-flow_test.go index 963c8f0..e21c736 100644 --- a/generator/control-flow_test.go +++ b/generator/control-flow_test.go @@ -340,6 +340,92 @@ U: (| Int F64 UInt) `) } +func TestMatchDefault (test *testing.T) { +testString (test, +`%"0zNZN147MN2wzMAQ6NS2dQ==::U" = type { i64, i64 } +%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64 +%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } +define void @"0zNZN147MN2wzMAQ6NS2dQ==::matchToInt"(%"0zNZN147MN2wzMAQ6NS2dQ==::U" %u) { +0: + %1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::U" + store %"0zNZN147MN2wzMAQ6NS2dQ==::U" %u, ptr %1 + %2 = alloca i64 + %3 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String" + %4 = alloca [3 x i8] + %5 = alloca double + %6 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String" + %7 = alloca [3 x i8] + %8 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String" + %9 = alloca [3 x i8] + %10 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::U", ptr %1, i32 0, i32 0 + %11 = load i64, ptr %10 + switch i64 %11, label %31 [ + i64 7620767046192759206, label %13 + i64 9186060094042213285, label %22 + ] +12: + ret void +13: + %14 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::U", ptr %1, i32 0, i32 1 + %15 = load i64, ptr %14 + store i64 %15, ptr %2 + %16 = getelementptr [3 x i8], ptr %4, i32 0, i32 0 + store i8 73, ptr %16 + %17 = getelementptr [3 x i8], ptr %4, i32 0, i32 1 + store i8 110, ptr %17 + %18 = getelementptr [3 x i8], ptr %4, i32 0, i32 2 + store i8 116, ptr %18 + %19 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %3, i32 0, i32 0 + store ptr %4, ptr %19 + %20 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %3, i32 0, i32 1 + store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 3, ptr %20 + %21 = load %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %3 + call void @"0zNZN147MN2wzMAQ6NS2dQ==::print"(%"AAAAAAAAAAAAAAAAAAAAAA==::String" %21) + br label %12 +22: + %23 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::U", ptr %1, i32 0, i32 1 + %24 = load double, ptr %23 + store double %24, ptr %5 + %25 = getelementptr [3 x i8], ptr %7, i32 0, i32 0 + store i8 70, ptr %25 + %26 = getelementptr [3 x i8], ptr %7, i32 0, i32 1 + store i8 54, ptr %26 + %27 = getelementptr [3 x i8], ptr %7, i32 0, i32 2 + store i8 52, ptr %27 + %28 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %6, i32 0, i32 0 + store ptr %7, ptr %28 + %29 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %6, i32 0, i32 1 + store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 3, ptr %29 + %30 = load %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %6 + call void @"0zNZN147MN2wzMAQ6NS2dQ==::print"(%"AAAAAAAAAAAAAAAAAAAAAA==::String" %30) + br label %12 +31: + %32 = getelementptr [3 x i8], ptr %9, i32 0, i32 0 + store i8 105, ptr %32 + %33 = getelementptr [3 x i8], ptr %9, i32 0, i32 1 + store i8 100, ptr %33 + %34 = getelementptr [3 x i8], ptr %9, i32 0, i32 2 + store i8 107, ptr %34 + %35 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %8, i32 0, i32 0 + store ptr %9, ptr %35 + %36 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %8, i32 0, i32 1 + store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 3, ptr %36 + %37 = load %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %8 + call void @"0zNZN147MN2wzMAQ6NS2dQ==::print"(%"AAAAAAAAAAAAAAAAAAAAAA==::String" %37) + br label %12 +} +declare void @"0zNZN147MN2wzMAQ6NS2dQ==::print"(%"AAAAAAAAAAAAAAAAAAAAAA==::String" %str) +`, +` +U: (| Int F64 UInt) +[print str:String] +[matchToInt u:U] = match u + | u:Int [print 'Int'] + | u:F64 [print 'F64'] + * [print 'idk'] +`) +} + func TestIfElseReturnValueUsed (test *testing.T) { testString (test, `%"AAAAAAAAAAAAAAAAAAAAAA==::Bool" = type i1 diff --git a/generator/expression.go b/generator/expression.go index 304e067..254efb7 100644 --- a/generator/expression.go +++ b/generator/expression.go @@ -122,7 +122,6 @@ func (this *generator) generateIfElse (ifelse *entity.IfElse, mode resultMode) ( } func (this *generator) generateMatch (match *entity.Match, mode resultMode) (llvm.Value, bool, error) { - var loc bool value, err := this.generateExpressionLoc(match.Value) if err != nil { return nil, false, err } irUnionType, err := this.generateType(match.Value.Type()) @@ -134,6 +133,31 @@ func (this *generator) generateMatch (match *entity.Match, mode resultMode) (llv irCases := make([]*llvm.Case, len(match.Cases)) irIncomings := []*llvm.Incoming { } + loc := false + first := true + + generateCaseExpression := func (expression entity.Expression) error { + thisMode := mode + if !first && mode != resultModeAny { + if loc { thisMode = resultModeLoc + } else { thisMode = resultModeVal } + } + result, thisLoc, err := this.generateExpression(expression, thisMode) + if err != nil { return err } + if first { loc = thisLoc } + if !this.blockManager.Terminated() { + if expression != nil { + irIncomings = append(irIncomings, &llvm.Incoming { + X: result, + Predecessor: this.blockManager.Block, + }) + } + this.blockManager.NewBr(exitBlock) + } + return nil + } + + // generate type cases for index, cas := range match.Cases { // set up ir case caseBlock := this.blockManager.newBlock() @@ -151,30 +175,27 @@ func (this *generator) generateMatch (match *entity.Match, mode resultMode) (llv this.blockManager.addDeclaration(cas.Declaration, data) // generate case expression - thisMode := mode - if index != 0 && mode != resultModeAny { - if loc { thisMode = resultModeLoc - } else { thisMode = resultModeVal } - } - caseExpression, thisLoc, err := this.generateExpression(cas.Expression, thisMode) + err = generateCaseExpression(cas.Expression) + if err != nil { return nil, false, err } + } + + // generate default case + var defaultBlock llvm.Value + if match.Default != nil { + defaultBlock = this.blockManager.newBlock() + err = generateCaseExpression(match.Default.Expression) if err != nil { return nil, false, err } - if index == 0 { loc = thisLoc } - if !this.blockManager.Terminated() { - if caseExpression != nil { - irIncomings = append(irIncomings, &llvm.Incoming { - X: caseExpression, - Predecessor: this.blockManager.Block, - }) - } - this.blockManager.NewBr(exitBlock) - } } // create switch branch this.blockManager.Block = previousBlock hashFieldLoc := this.getUnionTypeFieldLoc(value, irUnionType) hash := this.blockManager.NewLoad(irHashType, hashFieldLoc) - this.blockManager.NewSwitch(hash, exitBlock, irCases...) + if defaultBlock == nil { + this.blockManager.NewSwitch(hash, exitBlock, irCases...) + } else { + this.blockManager.NewSwitch(hash, defaultBlock, irCases...) + } // discard/obtain results this.blockManager.Block = exitBlock @@ -183,10 +204,12 @@ func (this *generator) generateMatch (match *entity.Match, mode resultMode) (llv } else { irType, err := this.generateType(match.Type()) if err != nil { return nil, false, err } - irIncomings = append(irIncomings, &llvm.Incoming { - X: llvm.NewConstZeroInitializer(irType), - Predecessor: previousBlock, - }) + if defaultBlock == nil { + irIncomings = append(irIncomings, &llvm.Incoming { + X: llvm.NewConstZeroInitializer(irType), + Predecessor: previousBlock, + }) + } return this.blockManager.NewPhi(irIncomings...), loc, nil } } From 650289f03e0cf46e7b099902c3aff2841fa43a94 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 26 Mar 2024 00:42:27 -0400 Subject: [PATCH 10/10] Generate switch statements --- compiler/compiler_test.go | 7 ++ .../data/match-default-print/fspl.mod | 2 +- .../test-data/data/switch-exit-code/fspl.mod | 1 + .../test-data/data/switch-exit-code/main.fspl | 10 +++ generator/control-flow_test.go | 53 ++++++++++++ generator/expression-multiplex.go | 8 ++ generator/expression.go | 82 +++++++++++++++++++ 7 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 compiler/test-data/data/switch-exit-code/fspl.mod create mode 100644 compiler/test-data/data/switch-exit-code/main.fspl diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 9370dde..2327744 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -92,6 +92,13 @@ testUnit (test, 0, )} +func TestSwitchExitCode (test *testing.T) { +testUnit (test, +"/test-data/data/switch-exit-code", nil, +"", "", +4, +)} + func TestReturnAssign (test *testing.T) { dependencies := []string { compileDependency(test, "io"), diff --git a/compiler/test-data/data/match-default-print/fspl.mod b/compiler/test-data/data/match-default-print/fspl.mod index bd8d3c7..81f8c53 100644 --- a/compiler/test-data/data/match-default-print/fspl.mod +++ b/compiler/test-data/data/match-default-print/fspl.mod @@ -1,2 +1,2 @@ -'624d4557-5291-4ad7-9283-7c200b9c2942' +'4f3d6ccb-c233-4648-abd2-72e3f9a82efd' + 'io' diff --git a/compiler/test-data/data/switch-exit-code/fspl.mod b/compiler/test-data/data/switch-exit-code/fspl.mod new file mode 100644 index 0000000..99dea8e --- /dev/null +++ b/compiler/test-data/data/switch-exit-code/fspl.mod @@ -0,0 +1 @@ +'433ed4ee-be14-4d0f-baef-a13919ec3b6c' diff --git a/compiler/test-data/data/switch-exit-code/main.fspl b/compiler/test-data/data/switch-exit-code/main.fspl new file mode 100644 index 0000000..5dde082 --- /dev/null +++ b/compiler/test-data/data/switch-exit-code/main.fspl @@ -0,0 +1,10 @@ +[main]: I32 'main' = { + x:I32 = 1 + [switch x] +} + +[switch x:I32]:I32 = switch x + | 0 5 + | 1 4 + | 2 3 + * 0 diff --git a/generator/control-flow_test.go b/generator/control-flow_test.go index e21c736..a443945 100644 --- a/generator/control-flow_test.go +++ b/generator/control-flow_test.go @@ -426,6 +426,59 @@ U: (| Int F64 UInt) `) } + +func TestSwitchReturn (test *testing.T) { +testString (test, +`%"AAAAAAAAAAAAAAAAAAAAAA==::Bool" = type i1 +define %"AAAAAAAAAAAAAAAAAAAAAA==::Bool" @"0zNZN147MN2wzMAQ6NS2dQ==::is5"(i64 %x) { +0: + %1 = alloca i64 + store i64 %x, ptr %1 + %2 = load i64, ptr %1 + switch i64 %2, label %3 [ + i64 5, label %4 + ] +3: + ret i1 false +4: + ret i1 true +} +`, +` +[is5 x:Int]:Bool = { + switch x | 5 [return true] + false +} +`) +} + +func TestSwitchReturnValueUsed (test *testing.T) { +testString (test, +`%"AAAAAAAAAAAAAAAAAAAAAA==::Bool" = type i1 +define %"AAAAAAAAAAAAAAAAAAAAAA==::Bool" @"0zNZN147MN2wzMAQ6NS2dQ==::is5"(i64 %x) { +0: + %1 = alloca i64 + store i64 %x, ptr %1 + %2 = load i64, ptr %1 + switch i64 %2, label %6 [ + i64 5, label %5 + ] +3: + %4 = phi i1 [ false, %6 ] + ret i1 %4 +5: + ret i1 true +6: + br label %3 +} +`, +` +[is5 x:Int]:Bool = switch x + | 5 [return true] + * false +`) +} + func TestIfElseReturnValueUsed (test *testing.T) { testString (test, `%"AAAAAAAAAAAAAAAAAAAAAA==::Bool" = type i1 diff --git a/generator/expression-multiplex.go b/generator/expression-multiplex.go index 5f40b70..4a4045e 100644 --- a/generator/expression-multiplex.go +++ b/generator/expression-multiplex.go @@ -95,6 +95,8 @@ func (this *generator) generateExpressionAny (expression entity.Expression) (reg return this.generateIfElse(expression, resultModeAny) case *entity.Match: return this.generateMatch(expression, resultModeAny) + case *entity.Switch: + return this.generateSwitch(expression, resultModeAny) case *entity.Loop: return this.generateLoop(expression, resultModeAny) case *entity.For: @@ -160,6 +162,9 @@ func (this *generator) generateExpressionVal (expression entity.Expression) (llv case *entity.Match: val, _, err := this.generateMatch(expression, resultModeVal) return val, err + case *entity.Switch: + val, _, err := this.generateSwitch(expression, resultModeVal) + return val, err case *entity.Loop: val, _, err := this.generateLoop(expression, resultModeVal) return val, err @@ -235,6 +240,9 @@ func (this *generator) generateExpressionLoc (expression entity.Expression) (llv case *entity.Match: loc, _, err := this.generateMatch(expression, resultModeLoc) return loc, err + case *entity.Switch: + loc, _, err := this.generateSwitch(expression, resultModeLoc) + return loc, err case *entity.Loop: loc, _, err := this.generateLoop(expression, resultModeLoc) return loc, err diff --git a/generator/expression.go b/generator/expression.go index 254efb7..3cd7fe7 100644 --- a/generator/expression.go +++ b/generator/expression.go @@ -214,6 +214,88 @@ func (this *generator) generateMatch (match *entity.Match, mode resultMode) (llv } } +func (this *generator) generateSwitch (match *entity.Switch, mode resultMode) (llvm.Value, bool, error) { + irValue, err := this.generateExpressionVal(match.Value) + if err != nil { return nil, false, err } + irValueType, err := this.generateType(match.Value.Type()) + if err != nil { return nil, false, err } + + previousBlock := this.blockManager.Block + exitBlock := this.blockManager.newBlock() + + irCases := make([]*llvm.Case, len(match.Cases)) + irIncomings := []*llvm.Incoming { } + loc := false + first := true + + generateCaseExpression := func (expression entity.Expression) error { + thisMode := mode + if !first && mode != resultModeAny { + if loc { thisMode = resultModeLoc + } else { thisMode = resultModeVal } + } + result, thisLoc, err := this.generateExpression(expression, thisMode) + if err != nil { return err } + if first { loc = thisLoc } + if !this.blockManager.Terminated() { + if expression != nil { + irIncomings = append(irIncomings, &llvm.Incoming { + X: result, + Predecessor: this.blockManager.Block, + }) + } + this.blockManager.NewBr(exitBlock) + } + return nil + } + + // generate value cases + for index, cas := range match.Cases { + caseBlock := this.blockManager.newBlock() + key := match.CaseOrder[index] + irCases[index] = &llvm.Case { + X: llvm.NewConstInt ( + llvm.ReduceToBase(irValueType).(*llvm.TypeInt), + key), + Target: caseBlock, + } + err = generateCaseExpression(cas.Expression) + if err != nil { return nil, false, err } + } + + // generate default case + var defaultBlock llvm.Value + if match.Default != nil { + defaultBlock = this.blockManager.newBlock() + err = generateCaseExpression(match.Default.Expression) + if err != nil { return nil, false, err } + } + + // create switch branch + this.blockManager.Block = previousBlock + if defaultBlock == nil { + this.blockManager.NewSwitch(irValue, exitBlock, irCases...) + } else { + this.blockManager.NewSwitch(irValue, defaultBlock, irCases...) + } + + // discard/obtain results + this.blockManager.Block = exitBlock + if mode == resultModeAny { + return nil, false, nil + } else { + irType, err := this.generateType(match.Type()) + if err != nil { return nil, false, err } + if defaultBlock == nil { + irIncomings = append(irIncomings, &llvm.Incoming { + X: llvm.NewConstZeroInitializer(irType), + Predecessor: previousBlock, + }) + } + return this.blockManager.NewPhi(irIncomings...), loc, nil + } +} + func (this *generator) generateLoop (loop *entity.Loop, mode resultMode) (llvm.Value, bool, error) { irResultType, err := this.generateType(loop.Type()) if err != nil { return nil, false, err }