diff --git a/docs/Using.md b/docs/Using.md index 50129e2..23e7dc6 100644 --- a/docs/Using.md +++ b/docs/Using.md @@ -96,7 +96,7 @@ enum State = { Starting, Started, Stopping, Stopped } ``` ## Structs You can define structs using the `struct` keyword -Structs can also hold structs within. +Structs can also hold structs within: ``` struct Entity { Identifier: u8, @@ -110,6 +110,46 @@ struct Entity { }? } ``` + +### Generics +--- +Structs support the use of generic type parameters, a generic is simply a type which allows you to slot in any other type, generics can be very handy in reducing repetition. +``` +struct Packet { + Sequence: u16, + Ack: u16, + Data: T +} + +struct Entity { + Identifier: u8, + Health: u8(0..100), + Angle: u16, + Position: vector +} + +struct Command { + X: u8, + Y: u8, + Z: u8, + -- ... +} + +event Snapshot { + From: Server, + Type: Unreliable, + Call: SingleSync, + Data: Packet +} + +event Command { + From: Server, + Type: Unreliable, + Call: SingleSync, + Data: Packet +} +``` +In the code above we have a simple packet transmission protocol which contains the current packets identifier (Sequence), the last recieved packet (Ack) and a generic data field. Instead of repeating the contents of `Packet` everytime we need to send something over the wire we can take advantage of generics to automatically fill the `Data` field with whatever we need to transmit. ## Maps You can define maps using the `map` keyword > [!NOTE] diff --git a/src/Generator/init.luau b/src/Generator/init.luau index c63ee52..4d2f395 100644 --- a/src/Generator/init.luau +++ b/src/Generator/init.luau @@ -745,6 +745,24 @@ function Generators.LuauType(Declaration: Parser.TypeDeclaration, UseTypeAsValue end end + if Value.Parameters then + local Parameters = "" + local Maximum = #Value.Parameters + + --> Generate luau types for parameters + for Index, Parameter in Value.Parameters do + Parameters ..= Generators.LuauType(Parameter) + + if Index ~= Maximum then + Parameters ..= "," + end + end + + --> Wrap in chevrons + Parameters = `<{Parameters}>` + Type = `{Type}{Parameters}` + end + --> Generalized type generation, works for everything except tuples if Declaration.Type ~= "Tuple" then Values = `Value: {UseTypeAsValue and Type or Export}` @@ -756,14 +774,14 @@ end function Generators.Type(Declaration: Type) local Value = Declaration.Value local Identifier = Value.Identifier - local Type, Values, Export, Returns = Generators.LuauType(Declaration) + local Read = Blocks.Function(GetTypesPath(Identifier, false), "", `({Export})`) local Write = Blocks.Function(GetTypesPath(Identifier, true), Values, "()") local Generics = "" if Value.Generics then - for Generic in Value.Generics do + for Generic in Value.Generics.Indices do Generics ..= `{Generic},` end diff --git a/src/Lexer.luau b/src/Lexer.luau index 7b93202..e587dd1 100644 --- a/src/Lexer.luau +++ b/src/Lexer.luau @@ -7,7 +7,7 @@ local Settings = require("./Settings") export type Types = "Comma" | "OpenParentheses" | "CloseParentheses" | "OpenBraces" | "CloseBraces" | "OpenBrackets" | "CloseBrackets" --> Structs & enums | "String" | "Boolean" | "Number" --> Literals - | "Array" | "Range" | "Optional" | "Class" | "Component" --> Attributes + | "Array" | "Range" | "Optional" | "Class" | "Component" | "OpenChevrons" | "CloseChevrons" --> Attributes | "Assign" | "FieldAssign" | "Keyword" | "Primitive" | "Identifier" --> Reserved | "Whitespace" | "Comment" | "Unknown" | "EndOfFile" @@ -40,6 +40,8 @@ local Matches = { {"^:", "FieldAssign"}, {"^{", "OpenBraces"}, {"^}", "CloseBraces"}, + {"^<", "OpenChevrons"}, + {"^>", "CloseChevrons"}, {"^,", "Comma"}, --> Comments @@ -56,7 +58,7 @@ local Matches = { {`^%({Number}..{Number}%)`, "Range"}, {`^%[{Number}]`, "Array"}, {`^%[{Number}..{Number}]`, "Array"}, - {`^%b<>`, "Component"}, + --{`^%b<>`, "Component"}, {"^%(", "OpenParentheses"}, {"^%)", "CloseParentheses"}, diff --git a/src/Modules/Error.luau b/src/Modules/Error.luau index d214214..7e84f1d 100644 --- a/src/Modules/Error.luau +++ b/src/Modules/Error.luau @@ -70,7 +70,8 @@ local Error = { AnalyzeInvalidRangeType = 3008, AnalyzeInvalidRange = 3009, AnalyzeDuplicateDeclaration = 3010, - AnalyzeDuplicateTypeGeneric = 3011 + AnalyzeDuplicateTypeGeneric = 3011, + AnalyzeInvalidGenerics = 3012, } Error.__index = Error diff --git a/src/Parser.luau b/src/Parser.luau index 9451908..5c6e3c4 100644 --- a/src/Parser.luau +++ b/src/Parser.luau @@ -3,6 +3,7 @@ --!optimize 2 local Lexer = require("./Lexer") +local Table = require("./Modules/Table") local Error = require("./Modules/Error") local Settings = require("./Settings") @@ -30,8 +31,9 @@ type Node = { } type Generics = { - Keys: {[string]: number}, - Ordered: {{{Key: string, Values: {[any]: any}}}} + Total: number, + Keys: {[number]: string}, + Indices: {[string]: number} } export type Scope = { @@ -60,6 +62,7 @@ type Attributes = { Range: NumberRange?, Optional: boolean?, Generics: Generics?, + Parameters: {TypeDeclaration}?, Components: {string}? } @@ -320,8 +323,8 @@ end ---- Utility Parsing Functions ---- -function Parser.GetReference(self: Parser, Bucket: Bucket, Identifier: Token): Reference? - local Path = string.split(Identifier.Value, ".") +function Parser.GetReference(self: Parser, Bucket: Bucket, Identifier: string): Reference? + local Path = string.split(Identifier, ".") local Scope: Scope? = self.Scope local Offset = 1 @@ -378,37 +381,109 @@ function Parser.GetGenerics(self: Parser): Generics? if self.Generics then return self.Generics end - - local Token = self:TryConsume("Component") - if not Token then + + if not self:TryConsume("OpenChevrons") then return end - --> Remove whitespace and seperate into identifiers - local Value = string.gsub(Token.Value, "[%s<>]+", "") - local Identifiers = string.split(Value, ",") - --> Parse generics local Generics: Generics = { + Total = 0, Keys = {}, - Ordered = {} + Indices = {} } - for Index, Identifier in Identifiers do - if Generics[Identifier] then - Error.new(Error.AnalyzeDuplicateTypeGeneric, self.Source, `Duplicate type parameter "{Identifier}"`) - :Primary(Token, `Type parameter "{Identifier}" was already used`) - :Emit() + while true do + if self:TryConsume("CloseChevrons") then + break end - Generics.Ordered[Index] = {} - Generics.Keys[Identifier] = Index + local Index = (Generics.Total + 1) + local Token = self:Consume("Identifier") + local Identifier = Token.Value + + Generics.Total = Index + Generics.Keys[Index] = Identifier + Generics.Indices[Identifier] = Index + + if self:Peek().Type ~= "CloseChevrons" then + self:Consume("Comma") + end end self.Generics = Generics return Generics end +function Parser.SolveGenerics(self: Parser, Identifier: Token, Declaration: StructDeclaration) + local Generics = Declaration.Value.Generics + if not Generics then + return + end + + local OpenToken = self:TryConsume("OpenChevrons") + if not OpenToken then + Error.new(Error.AnalyzeInvalidGenerics, self.Source, `Type expects parameters list`) + :Primary(self:Peek(), "") + :Emit() + + return + end + + local Maximum = Generics.Total + local Parameters = {} + local References: {[string]: TypeDeclaration} = {} + + for Index, Generic in Generics.Keys do + local Type = self:Type(Identifier) + References[Generic] = Type + table.insert(Parameters, Type) + + if Index ~= Maximum then + self:Consume("Comma") + end + end + + local CloseToken = self:Consume("CloseChevrons") + if #Parameters ~= Maximum then + Error.new(Error.AnalyzeInvalidGenerics, self.Source, `Type expects {Maximum} parameters, but {#Parameters} are specified`) + :Primary({Start = OpenToken.Start, End = CloseToken.End}, "") + :Emit() + end + + --> Solve references + Declaration = Table.DeepClone(Declaration) + + local function SolveValues(Values: {TypeDeclaration}) + for Index, Field in Values do + if Field.Type == "Struct" then + SolveValues(Field.Value.Values) + continue + end + + if Field.Type ~= "Generic" then + continue + end + + local Value = Field.Value + local Generic = Value.Generic + + --> Create a unique reference for each generic + local Reference = References[Generic] + Reference = Table.DeepClone(Reference) + ;(Reference :: Declaration).Value.Identifier = Value.Identifier + + --> Set value to reference + Values[Index] = Reference + end + end + + Declaration.Value.Generics = nil + SolveValues(Declaration.Value.Values) + + return Declaration, Parameters +end + function Parser.GetTypeAttributes(self: Parser, Primitive: Settings.Primitive?): (Attributes, AttributesTokens) local function TryToParseOptional(): (boolean, Token?) local Token = self:TryConsume("Optional") @@ -585,6 +660,7 @@ function Parser.GetTypeAttributes(self: Parser, Primitive: Settings.Primitive?): } end + ---- Top-Level Parsing Functions ---- function Parser.Parse(self: Parser, Source: string): Body @@ -655,7 +731,7 @@ function Parser.Declarations(self: Parser): {Declaration} --> Prevent duplicates local Type = KEYWORDS[Keyword.Value] local Bucket: Bucket = BUCKETS[Type] - local Reference = self:GetReference(Bucket, Identifier) + local Reference = self:GetReference(Bucket, Identifier.Value) if Reference then Error.new(Error.AnalyzeDuplicateDeclaration, self.Source, `Duplicate declaration`) @@ -887,6 +963,13 @@ function Parser.Type(self: Parser, Identifier: Token, Keyword: Token?, IsDataFie Declaration = self:Primitive(Identifier) elseif Type == "Identifier" then Declaration = self:Reference(Identifier) + if Declaration.Type == "Reference" then + local Value = Declaration.Value + local Referencing = Value.Declaration + if Referencing.Value.Generics then + Value.Declaration, Value.Parameters = self:SolveGenerics(Identifier, Referencing :: StructDeclaration) + end + end elseif Type == "OpenParentheses" and IsDataField then Declaration = self:Tuple(Identifier) end @@ -905,7 +988,6 @@ function Parser.Type(self: Parser, Identifier: Token, Keyword: Token?, IsDataFie Value.Range, Value.Optional, Value.Array, Value.Components = Attributes.Range, Attributes.Optional, Attributes.Array, Attributes.Components Tokens.Range, Tokens.Optional, Tokens.Array = AttributesTokens.Range, AttributesTokens.Optional, AttributesTokens.Array - return Declaration end @@ -1082,7 +1164,8 @@ function Parser.Reference(self: Parser, Identifier: Token): (ReferenceDeclaratio --> Generics reference resolution local Generics = self.Generics - local Generic = Generics and Generics[Token.Value] + local Generic = Generics and Generics.Indices[Token.Value] + if Generic then return { Type = "Generic", @@ -1095,32 +1178,18 @@ function Parser.Reference(self: Parser, Identifier: Token): (ReferenceDeclaratio end --> Normal reference resolution - local Reference = self:GetReference("Types", Token) + local Reference = self:GetReference("Types", Token.Value) if not Reference then error(Error.new(Error.AnalyzeUnknownReference, self.Source, `Unknown reference`) :Primary(Token, "Unknown reference") :Emit()) end - local Declaration = (Reference :: Reference).Declaration - local TypeGenerics: Generics? = Declaration.Value.Generics - if TypeGenerics then - local ProvidedTypes = self:Consume("Component") - - local Value = ProvidedTypes.Value - Value = string.gsub(Value, "%s<>", "") - - local Identifiers = string.split(Value, ",") - for Index, Identifier in Identifiers do - local Reference = self:GetReference("Types", {}) - end - end - return { Type = "Reference", Value = { Identifier = Identifier.Value, - Declaration = (Reference :: Reference).Declaration :: TypeDeclaration + Declaration = Reference.Declaration }, Tokens = {} } :: ReferenceDeclaration diff --git a/test/Sources/Generics.txt b/test/Sources/Generics.txt index 6513d97..8dacfa7 100644 --- a/test/Sources/Generics.txt +++ b/test/Sources/Generics.txt @@ -5,7 +5,7 @@ struct Packet { Sequence: u16, Ack: u16, AckBits: u32, - Data: T + Data: T, } struct Entity { @@ -18,6 +18,5 @@ event Snapshot { From: Client, Type: Unreliable, Call: SingleSync, - Data: u8 - -- Data: Packet + Data: Packet } \ No newline at end of file diff --git a/test/Sources/Test.txt b/test/Sources/Test.txt index 8bd5397..6ac1d5e 100644 --- a/test/Sources/Test.txt +++ b/test/Sources/Test.txt @@ -25,6 +25,13 @@ struct ArrayStruct { }[0..20] +struct Generic { + Data: A, + Nested: { + Value: B + } +} + type Number = u8 struct Example { Field: u8, @@ -40,6 +47,13 @@ struct MapStruct { Map: {[string]: u8} } +event Generic { + From: Server, + Type: Reliable, + Data: Generic, + Call: SingleSync +} + event Unknown { Type: Reliable, From: Server, diff --git a/test/Test.luau b/test/Test.luau index 4e7b629..62575a1 100644 --- a/test/Test.luau +++ b/test/Test.luau @@ -414,6 +414,8 @@ FireAndExpect("Unreliable Client", Client.UnreliableClient, Server.UnreliableCli FireAndExpect("Unknown", Server.Unknown, Client.Unknown, newproxy()) +FireAndExpect("Generic", Server.Generic, Client.Generic, {Data = 0, Nested = {Value = 0}}) + FireAndExpect("Map", Server.MapEvent, Client.MapEvent, {["string"] = 1}) FireAndExpect("Map Struct", Server.MapStructEvent, Client.MapStructEvent, {Map = {string = 1}}) FireAndExpect("Map Complex", Server.MapComplexEvent, Client.MapComplexEvent, {string = table.create(8, 1)})