80 Commits

Author SHA1 Message Date
dc72cc2010 generate: Support LSI tags 2025-08-11 20:59:20 -04:00
0e03f84b8a generate: Update tests with new TNs 2025-08-11 20:59:10 -04:00
02196edf61 design: Change tag for signed PDL integers 2025-08-11 20:10:03 -04:00
1058615f6f tape: Do something when receiving an LSI tag 2025-08-11 18:40:56 -04:00
024edfa922 tape: Actually test decoding lol 2025-08-11 18:39:38 -04:00
fe973af99c tape: Test dynamic encoding and decoding of signed integers 2025-08-10 23:28:43 -04:00
52f0d6932e tape: Add encoding and decoding of signed integers 2025-08-10 23:28:25 -04:00
8e14a2c3f1 tape: Add LSI to Tag constants 2025-08-07 21:14:40 -04:00
4fbb70081a generate: Finish test sub-case for MessageNestedArray 2025-08-06 22:15:22 -04:00
a108e53cb6 Merge pull request 'branched-generated-encoder' (#9) from branched-generated-encoder into message-size-increase
Reviewed-on: #9
2025-08-06 19:11:08 -06:00
57c30ac669 generate: Generator compiles 2025-08-06 20:19:31 -04:00
a270c22cb9 generate: The generics idea didn't work, use type names instead 2025-08-06 19:39:41 -04:00
a99d4dee66 generate: Fix no return statement, unused variables 2025-08-06 19:07:57 -04:00
c18e251b4a generate: Convert int64 to int to satisfy range
This is a stupid fucking restriction
2025-08-06 18:58:54 -04:00
170f79c914 generate: Fix bad variable names 2025-08-06 18:48:17 -04:00
77c6b67d65 generate: Break line after continue statements 2025-08-06 18:41:37 -04:00
195d0f9725 generate: Pass decoder to branch functions 2025-08-06 18:40:28 -04:00
fa4f591126 generate: make branch functions generic, take in ~ of base type 2025-08-06 18:38:30 -04:00
12142706e1 generate: Fix syntax and formatting errors 2025-08-06 17:59:26 -04:00
30e9ead1ab generate: Do the same for <user-type>.Decode 2025-08-06 17:27:04 -04:00
1118b11bcd generate: Properly check assignment within <message>.Decode 2025-08-06 17:24:51 -04:00
7343cf5853 generate: Fix array element tag variable 2025-08-06 17:03:58 -04:00
a9f583d2e7 generate: Validate OTA tags properly 2025-08-06 17:02:33 -04:00
c4dd129fc5 generate: Decode tables (but don't validate their length yet) 2025-08-06 17:00:39 -04:00
2cbf58d558 generate: Decode arrays (but don't validate their length yet) 2025-08-05 06:22:27 -04:00
7dcfc08678 generate: Add "stub" for actually generating branch functions 2025-08-04 16:01:50 -04:00
711ac30486 generate: Add branch decode function request queue 2025-08-04 12:26:16 -04:00
b15c3aa76c generate: Implement Generator.generateDecodeBranch 2025-08-04 09:36:52 -04:00
a1bfae443c design: Add paragraph about how we need a skimming function in tape 2025-08-03 22:28:06 -04:00
df3fe1280d generate: Remove abort parameter 2025-08-03 22:27:14 -04:00
41b3376fa3 generate: Add Generator.generateDecodeBranch stub 2025-08-03 22:19:06 -04:00
fae702edfd generate: Add String functions, TypeHash function for types 2025-08-03 22:07:31 -04:00
c86f9b03f2 generated: Remove unneeded code 2025-08-03 21:00:24 -04:00
dcbfbe9141 design: Import issue from Tebitea (it is down) 2025-08-03 20:59:59 -04:00
40444ee2f4 generate: Add TODOs about a big problem... 2025-07-25 21:01:48 -04:00
59cc90166f generate: WIP test of nested arrays 2025-07-25 21:01:23 -04:00
f222fb02b7 generate: Gracefully discard unexpected data while decoding 2025-07-22 20:20:47 -04:00
6ecc33a46b generate: Fix TestGenerateRun when testing numbers 2025-07-21 16:51:33 -04:00
5d84636b55 tape: Add functions to encode and decode float16 2025-07-21 15:58:32 -04:00
f009a970cd generate: Fix another syntax error when reading floats 2025-07-21 15:00:57 -04:00
8b63166ba1 generate: Test encoding floating point values 2025-07-21 14:57:34 -04:00
3ef7de118b generate: Fix syntax error when reading floats 2025-07-21 14:56:58 -04:00
51ed6aed9f generate: Fix TestGenerateRun so it snakes around User tables 2025-07-21 14:32:33 -04:00
6017ac1fa3 generate: Encode SI properly 2025-07-21 14:11:40 -04:00
b8047585fb generate: Test integer encoding 2025-07-21 14:10:34 -04:00
ad3973dd9e generate: Test array encoding 2025-07-20 23:21:59 -04:00
0f626b2e93 generate: Fix array encoding not writing length or item tag properly 2025-07-20 23:19:21 -04:00
272e47224d generate: Make output of testGenerateRun easier to compare 2025-07-20 23:18:36 -04:00
2c57423838 generate: Fix encoding of table length field 2025-07-20 10:26:29 -04:00
e2b9e809a8 generate: Fix TestGenerateRun 2025-07-20 10:25:53 -04:00
7e8b272ef0 generate: And now for an actual use of testGenerateRun 2025-07-19 06:24:30 -04:00
a257902705 generate: Flush the buffer before trying to compare it 2025-07-17 14:47:23 -04:00
4955f66ad6 generate: testEncode uses snakes now 2025-07-17 14:06:05 -04:00
f646207ab1 generate: Use newTemporarVar in more places 2025-07-17 11:28:53 -04:00
b50a199842 generate: Fix the testEncode function 2025-07-17 11:19:57 -04:00
b826cbf83e generate: The test now compiles 2025-07-17 10:26:31 -04:00
b73f9fa7ce generate: Implement decoding (untested) 2025-07-16 22:57:12 -04:00
d3d7b07a74 generate: Emit stub for message decoding function 2025-07-12 20:01:42 -04:00
daa6a44179 generate: Generate Method method 2025-07-12 19:37:58 -04:00
af7669c783 generate: Fix more nonsense surrounding named types 2025-07-11 20:08:43 -04:00
2305814e10 tape: Add StringTag 2025-07-10 21:56:38 -04:00
5a3296a842 generate: Parse primitive types into actual types rather than named types 2025-07-10 21:53:21 -04:00
3bf365a7a9 generate: Fix more semantic errors in the generated code 2025-07-08 21:50:29 -04:00
e48be0bc15 generate: Fix more semantic issues with generated code 2025-07-08 21:32:34 -04:00
a210f6112c generate: Fix more semantic issues in generated code 2025-07-08 20:35:11 -04:00
9ff317d443 generate: Change comment so it gets detected by the regex for generated files 2025-07-08 15:44:24 -04:00
cdba8ee601 generate: Fix a bunch of semantic issues with the generated code 2025-07-08 14:52:05 -04:00
e75d7534c1 generate: Fix syntax errors in generated code 2025-07-08 12:01:21 -04:00
8a0ae9b03f generate: Update tests to account for new changes 2025-07-08 11:39:37 -04:00
9bc90b0e17 generate: What the fuck is a teibibyte 2025-07-08 11:39:18 -04:00
c70c23d137 generate: Fix testGenerateRun so that it actually works 2025-07-08 11:38:00 -04:00
a9d5bb83a2 generate: Add framework for testing output of generated code 2025-07-07 16:00:25 -04:00
f1df5fa84d generate: Add stub return to generateDecodeValue so it compiles 2025-07-07 15:13:18 -04:00
76a8f9444a tape: Ignore type names when encoding primitives using reflection 2025-07-05 22:10:55 -04:00
0f20c4cdab tape: Fix TestEncodeDecodeAny using int instead of uint32 2025-07-05 19:14:36 -04:00
1b82f2cd83 tape: Use tu.Describe() in tests 2025-07-05 18:47:57 -04:00
6ba70ed046 internal/testutil: Create function to closely examine any data 2025-07-05 18:47:08 -04:00
c118a4d7ef tape: Change name of test to TestEncodeDecodeAnyTable 2025-07-04 14:30:23 -04:00
877698d402 tape: Remove print statements 2025-07-04 14:18:56 -04:00
5989a82bee tape: Fix negative slice length 2025-07-04 14:18:30 -04:00
15 changed files with 1503 additions and 128 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/generate/test

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

@@ -7,12 +7,12 @@ PDL allows defining a protocol using HOPP and TAPE.
| Syntax | TN | CN | Description
| ---------- | ------- | -: | -----------
| I5 | SI | |
| I8 | LI | 0 |
| I16 | LI | 1 |
| I32 | LI | 3 |
| I64 | LI | 7 |
| I128[^2] | LI | 15 |
| I256[^2] | LI | 31 |
| I8 | LSI | 0 |
| I16 | LSI | 1 |
| I32 | LSI | 3 |
| I64 | LSI | 7 |
| I128[^2] | LSI | 15 |
| I256[^2] | LSI | 31 |
| U5 | SI | |
| U8 | LI | 0 |
| U16 | LI | 1 |

View File

@@ -6,22 +6,20 @@ import "maps"
import "math"
import "slices"
import "strings"
import "encoding/hex"
import "git.tebibyte.media/sashakoshka/hopp/tape"
const imports =
`
import "git.teibibyte.media/sashakoshka/hopp/tape"
import "git.tebibyte.media/sashakoshka/hopp/tape"
`
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 <path>
* Please edit that file instead, and re-compile it to this location.
*
* HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz
*/
// Code generated by the Holanet PDL compiler. DO NOT EDIT.
// The source file is located at <path>
// Please edit that file instead, and re-compile it to this location.
// HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz
`
const static = `
@@ -36,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.
@@ -47,7 +58,16 @@ type Generator struct {
PackageName string
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) {
@@ -81,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
}
@@ -97,7 +125,7 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error)
nn, err = this.println()
n += nn; if err != nil { return n, err }
// Tag method
// '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 }
@@ -146,7 +174,27 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error)
name)
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.generateDecodeValue(typ, "this", "tag")
nn, err = this.iprintf("var nn int\n")
n += nn; if err != nil { return n, err }
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 }
@@ -170,11 +218,20 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e
nn, err = this.println()
n += nn; if err != nil { return n, err }
// Method method
nn, err = this.iprintf("\n// Method returns the message's method number.\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf(
"func(this *%s) Method() uint16 { return 0x%04X }\n",
this.resolveMessageName(message.Name),
method)
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 *tape.Encoder) (n int, err error) {\n",
"func(this *%s) Encode(encoder *tape.Encoder) (n int, err error) {\n",
this.resolveMessageName(message.Name))
n += nn; if err != nil { return n, err }
this.push()
@@ -184,7 +241,7 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e
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")
nn, err = this.iprintf("nn, err := encoder.WriteTag(tag)\n")
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
@@ -196,7 +253,43 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e
nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err }
// TODO decode method
// Decode method
nn, err = this.iprintf("\n// Decode decodes this message's tag and value.\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf(
"func(this *%s) Decode(decoder *tape.Decoder) (n int, err error) {\n",
this.resolveMessageName(message.Name))
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.iprintf("tag, 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 !(")
n += nn; if err != nil { return n, err }
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("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.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 }
this.pop()
nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err }
return n, nil
}
@@ -214,19 +307,23 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e
func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource string) (n int, err error) {
switch typ := typ.(type) {
case TypeInt:
// SI: (none)
// LI: <value: IntN>
// SI: (none)
// LI/LSI: <value: IntN>
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)
prefix := "WriteUint"
if typ.Signed {
prefix = "WriteInt"
}
nn, err := this.iprintf("nn, err = encoder.%s%d(%s)\n", prefix, 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: <value: FloatN>
nn, err := this.iprintf("nn, err = encoder.WriteFloat%d(%s)\n", bitsToBytes(typ.Bits), valueSource)
nn, err := this.iprintf("nn, err = encoder.WriteFloat%d(%s)\n", typ.Bits, valueSource)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
@@ -241,13 +338,13 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
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)
"nn, err = encoder.WriteUintN(uint64(len(%s)), %s.CN())\n",
valueSource, tagSource)
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)
nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("nn, err = encoder.Write([]byte(%s))\n", valueSource)
@@ -257,8 +354,8 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
case TypeArray:
// OTA: <length: UN> <elementTag: tape.Tag> <values>*
nn, err := this.iprintf(
"nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n",
tagSource, valueSource)
"nn, err = encoder.WriteUintN(uint64(len(%s)), %s.CN())\n",
valueSource, tagSource)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
@@ -271,24 +368,33 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
n += nn; if err != nil { return n, err }
nn, err = this.println()
n += nn; if err != nil { return n, err }
// TODO: we don't have to do this for loop for some
// types such as integers because the CN will be the
// same
nn, err = this.iprintf("for _, item := range %s {\n", valueSource)
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.iprintf("_ = item\n")
n += nn; if err != nil { return n, err }
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")
nn, err = this.iprintf("if 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")
nn, err = this.iprintf("if tag.CN() > itemTag.CN() { itemTag = 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("nn, err = encoder.WriteTag(itemTag)\n")
n += nn; if err != nil { return n, err }
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
nn, err = this.iprintf("for _, item := range %s {\n", valueSource)
n += nn; if err != nil { return n, err }
this.push()
@@ -311,8 +417,8 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
case TypeTableDefined:
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
nn, err := this.iprintf(
"nn, err = encoder.WriteUintN(%s.CN(), %d)\n",
tagSource, len(typ.Fields))
"nn, err = encoder.WriteUintN(%d, %s.CN())\n",
len(typ.Fields), tagSource)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
@@ -349,8 +455,10 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
default:
panic(fmt.Errorf("unknown type: %T", typ))
}
return n, nil
}
@@ -364,8 +472,357 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
// - n int
// - err error
// - nn int
func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource string) (n int, err error) {
// TODO
//
// 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)
// LI/LSI: <value: IntN>
if typ.Bits <= 5 {
// SI stores the value in the tag
nn, err := this.iprintf("*%s = uint8(%s.CN())\n", valueSource, tagSource)
n += nn; if err != nil { return n, err }
break
}
prefix := "ReadUint"
if typ.Signed {
prefix = "ReadInt"
}
nn, err := this.iprintf("*%s, nn, err = decoder.%s%d()\n", valueSource, prefix, typ.Bits)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
case TypeFloat:
// FP: <value: FloatN>
nn, err := this.iprintf("*%s, nn, err = decoder.ReadFloat%d()\n", valueSource, typ.Bits)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
case TypeString, TypeBuffer:
// SBA: <data: U8>*
// LBA: <length: UN> <data: U8>*
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("if %s.Is(tape.LBA) {\n", tagSource)
n += nn; if err != nil { return n, err }
this.push()
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 }
this.pop()
nn, err = this.iprintf("} else {\n")
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.iprintf(
"%s = uint64(%s.CN())\n",
lengthVar, tagSource)
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("buffer := make([]byte, int(%s))\n", lengthVar)
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("nn, err = decoder.Read(buffer)\n")
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
if _, ok := typ.(TypeString); ok {
nn, err = this.iprintf("*%s = string(buffer)\n", valueSource)
n += nn; if err != nil { return n, err }
} else {
nn, err = this.iprintf("*%s = buffer\n", valueSource)
n += nn; if err != nil { return n, err }
}
case TypeArray:
// OTA: <length: UN> <elementTag: tape.Tag> <values>*
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>)*
nn, err := this.iprintf(
"nn, err = tape.DecodeAny(decoder, %s, %s)\n",
valueSource, tagSource)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
case TypeTableDefined:
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
nn, err := this.generateDecodeBranchCall(typ, typeName, valueSource, tagSource)
n += nn; if err != nil { return n, err }
case TypeNamed:
// WHATEVER: [WHATEVER]
nn, err := this.iprintf("nn, err = %s.DecodeValue(decoder, %s)\n", valueSource, tagSource)
n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err }
default:
panic(fmt.Errorf("unknown type: %T", typ))
}
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) {
@@ -378,35 +835,40 @@ 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")
nn, err := this.printf("tape.SI.WithCN(int(%s))", source)
n += nn; if err != nil { return n, err }
} else if typ.Signed {
nn, err := this.printf("tape.LSI.WithCN(%d)", bitsToCN(typ.Bits))
n += nn; if err != nil { return n, err }
} else {
nn, err := this.printf("tape.TagLI.WithCN(%d)", bitsToCN(typ.Bits))
nn, err := this.printf("tape.LI.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))
nn, err := this.printf("tape.FP.WithCN(%d)", bitsToCN(typ.Bits))
n += nn; if err != nil { return n, err }
case TypeString:
nn, err := this.generateTag(TypeBuffer { }, source)
nn, err := this.printf("tape.StringTag(%s)", source)
n += nn; if err != nil { return n, err }
case TypeBuffer:
nn, err := this.printf("bufferTag(%s)", source)
nn, err := this.printf("tape.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)
nn, err := this.printf("tape.OTA.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)
nn, err := this.printf("tape.KTV.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)", tape.IntBytes(uint64(len(typ.Fields))))
nn, err := this.printf("tape.KTV.WithCN(%d)", tape.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 }
default:
panic(fmt.Errorf("unknown type: %T", typ))
}
return n, nil
@@ -420,29 +882,32 @@ 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")
nn, err := this.printf("tape.SI")
n += nn; if err != nil { return n, err }
} else if typ.Signed {
nn, err := this.printf("tape.LSI")
n += nn; if err != nil { return n, err }
} else {
nn, err := this.printf("tape.TagLI")
nn, err := this.printf("tape.LI")
n += nn; if err != nil { return n, err }
}
case TypeFloat:
nn, err := this.printf("tape.TagFP",)
nn, err := this.printf("tape.FP",)
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")
nn, err := this.printf("tape.LBA")
n += nn; if err != nil { return n, err }
case TypeArray:
nn, err := this.printf("tape.TagOTA")
nn, err := this.printf("tape.OTA")
n += nn; if err != nil { return n, err }
case TypeTable:
nn, err := this.printf("tape.TagKTV")
nn, err := this.printf("tape.KTV")
n += nn; if err != nil { return n, err }
case TypeTableDefined:
nn, err := this.printf("tape.TagKTV")
nn, err := this.printf("tape.KTV")
n += nn; if err != nil { return n, err }
case TypeNamed:
resolved, err := this.resolveTypeName(typ.Name)
@@ -460,6 +925,11 @@ func (this *Generator) generateType(typ Type) (n int, err error) {
if err := this.validateIntBitSize(typ.Bits); err != nil {
return n, err
}
if typ.Bits <= 5 {
nn, err := this.printf("uint8")
n += nn; if err != nil { return n, err }
break
}
if typ.Signed {
nn, err := this.printf("int%d", typ.Bits)
n += nn; if err != nil { return n, err }
@@ -496,9 +966,7 @@ func (this *Generator) generateType(typ Type) (n int, err error) {
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)
nn, err := this.print(typ.Name)
n += nn; if err != nil { return n, err }
}
return n, nil
@@ -525,9 +993,22 @@ 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 8, 16, 32, 64: return nil
case 5, 8, 16, 32, 64: return nil
default: return fmt.Errorf("integers of size %d are unsupported on this platform", size)
}
}
@@ -582,30 +1063,7 @@ 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
}
func (this *Generator) resolveTypeName(name string) (Type, error) {
if typ, ok := this.protocol.Types[name]; ok {
if typ, ok := typ.(TypeNamed); ok {
return this.resolveTypeName(typ.Name)
@@ -616,6 +1074,11 @@ func (this *Generator) resolveTypeName(name string) (Type, error) {
return nil, fmt.Errorf("no type exists called %s", name)
}
func (this *Generator) newTemporaryVar(base string) string {
this.temporaryVar += 1
return fmt.Sprintf("%s_%d", base, this.temporaryVar)
}
func bitsToBytes(bits int) int {
return int(math.Ceil(float64(bits) / 8.0))
}

386
generate/generate_test.go Normal file
View File

@@ -0,0 +1,386 @@
package generate
// import "fmt"
import "strings"
import "testing"
import "git.tebibyte.media/sashakoshka/goparse"
var testGenerateCorrect =
`package protocol
/* # Do not edit this package by hand!
*
* This file was automatically generated by the Holanet PDL compiler. The
* source file is located at input.pdl
* Please edit that file instead, and re-compile it to this location.
*
* HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz
*/
import "git.tebibyte.media/sashakoshka/hopp/tape"
// 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 {
tape.Encodable
tape.Decodable
// Method returns the method code of the message.
Method() uint16
}
// User represents the protocol data type User.
type User struct {
Name string
Bio string
Followers uint32
}
// EncodeValue encodes the value of this type without the tag. The value is
// encoded according to the parameters specified by the tag, if possible.
func (this *User) EncodeValue(encoder *tape.Encoder) (n int, err error) {
nn, err := tape.WriteTableHeader(2)
n += nn; if err != nil { return n, err }
nn, err := encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
nn, err := tape.WriteString(encoder, this.Name)
n += nn; if err != nil { return n, err }
nn, err := encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
nn, err := tape.WriteString(encoder, this.Bio)
n += nn; if err != nil { return n, err }
return n, nil
}
// Decode replaces the data in this User with information from the decoder.
func (this *User) Decode(decoder *tape.Decoder) (n int, err error) {
pull, nn, err := tape.ReadTableHeader(decoder)
n += nn; if err != nil { return n, err }
for {
key, tag, end, nn, err := pull()
n += nn; if err != nil { return n, err }
if end { break }
switch key {
case 0x0000:
value, nn, err := tape.ReadString(decoder)
n += nn; if err != nil { return n, err }
this.Name = value
case 0x0001:
value, nn, err := tape.ReadString(decoder)
n += nn; if err != nil { return n, err }
this.Bio = value
}
}
return n, nil
}
// MessageConnect represents the protocol message M0000 Connect.
type MessageConnect struct {
Name string
Password string
}
// Method returns the method code, M0000.
func (this *MessageConnect) Method() uint16 {
return 0x0000
}
// Encode encodes the message to the encoder.
func (this *MessageConnect) Encode(encoder *tape.Encoder) (n int, err error) {
nn, err := tape.WriteTableHeader(2)
n += nn; if err != nil { return n, err }
nn, err := encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
nn, err := tape.WriteString(encoder, this.Name)
n += nn; if err != nil { return n, err }
nn, err := encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
nn, err := tape.WriteString(encoder, this.Password)
n += nn; if err != nil { return n, err }
return n, nil
}
// Decode replaces the data in this message with information from the decoder.
func (this *MessageConnect) Decode(decoder *tape.Decoder) (n int, err error) {
pull, nn, err := tape.ReadTableHeader(decoder)
n += nn; if err != nil { return n, err }
for {
key, tag, end, nn, err := pull()
n += nn; if err != nil { return n, err }
if end { break }
switch key {
case 0x0000:
value, nn, err := tape.ReadString(decoder)
n += nn; if err != nil { return n, err }
this.Name = value
case 0x0001:
value, nn, err := tape.ReadString(decoder)
n += nn; if err != nil { return n, err }
this.Password = value
}
}
return n, nil
}
// MessageUserList represents the protocol message M0001 UserList.
type MessageUserList struct {
Users []User
}
// Method returns the method code, M0001.
func (this *MessageUserList) Method() uint16 {
return 0x0001
}
// TODO methods
`
func TestGenerate(test *testing.T) {
protocol := defaultProtocol()
protocol.Messages[0x0000] = Message {
Name: "Connect",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Password", Type: TypeString { } },
},
},
}
protocol.Messages[0x0001] = Message {
Name: "UserList",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } },
},
},
}
protocol.Types["User"] = TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
},
}
correct := testGenerateCorrect
builder := strings.Builder { }
generator := Generator { Output: &builder }
/* TODO test n: */ _, err := generator.Generate(&protocol)
if err != nil { test.Fatal(parse.Format(err)) }
got := builder.String()
test.Log("CORRECT:")
test.Log(correct)
test.Log("GOT:")
test.Log(got)
if correct != got {
test.Error("not equal")
for index := range min(len(correct), len(got)) {
if correct[index] == got[index] { continue }
test.Log("C:", correct[max(0, index - 8):min(len(correct), index + 8)])
test.Log("G:", got[max(0, index - 8):min(len(got), index + 8)])
break
}
test.FailNow()
}
}
func TestGenerateRun(test *testing.T) {
protocol := defaultProtocol()
protocol.Messages[0x0000] = Message {
Name: "Connect",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Password", Type: TypeString { } },
},
},
}
protocol.Messages[0x0001] = Message {
Name: "UserList",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } },
},
},
}
protocol.Messages[0x0002] = Message {
Name: "Pulse",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Index", Type: TypeInt { Bits: 5 } },
0x0001: Field { Name: "Offset", Type: TypeInt { Bits: 16, Signed: true }},
0x0002: Field { Name: "X", Type: TypeFloat { Bits: 16 }},
0x0003: Field { Name: "Y", Type: TypeFloat { Bits: 32 }},
0x0004: Field { Name: "Z", Type: TypeFloat { Bits: 64 }},
},
},
}
protocol.Messages[0x0003] = Message {
Name: "NestedArray",
Type: TypeArray { Element: TypeArray { Element: TypeInt { Bits: 8 } } },
}
protocol.Messages[0x0004] = Message {
Name: "Integers",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "U5", Type: TypeInt { Bits: 5 } },
0x0001: Field { Name: "U8", Type: TypeInt { Bits: 8 } },
0x0002: Field { Name: "U16", Type: TypeInt { Bits: 16 } },
0x0003: Field { Name: "U32", Type: TypeInt { Bits: 32 } },
0x0004: Field { Name: "U64", Type: TypeInt { Bits: 64 } },
0x0006: Field { Name: "I8", Type: TypeInt { Bits: 8, Signed: true } },
0x0007: Field { Name: "I16", Type: TypeInt { Bits: 16, Signed: true } },
0x0008: Field { Name: "I32", Type: TypeInt { Bits: 32, Signed: true } },
0x0009: Field { Name: "I64", Type: TypeInt { Bits: 64, Signed: true } },
0x000B: Field { Name: "NI8", Type: TypeInt { Bits: 8, Signed: true } },
0x000C: Field { Name: "NI16",Type: TypeInt { Bits: 16, Signed: true } },
0x000D: Field { Name: "NI32",Type: TypeInt { Bits: 32, Signed: true } },
0x000E: Field { Name: "NI64",Type: TypeInt { Bits: 64, Signed: true } },
},
},
}
protocol.Types["User"] = TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
},
}
testGenerateRun(test, &protocol, `
// imports
`, `
// test case
log.Println("MessageConnect")
messageConnect := MessageConnect {
Name: "rarity",
Password: "gems",
}
testEncode(
&messageConnect,
tu.S(0xE1, 0x02).AddVar(
[]byte { 0x00, 0x00, 0x86, 'r', 'a', 'r', 'i', 't', 'y' },
[]byte { 0x00, 0x01, 0x84, 'g', 'e', 'm', 's' },
))
log.Println("MessageUserList")
messageUserList := MessageUserList {
Users: []User {
User {
Name: "rarity",
Bio: "asdjads",
Followers: 0x324,
},
User {
Name: "deez nuts",
Bio: "logy",
Followers: 0x8000,
},
User {
Name: "creekflow",
Bio: "im creekflow",
Followers: 0x3894,
},
},
}
testEncode(
&messageUserList,
tu.S(0xE1, 0x01, 0x00, 0x00,
0xC1, 0x03, 0xE1,
).Add(0x03).AddVar(
[]byte { 0x00, 0x00, 0x86, 'r', 'a', 'r', 'i', 't', 'y' },
[]byte { 0x00, 0x01, 0x87, 'a', 's', 'd', 'j', 'a', 'd', 's' },
[]byte { 0x00, 0x02, 0x23, 0x00, 0x00, 0x03, 0x24 },
).Add(0x03).AddVar(
[]byte { 0x00, 0x00, 0x89, 'd', 'e', 'e', 'z', ' ', 'n', 'u', 't', 's' },
[]byte { 0x00, 0x01, 0x84, 'l', 'o', 'g', 'y' },
[]byte { 0x00, 0x02, 0x23, 0x00, 0x00, 0x80, 0x00 },
).Add(0x03).AddVar(
[]byte { 0x00, 0x00, 0x89, 'c', 'r', 'e', 'e', 'k', 'f', 'l', 'o', 'w' },
[]byte { 0x00, 0x01, 0x8C, 'i', 'm', ' ', 'c', 'r', 'e', 'e', 'k', 'f',
'l', 'o', 'w' },
[]byte { 0x00, 0x02, 0x23, 0x00, 0x00, 0x38, 0x94 },
))
log.Println("MessagePulse")
messagePulse := MessagePulse {
Index: 9,
Offset: -0x3521,
X: 45.389,
Y: 294.1,
Z: 384729384.234892034,
}
testEncode(
&messagePulse,
tu.S(0xE1, 0x05).AddVar(
[]byte { 0x00, 0x00, 0x09 },
[]byte { 0x00, 0x01, 0x41, 0xCA, 0xDF },
[]byte { 0x00, 0x02, 0x61, 0x51, 0xAC },
[]byte { 0x00, 0x03, 0x63, 0x43, 0x93, 0x0C, 0xCD },
[]byte { 0x00, 0x04, 0x67, 0x41, 0xB6, 0xEE, 0x81, 0x28, 0x3C, 0x21, 0xE2 },
))
log.Println("MessageNestedArray")
uint8s := func(n int) []uint8 {
array := make([]uint8, n)
for index := range array {
array[index] = uint8(index + 1) | 0xF0
}
return array
}
messageNestedArray := MessageNestedArray {
uint8s(6),
uint8s(35),
}
testEncode(
&messageNestedArray,
tu.S(0xC1, 0x02, 0xC1,
0x06, 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
35, 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC,
0xFD, 0xFE, 0xFF, 0xF0, 0xF1, 0xF2,
0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE,
0xFF, 0xF0, 0xF1, 0xF2, 0xF3))
log.Println("MessageIntegers")
messageIntegers := MessageIntegers {
U5: 0x13,
U8: 0xC9,
U16: 0x34C9,
U32: 0x10E134C9,
U64: 0x639109BC10E134C9,
I8: 0x35,
I16: 0x34C9,
I32: 0x10E134C9,
I64: 0x639109BC10E134C9,
NI8: -0x35,
NI16: -0x34C9,
NI32: -0x10E134C9,
NI64: -0x639109BC10E134C9,
}
testEncode(
&messageIntegers,
tu.S(0xE1, 13).AddVar(
[]byte { 0x00, 0x00, 0x13 },
[]byte { 0x00, 0x01, 0x20, 0xC9 },
[]byte { 0x00, 0x02, 0x21, 0x34, 0xC9 },
[]byte { 0x00, 0x03, 0x23, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x04, 0x27, 0x63, 0x91, 0x09, 0xBC, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x06, 0x40, 0x35 },
[]byte { 0x00, 0x07, 0x41, 0x34, 0xC9 },
[]byte { 0x00, 0x08, 0x43, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x09, 0x47, 0x63, 0x91, 0x09, 0xBC, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x0B, 0x40, 0xCB },
[]byte { 0x00, 0x0C, 0x41, 0xCB, 0x37 },
[]byte { 0x00, 0x0D, 0x43, 0xEF, 0x1E, 0xCB, 0x37 },
[]byte { 0x00, 0x0E, 0x47, 0x9C, 0x6E, 0xF6, 0x43, 0xEF, 0x1E, 0xCB, 0x37 },
))
`)
}

