Merge pull request 'branched-generated-encoder' (#9) from branched-generated-encoder into message-size-increase

Reviewed-on: #9
This commit is contained in:
Sasha Koshka 2025-08-06 19:11:08 -06:00
commit a108e53cb6
3 changed files with 524 additions and 117 deletions

View File

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

View File

@ -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: <length: UN> <elementTag: tape.Tag> <values>*
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: <length: UN> (<key: U16> <tag: Tag> <value>)*
@ -539,72 +553,7 @@ func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource, abo
n += nn; if err != nil { return n, err }
case TypeTableDefined:
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
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: <length: UN> <elementTag: tape.Tag> <values>*
// 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: <length: UN> (<key: U16> <tag: Tag> <value>)*
// 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

View File

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