From a34ad058ac0bb671afaff25884e560168f87d073 Mon Sep 17 00:00:00 2001 From: Mitchell Paulus Date: Sun, 22 Dec 2024 08:18:03 -0600 Subject: [PATCH] More type check work --- mshell/Main.go | 47 ++++++++++++++++--- mshell/Parser.go | 72 +++++++++++++++++++++++++++++ mshell/TypeChecking.go | 95 +++++++++++++++++++++++++++++++++++++++ tests/test.sh | 2 +- tests/typecheck/test1.msh | 1 + 5 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 tests/typecheck/test1.msh diff --git a/mshell/Main.go b/mshell/Main.go index b649256..489f65b 100644 --- a/mshell/Main.go +++ b/mshell/Main.go @@ -9,6 +9,15 @@ import ( // "runtime" ) +type CliCommand int + +const ( + CLILEX CliCommand = iota + CLIPARSE + CLITYPECHECK + CLIEXECUTE +) + func main() { // Enable profiling // runtime.SetCPUProfileRate(1000) @@ -32,8 +41,11 @@ func main() { // trace.Start(f) // defer trace.Stop() - printLex := false - printParse := false + command := CLIEXECUTE + + // printLex := false + // printParse := false + i := 1 input := "" @@ -44,9 +56,13 @@ func main() { arg := os.Args[i] i++ if arg == "--lex" { - printLex = true + command = CLILEX + // printLex = true + } else if arg == "--typecheck" { + command = CLITYPECHECK } else if arg == "--parse" { - printParse = true + command = CLIPARSE + // printParse = true } else if arg == "-h" || arg == "--help" { fmt.Println("Usage: mshell [options] INPUT") fmt.Println("Usage: mshell [options] < INPUT") @@ -83,7 +99,7 @@ func main() { l := NewLexer(input) - if printLex { + if command == CLILEX { tokens := l.Tokenize() fmt.Println("Tokens:") for _, t := range tokens { @@ -91,7 +107,7 @@ func main() { fmt.Printf("%d:%d:%s %s\n", t.Line, t.Column, t.Type, t.Lexeme) } return - } else if printParse { + } else if command == CLIPARSE { p := MShellParser{lexer: l} p.NextToken() file, err := p.ParseFile() @@ -103,7 +119,7 @@ func main() { fmt.Println(file.ToJson()) return - } + } state := EvalState{ PositionalArgs: positionalArgs, @@ -162,6 +178,23 @@ func main() { } allDefinitions = append(allDefinitions, file.Definitions...) + + if command == CLITYPECHECK { + var typeStack MShellTypeStack + typeStack = make([]MShellType, 0) + typeCheckResult := TypeCheck(file.Items, typeStack, allDefinitions, false) + + for _, typeError := range typeCheckResult.Errors { + fmt.Fprintf(os.Stderr, "%s", typeError) + } + + if len(typeCheckResult.Errors) > 0 { + os.Exit(1) + } else { + os.Exit(0) + } + } + result := state.Evaluate(file.Items, &stack, context, allDefinitions, callStack) if !result.Success { diff --git a/mshell/Parser.go b/mshell/Parser.go index 7b59624..173edd4 100644 --- a/mshell/Parser.go +++ b/mshell/Parser.go @@ -245,6 +245,7 @@ type MShellType interface { ToJson() string Equals(other MShellType) bool String() string + ToMshell() string } type TypeDefinition struct { @@ -252,6 +253,18 @@ type TypeDefinition struct { OutputTypes []MShellType } +func (def *TypeDefinition) ToMshell() string { + inputMshellCode := make([]string, len(def.InputTypes)) + for i, t := range def.InputTypes { + inputMshellCode[i] = t.ToMshell() + } + outputMshellCode := make([]string, len(def.OutputTypes)) + for i, t := range def.OutputTypes { + outputMshellCode[i] = t.ToMshell() + } + return fmt.Sprintf("%s -- %s", strings.Join(inputMshellCode, " "), strings.Join(outputMshellCode, " ")) +} + func (def *TypeDefinition) ToJson() string { return fmt.Sprintf("{\"input\": %s, \"output\": %s}", TypeListToJson(def.InputTypes), TypeListToJson(def.OutputTypes)) } @@ -260,6 +273,10 @@ type TypeGeneric struct { Name string } +func (generic TypeGeneric) ToMshell() string { + return generic.Name +} + func (generic TypeGeneric) ToJson() string { return fmt.Sprintf("{ \"generic\": \"%s\" }", generic.Name) } @@ -277,6 +294,10 @@ func (generic TypeGeneric) String() string { type TypeInt struct { } +func (t TypeInt) ToMshell() string { + return "int" +} + func (t TypeInt) ToJson() string { return "\"int\"" } @@ -292,6 +313,10 @@ func (t TypeInt) String() string { type TypeFloat struct { } +func (t TypeFloat) ToMshell() string { + return "float" +} + func (t TypeFloat) ToJson() string { return "\"float\"" } @@ -308,6 +333,10 @@ func (t TypeFloat) String() string { type TypeString struct { } +func (t TypeString) ToMshell() string { + return "str" +} + func (t TypeString) ToJson() string { return "\"string\"" } @@ -323,6 +352,10 @@ func (t TypeString) String() string { type TypeBool struct { } +func (t TypeBool) ToMshell() string { + return "bool" +} + func (t TypeBool) ToJson() string { return "\"bool\"" } @@ -341,6 +374,10 @@ type TypeList struct { Count int // This is < 0 if the Count is not known } +func (list *TypeList) ToMshell() string { + return fmt.Sprintf("[%s]", list.ListType.ToMshell()) +} + func (list *TypeList) ToJson() string { return fmt.Sprintf("{\"list\": %s}", list.ListType.ToJson()) } @@ -372,6 +409,18 @@ type TypeTuple struct { Types []MShellType } +func (tuple *TypeTuple) ToMshell() string { + builder := strings.Builder{} + builder.WriteString("[") + builder.WriteString(tuple.Types[0].ToMshell()) + for i := 1; i < len(tuple.Types); i++ { + builder.WriteString(" ") + builder.WriteString(tuple.Types[i].ToMshell()) + } + builder.WriteString("]") + return builder.String() +} + func (tuple *TypeTuple) ToJson() string { return fmt.Sprintf("{\"tuple\": %s}", TypeListToJson(tuple.Types)) } @@ -423,6 +472,29 @@ type TypeQuote struct { OutputTypes []MShellType } +func (quote *TypeQuote) ToMshell() string { + builder := strings.Builder{} + builder.WriteString("(") + + inputTypes := []string{} + for _, t := range quote.InputTypes { + inputTypes = append(inputTypes, t.ToMshell()) + } + + outputTypes := []string{} + for _, t := range quote.OutputTypes { + outputTypes = append(outputTypes, t.ToMshell()) + } + + // Write input types space separated + builder.WriteString(strings.Join(inputTypes, " ")) + builder.WriteString(" -- ") + // Write output types space separated + builder.WriteString(strings.Join(outputTypes, " ")) + builder.WriteString(")") + return builder.String() +} + func (quote *TypeQuote) ToJson() string { return fmt.Sprintf("{\"input\": %s, \"output\": %s}", TypeListToJson(quote.InputTypes), TypeListToJson(quote.OutputTypes)) } diff --git a/mshell/TypeChecking.go b/mshell/TypeChecking.go index 1ced8f2..df22b21 100644 --- a/mshell/TypeChecking.go +++ b/mshell/TypeChecking.go @@ -2,13 +2,33 @@ package main import ( "fmt" + "strings" ) +var typeDefAdd = []TypeDefinition { + { + InputTypes: []MShellType{TypeInt{}, TypeInt{}}, + OutputTypes: []MShellType{TypeInt{}}, + }, + { + InputTypes: []MShellType{TypeString{}, TypeString{}}, + OutputTypes: []MShellType{TypeString{}}, + }, + { + InputTypes: []MShellType{TypeFloat{}, TypeFloat{}}, + OutputTypes: []MShellType{TypeFloat{}}, + }, +} + type TypeCheckError struct { Token Token Message string } +func (err TypeCheckError) String() string { + return fmt.Sprintf("%d:%d: %s", err.Token.Line, err.Token.Column, err.Message) +} + type TypeCheckResult struct { Errors []TypeCheckError InputTypes []MShellType @@ -53,6 +73,65 @@ type TypeCheckContext struct { InQuote bool } +func TypeCheckTypeDef(stack MShellTypeStack, typeDef TypeDefinition) bool { + if (len(typeDef.InputTypes) > stack.Len()) { + return false + } + + for i := 0; i < len(typeDef.InputTypes); i++ { + stackIndex := len(stack) - len(typeDef.InputTypes) + i + stackType := stack[stackIndex] + if !stackType.Equals(typeDef.InputTypes[i]) { + return false + } + } + + return true +} + +func TypeCheckStack(stack MShellTypeStack, typeDefs []TypeDefinition) (int) { + for idx, typeDef := range typeDefs { + if TypeCheckTypeDef(stack, typeDef) { + return idx + } + } + + return -1 +} + +func TypeCheckErrorMessage(stack MShellTypeStack, typeDefs []TypeDefinition, tokenName string) (string) { + // First check for a length mismatch + minInputLength := 10000 + maxInputLength := 0 + for _, typeDef := range typeDefs { + if len(typeDef.InputTypes) < minInputLength { + minInputLength = len(typeDef.InputTypes) + } + + if len(typeDef.InputTypes) > maxInputLength { + maxInputLength = len(typeDef.InputTypes) + } + } + + if minInputLength > stack.Len() { + return fmt.Sprintf("Expected at least %d arguments on the stack for %s, but found %d.\n", minInputLength, tokenName, stack.Len()) + } + + // Start a builder for the error message + var builder strings.Builder + builder.WriteString(fmt.Sprintf("Could not find a matching type definition for %s.\n", tokenName)) + builder.WriteString(fmt.Sprintf("Current stack:\n")) + for i := stack.Len() - maxInputLength; i < stack.Len(); i++ { + builder.WriteString(fmt.Sprintf(" %s\n", stack[i].ToMshell())) + } + builder.WriteString(fmt.Sprintf("Expected types:\n")) + for _, typeDef := range typeDefs { + builder.WriteString(fmt.Sprintf(" %s\n", typeDef.ToMshell())) + } + + return builder.String() +} + func TypeCheck(objects []MShellParseItem, stack MShellTypeStack, definitions []MShellDefinition, inQuote bool) TypeCheckResult { // Short circuit if there are no objects to type check if len(objects) == 0 { @@ -137,6 +216,11 @@ MainLoop: // Search built-in definitions } + if typeDef == nil { + stack.Push(&TypeString{}) + continue MainLoop + } + if inQuote { if len(typeDef.InputTypes) > stack.Len() { // For the difference, add the types to the input stack @@ -176,6 +260,15 @@ MainLoop: stack.Push(TypeInt{}) } else if t.Type == STRING || t.Type == SINGLEQUOTESTRING { stack.Push(TypeString{}) + } else if t.Type == PLUS { + idx := TypeCheckStack(stack, typeDefAdd) + if idx == -1 { + message := TypeCheckErrorMessage(stack, typeDefAdd, "+") + + typeCheckResult.Errors = append(typeCheckResult.Errors, TypeCheckError{Token: t, Message: message}) + return typeCheckResult + // continue MainLoop + } } else if t.Type == IF { obj, err := stack.Pop() if err != nil { @@ -253,6 +346,8 @@ MainLoop: // The the types of the quotes for the conditions: // They should all be of the same type, and if they consume anything on the stack, they should put it back. // The top of the stack should be a boolean. + } else { + typeCheckResult.Errors = append(typeCheckResult.Errors, TypeCheckError{Token: t, Message: fmt.Sprintf("Unexpected token %s.\n", t.Lexeme)}) } } } diff --git a/tests/test.sh b/tests/test.sh index 936ea3c..c3b8875 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,2 +1,2 @@ #!/bin/sh -find . -name '*.msh' | parallel ./test_file.sh +find . -maxdepth 1 -name '*.msh' | parallel ./test_file.sh diff --git a/tests/typecheck/test1.msh b/tests/typecheck/test1.msh new file mode 100644 index 0000000..7b7c883 --- /dev/null +++ b/tests/typecheck/test1.msh @@ -0,0 +1 @@ +"asdf" 10 +