73
generate/misc_test.go Normal file
View File

@@ -0,0 +1,73 @@
package generate
import "os"
import "fmt"
import "os/exec"
import "testing"
import "path/filepath"
func testGenerateRun(test *testing.T, protocol *Protocol, imports string, testCase string) {
// reset data directory
dir := "test/generate-run"
err := os.RemoveAll(dir)
if err != nil { test.Fatal(err) }
err = os.MkdirAll(dir, 0750)
if err != nil { test.Fatal(err) }
// open files
sourceFile, err := os.Create(filepath.Join(dir, "protocol.go"))
if err != nil { test.Fatal(err) }
defer sourceFile.Close()
mainFile, err := os.Create(filepath.Join(dir, "main.go"))
if err != nil { test.Fatal(err) }
defer mainFile.Close()
// generate protocol
generator := Generator {
Output: sourceFile,
PackageName: "main",
}
_, err = generator.Generate(protocol)
if err != nil { test.Fatal(err) }
// build static source files
imports = `
import "log"
import "bytes"
import "git.tebibyte.media/sashakoshka/hopp/tape"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
` + imports
setup := `log.Println("*** BEGIN TEST CASE OUTPUT ***")`
teardown := `log.Println("--- END TEST CASE OUTPUT ---")`
static := `
func testEncode(message Message, correct tu.Snake) {
buffer := bytes.Buffer { }
encoder := tape.NewEncoder(&buffer)
n, err := message.Encode(encoder)
if err != nil { log.Fatalf("at %d: %v\n", n, err) }
encoder.Flush()
got := buffer.Bytes()
log.Printf("got: [%s]", tu.HexBytes(got))
log.Println("correct:", correct)
if n != len(got) {
log.Fatalf("n incorrect: %d != %d\n", n, len(got))
}
if ok, n := correct.Check(got); !ok {
log.Fatalln("not equal at", n)
}
}
`
fmt.Fprintf(
mainFile, "package main\n%s\nfunc main() {\n%s\n%s\n%s\n}\n%s",
imports, setup, testCase, teardown, static)
// build and run test
command := exec.Command("go", "run", "./generate/test/generate-run")
workingDirAbs, err := filepath.Abs("..")
if err != nil { test.Fatal(err) }
command.Dir = workingDirAbs
command.Env = os.Environ()
output, err := command.CombinedOutput()
test.Logf("output of %v:\n%s", command, output)
if err != nil { test.Fatal(err) }
}

