diff --git a/design/branched-generated-encoder.md b/design/branched-generated-encoder.md new file mode 100644 index 0000000..5bacc24 --- /dev/null +++ b/design/branched-generated-encoder.md @@ -0,0 +1,128 @@ +# Branched Generated Decoder + +Pasted here because Tebitea is down + +## The problem + +TAPE is designed so that the decoder can gloss over data it does not understand. +Technically the protocol allows for this, but I completely forgot to implement +this in the generated decoder, oops. This would be trivial if TAPE messages were +still flat tables, but they aren't, because those aren't useful enough. So, +let's analyze the problem. + +## When it happens + +There are two reasons something might not match up with the expected data: + +The first and most obvious is unrecognized keys. If the key is not in the set of +recognized keys for a KTV, it should leave the corresponding struct field blank. +Once #6 has been implemented, throw an error if the data was not optional. + +The second is wrong types. If we are expecting KTV and get SBA, we should leave +the data as empty. The aforementioned concern about #6 also applies here. We +don't need to worry about special cases at the structure root, because it would +be technically possible to make the structure root an option, so it really is +just a normal value. Until #6, we will leave that blank too. + +## Preliminary ideas + +The first is going to be pretty simple. All we need to do is have a skimmer +function that skims over TAPE data very, and then call that on the KTV value +each time we run into a mystery key. It should only return an error if the +structure of the data is malformed in such a way that it cannot continue to the +next one. This should be stored in the tape package alongside the dynamic +decoding functions, because they will essentially function the same way and +could probably share lots of code. + +The second is a bit more complicated because of the existence of KTV and OTA +because they are aggregate types. Go types work a bit differently, as if you +have an array of an array of an array of ints, that information is represented +in one place, whereas TAPE doesn't really do that. All of that information is +sort of buried within the data structure, so we don't know what we will be +decoding before we actually do it. Whenever we encounter a type we don't expect, +we would need to abort decoding of the entire data structure, and then skim over +whatever detritus is left, which would literally be in a half-decoded state. The +fact that the code is generated flat and thus cannot use return or defer +statements contributes to the complexity of this problem. We need to go up, but +we can't. There is no up, only forward. + +Of course, the dynamic decoder does not have this problem in the first place +because it doesn't expect anything, and constructs the destination to fit +whatever it sees in the TAPE structure as it is decoding it. KTVs are completely +dynamic because they are implemented as maps, so the only time it needs to +completely comprehend a type is with OTAs. There is a function called typeOf +that gets the type of the current tag and returns it as a reflect.Type, which +necessitates recursion and peeking at OTAs and their elements. + +We could try to do the same thing in the generated decoder, comparing the +determined type against the expected type to try to figure out whether we should +decode an array or a table, etc. This is immediately problematic as it requires +memory to be allocated, both for the peek buffer and the resulting tree of type +information. If we end up with some crazy way to keep track of the types, that's +only one half of the allocation problem and we would still be spending extra +cycles going over all of that twice. + +## Performance constraints + +The generated decoder is supposed to blaze through data, and it can't do that if +it does all the singing and dancing that the dynamic decoder does. It's time for +some performance constraints: + +- No allocations, except as required to build the destination for the data +- No redundant work +- So, no freaking peeking +- It should take well under 500 lines of generated code to decode one message of +reasonable size (i.e. be careful not to bloat the binary) + +I'm not really going to do my usual thing here of making a slow version and +speeding it up over time based on evidence and experimentation because these +constraints inform the design so much it would be impossible to continue without +them. I am 99% confident that these constraints will allow for an acceptable +baseline of performance (for generated code) and we can still profile and +micro-optimize later. This is good enough for me. +Heavy solution + +There is a solution that might work very well which involves completely redoing +the generated decoding code. We could create a function for every source type to +destination type mapping that exists in protocol, and then compose them all +together. The decoding methods for each message or type would be wrappers around +the correct function for their root TAPE -> Go type mapping. The main benefit of +this is it would make this problem a lot more manageable because the interface +points between the data would be represented by function boundaries. This would +allow the use of return and defer statements, and would allow more code sharing, +producing a smaller binary. Go would probably inline these where needed. + +Would this work? Probably. More investigation is required to make sure. I want +to stop re-writing things I don't need to. On the other hand, it is just the +decoder. + +## Light solution + +TODO: find a solution that satisfies the performance constraints, keeps the same +identical interface, and works off the same code. I am convinced this is doable, +and it might even allow us to extract more data from an unexpected structure. +However, continuing this way might introduce unmanageable complexity. It is +already a little unmanageable and I am just one pony (kind of). + +## Implementation + +Heavy solution is going to work here, applied to only the points of +`Generator.generateDecodeValue` where it decodes an aggregate data structure. +That way, only minimal amounts of code need to be redone. + +Whenever a branch needs to happen, a call shall be generated, a deferred +implementation request shall be added to a special FIFO queue within the +generator. After generating data structures and their root decoding functions, +the generator shall pick away at this queue until no requests remain. The +generator shall accept new items during this process, so that recursion is +possible. This is all to ensure it is only ever writing one function at a time + +The functions shall take a pointer to a type that accepts any type like (~) the +destination's base type. We should also probably just call +`Generator.generateDecodeValue` directly on user defined types this way, keeping +their public `Decode` methods just for convenience. + +The tape package shall contain a skimming function that takes a decoder and a +tag, and recursively consumes the decoder given the context of the tag. This +shall be utilized by the decoder functions to skip over values if their tags +or keys do not match up with what is expected. diff --git a/generate/generate.go b/generate/generate.go index 1e99f4c..0f21e37 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -6,6 +6,7 @@ import "maps" import "math" import "slices" import "strings" +import "encoding/hex" import "git.tebibyte.media/sashakoshka/hopp/tape" const imports = @@ -33,6 +34,19 @@ type Message interface { // Method returns the method code of the message. Method() uint16 } + +// canAssign determines if data from the given source tag can be assigned to +// a Go type represented by destination. It is designed to receive destination +// values from [generate.Generator.generateCanAssign]. The eventual Go type and +// the destination tag must come from the same (or hash-equivalent) PDL type. +func canAssign(destination, source tape.Tag) bool { + if destination.Is(source) { return true } + if (destination == tape.SBA || destination == tape.LBA) && + (source == tape.SBA || source == tape.LBA) { + return true + } + return false +} ` // Generator converts protocols into Go code. @@ -46,6 +60,14 @@ type Generator struct { nestingLevel int temporaryVar int protocol *Protocol + + decodeBranchRequestQueue []decodeBranchRequest +} + +type decodeBranchRequest struct { + hash [16]byte + typ Type + name string } func (this *Generator) Generate(protocol *Protocol) (n int, err error) { @@ -79,6 +101,14 @@ func (this *Generator) Generate(protocol *Protocol) (n int, err error) { n += nn; if err != nil { return n, err } } + // request queue + for { + hash, typ, name, ok := this.pullDecodeBranchRequest() + if !ok { break } + nn, err := this.generateDecodeBranch(hash, typ, name) + n += nn; if err != nil { return n, err } + } + return n, nil } @@ -146,7 +176,25 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) this.push() nn, err = this.iprintf("var nn int\n") n += nn; if err != nil { return n, err } - nn, err = this.generateDecodeValue(typ, "this", "tag", "return n, nil") + + nn, err = this.iprintf("if !(") + n += nn; if err != nil { return n, err } + nn, err = this.generateCanAssign(typ, "tag") + n += nn; if err != nil { return n, err } + nn, err = this.printf(") {\n") + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("nn, err = tape.Skim(decoder, tag)\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("return n, nil\n") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + nn, err = this.generateDecodeValue(typ, name, "this", "tag") n += nn; if err != nil { return n, err } nn, err = this.iprintf("return n, nil\n") n += nn; if err != nil { return n, err } @@ -217,20 +265,25 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() n += nn; if err != nil { return n, err } - abort := "return n, nil" // TODO: skip value somehow - nn, err = this.iprintf("if !tag.Is(") + + nn, err = this.iprintf("if !(") n += nn; if err != nil { return n, err } - nn, err = this.generateTN(message.Type) + nn, err = this.generateCanAssign(message.Type, "tag") n += nn; if err != nil { return n, err } nn, err = this.printf(") {\n") n += nn; if err != nil { return n, err } this.push() - nn, err = this.iprintf("%s\n", abort) + nn, err = this.iprintf("nn, err = tape.Skim(decoder, tag)\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("return n, nil\n") n += nn; if err != nil { return n, err } this.pop() nn, err = this.iprintf("}\n") n += nn; if err != nil { return n, err } - nn, err = this.generateDecodeValue(message.Type, "this", "tag", abort) + + nn, err = this.generateDecodeValue(message.Type, this.resolveMessageName(message.Name), "this", "tag") n += nn; if err != nil { return n, err } nn, err = this.iprintf("return n, nil\n") n += nn; if err != nil { return n, err } @@ -419,7 +472,10 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri // - n int // - err error // - nn int -func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource, abort string) (n int, err error) { +// +// The typeName paramterer is handled in the way described in the documentation +// for [Generator.generateDecodeBranch]. +func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagSource string) (n int, err error) { switch typ := typ.(type) { case TypeInt: // SI: (none) @@ -485,49 +541,7 @@ func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource, abo } case TypeArray: // OTA: * - lengthVar := this.newTemporaryVar("length") - nn, err := this.iprintf("var %s uint64\n", lengthVar) - n += nn; if err != nil { return n, err } - nn, err = this.iprintf( - "%s, nn, err = decoder.ReadUintN(int(%s.CN()))\n", - lengthVar, tagSource) - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("*%s = make(", valueSource) - n += nn; if err != nil { return n, err } - nn, err = this.generateType(typ) - n += nn; if err != nil { return n, err } - nn, err = this.printf(", int(%s))\n", lengthVar) - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("var itemTag tape.Tag\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("itemTag, nn, err = decoder.ReadTag()\n") - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("if !itemTag.Is(") - n += nn; if err != nil { return n, err } - nn, err = this.generateTN(typ.Element) - n += nn; if err != nil { return n, err } - nn, err = this.iprintf(") {\n") - n += nn; if err != nil { return n, err } - this.push() - nn, err = this.iprintf("%s\n", abort) - n += nn; if err != nil { return n, err } - this.pop() - nn, err = this.iprintf("}\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("for index := range %s {\n", lengthVar) - n += nn; if err != nil { return n, err } - this.push() - nn, err = this.generateDecodeValue( - typ.Element, - fmt.Sprintf("(&(*%s)[index])", valueSource), - "itemTag", abort) - n += nn; if err != nil { return n, err } - this.pop() - nn, err = this.iprintf("}\n") + nn, err := this.generateDecodeBranchCall(typ, typeName, valueSource, tagSource) n += nn; if err != nil { return n, err } case TypeTable: // KTV: ( )* @@ -539,72 +553,7 @@ func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource, abo n += nn; if err != nil { return n, err } case TypeTableDefined: // KTV: ( )* - lengthVar := this.newTemporaryVar("length") - nn, err := this.iprintf("var %s uint64\n", lengthVar) - n += nn; if err != nil { return n, err } - nn, err = this.iprintf( - "%s, nn, err = decoder.ReadUintN(int(%s.CN()))\n", - lengthVar, tagSource) - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("for _ = range %s {\n", lengthVar) - n += nn; if err != nil { return n, err } - this.push() - nn, err = this.iprintf("var key uint16\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("key, nn, err = decoder.ReadUint16()\n") - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("var itemTag tape.Tag\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("itemTag, nn, err = decoder.ReadTag()\n") - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("switch key {\n") - n += nn; if err != nil { return n, err } - keys := slices.Collect(maps.Keys(typ.Fields)) - slices.Sort(keys) - for _, key := range keys { - field := typ.Fields[key] - nn, err = this.iprintf("case 0x%04X:\n", key) - n += nn; if err != nil { return n, err } - this.push() - labelVar := this.newTemporaryVar("label") - fieldAbort := fmt.Sprintf("goto %s", labelVar) // TODO: skip value somehow - nn, err = this.iprintf("if !itemTag.Is(") - n += nn; if err != nil { return n, err } - nn, err = this.generateTN(field.Type) - n += nn; if err != nil { return n, err } - nn, err = this.printf(") {\n") - n += nn; if err != nil { return n, err } - this.push() - nn, err = this.iprintf("%s\n", fieldAbort) - n += nn; if err != nil { return n, err } - this.pop() - nn, err = this.iprintf("}\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("{\n") - n += nn; if err != nil { return n, err } - this.push() - nn, err = this.generateDecodeValue( - field.Type, - fmt.Sprintf("(&%s.%s)", valueSource, field.Name), - "itemTag", fieldAbort) - n += nn; if err != nil { return n, err } - this.pop() - nn, err = this.iprintf("}\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("%s:;\n", labelVar) - n += nn; if err != nil { return n, err } - this.pop() - } - nn, err = this.iprintf("}\n") - n += nn; if err != nil { return n, err } - this.pop() - nn, err = this.iprintf("}\n") + nn, err := this.generateDecodeBranchCall(typ, typeName, valueSource, tagSource) n += nn; if err != nil { return n, err } case TypeNamed: // WHATEVER: [WHATEVER] @@ -619,6 +568,263 @@ func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource, abo return n, nil } +// generateDecodeBranchCall generates code to call an aggregate decoder function, +// for a specified type. The definition of the function is deferred so no +// duplicates are created. The function overwrites memory pointed to by the +// variable (or parenthetical statement) specified by valueSource, and the value +// will be encoded according to the tag stored in the variable (or parenthetical +// statement) specified by tagSource. the code generated is a BLOCK and expects +// these variables to be defined: +// +// - decoder *tape.Decoder +// - n int +// - err error +// - nn int +// +// The typeName paramterer is handled in the way described in the documentation +// for [Generator.generateDecodeBranch]. +func (this *Generator) generateDecodeBranchCall(typ Type, typeName, valueSource, tagSource string) (n int, err error) { + hash := HashType(typ) + nn, err := this.iprintf( + "nn, err = %s(%s, decoder, %s)\n", + this.decodeBranchName(hash, typeName), valueSource, tagSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + this.pushDecodeBranchRequest(hash, typ, typeName) + return n, nil +} + +// generateDecodeBranch generates an aggregate decoder function definition for a +// specified type. It assumes that hash == HashType(typ). If typeName is not +// empty, it will be used as the type in the argument list instead of the result +// of [Generator.generateType]. +func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName string) (n int, err error) { + nn, err := this.iprintf("\nfunc %s(this *", this.decodeBranchName(hash, typeName)) + n += nn; if err != nil { return n, err } + if typeName == "" { + nn, err = this.generateType(typ) + n += nn; if err != nil { return n, err } + } else { + nn, err = this.print(typeName) + n += nn; if err != nil { return n, err } + } + nn, err = this.printf(", decoder *tape.Decoder, tag tape.Tag) (n int, err error) {\n") + n += nn; if err != nil { return n, err } + this.push() + + nn, err = this.iprintf("var nn int\n") + n += nn; if err != nil { return n, err } + + switch typ := typ.(type) { + case TypeArray: + // OTA: * + // read header + lengthVar := this.newTemporaryVar("length") + nn, err := this.iprintf("var %s uint64\n", lengthVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("%s, nn, err = decoder.ReadUintN(int(tag.CN()))\n", lengthVar) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + elementTagVar := this.newTemporaryVar("elementTag") + nn, err = this.iprintf("var %s tape.Tag\n", elementTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("%s, nn, err = decoder.ReadTag()\n", elementTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + + // abort macro + abort := func() (n int, err error) { + // skim entire array + nn, err = this.iprintf("for _ = range %s {\n", lengthVar) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("nn, err = tape.Skim(decoder, %s)\n", elementTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("return n, nil\n") + n += nn; if err != nil { return n, err } + return n, nil + } + + // validate header + // TODO: here, validate that length is less than the + // max, whatever that is configured to be. the reason we + // want to read it here is that we would have to skip + // the tag anyway so why not. + nn, err = this.iprintf("if !(") + n += nn; if err != nil { return n, err } + nn, err = this.generateCanAssign(typ.Element, elementTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.printf(") {\n") + n += nn; if err != nil { return n, err } + this.push() + nn, err = abort() + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // decode payloads + nn, err = this.iprintf("*this = make(") + n += nn; if err != nil { return n, err } + nn, err = this.generateType(typ) + n += nn; if err != nil { return n, err } + nn, err = this.printf(", %s)\n", lengthVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("for index := range int(%s) {\n", lengthVar) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.generateDecodeValue(typ.Element, "", "(&(*this)[index])", elementTagVar) + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + case TypeTableDefined: + // KTV: ( )* + // read header + lengthVar := this.newTemporaryVar("length") + nn, err := this.iprintf("var %s uint64\n", lengthVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("%s, nn, err = decoder.ReadUintN(int(tag.CN()))\n", lengthVar) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + + // validate header + // TODO: here, validate that length is less than the + // max, whatever that is configured to be. if not, stop + // ALL decoding. skimming huge big ass data could cause + // problems + + // read fields + nn, err = this.iprintf("for _ = range int(%s) {\n", lengthVar) + n += nn; if err != nil { return n, err } + this.push() + // read field header + fieldKeyVar := this.newTemporaryVar("fieldKey") + nn, err = this.iprintf("var %s uint16\n", fieldKeyVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("%s, nn, err = decoder.ReadUint16()\n", fieldKeyVar) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + fieldTagVar := this.newTemporaryVar("fieldTag") + nn, err = this.iprintf("var %s tape.Tag\n", fieldTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("%s, nn, err = decoder.ReadTag()\n", fieldTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + + // abort field macro + abortField := func() (n int, err error) { + nn, err = this.iprintf("tape.Skim(decoder, %s)\n", fieldTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("continue\n") + n += nn; if err != nil { return n, err } + return n, nil + } + + // switch on tag + nn, err = this.iprintf("switch %s {\n", fieldKeyVar) + n += nn; if err != nil { return n, err } + for _, key := range slices.Sorted(maps.Keys(typ.Fields)) { + field := typ.Fields[key] + nn, err = this.iprintf("case 0x%04X:\n", key) + n += nn; if err != nil { return n, err } + this.push() + + // validate field header + nn, err = this.iprintf("if !(") + n += nn; if err != nil { return n, err } + nn, err = this.generateCanAssign(field.Type, fieldTagVar) + n += nn; if err != nil { return n, err } + nn, err = this.printf(") {\n") + n += nn; if err != nil { return n, err } + this.push() + nn, err = abortField() + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // decode payload + nn, err = this.generateDecodeValue( + field.Type, "", + fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar) + n += nn; if err != nil { return n, err } + this.pop() + } + nn, err = this.iprintf("default:\n") + n += nn; if err != nil { return n, err } + this.push() + abortField() + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // TODO once options are implemented, have a set of + // bools for each non-optional field, and check here + // that they are all true. a counter will not work + // because if someone specifies a non-optional field + // twice, they can neglect to specify another + // non-optional field and we won't even know because the + // count will still be even. we shouldn't use a map + // either because its an allocation and its way more + // memory than just, like 5 bools (on the stack no less) + default: return n, fmt.Errorf("unexpected type: %T", typ) + } + + nn, err = this.iprintf("return n, nil\n") + + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + return n, nil +} + +func (this *Generator) decodeBranchName(hash [16]byte, name string) string { + if name == "" { + return fmt.Sprintf("decodeBranch_%s", hex.EncodeToString(hash[:])) + } else { + return fmt.Sprintf("decodeBranch_%s_%s", hex.EncodeToString(hash[:]), name) + } +} + +// pushDecodeBranchRequest pushes a new branch decode function request to the +// back of the queue, if it is not already in the queue. +func (this *Generator) pushDecodeBranchRequest(hash [16]byte, typ Type, name string) { + for _, item := range this.decodeBranchRequestQueue { + if item.hash == hash && item.name == name { return } + } + this.decodeBranchRequestQueue = append(this.decodeBranchRequestQueue, decodeBranchRequest { + hash: hash, + typ: typ, + name: name, + }) +} + +// pullDecodeBranchRequest pulls a branch decode function request from the front +// of the queue. +func (this *Generator) pullDecodeBranchRequest() (hash [16]byte, typ Type, name string, ok bool) { + if len(this.decodeBranchRequestQueue) < 1 { + return [16]byte { }, nil, "", false + } + request := this.decodeBranchRequestQueue[0] + this.decodeBranchRequestQueue = this.decodeBranchRequestQueue[1:] + return request.hash, request.typ, request.name, true +} + func (this *Generator) generateErrorCheck() (n int, err error) { return this.iprintf("n += nn; if err != nil { return n, err }\n") } @@ -781,6 +987,19 @@ func (this *Generator) generateTypeTableDefined(typ TypeTableDefined) (n int, er return n, nil } +// generateCanAssign generates an expression which checks if the tag specified +// by tagSource can be assigned to a Go destination generated from typ. The +// generated code is INLINE. +func (this *Generator) generateCanAssign(typ Type, tagSource string) (n int, err error) { + nn, err := this.printf("canAssign(") + n += nn; if err != nil { return n, err } + nn, err = this.generateTN(typ) + n += nn; if err != nil { return n, err } + nn, err = this.printf(", %s)", tagSource) + n += nn; if err != nil { return n, err } + return n, nil +} + func (this *Generator) validateIntBitSize(size int) error { switch size { case 5, 8, 16, 32, 64: return nil diff --git a/generate/protocol.go b/generate/protocol.go index 1c3ebc4..1610c86 100644 --- a/generate/protocol.go +++ b/generate/protocol.go @@ -1,5 +1,10 @@ package generate +import "fmt" +import "maps" +import "slices" +import "crypto/md5" + type Protocol struct { Messages map[uint16] Message Types map[string] Type @@ -11,7 +16,7 @@ type Message struct { } type Type interface { - + fmt.Stringer } type TypeInt struct { @@ -19,29 +24,84 @@ type TypeInt struct { Signed bool } +func (typ TypeInt) String() string { + output := "" + if typ.Signed { + output += "I" + } else { + output += "U" + } + output += fmt.Sprint(typ.Bits) + return output +} + type TypeFloat struct { Bits int } +func (typ TypeFloat) String() string { + return fmt.Sprintf("F%d", typ.Bits) +} + type TypeString struct { } +func (TypeString) String() string { + return "String" +} + type TypeBuffer struct { } +func (TypeBuffer) String() string { + return "Buffer" +} + type TypeArray struct { Element Type } +func (typ TypeArray) String() string { + return fmt.Sprintf("[]%v", typ.Element) +} + type TypeTable struct { } +func (TypeTable) String() string { + return "Table" +} + type TypeTableDefined struct { Fields map[uint16] Field } +func (typ TypeTableDefined) String() string { + output := "{" + for _, key := range slices.Sorted(maps.Keys(typ.Fields)) { + output += fmt.Sprintf("%04X %v", key, typ.Fields[key]) + } + output += "}" + return output +} + type Field struct { Name string Type Type } +func (field Field) String() string { + return fmt.Sprintf("%s %v", field.Name, field.Type) +} + type TypeNamed struct { Name string } + +func (typ TypeNamed) String() string { + return typ.Name +} + +func HashType(typ Type) [16]byte { + // TODO: if we ever want to make the compiler more efficient, this would + // be a good place to start, complex string concatenation in a hot path + // (sorta) + return md5.Sum([]byte(typ.String())) +}