Implemented struct parser

This commit is contained in:
Eduard Urbach 2025-02-04 14:41:04 +01:00
parent fc1b970f7f
commit 51e3c1ba0e
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
19 changed files with 388 additions and 252 deletions

4
lib/time/timespec.q Normal file
View File

@ -0,0 +1,4 @@
struct timespec {
seconds Int
nanoseconds Int
}

View File

@ -23,8 +23,8 @@ func New(files ...string) *Build {
// Run compiles the input files. // Run compiles the input files.
func (build *Build) Run() (compiler.Result, error) { func (build *Build) Run() (compiler.Result, error) {
files, functions, errors := scanner.Scan(build.Files) files, functions, types, errors := scanner.Scan(build.Files)
return compiler.Compile(files, functions, errors) return compiler.Compile(files, functions, types, errors)
} }
// Executable returns the path to the executable. // Executable returns the path to the executable.

View File

@ -6,13 +6,15 @@ import (
"git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/errors" "git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/fs" "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. // 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{} result := Result{}
allFiles := make([]*fs.File, 0, 8) allFiles := make([]*fs.File, 0, 8)
allFunctions := map[string]*core.Function{} allFunctions := map[string]*core.Function{}
allTypes := map[string]*types.Type{}
for functions != nil || files != nil || errs != nil { for functions != nil || files != nil || errs != nil {
select { select {
@ -25,6 +27,14 @@ func Compile(files <-chan *fs.File, functions <-chan *core.Function, errs <-chan
function.Functions = allFunctions function.Functions = allFunctions
allFunctions[function.UniqueName] = function allFunctions[function.UniqueName] = function
case typ, ok := <-structs:
if !ok {
structs = nil
continue
}
allTypes[typ.Name] = typ
case file, ok := <-files: case file, ok := <-files:
if !ok { if !ok {
files = nil files = nil

View File

@ -6,6 +6,7 @@ var (
ExpectedFunctionParameters = &Base{"Expected function parameters"} ExpectedFunctionParameters = &Base{"Expected function parameters"}
ExpectedFunctionDefinition = &Base{"Expected function definition"} ExpectedFunctionDefinition = &Base{"Expected function definition"}
ExpectedIfBeforeElse = &Base{"Expected an 'if' block before 'else'"} ExpectedIfBeforeElse = &Base{"Expected an 'if' block before 'else'"}
ExpectedStructName = &Base{"Expected struct name"}
InvalidNumber = &Base{"Invalid number"} InvalidNumber = &Base{"Invalid number"}
InvalidExpression = &Base{"Invalid expression"} InvalidExpression = &Base{"Invalid expression"}
InvalidRune = &Base{"Invalid rune"} InvalidRune = &Base{"Invalid rune"}

View File

@ -3,13 +3,15 @@ package scanner
import ( import (
"git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/fs" "git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/types"
) )
// Scan scans the list of files. // 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{ scanner := Scanner{
files: make(chan *fs.File), files: make(chan *fs.File),
functions: make(chan *core.Function), functions: make(chan *core.Function),
types: make(chan *types.Type),
errors: make(chan error), errors: make(chan error),
} }
@ -18,8 +20,9 @@ func Scan(files []string) (<-chan *fs.File, <-chan *core.Function, <-chan error)
scanner.group.Wait() scanner.group.Wait()
close(scanner.files) close(scanner.files)
close(scanner.functions) close(scanner.functions)
close(scanner.types)
close(scanner.errors) close(scanner.errors)
}() }()
return scanner.files, scanner.functions, scanner.errors return scanner.files, scanner.functions, scanner.types, scanner.errors
} }

View File

@ -5,12 +5,14 @@ import (
"git.akyoto.dev/cli/q/src/core" "git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/fs" "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. // Scanner is used to scan files before the actual compilation step.
type Scanner struct { type Scanner struct {
files chan *fs.File files chan *fs.File
functions chan *core.Function functions chan *core.Function
types chan *types.Type
errors chan error errors chan error
queued sync.Map queued sync.Map
group sync.WaitGroup group sync.WaitGroup

View File

@ -2,16 +2,10 @@ package scanner
import ( import (
"os" "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/errors"
"git.akyoto.dev/cli/q/src/fs" "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/token"
"git.akyoto.dev/cli/q/src/types"
"git.akyoto.dev/cli/q/src/x64"
) )
// scanFile scans a single file. // scanFile scans a single file.
@ -32,250 +26,31 @@ func (s *Scanner) scanFile(path string, pkg string) error {
} }
s.files <- file s.files <- file
i := 0
var ( for i < len(tokens) {
i = 0 switch tokens[i].Kind {
groupLevel = 0 case token.NewLine:
blockLevel = 0 case token.Import:
nameStart = -1 i, err = s.scanImport(file, tokens, i)
paramsStart = -1 case token.Struct:
paramsEnd = -1 i, err = s.scanStruct(file, tokens, i)
bodyStart = -1 case token.Identifier:
typeStart = -1 i, err = s.scanFunction(file, tokens, i)
typeEnd = -1 case token.EOF:
)
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++
return nil 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 { if err != nil {
return err return err
} }
s.functions <- function
nameStart = -1
paramsStart = -1
bodyStart = -1
typeStart = -1
typeEnd = -1
i++ i++
} }
return nil
} }

222
src/scanner/scanFunction.go Normal file
View File

@ -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
}

41
src/scanner/scanImport.go Normal file
View File

@ -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
}

66
src/scanner/scanStruct.go Normal file
View File

@ -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
}

View File

@ -70,6 +70,7 @@ const (
Import // import Import // import
Loop // loop Loop // loop
Return // return Return // return
Struct // struct
Switch // switch Switch // switch
___END_KEYWORDS___ // </keywords> ___END_KEYWORDS___ // </keywords>
) )