View File

@@ -95,6 +95,28 @@ func (this *parser) parseType() (Type, error) {
switch this.Kind() {
case TokenIdent:
switch this.Value() {
case "U8": return TypeInt { Bits: 8 }, this.Next()
case "U16": return TypeInt { Bits: 16 }, this.Next()
case "U32": return TypeInt { Bits: 32 }, this.Next()
case "U64": return TypeInt { Bits: 64 }, this.Next()
case "U128": return TypeInt { Bits: 128 }, this.Next()
case "U256": return TypeInt { Bits: 256 }, this.Next()
case "I8": return TypeInt { Bits: 8, Signed: true }, this.Next()
case "I16": return TypeInt { Bits: 16, Signed: true }, this.Next()
case "I32": return TypeInt { Bits: 32, Signed: true }, this.Next()
case "I64": return TypeInt { Bits: 64, Signed: true }, this.Next()
case "I128": return TypeInt { Bits: 128, Signed: true }, this.Next()
case "I256": return TypeInt { Bits: 256, Signed: true }, this.Next()
case "F16": return TypeFloat { Bits: 16 }, this.Next()
case "F32": return TypeFloat { Bits: 32 }, this.Next()
case "F64": return TypeFloat { Bits: 64 }, this.Next()
case "F128": return TypeFloat { Bits: 128 }, this.Next()
case "F256": return TypeFloat { Bits: 256 }, this.Next()
case "String": return TypeString { }, this.Next()
case "Buffer": return TypeBuffer { }, this.Next()
case "Table": return TypeTable { }, this.Next()
}
return this.parseTypeNamed()
case TokenLBracket:
return this.parseTypeArray()

View File

@@ -11,8 +11,8 @@ func TestParse(test *testing.T) {
Name: "Connect",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } },
0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } },
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Password", Type: TypeString { } },
},
},
}
@@ -26,9 +26,9 @@ func TestParse(test *testing.T) {
}
correct.Types["User"] = TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } },
0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } },
0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } },
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
},
}
test.Log("CORRECT:", &correct)

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

