diff --git a/src/core/Environment.go b/src/core/Environment.go index 1fde9e3..ff684ca 100644 --- a/src/core/Environment.go +++ b/src/core/Environment.go @@ -30,6 +30,7 @@ func (env *Environment) AddPackage(name string, isExtern bool) *Package { pkg = &Package{ Name: name, Constants: make(map[string]*Constant), + Enums: make(map[string]*types.Enum), Functions: make(map[string]*Function, 8), Structs: make(map[string]*types.Struct), Globals: make(map[string]*Global), diff --git a/src/core/Package.go b/src/core/Package.go index 056291b..a235f53 100644 --- a/src/core/Package.go +++ b/src/core/Package.go @@ -5,6 +5,7 @@ import "git.urbach.dev/cli/q/src/types" // Package represents a directory of functions. type Package struct { Constants map[string]*Constant + Enums map[string]*types.Enum Functions map[string]*Function Structs map[string]*types.Struct Globals map[string]*Global diff --git a/src/core/ReceiveEnum.go b/src/core/ReceiveEnum.go new file mode 100644 index 0000000..4a404bf --- /dev/null +++ b/src/core/ReceiveEnum.go @@ -0,0 +1,9 @@ +package core + +import "git.urbach.dev/cli/q/src/types" + +// ReceiveEnum receives an enum from the scanner. +func (env *Environment) ReceiveEnum(enum *types.Enum) { + pkg := env.AddPackage(enum.Package(), false) + pkg.Enums[enum.Name()] = enum +} \ No newline at end of file diff --git a/src/core/errors.go b/src/core/errors.go index 6133bdd..8e0b655 100644 --- a/src/core/errors.go +++ b/src/core/errors.go @@ -7,6 +7,7 @@ import ( ) var ( + ExpectedEnumMember = errors.String("Expected enum member") ExpectedFunctionCall = errors.String("Expected function call") ExpectedPackageMember = errors.String("Expected package member") ExpectedStructField = errors.String("Expected struct field") @@ -229,6 +230,21 @@ func (err *UndefinedStructField) Error() string { return fmt.Sprintf("Struct field '%s' of '%s' has an undefined value", err.FieldName, err.Identifier) } +// UnknownEnumMember represents unknown enum members. +type UnknownEnumMember struct { + EnumName string + MemberName string + CorrectMemberName string +} + +func (err *UnknownEnumMember) Error() string { + if err.CorrectMemberName != "" { + return fmt.Sprintf("Unknown enum member '%s' in '%s', did you mean '%s'?", err.MemberName, err.EnumName, err.CorrectMemberName) + } + + return fmt.Sprintf("Unknown enum member '%s' in '%s'", err.MemberName, err.EnumName) +} + // UnknownStructField represents unknown struct fields. type UnknownStructField struct { StructName string diff --git a/src/core/errors_test.go b/src/core/errors_test.go index 8f9cad9..df8b6ae 100644 --- a/src/core/errors_test.go +++ b/src/core/errors_test.go @@ -21,6 +21,7 @@ var errs = []struct { {"DefinitionCountMismatch2.q", &core.DefinitionCountMismatch{Function: "main.swap", Count: 3, ExpectedCount: 2}}, {"DefinitionCountMismatch3.q", &core.DefinitionCountMismatch{Function: "main.nothing", Count: 1, ExpectedCount: 0}}, {"ErrorNotChecked.q", &core.ErrorNotChecked{Identifier: "value"}}, + {"ExpectedEnumMember.q", core.ExpectedEnumMember}, {"ExpectedPackageMember.q", core.ExpectedPackageMember}, {"ExpectedStructField.q", core.ExpectedStructField}, {"InvalidCondition.q", core.InvalidCondition}, @@ -86,6 +87,7 @@ var errs = []struct { {"TypeMismatch15.q", &core.TypeMismatch{Encountered: "string", Expected: "int"}}, {"TypeMismatch16.q", &core.TypeMismatch{Encountered: "string", Expected: "int"}}, {"UndefinedStructField.q", &core.UndefinedStructField{Identifier: "p", FieldName: "y"}}, + {"UnknownEnumMember.q", &core.UnknownEnumMember{EnumName: "MyEnum", MemberName: "unknown"}}, {"UnknownIdentifier.q", &core.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier2.q", &core.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier3.q", &core.UnknownIdentifier{Name: "x"}}, diff --git a/src/core/evaluateDot.go b/src/core/evaluateDot.go index 64a4cb6..0d68ca8 100644 --- a/src/core/evaluateDot.go +++ b/src/core/evaluateDot.go @@ -48,6 +48,23 @@ func (f *Function) evaluateDot(expr *expression.Expression) (ssa.Value, error) { return f.evaluatePackageMember(pkg, rightText, expr) } + enumValue, isEnum := leftValue.(*ssa.Enum) + + if isEnum { + if right.Token.Kind != token.Identifier { + return nil, errors.New(ExpectedEnumMember, f.File, right.Source()) + } + + rightText := right.Token.StringFrom(f.File.Bytes) + constExpr, exists := enumValue.Typ.Member(rightText) + + if !exists { + return nil, errors.New(&UnknownEnumMember{EnumName: enumValue.Typ.Name(), MemberName: rightText}, f.File, right.Source()) + } + + return f.evaluateRight(constExpr.(*expression.Expression)) + } + if expr.Parent != nil && expr.Parent.Token.Kind == token.Call && expr.Parent.Children[0] == expr { f.Block().Instructions = f.Block().Instructions[:reset] return f.evaluateMethod(leftValue, left, right, expr) diff --git a/src/core/evaluatePackageMember.go b/src/core/evaluatePackageMember.go index 9687a8b..0f7d45a 100644 --- a/src/core/evaluatePackageMember.go +++ b/src/core/evaluatePackageMember.go @@ -11,6 +11,44 @@ import ( // evaluatePackageMember converts a pkg.something expression to an SSA value. func (f *Function) evaluatePackageMember(pkg *Package, rightText string, expr *expression.Expression) (ssa.Value, error) { + variants, exists := pkg.Functions[rightText] + + if exists { + if expr.Parent.Token.Kind == token.Call && expr.Parent.Children[0] == expr { + inputExpressions := expr.Parent.Children[1:] + fn, err := f.selectFunction(variants, inputExpressions, expr) + + if err != nil { + return nil, err + } + + if fn == nil { + return nil, errors.New(&NoMatchingFunction{Function: pkg.Name + "." + rightText}, f.File, expr.Source()) + } + + if fn.IsExtern() { + f.Assembler.Libraries.Append(fn.Package(), fn.Name()) + } + + v := &ssa.Function{ + FunctionRef: fn, + Typ: fn.Type, + Source: expr.Source(), + } + + return v, nil + } + + v := f.Append(&ssa.Function{ + FunctionRef: variants, + Typ: variants.Type, + Source: expr.Source(), + }) + + f.Dependencies.Add(variants) + return v, nil + } + constant, exists := pkg.Constants[rightText] if exists { @@ -27,60 +65,34 @@ func (f *Function) evaluatePackageMember(pkg *Package, rightText string, expr *e return v, err } - variants, exists := pkg.Functions[rightText] + enum, exists := pkg.Enums[rightText] - if !exists { - global, exists := pkg.Globals[rightText] - - if exists { - v := f.Append(&ssa.Global{ - Label: pkg.Name + "." + global.Name, - Typ: f.Env.Pointer(global.Typ), - ThreadLocal: global.ThreadLocal, - Source: expr.Source(), - }) - - return v, nil - } - - if pkg.Name != f.File.Package { - rightText = pkg.Name + "." + rightText + if exists { + value := &ssa.Enum{ + Typ: enum, + Source: expr.Source(), } - return nil, errors.New(&UnknownIdentifier{Name: rightText}, f.File, expr.Source()) + return value, nil } - if expr.Parent.Token.Kind == token.Call && expr.Parent.Children[0] == expr { - inputExpressions := expr.Parent.Children[1:] - fn, err := f.selectFunction(variants, inputExpressions, expr) - - if err != nil { - return nil, err - } + global, exists := pkg.Globals[rightText] - if fn == nil { - return nil, errors.New(&NoMatchingFunction{Function: pkg.Name + "." + rightText}, f.File, expr.Source()) - } - - if fn.IsExtern() { - f.Assembler.Libraries.Append(fn.Package(), fn.Name()) - } - - v := &ssa.Function{ - FunctionRef: fn, - Typ: fn.Type, + if exists { + v := f.Append(&ssa.Global{ + Label: pkg.Name + "." + global.Name, + Typ: f.Env.Pointer(global.Typ), + ThreadLocal: global.ThreadLocal, Source: expr.Source(), - } + }) return v, nil } - v := f.Append(&ssa.Function{ - FunctionRef: variants, - Typ: variants.Type, - Source: expr.Source(), - }) + if pkg.Name != f.File.Package { + rightText = pkg.Name + "." + rightText + } + + return nil, errors.New(&UnknownIdentifier{Name: rightText}, f.File, expr.Source()) - f.Dependencies.Add(variants) - return v, nil } \ No newline at end of file diff --git a/src/core/testdata/ExpectedEnumMember.q b/src/core/testdata/ExpectedEnumMember.q new file mode 100644 index 0000000..f63a228 --- /dev/null +++ b/src/core/testdata/ExpectedEnumMember.q @@ -0,0 +1,5 @@ +MyEnum const {} + +main() { + MyEnum.1() +} \ No newline at end of file diff --git a/src/core/testdata/UnknownEnumMember.q b/src/core/testdata/UnknownEnumMember.q new file mode 100644 index 0000000..7e02e6d --- /dev/null +++ b/src/core/testdata/UnknownEnumMember.q @@ -0,0 +1,5 @@ +MyEnum const {} + +main() { + x := MyEnum.unknown +} \ No newline at end of file diff --git a/src/scanner/Scan.go b/src/scanner/Scan.go index 76eb5d9..2a8fe26 100644 --- a/src/scanner/Scan.go +++ b/src/scanner/Scan.go @@ -14,6 +14,7 @@ import ( func Scan(build *config.Build) (*core.Environment, error) { s := scanner{ constants: make(chan *core.Constant, 128), + enums: make(chan *types.Enum, 128), functions: make(chan *core.Function, 128), files: make(chan *fs.File, 128), structs: make(chan *types.Struct, 128), @@ -28,6 +29,7 @@ func Scan(build *config.Build) (*core.Environment, error) { s.queue(build.Files...) s.group.Wait() close(s.constants) + close(s.enums) close(s.functions) close(s.files) close(s.structs) @@ -37,7 +39,7 @@ func Scan(build *config.Build) (*core.Environment, error) { env := core.NewEnvironment(build) - for s.functions != nil || s.files != nil || s.structs != nil || s.constants != nil || s.globals != nil || s.errors != nil { + for s.functions != nil || s.files != nil || s.structs != nil || s.constants != nil || s.enums != nil || s.globals != nil || s.errors != nil { select { case f, ok := <-s.functions: if !ok { @@ -71,6 +73,14 @@ func Scan(build *config.Build) (*core.Environment, error) { env.ReceiveConstant(constant) + case enum, ok := <-s.enums: + if !ok { + s.enums = nil + continue + } + + env.ReceiveEnum(enum) + case global, ok := <-s.globals: if !ok { s.globals = nil diff --git a/src/scanner/scanEnum.go b/src/scanner/scanEnum.go new file mode 100644 index 0000000..3301c3b --- /dev/null +++ b/src/scanner/scanEnum.go @@ -0,0 +1,78 @@ +package scanner + +import ( + "git.urbach.dev/cli/q/src/errors" + "git.urbach.dev/cli/q/src/expression" + "git.urbach.dev/cli/q/src/fs" + "git.urbach.dev/cli/q/src/token" + "git.urbach.dev/cli/q/src/types" +) + +// scanEnum scans a namespaced block of constants. +func (s *scanner) scanEnum(file *fs.File, tokens token.List, i int) (int, error) { + enumName := tokens[i].StringFrom(file.Bytes) + i += 2 + + if tokens[i].Kind != token.BlockStart { + return i, errors.NewAt(MissingBlockStart, file, tokens[i].Position) + } + + enum := types.NewEnum(file.Package, enumName) + i++ + start := -1 + blockLevel := 1 + + for i < len(tokens) { + switch tokens[i].Kind { + case token.Identifier: + if start == -1 { + start = i + } + + case token.BlockStart: + blockLevel++ + + case token.NewLine, token.BlockEnd: + if tokens[i].Kind == token.BlockEnd { + blockLevel-- + + if blockLevel > 0 { + break + } + } + + if start != -1 { + name := tokens[start].StringFrom(file.Bytes) + + if tokens[start+1].Kind != token.Assign { + return i, errors.NewAt(MissingAssign, file, tokens[start+1].Position) + } + + valueTokens := tokens[start+2 : i] + + if len(valueTokens) == 0 { + return i, errors.NewAt(MissingExpression, file, tokens[start+1].End()) + } + + value := expression.Parse(valueTokens) + + if value.Token.Kind == token.Invalid { + return i, errors.New(InvalidExpression, file, valueTokens) + } + + enum.AddMember(name, value) + } + + if tokens[i].Kind == token.BlockEnd { + s.enums <- enum + return i, nil + } + + start = -1 + } + + i++ + } + + return i, errors.NewAt(MissingBlockEnd, file, tokens[i].Position) +} \ No newline at end of file diff --git a/src/scanner/scanFile.go b/src/scanner/scanFile.go index a1feded..0362603 100644 --- a/src/scanner/scanFile.go +++ b/src/scanner/scanFile.go @@ -34,6 +34,8 @@ func (s *scanner) scanFile(path string, pkg string) error { i, err = s.scanFunction(file, tokens, i) case token.BlockStart: i, err = s.scanStruct(file, tokens, i) + case token.Const: + i, err = s.scanEnum(file, tokens, i) case token.GroupEnd: return errors.NewAt(MissingGroupStart, file, next.Position) case token.BlockEnd: diff --git a/src/scanner/scanner.go b/src/scanner/scanner.go index 15cda31..a6a1379 100644 --- a/src/scanner/scanner.go +++ b/src/scanner/scanner.go @@ -12,6 +12,7 @@ import ( // scanner is used to scan files before the actual compilation step. type scanner struct { constants chan *core.Constant + enums chan *types.Enum functions chan *core.Function files chan *fs.File structs chan *types.Struct diff --git a/src/ssa/Enum.go b/src/ssa/Enum.go new file mode 100644 index 0000000..9769f6c --- /dev/null +++ b/src/ssa/Enum.go @@ -0,0 +1,27 @@ +package ssa + +import "git.urbach.dev/cli/q/src/types" + +// Enum is an enum type reference. +type Enum struct { + Typ *types.Enum + Independent + Void + Source +} + +// Equals returns true if the enums are equal. +func (a *Enum) Equals(v Value) bool { + b, sameType := v.(*Enum) + + if !sameType { + return false + } + + return a.Typ == b.Typ +} + +// String returns the name of the enum type. +func (e *Enum) String() string { + return e.Typ.Name() +} \ No newline at end of file diff --git a/src/types/Enum.go b/src/types/Enum.go new file mode 100644 index 0000000..8381eb9 --- /dev/null +++ b/src/types/Enum.go @@ -0,0 +1,43 @@ +package types + +// Enum is a namespaced set of constants. +type Enum struct { + pkg string + name string + members map[string]any +} + +// NewEnum creates a new enum type. +func NewEnum(pkg string, name string) *Enum { + return &Enum{ + pkg: pkg, + name: name, + members: make(map[string]any), + } +} + +// AddMember adds a named member to the enum. +func (e *Enum) AddMember(name string, value any) { + e.members[name] = value +} + +// Member returns the value for the given member name, or nil if not found. +func (e *Enum) Member(name string) (any, bool) { + v, ok := e.members[name] + return v, ok +} + +// Name returns the name of the enum. +func (e *Enum) Name() string { + return e.name +} + +// Package returns the package of the enum. +func (e *Enum) Package() string { + return e.pkg +} + +// Size returns the size of the enum (same as int). +func (e *Enum) Size() int { + return Int.Size() +} \ No newline at end of file diff --git a/tests/enum.q b/tests/enum.q new file mode 100644 index 0000000..b73a214 --- /dev/null +++ b/tests/enum.q @@ -0,0 +1,13 @@ +Direction const { + North = 0 + East = 1 + South = 2 + West = 3 +} + +main() { + assert Direction.North == 0 + assert Direction.East == 1 + assert Direction.South == 2 + assert Direction.West == 3 +} \ No newline at end of file diff --git a/tests/tests_test.go b/tests/tests_test.go index 8f27770..e155ad4 100644 --- a/tests/tests_test.go +++ b/tests/tests_test.go @@ -170,6 +170,7 @@ var tests = []run{ {"random", nil, "", "", 0}, {"sqrt", nil, "", "", 0}, {"fold", nil, "", "", 0}, + {"enum", nil, "", "", 0}, {"cas", nil, "", "", 0}, {"mutex", nil, "", "", 0}, {"spill-call", nil, "", "", 0},