View File

@ -25,7 +25,7 @@ func TestFunction(t *testing.T) {
} }
func TestKeyword(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{ expected := []token.Kind{
token.Assert, token.Assert,
@ -34,6 +34,7 @@ func TestKeyword(t *testing.T) {
token.Else, token.Else,
token.Loop, token.Loop,
token.Return, token.Return,
token.Struct,
token.Switch, token.Switch,
token.EOF, token.EOF,
} }

View File

@ -25,6 +25,8 @@ func identifier(tokens List, buffer []byte, i Position) (List, Position) {
kind = Loop kind = Loop
case "return": case "return":
kind = Return kind = Return
case "struct":
kind = Struct
case "switch": case "switch":
kind = Switch kind = Switch
} }

View File

@ -0,0 +1 @@
struct{}

View File

@ -0,0 +1,3 @@
main() {
"Hello"
}

View File

@ -0,0 +1 @@
+

View File

@ -16,14 +16,17 @@ var errs = []struct {
}{ }{
{"EmptySwitch.q", errors.EmptySwitch}, {"EmptySwitch.q", errors.EmptySwitch},
{"ExpectedFunctionDefinition.q", errors.ExpectedFunctionDefinition}, {"ExpectedFunctionDefinition.q", errors.ExpectedFunctionDefinition},
{"ExpectedFunctionName.q", errors.ExpectedFunctionName},
{"ExpectedFunctionName2.q", errors.ExpectedFunctionName},
{"ExpectedFunctionParameters.q", errors.ExpectedFunctionParameters}, {"ExpectedFunctionParameters.q", errors.ExpectedFunctionParameters},
{"ExpectedIfBeforeElse.q", errors.ExpectedIfBeforeElse}, {"ExpectedIfBeforeElse.q", errors.ExpectedIfBeforeElse},
{"ExpectedIfBeforeElse2.q", errors.ExpectedIfBeforeElse}, {"ExpectedIfBeforeElse2.q", errors.ExpectedIfBeforeElse},
{"ExpectedStructName.q", errors.ExpectedStructName},
{"InvalidInstructionExpression.q", &errors.InvalidInstruction{Instruction: "+"}},
{"InvalidInstructionIdentifier.q", &errors.InvalidInstruction{Instruction: "abc"}}, {"InvalidInstructionIdentifier.q", &errors.InvalidInstruction{Instruction: "abc"}},
{"InvalidInstructionNumber.q", &errors.InvalidInstruction{Instruction: "123"}}, {"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}, {"InvalidExpression.q", errors.InvalidExpression},
{"InvalidCharacter.q", &errors.InvalidCharacter{Character: "@"}}, {"InvalidCharacter.q", &errors.InvalidCharacter{Character: "@"}},
{"InvalidCharacter2.q", &errors.InvalidCharacter{Character: "@"}}, {"InvalidCharacter2.q", &errors.InvalidCharacter{Character: "@"}},