diff --git a/lib/time/timespec.q b/lib/time/timespec.q new file mode 100644 index 0000000..84d53d6 --- /dev/null +++ b/lib/time/timespec.q @@ -0,0 +1,4 @@ +struct timespec { + seconds Int + nanoseconds Int +} \ No newline at end of file diff --git a/src/build/Build.go b/src/build/Build.go index 52c9f5f..e3b620e 100644 --- a/src/build/Build.go +++ b/src/build/Build.go @@ -23,8 +23,8 @@ func New(files ...string) *Build { // Run compiles the input files. func (build *Build) Run() (compiler.Result, error) { - files, functions, errors := scanner.Scan(build.Files) - return compiler.Compile(files, functions, errors) + files, functions, types, errors := scanner.Scan(build.Files) + return compiler.Compile(files, functions, types, errors) } // Executable returns the path to the executable. diff --git a/src/compiler/Compile.go b/src/compiler/Compile.go index 5d17cd5..dd35a48 100644 --- a/src/compiler/Compile.go +++ b/src/compiler/Compile.go @@ -6,13 +6,15 @@ import ( "git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/errors" "git.akyoto.dev/cli/q/src/fs" + "git.akyoto.dev/cli/q/src/types" ) // Compile waits for the scan to finish and compiles all functions. -func Compile(files <-chan *fs.File, functions <-chan *core.Function, errs <-chan error) (Result, error) { +func Compile(files <-chan *fs.File, functions <-chan *core.Function, structs <-chan *types.Type, errs <-chan error) (Result, error) { result := Result{} allFiles := make([]*fs.File, 0, 8) allFunctions := map[string]*core.Function{} + allTypes := map[string]*types.Type{} for functions != nil || files != nil || errs != nil { select { @@ -25,6 +27,14 @@ func Compile(files <-chan *fs.File, functions <-chan *core.Function, errs <-chan function.Functions = allFunctions allFunctions[function.UniqueName] = function + case typ, ok := <-structs: + if !ok { + structs = nil + continue + } + + allTypes[typ.Name] = typ + case file, ok := <-files: if !ok { files = nil diff --git a/src/errors/Common.go b/src/errors/Common.go index a4ebdc9..ab301d2 100644 --- a/src/errors/Common.go +++ b/src/errors/Common.go @@ -6,6 +6,7 @@ var ( ExpectedFunctionParameters = &Base{"Expected function parameters"} ExpectedFunctionDefinition = &Base{"Expected function definition"} ExpectedIfBeforeElse = &Base{"Expected an 'if' block before 'else'"} + ExpectedStructName = &Base{"Expected struct name"} InvalidNumber = &Base{"Invalid number"} InvalidExpression = &Base{"Invalid expression"} InvalidRune = &Base{"Invalid rune"} diff --git a/src/scanner/Scan.go b/src/scanner/Scan.go index f18cbff..df17032 100644 --- a/src/scanner/Scan.go +++ b/src/scanner/Scan.go @@ -3,13 +3,15 @@ package scanner import ( "git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/fs" + "git.akyoto.dev/cli/q/src/types" ) // Scan scans the list of files. -func Scan(files []string) (<-chan *fs.File, <-chan *core.Function, <-chan error) { +func Scan(files []string) (<-chan *fs.File, <-chan *core.Function, <-chan *types.Type, <-chan error) { scanner := Scanner{ files: make(chan *fs.File), functions: make(chan *core.Function), + types: make(chan *types.Type), errors: make(chan error), } @@ -18,8 +20,9 @@ func Scan(files []string) (<-chan *fs.File, <-chan *core.Function, <-chan error) scanner.group.Wait() close(scanner.files) close(scanner.functions) + close(scanner.types) close(scanner.errors) }() - return scanner.files, scanner.functions, scanner.errors + return scanner.files, scanner.functions, scanner.types, scanner.errors } diff --git a/src/scanner/Scanner.go b/src/scanner/Scanner.go index 434fe30..f1f0835 100644 --- a/src/scanner/Scanner.go +++ b/src/scanner/Scanner.go @@ -5,12 +5,14 @@ import ( "git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/fs" + "git.akyoto.dev/cli/q/src/types" ) // Scanner is used to scan files before the actual compilation step. type Scanner struct { files chan *fs.File functions chan *core.Function + types chan *types.Type errors chan error queued sync.Map group sync.WaitGroup diff --git a/src/scanner/scanFile.go b/src/scanner/scanFile.go index 6d57c0a..3dfa4ac 100644 --- a/src/scanner/scanFile.go +++ b/src/scanner/scanFile.go @@ -2,16 +2,10 @@ package scanner import ( "os" - "path/filepath" - "git.akyoto.dev/cli/q/src/config" - "git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/errors" "git.akyoto.dev/cli/q/src/fs" - "git.akyoto.dev/cli/q/src/scope" "git.akyoto.dev/cli/q/src/token" - "git.akyoto.dev/cli/q/src/types" - "git.akyoto.dev/cli/q/src/x64" ) // scanFile scans a single file. @@ -32,250 +26,31 @@ func (s *Scanner) scanFile(path string, pkg string) error { } s.files <- file + i := 0 - var ( - i = 0 - groupLevel = 0 - blockLevel = 0 - nameStart = -1 - paramsStart = -1 - paramsEnd = -1 - bodyStart = -1 - typeStart = -1 - typeEnd = -1 - ) - - for { - for i < len(tokens) && tokens[i].Kind == token.Import { - i++ - - if tokens[i].Kind != token.Identifier { - panic("expected package name") - } - - packageName := tokens[i].Text(contents) - - if file.Imports == nil { - file.Imports = map[string]*fs.Import{} - } - - fullPath := filepath.Join(config.Library, packageName) - - file.Imports[packageName] = &fs.Import{ - Path: packageName, - FullPath: fullPath, - Position: tokens[i].Position, - } - - s.queueDirectory(fullPath, packageName) - i++ - - if tokens[i].Kind != token.NewLine && tokens[i].Kind != token.EOF { - panic("expected newline or eof") - } - - i++ - } - - // Function name - for i < len(tokens) { - if tokens[i].Kind == token.Identifier { - nameStart = i - i++ - break - } - - if tokens[i].Kind == token.NewLine || tokens[i].Kind == token.Comment { - i++ - continue - } - - if tokens[i].Kind == token.Invalid { - return errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(contents)}, file, tokens[i].Position) - } - - if tokens[i].Kind == token.EOF { - return nil - } - - return errors.New(errors.ExpectedFunctionName, file, tokens[i].Position) - } - - // Function parameters - for i < len(tokens) { - if tokens[i].Kind == token.GroupStart { - groupLevel++ - i++ - - if groupLevel == 1 { - paramsStart = i - } - - continue - } - - if tokens[i].Kind == token.GroupEnd { - groupLevel-- - - if groupLevel < 0 { - return errors.New(errors.MissingGroupStart, file, tokens[i].Position) - } - - if groupLevel == 0 { - paramsEnd = i - i++ - break - } - - i++ - continue - } - - if tokens[i].Kind == token.Invalid { - return errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(contents)}, file, tokens[i].Position) - } - - if tokens[i].Kind == token.EOF { - if groupLevel > 0 { - return errors.New(errors.MissingGroupEnd, file, tokens[i].Position) - } - - if paramsStart == -1 { - return errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position) - } - - return nil - } - - if groupLevel > 0 { - i++ - continue - } - - return errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position) - } - - // Return type - if i < len(tokens) && tokens[i].Kind == token.ReturnType { - typeStart = i + 1 - - for i < len(tokens) && tokens[i].Kind != token.BlockStart { - i++ - } - - typeEnd = i - } - - // Function definition - for i < len(tokens) { - if tokens[i].Kind == token.ReturnType { - i++ - continue - } - - if tokens[i].Kind == token.BlockStart { - blockLevel++ - i++ - - if blockLevel == 1 { - bodyStart = i - } - - continue - } - - if tokens[i].Kind == token.BlockEnd { - blockLevel-- - - if blockLevel < 0 { - return errors.New(errors.MissingBlockStart, file, tokens[i].Position) - } - - if blockLevel == 0 { - break - } - - i++ - continue - } - - if tokens[i].Kind == token.Invalid { - return errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(contents)}, file, tokens[i].Position) - } - - if tokens[i].Kind == token.EOF { - if blockLevel > 0 { - return errors.New(errors.MissingBlockEnd, file, tokens[i].Position) - } - - if bodyStart == -1 { - return errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position) - } - - return nil - } - - if blockLevel > 0 { - i++ - continue - } - - return errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position) - } - - name := tokens[nameStart].Text(contents) - body := tokens[bodyStart:i] - function := core.NewFunction(pkg, name, file, body) - - if typeStart != -1 { - if tokens[typeStart].Kind == token.GroupStart && tokens[typeEnd-1].Kind == token.GroupEnd { - typeStart++ - typeEnd-- - } - - function.ReturnTypes = types.ParseList(tokens[typeStart:typeEnd], contents) - } - - parameters := tokens[paramsStart:paramsEnd] - count := 0 - - err := parameters.Split(func(tokens token.List) error { - if len(tokens) < 2 { - return errors.New(errors.MissingType, file, tokens[0].End()) - } - - name := tokens[0].Text(contents) - dataType := types.Parse(tokens[1:].Text(contents)) - register := x64.InputRegisters[count] - uses := token.Count(function.Body, contents, token.Identifier, name) - - if uses == 0 && name != "_" { - return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position) - } - - variable := &scope.Variable{ - Name: name, - Type: dataType, - Register: register, - Alive: uses, - } - - function.Parameters = append(function.Parameters, variable) - function.AddVariable(variable) - count++ + for i < len(tokens) { + switch tokens[i].Kind { + case token.NewLine: + case token.Import: + i, err = s.scanImport(file, tokens, i) + case token.Struct: + i, err = s.scanStruct(file, tokens, i) + case token.Identifier: + i, err = s.scanFunction(file, tokens, i) + case token.EOF: return nil - }) + case token.Invalid: + return errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(file.Bytes)}, file, tokens[i].Position) + default: + return errors.New(&errors.InvalidInstruction{Instruction: tokens[i].Text(file.Bytes)}, file, tokens[i].Position) + } if err != nil { return err } - s.functions <- function - nameStart = -1 - paramsStart = -1 - bodyStart = -1 - typeStart = -1 - typeEnd = -1 i++ } + + return nil } diff --git a/src/scanner/scanFunction.go b/src/scanner/scanFunction.go new file mode 100644 index 0000000..9a51bdf --- /dev/null +++ b/src/scanner/scanFunction.go @@ -0,0 +1,222 @@ +package scanner + +import ( + "git.akyoto.dev/cli/q/src/core" + "git.akyoto.dev/cli/q/src/errors" + "git.akyoto.dev/cli/q/src/fs" + "git.akyoto.dev/cli/q/src/scope" + "git.akyoto.dev/cli/q/src/token" + "git.akyoto.dev/cli/q/src/types" + "git.akyoto.dev/cli/q/src/x64" +) + +// scanFunction scans a function. +func (s *Scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, error) { + var ( + groupLevel = 0 + blockLevel = 0 + nameStart = -1 + paramsStart = -1 + paramsEnd = -1 + bodyStart = -1 + typeStart = -1 + typeEnd = -1 + ) + + // Function name + for i < len(tokens) { + if tokens[i].Kind == token.Identifier { + nameStart = i + i++ + break + } + + if tokens[i].Kind == token.NewLine || tokens[i].Kind == token.Comment { + i++ + continue + } + + if tokens[i].Kind == token.Invalid { + return i, errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(file.Bytes)}, file, tokens[i].Position) + } + + if tokens[i].Kind == token.EOF { + return i, nil + } + + return i, errors.New(errors.ExpectedFunctionName, file, tokens[i].Position) + } + + // Function parameters + for i < len(tokens) { + if tokens[i].Kind == token.GroupStart { + groupLevel++ + i++ + + if groupLevel == 1 { + paramsStart = i + } + + continue + } + + if tokens[i].Kind == token.GroupEnd { + groupLevel-- + + if groupLevel < 0 { + return i, errors.New(errors.MissingGroupStart, file, tokens[i].Position) + } + + if groupLevel == 0 { + paramsEnd = i + i++ + break + } + + i++ + continue + } + + if tokens[i].Kind == token.Invalid { + return i, errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(file.Bytes)}, file, tokens[i].Position) + } + + if tokens[i].Kind == token.EOF { + if groupLevel > 0 { + return i, errors.New(errors.MissingGroupEnd, file, tokens[i].Position) + } + + if paramsStart == -1 { + return i, errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position) + } + + return i, nil + } + + if groupLevel > 0 { + i++ + continue + } + + return i, errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position) + } + + // Return type + if i < len(tokens) && tokens[i].Kind == token.ReturnType { + typeStart = i + 1 + + for i < len(tokens) && tokens[i].Kind != token.BlockStart { + i++ + } + + typeEnd = i + } + + // Function definition + for i < len(tokens) { + if tokens[i].Kind == token.ReturnType { + i++ + continue + } + + if tokens[i].Kind == token.BlockStart { + blockLevel++ + i++ + + if blockLevel == 1 { + bodyStart = i + } + + continue + } + + if tokens[i].Kind == token.BlockEnd { + blockLevel-- + + if blockLevel < 0 { + return i, errors.New(errors.MissingBlockStart, file, tokens[i].Position) + } + + if blockLevel == 0 { + break + } + + i++ + continue + } + + if tokens[i].Kind == token.Invalid { + return i, errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(file.Bytes)}, file, tokens[i].Position) + } + + if tokens[i].Kind == token.EOF { + if blockLevel > 0 { + return i, errors.New(errors.MissingBlockEnd, file, tokens[i].Position) + } + + if bodyStart == -1 { + return i, errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position) + } + + return i, nil + } + + if blockLevel > 0 { + i++ + continue + } + + return i, errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position) + } + + name := tokens[nameStart].Text(file.Bytes) + body := tokens[bodyStart:i] + function := core.NewFunction(file.Package, name, file, body) + + if typeStart != -1 { + if tokens[typeStart].Kind == token.GroupStart && tokens[typeEnd-1].Kind == token.GroupEnd { + typeStart++ + typeEnd-- + } + + function.ReturnTypes = types.ParseList(tokens[typeStart:typeEnd], file.Bytes) + } + + parameters := tokens[paramsStart:paramsEnd] + count := 0 + + err := parameters.Split(func(tokens token.List) error { + if len(tokens) < 2 { + return errors.New(errors.MissingType, file, tokens[0].End()) + } + + name := tokens[0].Text(file.Bytes) + dataType := types.Parse(tokens[1:].Text(file.Bytes)) + register := x64.InputRegisters[count] + uses := token.Count(function.Body, file.Bytes, token.Identifier, name) + + if uses == 0 && name != "_" { + return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position) + } + + variable := &scope.Variable{ + Name: name, + Type: dataType, + Register: register, + Alive: uses, + } + + function.Parameters = append(function.Parameters, variable) + function.AddVariable(variable) + count++ + return nil + }) + + if err != nil { + return i, err + } + + s.functions <- function + i++ + return i, nil +} diff --git a/src/scanner/scanImport.go b/src/scanner/scanImport.go new file mode 100644 index 0000000..b430e2e --- /dev/null +++ b/src/scanner/scanImport.go @@ -0,0 +1,41 @@ +package scanner + +import ( + "path/filepath" + + "git.akyoto.dev/cli/q/src/config" + "git.akyoto.dev/cli/q/src/fs" + "git.akyoto.dev/cli/q/src/token" +) + +// scanImport scans an import. +func (s *Scanner) scanImport(file *fs.File, tokens token.List, i int) (int, error) { + i++ + + if tokens[i].Kind != token.Identifier { + panic("expected package name") + } + + packageName := tokens[i].Text(file.Bytes) + + if file.Imports == nil { + file.Imports = map[string]*fs.Import{} + } + + fullPath := filepath.Join(config.Library, packageName) + + file.Imports[packageName] = &fs.Import{ + Path: packageName, + FullPath: fullPath, + Position: tokens[i].Position, + } + + s.queueDirectory(fullPath, packageName) + i++ + + if tokens[i].Kind != token.NewLine && tokens[i].Kind != token.EOF { + panic("expected newline or eof") + } + + return i, nil +} diff --git a/src/scanner/scanStruct.go b/src/scanner/scanStruct.go new file mode 100644 index 0000000..dddf481 --- /dev/null +++ b/src/scanner/scanStruct.go @@ -0,0 +1,66 @@ +package scanner + +import ( + "git.akyoto.dev/cli/q/src/errors" + "git.akyoto.dev/cli/q/src/fs" + "git.akyoto.dev/cli/q/src/token" + "git.akyoto.dev/cli/q/src/types" +) + +// scanStruct scans a struct. +func (s *Scanner) scanStruct(file *fs.File, tokens token.List, i int) (int, error) { + i++ + + if tokens[i].Kind != token.Identifier { + return i, errors.New(errors.ExpectedStructName, file, tokens[i].Position) + } + + structName := tokens[i].Text(file.Bytes) + + typ := &types.Type{ + Name: structName, + } + + i++ + + if tokens[i].Kind != token.BlockStart { + return i, errors.New(errors.MissingBlockStart, file, tokens[i].Position) + } + + i++ + closed := false + + for i < len(tokens) { + if tokens[i].Kind == token.Identifier { + fieldPosition := i + fieldName := tokens[i].Text(file.Bytes) + i++ + fieldTypeName := tokens[i].Text(file.Bytes) + fieldType := types.Parse(fieldTypeName) + i++ + + typ.Fields = append(typ.Fields, &types.Field{ + Type: fieldType, + Name: fieldName, + Position: token.Position(fieldPosition), + Offset: typ.Size, + }) + + typ.Size += fieldType.Size + } + + if tokens[i].Kind == token.BlockEnd { + closed = true + break + } + + i++ + } + + if !closed { + return i, errors.New(errors.MissingBlockEnd, file, tokens[i].Position) + } + + s.types <- typ + return i, nil +} diff --git a/src/token/Kind.go b/src/token/Kind.go index 737ef55..1a652b2 100644 --- a/src/token/Kind.go +++ b/src/token/Kind.go @@ -70,6 +70,7 @@ const ( Import // import Loop // loop Return // return + Struct // struct Switch // switch ___END_KEYWORDS___ // ) diff --git a/src/token/Tokenize_test.go b/src/token/Tokenize_test.go index 267f736..e755f97 100644 --- a/src/token/Tokenize_test.go +++ b/src/token/Tokenize_test.go @@ -25,7 +25,7 @@ func TestFunction(t *testing.T) { } func TestKeyword(t *testing.T) { - tokens := token.Tokenize([]byte("assert if import else loop return switch")) + tokens := token.Tokenize([]byte("assert if import else loop return struct switch")) expected := []token.Kind{ token.Assert, @@ -34,6 +34,7 @@ func TestKeyword(t *testing.T) { token.Else, token.Loop, token.Return, + token.Struct, token.Switch, token.EOF, } diff --git a/src/token/identifier.go b/src/token/identifier.go index f44bcf6..fa9cc63 100644 --- a/src/token/identifier.go +++ b/src/token/identifier.go @@ -25,6 +25,8 @@ func identifier(tokens List, buffer []byte, i Position) (List, Position) { kind = Loop case "return": kind = Return + case "struct": + kind = Struct case "switch": kind = Switch } diff --git a/tests/errors/ExpectedStructName.q b/tests/errors/ExpectedStructName.q new file mode 100644 index 0000000..6c8a4ee --- /dev/null +++ b/tests/errors/ExpectedStructName.q @@ -0,0 +1 @@ +struct{} \ No newline at end of file diff --git a/tests/errors/InvalidInstructionString.q b/tests/errors/InvalidInstructionString.q new file mode 100644 index 0000000..09905c2 --- /dev/null +++ b/tests/errors/InvalidInstructionString.q @@ -0,0 +1,3 @@ +main() { + "Hello" +} \ No newline at end of file diff --git a/tests/errors/ExpectedFunctionName.q b/tests/errors/InvalidInstructionTopLevel.q similarity index 100% rename from tests/errors/ExpectedFunctionName.q rename to tests/errors/InvalidInstructionTopLevel.q diff --git a/tests/errors/ExpectedFunctionName2.q b/tests/errors/InvalidInstructionTopLevel2.q similarity index 100% rename from tests/errors/ExpectedFunctionName2.q rename to tests/errors/InvalidInstructionTopLevel2.q diff --git a/tests/errors/InvalidInstructionTopLevel3.q b/tests/errors/InvalidInstructionTopLevel3.q new file mode 100644 index 0000000..9b26e9b --- /dev/null +++ b/tests/errors/InvalidInstructionTopLevel3.q @@ -0,0 +1 @@ ++ \ No newline at end of file diff --git a/tests/errors_test.go b/tests/errors_test.go index 9de4094..7839f63 100644 --- a/tests/errors_test.go +++ b/tests/errors_test.go @@ -16,14 +16,17 @@ var errs = []struct { }{ {"EmptySwitch.q", errors.EmptySwitch}, {"ExpectedFunctionDefinition.q", errors.ExpectedFunctionDefinition}, - {"ExpectedFunctionName.q", errors.ExpectedFunctionName}, - {"ExpectedFunctionName2.q", errors.ExpectedFunctionName}, {"ExpectedFunctionParameters.q", errors.ExpectedFunctionParameters}, {"ExpectedIfBeforeElse.q", errors.ExpectedIfBeforeElse}, {"ExpectedIfBeforeElse2.q", errors.ExpectedIfBeforeElse}, + {"ExpectedStructName.q", errors.ExpectedStructName}, + {"InvalidInstructionExpression.q", &errors.InvalidInstruction{Instruction: "+"}}, {"InvalidInstructionIdentifier.q", &errors.InvalidInstruction{Instruction: "abc"}}, {"InvalidInstructionNumber.q", &errors.InvalidInstruction{Instruction: "123"}}, - {"InvalidInstructionExpression.q", &errors.InvalidInstruction{Instruction: "+"}}, + {"InvalidInstructionString.q", &errors.InvalidInstruction{Instruction: "\"Hello\""}}, + {"InvalidInstructionTopLevel.q", &errors.InvalidInstruction{Instruction: "123"}}, + {"InvalidInstructionTopLevel2.q", &errors.InvalidInstruction{Instruction: "\"Hello\""}}, + {"InvalidInstructionTopLevel3.q", &errors.InvalidInstruction{Instruction: "+"}}, {"InvalidExpression.q", errors.InvalidExpression}, {"InvalidCharacter.q", &errors.InvalidCharacter{Character: "@"}}, {"InvalidCharacter2.q", &errors.InvalidCharacter{Character: "@"}},