View File

@@ -3,6 +3,7 @@ package testutil
import "fmt"
import "slices"
import "strings"
import "reflect"
// Snake lets you compare blocks of data where the ordering of certain parts may
// be swapped every which way. It is designed for comparing the encoding of
@@ -92,3 +93,69 @@ func HexBytes(data []byte) string {
}
return out.String()
}
// Describe returns a string representing the type and data of the given value.
func Describe(value any) string {
desc := describer { }
desc.describe(reflect.ValueOf(value))
return desc.String()
}
type describer struct {
strings.Builder
indent int
}
func (this *describer) describe(value reflect.Value) {
value = reflect.ValueOf(value.Interface())
switch value.Kind() {
case reflect.Array, reflect.Slice:
this.printf("[\n")
this.indent += 1
for index := 0; index < value.Len(); index ++ {
this.iprintf("")
this.describe(value.Index(index))
this.iprintf("\n")
}
this.indent -= 1
this.iprintf("]")
case reflect.Struct:
this.printf("struct {\n")
this.indent += 1
typ := value.Type()
for index := range typ.NumField() {
indexBuffer := [1]int { index }
this.iprintf("%s: ", typ.Field(index).Name)
this.describe(value.FieldByIndex(indexBuffer[:]))
this.iprintf("\n")
}
this.indent -= 1
this.iprintf("}\n")
case reflect.Map:
this.printf("map {\n")
this.indent += 1
iter := value.MapRange()
for iter.Next() {
this.iprintf("")
this.describe(iter.Key())
this.printf(": ")
this.describe(iter.Value())
this.iprintf("\n")
}
this.indent -= 1
this.iprintf("}\n")
case reflect.Pointer:
this.printf("& ")
this.describe(value.Elem())
default:
this.printf("<%v %v>", value.Type(), value.Interface())
}
}
func (this *describer) printf(format string, v ...any) {
fmt.Fprintf(this, format, v...)
}
func (this *describer) iprintf(format string, v ...any) {
fmt.Fprintf(this, strings.Repeat("\t", this.indent) + format, v...)
}

