RFC: Common type syntax within Hexcasting documentation
tizu69 opened this issue ยท 6 comments
As many of the hexcasters may have noticed, type syntax in documentation is all over the place. This proposal is to standardize the syntax for types within Hexcasting documentation, for both human and machine readers.
The syntax section of this document contains some collapsed examples. These are represented as JSON. In reality, these would be represented as the Hexcasting equivalent of the JSON, like an array of patterns instead of a string.
This is not a final document. It is an RFC - a request for comments. Tell me what you think, get updating.
Definitions
- type A type is a representation of a type of iota, like number or string.
- stack The stack is the list of iotas present in memory.
- iota An iota is a single value in the stack.
- pattern A "function".
- input The input is the top of the stack before the pattern is run.
- output The output is the top of the stack after the pattern is run.
Usage
Like the old "syntax", the last item in the array is the "uppermost" stack item, that is to say, the iota of the top of the stack.
This proposal proposes a JSON-based tree syntax for types. Depending on the implementation in Hexcasting, this may be another format, like XML, a Builder pattern in Code, or something else with similar structure. HexBug exports and patchouli entries should use inputs and outputs keys to indicate input and output types, both of which are an array.
As such, any type root should be an array. If no input or output types are given, this proposal suggests an empty array, [], although a null may also be feasible. This should not be mixed, so choose globally.
Syntax
The following documentation specifies a single iotan type. The most basic type is an any type. As the name implies, this can represent any iota. It is written as an empty string.
""Tests
Valid
123.45"aadadaaw"[[[[1, 2]]]]An iota type is a type that represents a specific iota. It is written as an identifier, such as hexcasting:number or moreiotas:string. The identifier should be the mod id, followed by a colon, followed by the type name. This spec enforces the name (after the :) to be lowercase letters only. This allows it to be used as a display name in the UI (for example).
"hexcasting:number"Tests
Valid
123.45Invalid
"aadadaaw"A tuple type is a type that represents a tuple of iotas (an array with a fixed length and position of types). It is written as a tuple key, followed by an array of types.
{ "tuple": ["hexcasting:number", "hexcasting:number"] }Tests
Valid
[123.45, 123.45]Invalid
[123.45, 123.45, 123.45][123.45]123.45A union type is a type that represents a union of types (one of a set of iotas). It is written as an oneof key, followed by an array of types.
{ "oneof": ["hexcasting:number", "hexcasting:player"] }Tests
Valid
123.45<Petrak>Invalid
[123.45, 123.45]An array type is a type that represents an array of iotas. It is written as an array key, followed by a type.
{ "array": "hexcasting:number" }Tests
Valid
[123.45, 123.45][123.45, 123.45, 123.45, 123.45, 123.45, 123.45, 123.45, 123.45, 123.45, 123.45]Invalid
123.45["wqw"]An optional type is a type that represents an optional iota. It is written as an optional key, followed by a type. Optional types may be empty if the stack is not long enough.
If not at the top of the stack, this type implies that if the type does not match, the iota is ignored and kept. If at the top of the stack, this type will fail if the type does not match the optional type, or the type before.
This type should not be used - prefer using a { "oneof": [type, "hexcasting:null"] } instead. It exists because mods have already made use of this behaviour.
{ "optional": "hexcasting:number" }Tests
Valid
123.45// This value gets ignored and not pulled from the stack.
[123.45, 123.45]A stack exhaust type is a type that represents an unspecified number of iotas. It is written as a "..." key. Only one is allowed in a single input or output, and the number of other types before/after define how many iotas are stripped from this type, so ["hexcasting:number", "...", "hexcasting:number"]
implies, that the uppermost type is hexcasting:number, the "lowermost" type is also hexcasting:number, and the remaining types in-between are not explicitly type-defined. This type should generally be avoided, as it makes it difficult to automatically infer stack size without testing, but if required, it may be used.
This is a black box - it does not define the length of taken iotas. It should not be used unless you know what you're doing and the description of the pattern should define how many iotas are taken. It exists because mods have already made use of this behaviour, and for stack manipulation, it is somewhat required. [0, 0, 0, 0, 0] may end up as [0, 0] or [0, 0, 0, 0] if not documented properly.
"..."A named type is a new type used for cases where order does not imply meaning. It wraps existing types and is written as an object with a name and a type key. When stringified for documentation purposes, the name should be appended after the type, separated with brackets, like string<url>.
{ "name": "url", "type": "moreiotas:string" }Tests
Valid
"http://example.com""foo"Invalid
123.45Metatypes
Not all types are "real". For example, a hexcasting:vector is not really a real type, but moreso, a tuple of three hexcasting:numbers. These metatypes should be defined in HexBug in a metatypes key. How this works in-game is unspecified, as this is mostly for documentation and machine parsing purposes.
{
"metatypes": {
"hexcasting:vector": {
"tuple": ["hexcasting:number", "hexcasting:number", "hexcasting:number"]
}, // or...
"hexcasting:vector_": {
"tuple": [
{ "name": "X", "type": "hexcasting:number" }
{ "name": "Y", "type": "hexcasting:number" }
{ "name": "Z", "type": "hexcasting:number" }
]
} // this would not get represented in-game, as the book would show "vector", not the tuple.
}
}Limitations
- No generics are supported.
- No recursion is supported.
- Clamped types, like
0-24orint >= 0are not supported. - You tell me!
Examples
These examples are from the registry.json that HexBug provides.
{
"description": "Copy the top two iotas of the stack. [0, 1] becomes [0, 1, 0, 1].",
// "inputs": "any, any",
"inputs": ["", ""],
// "outputs": "any, any, any, any",
"outputs": ["", "", "", ""],
"book_url": "https://hexcasting.hexxy.media/v/0.11.2/1.0/en_us#patterns/stackmanip@hexcasting:2dup",
"mod_id": "hexcasting"
}{
"description": "Takes the intersection of two sets.",
// "inputs": "(num, num)|(list, list)",
"inputs": [{
"oneof": [
{ "tuple": ["hexcasting:number", "hexcasting:number"] },
{ "tuple": [{ "array": "" }, { "array": "" }] }
]
}],
// "outputs": "num|list",
"outputs": [{
"oneof": [
"hexcasting:number",
{ "array": "" }
]
}],
"book_url": "https://hexcasting.hexxy.media/v/0.11.2/1.0/en_us#patterns/sets@hexcasting:and",
"mod_id": "hexcasting"
}{
"description": "Removes a string and a bool or null. If it was true, return the string in upper case. If false, lowercase. If null, toggle each character's case.",
// "inputs": "str, bool | null",
"inputs": ["hexcasting:string", { "oneof": ["hexcasting:boolean", "hexcasting:null"] }],
// "outputs": "str",
"outputs": ["hexcasting:string"],
"book_url": "https://moreiotas.hexxy.media/v/0.1.1/1.0/en_us#patterns/strings@moreiotas:string/case",
"mod_id": "moreiotas"
}Pseudocode turning 956types back into a human-readable string
def _tostr(obj: IotanType) -> str:
if obj == "":
return "any"
if obj == "...":
return "..."
if isinstance(obj, str):
_, name = obj.split(":", 1)
return name
if not isinstance(obj, dict):
raise ValueError("Expected a dictionary")
if "tuple" in obj:
elements = [_tostr(e) for e in obj["tuple"]]
return f"({', '.join(elements)})"
if "oneof" in obj:
options = [_tostr(e) for e in obj["oneof"]]
return " | ".join(options)
if "array" in obj:
element = _tostr(obj["array"])
return f"{element}[]"
if "optional" in obj:
inner = _tostr(obj["optional"])
return f"{inner}?"
raise ValueError("Unknown object structure")
def tostr(input: list[IotanType], output: list[IotanType]) -> str:
in_types = [_tostr(i) for i in input]
out_types = [_tostr(o) for o in output]
in_str = ", ".join(in_types)
out_str = ", ".join(out_types)
return f"{in_str} -> {out_str}".strip()what if we just had a standard instead of something strictly enforced like this? i quite prefer something like vec, [0-24], 0-20 for inputs.
additionally Offering Purification from Hexal has the output type [complicated!] (or, if properly annotated, trades[trade(input[items(item, amount)], output(item, amount))] or [([(item, amount)], (item, amount))])
having it enforced like this would let you do some cool things though, like automatic type checking in the vscode extension
and there could still be a second thing to let you put custom text in the book if it's too big to fit
but then there'd be the risk of the 2 not matching ofc
and there could still be a second thing to let you put custom text in the book if it's too big to fit
but then there'd be the risk of the 2 not matching ofc
Actually one of the reasons why I chose inputs/outputs instead of input/output as key name.
That way you could specify the old style (either for slow migration, or for complex thingies), and gradually adopt (removing the old if not needed) the new syntax as you desire.
Hell, this could even be done (minus the removing part) before official Hex book adoption gets done.
Actually one of the reasons why I chose inputs/outputs instead of input/output as key name.
That way you could specify the old style (either for slow migration, or for complex thingies), and gradually adopt (removing the old if not needed) the new syntax as you desire.
You could also do that by just using the current field names and allowing it to be a string. Then there aren't two fields for the same thing, which would be confusing.
Also, see #580 for some earlier thoughts on this.
Hell, this could even be done (minus the removing part) before official Hex book adoption gets done.
Not really - hexdoc would prevent it.
Actually one of the reasons why I chose inputs/outputs instead of input/output as key name.
That way you could specify the old style (either for slow migration, or for complex thingies), and gradually adopt (removing the old if not needed) the new syntax as you desire.You could also do that by just using the current field names and allowing it to be a string. Then there aren't two fields for the same thing, which would be confusing
I disagree. That would encourage people to simply not use it. Yes, offering both names is weird, I agree, but there should be some middleground:
additionally Offering Purification from Hexal has the output type [complicated!] (or, if properly annotated, trades[trade(input[items(item, amount)], output(item, amount))] or [([(item, amount)], (item, amount))])
Also, see #580 for some earlier thoughts on this.
I did not realize this was already an issue (even if we do share some concepts), thanks.