From ce503c4689f22b565a26ef711d245115380dd100 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 20 Jun 2025 15:05:58 -0400 Subject: [PATCH] Big nasty commit to add code generation for encoding --- codec/decode.go | 25 +- codec/encode.go | 26 +- codec/measure.go | 11 + codec/measure_test.go | 21 ++ design/pdl-compiler.md | 19 +- generate/generate.go | 603 +++++++++++++++++++++++++++++++++++++++++ generate/parse.go | 23 +- tape/dynamic.go | 48 ++++ tape/tag.go | 45 +++ 9 files changed, 786 insertions(+), 35 deletions(-) create mode 100644 codec/measure_test.go create mode 100644 generate/generate.go create mode 100644 tape/dynamic.go create mode 100644 tape/tag.go diff --git a/codec/decode.go b/codec/decode.go index 1c27312..c4cdb9f 100644 --- a/codec/decode.go +++ b/codec/decode.go @@ -81,7 +81,7 @@ func (this *Decoder) ReadUint64() (value uint64, n int, err error) { n, err = this.ReadFull(buffer[:]) return uint64(buffer[0]) << 56 | uint64(buffer[1]) << 48 | - uint64(buffer[2]) << 48 | + uint64(buffer[2]) << 40 | uint64(buffer[3]) << 32 | uint64(buffer[4]) << 24 | uint64(buffer[5]) << 16 | @@ -89,14 +89,33 @@ func (this *Decoder) ReadUint64() (value uint64, n int, err error) { uint64(buffer[7]), n, err } +// ReadIntN decodes an N-byte signed integer from the input reader. +func (this *Decoder) ReadIntN(bytes int) (value int64, n int, err error) { + uncasted, n, err := this.ReadUintN(bytes) + return int64(uncasted), n, err +} + +// ReadUintN decodes an N-byte unsigned integer from the input reader. +func (this *Decoder) ReadUintN(bytes int) (value uint64, n int, err error) { + // TODO: don't make multiple read calls (without allocating) + buffer := [1]byte { } + for bytesLeft := bytes; bytesLeft > 0; bytesLeft -- { + nn, err := this.ReadFull(buffer[:]) + n += nn; if err != nil { return 0, n, err } + value |= uint64(buffer[0]) << ((bytesLeft - 1) * 8) + } + // *read* integers too big, but don't return them. + if bytes > 8 { value = 0 } + return value, n, nil +} + // ReadGBEU decodes a growing unsigned integer of up to 64 bits from the input // reader. func (this *Decoder) ReadGBEU() (value uint64, n int, err error) { var fullValue uint64 for { chunk, nn, err := this.ReadByte() - if err != nil { return 0, n, err } - n += nn + n += nn; if err != nil { return 0, n, err } fullValue *= 0x80 fullValue += uint64(chunk & 0x7F) diff --git a/codec/encode.go b/codec/encode.go index 2c96bba..125c11a 100644 --- a/codec/encode.go +++ b/codec/encode.go @@ -76,13 +76,31 @@ func (this *Encoder) WriteUint64(value uint64) (n int, err error) { }) } +// WriteIntN encodes an N-byte signed integer to the output writer. +func (this *Encoder) WriteIntN(value int64, bytes int) (n int, err error) { + return this.WriteUintN(uint64(value), bytes) +} + +// for below functions, increase buffers if go somehow gets support for over 64 +// bit integers. we could also make an expanding int type in goutil to use here, +// or maybe there is one in the stdlib. keep the int64 versions as well though +// because its ergonomic. + +// WriteUintN encodes an N-byte unsigned integer to the output writer. +func (this *Encoder) WriteUintN(value uint64, bytes int) (n int, err error) { + // TODO: don't make multiple write calls (without allocating) + buffer := [1]byte { } + for bytesLeft := bytes; bytesLeft > 0; bytesLeft -- { + buffer[0] = byte(buffer[0]) >> ((bytesLeft - 1) * 8) + nn, err := this.Write(buffer[:]) + n += nn; if err != nil { return n, err } + } + return n, nil +} + // EncodeGBEU encodes a growing unsigned integer of up to 64 bits to the output // writer. func (this *Encoder) EncodeGBEU(value uint64) (n int, err error) { - // increase if go somehow gets support for over 64 bit integers. we - // could also make an expanding int type in goutil to use here, or maybe - // there is one in the stdlib. keep this int64 version as well though - // because its ergonomic. buffer := [16]byte { } window := (GBEUSize(value) - 1) * 7 diff --git a/codec/measure.go b/codec/measure.go index 777a982..5c2e134 100644 --- a/codec/measure.go +++ b/codec/measure.go @@ -9,3 +9,14 @@ func GBEUSize(value uint64) int { if value == 0 { return length } } } + +// IntBytes returns the number of bytes required to hold a given unsigned +// integer. +func IntBytes(value uint64) int { + bytes := 0 + for value > 0 || bytes == 0 { + value >>= 8; + bytes ++ + } + return bytes +} diff --git a/codec/measure_test.go b/codec/measure_test.go new file mode 100644 index 0000000..a1f4291 --- /dev/null +++ b/codec/measure_test.go @@ -0,0 +1,21 @@ +package codec + +import "testing" + +func TestIntBytes(test *testing.T) { + if correct, got := 1, IntBytes(0); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 1, IntBytes(1); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 1, IntBytes(16); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 1, IntBytes(255); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 2, IntBytes(256); correct != got { + test.Fatal("wrong:", got) + } +} diff --git a/design/pdl-compiler.md b/design/pdl-compiler.md index 364961d..b87e3f3 100644 --- a/design/pdl-compiler.md +++ b/design/pdl-compiler.md @@ -56,20 +56,27 @@ static section. For each defined type, the compiler shall generate a Go type with the same name as written in its definition. The Go type shall be encodable, and shall have -`Encode` and `Decode` methods as described below. +`EncodeValue`, `DecodeValue`, and `Tag` methods as described below. ## Encoding and Decoding Methods -Each encodable type shall be given an `Encode` method and a `Decode` method, -which will take in a `codec.Encoder` and a `codec.Decoder` respectively. Both +Each message shall be given an `Encode` method and a `Decode` method, +which shall take in a `codec.Encoder` and a `codec.Decoder` respectively. Both return an `(int, error)` pair describing the amount of bytes written and an -error if the write stopped early. `Encode` will encode the data within the -message to the given encoder, and `Decode` will decode data from the given +error if the write stopped early. `Encode` shall encode the data within the +message to the given encoder, and `Decode` shall decode data from the given decoder and place it in the type's value. The methods shall not retain or close any encoders or decoders they are given. Both methods shall have pointer -receivers. In effect, these methods will satisfy `codec.Encodable` and +receivers. In effect, these methods shall satisfy `codec.Encodable` and `codec.Decodable`. +Each defined type shall be given an `EncodeValue` method and a `DecodeValue` +method, which shall both take in a `tape.Tag`, then a `codec.Encoder` and a +`codec.Decoder` respectively. These methods shall encode and decode the value +according to the CN given by the tag. The TN shall be ignored. The message shall +also have a method `Tag` that takes no arguments and returns the preferred tag +of the type including the TN and CN. + ## Connection The compiler shall generate a `Conn` struct which embeds a `hopp.Conn`, which diff --git a/generate/generate.go b/generate/generate.go new file mode 100644 index 0000000..e3e262b --- /dev/null +++ b/generate/generate.go @@ -0,0 +1,603 @@ +package generate + +import "io" +import "fmt" +import "maps" +import "math" +import "slices" +import "strings" +import "git.tebibyte.media/sashakoshka/hopp/codec" + +const imports = +` +import "git.teibibyte.media/sashakoshka/hopp/tape" +import "git.teibibyte.media/sashakoshka/hopp/codec" +` + +const preamble = ` +/* # Do not edit this package by hand! + * + * This file was automatically generated by the Holanet PDL compiler. The + * source file is located at + * Please edit that file instead, and re-compile it to this location. + * + * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz + */ +` + +const static = ` +// Table is a KTV table with an undefined schema. +type Table map[uint16] any + +// Message is any message that can be sent along this protocol. +type Message interface { + codec.Encodable + codec.Decodable + + // Method returns the method code of the message. + Method() uint16 +} +` + +// Generator converts protocols into Go code. +type Generator struct { + // Output is where the generated code will be sent. + Output io.Writer + // PackageName is the package name that will be used in the file. If + // left empty, the default is "protocol". + PackageName string + + nestingLevel int + protocol *Protocol +} + +func (this *Generator) Generate(protocol *Protocol) (n int, err error) { + this.nestingLevel = 0 + this.protocol = protocol + defer func() { this.protocol = nil }() + + // preamble and static section + packageName := "protocol" + if this.PackageName != "" { + packageName = this.PackageName + } + nn, err := this.iprintf("package %s\n", packageName) + n += nn; if err != nil { return n, err } + nn, err = this.print(preamble) + n += nn; if err != nil { return n, err } + nn, err = this.print(imports) + n += nn; if err != nil { return n, err } + nn, err = this.print(static) + n += nn; if err != nil { return n, err } + + // type definitions + for _, name := range slices.Sorted(maps.Keys(protocol.Types)) { + nn, err := this.generateTypedef(name, protocol.Types[name]) + n += nn; if err != nil { return n, err } + } + + // messages + for _, method := range slices.Sorted(maps.Keys(protocol.Messages)) { + nn, err := this.generateMessage(method, protocol.Messages[method]) + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) { + // type definition + nn, err := this.iprintf( + "\n// %s represents the protocol data type %s.\n", + name, name) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("type %s ", name) + n += nn; if err != nil { return n, err } + nn, err = this.generateType(typ) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + + // Tag method + // to be honest we probably don't need this method at all + // nn, err = this.iprintf("\n// Tag returns the preferred TAPE tag.\n") + // n += nn; if err != nil { return n, err } + // nn, err = this.iprintf("func (this *%s) Tag() tape.Tag {\n", name) + // n += nn; if err != nil { return n, err } + // this.push() + // nn, err = this.iprintf("return ") + // n += nn; if err != nil { return n, err } + // nn, err = this.generateTag(typ, "(*this)") + // n += nn; if err != nil { return n, err } + // nn, err = this.println() + // n += nn; if err != nil { return n, err } + // this.pop() + // nn, err = this.iprintf("}\n") + // n += nn; if err != nil { return n, err } + + // EncodeValue method + nn, err = this.iprintf( + "\n// EncodeValue encodes the value of this type without the " + + "tag. The value is\n// encoded according to the parameters " + + "specified by the tag, if possible.\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf( + "func (this *%s) EncodeValue(encoder *codec.Encoder, tag tape.Tag) (n int, err error) {\n", + name) + 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 } + nn, err = this.generateEncodeValue(typ, "(*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 } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // TODO DecodeValue method + + return n, nil +} + +func (this *Generator) generateMessage(method uint16, message Message) (n int, err error) { + nn, err := this.iprintf( + "\n// %s represents the protocol message M%04X %s.\n", + message.Name, method, message.Name) + nn, err = this.iprintf("type %s ", this.resolveMessageName(message.Name)) + n += nn; if err != nil { return n, err } + nn, err = this.generateType(message.Type) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + + // Encode method + nn, err = this.iprintf("\n// Encode encodes this message's tag and value.\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf( + "func(this %s) Encode(encoder *codec.Encoder) (n int, err error) {\n", + this.resolveMessageName(message.Name)) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("tag := ") + n += nn; if err != nil { return n, err } + nn, err = this.generateTag(message.Type, "(*this)") + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err := encoder.WriteUint8()\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.generateEncodeValue(message.Type, "(*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 } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // TODO decode method + + return n, nil +} + +func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource string) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + // SI: (none) + // LI: + if typ.Bits <= 5 { + // SI stores the value in the tag, so we write nothing here + break + } + nn, err := this.iprintf("nn, err = encoder.WriteInt%d(%s)\n", bitsToBytes(typ.Bits), valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + case TypeFloat: + // FP: + nn, err := this.iprintf("nn, err = encoder.WriteFloat%d(%s)\n", bitsToBytes(typ.Bits), valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + case TypeString: + // see TypeBuffer + nn, err := this.generateEncodeValue(TypeBuffer { }, valueSource, tagSource) + n += nn; if err != nil { return n, err } + case TypeBuffer: + // SBA: * + // LBA: * + nn, err := this.iprintf("if %s.Is(tape.LBA) {\n", tagSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + tagSource, valueSource) + 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", tagSource) + n += nn; if err != nil { return n, err } + + nn, err = this.iprintf("nn, err = encoder.Write([]byte(%s))\n", valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + case TypeArray: + // OTA: * + nn, err := this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + tagSource, valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + 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("itemTag := ") + 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.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("for _, item := range %s {\n", valueSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("tag := ") + n += nn; if err != nil { return n, err } + nn, err = this.generateTag(typ.Element, "item") + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag.Is(tape.SBA) { continue }\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag.CN() > itemTag.CN() { largest = tag }\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.iprintf("if itemTag.Is(tape.SBA) { itemTag += 1 << 5 }\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("for _, item := range %s {\n", valueSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.generateEncodeValue(typ.Element, "item", "itemTag") + 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") + n += nn; if err != nil { return n, err } + case TypeTable: + // KTV: ( )* + nn, err := this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + tagSource, valueSource) + 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 key, item := range %s {\n", valueSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("nn, err = encoder.WriteUint16(key)\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("tag := tape.TagAny(tag)\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(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("nn, err = tape.EncodeAny(tag)\n") + 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 } + case TypeTableDefined: + // KTV: ( )* + nn, err := this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), %d)\n", + tagSource, len(typ.Fields)) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + 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("var tag tape.Tag\n") + n += nn; if err != nil { return n, err } + for key, field := range typ.Fields { + nn, err = this.iprintf("nn, err = encoder.WriteUint16(0x%04X)\n", key) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag = ") + n += nn; if err != nil { return n, err } + fieldSource := fmt.Sprintf("%s.%s", valueSource, field.Name) + nn, err = this.generateTag(field.Type, fieldSource) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(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.generateEncodeValue(field.Type, fieldSource, "tag") + n += nn; if err != nil { return n, err } + } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + case TypeNamed: + // WHATEVER: [WHATEVER] + nn, err := this.iprintf("nn, err = %s.EncodeValue(encoder, %s)\n", valueSource, tagSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +func (this *Generator) generateErrorCheck() (n int, err error) { + return this.iprintf("n += nn; if err != nil { return n, err }\n") +} + +// generateTag generates the preferred TN and CN for the given type and value. +// The generated code is INLINE. +func (this *Generator) generateTag(typ Type, source string) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + if typ.Bits <= 5 { + nn, err := this.printf("tape.TagSI") + n += nn; if err != nil { return n, err } + } else { + nn, err := this.printf("tape.TagLI.WithCN(%d)", bitsToCN(typ.Bits)) + n += nn; if err != nil { return n, err } + } + case TypeFloat: + nn, err := this.printf("tape.TagFP.WithCN(%d)", bitsToCN(typ.Bits)) + n += nn; if err != nil { return n, err } + case TypeString: + nn, err := this.generateTag(TypeBuffer { }, source) + n += nn; if err != nil { return n, err } + case TypeBuffer: + nn, err := this.printf("bufferTag(%s)", source) + n += nn; if err != nil { return n, err } + case TypeArray: + nn, err := this.printf("arrayTag(tape.TagOTA.WithCN(tape.IntBytes(uint64(len(%s))))", source) + n += nn; if err != nil { return n, err } + case TypeTable: + nn, err := this.printf("tape.TagKTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) + n += nn; if err != nil { return n, err } + case TypeTableDefined: + nn, err := this.printf("tape.TagKTV.WithCN(%d)", codec.IntBytes(uint64(len(typ.Fields)))) + n += nn; if err != nil { return n, err } + case TypeNamed: + resolved, err := this.resolveTypeName(typ.Name) + if err != nil { return n, err } + nn, err := this.generateTag(resolved, source) + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +// generateTN generates the appropriate TN for the given type. The generated +// code is INLINE. The generated tag will have a CN as zero. For types that +// change TN based on their length, the TN capable of supporting more +// information is chosen. +func (this *Generator) generateTN(typ Type) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + if typ.Bits <= 5 { + nn, err := this.printf("tape.TagSI") + n += nn; if err != nil { return n, err } + } else { + nn, err := this.printf("tape.TagLI") + n += nn; if err != nil { return n, err } + } + case TypeFloat: + nn, err := this.printf("tape.TagFP",) + n += nn; if err != nil { return n, err } + case TypeString: + nn, err := this.generateTN(TypeBuffer { }) + n += nn; if err != nil { return n, err } + case TypeBuffer: + nn, err := this.printf("tape.TagLBA") + n += nn; if err != nil { return n, err } + case TypeArray: + nn, err := this.printf("tape.TagOTA") + n += nn; if err != nil { return n, err } + case TypeTable: + nn, err := this.printf("tape.TagKTV") + n += nn; if err != nil { return n, err } + case TypeTableDefined: + nn, err := this.printf("tape.TagKTV") + n += nn; if err != nil { return n, err } + case TypeNamed: + resolved, err := this.resolveTypeName(typ.Name) + if err != nil { return n, err } + nn, err := this.generateTN(resolved) + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +func (this *Generator) generateType(typ Type) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + if err := this.validateIntBitSize(typ.Bits); err != nil { + return n, err + } + if typ.Signed { + nn, err := this.printf("int%d", typ.Bits) + n += nn; if err != nil { return n, err } + } else { + nn, err := this.printf("uint%d", typ.Bits) + n += nn; if err != nil { return n, err } + } + case TypeFloat: + switch typ.Bits { + case 16: + nn, err := this.print("float32") + n += nn; if err != nil { return n, err } + case 32, 64: + nn, err := this.printf("float%d", typ.Bits) + n += nn; if err != nil { return n, err } + default: + return n, fmt.Errorf("floats of size %d are unsupported on this platform", typ.Bits) + } + case TypeString: + nn, err := this.print("string") + n += nn; if err != nil { return n, err } + case TypeBuffer: + nn, err := this.print("[]byte") + n += nn; if err != nil { return n, err } + case TypeArray: + nn, err := this.print("[]") + n += nn; if err != nil { return n, err } + nn, err = this.generateType(typ.Element) + n += nn; if err != nil { return n, err } + case TypeTable: + nn, err := this.print("Table") + n += nn; if err != nil { return n, err } + case TypeTableDefined: + nn, err := this.generateTypeTableDefined(typ) + n += nn; if err != nil { return n, err } + case TypeNamed: + actual, err := this.resolveTypeName(typ.Name) + if err != nil { return n, err } + nn, err := this.generateType(actual) + n += nn; if err != nil { return n, err } + } + return n, nil +} + +func (this *Generator) generateTypeTableDefined(typ TypeTableDefined) (n int, err error) { + nn, err := this.print("struct {\n") + n += nn; if err != nil { return n, err } + this.push() + + for _, key := range slices.Sorted(maps.Keys(typ.Fields)) { + field := typ.Fields[key] + nn, err := this.iprintf("%s ", field.Name) + n += nn; if err != nil { return n, err } + nn, err = this.generateType(field.Type) + n += nn; if err != nil { return n, err } + nn, err = this.print("\n") + n += nn; if err != nil { return n, err } + } + + this.pop() + nn, err = this.iprint("}") + n += nn; if err != nil { return n, err } + return n, nil +} + +func (this *Generator) validateIntBitSize(size int) error { + switch size { + case 8, 16, 32, 64: return nil + default: return fmt.Errorf("integers of size %d are unsupported on this platform", size) + } +} + +func (this *Generator) validateFloatBitSize(size int) error { + switch size { + case 16, 32, 64: return nil + default: return fmt.Errorf("floats of size %d are unsupported on this platform", size) + } +} + +func (this *Generator) push() { + this.nestingLevel ++ +} + +func (this *Generator) pop() { + if this.nestingLevel < 1 { + panic("cannot pop when nesting level is less than 1") + } + this.nestingLevel -- +} + +func (this *Generator) indent() string { + return strings.Repeat("\t", this.nestingLevel) +} + +func (this *Generator) print(args ...any) (n int, err error) { + return fmt.Fprint(this.Output, args...) +} + +func (this *Generator) println(args ...any) (n int, err error) { + return fmt.Fprintln(this.Output, args...) +} + +func (this *Generator) printf(format string, args ...any) (n int, err error) { + return fmt.Fprintf(this.Output, format, args...) +} + +func (this *Generator) iprint(args ...any) (n int, err error) { + return fmt.Fprint(this.Output, this.indent() + fmt.Sprint(args...)) +} + +func (this *Generator) iprintln(args ...any) (n int, err error) { + return fmt.Fprintln(this.Output, this.indent() + fmt.Sprint(args...)) +} + +func (this *Generator) iprintf(format string, args ...any) (n int, err error) { + return fmt.Fprintf(this.Output, this.indent() + format, args...) +} + +func (this *Generator) resolveMessageName(message string) string { + return "Message" + message +} + +func (this *Generator) resolveTypeName(name string) (Type, error) { + switch name { + case "U8": return TypeInt { Bits: 8 }, nil + case "U16": return TypeInt { Bits: 16 }, nil + case "U32": return TypeInt { Bits: 32 }, nil + case "U64": return TypeInt { Bits: 64 }, nil + case "U128": return TypeInt { Bits: 128 }, nil + case "U256": return TypeInt { Bits: 256 }, nil + case "I8": return TypeInt { Bits: 8, Signed: true }, nil + case "I16": return TypeInt { Bits: 16, Signed: true }, nil + case "I32": return TypeInt { Bits: 32, Signed: true }, nil + case "I64": return TypeInt { Bits: 64, Signed: true }, nil + case "I128": return TypeInt { Bits: 128, Signed: true }, nil + case "I256": return TypeInt { Bits: 256, Signed: true }, nil + case "F16": return TypeFloat { Bits: 16 }, nil + case "F32": return TypeFloat { Bits: 32 }, nil + case "F64": return TypeFloat { Bits: 64 }, nil + case "F128": return TypeFloat { Bits: 128 }, nil + case "F256": return TypeFloat { Bits: 256 }, nil + case "String": return TypeString { }, nil + case "Buffer": return TypeBuffer { }, nil + case "Table": return TypeTable { }, nil + } + + if typ, ok := this.protocol.Types[name]; ok { + if typ, ok := typ.(TypeNamed); ok { + return this.resolveTypeName(typ.Name) + } + + return typ, nil + } + return nil, fmt.Errorf("no type exists called %s", name) +} + +func bitsToBytes(bits int) int { + return int(math.Ceil(float64(bits) / 8.0)) +} + +func bitsToCN(bits int) int { + return bitsToBytes(bits) - 1 +} diff --git a/generate/parse.go b/generate/parse.go index 9d945e5..287a693 100644 --- a/generate/parse.go +++ b/generate/parse.go @@ -21,28 +21,7 @@ func Parse(lx parse.Lexer) (*Protocol, error) { func defaultProtocol() Protocol { return Protocol { Messages: make(map[uint16] Message), - Types: map[string] Type { - "U8": TypeInt { Bits: 8 }, - "U16": TypeInt { Bits: 16 }, - "U32": TypeInt { Bits: 32 }, - "U64": TypeInt { Bits: 64 }, - "U128": TypeInt { Bits: 128 }, - "U256": TypeInt { Bits: 256 }, - "I8": TypeInt { Bits: 8, Signed: true }, - "I16": TypeInt { Bits: 16, Signed: true }, - "I32": TypeInt { Bits: 32, Signed: true }, - "I64": TypeInt { Bits: 64, Signed: true }, - "I128": TypeInt { Bits: 128, Signed: true }, - "I256": TypeInt { Bits: 256, Signed: true }, - "F16": TypeFloat { Bits: 16 }, - "F32": TypeFloat { Bits: 32 }, - "F64": TypeFloat { Bits: 64 }, - "F128": TypeFloat { Bits: 128 }, - "F256": TypeFloat { Bits: 256 }, - "String": TypeString { }, - "Buffer": TypeBuffer { }, - "Table": TypeTable { }, - }, + Types: map[string] Type { }, } } diff --git a/tape/dynamic.go b/tape/dynamic.go new file mode 100644 index 0000000..4564e2e --- /dev/null +++ b/tape/dynamic.go @@ -0,0 +1,48 @@ +package tape + +import "fmt" +import "reflect" +import "git.tebibyte.media/sashakoshka/hopp/codec" + +// EncodeAny encodes an "any" value. Returns an error if the underlying type is +// unsupported. Supported types are: +// +// - int +// - int +// - uint +// - uint +// - string +// - [] +// - map[uint16] +func EncodeAny(encoder *codec.Encoder, value any) (Tag, error) { + // TODO +} + +// TagAny returns the correct tag for an "any" value. Returns an error if the +// underlying type is unsupported. See [EncodeAny] for a list of supported +// types. +func TagAny(value any) (Tag, error) { + // primitives + switch value := value.(type) { + case int, uint: return LI.WithCN(3), nil + case int8, uint8: return LI.WithCN(0), nil + case int16, uint16: return LI.WithCN(1), nil + case int32, uint32: return LI.WithCN(3), nil + case int64, uint64: return LI.WithCN(8), nil + case string: return bufferLenTag(len(value)), nil + case []byte: return bufferLenTag(len(value)), nil + } + + // aggregates + reflectType := reflect.TypeOf(value) + switch reflectType.Kind() { + case reflect.Slice: return OTA.WithCN(reflect.ValueOf(value).Len()), nil + case reflect.Array: return OTA.WithCN(reflectType.Len()), nil + case reflect.Map: + if reflectType.Key() == reflect.TypeOf(uint16(0)) { + return OTA.WithCN(reflect.ValueOf(value).Len()), nil + } + return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value) + } + return 0, fmt.Errorf("cannot encode type %T", value) +} diff --git a/tape/tag.go b/tape/tag.go new file mode 100644 index 0000000..fb603df --- /dev/null +++ b/tape/tag.go @@ -0,0 +1,45 @@ +package tape + +import "git.tebibyte.media/sashakoshka/hopp/codec" + +type Tag byte; const ( + SI Tag = 0 << 5 // Small integer + LI Tag = 1 << 5 // Large integer + FP Tag = 2 << 5 // Floating point + SBA Tag = 3 << 5 // Small byte array + LBA Tag = 4 << 5 // Large byte array + OTA Tag = 5 << 5 // One-tag array + KTV Tag = 6 << 5 // Key-tag-value table + TNMask Tag = 0xE0 // The entire TN bitfield + CNMask Tag = 0x20 // The entire CN bitfield + CNLimit Tag = 32 // All valid CNs are < CNLimit +) + +func (tag Tag) TN() int { + return int(tag >> 5) +} + +func (tag Tag) CN() int { + return int(tag & CNMask) +} + +func (tag Tag) WithCN(cn int) Tag { + return (tag & TNMask) | Tag(cn % 32) +} + +func (tag Tag) Is(other Tag) bool { + return tag.TN() == other.TN() +} + +// BufferTag returns the appropriate tag for a buffer. +func BufferTag(value []byte) Tag { + return bufferLenTag(len(value)) +} + +func bufferLenTag(length int) Tag { + if length < int(CNLimit) { + return SBA.WithCN(length) + } else { + return LBA.WithCN(codec.IntBytes(uint64(length))) + } +}