View File

@@ -112,6 +112,13 @@ func (this *Decoder) ReadUintN(bytes int) (value uint64, n int, err error) {
return value, n, nil
}
// ReadFloat16 decodes a 16-bit floating point value from the input reader.
func (this *Decoder) ReadFloat16() (value float32, n int, err error) {
bits, nn, err := this.ReadUint16()
n += nn; if err != nil { return 0, n, err }
return math.Float32frombits(f16bitsToF32bits(bits)), n, nil
}
// ReadFloat32 decldes a 32-bit floating point value from the input reader.
func (this *Decoder) ReadFloat32() (value float32, n int, err error) {
bits, nn, err := this.ReadUint32()
@@ -132,3 +139,54 @@ func (this *Decoder) ReadTag() (value Tag, n int, err error) {
n += nn; if err != nil { return 0, n, err }
return Tag(uncasted), n, nil
}
// f16bitsToF32bits returns uint32 (float32 bits) converted from specified uint16.
// Taken from https://github.com/x448/float16/blob/v0.8.4/float16
//
// MIT License
//
// Copyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
func f16bitsToF32bits(in uint16) uint32 {
// All 65536 conversions with this were confirmed to be correct
// by Montgomery Edwards⁴⁴⁸ (github.com/x448).
sign := uint32(in&0x8000) << 16 // sign for 32-bit
exp := uint32(in&0x7c00) >> 10 // exponenent for 16-bit
coef := uint32(in&0x03ff) << 13 // significand for 32-bit
if exp == 0x1f {
if coef == 0 {
// infinity
return sign | 0x7f800000 | coef
}
// NaN
return sign | 0x7fc00000 | coef
}
if exp == 0 {
if coef == 0 {
// zero
return sign
}
// normalize subnormal numbers
exp++
for coef&0x7f800000 == 0 {
coef <<= 1
exp--
}
coef &= 0x007fffff
}
return sign | ((exp + (0x7f - 0xf)) << 23) | coef
}

View File

@@ -12,6 +12,7 @@ import "fmt"
import "reflect"
var dummyMap map[uint16] any
var dummyBuffer []byte
// EncodeAny encodes an "any" value. Returns an error if the underlying type is
// unsupported. Supported types are:
@@ -24,26 +25,27 @@ var dummyMap map[uint16] any
// - []<supported type>
// - map[uint16]<supported type>
func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
// TODO use reflection for all of this to ignore type names
// primitives
switch value := value.(type) {
case int: return encoder.WriteInt32(int32(value))
case uint: return encoder.WriteUint32(uint32(value))
case int8: return encoder.WriteInt8(value)
case uint8: return encoder.WriteUint8(value)
case int16: return encoder.WriteInt16(value)
case uint16: return encoder.WriteUint16(value)
case int32: return encoder.WriteInt32(value)
case uint32: return encoder.WriteUint32(value)
case int64: return encoder.WriteInt64(value)
case uint64: return encoder.WriteUint64(value)
case string: return EncodeAny(encoder, []byte(value), tag)
case []byte:
reflectValue := reflect.ValueOf(value)
switch reflectValue.Kind() {
case reflect.Int: return encoder.WriteInt32(int32(reflectValue.Int()))
case reflect.Uint: return encoder.WriteUint32(uint32(reflectValue.Uint()))
case reflect.Int8: return encoder.WriteInt8(int8(reflectValue.Int()))
case reflect.Uint8: return encoder.WriteUint8(uint8(reflectValue.Uint()))
case reflect.Int16: return encoder.WriteInt16(int16(reflectValue.Int()))
case reflect.Uint16: return encoder.WriteUint16(uint16(reflectValue.Uint()))
case reflect.Int32: return encoder.WriteInt32(int32(reflectValue.Int()))
case reflect.Uint32: return encoder.WriteUint32(uint32(reflectValue.Uint()))
case reflect.Int64: return encoder.WriteInt64(int64(reflectValue.Int()))
case reflect.Uint64: return encoder.WriteUint64(uint64(reflectValue.Uint()))
case reflect.String: return EncodeAny(encoder, []byte(reflectValue.String()), tag)
}
if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) {
if tag.Is(LBA) {
nn, err := encoder.WriteUintN(uint64(len(value)), tag.CN() + 1)
nn, err := encoder.WriteUintN(uint64(reflectValue.Len()), tag.CN() + 1)
n += nn; if err != nil { return n, err }
}
nn, err := encoder.Write(value)
nn, err := encoder.Write(reflectValue.Bytes())
n += nn; if err != nil { return n, err }
return n, nil
}
@@ -97,6 +99,10 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err
if err != nil { return n, err }
case LI:
// LI: <value: IntN>
nn, err := decodeAndSetUint(decoder, destination, tag.CN() + 1)
n += nn; if err != nil { return n, err }
case LSI:
// LSI: <value: IntN>
nn, err := decodeAndSetInt(decoder, destination, tag.CN() + 1)
n += nn; if err != nil { return n, err }
case FP:
@@ -129,7 +135,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err
return n, errWrongDestinationType("slice")
}
if destination.Cap() < int(length) {
destination.Grow(destination.Cap() - int(length))
destination.Grow(int(length) - destination.Cap())
}
destination.SetLen(int(length))
for index := range length {
@@ -154,7 +160,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err
if err != nil { return n, err }
nn, err = decodeAny(decoder, value.Elem(), itemTag)
n += nn; if err != nil { return n, err }
table.SetMapIndex(reflect.ValueOf(key), value)
table.SetMapIndex(reflect.ValueOf(key), value.Elem())
}
default:
return n, fmt.Errorf("unknown TN %d", tag.TN())
@@ -169,13 +175,18 @@ func TagAny(value any) (Tag, error) {
// TODO use reflection for all of this to ignore type names
// 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(7), nil
case string: return bufferLenTag(len(value)), nil
case []byte: return bufferLenTag(len(value)), nil
case int: return LSI.WithCN(3), nil
case int8: return LSI.WithCN(0), nil
case int16: return LSI.WithCN(1), nil
case int32: return LSI.WithCN(3), nil
case int64: return LSI.WithCN(7), nil
case uint: return LI.WithCN(3), nil
case uint8: return LI.WithCN(0), nil
case uint16: return LI.WithCN(1), nil
case uint32: return LI.WithCN(3), nil
case uint64: return LI.WithCN(7), nil
case string: return bufferLenTag(len(value)), nil
case []byte: return bufferLenTag(len(value)), nil
}
// aggregates
@@ -239,7 +250,7 @@ func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) {
}
// setInt expects a settable destination.
func setInt(destination reflect.Value, value uint64) error {
func setInt[T int64 | uint64](destination reflect.Value, value T) error {
switch {
case destination.CanInt():
destination.Set(reflect.ValueOf(int64(value)).Convert(destination.Type()))
@@ -275,6 +286,13 @@ func setByteArray(destination reflect.Value, value []byte) error {
// decodeAndSetInt expects a settable destination.
func decodeAndSetInt(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) {
value, nn, err := decoder.ReadIntN(bytes)
n += nn; if err != nil { return n, err }
return n, setInt(destination, value)
}
// decodeAndSetUint expects a settable destination.
func decodeAndSetUint(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) {
value, nn, err := decoder.ReadUintN(bytes)
n += nn; if err != nil { return n, err }
return n, setInt(destination, value)
@@ -317,6 +335,14 @@ func typeOf(decoder *Decoder, tag Tag) (reflect.Type, error) {
case 7: return reflect.TypeOf(uint64(0)), nil
}
return nil, fmt.Errorf("unknown CN %d for LI", tag.CN())
case LSI:
switch tag.CN() {
case 0: return reflect.TypeOf(int8(0)), nil
case 1: return reflect.TypeOf(int16(0)), nil
case 3: return reflect.TypeOf(int32(0)), nil
case 7: return reflect.TypeOf(int64(0)), nil
}
return nil, fmt.Errorf("unknown CN %d for LSI", tag.CN())
case FP:
switch tag.CN() {
case 3: return reflect.TypeOf(float32(0)), nil
@@ -352,7 +378,6 @@ func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) {
currentTag = elem
offset = n
dimension += 1
fmt.Println(n)
if elem.Is(OTA) {
if !populated {
// default to a large byte array, will be

View File

@@ -9,7 +9,7 @@ import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
func TestEncodeAnyInt(test *testing.T) {
err := testEncodeAny(test, uint8(0xCA), LI.WithCN(0), tu.S(0xCA))
if err != nil { test.Fatal(err) }
err = testEncodeAny(test, 400, LI.WithCN(3), tu.S(
err = testEncodeAny(test, 400, LSI.WithCN(3), tu.S(
0, 0, 0x1, 0x90,
))
if err != nil { test.Fatal(err) }
@@ -22,15 +22,16 @@ func TestEncodeAnyTable(test *testing.T) {
0x0000: "hi!",
0xFFFF: []uint16 { 0xBEE5, 0x7777 },
0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} },
}, KTV.WithCN(0), tu.S(5).AddVar(
0x2345: [][]int16 { []int16 { 0x5 }, []int16 { 0x17, -0xAAA } },
}, KTV.WithCN(0), tu.S(6).AddVar(
[]byte {
0xF3, 0xB9,
byte(LI.WithCN(3)),
byte(LSI.WithCN(3)),
0, 0, 0, 1,
},
[]byte {
0x01, 0x02,
byte(LI.WithCN(3)),
byte(LSI.WithCN(3)),
0, 0, 0, 2,
},
[]byte {
@@ -52,15 +53,26 @@ func TestEncodeAnyTable(test *testing.T) {
0, 0x17,
0xAA, 0xAA,
},
[]byte {
0x23, 0x45,
byte(OTA.WithCN(0)), 2, byte(OTA.WithCN(0)),
1, byte(LSI.WithCN(1)),
0, 0x5,
2, byte(LSI.WithCN(1)),
0, 0x17,
0xF5, 0x56,
},
))
if err != nil { test.Fatal(err) }
}
func TestEncodeDecodeAnyMap(test *testing.T) {
func TestEncodeDecodeAnyTable(test *testing.T) {
err := testEncodeDecodeAny(test, map[uint16] any {
0xF3B9: 1,
0x0102: 2,
0x0000: "hi!",
0xF3B9: uint32(1),
0x0102: uint32(2),
0x0103: int64(23432),
0x0104: int64(-88777),
0x0000: []byte("hi!"),
0xFFFF: []uint16 { 0xBEE5, 0x7777 },
0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} },
}, nil)
@@ -183,8 +195,8 @@ func testEncodeDecodeAny(test *testing.T, value, correctValue any) error {
_, decoded, n, err := decAny(bytes)
if err != nil { return err }
test.Log("got: ", decoded)
test.Log("correct:", correctValue)
test.Log("got: ", tu.Describe(decoded))
test.Log("correct:", tu.Describe(correctValue))
if !reflect.DeepEqual(decoded, correctValue) {
return fmt.Errorf("values not equal")
}

View File

@@ -102,6 +102,11 @@ func (this *Encoder) WriteUintN(value uint64, bytes int) (n int, err error) {
return n, nil
}
// WriteFloat16 encodes a 16-bit floating point value to the output writer.
func (this *Encoder) WriteFloat16(value float32) (n int, err error) {
return this.WriteUint16(f32bitsToF16bits(math.Float32bits(value)))
}
// WriteFloat32 encodes a 32-bit floating point value to the output writer.
func (this *Encoder) WriteFloat32(value float32) (n int, err error) {
return this.WriteUint32(math.Float32bits(value))
@@ -116,3 +121,69 @@ func (this *Encoder) WriteFloat64(value float64) (n int, err error) {
func (this *Encoder) WriteTag(value Tag) (n int, err error) {
return this.WriteUint8(uint8(value))
}
// f32bitsToF16bits returns uint16 (Float16 bits) converted from the specified float32.
// Conversion rounds to nearest integer with ties to even.
// Taken from https://github.com/x448/float16/blob/v0.8.4/float16
//
// MIT License
//
// Copyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
func f32bitsToF16bits(u32 uint32) uint16 {
// Translated from Rust to Go by Montgomery Edwards⁴⁴⁸ (github.com/x448).
// All 4294967296 conversions with this were confirmed to be correct by x448.
// Original Rust implementation is by Kathryn Long (github.com/starkat99) with MIT license.
sign := u32 & 0x80000000
exp := u32 & 0x7f800000
coef := u32 & 0x007fffff
if exp == 0x7f800000 {
// NaN or Infinity
nanBit := uint32(0)
if coef != 0 {
nanBit = uint32(0x0200)
}
return uint16((sign >> 16) | uint32(0x7c00) | nanBit | (coef >> 13))
}
halfSign := sign >> 16
unbiasedExp := int32(exp>>23) - 127
halfExp := unbiasedExp + 15
if halfExp >= 0x1f {
return uint16(halfSign | uint32(0x7c00))
}
if halfExp <= 0 {
if 14-halfExp > 24 {
return uint16(halfSign)
}
coef := coef | uint32(0x00800000)
halfCoef := coef >> uint32(14-halfExp)
roundBit := uint32(1) << uint32(13-halfExp)
if (coef&roundBit) != 0 && (coef&(3*roundBit-1)) != 0 {
halfCoef++
}
return uint16(halfSign | halfCoef)
}
uHalfExp := uint32(halfExp) << 10
halfCoef := coef >> 13
roundBit := uint32(0x00001000)
if (coef&roundBit) != 0 && (coef&(3*roundBit-1)) != 0 {
return uint16((halfSign | uHalfExp | halfCoef) + 1)
}
return uint16(halfSign | uHalfExp | halfCoef)
}

View File

@@ -2,14 +2,17 @@ package tape
import "fmt"
// TODO: fix #7
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
LI Tag = 1 << 5 // Large unsigned integer
LSI Tag = 2 << 5 // Large signed integer
FP Tag = 3 << 5 // Floating point
SBA Tag = 4 << 5 // Small byte array
LBA Tag = 5 << 5 // Large byte array
OTA Tag = 6 << 5 // One-tag array
KTV Tag = 7 << 5 // Key-tag-value table
TNMask Tag = 0xE0 // The entire TN bitfield
CNMask Tag = 0x1F // The entire CN bitfield
CNLimit Tag = 32 // All valid CNs are < CNLimit
@@ -40,6 +43,7 @@ func (tag Tag) String() string {
switch tag.WithoutCN() {
case SI: tn = "SI"
case LI: tn = "LI"
case LSI: tn = "LSI"
case FP: tn = "FP"
case SBA: tn = "SBA"
case LBA: tn = "LBA"
@@ -54,6 +58,11 @@ func BufferTag(value []byte) Tag {
return bufferLenTag(len(value))
}
// StringTag returns the appropriate tag for a string.
func StringTag(value string) Tag {
return bufferLenTag(len(value))
}
func bufferLenTag(length int) Tag {
if length < int(CNLimit) {
return SBA.WithCN(length)